dosmqtt / bridge.js
HelloSun's picture
Real DOS networking: FreeDOS + NE2000 + mTCP MQTT clients on v86, TCP-to-WSS bridge
c722e6d verified
Raw History Blame Contribute Delete
5.76 kB
/*
* 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;
})();