Communication between client and proxy keeps failing because of self-signed certificate - node.js

I have a proxy application written in Javascript/Node.js that uses the http-mitm-proxy library.
I also have an https server on a separate machine that will serve files (currently it just replies "hello world"). I used a self signed root CA and a self-signed certificate for this (details below).
On a third machine, I try to access the https file server via the proxy. I just use Firefox for this (I've configured the proxy settings with my proxy machine's IP).
What I'm trying to do is intercept the https traffic (i.e. the files) on the proxy and cache it (i.e. decrypt it, store it locally and then pass it on). This is basically a Man-In-The-Middle attack.
The problem is that the proxy and client keep rejecting the self-signed certificate during communication.
I generated the rootCA and self-signed certificate using the steps found here. I can provide the exact values/details, if necessary.
I imported the certificate in Firefox on my client machine and I use them in the proxy app code.
I also added the appropriate entries in the hosts file on both client and proxy machine.
Here is my code:
// HTTPS File Server:
const https = require('https');
const fs = require('fs');
const options = {
key: fs.readFileSync('myupdateproxy.com.key'),
cert: fs.readFileSync('myupdateproxy.com.crt')
};
https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
// proxy server code:
'use strict';
var port = 8081;
var path = require('path');
var Proxy = require('http-mitm-proxy');
var proxy = Proxy();
const fs = require('fs');
var chunks1 = [];
var chunks2 = [];
proxy.onCertificateRequired = function(hostname, callback) {
return callback(null, {
keyFile: path.resolve('myupdateproxy.com.key'),
certFile: path.resolve('myupdateproxy.com.crt')
});
};
proxy.onError(function(ctx, err, errorKind) {
// ctx may be null
var url = (ctx && ctx.clientToProxyRequest) ? ctx.clientToProxyRequest.url : '';
console.error(errorKind + ' on ' + url + ':', err);
});
proxy.onRequest(function(ctx, callback) {
console.log('onRequest');
callback();
});
proxy.onRequestData(function(ctx, chunk, callback) {
console.log('onRequestData');
chunks1.push(chunk);
callback(null, chunk);
});
proxy.onRequestEnd(function(ctx, callback) {
if (ctx.clientToProxyRequest.socket.remoteAddress !== undefined &&
ctx.proxyToServerRequest.socket.remoteAddress !== undefined &&
(Buffer.concat(chunks1)).length > 0)
{
console.log('From: ' + ctx.clientToProxyRequest.socket.remoteAddress);
console.log('To: ' + ctx.proxyToServerRequest.socket.remoteAddress);
console.log('Size: ' + (Buffer.concat(chunks1)).length);
console.log('');
}
chunks1 = [];
callback();
});
proxy.onResponse(function(ctx, callback) {
callback(null);
});
proxy.onResponseData(function(ctx, chunk, callback) {
chunks2.push(chunk);
callback(null, chunk);
});
proxy.onResponseEnd(function(ctx, callback) {
var total_size=(Buffer.concat(chunks2)).length;
if (ctx.serverToProxyResponse.socket.remoteAddress !== undefined &&
ctx.proxyToClientResponse.socket.remoteAddress !== undefined &&
total_size > 0)
{
console.log('From: ' + ctx.serverToProxyResponse.socket.remoteAddress);
console.log('To: ' + ctx.proxyToClientResponse.socket.remoteAddress);
console.log('Size: ' + total_size);
console.log('');
}
console.log((Buffer.concat(chunks2)).toString());
chunks2 = [];
callback();
});
proxy.listen({ port: port, sslCaDir: "/home/user/mycerts/" });
console.log('listening on ' + port);
I keep getting this error in the proxy app:
onRequest
PROXY_TO_SERVER_REQUEST_ERROR on /: Error: self signed certificate
at TLSSocket.onConnectSecure (_tls_wrap.js:1473:34)
at TLSSocket.emit (events.js:311:20)
at TLSSocket._finishInit (_tls_wrap.js:916:8)
at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:686:12) {
code: 'DEPTH_ZERO_SELF_SIGNED_CERT'
}
And this message on the client machine (in Firefox):
PROXY_TO_SERVER_REQUEST_ERROR: Error: self signed certificate
Does anyone know what I'm doing wrong and how I can make this work?
Thanks!

Try to make insecure TLS connection from the proxy to the app - add this snippet to your proxy code:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
Or try to add your custom CA cert to your system (where proxy app is running) CA certs. Exact command depends on used OS.

Related

Apache, SSL, NodeJS, Express, Socket.io, Digital Ocean set up?

I tried setting up some mod_proxy methods (link below) but when active, it gives me a Service Unavailable message, (sorry, not a server/sysadmin guy)
We have a development server without any SSL and it works perfectly.
Our code so far (nodejs/server.js):
var app = require("express")();
var https = require("https");
var io = require("socket.io")(https);
var port = 3000;
var privateKey = fs.readFileSync('/etc/apache2/ssl-certificate/site.key', 'utf8'); // change with your ssl .key file
var certificate = fs.readFileSync('/etc/apache2/ssl-certificate/site.crt', 'utf8'); // change with your ssl .crt file
option = {
key: privateKey,
cert: certificate
}
io.on("connection", function (socket) {
socket.on("message", function (data) {
console.log("message Recieved: " + JSON.stringify(data));
io.emit("conversation:" + data.conversation_id, data);
});
socket.on("chat_attachment", function (data) {
console.log("message Recieved: " + JSON.stringify(data));
io.emit("conversation:" + data.conversation_id, data);
});
});
https.createServer(option, app).listen(port, '0.0.0.0', function () {
console.log("Listening on Port " + port);
});
Same code but without SSL config, works over our dev server with normal HTTP.
I tried to follow recommendations at:
Apache and NodeJS over SSL
Just to guide anyone experiencing this issue, we were able to resolve this issue following #paul's advice (see comments).
Basically we have set up a subdomain (backend.ourdomain.com) which resolves port 3000 and from there we can run nodejs just fine.

nodejs can't work with SSL

Im trying to run nodejs app to work with my php project. the problem is I think with SSL which is enabled in the server.
I have two files that I found in my root directory after SSL install: domain.com.csr and domain.com.key and I tried to combine them to connection while creating https server, but nothing worked for me.
so far I have this code:
var socket = require('socket.io');
var express = require('express');
var http = require('http');
var app = express();
var server = http.createServer(app);
var io = socket.listen(server);
app.get('/test', function(req, res) {
res.send('hello world');
console.log('visited test')
});
io.sockets.on('connection', function (client) {
console.log("New client !");
client.on('message', function (data) {
console.log('Message received ' + data.name + ":" + data.message);
io.sockets.emit('message', {name: data.name, message: data.message});
});
});
server.listen(8080, function () {
console.log('listen me on: 8080');
});
and it works well when I'm trying to visit http://ip:8080/test so it means that node server is working, but when I try to create socket connection on my view file var socket = io.connect('http://ip:8080'); it gives me error:
The page at 'https://www.domain.com/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://ip:8080/socket.io/?EIO=3&transport=polling&t=1446818946199-0'. This request has been blocked; the content must be served over HTTPS.
so the problem is clear enough, but how to deal with it?
also I have tried this connection:
var socket = io.connect('https://www.domain.com:8080');
but the result is 404 GET Error. How to deal with it?
Update
now the part of code I should use, but don't know how to get cert of existing SSL in the server.
var socket = require('socket.io');
var express = require('express');
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('path/to/key.pem'), // dont have
cert: fs.readFileSync('path/to/cert.cert') // dont have
};
var app = express();
var server = https.createServer(options, app);
var io = socket.listen(server);
app.get('/test', function(req, res) {
res.send('hello world');
console.log('visited test')
});
io.sockets.on('connection', function (client) {
console.log("New client !");
client.on('message', function (data) {
console.log('Message received ' + data.name + ":" + data.message);
io.sockets.emit('message', {name: data.name, message: data.message});
});
});
server.listen(443, function () {
console.log('listen me on: 443');
});
I think you need to contact your certificate authority (the organization that issued your first ssl certificate) and get a copy of the certificate (the path/to/key.pem and path/to/cert.cert) or find the existing keys somewhere on your existing server.
If you're running apache, your configuration file will have a section with values for the paths of the .cert and .pem files labeled SSLCertificateFile and SSLCertificateKeyFile, then just update the paths in your node app to point to them. You also have to make sure that your SSL certificate meets the requirements (for example, needs to be Multi-domain if your node app runs on a different domain, or a Wildcard SSL certificate to run your node app on a subdomain).
The domain.com.csr and domain.com.key files you found are the private key and certificate request used to generate your initial SSL certificate and aren't going to do anything to enable SSL on your node app.

Node.JS https server for Webhook gets handshake error

The following NodeJS just handles a webhook coming from another source. I tested it out, it works on http port 80 but the source requires https.
When I opened that port and ran this script on 443, testing it with curl gets the following error. Yet I don't think it should require a certificate should it? How would I even solve this?
curl: (35) SSL peer handshake failed, the server most likely requires a client certificate to connect
Here is the script:
var http = require('https')
var server = http.createServer(function(req, res) {
if (req.method == "POST") {
var str = ''
req.on('data', function(data) {
str += data
})
req.on('end', function() {
var json = JSON.parse(str)
res.end(json.meta.status)
})
}
})
console.log("HTTPS server listening on port 443...")
server.listen(443)
UPDATE:
Here's the latest code. I created a self-signed cert without a passphrase. I get further but I still get an error using curl with the -k option added. I get a certificate verification error without the -k.
Cannot POST /
var https = require('https')
var fs = require('fs')
var express = require('express');
var options = {
key: fs.readFileSync('./server.key'),
cert: fs.readFileSync('./server.cert')
}
var app = express()
var server = https.createServer(options, app, function(req, res) {
if (req.method == "POST") {
var str = ''
req.on('data', function(data) {
str += data
})
req.on('end', function() {
var json = JSON.parse(str)
res.end(json.meta.status)
})
}
})
console.log("HTTPS server listening on port 443...")
server.listen(443)
HTTPS server config always requires SSL certificate. You can generate it using openssl here is in more details.
Then for node server use crypto,fs modules. Detailed config is here.

Making a REST call in Node.js that requires client certificate for authentication

Is there a way to make a rest call that requires a client certificate for authentication through Node.js ?
Yes, you can do that quite simply, here done using a regular https request;
var https = require('https'), // Module for https
fs = require('fs'); // Required to read certs and keys
var options = {
key: fs.readFileSync('ssl/client.key'), // Secret client key
cert: fs.readFileSync('ssl/client.crt'), // Public client key
// rejectUnauthorized: false, // Used for self signed server
host: "rest.localhost", // Server hostname
port: 8443 // Server port
};
callback = function(response) {
var str = '';
response.on('data', function (chunk) {
str += chunk;
});
response.on('end', function () {
console.log(str);
});
}
https.request(options, callback).end();

creating a forward https proxy using http-node-proxy

I am trying to create a forward proxy capable of handling HTTPS websites as well. I am trying to observe and modify traffic for different sites. This is my code which works for http sites but not for https sites.
httpProxy.createServer(function(req, res, next) {
//custom logic
next();
}, function(req, res) {
var proxy = new httpProxy.RoutingProxy();
var buffer = httpProxy.buffer(req);
var urlObj = url.parse(req.url);
req.headers.host = urlObj.host;
req.url = urlObj.path;
console.log(urlObj.protocol);
setTimeout(function() {
proxy.proxyRequest(req, res, {
host: urlObj.host,
port: 80,
buffer: buffer,
}
)}, 5000);
}).listen(9000, function() {
console.log("Waiting for requests...");
});
Thanks for your help guys!
There are https options which must be specified when handling the https traffic. Here is what I am doing in my proxy setup.
var fs = require('fs'),
httpProxy = require('http-proxy');
var proxyTable = {};
proxyTable['domain.com'] = 'localhost:3001';
proxyTable['domain.com/path'] = 'localhost:3002';
proxyTable['sub.domain.com'] = 'localhost:3003';
var httpOptions = {
router: proxyTable
};
var httpsOptions = {
router: proxyTable,
https: {
passphrase: 'xxxxxxx',
key: fs.readFileSync('/path/to/key'),
ca: fs.readFileSync('/path/to/ca'),
cert: fs.readFileSync('/path/to/crt')}
};
httpProxy.createServer(httpOptions).listen(80);
httpProxy.createServer(httpsOptions).listen(443);
The documentation for https is on their github page as well.
https://github.com/nodejitsu/node-http-proxy
If you're just doing a forward proxy there's a few things you'll have to take into account.
A regular request is NOT triggered on a proxy for a HTTPS request - instead you'll see a HTTP CONNECT.
Here's the sequence flow you'll need to handle.
CONNECT event is sent from the browser to the proxy specified in the HTTPS section. You'll catch this here: http://nodejs.org/api/http.html#http_event_connect Note that this comes over the HTTP module, not the HTTPS connection.
You create a new socket connection to the requested domain (or your mapped domain). [srvSocket]
You'll respond back to the CONNECT socket with a 200
You'll write the buffer you received with the CONNECT event to srvSocket, then pipe the two sockets together srvSocket.pipe(socket);
socket.pipe(srvSocket);
Since you're trying to spoof the requested domain locally you'll need a few more things in place
You'll need to generate a root CA.
You will need to import this cert as a trusted authority to your OS
You'll use this cert to create a new key/cert file for the domains you're trying to access
Your mapped hosts will need to respond with the appropriate key/cert file generated in step 3 for EACH domain you are mapping.
https://github.com/substack/bouncy
var bouncy = require('bouncy');
var server = bouncy(function (req, res, bounce) {
if (req.headers.host === 'beep.example.com') {
bounce(8001);
}
else if (req.headers.host === 'boop.example.com') {
bounce(8002);
}
else {
res.statusCode = 404;
res.end('no such host');
}
});
server.listen(8000);
If you specify opts.key and opts.cert, the connection will be set to secure mode using tls. Do this if you want to make an https router.
We can have a middleware as below
request = require("request");
app.use(function (req, res, next) {
request('http://anotherurl.that.serves/the/request').pipe(res);
});
See example https://github.com/manuks/proxy
Basically, underneath the http-proxy npm is some networking libraries Node uses (specifically http://nodejs.org/api/https.html and TLS). Even though my Apache was able to connect me just fine on a self-signed certificate w/o the proxy by accessing it in my browser:
https://localhost:8002
You need to establish a certificate authority to get past the "unable to verify leaf signature" error in Node (I used the SSLCACertificateFile option). Then, you'll get hit with "self_signed_cert_in_chain". This led to some Google results indicating npm abandoned self-signed certificates, but I'm pretty sure this does not regard Node.
What you end up with are some people indicating you use process.env.NODE_TLS_REJECT_UNAUTHORIZED or rejectUnauthorized within your https agent. If you dig through the http-proxy souce, you'll find it accepts an agent option. Use this:
/**
* Module dependencies
*/
// basic includes
express = require('express');
fs = require('fs');
http = require('http');
https = require('https');
httpProxy = require('http-proxy');
require('child_process').spawn(__dirname+'/../../../dependencies/apache/bin/httpd.exe',['-f',__dirname+'/../../../dependencies/apache/conf/httpd-payments.conf']);
var app = module.exports = express();
app.set('port', process.env.PORT || 8001); // we sometimes change the port
// creates an output object for this particular request
//app.use(express.cookieParser(''));
//app.use(express.bodyParser());
//app.use(express.methodOverride());
proxy = httpProxy.createProxyServer();
proxy.on('error', function (err, req, res) {
console.log(err);
res.send(500,err);
res.end();
});
app.all('*',function(req,res,next) {
var options = {
hostname: '127.0.0.1',
port: 8002,
rejectUnauthorized: false,
key: fs.readFileSync(__dirname+"/../../../deployment/server.key.pem"),
cert: fs.readFileSync(__dirname+"/../../../deployment/server.crt.pem")
};
agent = new https.Agent(options);
try {
proxy.web(req,res, {
target: "https://localhost:8002",
proxyTimeout: 30,
agent: agent
});
} catch(e) {
// 500 error
res.send(500,e);
}
})
/**
* Start Server
*/
var options = {
key: fs.readFileSync(__dirname+"/../../../deployment/server.key.pem"),
cert: fs.readFileSync(__dirname+"/../../../deployment/server.crt.pem")
};
server = https.createServer(options,app).listen(app.get('port'), function () {
console.log('Running payments server on port ' + app.get('port'));
});

Resources