File size: 5,763 Bytes
c722e6d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/*
 * In-page TCP -> WebSocket bridge for v86 guests.
 *
 * A DOS machine talks real TCP/IP through its emulated NE2000 card. v86's "wisp"
 * network backend terminates the guest's TCP streams and forwards the bytes as
 * WISP frames over a WebSocket. This file plays the WISP *server* inside the page
 * (no backend needed): every TCP stream the guest opens to port 1883 is relayed,
 * byte for byte, to a public MQTT-over-WebSocket broker. MQTT is identical on both
 * transports, so the DOS program speaks genuine MQTT 3.1.1 over its own TCP stack.
 *
 * Install with MqttBridge.install(); then give v86 `relay_url: "wisp://mqttbridge.invalid/"`.
 */
(function () {
  'use strict';
  const RealWebSocket = window.WebSocket;
  const FAKE_HOST = 'mqttbridge.invalid';
  const WINDOW = 128; // WISP flow-control credit handed to the guest per stream

  const bridge = {
    brokerUrl: 'wss://broker.emqx.io:8084/mqtt',
    onEvent: null,
    verbose: false,
    stats: { streams: 0, toBroker: 0, toGuest: 0 },
  };

  function 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];

  function frame(type, streamId, payload) {
    const body = payload || [];
    const out = new Uint8Array(5 + body.length);
    out[0] = type;
    out.set(u32(streamId), 1);
    out.set(body, 5);
    return out;
  }

  class FakeWispServer {
    constructor(url) {
      this.url = url;
      this.machine = (/[?&]m=([^&]+)/.exec(url) || [])[1] || 'dos';
      this.binaryType = 'arraybuffer';
      this.readyState = 0; // CONNECTING
      this.streams = new Map();
      this.onopen = this.onmessage = this.onclose = this.onerror = null;
      setTimeout(() => {
        this.readyState = 1; // OPEN
        if (this.onopen) this.onopen({ type: 'open' });
        this.reply(frame(3, 0, u32(WINDOW))); // CONTINUE for stream 0: initial credit
      }, 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];
      const 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.closeStream(id, false);
    }

    connect(id, f) {
      const port = f[6] | (f[7] << 8);
      const host = new TextDecoder().decode(f.subarray(8));
      if (port !== 1883) {
        emit(this.machine, `TCP ${host}:${port} refused (bridge only relays MQTT port 1883)`);
        this.reply(frame(4, id, [0x44])); // CLOSE: connection refused
        return;
      }
      bridge.stats.streams += 1;
      emit(this.machine, `DOS opened TCP ${host}:${port} -> relaying to ${bridge.brokerUrl}`);
      const s = { ws: new RealWebSocket(bridge.brokerUrl, 'mqtt'), queue: [], open: false, done: false };
      s.ws.binaryType = 'arraybuffer';
      s.ws.onopen = () => {
        s.open = true;
        emit(this.machine, 'WSS to broker is open');
        s.queue.forEach((chunk) => s.ws.send(chunk));
        s.queue = [];
      };
      s.ws.onmessage = (ev) => {
        const bytes = new Uint8Array(ev.data);
        bridge.stats.toGuest += bytes.length;
        if (bridge.verbose) emit(this.machine, `broker -> DOS ${bytes.length} bytes (${Array.from(bytes.subarray(0, 4)).map((b) => b.toString(16)).join(' ')})`);
        // The guest TCP stack segments large writes itself; keep frames modest anyway.
        for (let o = 0; o < bytes.length; o += 1024) this.reply(frame(2, id, bytes.subarray(o, o + 1024)));
      };
      const finish = (why) => {
        if (s.done) return;
        s.done = true;
        emit(this.machine, `broker connection closed (${why})`);
        if (this.streams.has(id)) { this.streams.delete(id); this.reply(frame(4, id, [0x02])); }
      };
      s.ws.onclose = () => finish('close');
      s.ws.onerror = () => finish('error');
      this.streams.set(id, s);
    }

    data(id, bytes) {
      const s = this.streams.get(id);
      if (!s) return;
      const chunk = bytes.slice();
      bridge.stats.toBroker += chunk.length;
      if (bridge.verbose) emit(this.machine, `DOS -> broker ${chunk.length} bytes (${Array.from(chunk.subarray(0, 4)).map((b) => b.toString(16)).join(' ')})${s.open ? '' : ' [queued until WSS opens]'}`);
      if (s.open) s.ws.send(chunk); else s.queue.push(chunk);
      this.reply(frame(3, id, u32(WINDOW))); // replenish the guest's send credit
    }

    closeStream(id) {
      const s = this.streams.get(id);
      if (!s) return;
      this.streams.delete(id);
      s.done = true;
      try { s.ws.close(); } catch (e) { /* already closed */ }
      emit(this.machine, 'DOS closed its TCP connection');
    }

    close() {
      this.readyState = 3;
      for (const id of Array.from(this.streams.keys())) this.closeStream(id);
      if (this.onclose) this.onclose({ type: 'close' });
    }
  }

  bridge.install = function (opts) {
    Object.assign(bridge, opts || {});
    if (window.WebSocket.__mqttBridge) 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.__mqttBridge = true;
    window.WebSocket = patched;
  };

  bridge.relayUrl = (machine) => `wisp://${FAKE_HOST}/?m=${encodeURIComponent(machine)}`;
  window.MqttBridge = bridge;
})();