Nodejs HTTP and HTTPS over same port
Asked Answered
L

5

50

I've been googling and looking here at stackoverflow, but I can't find an answer I like ;-)

I have a NodeJS server that runs over HTTPS and port 3001. Now I'd like to fetch all incoming HTTP requests on port 3001 and redirect them to the same URL but over HTTPS.

This must be possible. Isn't it?

Thanks!

Luetic answered 17/3, 2014 at 12:3 Comment(2)
possible duplicate of Automatic HTTPS connection/redirect with node.js/expressPrecambrian
@ScottGress that question / answer is really old. I believe the poster here want's an updated replyBienne
B
92

You don't need to listen on same port if you follow convention

By convention when you request http://127.0.0.1 your browser will try to connect to port 80. If you try to open https://127.0.0.1 your browser will try to connect to port 443. So to secure all traffic it is simply conventional to listen to port 80 on http with a redirect to https where we already have a listener for https for port 443. Here's the code:

var https = require('https');

var fs = require('fs');
var options = {
    key: fs.readFileSync('./key.pem'),
    cert: fs.readFileSync('./cert.pem')
};

https.createServer(options, function (req, res) {
    res.end('secure!');
}).listen(443);

// Redirect from http port 80 to https
var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
    res.end();
}).listen(80);

Test with https:

$ curl https://127.0.0.1 -k
secure!

With http:

$ curl http://127.0.0.1 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Date: Sun, 01 Jun 2014 06:15:16 GMT
Connection: keep-alive
Transfer-Encoding: chunked

If you must listen on same port

There isn't simple way to have http / https listen on the same port. You best bet is to create proxy server on a simple net socket that pipes to (http or https) based on the nature of the incoming connection (http vs. https).

Here is the complete code (based on https://gist.github.com/bnoordhuis/4740141) that does exactly that. It listens on localhost:3000 and pipes it to http (which in turn redirects it to https) or if the incomming connection is in https it just passes it to https handler

var fs = require('fs');
var net = require('net');
var http = require('http');
var https = require('https');

var baseAddress = 3000;
var redirectAddress = 3001;
var httpsAddress = 3002;
var httpsOptions = {
    key: fs.readFileSync('./key.pem'),
    cert: fs.readFileSync('./cert.pem')
};

net.createServer(tcpConnection).listen(baseAddress);
http.createServer(httpConnection).listen(redirectAddress);
https.createServer(httpsOptions, httpsConnection).listen(httpsAddress);

function tcpConnection(conn) {
    conn.once('data', function (buf) {
        // A TLS handshake record starts with byte 22.
        var address = (buf[0] === 22) ? httpsAddress : redirectAddress;
        var proxy = net.createConnection(address, function () {
            proxy.write(buf);
            conn.pipe(proxy).pipe(conn);
        });
    });
}

function httpConnection(req, res) {
    var host = req.headers['host'];
    res.writeHead(301, { "Location": "https://" + host + req.url });
    res.end();
}

function httpsConnection(req, res) {
    res.writeHead(200, { 'Content-Length': '5' });
    res.end('HTTPS');
}

As a test, If you connect it with https you get the https handler:

$ curl https://127.0.0.1:3000 -k
HTTPS

if you connect it with http you get the redirect handler (which simply takes you to the https handler):

$ curl http://127.0.0.1:3000 -i
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1:3000/
Date: Sat, 31 May 2014 16:36:56 GMT
Connection: keep-alive
Transfer-Encoding: chunked
Bienne answered 1/6, 2014 at 1:19 Comment(3)
The connection gets refused for the HTTPS redirect. Any ideas?Motile
This is really useful for having a HTTPS redirect on a non-standard port, thanks!Suburbanize
This will not handle big requests - try with few 100 kb, only the first "chunk" gets through, the rest is not piped correctly.Telecommunication
D
38

If serving HTTP and HTTPS over a single port is an absolute requirement you can proxy the request to the relevant HTTP implementation directly, rather than piping the socket to another port.

httpx.js

'use strict';
let net = require('net');
let http = require('http');
let https = require('https');

exports.createServer = (opts, handler) => {

    let server = net.createServer(socket => {
        socket.once('data', buffer => {
            // Pause the socket
            socket.pause();

            // Determine if this is an HTTP(s) request
            let byte = buffer[0];

            let protocol;
            if (byte === 22) {
                protocol = 'https';
            } else if (32 < byte && byte < 127) {
                protocol = 'http';
            }

            let proxy = server[protocol];
            if (proxy) {
                // Push the buffer back onto the front of the data stream
                socket.unshift(buffer);

                // Emit the socket to the HTTP(s) server
                proxy.emit('connection', socket);
            }
            
            // As of NodeJS 10.x the socket must be 
            // resumed asynchronously or the socket
            // connection hangs, potentially crashing
            // the process. Prior to NodeJS 10.x
            // the socket may be resumed synchronously.
            process.nextTick(() => socket.resume()); 
        });
    });

    server.http = http.createServer(handler);
    server.https = https.createServer(opts, handler);
    return server;
};

example.js

'use strict';
let express = require('express');
let fs = require('fs');
let io =  require('socket.io');

let httpx = require('./httpx');

let opts = {
    key: fs.readFileSync('./server.key'),
    cert: fs.readFileSync('./server.cert')
};

let app = express();
app.use(express.static('public'));

let server = httpx.createServer(opts, app);
let ws = io(server.http);
let wss = io(server.https);
server.listen(8080, () => console.log('Server started'));
Dissipated answered 3/2, 2017 at 8:24 Comment(2)
So far, the best answer. Really helpful. Much better than having to serve many ports for one app.Wingover
This actually works. It's better than using .once('data') as in one of the answers above. This will handle bigger requests too.Telecommunication
A
18

I know its an old question but just putting it as a reference for someone else. The easiest way that I found was to use the https://github.com/mscdex/httpolyglot module. Seems to do what it says quite reliably

    var httpolyglot = require('httpolyglot');
    var server = httpolyglot.createServer(options,function(req,res) {
      if (!req.socket.encrypted) {
      // Redirect to https
        res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
        res.end();
      } else {
        // The express app or any other compatible app 
        app.apply(app,arguments);
      }
  });
 // Some port
 server.listen(11000);
Affricative answered 19/9, 2015 at 6:54 Comment(0)
M
0

I found another way, but I would call this a dark way:

Code in Typescript: Server:

    const httpsServer = https.createServer({
                    key: ck.key,
                    cert: ck.crt,
    
                }, app);
    
                httpsServer.on('tlsClientError', (err, tlsSocket) => {
                    const tlsError = err as TlsClientError;
    
                    if (tlsError.reason === 'http request') {
                        const tTlsSocket = tlsSocket as TlsSocket;
    
                        if (tTlsSocket._parent) {
                            tTlsSocket._parent.write('HTTP/1.1 302 Found\n' +
                                `Location: https://localhost:${this._port}`);
                        }
    
                        Logger.getLogger().error(
                            `The client call the Server over HTTP protocol. Please use HTTPS, example: https://localhost:${this._port}`,
                            {
                                class: 'BaseHttpServer::listen'
                            }
                        );
                    }
                });

httpsServer.listen(this._port, () => {
                Logger.getLogger().info(
                    `${this._realm} listening on the https://localhost:${this._port}`,
                    {
                        class: 'BaseHttpServer::listen'
                    }
                );
            });

TlsClientError:

export class TlsClientError extends Error {

    public library: string = '';

    public reason: string = '';

    public code: string = '';

    public stack: string = '';

}

TlsSocket:

import * as net from 'net';
import * as tls from 'tls';

export class TlsSocket extends tls.TLSSocket {

    public _parent?: net.Socket;

}

Unfortunately, I couldn't find the right object for the error and defined my own. Error knows the reason "http request".

Just like the TlsSocket to get to the originating socket so that the data is not sent to the client in encrypted form.

Maybe someone knows a way to make this nicer? In any case, there would be no proxy in between, which should be noticeable with a lot of queries at speed.

Medievalist answered 15/1 at 10:28 Comment(0)
R
-1

If it's pure Node.JS HTTP module then you can try this:

if (!request.connection.encrypted) { // Check if the request is not HTTPS
    response.writeHead(301, { // May be 302
        Location: 'https://' + YourHostName + ':3001' + request.url
        /* Here you can add some more headers */
    });

    response.end(); // End the response
} else {
    // Behavior for HTTPS requests
}
Rafi answered 17/3, 2014 at 13:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.