nasty! it's a browser

This commit is contained in:
Chris Wanstrath 2025-10-02 21:44:55 -07:00
parent f598beb406
commit ba44f8e65b
12 changed files with 350 additions and 5 deletions

View File

@ -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">

View File

@ -0,0 +1,4 @@
<html>
<head><title>other</title></head>
<body><h1>other</h1></body>
</html>

View File

@ -1,2 +0,0 @@
export default () =>
"pong"

2
nose/ping/index.tsx Normal file
View 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
View 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
View 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;
}

View File

@ -16,6 +16,8 @@
--purple: #7C3AED;
--blue: #1565C0;
--magenta: #ff66cc;
--gray: #BEBEBE;
--grey: #BEBEBE;
--c64-light-blue: #6C6FF6;
--c64-dark-blue: #40318D;

View File

@ -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>

View File

@ -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">&gt;</span>
<textarea

138
src/js/browser.ts Normal file
View 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}`)
}

View File

@ -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)
}
}

View File

@ -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")