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)
|
||||
- [Environment Variables](#environment-variables-1)
|
||||
- [Health Checks](#health-checks)
|
||||
- [Running over HTTP](#running-over-http)
|
||||
- [App Lifecycle](#app-lifecycle)
|
||||
- [Cron Jobs](#cron-jobs)
|
||||
- [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
|
||||
|
||||
Apps move through these states:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ interface WsData {
|
|||
port: number
|
||||
path: string
|
||||
protocols: string[]
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
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 protocols = protocolHeader ? protocolHeader.split(',').map(p => p.trim()) : []
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (protocolHeader) headers['sec-websocket-protocol'] = protocolHeader
|
||||
// Collect headers to forward to the upstream app
|
||||
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
|
||||
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 = {
|
||||
open(ws: ServerWebSocket<WsData>) {
|
||||
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'
|
||||
upstreams.set(ws, upstream)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user