Node.js, Proxy HTTPS traffic without re-signing - node.js

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;

Related

How do I make sure socket.id is the same on the server and the client after a page reload?

I am writing a Proof Of Concept (for at least 2 months now) that uses Node cluster (workers), Redis and socket.io.
Socket.io is not in use for chat in this instance - just back to front communication. Ajax is not an option.
I am using pub/sub for redis and have that piece working (I think). At least the values returned from pubClient.get('key') are correct.
When I make a request from the front end and do not navigate or reload the page in any way, things work perfectly - I can make 10 requests and 10 responses are received.
Conversely, when I navigate, the same is not true - and I need to deliver the results no matter how much someone navigates on the front end.
It seems there is a disconnect after a reload. In both consoles - Dev Tools and node js, the socket ids are the same. I'm really scratching my head on this one!
Any help out there?
So, for some mainly socket.io code:
CLIENT:
socket = io('https://' + location.hostname + ':4444/', {
transports: ['websocket', 'polling'],
secure: true,
});
socket.on('download', function(data){// after reload, this never hits
console.log('DOWNLOAD '+ data.download);
});
var pkgs = ['y14Vfk617n6j', 'My77gWYmBLxT', 'IYd6dL9UoXkx'];
if(pkgs.length > 0){
for(var i = 0; i < pkgs.length; i++){
socket.emit('get-request', pkgs[i]);
}
}
SERVER:
var cluster = require('cluster');
var express = require('express');
var numCPUs = require('os').cpus().length;
const { setupMaster, setupWorker } = require("#socket.io/sticky");
const { createAdapter, setupPrimary } = require("#socket.io/cluster-adapter");
var app = express();
const https = require('https');
const { Server } = require("socket.io");
const Redis = require("ioredis");
const sock_nodes = [
{port: 6379, host: '192.168.0.41'},
{port: 6380, host: '192.168.0.34'},
{port: 6381, host: '192.168.0.35'},
{port: 6379, host: '192.168.0.34'},
{port: 6380, host: '192.168.0.35'},
{port: 6381, host: '192.168.0.41'}
];
const port = 4444;
const httpServer = https.createServer(options, app);
const io = new Server(httpServer, {maxHttpBufferSize: 10240000});
const pubClient = new Redis.Cluster(sock_nodes, {
redisOptions: {
password: 'my secret!'
}
});
const subClient = pubClient.duplicate(); // I am not actually using this - should I be?
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
// Create a worker
cluster.fork();
}
cluster.on("exit", (worker) => {
console.log(`Worker PID ${worker.process.pid} died`);
var w = cluster.fork();
console.log('WORKER %d died (%s). restarting...', worker.process.pid, worker.state);
w.on('message', function(msg){
console.log("Message Received : " , msg);
});
});
} else {
app.use((req, res, next) => {
var reqip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
//~ console.log(reqip, md5(reqip));
var sess = parseCookies(req, 'session_state');
if(!sess){
res.cookie('session_state', md5(reqip));
}
next();
});
app.get('/', (req, res) => {
getSession(req, res, function(sess){
getPub('currSockets', sess, function(err, socket){
res.render("pages/shared/index", {'ns': sess, 'socket': socket});
});
});
});
});
app.get('/start', function(req, res){
getSession(req, res, function(sess){
getPub('currSockets', sess, function(err, socket){
res.render("pages/shared/start", {'ns': sess, 'socket': socket});
});
});
});
io.on('connection', function (socket) {
var currUser = parseCookies(socket.request, 'session_state');
socket.join(currUser);
getPub('currSockets', currUser, function(err, currSockets){
if (currSockets) {
currSockets = JSON.parse(currSockets);
if (currSockets[currUser]) {
if (currSockets[currUser].stream) {
currSockets[currUser].sock = socket.id;
setCurrSockets(currSockets, currUser, null, function(cSocks){
});
}
}
}
});
socket.on('get-request', function(data){ // can be one or many requests
// there is a similar, currently irrelevant, socket.on('new-request') that is left out here
if(data){
getPub('currSockets', currUser, function(err, currSockets){
currSockets = JSON.parse(currSockets);
if(currSockets){
if(currUser){
if(currSockets[currUser]){
if(currSockets[currUser].stream){
var str = Object.keys(currSockets[currUser].stream);
for(var i = 0; i < str.length; i++){
if(str[i] !== 'sock'){
if(!currSockets[currUser].stream[str[i]]){
delete currSockets[currUser].stream[str[i]];
setCurrSockets(currSockets, currUser, null, function(cSocks){
checkCurrSockets(currUser, data, socket);
});
}
}
}
}
}
}
}
});
}
});
});
httpServer.listen(port, () => {
logs(__line__, `Worker ${process.pid} listening on ${port}`);
});
}
function existsPub(key, cb){
return pubClient.exists(key, cb);
}
function setPub(key, val, cb){
if(val === JSON.stringify({})){
return pubClient.get(key, cb);
}
return pubClient.set(key, val, cb);
}
function getPub(key, currUser, cb){
existsPub(key, function(err, reply){
if(reply === 1){
return pubClient.get(key, cb);// always getting an old socket.id
}
});
}
// Here is the piece that doesn't work after reloading the page
function ioEmit (currSock, target, payload) {
io.to(currSock).emit(target, payload); // doesn't work after page reload
}
// end piece where after reload does not work
getPub('currSockets', currUser, function(err, currSockets){
if( currSockets){
currSockets = JSON.parse(currSockets);
ioEmit(currUser, 'download', {'download': currSockets[currUser].stream[data]);
}
});
function parseCookies (req, name) {
var list = {}, rc;
rc && rc.split(';').forEach(function( cookie ) {
var parts = cookie.split('=');
list[parts.shift().trim()] = decodeURI(parts.join('='));
});
return list[name];
}
function getSession(req, res, callback) {
var sess = false;
if(req.headers) {// handle req
var reqip = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
if(req.headers.cookie){
sess = req.headers.cookie.split('=')[1].split(';')[0];
} else {
res.cookie('session_state', md5(reqip));
}
return callback(sess);
} else if(req.request) {// handle socket
//~ console.log('req.request.headers.cookie', req.request.headers.cookie.split('=')[1]);
if(req.request.headers.cookie){
sess = req.request.headers.cookie.split('=')[1].split(';')[0];
//~ req.emit('join', sess);
//~ callback({[sess]: {'sock': req.id}});
callback(req.id);
}
} else {
return callback(null);
}
}
function setCurrSockets(currSockets, currUser, data, cb){
if(Object.keys(currSockets[currUser].stream).length > 0){
if(data){
if(ready(currSockets, currUser, data)){
delete currSockets[currUser].stream[data];// it appears that setCurrSockets is getting called too soon
}
}
setPub('currSockets', JSON.stringify(currSockets), function(err){
});
if(typeof cb === 'function'){
setTimeout(() => {
getPub('currSockets', currUser, function(err, cSocks){
cb(cSocks);// updated callback to return cSocks
}, 2000);
});
}
} else {
currSockets[currUser].stream = {};
setPub('currSockets', JSON.stringify(currSockets), function(err){
if(err){
} else {
if(typeof cb === 'function'){
cb(currSockets);// updated callback to return cSocks
}
}
});
}
}
figured this out. The problem was in here:
for(var i = 0; i < str.length; i++){
if(str[i] !== 'sock'){
>>>> if(!currSockets[currUser].stream[str[i]]){ // never true
// delete currSockets[currUser].stream[str[i]];
setCurrSockets(currSockets, currUser, null, function(cSocks){
checkCurrSockets(currUser, data, socket);
});
}
}
}
so I commented the for loop and kept the setCurrSockets part and it works.
Just thought I would share, in case someone else tries to use redis, node cluster and socket.io together. As #jfreind00 said, you should use an authentication system with a randomly gen'd string for storing cookies.

how to create an http reverse proxy and transform the stream with node js

I have this working reverse http proxy, but when I add a transform stream and try to pipe it between, nothing seems to be streamed in any direction.
Here's the code:
const http = require('http')
const {Transform} = require('stream')
const onRequest = (clientRequest, clientResponse) => {
const options = {
hostname: 'test-host',
port: 80,
path: clientRequest.url,
method: clientRequest.method,
headers: clientRequest.headers
}
const proxy = http.request(options, (serverResponse) => {
serverResponse.pipe(myTransform, {end: true})
myTransform.pipe(clientResponse, {end: true})
})
clientRequest.pipe(myTransform, {end: true})
myTransform.pipe(proxy, {end: true})
}
const myTransform = new Transform({
transform(data, encoding, callback) {
console.log(data)
callback(null, data)
}
})
http.createServer(onRequest).listen(5050)
Here's a reduced version of a toy proxy I wrote.
It is able to work with body parser (this version is JSON-only) and transform both the request and the response.
The downside is that it's not very efficient for large requests or responses, because it reads the entire bodies of both the request and response before re-streaming them.
const express = require('express');
const bodyParser = require('body-parser');
const proxy = require('http-proxy-middleware');
const httpStatus = require('http-status-codes');
const app = express();
app.use(bodyParser.json());
app.use(function (req, res, next) {
console.log("this is where you transform the request");
});
function restreamReq(proxyReq, req, res) {
if (!req.body) {
return;
}
const contentType = req.get('content-type');
if (contentType) {
proxyReq.setHeader('content-type', contentType);
}
const contentLength = req.get('content-length');
if (contentLength) {
const bodyData = JSON.stringify(req.body);
const bufferLength = Buffer.byteLength(bodyData);
if (bufferLength != contentLength) {
console.warn(`buffer length = ${bufferLength}, content length = ${contentLength}`);
proxyReq.setHeader('content-length', bufferLength);
}
proxyReq.write(bodyData);
}
}
function restreamRes(proxyRes, req, res) {
res.status(proxyRes.statusCode);
for (const key of Object.keys(proxyRes.headers)) {
let rawValue = proxyRes.headers[key];
if (!Array.isArray(rawValue)) {
rawValue = [ rawValue ];
}
for (const value of rawValue) {
res.set(key, value);
}
}
console.log("this is where you transform the response");
let body = new Buffer('');
const bodyPromise = new Promise(function(resolve, reject) {
proxyRes.on('data', (data) => body = Buffer.concat([body, data]));
proxyRes.on('end', () => resolve());
proxyRes.on('error', (err) => reject(err));
});
bodyPromise
.then(() => res.end(body))
.catch(err => res.status(httpStatus.INTERNAL_SERVER_ERROR).end());
}
app.use(proxy({
target: 'http://localhost:8080',
changeOrigin: true,
xfwd: false,
preserveHeaderKeyCase: true,
selfHandleResponse: true,
onProxyReq: restreamReq,
onProxyRes: restreamRes,
}));
const server = app.listen(process.env.PORT || 8081);
process.on('SIGINT', function() {
server.close();
process.exit();
});

Chunk/Stream API data using Node.js

We have requirement where we need to write a node application which can read URL of image from database (approx more than million). Use image-size npm package to retrieve image meta data like height, width. Here should be an API which can list out result.
I am able to console log data but when i convert it to API, i need to chunk data so it can start appearing on browser and i'm unable to do that and need help. Here is my code
var express = require('express');
var url = require('url');
var http = require('http');
var sizeOf = require('image-size');
const sql = require('mssql');
var app = express();
var port = process.env.PORT || 3000;
const hostname = 'localhost';
var config1 = {
user: '*********',
password: '*********',
server: '*********',
database: '*******',
port: 1433,
debug: true,
options: {
encrypt: false // Use this if you're on Windows Azure
}
};
app.get('/', function(req, res){
//res.writeHead(200, { 'Content-Type': 'application/json' });
var finalResult = [];
sql.close();
sql.connect(config1, function (err) {
if (err) console.log(err);
const request = new sql.Request()
var myQuery = `select imagename from media`;
request.stream = true;
request.query(myQuery);
request.on('row', row => {
//console.log('Image : ' + row.ImageUrl);
if (row.ImageUrl != ''){
if (row.ImageUrl.indexOf('http') < 0)
row.ImageUrl = "http:" + row.ImageUrl;
var options = url.parse(row.ImageUrl);
http.get(options, function (response) {
if (response.statusCode == 200)
{
var chunks = [];
response.on('data', function (chunk) {
chunks.push(chunk);
}).on('end', function() {
var buffer = Buffer.concat(chunks);
//console.log(options.href);
//console.log(sizeOf(buffer).height);
var result = {};
result.MaskUrl = row.MaskUrl;
result.ImageUrl = options.href;
result.Height = sizeOf(buffer).height;
result.Width = sizeOf(buffer).width;
result.statusCode = 200;
finalResult.push(result);
//console.log(result);
console.log(finalResult);
res.write(result, function(){
res.end();
});
});
}
else
{
var result = {};
result.MaskUrl = row.MaskUrl;
result.ImageUrl = options.href;
result.Height = 0;
result.Width = 0;
result.statusCode = response.statusCode;
finalResult.push(result);
console.log(result);
res.write(result, function(){
res.end();
});
}
});
}
})
request.on('error', err => {
console.log ('Error for ' + row.ImageUrl );
})
request.on('done', err => {
console.log('Last Time' + finalResult.length);
})
// request.query(myQuery,(err,result) =>{
// console.log(result);
// });
});
console.log('Last Time' + finalResult.length);
res.send(finalResult);
});
app.listen(port, hostname, function(){
console.log('ImageSize running on PORT: ' + port);
});
I tried res.write, res.end without any success.
The probable reason for your problem is that here:
res.write(result, function(){
res.end();
});
You end and close the request just after the first image is read.
I would rewrite the code a little and use some functional framework, like scramjet, to stream the data straight from the DB. As Nicholas pointed out it's not super easy to run your code so I'm writing blindly - but if you fix any of my obvious error this should just work:
First:
npm install scramjet JSONStream node-fetch
Next, try this code:
var express = require('express');
var sizeOf = require('image-size');
const sql = require('mssql');
var app = express();
var port = process.env.PORT || 3000;
const hostname = 'localhost';
const {DataStream} = require('scramjet');
const fetch = require('node-fetch');
const JSONStream = require('JSONStream');
var config1 = {
user: '*********',
password: '*********',
server: '*********',
database: '*******',
port: 1433,
debug: true,
options: {
encrypt: false // Use this if you're on Windows Azure
}
};
app.get('/', function(req, res, next){
// you should consider not doing these two lines on each request,
// but I don't want to mess you code...
sql.close();
sql.connect(config1, function (err) {
if (err) next(err);
res.writeHead(200, { 'Content-Type': 'application/json' });
const request = new sql.Request();
var myQuery = `select imagename from media`;
request.stream = true;
request.query(myQuery);
const stream = new DataStream();
request.on('row', row => stream.write(row));
stream.filter(
row => row.ImageUrl !== ''
)
.map(
async row => {
if (row.ImageUrl.indexOf('http') !== 0) // url must start with http.
row.ImageUrl = "http:" + row.ImageUrl;
const response = await fetch(row.ImageUrl);
let size = {width:0, height:0};
if (response.status === 200) {
const buffer = await response.buffer();
size = sizeOf(buffer);
}
return {
MaskUrl: row.MaskUrl,
ImageUrl: row.ImageUrl,
Height: size.height,
Width: size.width,
statusCode: response.status
};
}
)
.pipe(
JSONStream.stringify()
).pipe(
res
);
request.on('error', () => {
res.writeHead(500, { 'Content-Type': 'application/json' });
stream.end("{error:true}");
});
request.on('done', () => stream.end());
});
});
app.listen(port, hostname, function(){
console.log('ImageSize running on PORT: ' + port);
});

node crashing with http://www.nearby.org.uk/api/

os2stl error when submitting postcode, how do i check what im getting back to start with?
getting error http://i.imgur.com/oGx4kOL
code at http://pastebin.com/jvFiH8sP
the https proxy thing is just this
http.request = function (options, callback) {
var __options = options;
__options.path = 'http://' + options.host + options.path;
__options.host = proxy.host;
__options.port = proxy.port;
if (_debug) {
console.log('=== http-proxy.js begin debug ===');
console.log(JSON.stringify(__options, null, 2));
console.log('=== http-proxy.js end debug ===');
}
var req = __request(__options, function (res) {
callback(res);
});
return req;
};
module.exports = function (host, port, debug) {
proxy.host = host;
proxy.port = port;
_debug = debug || false;
};

Implementing STARTTLS in a protocol in NodeJS

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);
});

Resources