Handling http and https requests using a single port with nginx

i was wondering if nginx is able to handle http and https requests on the same port. [*]

This is what i'm trying to do. I'm running a web server (lighttpd) handling http requests, and a C program that serves a particular section of the document tree through https. These two processes run on the same server.

At the firewall level, i can have only one port forwarding traffic into this server. So what i'd like to do is to set up nginx on this server so that it listens for requests on a single port and then:

A) redirects all http://myhost.com/* requests so that they go to localhost:8080 (where lighttpd is listening)

B) if a user requests a URL starting with, for example, https:// myhost.com/app, it sends that request to localhost:8008 (C program). Note that in this case, traffic between the remote browser and nginx must be encrypted.

Do you think this could be possible? If it is, how can it be done?

I know how to do this using two different ports. The challenge that i face is doing this with just a single port (unfortunately, i don't have control over the firewall configuration on this particular environment, so that's a restriction that i cannot avoid). Using techniques like reverse port fowarding through ssh to bypass the firewall won't work either, because this should work for remote users having nothing more than a web browser and an internet link.

If this is beyond nginx capabilities, do you know of any other product that could meet this requirements? (so far i've been unsuccessful in setting this up with lighttpd and pound). I'd also prefer avoiding Apache (although i'm willing to use it if it's the only possible choice).

Thanks in advance, Alex

[*] Just to be clear, i'm talking about handling encrypted and unencrypted HTTP connections through the same port. It doesn't matter if the encryption is done through SSL or TLS.


Solution 1:

According to wikipedia article on status codes, Nginx has a custom error code when http traffic is sent to https port(error code 497)

And according to nginx docs on error_page, you can define a URI that will be shown for a specific error.
Thus we can create a uri that clients will be sent to when error code 497 is raised.

nginx.conf

#lets assume your IP address is 89.89.89.89 and also that you want nginx to listen on port 7000 and your app is running on port 3000

server {
    listen 7000 ssl;
 
    ssl_certificate /path/to/ssl_certificate.cer;
    ssl_certificate_key /path/to/ssl_certificate_key.key;
    ssl_client_certificate /path/to/ssl_client_certificate.cer;

    error_page 497 301 =307 https://89.89.89.89:7000$request_uri;

    location / {
        proxy_pass http://89.89.89.89:3000/;

        proxy_pass_header Server;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Protocol $scheme;
    }
}

However if a client makes a request via any other method except a GET, that request will be turned into a GET. Thus to preserve the request method that the client came in via; we use error processing redirects as shown in nginx docs on error_page

And thats why we use the 301 =307 redirect.

Using the nginx.conf file shown here, we are able to have http and https listen in on the same port

Solution 2:

For those who might be searching:

Add ssl on; and error_page 497 $request_uri; to your server definition.

Solution 3:

This is finally possible to do properly since 1.15.2. See the information here.

In your nginx.conf add a block like this (outside the http block):

stream {
    upstream http {
        server localhost:8000;
    }

    upstream https {
        server localhost:8001;
    }

    map $ssl_preread_protocol $upstream {
        default https;
        "" http;
    }

    server {
        listen 8080;
        listen [::]:8080;
        proxy_pass $upstream;
        ssl_preread on;
    }
}

Then you can create your normal server block, but listening on these different ports:

server {
    listen 8000;
    listen [::]:8000;
    listen 8001 ssl;
    listen [::]:8001 ssl;
...

This way, the stream block is able to preread and detect if it is TLS or not (on port 8080 in this example), and then proxy passes it to the correct server port locally.

Solution 4:

If you wanted to be really clever, you could use a connection proxy thing to sniff the first couple of bytes of the incoming data stream, and hand off the connection based on the contents of byte 0: if it's 0x16 (the SSL/TLS 'handshake' byte), pass the connection to the SSL side, if it's an alphabetical character, do normal HTTP. My comment about port numbering applies.