File size: 8,566 Bytes
d4c5ec7 1cd0943 d4c5ec7 1cd0943 d4c5ec7 1cd0943 d4c5ec7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 | /*
* 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;
})();
|