//// // Sneaker is our tunneling service that allows you to share your local NOSE webapps // with the public internet. It requires a sneaker server, usually hosted by us. import nose from "./server" import { clearState, setState, getState } from "app/src/state" const SNEAKER_URL = "nose.space" const SNEAKER_TLS = true // const SNEAKER_URL = "localhost:3100" // const SNEAKER_TLS = false const PREFIX = "sneaker:" type Connection = { subdomain: string ws: any close: boolean // manual close } const conns: Record = {} export function initSneakers() { const state = getState() if (!state) return for (const key in state) { if (key.startsWith(PREFIX)) { const app = key.replace(PREFIX, "") console.log("sharing", app, state[key]) connectSneaker(app, state[key]) } } } export function sneakerUrl(appOrSubdomain: string): string { let conn = conns[appOrSubdomain] if (!conn) { for (const appName of Object.keys(conns)) { if (conns[appName]?.subdomain === appOrSubdomain) { conn = conns[appName] break } } if (!conn) return "none" } let url = "http" + (SNEAKER_TLS ? "s" : "") + "://" url += conn.subdomain + "." + SNEAKER_URL return url } export function sneakers(): string[] { return Object.keys(conns) } export async function disconnectSneakers() { for (const app in conns) { const conn = conns[app]! conn.close = true conn.ws.close() } } export async function disconnectSneaker(app: string): Promise { if (!sneakers().includes(app) || !conns[app]) return false await clearState(`${PREFIX}${app}`) conns[app].close = true conns[app].ws.close() return true } // returns the sneaker subdomain if successful export async function connectSneaker(app: string, subdomain = ""): Promise { if (conns[app]) { return conns[app].subdomain } let url = `ws${SNEAKER_TLS ? "s" : ""}://${SNEAKER_URL}/tunnel?app=${app}` if (subdomain) url += `&subdomain=${subdomain}` const ws = new WebSocket(url) ws.binaryType = 'arraybuffer' let resolve: (v: string) => void let promise = new Promise(res => resolve = res) ws.onclose = e => { if (!conns[app]?.close) setTimeout(() => connectSneaker(app, subdomain), 1000) // simple retry delete conns[app] } ws.onerror = e => console.error("sneaker error", e) ws.onmessage = async event => { const msg = JSON.parse(event.data.toString()) if (msg.subdomain) { conns[app] = { subdomain: msg.subdomain, ws, close: false } await setState(`${PREFIX}${app}`, msg.subdomain) subdomain = msg.subdomain resolve(msg.subdomain) return } try { const req = new Request(`http://${msg.app}.localhost` + msg.path, { method: msg.method, headers: msg.headers, body: msg.body || undefined, }) const res = await nose.fetch(req) const headers: Record = {} res.headers.forEach((v, k) => (headers[k] = v)) const contentType = res.headers.get('content-type') || '' const isBinary = isBinaryContentType(contentType) let body: string | ArrayBuffer let responseData: any if (isBinary) { const arrayBuffer = await res.arrayBuffer() const base64 = Buffer.from(arrayBuffer).toString('base64') responseData = { id: msg.id, status: res.status, headers, body: base64, isBinary: true } } else { body = await res.text() responseData = { id: msg.id, status: res.status, headers, body, isBinary: false } } ws.send(JSON.stringify(responseData)) } catch (err: any) { ws.send(JSON.stringify({ id: msg.id, status: 500, headers: { "content-type": "text/plain" }, body: "error: " + err.message, isBinary: false })) } } return promise } function isBinaryContentType(contentType: string): boolean { const binaryTypes = [ 'image/', 'audio/', 'video/', 'application/octet-stream', 'application/pdf', 'application/zip', 'font/' ] return binaryTypes.some(type => contentType.startsWith(type)) }