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