var dgram = require('node:dgram'); var net = require('node:net'); var fs = require('node:fs'); var { pipeline, Transform } = require('node:stream'); var child_process = require('node:child_process'); function spawner(cmd) { return async function() { console.log('$ '+cmd); let proc = child_process.spawn('bash', ['-c', cmd], { stdio: 'inherit' }); await new Promise((ok, err) => { proc.on('error', err); proc.on('exit', (code, signal) => {if (code != 0) err(new Error('exited with error '+code+', '+signal)); else ok();}); }); } } async function minetest_preread(conn) { let msg = await conn.recv(); if (Buffer.from([0x4f,0x45,0x74,0x03,0x00,0x00,0x00,0x01]).compare(msg)) { let run = true; (async function() { await new Promise((ok, err)=>setTimeout(ok, 7000)); if (!run) return; let reason = 'Server is taking a bit long to start. Please press "reconnect" immediately.'; reason = [...(new TextEncoder()).encode(reason)]; Promise.resolve(conn.service.powerOn()) .then(() => new Promise((ok) => setTimeout(ok,60000*0.5))) // +0.5 min .then(() => conn.service.powerOff()); conn.send(Buffer.from([ 0x4f,0x45,0x74,0x03, // proto 0,1, // peer 0, // chan 1, // type: original 0x00, 0x0A, // cmd: Access denied 11, // reason: shutdown (reason.length >> 8) & 0xFF, (reason.length >> 0) & 0xFF, ...reason, 1 // reconnect ])); })(); conn.cancel = function() { run = false; } return true; } conn.send(Buffer.from([...Array(14)].map(e=>0))); await conn.recv(); return false; } function minetestPort(opt) { return { type: 'udp', timeout: 5000, preread: minetest_preread, ...opt, } } function minetestDockerService(opt) { opt = {...opt}; let container = opt.container; delete opt.container; return { ports: [ minetestPort(opt) ], powerOn: spawner(`if ! docker ps --format '{{.Names}}' | grep '^${container}$'; then ct=$(date +%s); path=/tmp/pid-$(head -c 8 /dev/urandom | xxd -p); docker start ${container} && (sh -c 'echo $PPID' > $path && docker logs --follow --since $ct ${container} 2>&1 { [ok, err] = [a, b]; socket.on('error', err); socket.on(ev, ok); }); socket.off('error', err); socket.off(ev, ok); } // timeout but u can bump it so it fires later var Bumpout = function(fun, time) { this._callback = (...args) => { this._handle = null; return fun(...args); } this._timeout = time; this._handle = setTimeout(this._callback, this._timeout) } Bumpout.prototype.bump = function() { if (!this._handle) throw new Error('Gone'); clearTimeout(this._handle); this._handle = setTimeout(this._callback, this._timeout); } Bumpout.prototype.cancel = function() { if (!this._handle) throw new Error('Gone'); clearTimeout(this._handle); this._handle = null; } Bumpout.prototype.restart = function() { if (this._handle) throw new Error('Non-Gone'); this._handle = setTimeout(this._callback, this._timeout); } async function bindUdp(...arg) { var server; await (async fun => { // this is stupid but im stupid too so its ok try { server = dgram.createSocket('udp6'); await fun(); } catch { server = dgram.createSocket('udp4'); await fun(); } })(async () => { server.bind(...arg); await sockWaitFor(server, 'listening'); }); return server; } async function connectUdp(...arg) { var client; await (async fun => { // see ${_FILE}:${_LINE - 18} try { client = dgram.createSocket('udp6'); await fun(); } catch { client = dgram.createSocket('udp4'); await fun(); } })(async () => { client.bind(0); await sockWaitFor(client, 'listening'); client.connect(...arg); await sockWaitFor(client, 'connect'); }); return client } async function bindTcp(...arg) { var server = net.createServer(); server.listen(...arg); await sockWaitFor(server, 'listening'); return server; } async function connectTcp(...arg) { var client = new net.Socket(); client.connect(...arg); await sockWaitFor(client, 'connect'); return client; } function connTemplate(conns, key, sendCl, timeout, filter) { let conn = {}; return Object.assign(conn, { queue: [], permaque: [], ...(fun => ({ promReset: fun, ...fun() }))(() => ({ cond() {}, decond(err) { this.prom = Promise.reject(err); this.prom.catch(() => {}); }, prom: null, })), ...(conns ? { onkill() { this.decond(new Error("timeout")); this.killed = true; conns.delete(key); }, bumpout: new Bumpout(() => { conn.onkill(); }, timeout), } : {}), async recv() { while (!this.queue.length) { if (this.prom) await this.prom; else { this.prom = new Promise((ok, err) => { this.cond = () => { Object.assign(this, this.promReset()); ok(); } this.decond = err; }); this.prom.catch(() => {}); } } return this.queue.shift(); }, sendCl, async send(msg) { this.queue.push(msg); Promise.resolve(filter(msg, 'client->server')) .then(msg => this.permaque.push(msg)); if (this.bumpout) this.bumpout.bump(); this.cond(); } }); } async function proxyUdp(address, upstreamAddr, service, timeout, preread, filter) { var server = await bindUdp(...address); var conns = new Map(); server.on('message', async (msg, rinfo) => { let key = JSON.stringify([rinfo.address, rinfo.port]); if (!conns.has(key)) { conns.set(key, connTemplate(conns, key, function(msg) { server.send(msg, rinfo.port, rinfo.address); }, timeout, filter)); let conn = conns.get(key); (async () => { let filc; let pass = await preread(filc = { service, async recv(len) { return conn.recv(); }, async send(msg) { return conn.sendCl(msg); } }); if (!pass) { conns.delete(key); return; } try { await service.powerOn(); } finally { if(filc.cancel) { try { filc.cancel() } catch(e) { console.error(e); } } } let upstream; try { upstream = await connectUdp(...upstreamAddr); } catch(e) { console.error(e); conn.killed = true; } if (conn.killed) { upstream.close(); await service.powerOff(); return; } upstream.on('message', (msg) => { conn.bumpout.bump(); Promise.resolve(filter(msg, 'server->client')) .then(msg => conn.sendCl(msg)); }); conn.permaque.forEach(msg => upstream.send(msg)); for (let k of ['queue','permaque','cond','decond','recv']) { delete conn[k]; } Object.assign(conn, { upstream, onkill() { this.upstream.close(); conns.delete(key); service.powerOff().catch(e => console.error(e)); }, send(msg) { this.bumpout.bump(); Promise.resolve(filter(msg, 'client->server')) .then(msg => this.upstream.send(msg)); } }); })().catch(e => console.error(e)); } let conn = conns.get(key); await conn.send(msg); }); } async function proxyTcp(address, upstreamAddr, service, timeout, preread, filter) { var server = await bindTcp(...address); server.on('connection', async (client) => { client.setTimeout(timeout); client.on('timeout', () => { client.destroy(); }); let conn = connTemplate(null, null, (msg) => client.write(msg), filter); let msgh, clsh; client.on('data', msgh = (msg) => { filter(msg, 'client->server') .then(msg => conn.send(msg)); }); client.on('close', clsh = () => { conn.decond(new Error("closed")); }) let pass = await filter({ service, async recv(len) { return conn.recv(); }, async send(msg) { return conn.sendCl(msg); } }); if (!pass) { client.close(); return; } try { await service.powerOn(); } finally { if(filc.cancel) { try { filc.cancel() } catch(e) { console.error(e); } } } let upstream; try { upstream = await connectTcp(...upstreamAddr); conn.permaque.forEach(msg => upstream.write(msg)); upstream.setTimeout(timeout); upstream.on('timeout', () => { upstream.destroy(); }); } catch(e) { await service.powerOff(); return; } function trans(gender) { return new Transform({ transform(msg, enc, next) { filter(msg, gender) .then(msg => next(null, msg)) .catch(err => next(err)); } }) } try { await Promise.all( pipeline(client, trans('client->server'), upstream), pipeline(upstream, trans('server->client'), client) ); } finally { await service.powerOff(); } }); } function MultiplexedService(service) { this._service = service; this._count = 0; this._prom = null; } Object.assign(MultiplexedService.prototype, { async powerOn() { if (this._prom) await this._prom; if ((this._count++) == 0) { try { await (this._prom = this._service.powerOn()); } finally { this._prom = null; } } }, async powerOff() { if (this._prom) await this._prom; if (this._count == 0) { throw new Error("Too many powerOffs"); } if ((--this._count) == 0) { try { await (this._prom = this._service.powerOff()); } finally { this._prom = null; } } }, }); function StagedService(service) { this._service = service; this._stack = []; this._prom = null; this._timeout = null; } Object.assign(StagedService.prototype, { async powerOn() { if (this._prom) await this._prom; await (this._prom = (async () => { if (this._timeout) { clearTimeout(this._timeout); } while (this._stack.length > 0) { await this._stack.pop()(); } })()); this._prom = null; }, setOffPower() { let level = this._service.stages[this._stack.length] if(this._timeout || !level) return; this._timeout = setTimeout(async () => { await (this._prom = level.powerOff()); this._prom = null; this._stack.push(() => level.powerOn()); this._timeout = null; setOffPower(); }, level.timeout); }, async powerOff() { if (this._prom) await this._prom; setOffPower(); }, }); function errHandler(err) { console.error('ERROR!', err); } process.on('uncaughtException', errHandler); process.on('unhandledRejection', errHandler); let myServices = Object.create(null); (async () => { var config = await loadConfig(); for (let [name, service] of Object.entries(config.services)) { let xservice = new MultiplexedService(service); myServices[name] = xservice; xservice.name = name; for (let port of service.ports) { await ({ tcp: proxyTcp, udp: proxyUdp })[port.type]( port.bindAddr, port.upstreamAddr, xservice, port.timeout ?? 30000, port.preread ?? (() => true), port.filter ?? (msg => msg), ); } } })();