diff --git a/README.md b/README.md index ba7c32a..336557b 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,7 @@ Browser <--/ \--> Device It's the kind of thing you'd use to build a poor man's VPN, or port-forward router. -The M-PROXY Protocol -=================== +# The M-PROXY Protocol This is similar to "The PROXY Protocol" (a la HAProxy), but desgined for multiplexed tls, http, tcp, and udp tunneled over arbitrary streams (such as WebSockets). @@ -60,8 +59,7 @@ service port (string) The listening port, such as 443. Useful for no host or server name (string) Useful for services that can be routed by name, such as http, https, smtp, and dns. ``` -Tunneled TCP SNI Packet ------------------------ +## Tunneled TCP SNI Packet You should see that the result is simply all of the original packet with a leading header. @@ -91,15 +89,13 @@ Note that `16 03 01 00` starts at the 29th byte (at index 28 or 0x1C) instead of The v1 header uses strings for address and service descriptor information, but future versions may be binary. -API -=== +# API ```js var Packer = require('proxy-packer'); ``` -Unpacker / Parser State Machine ------------------------ +## Unpacker / Parser State Machine The unpacker creates a state machine. @@ -108,52 +104,52 @@ composing a full message with header and data (unless data length is 0). The state machine progresses through these states: -* version -* headerLength -* header -* data +- version +- headerLength +- header +- data At the end of the data event (which may or may not contain a buffer of data) one of the appropriate handlers will be called. -* control -* connection -* message -* pause -* resume -* end -* error +- control +- connection +- message +- pause +- resume +- end +- error ```js -unpacker = Packer.create(handlers); // Create a state machine for unpacking +unpacker = Packer.create(handlers); // Create a state machine for unpacking -unpacker.fns.addData(chunk); // process a chunk of data +unpacker.fns.addData(chunk); // process a chunk of data -handlers.oncontrol = function (tun) { } // for communicating with the proxy - // tun.data is an array - // '[ -1, "[Error] bad hello" ]' - // '[ 0, "[Error] out-of-band error message" ]' - // '[ 1, "hello", 254, [ "add_token", "delete_token" ] ]' - // '[ 1, "add_token" ]' - // '[ 1, "delete_token" ]' +handlers.oncontrol = function(tun) {}; // for communicating with the proxy +// tun.data is an array +// '[ -1, "[Error] bad hello" ]' +// '[ 0, "[Error] out-of-band error message" ]' +// '[ 1, "hello", 254, [ "add_token", "delete_token" ] ]' +// '[ 1, "add_token" ]' +// '[ 1, "delete_token" ]' -handlers.onconnection = function (tun) { } // a client has established a connection +handlers.onconnection = function(tun) {}; // a client has established a connection -handlers.onmessage = function (tun) { } // a client has sent a message - // tun = { family, address, port, data - // , service, serviceport, name }; +handlers.onmessage = function(tun) {}; // a client has sent a message +// tun = { family, address, port, data +// , service, serviceport, name }; -handlers.onpause = function (tun) { } // proxy requests to pause upload to a client - // tun = { family, address, port }; +handlers.onpause = function(tun) {}; // proxy requests to pause upload to a client +// tun = { family, address, port }; -handlers.onresume = function (tun) { } // proxy requests to resume upload to a client - // tun = { family, address, port }; +handlers.onresume = function(tun) {}; // proxy requests to resume upload to a client +// tun = { family, address, port }; -handlers.onend = function (tun) { } // proxy requests to close a client's socket - // tun = { family, address, port }; +handlers.onend = function(tun) {}; // proxy requests to close a client's socket +// tun = { family, address, port }; -handlers.onerror = function (err) { } // proxy is relaying a client's error - // err = { message, family, address, port }; +handlers.onerror = function(err) {}; // proxy is relaying a client's error +// err = { message, family, address, port }; ``` -Packer & Extras ------- +## Packer & Extras Packs header metadata about connection into a buffer (potentially with original data), ready to send. ```js -var headerAndBody = Packer.pack(tun, data); // Add M-PROXY header to data - // tun = { family, address, port - // , service, serviceport, name } +var headerAndBody = Packer.pack(tun, data); // Add M-PROXY header to data +// tun = { family, address, port +// , service, serviceport, name } -var headerBuf = Packer.packHeader(tun, data); // Same as above, but creates a buffer for header only - // (data can be converted to a buffer or sent as-is) +var headerBuf = Packer.packHeader(tun, data); // Same as above, but creates a buffer for header only +// (data can be converted to a buffer or sent as-is) -var addr = Packer.socketToAddr(socket); // Probe raw, raw socket for address info +var addr = Packer.socketToAddr(socket); // Probe raw, raw socket for address info -var id = Packer.addrToId(address); // Turn M-PROXY address info into a deterministic id +var id = Packer.addrToId(address); // Turn M-PROXY address info into a deterministic id -var id = Packer.socketToId(socket); // Turn raw, raw socket info into a deterministic id +var id = Packer.socketToId(socket); // Turn raw, raw socket info into a deterministic id ``` ## API Helpers ```js -var socket = Packer.Stream.wrapSocket(socketOrStream); // workaround for https://github.com/nodejs/node/issues/8854 - // which was just closed recently, but probably still needs - // something more like this (below) to work as intended - // https://github.com/findhit/proxywrap/blob/master/lib/proxywrap.js +var socket = Packer.Stream.wrapSocket(socketOrStream); // workaround for https://github.com/nodejs/node/issues/8854 +// which was just closed recently, but probably still needs +// something more like this (below) to work as intended +// https://github.com/findhit/proxywrap/blob/master/lib/proxywrap.js ``` ```js var myTransform = Packer.Transform.create({ - address: { - family: '...' - , address: '...' - , port: '...' - } - // hint at the service to be used -, service: 'https' + address: { + family: '...', + address: '...', + port: '...' + }, + // hint at the service to be used + service: 'https' }); ``` @@ -217,6 +212,7 @@ hexdump output.bin Where `input.json` looks something like this: `input.json`: + ``` { "version": 1 , "address": { @@ -231,12 +227,12 @@ Where `input.json` looks something like this: } ``` -Raw TCP SNI Packet ------------------- +## Raw TCP SNI Packet and `sni.tcp.bin` is any captured tcp packet, such as this one with a tls hello: `sni.tcp.bin`: + ``` 0 1 2 3 4 5 6 7 8 9 A B C D D F 0000000 16 03 01 00 c2 01 00 00 be 03 03 57 e3 76 50 66 @@ -255,8 +251,7 @@ and `sni.tcp.bin` is any captured tcp packet, such as this one with a tls hello: 00000c7 ``` -Tunneled TCP SNI Packet ------------------------ +## Tunneled TCP SNI Packet You should see that the result is simply all of the original packet with a leading header. diff --git a/index.js b/index.js index 46ae795..404d3a3 100644 --- a/index.js +++ b/index.js @@ -3,344 +3,374 @@ var Packer = module.exports; var serviceEvents = { - default: 'tunnelData' -, connection: 'tunnelConnection' -, control: 'tunnelControl' -, error: 'tunnelError' -, end: 'tunnelEnd' -, pause: 'tunnelPause' -, resume: 'tunnelResume' + default: 'tunnelData', + connection: 'tunnelConnection', + control: 'tunnelControl', + error: 'tunnelError', + end: 'tunnelEnd', + pause: 'tunnelPause', + resume: 'tunnelResume' }; var serviceFuncs = { - default: 'onmessage' -, connection: 'onconnection' -, control: 'oncontrol' -, error: 'onerror' -, end: 'onend' -, pause: 'onpause' -, resume: 'onresume' + default: 'onmessage', + connection: 'onconnection', + control: 'oncontrol', + error: 'onerror', + end: 'onend', + pause: 'onpause', + resume: 'onresume' }; -Packer.create = function (opts) { - var machine; +Packer.create = function(opts) { + var machine; - if (!opts.onMessage && !opts.onmessage) { - machine = new (require('events').EventEmitter)(); - } else { - machine = {}; - } + if (!opts.onMessage && !opts.onmessage) { + machine = new (require('events')).EventEmitter(); + } else { + machine = {}; + } - machine.onmessage = opts.onmessage || opts.onMessage; - machine.oncontrol = opts.oncontrol || opts.onControl; - machine.onconnection = opts.onconnection || opts.onConnection || function () {}; - machine.onerror = opts.onerror || opts.onError; - machine.onend = opts.onend || opts.onEnd; - machine.onpause = opts.onpause || opts.onPause; - machine.onresume = opts.onresume || opts.onResume; + machine.onmessage = opts.onmessage || opts.onMessage; + machine.oncontrol = opts.oncontrol || opts.onControl; + machine.onconnection = + opts.onconnection || opts.onConnection || function() {}; + machine.onerror = opts.onerror || opts.onError; + machine.onend = opts.onend || opts.onEnd; + machine.onpause = opts.onpause || opts.onPause; + machine.onresume = opts.onresume || opts.onResume; - machine._version = 1; - machine.fns = {}; + machine._version = 1; + machine.fns = {}; - machine.chunkIndex = 0; - machine.buf = null; - machine.bufIndex = 0; - machine.fns.collectData = function (chunk, size) { - var chunkLeft = chunk.length - machine.chunkIndex; - var hasLen = (size > 0); + machine.chunkIndex = 0; + machine.buf = null; + machine.bufIndex = 0; + machine.fns.collectData = function(chunk, size) { + var chunkLeft = chunk.length - machine.chunkIndex; + var hasLen = size > 0; - if (!hasLen) { - return Buffer.alloc(0); - } + if (!hasLen) { + return Buffer.alloc(0); + } - // First handle case where we don't have all the data we need yet. We need to save - // what we have in a buffer, and increment the index for both the buffer and the chunk. - if (machine.bufIndex + chunkLeft < size) { - if (!machine.buf) { - machine.buf = Buffer.alloc(size); - } - chunk.copy(machine.buf, machine.bufIndex, machine.chunkIndex); - machine.bufIndex += chunkLeft; - machine.chunkIndex += chunkLeft; + // First handle case where we don't have all the data we need yet. We need to save + // what we have in a buffer, and increment the index for both the buffer and the chunk. + if (machine.bufIndex + chunkLeft < size) { + if (!machine.buf) { + machine.buf = Buffer.alloc(size); + } + chunk.copy(machine.buf, machine.bufIndex, machine.chunkIndex); + machine.bufIndex += chunkLeft; + machine.chunkIndex += chunkLeft; - return null; - } + return null; + } - // Read and mark as read however much data we need from the chunk to complete our buffer. - var partLen = size - machine.bufIndex; - var part = chunk.slice(machine.chunkIndex, machine.chunkIndex+partLen); - machine.chunkIndex += partLen; + // Read and mark as read however much data we need from the chunk to complete our buffer. + var partLen = size - machine.bufIndex; + var part = chunk.slice( + machine.chunkIndex, + machine.chunkIndex + partLen + ); + machine.chunkIndex += partLen; - // If we had nothing buffered than the part of the chunk we just read is all we need. - if (!machine.buf) { - return part; - } + // If we had nothing buffered than the part of the chunk we just read is all we need. + if (!machine.buf) { + return part; + } - // Otherwise we need to copy the new data into the buffer. - part.copy(machine.buf, machine.bufIndex); - // Before returning the buffer we need to clear our reference to it. - var buf = machine.buf; - machine.buf = null; - machine.bufIndex = 0; - return buf; - }; + // Otherwise we need to copy the new data into the buffer. + part.copy(machine.buf, machine.bufIndex); + // Before returning the buffer we need to clear our reference to it. + var buf = machine.buf; + machine.buf = null; + machine.bufIndex = 0; + return buf; + }; - machine.fns.version = function (chunk) { - //console.log(''); - //console.log('[version]'); - if ((255 - machine._version) !== chunk[machine.chunkIndex]) { - console.error("not v" + machine._version + " (or data is corrupt)"); - // no idea how to fix this yet - } - machine.chunkIndex += 1; + machine.fns.version = function(chunk) { + //console.log(''); + //console.log('[version]'); + if (255 - machine._version !== chunk[machine.chunkIndex]) { + console.error('not v' + machine._version + ' (or data is corrupt)'); + // no idea how to fix this yet + } + machine.chunkIndex += 1; - return true; - }; + return true; + }; + machine.headerLen = 0; + machine.fns.headerLength = function(chunk) { + //console.log(''); + //console.log('[headerLength]'); + machine.headerLen = chunk[machine.chunkIndex]; + machine.chunkIndex += 1; - machine.headerLen = 0; - machine.fns.headerLength = function (chunk) { - //console.log(''); - //console.log('[headerLength]'); - machine.headerLen = chunk[machine.chunkIndex]; - machine.chunkIndex += 1; + return true; + }; - return true; - }; + machine.fns.header = function(chunk) { + //console.log(''); + //console.log('[header]'); + var header = machine.fns.collectData(chunk, machine.headerLen); - machine.fns.header = function (chunk) { - //console.log(''); - //console.log('[header]'); - var header = machine.fns.collectData(chunk, machine.headerLen); + // We don't have the entire header yet so return false. + if (!header) { + return false; + } - // We don't have the entire header yet so return false. - if (!header) { - return false; - } + machine._headers = header.toString().split(/,/g); - machine._headers = header.toString().split(/,/g); + machine.family = machine._headers[0]; + machine.address = machine._headers[1]; + machine.port = machine._headers[2]; + machine.bodyLen = parseInt(machine._headers[3], 10) || 0; + machine.service = machine._headers[4]; + machine.serviceport = machine._headers[5]; + machine.name = machine._headers[6]; + machine.servicename = machine._headers[7]; + //console.log('machine.service', machine.service); - machine.family = machine._headers[0]; - machine.address = machine._headers[1]; - machine.port = machine._headers[2]; - machine.bodyLen = parseInt(machine._headers[3], 10) || 0; - machine.service = machine._headers[4]; - machine.serviceport = machine._headers[5]; - machine.name = machine._headers[6]; - machine.servicename = machine._headers[7]; - //console.log('machine.service', machine.service); + return true; + }; - return true; - }; + machine.fns.data = function(chunk) { + //console.log(''); + //console.log('[data]'); + var data; + // The 'connection' event may not have a body + // Other events may not have a body either + if (machine.bodyLen) { + data = machine.fns.collectData(chunk, machine.bodyLen); + // We don't have the entire body yet so return false. + if (!data) { + return false; + } + } - machine.fns.data = function (chunk) { - //console.log(''); - //console.log('[data]'); - var data; - // The 'connection' event may not have a body - // Other events may not have a body either - if (machine.bodyLen) { - data = machine.fns.collectData(chunk, machine.bodyLen); - // We don't have the entire body yet so return false. - if (!data) { - return false; - } - } + // + // data, end, error + // + var msg = {}; + if ('error' === machine.service) { + try { + msg = JSON.parse(data.toString()); + } catch (e) { + msg.message = 'e:' + JSON.stringify(data); + msg.code = 'E_UNKNOWN_ERR'; + } + } + msg.family = machine.family; + msg.address = machine.address; + msg.port = machine.port; + msg.service = machine.service; + msg.serviceport = machine.serviceport; + msg.name = machine.name; + msg.data = data; - // - // data, end, error - // - var msg = {}; - if ('error' === machine.service) { - try { - msg = JSON.parse(data.toString()); - } catch(e) { - msg.message = 'e:' + JSON.stringify(data); - msg.code = 'E_UNKNOWN_ERR'; - } - } + if ('connection' === machine.service) { + msg.service = machine.servicename; + } - msg.family = machine.family; - msg.address = machine.address; - msg.port = machine.port; - msg.service = machine.service; - msg.serviceport = machine.serviceport; - msg.name = machine.name; - msg.data = data; + //console.log('msn', machine.service); + if (machine.emit) { + machine.emit( + serviceEvents[machine.service] || + serviceEvents[msg.service] || + serviceEvents.default + ); + } else { + (machine[serviceFuncs[machine.service]] || + machine[serviceFuncs[msg.service]] || + machine[serviceFuncs.default])(msg); + } - if ('connection' === machine.service) { - msg.service = machine.servicename; - } + return true; + }; - //console.log('msn', machine.service); - if (machine.emit) { - machine.emit(serviceEvents[machine.service] || serviceEvents[msg.service] || serviceEvents.default); - } else { - (machine[serviceFuncs[machine.service]] || machine[serviceFuncs[msg.service]] || machine[serviceFuncs.default])(msg); - } + machine.state = 0; + machine.states = ['version', 'headerLength', 'header', 'data']; + machine.fns.addChunk = function(chunk) { + //console.log(''); + //console.log('[addChunk]'); + machine.chunkIndex = 0; + while (machine.chunkIndex < chunk.length) { + //console.log('chunkIndex:', machine.chunkIndex, 'state:', machine.state); - return true; - }; + if (true === machine.fns[machine.states[machine.state]](chunk)) { + machine.state += 1; + machine.state %= machine.states.length; + } + } + if ('data' === machine.states[machine.state] && 0 === machine.bodyLen) { + machine.fns[machine.states[machine.state]](chunk); + machine.state += 1; + machine.state %= machine.states.length; + } + }; - machine.state = 0; - machine.states = ['version', 'headerLength', 'header', 'data']; - machine.fns.addChunk = function (chunk) { - //console.log(''); - //console.log('[addChunk]'); - machine.chunkIndex = 0; - while (machine.chunkIndex < chunk.length) { - //console.log('chunkIndex:', machine.chunkIndex, 'state:', machine.state); - - if (true === machine.fns[machine.states[machine.state]](chunk)) { - machine.state += 1; - machine.state %= machine.states.length; - } - } - if ('data' === machine.states[machine.state] && 0 === machine.bodyLen) { - machine.fns[machine.states[machine.state]](chunk) - machine.state += 1; - machine.state %= machine.states.length; - } - }; - - return machine; + return machine; }; -Packer.packHeader = function (meta, data, service, andBody, oldways) { - if (oldways && !data) { - data = Buffer.from(' '); - } - if (data && !Buffer.isBuffer(data)) { - data = Buffer.from(JSON.stringify(data)); - } - if (oldways && !data.byteLength) { - data = Buffer.from(' '); - } +Packer.packHeader = function(meta, data, service, andBody, oldways) { + if (oldways && !data) { + data = Buffer.from(' '); + } + if (data && !Buffer.isBuffer(data)) { + data = Buffer.from(JSON.stringify(data)); + } + if (oldways && !data.byteLength) { + data = Buffer.from(' '); + } - if (service && -1 === ['control','connection'].indexOf(service)) { - //console.log('end?', service); - meta.service = service; - } + if (service && -1 === ['control', 'connection'].indexOf(service)) { + //console.log('end?', service); + meta.service = service; + } - var size = data && data.byteLength || 0; - var sizeReserve = andBody ? size : 0; - var version = 1; - var header; - if (service === 'control') { - header = Buffer.from(['', '', '', size, service].join(',')); - } - else if (service === 'connection') { - header = Buffer.from([ - meta.family, meta.address, meta.port, size, - 'connection', (meta.serviceport || ''), (meta.name || ''), - (meta.service || '') - ].join(',')); - } - else { - header = Buffer.from([ - meta.family, meta.address, meta.port, size, - (meta.service || ''), (meta.serviceport || ''), (meta.name || '') - ].join(',')); - } - var metaBuf = Buffer.from([ 255 - version, header.length ]); - var buf = Buffer.alloc(metaBuf.byteLength + header.byteLength + sizeReserve); + var size = (data && data.byteLength) || 0; + var sizeReserve = andBody ? size : 0; + var version = 1; + var header; + if (service === 'control') { + header = Buffer.from(['', '', '', size, service].join(',')); + } else if (service === 'connection') { + header = Buffer.from( + [ + meta.family, + meta.address, + meta.port, + size, + 'connection', + meta.serviceport || '', + meta.name || '', + meta.service || '' + ].join(',') + ); + } else { + header = Buffer.from( + [ + meta.family, + meta.address, + meta.port, + size, + meta.service || '', + meta.serviceport || '', + meta.name || '' + ].join(',') + ); + } + var metaBuf = Buffer.from([255 - version, header.length]); + var buf = Buffer.alloc( + metaBuf.byteLength + header.byteLength + sizeReserve + ); - metaBuf.copy(buf, 0); - header.copy(buf, 2); - if (sizeReserve) { data.copy(buf, 2 + header.byteLength); } + metaBuf.copy(buf, 0); + header.copy(buf, 2); + if (sizeReserve) { + data.copy(buf, 2 + header.byteLength); + } - return buf; + return buf; }; -Packer.pack = function (meta, data, service) { - return Packer.packHeader(meta, data, service, true, true); +Packer.pack = function(meta, data, service) { + return Packer.packHeader(meta, data, service, true, true); }; function extractSocketProps(socket, propNames) { - var props = {}; + var props = {}; - if (socket.remotePort) { - propNames.forEach(function (propName) { - props[propName] = socket[propName]; - }); - } else if (socket._remotePort) { - propNames.forEach(function (propName) { - props[propName] = socket['_' + propName]; - }); - } else if (socket._handle) { - if ( - socket._handle._parent - && socket._handle._parent.owner - && socket._handle._parent.owner.stream - && socket._handle._parent.owner.stream.remotePort - ) { - propNames.forEach(function (propName) { - props[propName] = socket._handle._parent.owner.stream[propName]; - }); - } else if ( - socket._handle._parentWrap - && socket._handle._parentWrap.remotePort - ) { - propNames.forEach(function (propName) { - props[propName] = socket._handle._parentWrap[propName]; - }); - } else if ( - socket._handle._parentWrap - && socket._handle._parentWrap._handle - && socket._handle._parentWrap._handle.owner - && socket._handle._parentWrap._handle.owner.stream - && socket._handle._parentWrap._handle.owner.stream.remotePort - ) { - propNames.forEach(function (propName) { - props[propName] = socket._handle._parentWrap._handle.owner.stream[propName]; - }); - } - } - return props; + if (socket.remotePort) { + propNames.forEach(function(propName) { + props[propName] = socket[propName]; + }); + } else if (socket._remotePort) { + propNames.forEach(function(propName) { + props[propName] = socket['_' + propName]; + }); + } else if (socket._handle) { + if ( + socket._handle._parent && + socket._handle._parent.owner && + socket._handle._parent.owner.stream && + socket._handle._parent.owner.stream.remotePort + ) { + propNames.forEach(function(propName) { + props[propName] = socket._handle._parent.owner.stream[propName]; + }); + } else if ( + socket._handle._parentWrap && + socket._handle._parentWrap.remotePort + ) { + propNames.forEach(function(propName) { + props[propName] = socket._handle._parentWrap[propName]; + }); + } else if ( + socket._handle._parentWrap && + socket._handle._parentWrap._handle && + socket._handle._parentWrap._handle.owner && + socket._handle._parentWrap._handle.owner.stream && + socket._handle._parentWrap._handle.owner.stream.remotePort + ) { + propNames.forEach(function(propName) { + props[propName] = + socket._handle._parentWrap._handle.owner.stream[propName]; + }); + } + } + return props; } function extractSocketProp(socket, propName) { - // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854 - var value = socket[propName] || socket['_' + propName]; - try { - value = value || socket._handle._parent.owner.stream[propName]; - } catch (e) {} + // remoteAddress, remotePort... ugh... https://github.com/nodejs/node/issues/8854 + var value = socket[propName] || socket['_' + propName]; + try { + value = value || socket._handle._parent.owner.stream[propName]; + } catch (e) {} - try { - value = value || socket._handle._parentWrap[propName]; - value = value || socket._handle._parentWrap._handle.owner.stream[propName]; - } catch (e) {} + try { + value = value || socket._handle._parentWrap[propName]; + value = + value || socket._handle._parentWrap._handle.owner.stream[propName]; + } catch (e) {} - return value || ''; + return value || ''; } -Packer.socketToAddr = function (socket) { - // TODO BUG XXX - // https://github.com/nodejs/node/issues/8854 - // tlsSocket.remoteAddress = remoteAddress; // causes core dump - // console.log(tlsSocket.remoteAddress); +Packer.socketToAddr = function(socket) { + // TODO BUG XXX + // https://github.com/nodejs/node/issues/8854 + // tlsSocket.remoteAddress = remoteAddress; // causes core dump + // console.log(tlsSocket.remoteAddress); - var props = extractSocketProps(socket, [ 'remoteFamily', 'remoteAddress', 'remotePort', 'localPort' ]); - return { - family: props.remoteFamily - , address: props.remoteAddress - , port: props.remotePort - , serviceport: props.localPort - }; + var props = extractSocketProps(socket, [ + 'remoteFamily', + 'remoteAddress', + 'remotePort', + 'localPort' + ]); + return { + family: props.remoteFamily, + address: props.remoteAddress, + port: props.remotePort, + serviceport: props.localPort + }; }; -Packer.addrToId = function (address) { - return address.family + ',' + address.address + ',' + address.port; +Packer.addrToId = function(address) { + return address.family + ',' + address.address + ',' + address.port; }; -Packer.socketToId = function (socket) { - return Packer.addrToId(Packer.socketToAddr(socket)); +Packer.socketToId = function(socket) { + return Packer.addrToId(Packer.socketToAddr(socket)); }; - var addressNames = [ - 'remoteAddress' -, 'remotePort' -, 'remoteFamily' -, 'localAddress' -, 'localPort' + 'remoteAddress', + 'remotePort', + 'remoteFamily', + 'localAddress', + 'localPort' ]; /* var sockFuncs = [ @@ -355,20 +385,20 @@ var sockFuncs = [ ]; */ // Unlike Packer.Stream.create this should handle all of the events needed to make everything work. -Packer.wrapSocket = function (socket) { - // node v10.2+ doesn't need a workaround for https://github.com/nodejs/node/issues/8854 - addressNames.forEach(function (name) { - Object.defineProperty(socket, name, { - enumerable: false, - configurable: true, - get: function() { - return extractSocketProp(socket, name); - } - }); - }); - return socket; - // Improved workaround for https://github.com/nodejs/node/issues/8854 - /* +Packer.wrapSocket = function(socket) { + // node v10.2+ doesn't need a workaround for https://github.com/nodejs/node/issues/8854 + addressNames.forEach(function(name) { + Object.defineProperty(socket, name, { + enumerable: false, + configurable: true, + get: function() { + return extractSocketProp(socket, name); + } + }); + }); + return socket; + // Improved workaround for https://github.com/nodejs/node/issues/8854 + /* // TODO use defineProperty to override remotePort, etc var myDuplex = new require('stream').Duplex(); addressNames.forEach(function (name) { @@ -418,79 +448,79 @@ var Transform = require('stream').Transform; var util = require('util'); function MyTransform(options) { - if (!(this instanceof MyTransform)) { - return new MyTransform(options); - } - this.__my_addr = options.address; - this.__my_service = options.service; - this.__my_serviceport = options.serviceport; - this.__my_name = options.name; - Transform.call(this, options); + if (!(this instanceof MyTransform)) { + return new MyTransform(options); + } + this.__my_addr = options.address; + this.__my_service = options.service; + this.__my_serviceport = options.serviceport; + this.__my_name = options.name; + Transform.call(this, options); } util.inherits(MyTransform, Transform); -MyTransform.prototype._transform = function (data, encoding, callback) { - var address = this.__my_addr; +MyTransform.prototype._transform = function(data, encoding, callback) { + var address = this.__my_addr; - address.service = address.service || this.__my_service; - address.serviceport = address.serviceport || this.__my_serviceport; - address.name = address.name || this.__my_name; - this.push(Packer.pack(address, data)); - callback(); + address.service = address.service || this.__my_service; + address.serviceport = address.serviceport || this.__my_serviceport; + address.name = address.name || this.__my_name; + this.push(Packer.pack(address, data)); + callback(); }; Packer.Stream = {}; var Dup = { - write: function (chunk, encoding, cb) { - //console.log('_write', chunk.byteLength); - this.__my_socket.write(chunk, encoding, cb); - } -, read: function (size) { - //console.log('_read'); - var x = this.__my_socket.read(size); - if (x) { - console.log('_read', size); - this.push(x); - } - } + write: function(chunk, encoding, cb) { + //console.log('_write', chunk.byteLength); + this.__my_socket.write(chunk, encoding, cb); + }, + read: function(size) { + //console.log('_read'); + var x = this.__my_socket.read(size); + if (x) { + console.log('_read', size); + this.push(x); + } + } }; -Packer.Stream.create = function (socket) { - if (!Packer.Stream.warned) { - console.warn('`Stream.create` deprecated, use `wrapSocket` instead'); - Packer.Stream.warned = true; - } +Packer.Stream.create = function(socket) { + if (!Packer.Stream.warned) { + console.warn('`Stream.create` deprecated, use `wrapSocket` instead'); + Packer.Stream.warned = true; + } - // Workaround for - // https://github.com/nodejs/node/issues/8854 + // Workaround for + // https://github.com/nodejs/node/issues/8854 - // https://www.google.com/#q=get+socket+address+from+file+descriptor - // TODO try creating a new net.Socket({ handle: socket._handle, fd: socket._handle.fd }) - // from the old one and then adding back the data with - // sock.push(firstChunk) - var Duplex = require('stream').Duplex; - var myDuplex = new Duplex(); + // https://www.google.com/#q=get+socket+address+from+file+descriptor + // TODO try creating a new net.Socket({ handle: socket._handle, fd: socket._handle.fd }) + // from the old one and then adding back the data with + // sock.push(firstChunk) + var Duplex = require('stream').Duplex; + var myDuplex = new Duplex(); - myDuplex.__my_socket = socket; - myDuplex._write = Dup.write; - myDuplex._read = Dup.read; - //console.log('plainSocket.*Address'); - //console.log('remote:', socket.remoteAddress); - //console.log('local:', socket.localAddress); - //console.log('address():', socket.address()); - myDuplex.remoteFamily = socket.remoteFamily; - myDuplex.remoteAddress = socket.remoteAddress; - myDuplex.remotePort = socket.remotePort; - myDuplex.localFamily = socket.localFamily; - myDuplex.localAddress = socket.localAddress; - myDuplex.localPort = socket.localPort; + myDuplex.__my_socket = socket; + myDuplex._write = Dup.write; + myDuplex._read = Dup.read; + //console.log('plainSocket.*Address'); + //console.log('remote:', socket.remoteAddress); + //console.log('local:', socket.localAddress); + //console.log('address():', socket.address()); + myDuplex.remoteFamily = socket.remoteFamily; + myDuplex.remoteAddress = socket.remoteAddress; + myDuplex.remotePort = socket.remotePort; + myDuplex.localFamily = socket.localFamily; + myDuplex.localAddress = socket.localAddress; + myDuplex.localPort = socket.localPort; - return myDuplex; + return myDuplex; }; Packer.Transform = {}; -Packer.Transform.create = function (opts) { - // Note: service refers to the port that the incoming request was from, - // if known (smtps, smtp, https, http, etc) - // { address: '127.0.0.1', service: 'https' } - return new MyTransform(opts); +Packer.Transform.create = function(opts) { + // Note: service refers to the port that the incoming request was from, + // if known (smtps, smtp, https, http, etc) + // { address: '127.0.0.1', service: 'https' } + return new MyTransform(opts); }; diff --git a/package.json b/package.json index f44e968..f8b2ad3 100644 --- a/package.json +++ b/package.json @@ -1,40 +1,40 @@ { - "name": "proxy-packer", - "version": "2.0.3", - "description": "A strategy for packing and unpacking a proxy stream (i.e. packets through a tunnel). Handles multiplexed and tls connections. Used by telebit and telebitd.", - "main": "index.js", - "scripts": { - "test": "node test.js" - }, - "repository": { - "type": "git", - "url": "git+https://git.coolaj86.com/coolaj86/proxy-packer.js.git" - }, - "keywords": [ - "tunnel", - "telebit", - "telebitd", - "localtunnel", - "ngrok", - "underpass", - "tcp", - "sni", - "https", - "ssl", - "tls", - "http", - "proxy", - "pack", - "unpack", - "message", - "msg", - "packer", - "unpacker" - ], - "author": "AJ ONeal (https://coolaj86.com/)", - "license": "(MIT OR Apache-2.0)", - "bugs": { - "url": "https://git.coolaj86.com/coolaj86/proxy-packer.js/issues" - }, - "homepage": "https://git.coolaj86.com/coolaj86/proxy-packer.js#readme" + "name": "proxy-packer", + "version": "2.0.4", + "description": "A strategy for packing and unpacking a proxy stream (i.e. packets through a tunnel). Handles multiplexed and tls connections. Used by telebit and telebitd.", + "main": "index.js", + "scripts": { + "test": "node test.js" + }, + "repository": { + "type": "git", + "url": "git+https://git.coolaj86.com/coolaj86/proxy-packer.js.git" + }, + "keywords": [ + "tunnel", + "telebit", + "telebitd", + "localtunnel", + "ngrok", + "underpass", + "tcp", + "sni", + "https", + "ssl", + "tls", + "http", + "proxy", + "pack", + "unpack", + "message", + "msg", + "packer", + "unpacker" + ], + "author": "AJ ONeal (https://coolaj86.com/)", + "license": "(MIT OR Apache-2.0)", + "bugs": { + "url": "https://git.coolaj86.com/coolaj86/proxy-packer.js/issues" + }, + "homepage": "https://git.coolaj86.com/coolaj86/proxy-packer.js#readme" } diff --git a/test/input.json b/test/input.json index 8c0751c..cd49441 100644 --- a/test/input.json +++ b/test/input.json @@ -1,10 +1,11 @@ -{ "version": 1 -, "address": { - "family": "IPv4" - , "address": "127.0.1.1" - , "port": 4321 - , "service": "https" - , "serviceport": 443 - } -, "filepath": "./sni.hello.bin" +{ + "version": 1, + "address": { + "family": "IPv4", + "address": "127.0.1.1", + "port": 4321, + "service": "https", + "serviceport": 443 + }, + "filepath": "./sni.hello.bin" } diff --git a/test/pack.js b/test/pack.js index ba1f822..a8cd358 100644 --- a/test/pack.js +++ b/test/pack.js @@ -1,28 +1,31 @@ -;(function () { -'use strict'; +(function() { + 'use strict'; -var fs = require('fs'); -var infile = process.argv[2]; -var outfile = process.argv[3]; -var sni = require('sni'); + var fs = require('fs'); + var infile = process.argv[2]; + var outfile = process.argv[3]; + var sni = require('sni'); -if (!infile || !outfile) { - console.error("Usage:"); - console.error("node test/pack.js test/input.json test/output.bin"); - process.exit(1); - return; -} + if (!infile || !outfile) { + console.error('Usage:'); + console.error('node test/pack.js test/input.json test/output.bin'); + process.exit(1); + return; + } -var path = require('path'); -var json = JSON.parse(fs.readFileSync(infile, 'utf8')); -var data = require('fs').readFileSync(path.resolve(path.dirname(infile), json.filepath), null); -var Packer = require('../index.js'); + var path = require('path'); + var json = JSON.parse(fs.readFileSync(infile, 'utf8')); + var data = require('fs').readFileSync( + path.resolve(path.dirname(infile), json.filepath), + null + ); + var Packer = require('../index.js'); -var servername = sni(data); -var m = data.toString().match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); -var hostname = (m && m[1].toLowerCase() || '').split(':')[0]; + var servername = sni(data); + var m = data.toString().match(/(?:^|[\r\n])Host: ([^\r\n]+)[\r\n]*/im); + var hostname = ((m && m[1].toLowerCase()) || '').split(':')[0]; -/* + /* function pack() { var version = json.version; var address = json.address; @@ -37,9 +40,16 @@ function pack() { } */ -json.address.name = servername || hostname; -var buf = Packer.pack(json.address, data); -fs.writeFileSync(outfile, buf, null); -console.log("wrote " + buf.byteLength + " bytes to '" + outfile + "' ('hexdump " + outfile + "' to inspect)"); - -}()); + json.address.name = servername || hostname; + var buf = Packer.pack(json.address, data); + fs.writeFileSync(outfile, buf, null); + console.log( + 'wrote ' + + buf.byteLength + + " bytes to '" + + outfile + + "' ('hexdump " + + outfile + + "' to inspect)" + ); +})(); diff --git a/test/parse.js b/test/parse.js index 5b48111..061508c 100644 --- a/test/parse.js +++ b/test/parse.js @@ -4,82 +4,114 @@ var sni = require('sni'); var hello = require('fs').readFileSync(__dirname + '/sni.hello.bin'); var version = 1; function getAddress() { - return { - family: 'IPv4' - , address: '127.0.1.1' - , port: 4321 - , service: 'foo-https' - , serviceport: 443 - , name: 'foo-pokemap.hellabit.com' - }; + return { + family: 'IPv4', + address: '127.0.1.1', + port: 4321, + service: 'foo-https', + serviceport: 443, + name: 'foo-pokemap.hellabit.com' + }; } var addr = getAddress(); -var connectionHeader = addr.family + ',' + addr.address + ',' + addr.port - + ',0,connection,' - + (addr.serviceport || '') + ',' + (addr.name || '') + ',' + (addr.service || '') - ; -var header = addr.family + ',' + addr.address + ',' + addr.port - + ',' + hello.byteLength + ',' + (addr.service || '') + ',' - + (addr.serviceport || '') + ',' + (addr.name || '') - ; -var endHeader = addr.family + ',' + addr.address + ',' + addr.port - + ',0,end,' - + (addr.serviceport || '') + ',' + (addr.name || '') - ; +var connectionHeader = + addr.family + + ',' + + addr.address + + ',' + + addr.port + + ',0,connection,' + + (addr.serviceport || '') + + ',' + + (addr.name || '') + + ',' + + (addr.service || ''); +var header = + addr.family + + ',' + + addr.address + + ',' + + addr.port + + ',' + + hello.byteLength + + ',' + + (addr.service || '') + + ',' + + (addr.serviceport || '') + + ',' + + (addr.name || ''); +var endHeader = + addr.family + + ',' + + addr.address + + ',' + + addr.port + + ',0,end,' + + (addr.serviceport || '') + + ',' + + (addr.name || ''); var buf = Buffer.concat([ - Buffer.from([ 255 - version, connectionHeader.length ]) -, Buffer.from(connectionHeader) -, Buffer.from([ 255 - version, header.length ]) -, Buffer.from(header) -, hello -, Buffer.from([ 255 - version, endHeader.length ]) -, Buffer.from(endHeader) + Buffer.from([255 - version, connectionHeader.length]), + Buffer.from(connectionHeader), + Buffer.from([255 - version, header.length]), + Buffer.from(header), + hello, + Buffer.from([255 - version, endHeader.length]), + Buffer.from(endHeader) ]); -var services = { 'ssh': 22, 'http': 4080, 'https': 8443 }; +var services = { ssh: 22, http: 4080, https: 8443 }; var clients = {}; var count = 0; var packer = require('../'); var machine = packer.create({ - onconnection: function (tun) { - console.info(''); - if (!tun.service || 'connection' === tun.service) { - throw new Error("missing service: " + JSON.stringify(tun)); - } - console.info('[onConnection]'); - count += 1; - } -, onmessage: function (tun) { - //console.log('onmessage', tun); - var id = tun.family + ',' + tun.address + ',' + tun.port; - var service = 'https'; - var port = services[service]; - var servername = sni(tun.data); + onconnection: function(tun) { + console.info(''); + if (!tun.service || 'connection' === tun.service) { + throw new Error('missing service: ' + JSON.stringify(tun)); + } + console.info('[onConnection]'); + count += 1; + }, + onmessage: function(tun) { + //console.log('onmessage', tun); + var id = tun.family + ',' + tun.address + ',' + tun.port; + var service = 'https'; + var port = services[service]; + var servername = sni(tun.data); - console.info('[onMessage]', service, port, servername, tun.data.byteLength); - if (!tun.data.equals(hello)) { - throw new Error("'data' packet is not equal to original 'hello' packet"); - } - //console.log('all', tun.data.byteLength, 'bytes are equal'); - //console.log('src:', tun.family, tun.address + ':' + tun.port + ':' + tun.serviceport); - //console.log('dst:', 'IPv4 127.0.0.1:' + port); + console.info( + '[onMessage]', + service, + port, + servername, + tun.data.byteLength + ); + if (!tun.data.equals(hello)) { + throw new Error( + "'data' packet is not equal to original 'hello' packet" + ); + } + //console.log('all', tun.data.byteLength, 'bytes are equal'); + //console.log('src:', tun.family, tun.address + ':' + tun.port + ':' + tun.serviceport); + //console.log('dst:', 'IPv4 127.0.0.1:' + port); - if (!clients[id]) { - clients[id] = true; - if (!servername) { - throw new Error("no servername found for '" + id + "'"); - } - //console.log("servername: '" + servername + "'", tun.name); - } + if (!clients[id]) { + clients[id] = true; + if (!servername) { + throw new Error("no servername found for '" + id + "'"); + } + //console.log("servername: '" + servername + "'", tun.name); + } - count += 1; - } -, onerror: function () { - throw new Error("Did not expect onerror"); - } -, onend: function () { - console.info('[onEnd]'); - count += 1; - } + count += 1; + }, + onerror: function() { + throw new Error('Did not expect onerror'); + }, + onend: function() { + console.info('[onEnd]'); + count += 1; + } }); var packts, packed; @@ -93,16 +125,16 @@ packts.push(packer.packHeader(getAddress(), null, 'end')); packed = Buffer.concat(packts); if (!packed.equals(buf)) { - console.error(""); - console.error(buf.toString('hex') === packed.toString('hex')); - console.error(""); - console.error("auto-packed:"); - console.error(packed.toString('hex'), packed.byteLength); - console.error(""); - console.error("hand-packed:"); - console.error(buf.toString('hex'), buf.byteLength); - console.error(""); - throw new Error("packer (new) did not pack as expected"); + console.error(''); + console.error(buf.toString('hex') === packed.toString('hex')); + console.error(''); + console.error('auto-packed:'); + console.error(packed.toString('hex'), packed.byteLength); + console.error(''); + console.error('hand-packed:'); + console.error(buf.toString('hex'), buf.byteLength); + console.error(''); + throw new Error('packer (new) did not pack as expected'); } packts = []; @@ -126,27 +158,26 @@ packed = Buffer.concat(packts); // maching a few things on either side. // // Only 6 bytes are changed - two 1 => 0, four ' ' => '' -var hex = packed.toString('hex') - //.replace(/2c313939/, '2c30') - .replace(/32312c312c636f/, '32312c302c636f') - .replace(/3332312c312c656e64/, '3332312c302c656e64') - .replace(/7320/, '73') - .replace(/20$/, '') - ; +var hex = packed + .toString('hex') + //.replace(/2c313939/, '2c30') + .replace(/32312c312c636f/, '32312c302c636f') + .replace(/3332312c312c656e64/, '3332312c302c656e64') + .replace(/7320/, '73') + .replace(/20$/, ''); if (hex !== buf.toString('hex')) { - console.error(""); - console.error(buf.toString('hex') === hex); - console.error(""); - console.error("auto-packed:"); - console.error(hex, packed.byteLength); - console.error(""); - console.error("hand-packed:"); - console.error(buf.toString('hex'), buf.byteLength); - console.error(""); - throw new Error("packer (old) did not pack as expected"); + console.error(''); + console.error(buf.toString('hex') === hex); + console.error(''); + console.error('auto-packed:'); + console.error(hex, packed.byteLength); + console.error(''); + console.error('hand-packed:'); + console.error(buf.toString('hex'), buf.byteLength); + console.error(''); + throw new Error('packer (old) did not pack as expected'); } - console.info(''); // full message in one go @@ -156,16 +187,14 @@ clients = {}; machine.fns.addChunk(buf); console.info(''); - // messages one byte at a time console.info('[BYTE-BY-BYTE BUFFER]', 1); clients = {}; -buf.forEach(function (byte) { - machine.fns.addChunk(Buffer.from([ byte ])); +buf.forEach(function(byte) { + machine.fns.addChunk(Buffer.from([byte])); }); console.info(''); - // split messages in overlapping thirds // 0-2 (2) // 2-24 (22) @@ -173,26 +202,27 @@ console.info(''); // 223-225 (2) // 225-247 (22) // 247-446 (199) -buf = Buffer.concat([ buf, buf ]); +buf = Buffer.concat([buf, buf]); console.info('[OVERLAPPING BUFFERS]', buf.length); clients = {}; -[ buf.slice(0, 7) // version + header -, buf.slice(7, 14) // header -, buf.slice(14, 21) // header -, buf.slice(21, 28) // header + body -, buf.slice(28, 217) // body -, buf.slice(217, 224) // body + version -, buf.slice(224, 238) // version + header -, buf.slice(238, buf.byteLength) // header + body -].forEach(function (buf) { - machine.fns.addChunk(Buffer.from(buf)); +[ + buf.slice(0, 7), // version + header + buf.slice(7, 14), // header + buf.slice(14, 21), // header + buf.slice(21, 28), // header + body + buf.slice(28, 217), // body + buf.slice(217, 224), // body + version + buf.slice(224, 238), // version + header + buf.slice(238, buf.byteLength) // header + body +].forEach(function(buf) { + machine.fns.addChunk(Buffer.from(buf)); }); console.info(''); -process.on('exit', function () { - if (count !== 12) { - throw new Error("should have delivered 12 messages, not " + count); - } - console.info('TESTS PASS'); - console.info(''); +process.on('exit', function() { + if (count !== 12) { + throw new Error('should have delivered 12 messages, not ' + count); + } + console.info('TESTS PASS'); + console.info(''); });