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;
})();