v1.0
parent
21fd819ee5
commit
1383af2646
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
22
README.md
22
README.md
|
@ -1,2 +1,20 @@
|
||||||
# minetest-wasm-sample-proxy
|
minetest-wasm-sample-proxy
|
||||||
Sample proxy for minetest-wasm
|
==========================
|
||||||
|
Sample websocket proxy for minetest-wasm
|
||||||
|
|
||||||
|
|
||||||
|
Requires
|
||||||
|
------------
|
||||||
|
node.js
|
||||||
|
npm
|
||||||
|
|
||||||
|
Install Package Dependencies
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
$ cd minetest-wasm-sample-proxy
|
||||||
|
$ npm install
|
||||||
|
|
||||||
|
Run Proxy
|
||||||
|
---------
|
||||||
|
|
||||||
|
$ node main.js
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import { extract_ip_chain, sanitize } from './util.js';
|
||||||
|
import { vpn_make, vpn_connect} from './vpn.js';
|
||||||
|
import { format } from 'util'; // node.js built-in
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
|
||||||
|
let lastlog = null;
|
||||||
|
let lastlogcount = 0;
|
||||||
|
|
||||||
|
export class Client {
|
||||||
|
constructor(id, socket, request) {
|
||||||
|
this.id = id;
|
||||||
|
this.socket = socket;
|
||||||
|
this.ip_chain = extract_ip_chain(request);
|
||||||
|
this.target = null;
|
||||||
|
this.socket.on('message', this.handle_message.bind(this));
|
||||||
|
this.socket.on('error', this.handle_error.bind(this));
|
||||||
|
this.socket.on('close', this.handle_close.bind(this));
|
||||||
|
this.log("New client from ", this.ip_chain);
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
let line = [`[CLIENT ${this.id}]`, ...arguments].map(o => format("%s", o)).join(" ");
|
||||||
|
if (lastlog != line) {
|
||||||
|
if (lastlogcount > 0) {
|
||||||
|
console.log(lastlog + ` [repeated ${lastlogcount} times]`);
|
||||||
|
}
|
||||||
|
console.log(line);
|
||||||
|
lastlog = line;
|
||||||
|
lastlogcount = 0;
|
||||||
|
} else {
|
||||||
|
lastlogcount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(data) {
|
||||||
|
let binary =
|
||||||
|
Buffer.isBuffer(data) ||
|
||||||
|
(data instanceof ArrayBuffer) ||
|
||||||
|
ArrayBuffer.isView(data);
|
||||||
|
if (this.socket) {
|
||||||
|
this.socket.send(data, {binary});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
let socket = this.socket;
|
||||||
|
this.socket = null;
|
||||||
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
let target = this.target;
|
||||||
|
this.target = null;
|
||||||
|
if (target) {
|
||||||
|
target.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_error() {
|
||||||
|
this.log("Error");
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_close() {
|
||||||
|
this.socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_message(buffer, isBinary) {
|
||||||
|
// node.js specific fix: Convert Buffer to ArrayBuffer
|
||||||
|
buffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||||
|
if (!isBinary) {
|
||||||
|
buffer = textDecoder.decode(buffer);
|
||||||
|
}
|
||||||
|
if (this.target) {
|
||||||
|
this.target.forward(buffer);
|
||||||
|
} else {
|
||||||
|
this.handle_command(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_command(data) {
|
||||||
|
data = sanitize(data);
|
||||||
|
let tokens = data.split(' ');
|
||||||
|
let command = tokens[0];
|
||||||
|
let response = null;
|
||||||
|
if (command == 'MAKEVPN') {
|
||||||
|
const game = tokens[1];
|
||||||
|
const [serverCode, clientCode] = vpn_make(game);
|
||||||
|
response = `NEWVPN ${serverCode} ${clientCode}`;
|
||||||
|
} else if (command == 'VPN') {
|
||||||
|
const code = tokens[1];
|
||||||
|
const bindport = parseInt(tokens[5], 10);
|
||||||
|
this.target = vpn_connect(this, code, bindport);
|
||||||
|
if (this.target == null) {
|
||||||
|
this.log(`VPN connect failed`);
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response = 'BIND OK';
|
||||||
|
} else {
|
||||||
|
this.log('Unhandled command: ', data);
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PROXY_PORT = 8888;
|
||||||
|
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { Client } from './client.js';
|
||||||
|
|
||||||
|
const options = {};
|
||||||
|
options.port = PROXY_PORT;
|
||||||
|
const wss = new WebSocketServer(options);
|
||||||
|
|
||||||
|
let connId = 1;
|
||||||
|
wss.on('connection', (socket, request) => {
|
||||||
|
new Client(connId++, socket, request);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Proxy listening on port ${PROXY_PORT}`);
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "sampleproxy",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"crypto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
|
||||||
|
},
|
||||||
|
"ws": {
|
||||||
|
"version": "8.13.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz",
|
||||||
|
"integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "sampleproxy",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"ws": "^8.13.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
import { isIPv4 } from 'net';
|
||||||
|
|
||||||
|
export function inet_pton(ip) {
|
||||||
|
assert(isIPv4(ip));
|
||||||
|
const ret = new ArrayBuffer(4);
|
||||||
|
const v = new DataView(ret);
|
||||||
|
var [a, b, c, d] = ip.split('.');
|
||||||
|
v.setUint8(0, parseInt(a));
|
||||||
|
v.setUint8(1, parseInt(b));
|
||||||
|
v.setUint8(2, parseInt(c));
|
||||||
|
v.setUint8(3, parseInt(d));
|
||||||
|
return ret; // network order
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inet_ntop(n) {
|
||||||
|
assert(n instanceof ArrayBuffer)
|
||||||
|
assert(n.byteLength == 4);
|
||||||
|
const v = new DataView(n);
|
||||||
|
const a = v.getUint8(0);
|
||||||
|
const b = v.getUint8(1);
|
||||||
|
const c = v.getUint8(2);
|
||||||
|
const d = v.getUint8(3);
|
||||||
|
return `${a}.${b}.${c}.${d}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make an untrusted string safe to print
|
||||||
|
export function sanitize(s) {
|
||||||
|
return s.replace(/[^\x20-\x7E]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the IP address(es) making the request.
|
||||||
|
// Because websocket requests can be forwarded
|
||||||
|
// between hosts, this may be a chain of addresses.
|
||||||
|
// These addresses are reported voluntarily by each host,
|
||||||
|
// and thus may or may not be accurate.
|
||||||
|
export function extract_ip_chain(request) {
|
||||||
|
const chain = [];
|
||||||
|
for (let entry of request.headers['x-forwarded-for'].split(',')) {
|
||||||
|
chain.push(sanitize(entry.trim()));
|
||||||
|
}
|
||||||
|
chain.push(request.socket.remoteAddress);
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a random integer in [a, b]
|
||||||
|
export function randint(a, b) {
|
||||||
|
return a + Math.floor(Math.random() * (b - a + 1));
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
import { randint, inet_ntop, inet_pton } from './util.js';
|
||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
function rand_vpn_code() {
|
||||||
|
return randomBytes(6).toString("hex").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
const vpns = new Map();
|
||||||
|
|
||||||
|
class VPN {
|
||||||
|
constructor() {
|
||||||
|
this.serverCode = rand_vpn_code();
|
||||||
|
this.clientCode = rand_vpn_code();
|
||||||
|
this.game = null; // not tracked
|
||||||
|
this.targets = new Map();
|
||||||
|
vpns.set(this.serverCode, this);
|
||||||
|
vpns.set(this.clientCode, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
route(ip, port) {
|
||||||
|
let addr = `${ip}:${port}`;
|
||||||
|
if (this.targets.has(addr)) {
|
||||||
|
return this.targets.get(addr);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function vpn_make(game) {
|
||||||
|
const vpn = new VPN();
|
||||||
|
return [vpn.serverCode, vpn.clientCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function vpn_connect(client, code, bindport) {
|
||||||
|
if (!vpns.has(code)) return null;
|
||||||
|
const vpn = vpns.get(code);
|
||||||
|
return new VPNTarget(vpn, client, code, bindport);
|
||||||
|
}
|
||||||
|
|
||||||
|
class VPNTarget {
|
||||||
|
constructor(vpn, client, code, bindport) {
|
||||||
|
this.vpn = vpn;
|
||||||
|
this.client = client;
|
||||||
|
this.bindport = bindport;
|
||||||
|
if (code == vpn.serverCode) {
|
||||||
|
this.ip = '172.16.0.1';
|
||||||
|
} else if (code == vpn.clientCode) {
|
||||||
|
const b = randint(16, 32);
|
||||||
|
const c = randint(1, 254);
|
||||||
|
const d = randint(1, 254);
|
||||||
|
this.ip = `172.${b}.${c}.${d}`;
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid code');
|
||||||
|
}
|
||||||
|
this.addr = `${this.ip}:${this.bindport}`;
|
||||||
|
vpn.targets.set(this.addr, this);
|
||||||
|
client.log("VPN connect to ${this.addr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward a message from the client
|
||||||
|
forward(data) {
|
||||||
|
// Data is encapsulated with a 12 byte header.
|
||||||
|
// Magic - 4 bytes 0x778B4CF3
|
||||||
|
// Dest IP - 4 bytes 0xAABBCCDD for AA.BB.CC.DD
|
||||||
|
// Dest Port - 2 bytes
|
||||||
|
// Packet Len - 2 bytes
|
||||||
|
const EP_MAGIC = 0x778B4CF3;
|
||||||
|
assert(data instanceof ArrayBuffer);
|
||||||
|
const view = new DataView(data);
|
||||||
|
assert(data.byteLength >= 12);
|
||||||
|
assert(view.getUint32(0) == EP_MAGIC);
|
||||||
|
const dest_ip = inet_ntop(data.slice(4, 8));
|
||||||
|
const dest_port = view.getUint16(8);
|
||||||
|
const pktlen = view.getUint16(10);
|
||||||
|
assert(data.byteLength == 12 + pktlen);
|
||||||
|
const remote = this.vpn.route(dest_ip, dest_port);
|
||||||
|
if (!remote) {
|
||||||
|
// Packet is dropped
|
||||||
|
this.client.log(`${this.addr} -> ${dest_ip}:${dest_port} (dropped)`);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.client.log(`${this.addr} -> ${remote.addr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite the header to contain source ip/port
|
||||||
|
(new Uint8Array(data, 4, 4)).set(new Uint8Array(inet_pton(this.ip)));
|
||||||
|
view.setUint16(8, this.bindport);
|
||||||
|
remote.client.send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.client.close();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue