nasty! it's a browser
This commit is contained in:
parent
f598beb406
commit
ba44f8e65b
|
|
@ -13,6 +13,9 @@
|
|||
<div class="text-content">
|
||||
<h1>@defunkt</h1>
|
||||
<p>This is my website. I am Chris.</p>
|
||||
<p><a href="/other.html">Other.html</a></p>
|
||||
<p><a href="http://corey.localhost:3000">corey</a></p>
|
||||
<p><a href="https://google.com">google</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile burger">
|
||||
|
|
|
|||
4
nose/chris/pub/other.html
Normal file
4
nose/chris/pub/other.html
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<html>
|
||||
<head><title>other</title></head>
|
||||
<body><h1>other</h1></body>
|
||||
</html>
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export default () =>
|
||||
"pong"
|
||||
2
nose/ping/index.tsx
Normal file
2
nose/ping/index.tsx
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export default () =>
|
||||
<html><head><title>pong </title></head><body>pong - {Date.now()}</body></html>
|
||||
96
public/browser-nav.js
Normal file
96
public/browser-nav.js
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// browser-nav.js - Injected into NOSE apps to get the UI browser working.
|
||||
(function () {
|
||||
const ALLOWED_DOMAINS = ['localhost', '.local', '.nose.space']
|
||||
|
||||
function isAllowedOrigin(url) {
|
||||
try {
|
||||
const urlObj = new URL(url, window.location.href)
|
||||
return ALLOWED_DOMAINS.some(domain =>
|
||||
urlObj.hostname === domain || urlObj.hostname.endsWith(domain)
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept navigation attempts
|
||||
function interceptNavigation(e) {
|
||||
const target = e.target.closest('a')
|
||||
if (!target || !target.href) return
|
||||
|
||||
// Allow relative URLs and hash links
|
||||
if (target.getAttribute('href').startsWith('#')) return
|
||||
if (target.getAttribute('href').startsWith('/')) return
|
||||
|
||||
// Check if external
|
||||
if (!isAllowedOrigin(target.href)) {
|
||||
e.preventDefault()
|
||||
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'NAV_BLOCKED',
|
||||
data: { url: target.href, reason: 'External domain not allowed' }
|
||||
}, '*')
|
||||
} else {
|
||||
alert('Navigation to external sites is not allowed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for clicks on links
|
||||
document.addEventListener('click', interceptNavigation, true)
|
||||
|
||||
// Intercept programmatic navigation
|
||||
const originalPushState = history.pushState
|
||||
const originalReplaceState = history.replaceState
|
||||
|
||||
history.pushState = function (state, title, url) {
|
||||
if (url && !isAllowedOrigin(url)) {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'NAV_BLOCKED',
|
||||
data: { url, reason: 'External domain not allowed' }
|
||||
}, '*')
|
||||
}
|
||||
return
|
||||
}
|
||||
return originalPushState.apply(this, arguments)
|
||||
}
|
||||
|
||||
history.replaceState = function (state, title, url) {
|
||||
if (url && !isAllowedOrigin(url)) {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'NAV_BLOCKED',
|
||||
data: { url, reason: 'External domain not allowed' }
|
||||
}, '*')
|
||||
}
|
||||
return
|
||||
}
|
||||
return originalReplaceState.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Listen for navigation commands from parent
|
||||
window.addEventListener('message', (event) => {
|
||||
console.log(event)
|
||||
if (event.data.type === 'NAV_COMMAND') {
|
||||
switch (event.data.action) {
|
||||
case 'back': history.back(); break
|
||||
case 'forward': history.forward(); break
|
||||
case 'reload': window.location.reload(); break
|
||||
case 'stop': window.stop(); break
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'NAV_READY' }, '*')
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
window.parent.postMessage({
|
||||
type: 'URL_CHANGED',
|
||||
data: { url: location.href }
|
||||
}, '*')
|
||||
})
|
||||
}
|
||||
})()
|
||||
54
src/css/browser.css
Normal file
54
src/css/browser.css
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
:root {
|
||||
--browser-bar-height: 34px;
|
||||
}
|
||||
|
||||
iframe.browser {
|
||||
display: block;
|
||||
background-color: white;
|
||||
z-index: 10;
|
||||
border: none;
|
||||
margin-top: var(--browser-bar-height);
|
||||
}
|
||||
|
||||
[data-mode="tall"] iframe.browser {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
iframe:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.browser.active {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#browser-controls {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 15;
|
||||
width: 100%;
|
||||
height: var(--browser-bar-height);
|
||||
background-color: var(--gray);
|
||||
}
|
||||
|
||||
#browser-controls span,
|
||||
#browser-controls a {
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
padding: 5px;
|
||||
background-color: var(--gray);
|
||||
color: var(--c64-light-blue);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#forward-button {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
#close-button {
|
||||
position: absolute;
|
||||
right: -10;
|
||||
}
|
||||
|
|
@ -16,6 +16,8 @@
|
|||
--purple: #7C3AED;
|
||||
--blue: #1565C0;
|
||||
--magenta: #ff66cc;
|
||||
--gray: #BEBEBE;
|
||||
--grey: #BEBEBE;
|
||||
|
||||
--c64-light-blue: #6C6FF6;
|
||||
--c64-dark-blue: #40318D;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const Layout: FC = async ({ children, title }) => (
|
|||
<link href="/css/reset.css" rel="stylesheet" />
|
||||
<link href="/css/main.css" rel="stylesheet" />
|
||||
<link href="/css/game.css" rel="stylesheet" />
|
||||
<link href="/css/browser.css" rel="stylesheet" />
|
||||
|
||||
<script type="importmap" dangerouslySetInnerHTML={{ __html: `{ "imports": { "@/": "/" } }` }} />
|
||||
<script src={process.env.NODE_ENV === "production" ? `/bundle.js?${GIT_SHA}` : "/js/main.js"} type="module" async></script>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ export const Terminal: FC = async () => (
|
|||
<link rel="stylesheet" href="/css/terminal.css" />
|
||||
<link rel="stylesheet" href="/css/editor.css" />
|
||||
|
||||
<div id="browser-controls" style="display:none">
|
||||
<a id="back-button" href="#"></a>
|
||||
<a id="forward-button" href="#"></a>
|
||||
<a id="stop-button" href="#"></a>
|
||||
<a id="reload-button" href="#">@</a>
|
||||
<a id="close-button" href="#">╳</a>
|
||||
<span id="browser-address">chris.nose-pluto.local</span>
|
||||
</div>
|
||||
|
||||
<div id="command-line">
|
||||
<span id="command-prompt">></span>
|
||||
<textarea
|
||||
|
|
|
|||
138
src/js/browser.ts
Normal file
138
src/js/browser.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
////
|
||||
// Our tiny browser, only for NOSE apps.
|
||||
// Check /public/browser-nav.js for the 2nd part.
|
||||
|
||||
import { $, $$ } from "./dom"
|
||||
import { focusInput } from "./focus"
|
||||
|
||||
const controls = $("browser-controls") as HTMLDivElement
|
||||
const address = $("browser-address") as HTMLSpanElement
|
||||
let iframe: HTMLIFrameElement
|
||||
let iframeWindow: Window
|
||||
|
||||
export function isBrowsing(): boolean {
|
||||
return document.querySelector("iframe.browser.active") !== null
|
||||
}
|
||||
|
||||
export function openBrowser(url: string) {
|
||||
iframe = $$("iframe.browser.active") as HTMLIFrameElement
|
||||
iframeWindow = iframe.contentWindow as Window
|
||||
iframe.src = url
|
||||
iframe.sandbox.add("allow-scripts", "allow-same-origin", "allow-forms")
|
||||
iframe.height = "540"
|
||||
iframe.width = "960"
|
||||
iframe.tabIndex = 0
|
||||
|
||||
window.addEventListener("message", handleAppMessage)
|
||||
window.addEventListener("keydown", handleBrowserKeydown)
|
||||
controls.addEventListener("click", handleClick)
|
||||
iframe.addEventListener("load", handlePageLoad)
|
||||
|
||||
controls.style.display = ""
|
||||
const main = document.querySelector("#content")
|
||||
main?.prepend(iframe)
|
||||
|
||||
setAddress(url)
|
||||
}
|
||||
|
||||
function handleAppMessage(event: MessageEvent) {
|
||||
const origin = event.origin
|
||||
if (!origin.includes('localhost') && !origin.match(/\.local$/)) {
|
||||
return
|
||||
}
|
||||
|
||||
const { type, data } = event.data
|
||||
|
||||
switch (type) {
|
||||
case 'NAV_READY':
|
||||
break
|
||||
|
||||
case 'URL_CHANGED':
|
||||
setAddress(data.url)
|
||||
break
|
||||
|
||||
case 'TITLE_CHANGED':
|
||||
// TODO: browser titles
|
||||
console.log('Page title:', data.title)
|
||||
break
|
||||
|
||||
case 'NAV_BLOCKED':
|
||||
showNavigationError(data.url, data.reason)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function closeBrowser() {
|
||||
iframe.remove()
|
||||
controls.style.display = "none"
|
||||
window.removeEventListener("keydown", handleBrowserKeydown)
|
||||
window.removeEventListener("message", handleAppMessage)
|
||||
controls.removeEventListener("click", handleClick)
|
||||
iframe.removeEventListener("load", handlePageLoad)
|
||||
focusInput()
|
||||
}
|
||||
|
||||
function handleBrowserKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape" || (e.ctrlKey && e.key === "c")) {
|
||||
e.preventDefault()
|
||||
closeBrowser()
|
||||
}
|
||||
}
|
||||
|
||||
function handleClick(e: MouseEvent) {
|
||||
const target = e.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
|
||||
switch (target.id) {
|
||||
case "back-button":
|
||||
navigateBack(); break
|
||||
case "forward-button":
|
||||
navigateForward(); break
|
||||
case "stop-button":
|
||||
stopLoading(); break
|
||||
case "reload-button":
|
||||
reloadBrowser(); break
|
||||
case "close-button":
|
||||
closeBrowser(); break
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
function handlePageLoad() {
|
||||
}
|
||||
|
||||
function setAddress(url: string) {
|
||||
address.textContent = url.replace(/https?:\/\//, "")
|
||||
}
|
||||
|
||||
function navigateBack() {
|
||||
sendNavCommand('back')
|
||||
}
|
||||
|
||||
function navigateForward() {
|
||||
sendNavCommand('forward')
|
||||
}
|
||||
|
||||
function reloadBrowser() {
|
||||
sendNavCommand('reload')
|
||||
}
|
||||
|
||||
function stopLoading() {
|
||||
sendNavCommand('stop')
|
||||
}
|
||||
|
||||
function sendNavCommand(action: 'back' | 'forward' | 'reload' | 'stop') {
|
||||
if (!iframe.contentWindow) return
|
||||
|
||||
iframe.contentWindow.postMessage({
|
||||
type: 'NAV_COMMAND',
|
||||
action
|
||||
}, '*')
|
||||
}
|
||||
|
||||
function showNavigationError(url: string, reason: string) {
|
||||
alert(`NAVIGATION BLOCKED\n\n${url}\n\n${reason}`)
|
||||
}
|
||||
|
|
@ -1,6 +1,10 @@
|
|||
////
|
||||
// Links can be #command (will run in the prompt) or real links, which run in
|
||||
// a NOSE browser so you always stay in the UI's safe embrace.
|
||||
|
||||
import { runCommand } from "./shell"
|
||||
import { focusInput } from "./focus"
|
||||
import { status } from "./statusbar"
|
||||
import { openBrowser, isBrowsing } from "./browser"
|
||||
|
||||
export function initHyperlink() {
|
||||
window.addEventListener("click", handleClick)
|
||||
|
|
@ -21,8 +25,8 @@ async function handleClick(e: MouseEvent) {
|
|||
e.preventDefault()
|
||||
await runCommand(href.slice(1))
|
||||
focusInput()
|
||||
} else {
|
||||
} else if (!isBrowsing()) {
|
||||
e.preventDefault()
|
||||
status(href)
|
||||
openBrowser(href)
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,40 @@ app.on("GET", ["/js/:path{.+}", "/shared/:path{.+}"], async c => {
|
|||
// app routes
|
||||
//
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
await next()
|
||||
|
||||
const url = new URL(c.req.url)
|
||||
const domains = url.hostname.split(".")
|
||||
const isSubdomain = domains.length > (url.hostname.endsWith("localhost") ? 1 : 2)
|
||||
|
||||
if (!isSubdomain) return
|
||||
|
||||
const contentType = c.res.headers.get('content-type')
|
||||
if (!contentType?.includes('text/html')) return
|
||||
|
||||
const secFetchDest = c.req.header('Sec-Fetch-Dest')
|
||||
const isIframe = secFetchDest === 'iframe'
|
||||
|
||||
if (!isIframe) return
|
||||
|
||||
const originalBody = await c.res.text()
|
||||
const shimScript = `<script src="/browser-nav.js"></script>`
|
||||
|
||||
let modifiedBody = originalBody
|
||||
if (originalBody.includes('</head>')) {
|
||||
modifiedBody = originalBody.replace('</head>', `${shimScript}\n</head>`)
|
||||
} else if (originalBody.includes('<body>')) {
|
||||
modifiedBody = originalBody.replace('<body>', `<body>\n${shimScript}`)
|
||||
}
|
||||
|
||||
// Return modified response
|
||||
c.res = new Response(modifiedBody, {
|
||||
status: c.res.status,
|
||||
headers: c.res.headers
|
||||
})
|
||||
})
|
||||
|
||||
app.use("*", async (c, next) => {
|
||||
const url = new URL(c.req.url)
|
||||
const localhost = url.hostname.endsWith("localhost")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user