My node.js https client always works regardless of certificate validity - javascript

This test program connects to an https server and gets some content. I've checked my server in browsers and with curl and the certificate is working correctly. If I run curl to grab data from the server it correctly complains about the certificate being unknown unless I pass it in with --cacert or turn security off with -k.
So the problem I am having is that although I think my client should be doing certificate authentication and I am telling it where the public certificate is, it just always works. If I remove the ca: option so it has no idea what the certificate is from the server then it silently works. I would like to catch the authentication error but I can't seem to do so.
var https = require('https');
var fs = require('fs');
function main() {
var data = '';
var get = https.get({
path: '/',
host: 'localhost',
port: 8000,
agent: false,
ca: [ fs.readFileSync('https_simple/cacert.pem') ]
}, function(x) {
x.setEncoding('utf8');
x.on('data', function(c) {data += c});
x.on('error', function(e) {
throw e;
});
x.on('end', function() {
console.log('Hai!. Here is the response:');
console.log(data);
});
});
get.on('error', function(e) {throw e});
get.end();
}
main();

In order to make this work I needed to upgrade to v0.7.8 (although any v0.7 should be fine) where the rejectUnauthorized functionality has been added to https.get
This combination of options is needed:
agent: false, // or you can supply your own agent, but if you don't you must set to false
rejectUnauthorized: true,
ca: [ fs.readFileSync('https_simple/cacert.pem') ]
Now if the authentication fails you will get an 'error' event and the request will not go ahead.
See the https.request documentation for details on making your own Agent
The bug fix was committed in this change: https://github.com/joyent/node/commit/f8c335d0

As per the documentation for https.request, the ca option of both https.get and https.request is an option from tls.connect. The documentation for the options to the tls.connect module function states:
ca: An array of strings or Buffers of trusted certificates. If this is
omitted several well known "root" CAs will be used, like VeriSign.
These are used to authorize connections.
Digging into the node.js source, the root certs used can be found here: https://github.com/joyent/node/blob/master/src/node_root_certs.h
So in short, with no authority cert provided as an option to https.get the tls module will attempt to authenticate the connection using the list of root certs anyway.

I do this in npm, using the request module. It goes like this:
var cacert = ... // in npm, this is a config setting
var request = require("request")
request.get({ url: "https://...",
ca: cacert,
strictSSL: true })
.on("response", function (resp) { ... })
.on("error", function (er) { ... })
The error event will be raised if the ssl isn't valid.

In V 0.6.15 you need to explicitly check whether or not the certificate validation passed or failed.
if (x.connection.authorized === false) {
console.log('SSL Authentication failed');
} else if (x.connection.authorized === true) {
console.log('SSL Authentication succeeded');
}

Related

NodeJS reverse SSH tunnel: unable to bind to serveo.net:80

Context
Not long ago, I discovered a great service called Serveo. It allows me to expose my local apps to the Internet using reverse SSH tunneling.
e.g. Connections to https://abc.serveo.net get forwarded to http://localhost:3000 on my machine.
To do this, they require no client installation, and I can just type this in the command line:
ssh -R 80:localhost:3000 serveo.net
where 80 is the remote port on serveo.net to which I want to bind, and localhost:3000 is the local address for my app.
If I just type 80 on the left-hand side, Serveo will answer Forwarding HTTP traffic from https://xxxx.serveo.net where xxxx is an available subdomain, with https support.
However, if I type another port, like 59000, the app will be available through serveo.net:59000, but without SSL.
Problem
Now, I would like to do this with NodeJS, to automate things in a tool I'm building for my coworkers and my company's partners, so that they don't need to worry about it, nor to have an SSH client on their machine. I'm using the SSH2 Node module.
Here is an example of working code, using the custom port configuration (here, 59000), with an app listening on http://localhost:3000:
/**
* Want to try it out?
* Go to https://github.com/blex41/demo-ssh2-tunnel
*/
const Client = require("ssh2").Client; // To communicate with Serveo
const Socket = require("net").Socket; // To accept forwarded connections (native module)
// Create an SSH client
const conn = new Client();
// Config, just like the second example in my question
const config = {
remoteHost: "",
remotePort: 59000,
localHost: "localhost",
localPort: 3000
};
conn
.on("ready", () => {
// When the connection is ready
console.log("Connection ready");
// Start an interactive shell session
conn.shell((err, stream) => {
if (err) throw err;
// And display the shell output (so I can see how Serveo responds)
stream.on("data", data => {
console.log("SHELL OUTPUT: " + data);
});
});
// Request port forwarding from the remote server
conn.forwardIn(config.remoteHost, config.remotePort, (err, port) => {
if (err) throw err;
conn.emit("forward-in", port);
});
})
// ===== Note: this part is irrelevant to my problem, but here for the demo to work
.on("tcp connection", (info, accept, reject) => {
console.log("Incoming TCP connection", JSON.stringify(info));
let remote;
const srcSocket = new Socket();
srcSocket
.on("error", err => {
if (remote === undefined) reject();
else remote.end();
})
.connect(config.localPort, config.localPort, () => {
remote = accept()
.on("close", () => {
console.log("TCP :: CLOSED");
})
.on("data", data => {
console.log(
"TCP :: DATA: " +
data
.toString()
.split(/\n/g)
.slice(0, 2)
.join("\n")
);
});
console.log("Accept remote connection");
srcSocket.pipe(remote).pipe(srcSocket);
});
})
// ===== End Note
// Connect to Serveo
.connect({
host: "serveo.net",
username: "johndoe",
tryKeyboard: true
});
// Just for the demo, create a server listening on port 3000
// Accessible both on:
// http://localhost:3000
// https://serveo.net:59000
const http = require("http"); // native module
http
.createServer((req, res) => {
res.writeHead(200, {
"Content-Type": "text/plain"
});
res.write("Hello world!");
res.end();
})
.listen(config.localPort);
This works fine, I can access my app on http://serveo.net:59000. But it does not support HTTPS, which is one of my requirements. If I want HTTPS, I need to set the port to 80, and leave the remote host blank just like the plain SSH command given above, so that Servo assigns me an available subdomain:
// equivalent to `ssh -R 80:localhost:3000 serveo.net`
const config = {
remoteHost: "",
remotePort: 80,
localHost: "localhost",
localPort: 3000
};
However, this is throwing an error:
Error: Unable to bind to :80
at C:\workspace\demo-ssh2-tunnel\node_modules\ssh2\lib\client.js:939:21
at SSH2Stream.<anonymous> (C:\workspace\demo-ssh2-tunnel\node_modules\ssh2\lib\client.js:628:24)
at SSH2Stream.emit (events.js:182:13)
at parsePacket (C:\workspace\demo-ssh2-tunnel\node_modules\ssh2-streams\lib\ssh.js:3851:10)
at SSH2Stream._transform (C:\workspace\demo-ssh2-tunnel\node_modules\ssh2-streams\lib\ssh.js:693:13)
at SSH2Stream.Transform._read (_stream_transform.js:190:10)
at SSH2Stream._read (C:\workspace\demo-ssh2-tunnel\node_modules\ssh2-streams\lib\ssh.js:252:15)
at SSH2Stream.Transform._write (_stream_transform.js:178:12)
at doWrite (_stream_writable.js:410:12)
at writeOrBuffer (_stream_writable.js:394:5)
I've tried many things without any success. If anyone has an idea about what might be wrong in my example, I'll be really grateful. Thanks!
OpenSSH defaults to "localhost" for the remote host when it's not specified. You can also verify this by checking the debug output from the OpenSSH client by adding -vvv to the command line. You should see a line like:
debug1: Remote connections from LOCALHOST:80 forwarded to local address localhost:3000
If you mimic this by setting config.remoteHost = 'localhost' in your JS code you should get the same result as the OpenSSH client.

Why does sencha ajax requests not send/store session cookie given by the server (sails)

I have a system that works perfectly using postman, I can POST to
localhost:1337/api/user/login with username and password set in the body of the request. This then verifies the login and allows me access to further calls. (And repeated calls to localhost:1337/api/user/login recognize I'm already logged it and respond to that).
When I check the server I notice that postman is indeed sending the (htmlonly) cookie with each request I make.
Now I tried to do the same log in through sencha 6.5 modern. In some controller I have the following code:
const form = this.getView();
if (form.isValid()) {
const data = form.getValues();
const u = 'http://localhost:1337/api/user/login'; //form.getUrl();
debugger;
Ext.Ajax.request({
url: u,
method: 'POST',
params : data,
success: function(response, opts) {
const obj = Ext.decode(response.responseText);
console.dir(obj);
},
failure: function(response, opts) {
console.log('server-side failure with status code ' + response.status);
}
});
}
This makes the "correct" call to the database, and I am seeing the credentials as expected. However I notice that on any future calls the session cookie is not being send stored (chrome doesn't store a cookie), and each time a new session cookie is being send/created.
What creates this discrepancy? Why does sencha/chrome not store the cookie?
For completeness sake, the server in sails (which uses expressjs session storage) goes to:
login: async function(req, res) {
try {
const user = await User.findOne({username: req.body.username});
const sess = req.session;
if (sess.userId === user.id) {
res.json({succes: false, message: 'already logged in'});
return;
}
if (!user) {
res.json({success: false, message: 'Authentication failed. User not found.'});
} else {
// check if password matches
const valid = await comparePromise(req.body.password, user.password);
if(valid) {
// if user is found and password is right
req.session.userId = user.id;
res.json({success: true, user: user});
} else {
res.forbidden(JSON.stringify({ success: false, message: 'Authentication failed. Wrong password.' }));
}
}
} catch (err) {
console.log(err);
res.badRequest(JSON.stringify({success: false, message: err.message}));
}
},
If your webpage is at one port and your ajax call is made to another port, this could explain why cookies are not being saved (even if the rest of the request and response works).
I think that different ports are treated as different origins. This would certainly explain why postman is successful while the code run from the page is not. I can tell you how I solved this problem (not for ports, but for different domain names served by the same ip) in my app...
In the ajax call, you need to set it up to allow credentials since the url is different origin:
$.ajax({
url: 'http://somehost.com/some/path',
method: 'GET',
xhrFields: { withCredentials: true },
success: function(data) {
// the cookie should be set here
}
});
Then you have to allow this at the receiving url. This can be insecure. In my case, I allowed cross origin requests at a very specific url only by configuring the routes.js file:
'GET /some/path': {
controller: 'MyController',
action: 'someAction',
cors: {
origin: '*',
credentials: true
}
},
In my case, that allowed any origin to access that route with credentials, and get a cookie. In your case, you might be able to use something like
cors: {
allowOrigins: ['http://localhost:1337', 'http://localhost:1841']
credentials: true
}
... but I'm not sure I have the syntax just right for that. Some docs are here.
If you need to do more universal setup over your whole app, I think sails has some ways to do that from the cors.js config file, but you should think through the security before doing this.
I think that once you have the cookie, it should be included with all requests across all ports (ie, I don't think including cookies is port specific, only storing them).
Good luck!

Checking other domains SSL certificates using nodeJS

I need to check if a given domain has a valid SSL certificate using NodeJS, so I'm using the https module like so:
const https = require('https');
const options = {
host: 'www.some-site.com',
method: 'get',
path: '/'
};
const req = https.request(options, res => {
console.log('Certificate Status: ', res.socket.authorized);
});
req.on('error', error => {
console.error('Error: ', error);
});
req.end();
I've been testing this against some of the sites listed as being insecure in Google's transparency report: https://transparencyreport.google.com/https/top-sites
Some of the results:
aliexpress.com - Certificate Status: true Correct
expired.badssl.com - Certificate Status: false Correct
Both correct. However, when a site does not use SSL at all the value for socket.authorized is still true:
alibaba.com - Certificate Status: true Wrong
www.bbc.com - Certificate Status: true Wrong
What is the correct way to validate whether a domain has a valid SSL certificate?
As pointed out in the comments for my question, these sites happened to be using redirects from https to http as soon as the sites loaded. So the initial 'valid cert' results were correct.
To get around this I used an npm package zfollow-redirects`, and this solved the issue.
From the package description:
Drop-in replacement for Nodes http and https that automatically
follows redirects.

How to ignore self signed certificate issue in request-promise

I am using request-promise module for my node app to make some API call.
https://www.npmjs.com/package/request-promise
import request from 'request-promise';
let options = {
method: GET,
json: true,
uri : "https://" +this.urls + endpoint,
body: payload,
rejectUnauthorized: false // This doesn't work
};
let response = await request(options)
SInce the API what I am trying to use is insecure (having self signed certificate), the conncetion is failing with this error:
Error: connect ECONNREFUSED
I know with "request" module, we could pass rejectUnauthorized: false , to handle such case. I am not sure how can I pass such option with request-promise module.
For any one still searching for this:
add strictSSL: false to the options object works for me
Try adding this to top of your code. But this approach is insecure.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";

Cannot connect to SQL Server with Node.js and Tedious

When I try to use Node.js and Tedioius to connect to a local SQL Server instance I get this error:
{ [ConnectionError: Failed to connect to XXXXX:1433 - connect ECONNREFUSED]
name: 'ConnectionError',
message: 'Failed to connect to XXXXX:1433 - connect ECONNREFUSED',
code: 'ESOCKET' }
Here is my connection object:
var config = {
userName: 'username',
password: 'password',
server: 'XXXXX',
options: {
database: 'databasename',
instancename: 'SQLEXPRESS'
}
};
I have checked and TCP/IP is enabled and broadcasting on port 1443 according to Configuration Manager. The SQL Server Browser service is also running, which I read may be causing this type of issue if not. I have disabled my antivirus and firewall and that hasn't helped either.
Any insight?
So what I am guessing happens is that even though Tedious lets you include instance name in 'options' it either doesn't use it or can't use it as it needs to be used. After doing some research, what should be happening is when you give SQL Server the instance name, it redirects you from port 1433 to the dynamic port it is using for that instance. I didn't know it was using a dynamic port, but if your instance is named the port will always be dynamic. I don't know where I saw it broadcasting on 1433, that was my mistake.
To check the dynamic port, look here:
From this information, I changed my code to this:
var config = {
userName: 'username',
password: 'password',
server: 'XXXXX',
options: {
port: 49175,
database: 'databasename',
instancename: 'SQLEXPRESS'
}
};
All is good now, hope this helps someone.
If anyone else is new to SQL Server like I am, and is dealing with this issue, once you enable TCP/IP in SQL Server Config Manager by following these steps:
> SQL Server Network Config
> Protocols for YOURSQLSERVERINSTANCE
> TCP/IP
> Enable
you get a warning message that looks like this:
Any changes made will be saved; however, they will not take effect until the service is stopped and restarted.
I took this to mean, disconnect from the database service in SQL Server Management Studio and reconnect, but this needs to happen in SQL Server Config Manager under the SQL Server Services tab. Find you SQL Server instance, stop and restart it, and hopefully you will be golden! This worked like a charm for me. Oddly, enabling the Named Pipes protocol seemed to work without a restart (I could see the difference in the error message), so I thought for sure it had stopped and restarted as needed.
Also, be sure to enable SQL Server Browser services as well. This and enabling TCP/IP and restarting the service were the keys for me.
If you still have problems after enabling TCP/IP protocol, I would suggest you check that SQL Server Browser Service is running. In my case I spent a lot of time till I realised it wasn't running.
This configuration run fine for me:
var config = {
user: 'user',
password: 'userPwd',
server: 'localhost',
database: 'myDatabase',
options: {
truestedConnection: true,
instanceName: 'SQLEXPRESS'
}
If you still got this error,
"...'Failed to connect to Server:1433 - connect ECONNREFUSED Server IP:1433',
code: 'ESOCKET' }"
and you've checked all the following:
Enable TCP/IP
Open Port 1433
Config setup correctly (database, server, username and password}
No Dynamic ports configured
Check your SQL server version. In my case, I discovered that I could connect to SQL 2012, but not SQL server 2016 with the same code. It appears SQL Server 2016 is not supported by the tedious driver yet.
...
You have to enabled tcp/ip on protocol for MSSQLSERVER
and activate both authentication
here is the complete code
const {
Request
} = require('tedious');
var Connection = require('tedious').Connection;
var config = {
server: 'DESKTOP-RU9C12L', //update me
authentication: {
type: 'default',
options: {
userName: 'begaak', //update me
password: 'begaak#123', //update me
}
},
options: {
encrypt: true,
enableArithAbort: true,
integratedSecurity: true,
trustServerCertificate: true,
rowCollectionOnDone: true,
database: 'selvapoc' //update me
}
};
var connection = new Connection(config);
connection.connect(function(err) {
console.log('testing')
// var request = new Request("Select * from products", function(err, rowCount, rows) {
// console.log(rowCount);
// console.log(JSON.stringify(rows))
// });
// connection.execSql(request);
connection.execSql(new Request('SELECT * FROM Products', function(err, rowCount, rows) {
if (err) {
throw err;
}
})
.on('doneInProc', function(rowCount, more, rows) {
console.log(more, rows[0], rowCount); // not empty
}));
});
connection.on('connect', function(err) {
// If no error, then good to proceed.
if (err) console.log(err)
console.log("Connected");
});
before starting the code configure these with SQL SERVER CONFIGURATION MANAGER

Categories