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