forked from defunkt/toes
Add HTTP guide and WebSocket header forwarding
This commit is contained in:
parent
75b40b7ed1
commit
d53e8c8cf1
|
|
@ -26,6 +26,7 @@ Toes is a personal web appliance that runs multiple web apps on your home networ
|
||||||
- [Sharing](#sharing)
|
- [Sharing](#sharing)
|
||||||
- [Environment Variables](#environment-variables-1)
|
- [Environment Variables](#environment-variables-1)
|
||||||
- [Health Checks](#health-checks)
|
- [Health Checks](#health-checks)
|
||||||
|
- [Running over HTTP](#running-over-http)
|
||||||
- [App Lifecycle](#app-lifecycle)
|
- [App Lifecycle](#app-lifecycle)
|
||||||
- [Cron Jobs](#cron-jobs)
|
- [Cron Jobs](#cron-jobs)
|
||||||
- [Data Persistence](#data-persistence)
|
- [Data Persistence](#data-persistence)
|
||||||
|
|
@ -747,6 +748,92 @@ app.get('/ok', c => c.text('ok'))
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Running over HTTP
|
||||||
|
|
||||||
|
Toes serves apps over plain HTTP (`http://<app>.toes.local`), not HTTPS. This is fine for a home network appliance, but a few browser features assume HTTPS and will silently break if you're not aware of them.
|
||||||
|
|
||||||
|
> **Note:** `localhost` gets a special pass — browsers treat it as a secure context even over HTTP. But `.local` domains don't get that exemption, so these gotchas apply when accessing your apps at `<app>.toes.local` from another device.
|
||||||
|
|
||||||
|
### Cookies
|
||||||
|
|
||||||
|
If you set cookies with the `Secure` flag, browsers will silently ignore them — the cookie just won't be stored.
|
||||||
|
|
||||||
|
Don't do this:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
c.header('Set-Cookie', 'session=abc123; HttpOnly; Secure; SameSite=Lax')
|
||||||
|
```
|
||||||
|
|
||||||
|
Do this instead:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
c.header('Set-Cookie', 'session=abc123; HttpOnly; SameSite=Lax')
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're using a cookie library, make sure `secure` is set to `false` (or omitted):
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { setCookie } from 'hono/cookie'
|
||||||
|
|
||||||
|
setCookie(c, 'session', token, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'Lax',
|
||||||
|
secure: false, // toes apps run over HTTP
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clipboard API
|
||||||
|
|
||||||
|
`navigator.clipboard.writeText()` and `navigator.clipboard.readText()` require a secure context. They'll throw on `.local` domains.
|
||||||
|
|
||||||
|
Use the legacy fallback instead:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function copyToClipboard(text: string) {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = text
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Workers
|
||||||
|
|
||||||
|
Service workers only register on HTTPS origins (plus `localhost`). If you're building a PWA or want offline caching, it won't work on `.local`. This is a hard browser restriction with no workaround.
|
||||||
|
|
||||||
|
### Web Push Notifications
|
||||||
|
|
||||||
|
The Push API and `Notification.requestPermission()` require a secure context. For notifications on the local network, consider polling or SSE instead:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
app.sse('/notifications', (send, c) => {
|
||||||
|
// push updates over SSE instead of Web Push
|
||||||
|
send({ title: 'New item', body: 'Something happened' })
|
||||||
|
return () => {}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Geolocation & Camera/Mic
|
||||||
|
|
||||||
|
`navigator.geolocation` and `navigator.mediaDevices.getUserMedia()` require a secure context. These won't work on `.local` domains.
|
||||||
|
|
||||||
|
### Web Crypto
|
||||||
|
|
||||||
|
`crypto.subtle` (for hashing, encryption, key generation) requires a secure context. Use a library like `tweetnacl` if you need crypto in the browser, or do it server-side:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// Server-side — works fine, no secure context needed
|
||||||
|
const hash = new Bun.CryptoHasher('sha256').update(data).digest('hex')
|
||||||
|
```
|
||||||
|
|
||||||
|
### What about `toes share`?
|
||||||
|
|
||||||
|
`toes share` tunnels your app through HTTPS, so all of the above works when accessed through the tunnel URL. But since your app should also work locally, don't rely on secure-context APIs unless you're okay with them only working when shared.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## App Lifecycle
|
## App Lifecycle
|
||||||
|
|
||||||
Apps move through these states:
|
Apps move through these states:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ interface WsData {
|
||||||
port: number
|
port: number
|
||||||
path: string
|
path: string
|
||||||
protocols: string[]
|
protocols: string[]
|
||||||
|
headers: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractSubdomain(host: string): string | null {
|
export function extractSubdomain(host: string): string | null {
|
||||||
|
|
@ -98,10 +99,20 @@ export function proxyWebSocket(subdomain: string, req: Request, server: Server<W
|
||||||
const protocolHeader = req.headers.get('sec-websocket-protocol')
|
const protocolHeader = req.headers.get('sec-websocket-protocol')
|
||||||
const protocols = protocolHeader ? protocolHeader.split(',').map(p => p.trim()) : []
|
const protocols = protocolHeader ? protocolHeader.split(',').map(p => p.trim()) : []
|
||||||
|
|
||||||
const headers: Record<string, string> = {}
|
// Collect headers to forward to the upstream app
|
||||||
if (protocolHeader) headers['sec-websocket-protocol'] = protocolHeader
|
const forwardHeaders: Record<string, string> = {}
|
||||||
|
for (const name of ['cookie', 'authorization', 'x-app-url']) {
|
||||||
|
const value = req.headers.get(name)
|
||||||
|
if (value) forwardHeaders[name] = value
|
||||||
|
}
|
||||||
|
if (!forwardHeaders['x-app-url']) {
|
||||||
|
forwardHeaders['x-app-url'] = app.tunnelUrl ?? `${url.protocol}//${subdomain}.${url.hostname}`
|
||||||
|
}
|
||||||
|
|
||||||
const ok = server.upgrade(req, { data: { port: app.port, path, protocols } as WsData, headers })
|
const upgradeHeaders: Record<string, string> = {}
|
||||||
|
if (protocolHeader) upgradeHeaders['sec-websocket-protocol'] = protocolHeader
|
||||||
|
|
||||||
|
const ok = server.upgrade(req, { data: { port: app.port, path, protocols, headers: forwardHeaders } as WsData, headers: upgradeHeaders })
|
||||||
if (ok) return undefined
|
if (ok) return undefined
|
||||||
return new Response('WebSocket upgrade failed', { status: 500 })
|
return new Response('WebSocket upgrade failed', { status: 500 })
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +120,10 @@ export function proxyWebSocket(subdomain: string, req: Request, server: Server<W
|
||||||
export const websocket = {
|
export const websocket = {
|
||||||
open(ws: ServerWebSocket<WsData>) {
|
open(ws: ServerWebSocket<WsData>) {
|
||||||
const { port, path } = ws.data
|
const { port, path } = ws.data
|
||||||
const upstream = new WebSocket(`ws://localhost:${port}${path}`, ws.data.protocols)
|
const upstream = new WebSocket(`ws://localhost:${port}${path}`, {
|
||||||
|
headers: { ...ws.data.headers, host: `localhost:${port}` },
|
||||||
|
protocols: ws.data.protocols,
|
||||||
|
})
|
||||||
|
|
||||||
upstream.binaryType = 'arraybuffer'
|
upstream.binaryType = 'arraybuffer'
|
||||||
upstreams.set(ws, upstream)
|
upstreams.set(ws, upstream)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user