Download bridge.js from HelloSun/dosllm: direct link, hf CLI and curl.
- Browser
- Download file 8.57 kB
-
https://huggingface.co/spaces/HelloSun/dosllm/resolve/main/bridge.js
- Command line
-
hf download hf://spaces/HelloSun/dosllm/bridge.js
-
curl -L -o bridge.js https://huggingface.co/spaces/HelloSun/dosllm/resolve/main/bridge.js
8.57 kB
| /* | |
| * HTTP -> HTTPS bridge for a v86 DOS guest, restricted to Gradio Spaces. | |
| * | |
| * DOS (mTCP) can only speak plain TCP: no TLS. v86's "wisp" network backend terminates the guest's TCP | |
| * streams; this file plays the WISP *server* inside the page. For every TCP stream the guest opens to | |
| * port 80 it collects one HTTP request, performs the same request with fetch() over HTTPS, and writes the | |
| * HTTP response back to the guest. Hugging Face Spaces answer CORS requests, so this works from a static page. | |
| * | |
| * Chinese: XiaoMao Chinese shows Big5 on the DOS screen, Gradio speaks UTF-8/JSON. When the DOS request body | |
| * contains Big5 bytes the bridge decodes them (TextDecoder('big5')) before the HTTPS request, and the JSON reply | |
| * (\uXXXX escapes) is converted to Big5 bytes (see big5.js) before it is handed to DOS. | |
| * | |
| * Safety: only hosts matching *.hf.space and only paths starting with /gradio_api/ are relayed (GET/POST, | |
| * request body <= 8 KB). It is not an open proxy. No credentials are ever added. | |
| * | |
| * Install with GradioBridge.install(); then give v86 `relay_url: GradioBridge.relayUrl('dos')`. | |
| */ | |
| (function () { | |
| 'use strict'; | |
| const RealWebSocket = window.WebSocket; | |
| const FAKE_HOST = 'dosllm-bridge.invalid'; // fake WISP endpoint: never resolved, only recognised by this file | |
| const HOST_RE = /^[a-z0-9][a-z0-9-]*\.hf\.space$/i; | |
| const MAX_BODY = 8192; | |
| const TIMEOUT_MS = 150000; | |
| const WINDOW = 128; | |
| const bridge = { onEvent: null, requests: 0 }; | |
| const emit = (machine, text) => { if (typeof bridge.onEvent === 'function') bridge.onEvent(machine, text); }; | |
| const u32 = (n) => [n & 255, (n >> 8) & 255, (n >> 16) & 255, (n >>> 24) & 255]; | |
| const frame = (type, id, payload) => { | |
| const body = payload || []; | |
| const out = new Uint8Array(5 + body.length); | |
| out[0] = type; out.set(u32(id), 1); out.set(body, 5); | |
| return out; | |
| }; | |
| const concat = (parts) => { | |
| const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); | |
| let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } | |
| return out; | |
| }; | |
| const indexOfCrlf2 = (b) => { for (let i = 0; i + 3 < b.length; i++) if (b[i] === 13 && b[i + 1] === 10 && b[i + 2] === 13 && b[i + 3] === 10) return i; return -1; }; | |
| class FakeWispServer { | |
| constructor(url) { | |
| this.machine = (/[?&]m=([^&]+)/.exec(url) || [])[1] || 'dos'; | |
| this.binaryType = 'arraybuffer'; | |
| this.readyState = 0; | |
| this.streams = new Map(); | |
| this.onopen = this.onmessage = this.onclose = this.onerror = null; | |
| setTimeout(() => { | |
| this.readyState = 1; | |
| if (this.onopen) this.onopen({ type: 'open' }); | |
| this.reply(frame(3, 0, u32(WINDOW))); | |
| }, 0); | |
| } | |
| reply(bytes) { | |
| if (this.readyState !== 1 || !this.onmessage) return; | |
| this.onmessage({ data: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) }); | |
| } | |
| send(data) { | |
| const f = new Uint8Array(data.buffer ? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) : data); | |
| const type = f[0], id = new DataView(f.buffer).getUint32(1, true); | |
| if (type === 1) this.connect(id, f); | |
| else if (type === 2) this.data(id, f.subarray(5)); | |
| else if (type === 4) this.streams.delete(id); | |
| } | |
| connect(id, f) { | |
| const port = f[6] | (f[7] << 8); | |
| if (port !== 80) { | |
| emit(this.machine, `TCP port ${port} refused (this bridge only relays plain HTTP on port 80)`); | |
| this.reply(frame(4, id, [0x44])); | |
| return; | |
| } | |
| this.streams.set(id, { chunks: [], handled: false }); | |
| } | |
| data(id, bytes) { | |
| const s = this.streams.get(id); | |
| if (!s || s.handled) return; | |
| s.chunks.push(bytes.slice()); | |
| this.reply(frame(3, id, u32(WINDOW))); | |
| const all = concat(s.chunks); | |
| const end = indexOfCrlf2(all); | |
| if (end < 0) return; | |
| const head = new TextDecoder().decode(all.subarray(0, end)); | |
| const len = Math.min(parseInt((/content-length:\s*(\d+)/i.exec(head) || [])[1] || '0', 10), MAX_BODY + 1); | |
| if (all.length < end + 4 + len) return; // body not complete yet | |
| s.handled = true; | |
| this.relay(id, head, all.subarray(end + 4, end + 4 + len)); | |
| } | |
| async respond(id, status, text, type, bodyBytes) { | |
| const body = bodyBytes || new TextEncoder().encode(text); | |
| const head = new TextEncoder().encode(`HTTP/1.0 ${status}\r\nContent-Type: ${type || 'text/plain'}\r\nContent-Length: ${body.length}\r\nConnection: close\r\n\r\n`); | |
| const all = concat([head, body]); | |
| for (let o = 0; o < all.length; o += 1024) this.reply(frame(2, id, all.subarray(o, o + 1024))); | |
| this.reply(frame(4, id, [0x02])); // close: guest sees FIN after the last data | |
| this.streams.delete(id); | |
| } | |
| async relay(id, head, body) { | |
| const m = /^(GET|POST) (\S+) HTTP\/1\.[01]/.exec(head); | |
| const host = ((/^host:\s*(\S+)/im.exec(head) || [])[1] || '').toLowerCase(); | |
| if (!m || !HOST_RE.test(host) || !m[2].startsWith('/gradio_api/') || body.length > MAX_BODY) { | |
| emit(this.machine, `refused ${m ? m[1] + ' ' + host + m[2] : 'malformed request'} (only *.hf.space /gradio_api/ is relayed)`); | |
| return this.respond(id, '403 Forbidden', 'blocked by bridge'); | |
| } | |
| const url = `https://${host}${m[2]}`; | |
| bridge.requests += 1; | |
| const t0 = performance.now(); | |
| emit(this.machine, `DOS -> ${m[1]} ${url}${m[1] === 'POST' ? ` (${body.length} bytes)` : ''}`); | |
| const ctl = new AbortController(); | |
| const timer = setTimeout(() => ctl.abort(), TIMEOUT_MS); | |
| try { | |
| const init = { method: m[1], signal: ctl.signal, headers: {} }; | |
| if (m[1] === 'POST') { | |
| const big5 = window.Big5 && window.Big5.available && body.some((b) => b >= 0x80); | |
| init.body = big5 ? window.Big5.decode(body) : new TextDecoder().decode(body); | |
| if (big5) emit(this.machine, `request body has Big5 bytes: decoded to UTF-8 ("${init.body.replace(/^.*?\[\"/, '').slice(0, 60)}...")`); | |
| init.headers['Content-Type'] = 'application/json'; | |
| } | |
| const resp = await fetch(url, init); | |
| const bytes = new Uint8Array(await resp.arrayBuffer()); | |
| const secs = ((performance.now() - t0) / 1000).toFixed(1); | |
| emit(this.machine, `HTTPS ${resp.status} ${resp.headers.get('content-type') || ''} ${bytes.length} bytes in ${secs}s`); | |
| if (/text\/event-stream/.test(resp.headers.get('content-type') || '')) { | |
| const text = new TextDecoder().decode(bytes); | |
| const frames = (text.match(/^event: generating/gm) || []).length; | |
| const done = /event: complete\ndata: (.*)/.exec(text); | |
| let answer = ''; | |
| try { answer = JSON.parse(done[1])[0][0]; } catch (e) { /* not a chat reply */ } | |
| emit(this.machine, `Gradio stream: ${frames} generating frames, ${done ? 'complete' : 'NO complete event'}`); | |
| if (answer) emit(this.machine, `Gradio answer: ${answer}`); | |
| bridge.lastAnswer = answer; | |
| } | |
| let out = bytes; | |
| const ct = resp.headers.get('content-type') || ''; | |
| if (window.Big5 && window.Big5.available && /json|event-stream/.test(ct)) { | |
| out = window.Big5.jsonToBig5Bytes(new TextDecoder().decode(bytes)); // UTF-8/JSON -> Big5 bytes for the DOS screen | |
| if (out.some((b) => b >= 0x80)) emit(this.machine, `reply contains Chinese: converted to Big5 for DOS (${bytes.length} -> ${out.length} bytes)`); | |
| } | |
| await this.respond(id, `${resp.status} ${resp.statusText || 'OK'}`, '', ct || 'text/plain', out); | |
| } catch (e) { | |
| emit(this.machine, `HTTPS request failed: ${e && e.message ? e.message : e}`); | |
| await this.respond(id, '502 Bad Gateway', 'bridge: upstream request failed'); | |
| } finally { clearTimeout(timer); } | |
| } | |
| close() { this.readyState = 3; this.streams.clear(); if (this.onclose) this.onclose({ type: 'close' }); } | |
| } | |
| bridge.install = function (opts) { | |
| Object.assign(bridge, opts || {}); | |
| if (window.WebSocket.__gradioBridge) return; | |
| const patched = new Proxy(RealWebSocket, { | |
| construct(target, args) { | |
| if (String(args[0]).indexOf(FAKE_HOST) >= 0) return new FakeWispServer(String(args[0])); | |
| return Reflect.construct(target, args); | |
| }, | |
| }); | |
| patched.__gradioBridge = true; | |
| window.WebSocket = patched; | |
| }; | |
| bridge.relayUrl = (machine) => `wisp://${FAKE_HOST}/?m=${encodeURIComponent(machine || 'dos')}`; | |
| window.GradioBridge = bridge; | |
| })(); | |