Implementing STARTTLS in a protocol in NodeJS - node.js

I'm trying to add a STARTTLS upgrade to an existing protocol (which currently works in plaintext).
As a start, I'm using a simple line-based echoing server (it's a horrible kludge with no error handling or processing of packets into lines - but it usually just works as the console sends a line-at-a-time to stdin).
I think my server is right, but both ends exit with identical errors when I type starttls:
events.js:72
throw er; // Unhandled 'error' event
^
Error: 139652888721216:error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol:../deps/openssl/openssl/ssl/s23_clnt.c:766:
at SlabBuffer.use (tls.js:232:18)
at CleartextStream.read [as _read] (tls.js:450:29)
at CleartextStream.Readable.read (_stream_readable.js:320:10)
at EncryptedStream.write [as _write] (tls.js:366:25)
at doWrite (_stream_writable.js:221:10)
at writeOrBuffer (_stream_writable.js:211:5)
at EncryptedStream.Writable.write (_stream_writable.js:180:11)
at Socket.ondata (stream.js:51:26)
at Socket.EventEmitter.emit (events.js:95:17)
at Socket.<anonymous> (_stream_readable.js:746:14)
Have I completely misunderstood how to do an upgrade on the client side?
Currently, I'm using the same method to add TLS-ness to plain streams at each end. This feels wrong, as both client and server will be trying to play the same role in the negotiation.
tlsserver.js:
r tls = require('tls');
var net = require('net');
var fs = require('fs');
var options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// This is necessary only if using the client certificate authentication.
requestCert: true,
// This is necessary only if the client uses the self-signed certificate.
ca: [ fs.readFileSync('client-cert.pem') ],
rejectUnauthorized: false
};
var server = net.createServer(function(socket) {
socket.setEncoding('utf8');
socket.on('data', function(data) {
console.log('plain data: ', data);
// FIXME: this is not robust, it should be processing the stream into lines
if (data.substr(0, 8) === 'starttls') {
console.log('server starting TLS');
//socket.write('server starting TLS');
socket.removeAllListeners('data');
options.socket = socket;
sec_socket = tls.connect(options, (function() {
sec_socket.on('data', function() {
console.log('secure data: ', data);
});
return callback(null, true);
}).bind(this));
} else {
console.log('plain data', data);
}
});
});
server.listen(9999, function() {
console.log('server bound');
});
client.js:
var tls = require('tls');
var fs = require('fs');
var net = require('net');
var options = {
// These are necessary only if using the client certificate authentication
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),
// This is necessary only if the server uses the self-signed certificate
ca: [ fs.readFileSync('server-cert.pem') ],
rejectUnauthorized: false
};
var socket = new net.Socket();
var sec_socket = undefined;
socket.setEncoding('utf8');
socket.on('data', function(data) {
console.log('plain data:', data);
});
socket.connect(9999, function() {
process.stdin.setEncoding('utf8');
process.stdin.on('data', function(data) {
if (!sec_socket) {
console.log('sending plain:', data);
socket.write(data);
} else {
console.log('sending secure:', data);
sec_socket.write(data);
}
if (data.substr(0, 8) === 'starttls') {
console.log('client starting tls');
socket.removeAllListeners('data');
options.socket = socket;
sec_socket = tls.connect(options, (function() {
sec_socket.on('data', function() {
console.log('secure data: ', data);
});
return callback(null, true);
}).bind(this));
}
});
});
Got it working, thanks to Matt Seargeant's answer. My code now looks like:
server.js:
var ts = require('./tls_socket');
var fs = require('fs');
var options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
// This is necessary only if using the client certificate authentication.
requestCert: false,
// This is necessary only if the client uses the self-signed certificate.
ca: [ fs.readFileSync('client-cert.pem') ],
rejectUnauthorized: false
};
var server = ts.createServer(function(socket) {
console.log('connected');
socket.on('data', function(data) {
console.log('data', data);
if (data.length === 9) {
console.log('upgrading to TLS');
socket.upgrade(options, function() {
console.log('upgraded to TLS');
});
}
});
});
server.listen(9999);
client.js:
var ts = require('./tls_socket');
var fs = require('fs');
var crypto = require('crypto');
var options = {
// These are necessary only if using the client certificate authentication
key: fs.readFileSync('client-key.pem'),
cert: fs.readFileSync('client-cert.pem'),
// This is necessary only if the server uses the self-signed certificate
ca: [ fs.readFileSync('server-cert.pem') ],
rejectUnauthorized: false
};
var socket = ts.connect(9999, 'localhost', function() {
console.log('secured');
});
process.stdin.on('data', function(data) {
console.log('sending:', data);
socket.write(data);
if (data.length === 9) {
socket.upgrade(options);
}
});
tls_socket.js:
"use strict";
/*----------------------------------------------------------------------------------------------*/
/* Obtained and modified from http://js.5sh.net/starttls.js on 8/18/2011. */
/*----------------------------------------------------------------------------------------------*/
var tls = require('tls');
var crypto = require('crypto');
var util = require('util');
var net = require('net');
var stream = require('stream');
var SSL_OP_ALL = require('constants').SSL_OP_ALL;
// provides a common socket for attaching
// and detaching from either main socket, or crypto socket
function pluggableStream(socket) {
stream.Stream.call(this);
this.readable = this.writable = true;
this._timeout = 0;
this._keepalive = false;
this._writeState = true;
this._pending = [];
this._pendingCallbacks = [];
if (socket)
this.attach(socket);
}
util.inherits(pluggableStream, stream.Stream);
pluggableStream.prototype.pause = function () {
if (this.targetsocket.pause) {
this.targetsocket.pause();
this.readable = false;
}
}
pluggableStream.prototype.resume = function () {
if (this.targetsocket.resume) {
this.readable = true;
this.targetsocket.resume();
}
}
pluggableStream.prototype.attach = function (socket) {
var self = this;
self.targetsocket = socket;
self.targetsocket.on('data', function (data) {
self.emit('data', data);
});
self.targetsocket.on('connect', function (a, b) {
self.emit('connect', a, b);
});
self.targetsocket.on('secureConnection', function (a, b) {
self.emit('secureConnection', a, b);
self.emit('secure', a, b);
});
self.targetsocket.on('secure', function (a, b) {
self.emit('secureConnection', a, b);
self.emit('secure', a, b);
});
self.targetsocket.on('end', function () {
self.writable = self.targetsocket.writable;
self.emit('end');
});
self.targetsocket.on('close', function (had_error) {
self.writable = self.targetsocket.writable;
self.emit('close', had_error);
});
self.targetsocket.on('drain', function () {
self.emit('drain');
});
self.targetsocket.on('error', function (exception) {
self.writable = self.targetsocket.writable;
self.emit('error', exception);
});
self.targetsocket.on('timeout', function () {
self.emit('timeout');
});
if (self.targetsocket.remotePort) {
self.remotePort = self.targetsocket.remotePort;
}
if (self.targetsocket.remoteAddress) {
self.remoteAddress = self.targetsocket.remoteAddress;
}
};
pluggableStream.prototype.clean = function (data) {
if (this.targetsocket && this.targetsocket.removeAllListeners) {
this.targetsocket.removeAllListeners('data');
this.targetsocket.removeAllListeners('secureConnection');
this.targetsocket.removeAllListeners('secure');
this.targetsocket.removeAllListeners('end');
this.targetsocket.removeAllListeners('close');
this.targetsocket.removeAllListeners('error');
this.targetsocket.removeAllListeners('drain');
}
this.targetsocket = {};
this.targetsocket.write = function () {};
};
pluggableStream.prototype.write = function (data, encoding, callback) {
if (this.targetsocket.write) {
return this.targetsocket.write(data, encoding, callback);
}
return false;
};
pluggableStream.prototype.end = function (data, encoding) {
if (this.targetsocket.end) {
return this.targetsocket.end(data, encoding);
}
}
pluggableStream.prototype.destroySoon = function () {
if (this.targetsocket.destroySoon) {
return this.targetsocket.destroySoon();
}
}
pluggableStream.prototype.destroy = function () {
if (this.targetsocket.destroy) {
return this.targetsocket.destroy();
}
}
pluggableStream.prototype.setKeepAlive = function (bool) {
this._keepalive = bool;
return this.targetsocket.setKeepAlive(bool);
};
pluggableStream.prototype.setNoDelay = function (/* true||false */) {
};
pluggableStream.prototype.setTimeout = function (timeout) {
this._timeout = timeout;
return this.targetsocket.setTimeout(timeout);
};
function pipe(pair, socket) {
pair.encrypted.pipe(socket);
socket.pipe(pair.encrypted);
pair.fd = socket.fd;
var cleartext = pair.cleartext;
cleartext.socket = socket;
cleartext.encrypted = pair.encrypted;
cleartext.authorized = false;
function onerror(e) {
if (cleartext._controlReleased) {
cleartext.emit('error', e);
}
}
function onclose() {
socket.removeListener('error', onerror);
socket.removeListener('close', onclose);
}
socket.on('error', onerror);
socket.on('close', onclose);
return cleartext;
}
function createServer(cb) {
var serv = net.createServer(function (cryptoSocket) {
var socket = new pluggableStream(cryptoSocket);
socket.upgrade = function (options, cb) {
console.log("Upgrading to TLS");
socket.clean();
cryptoSocket.removeAllListeners('data');
// Set SSL_OP_ALL for maximum compatibility with broken clients
// See http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html
if (!options) options = {};
// TODO: bug in Node means we can't do this until it's fixed
// options.secureOptions = SSL_OP_ALL;
var sslcontext = crypto.createCredentials(options);
var pair = tls.createSecurePair(sslcontext, true, true, false);
var cleartext = pipe(pair, cryptoSocket);
pair.on('error', function(exception) {
socket.emit('error', exception);
});
pair.on('secure', function() {
var verifyError = (pair.ssl || pair._ssl).verifyError();
console.log("TLS secured.");
if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
cleartext.authorized = true;
}
var cert = pair.cleartext.getPeerCertificate();
if (pair.cleartext.getCipher) {
var cipher = pair.cleartext.getCipher();
}
socket.emit('secure');
if (cb) cb(cleartext.authorized, verifyError, cert, cipher);
});
cleartext._controlReleased = true;
socket.cleartext = cleartext;
if (socket._timeout) {
cleartext.setTimeout(socket._timeout);
}
cleartext.setKeepAlive(socket._keepalive);
socket.attach(socket.cleartext);
};
cb(socket);
});
return serv;
}
if (require('semver').gt(process.version, '0.7.0')) {
var _net_connect = function (options) {
return net.connect(options);
}
}
else {
var _net_connect = function (options) {
return net.connect(options.port, options.host);
}
}
function connect(port, host, cb) {
var options = {};
if (typeof port === 'object') {
options = port;
cb = host;
}
else {
options.port = port;
options.host = host;
}
var cryptoSocket = _net_connect(options);
var socket = new pluggableStream(cryptoSocket);
socket.upgrade = function (options) {
socket.clean();
cryptoSocket.removeAllListeners('data');
// Set SSL_OP_ALL for maximum compatibility with broken servers
// See http://www.openssl.org/docs/ssl/SSL_CTX_set_options.html
if (!options) options = {};
// TODO: bug in Node means we can't do this until it's fixed
// options.secureOptions = SSL_OP_ALL;
var sslcontext = crypto.createCredentials(options);
var pair = tls.createSecurePair(sslcontext, false);
socket.pair = pair;
var cleartext = pipe(pair, cryptoSocket);
pair.on('error', function(exception) {
socket.emit('error', exception);
});
pair.on('secure', function() {
var verifyError = (pair.ssl || pair._ssl).verifyError();
console.log("client TLS secured.");
if (verifyError) {
cleartext.authorized = false;
cleartext.authorizationError = verifyError;
} else {
cleartext.authorized = true;
}
if (cb) cb();
socket.emit('secure');
});
cleartext._controlReleased = true;
socket.cleartext = cleartext;
if (socket._timeout) {
cleartext.setTimeout(socket._timeout);
}
cleartext.setKeepAlive(socket._keepalive);
socket.attach(socket.cleartext);
console.log("client TLS upgrade in progress, awaiting secured.");
};
return (socket);
}
exports.connect = connect;
exports.createConnection = connect;
exports.Server = createServer;
exports.createServer = createServer;

tls.connect() doesn't support the server doing the upgrade unfortunately.
You have to use code similar to how Haraka does it - basically creating your own shim using a SecurePair.
See here for the code we use: https://github.com/baudehlo/Haraka/blob/master/tls_socket.js#L171

STARTTLS Client-Server is trivial to implement in node, but it be a little buggy so, we need to go around.
For Server of STARTTLS we need do it:
// sock come from: net.createServer(function(sock) { ... });
sock.removeAllListeners('data');
sock.removeAllListeners('error');
sock.write('220 Go ahead' + CRLF);
sock = new tls.TLSSocket(sock, { secureContext : tls.createSecureContext({ key: cfg.stls.key, cert: cfg.stls.cert }), rejectUnauthorized: false, isServer: true });
sock.setEncoding('utf8');
// 'secureConnect' event is buggy :/ we need to use 'secure' here.
sock.on('secure', function() {
// STARTTLS is done here. sock is a secure socket in server side.
sock.on('error', parseError);
sock.on('data', parseData);
});
For Client of STARTTLS we need do it:
// sock come from: net.connect(cfg.port, curr.exchange);
// here we already read the '220 Go ahead' from the server.
sock.removeAllListeners('data');
sock.removeAllListeners('error');
sock = tls.connect({ socket: sock, secureContext : tls.createSecureContext({ key: cfg.stls.key, cert: cfg.stls.cert }), rejectUnauthorized: false });
sock.on('secureConnect', function() {
// STARTTLS is done here. sock is a secure socket in client side.
sock.on('error', parseError);
sock.on('data', parseData);
// Resend the helo message.
sock.write(helo);
});

Related

Clear the client console in SSH2 for NodeJS?

I am building a command-line game that players will connect to over SSH. I have an SSH2 server script that responds to commands, but I would like to know if it is possible to clear the client console from the server?
I would like to have a cls/clear command that clears the console at a user's request, like the Unix command 'clear'.
Here is my code:
var fs = require('fs');
var crypto = require('crypto');
var inspect = require('util').inspect;
var ssh2 = require('ssh2');
var utils = ssh2.utils;
var allowedUser = Buffer.from('foo');
var allowedPassword = Buffer.from('bar');
var allowedPubKey = utils.parseKey(fs.readFileSync('public.key')); ;
new ssh2.Server({
hostKeys: [{
key: fs.readFileSync('private.key'),
passphrase: '<private key passphrase>'
}
]
}, function (client) {
console.log('Client connected!');
client.on('authentication', function (ctx) {
var user = Buffer.from(ctx.username);
if (user.length !== allowedUser.length
|| !crypto.timingSafeEqual(user, allowedUser)) {
return ctx.reject();
}
switch (ctx.method) {
case 'password':
var password = Buffer.from(ctx.password);
if (password.length !== allowedPassword.length
|| !crypto.timingSafeEqual(password, allowedPassword)) {
return ctx.reject(['password'],false);
}
break;
default:
return ctx.reject(['password'],false);
}
ctx.accept();
}).on('ready', function () {
console.log('Client authenticated!');
client.once('session', function (accept, reject) {
var session = accept();
session.on('shell', (accept, reject) => {
var stream = accept();
stream.write('Welcome to EnLore!\n\r');
stream.on('data', data => {
var args = data.toString().replace("\n","").split(" ");
if (args[0] == '\r') return;
switch(args[0]) {
case 'exit':
stream.exit(0);
stream.end();
stream = undefined;
break;
case 'echo':
stream.write(args[1]);
break;
case 'clear':
// -------------- CLEAR CONSOLE HERE --------------
break;
default:
stream.write('Unknown command');
break;
}
stream.write('\n\r');
});
});
});
}).on('end', function () {
console.log('Client disconnected');
});
}).listen(8080, '0.0.0.0', function () {
console.log('Listening on port ' + this.address().port);
});
EDIT:
I have found that writing \f to the stream will clear the current window, however, it only appends to the console, rather than clearing it

NodeJS Parse raw minecraft packet

I'm trying to create a minecraft proxy server to intercept and read packets between client and server.
I've already read the documentation about minecraft protocol here: https://wiki.vg , but my raw packets and the documentation didn't match...
Can someone explain me how i have to parse my packets to get length, id and data? Thanks ^^
packets examples:
1000d402093132372e302e302e3163dd0210000e4275726e47656d696f7333363433
bb01011035353730626633306336626363343266a20130819f300d06092a864886f70d010101050003818d0030818902818100a6f8df08dce2072565b5beef1bb27ed5c7cd02dd02e165019b9e4c4ae25a51a288c9474c643812036d07dc6715b0fc407824b6af1cb5b3efc1cf8739cb0a1f8e128b432f156ebe443bb69dd85c112d10426ab78ec99b0de8ab85b51140d0f5fc7b2d6b9c466b1272be09bc9551f1a0ab2121a0f35fd6bfc9d62b24cef2bb0e53020301000104051a9b77
8502018001584fe6b9c8388760fc20f4bab8bc3bb2c3026613eaff02d3cda41a541456d945b0e6dbdc62d5396b806e3ba5cda446ac6af681db1c584300fe7317d73a4310aea4f98dbecfcc71a1a0a6880ed0d7c6f7fa1b99c6b003226f38703c07871904574f855474cd41975e0ceccbd99b35a7da26121938af63484a26a8afb3772f947e800187a53861917ac19e8ffa04bb88e69a23a594ac22d268b25782e8de1b8f9dd3d8aff0c5222a286945a0de25c2ec75ee21c7bd49980910f9270d67f4290ae47b0aedc75f4e60406e406827bea309696fd9628aaf33270d7119292c10bcf0bbd955c54d97e57c7514e0080c373f8cad1fd3bbca6c90c6131c1592834446f28a1807
Edit:
my actual code:
const mc = require("minecraft-protocol");
const states = mc.states
const mcData = require('minecraft-data')('1.12.2');
const version = mcData.version
const Cserializer = mc.createSerializer({ state: states.PLAY, isServer: false, version: '1.12.2' })
const Sserializer = mc.createSerializer({ state: states.PLAY, isServer: true, version: '1.12.2' })
//var serialized = Cserializer.createPacketBuffer({ name: 'chat', params: { message: '!p' } })
//console.log("serialisé", serialized);
const Cdeserializer = mc.createDeserializer({ state: states.PLAY, isServer: false, version: '1.12.2' })
const Sdeserializer = mc.createDeserializer({ state: states.PLAY, isServer: true, version: '1.12.2' })
//console.log("deserialisé", Sdeserializer.parsePacketBuffer(Buffer.from("0c010d50455449542046445021 ", "hex")));
var net = require("net");
var fs = require("fs");
if(fs.existsSync("logs.txt"))fs.unlinkSync("logs.txt");
//packet format: [length][id][data...............]
function parsePacket(buffer, server){
let packet;
try {
if(server){
packet = Sdeserializer.parsePacketBuffer(buffer);
}else{
packet = Cdeserializer.parsePacketBuffer(buffer);
}
} catch (error) {
console.log("!");
}
return packet;
}
process.on("uncaughtException", function(error) {
console.error(error);
});
if (process.argv.length != 5) {
console.log("usage: %s <localport> <remotehost> <remoteport>", process.argv[1]);
process.exit();
}
var localport = process.argv[2];
var remotehost = process.argv[3];
var remoteport = process.argv[4];
var remotesocket = null;
var Clocalsocket = null;
var server = net.createServer(function (localsocket) {
Clocalsocket = localsocket;
remotesocket = new net.Socket();
remotesocket.connect(remoteport, remotehost);
localsocket.on('connect', function (data) {
console.log(">>> connection #%d from %s:%d",
server.connections,
localsocket.remoteAddress,
localsocket.remotePort
);
});
localsocket.on('data', function (data) {
let custom = doPacket(data, true);
if(!custom) custom = data;
fs.appendFileSync('logs.txt', '\n>>> remote >>> '+toHexString(data));
let p = parsePacket(data, true);
console.log(p);
if(custom != "cancel"){
var flushed = remotesocket.write(custom);
if (!flushed) {
console.log("remote not flushed; pausing local");
}
}
});
remotesocket.on('data', function(data) {
let custom = doPacket(data, false);
if(!custom) custom = data;
fs.appendFileSync('logs.txt', '\n>>> local >>> '+toHexString(data));
let p = parsePacket(data, false);
console.log(p);
if(custom != "cancel"){
var flushed = localsocket.write(custom);
if (!flushed) {
console.log("local not flushed; pausing remote");
}
}
});
localsocket.on('drain', function() {
console.log("%s:%d - resuming remote",
localsocket.remoteAddress,
localsocket.remotePort
);
remotesocket.resume();
});
remotesocket.on('drain', function() {
console.log("%s:%d - resuming local",
localsocket.remoteAddress,
localsocket.remotePort
);
localsocket.resume();
});
localsocket.on('close', function(had_error) {
console.log("%s:%d - closing remote",
localsocket.remoteAddress,
localsocket.remotePort
);
remotesocket.end();
});
remotesocket.on('close', function(had_error) {
console.log("%s:%d - closing local",
localsocket.remoteAddress,
localsocket.remotePort
);
localsocket.end();
});
});
server.listen(localport);
console.log("redirecting connections from 127.0.0.1:%d to %s:%d", localport, remotehost, remoteport);
function toHexString(byteArray) {
return Array.from(byteArray, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('')
}

Local WebRTC signaling server

I am using firebase signaling server for my webRTC website.
I am informed I can use websocket for implementation of my own local signaling server.
I tried the example on Link and it works. When I replace my firebase code it doesn't work.
Firebase code
var config = {
openSocket: function(config) {
var channel = config.channel || location.href.replace(/\/|:|#|%|\.|\[|\]/g, '');
var socket = new Firebase('https://webrtc.firebaseIO.com/' + channel); //EXTERNAL SERVER need change
socket.channel = channel;
socket.on("child_added", function(data) {
config.onmessage && config.onmessage(data.val());
});
socket.send = function(data) {
this.push(data);
};
config.onopen && setTimeout(config.onopen, 1);
socket.onDisconnect().remove();
return socket;
},
Websocket over nodejs code
var channelName = location.href.replace(/\/|:|#|%|\.|\[|\]/g, ''); // using URL as room-name
var SIGNALING_SERVER = 'ws://172.20.34.132:12034';
connection.openSignalingChannel = function(config) {
config.channel = config.channel || this.channel;
var websocket = new WebSocket(SIGNALING_SERVER);
websocket.channel = config.channel;
websocket.onopen = function() {
websocket.push(JSON.stringify({
open: true,
channel: config.channel
}));
if (config.callback)
config.callback(websocket);
};
websocket.onmessage = function(event) {
config.onmessage(JSON.parse(event.data));
};
websocket.push = websocket.send;
websocket.send = function(data) {
websocket.push(JSON.stringify({
data: data,
channel: config.channel
}));
};
}

Custom TCP-Protocol on node.js

how i can implement a custom protocol on a Node.js NET-Client?
The problem is:
I want to connect to a server. That server has an simple protocol.
Each packet has a length-prefix, it means the first byte say how long the packet is.
How i can implement that?
How i can read for example the first byte to get the packet-length to read the other stuff?
var net = require('net');
var client = new net.Socket();
client.connect(2204, 'myexampledomain.com', function() {
console.log('Connecting...', protocol);
});
client.on('data', function(data) {
console.log('DATA: ' + data);
});
client.on('end', function() {
console.log('end');
});
client.on('timeout', function() {
console.log('timeout');
});
client.on('drain', function() {
console.log('drain');
});
client.on('error', function() {
console.log('error');
});
client.on('close', function() {
console.log('Connection closed');
});
client.on('connect', function() {
console.log('Connect');
client.write("Hello World");
});
You'll have to maintain an internal buffer holding received data and check if it has length bytes before slicing the packet from it. The buffer must be concatenated with received data until it has length bytes and emptied on receiving a full packet. This can be better handled using a Transform stream.
This is what I used in my json-rpc implementation. Each JSON packet is lengthPrefixed. The message doesn't have to be JSON – you can replace the call to this.push(JSON.parse(json.toString()));.
var Transform = require('stream').Transform;
function JsonTransformer(options) {
if (!(this instanceof JsonTransformer)) {
return new JsonTransformer(options);
}
Transform.call(this, {
objectMode: true
});
/*Transform.call(this);
this._readableState.objectMode = false;
this._writableState.objectMode = true;*/
this.buffer = new Buffer(0);
this.lengthPrefix = options.lengthPrefix || 2;
this._readBytes = {
1: 'readUInt8',
2: 'readUInt16BE',
4: 'readUInt32BE'
}[this.lengthPrefix];
}
JsonTransformer.prototype = Object.create(Transform.prototype, {
constructor: {
value: JsonTransformer,
enumerable: false,
writable: false
}
});
function transform() {
var buffer = this.buffer,
lengthPrefix = this.lengthPrefix;
if (buffer.length > lengthPrefix) {
this.bytes = buffer[this._readBytes](0);
if (buffer.length >= this.bytes + lengthPrefix) {
var json = buffer.slice(lengthPrefix, this.bytes + lengthPrefix);
this.buffer = buffer.slice(this.bytes + lengthPrefix);
try {
this.push(JSON.parse(json.toString()));
} catch(err) {
this.emit('parse error', err);
}
transform.call(this);
}
}
}
JsonTransformer.prototype._transform = function(chunk, encoding, next) {
this.buffer = Buffer.concat([this.buffer, chunk]);
transform.call(this);
next();
}
JsonTransformer.prototype._flush = function() {
console.log('Flushed...');
}
var json = new JsonTransformer({lengthPrefix: 2});
var socket = require('net').createServer(function (socket) {
socket.pipe(json).on('data', console.log);
});
socket.listen(3000);
module.exports = JsonTransformer;
json-transformer

Node.js, Proxy HTTPS traffic without re-signing

I'm building a proxy using Node.js to be run on my local machine that will log all domains accessed. Kind of the same way Fiddler works, but my program is more simple, I don't need to look at the data or decrypt anything.
I got this working fine for HTTP but for HTTPS it resigns the traffic with the self-signed certificate provided. This results in that the browser displays a warning. The same thing doesn't happen in fiddler unless you choose to decrypt HTTPS traffic.
So my question is: How do I proxy HTTPS traffic using Node.js so that it is completely transparent for the user?
This is the code I'm using right now, using the Node.js http-proxy. Based on the Github project, proxy-mirror.
var httpProxy = require('http-proxy'),
https = require('https'),
connect = require('connect'),
logger = connect.logger('dev'),
EventEmitter = require('events').EventEmitter,
util = require('util'),
Iconv = require('iconv').Iconv,
convertBuffer = require('./convertBuffer'),
fs = require('fs'),
net = require('net'),
url = require('url'),
path = require('path'),
certDir = path.join(__dirname, '/../cert'),
httpsOpts = {
key: fs.readFileSync(path.join(certDir, 'proxy-mirror.key'), 'utf8'),
cert: fs.readFileSync(path.join(certDir, 'proxy-mirror.crt'), 'utf8')
};
var Session = function (sessionId, req, res) {
EventEmitter.call(this);
var that = this;
this.id = req._uniqueSessionId = res._uniqueSessionId = sessionId;
this.request = req;
this.request.body = {asString: null, asBase64: null};
this.response = res;
this.response.body = {asString: null, asBase64: null};
this.state = 'start';
var hookInResponseWrite = function () {
var response = that.response;
var _write = response.write;
var output = [];
response.write = function (chunk, encoding) {
output.push(chunk);
_write.apply(response, arguments);
};
return output;
}, hookInRequestData = function () {
var request = that.request,
output = [];
request.on('data', function (chunk, encoding) {
output.push(chunk);
});
request.on('end', function () {
var buffersConcatenated = Buffer.concat(output);
request.body.asString = buffersConcatenated.toString();
that.emit('request.end', that);
});
return output;
};
this.response.bodyBuffers = hookInResponseWrite();
this.request.bodyBuffers = hookInRequestData();
this.ended = function () {
var buffersConcatenated = Buffer.concat(this.response.bodyBuffers);
this.response.body.asString = convertBuffer.convertEncodingContentType(buffersConcatenated,this.response.getHeader('content-type') || '');
this.response.body.asBase64 = buffersConcatenated.toString('base64');
this.removeAllListeners();
};
};
util.inherits(Session, EventEmitter);
Session.extractSessionId = function (req) {
return req._uniqueSessionId;
};
var SessionStorage = function () {
var sessionHash = {},
sessionIdCounter = 0,
nextId = function () {
return sessionIdCounter += 1;
};
this.startSession = function (req, res) {
var sessionId = nextId(), session;
sessionHash[sessionId] = session = new Session(sessionId, req, res);
return session;
};
this.popSession = function (req) {
var sessionId = Session.extractSessionId(req),
session = sessionHash[sessionId];
delete sessionHash[sessionId];
return session;
};
};
var ProxyServer = function ProxyServer() {
EventEmitter.call(this);
var proxyPort = 8888,
secureServerPort = 8887,
parseHostHeader = function (headersHost, defaultPort) {
var hostAndPort = headersHost.split(':'),
targetHost = hostAndPort[0],
targetPort = parseInt(hostAndPort[1]) || defaultPort;
return {hostname: targetHost, port: targetPort, host: headersHost};
},
sessionStorage = new SessionStorage(),
adjustRequestUrl = function(req){
if (requestToProxyMirrorWebApp(req)) {
req.url = req.url.replace(/http:\/\/localhost:8889\//, '/');
}
req.url = url.parse(req.url).path;
},
proxyServer = httpProxy.createServer({
changeOrigin: true,
enable: {
xforward: false
}
}, function (req, res, proxy) {
var parsedHostHeader = parseHostHeader(req.headers['host'], 80),
targetHost = parsedHostHeader.hostname,
targetPort = parsedHostHeader.port;
req.originalUrl = req.url;
adjustRequestUrl(req);
logger(req, res, function () {
proxy.proxyRequest(req, res, {
host: targetHost,
port: targetPort
});
})
}),
proxy = proxyServer.proxy,
secureServer = https.createServer(httpsOpts, function (req, res) {
var parsedHostHeader = parseHostHeader(req.headers.host, 443);
// console.log('secure handler ', req.headers);
req.originalUrl = req.url;
if(!req.originalUrl.match(/https/)){
req.originalUrl = 'https://' + parsedHostHeader.host + req.url;
}
adjustRequestUrl(req);
logger(req, res, function () {
proxy.proxyRequest(req, res, {
host: parsedHostHeader.hostname,
port: parsedHostHeader.port,
changeOrigin: true,
target: {
https: true
}
});
});
}),
listening = false,
requestToProxyMirrorWebApp = function (req) {
var matcher = /(proxy-mirror:8889)|(proxy-mirror:35729)/;
return req.url.match(matcher) || (req.originalUrl && req.originalUrl.match(matcher));
};
[secureServer,proxyServer].forEach(function(server){
server.on('upgrade', function (req, socket, head) {
// console.log('upgrade', req.url);
proxy.proxyWebSocketRequest(req, socket, head);
});
});
proxyServer.addListener('connect', function (request, socketRequest, bodyhead) {
//TODO: trying fixing web socket connections to proxy - other web socket connections won't work :(
// console.log('conenct', request.method, request.url, bodyhead);
var targetPort = secureServerPort,
parsedHostHeader = parseHostHeader(request.headers.host);
if(requestToProxyMirrorWebApp(request)){
targetPort = parsedHostHeader.port;
}
var srvSocket = net.connect(targetPort, 'localhost', function () {
socketRequest.write('HTTP/1.1 200 Connection Established\r\n\r\n');
srvSocket.write(bodyhead);
srvSocket.pipe(socketRequest);
socketRequest.pipe(srvSocket);
});
});
this.emitSessionRequestEnd = function (session) {
this.emit('session.request.end', session);
};
this.startSession = function (req, res) {
if (requestToProxyMirrorWebApp(req)) {
return;
}
var session = sessionStorage.startSession(req, res);
this.emit('session.request.start', session);
session.on('request.end', this.emitSessionRequestEnd.bind(this));
};
this.endSession = function (req, res) {
if (requestToProxyMirrorWebApp(req)) {
return;
}
var session = sessionStorage.popSession(req);
if (session) {
session.ended();
this.emit('session.response.end', session);
}
};
this.start = function (done) {
done = done || function () {
};
proxy.on('start', this.startSession.bind(this));
proxy.on('end', this.endSession.bind(this));
proxyServer.listen(proxyPort, function () {
secureServer.listen(secureServerPort, function () {
listening = true;
done();
});
});
};
this.stop = function (done) {
done = done || function () {
};
proxy.removeAllListeners('start');
proxy.removeAllListeners('end');
if (listening) {
secureServer.close(function () {
proxyServer.close(function () {
listening = false;
done();
});
});
}
};
};
util.inherits(ProxyServer, EventEmitter);
module.exports = ProxyServer;

Resources