UNPKG

utp-punch

Version:

UTP over UDP with NAT hole punching

335 lines (273 loc) 9.72 kB
const dgram = require('dgram'); const EventEmitter = require('events').EventEmitter; const debug = require('debug')('utp'); const connection = require('./connection'); const PUNCH_SYN = 'PUNCH'; const PUNCH_ACK = 'PUNCHED'; const ID_LIFETIME = 5 * 1000; // ms class Node extends EventEmitter { constructor(options, onconnection) { super(); if (typeof options === 'function') { onconnection = options; options = undefined; } this._options = options || {}; this._socket = dgram.createSocket('udp4'); this._socket.setMaxListeners(0); this._bound = false; this._closing = false; this._closed = false; this._serverConnections = null; this._clientConnections = new Map(); this._clientIds = new Set(); this._lastId = Math.floor(Math.random() * (connection.MAX_CONNECTION_ID + 1)); if (this._lastId % 2) this._lastId--; this._socket.on('message', this.onMessage.bind(this)); this._socket.on('error', error => { this.emit('error', error); }); if (onconnection) this.on('connection', onconnection); } get maxConnections() { return (connection.MAX_CONNECTION_ID + 1) / 2; } get serverConnections() { return this._serverConnections ? this._serverConnections.size : 0; } get clientConnections() { return this._clientConnections.size; } getUdpSocket() { return this._socket; } address() { return this._socket.address(); } bind(port, host, onbound) { if (this._closing || this._closed) throw new Error('Node is closed'); if (this._bound) throw new Error('Node is already bound'); if (typeof host === 'function') { onbound = host; host = undefined; } else if (typeof port === 'function') { onbound = port; port = undefined; host = undefined; } if (onbound) this.once('bound', onbound); this._socket.once('listening', () => { this._bound = true; this.emit('bound'); }); this._socket.bind(port, host); } punch(attempts, port, host = '127.0.0.1', cb = undefined) { if (this._closing || this._closed) throw new Error('Node is closed'); if (typeof host === 'function') { cb = host; host = '127.0.0.1'; } port = parseInt(port); let synBuffer = Buffer.from(PUNCH_SYN); let ackBuffer = Buffer.from(PUNCH_ACK); let ackSent = false; let ackReceived = false; let done = false; let punchCounter = 0; let onMessage = (data, rinfo) => { if (rinfo.address !== host || String(rinfo.port) !== String(port)) return; if (data.equals(synBuffer)) { debug('Received PUNCH SYN'); this._socket.send(ackBuffer, port, host, () => { ackSent = true; this._socket.send(ackBuffer, port, host); }); } else if (data.equals(ackBuffer)) { debug('Received PUNCH ACK'); ackReceived = true; } }; let sendPunch = () => { if (done) return; if (ackSent && ackReceived) { done = true; this._socket.removeListener('message', onMessage); if (cb) cb(true); return; } if (++punchCounter > attempts) { done = true; this._socket.removeListener('message', onMessage); if (cb) cb(false); return; } this._socket.send(synBuffer, port, host, () => { setTimeout(sendPunch, 500); }); }; this._socket.on('message', onMessage); if (this._bound) sendPunch(); else this.once('bound', sendPunch); } listen(onlistening) { if (this._closing || this._closed) throw new Error('Node is closed'); if (this._serverConnections) throw new Error('Node is already listening'); this._serverConnections = new Map(); if (onlistening) this.once('listening', onlistening); if (this._bound) { this.emit('listening'); } else { this.once('bound', function () { this.emit('listening'); }); } } connect(port, host = '127.0.0.1', onconnect = undefined) { if (this._closing || this._closed) throw new Error('Node is closed'); if (typeof host === 'function') { onconnect = host; host = '127.0.0.1'; } let id = this._generateId(); let key = this._getKey(host, port, id); let socket = new connection.Connection(id, port, host, this._socket, null, this._options); this._clientConnections.set(key, socket); this._clientIds.add(id); socket.once('close', () => { setTimeout(() => { this._clientConnections.delete(key); this._clientIds.delete(id); }, ID_LIFETIME); }); socket.once('connect', function () { this.emit('connect', socket); if (onconnect) onconnect(socket); }); if (this._bound) { socket._connect(); } else { this.once('bound', function () { socket._connect(); }); } return socket; } close(onclose) { if (this._closing || this._closed) return; this._closing = true; let openConnections = 0; let done = () => { if (this._socket) this._socket.close(); this._closing = false; this._closed = true; this._clientConnections.clear(); if (this._serverConnections) this._serverConnections.clear(); this.emit('close'); }; let onClose = () => { if (--openConnections === 0) done(); }; if (onclose) this.once('close', onclose); let sockets = Array.from(this._clientConnections.values()); if (this._serverConnections) sockets = sockets.concat(Array.from(this._serverConnections.values())); for (let socket of sockets) { if (socket._closed) continue; openConnections++; socket.once('close', onClose); socket.end(); } if (openConnections === 0) done(); } onMessage(message, rinfo) { if (message.length < connection.MIN_PACKET_SIZE) return; let packet = connection.bufferToPacket(message); let reply = false; let id = packet.connection; if (id % 2) { reply = true; id--; } if (this._closed) { connection.Connection.reset(this._socket, rinfo.port, rinfo.address, reply ? packet.connection - 1 : packet.connection + 1); return; } let key = this._getKey(rinfo.address, rinfo.port, id); if (reply) this._handleClient(key, packet, rinfo); else if (this._serverConnections) this._handleServer(key, packet, rinfo); else connection.Connection.reset(this._socket, rinfo.port, rinfo.address, packet.connection + 1); } _handleServer(key, packet, rinfo) { if (this._serverConnections.has(key)) return this._serverConnections.get(key)._recvIncoming(packet); if (packet.id !== connection.PACKET_SYN) { debug(`Invalid incoming packet ${key}`); connection.Connection.reset(this._socket, rinfo.port, rinfo.address, packet.connection + 1); return; } if (this._closing) { connection.Connection.reset(this._socket, rinfo.port, rinfo.address, packet.connection + 1); return; } debug(`Incoming connection ${key}`); let socket = new connection.Connection(packet.connection, rinfo.port, rinfo.address, this._socket, packet, this._options); this._serverConnections.set(key, socket); socket.once('close', () => { setTimeout(() => { this._serverConnections.delete(key); }, ID_LIFETIME); }); this.emit('connection', socket); } _handleClient(key, packet, rinfo) { let socket = this._clientConnections.get(key); if (!socket) { debug(`Invalid reply packet ${key}`); connection.Connection.reset(this._socket, rinfo.port, rinfo.address, packet.connection - 1); return; } socket._recvIncoming(packet); } _generateId() { let range = 0; while (range <= this.maxConnections) { this._lastId = this._lastId + 2; if (this._lastId > connection.MAX_CONNECTION_ID) this._lastId -= connection.MAX_CONNECTION_ID + 2; if (!this._clientIds.has(this._lastId)) return this._lastId; range++; } throw new Error('Out of connections'); } _getKey(host, port, id) { let key = host + '/' + port; if (id) key += '/' + id; return key; } } module.exports = Node;