Extract HTTP Host Header from Nginx Stream Proxy – How to

httphttp-headershttpsnginxreverse-proxy

I'm looking to use the stream module of nginx to proxy HTTP traffic. This works well for HTTPS, as the ngx_stream_ssl_preread module exists. This allows me to extract the requested server name from the TLS handshake, which I can then use to determine which server I should proxy the stream to. However, I can't seem to find an equivalent for plain HTTP.

I would imagine that this is because most people simply use a normal HTTP proxy, as the proxy server can see the Host header in the HTTP request (since it's unencrypted). Using streams would be a much nicer solution for my scenario though, and it seems to be more lightweight than a full HTTP proxy.

With the ngx_stream_ssl_preread module, you gain access to a variable named ssl_preread_server_name. I'm looking for something that would provide essentially the same thing, but derived from the Host header in the HTTP request. Does such a thing exist?

Best Answer

I couldn't find any built-in way, so I implemented one myself.

With the ngx_stream_js_module module and some custom JavaScript (njs) I can read the server name from the HTTP Host header into $preread_server_name and configure HTTP traffic pretty similar to HTTPS.

First, install the extra module required to support JavaScript in nginx:

$ apt install nginx-module-njs

Load the module in nginx.conf:

load_module modules/ngx_stream_js_module.so;

/etc/nginx/http_server_name.js implements reading the server name:

var server_name = '-';

/**
 * Read the server name from the HTTP stream.
 *
 * @param s
 *   Stream.
 */
function read_server_name(s) {
  s.on('upload', function (data, flags) {
    if (data.length || flags.last) {
      s.done();
    }

    // If we can find the Host header.
    var n = data.indexOf('\r\nHost: ');
    if (n != -1) {
      // Determine the start of the Host header value and of the next header.
      var start_host = n + 8;
      var next_header = data.indexOf('\r\n', start_host);

      // Extract the Host header value.
      server_name = data.substr(start_host, next_header - start_host);

      // Remove the port if given.
      var port_start = server_name.indexOf(':');
      if (port_start != -1) {
        server_name = server_name.substr(0, port_start);
      }
    }
  });
}

function get_server_name(s) {
  return server_name;
}

export default {read_server_name, get_server_name}

Here's my stream.conf

stream {
  # The HTTP map is based on the server name read from the HTTP stream in
  # http_server_name.js.
  js_import main from http_server_name.js;
  js_set $preread_server_name main.get_server_name;

  map $preread_server_name $internal_port {
    foo.example.com 8080;
    bar.example.com 8081;
  }

  # The HTTPS map is based on the server name provided by the
  # ngx_stream_ssl_preread_module module.
  map $ssl_preread_server_name $ssl_internal_port {
    foo.example.com 8443;
    bar.example.com 8444;
  }

  server {
    listen 443;

    # Have $ssl_preread_server_name populated.
    ssl_preread on;

    proxy_protocol on;

    proxy_pass my_host:$ssl_internal_port;
  }

  server {
    listen 80;

    # Read the server name at the preread phase.
    js_preread main.read_server_name;

    proxy_protocol on;

    proxy_pass my_host:$internal_port;
  }
}

Include stream.conf in nginx.conf:

include /etc/nginx/stream.conf;

Apply the configuration by reloading nginx:

$ service nginx reload

The proxied servers can now be configured in one way to read the real client data from the PROXY protocol for both HTTP and HTTPS:

server {
  listen 80 proxy_protocol;
  listen 443 ssl proxy_protocol;

  set_real_ip_from 10.0.0.0/8;
  set_real_ip_from 172.16.0.0/12;
  set_real_ip_from 192.168.0.0/16;
  set_real_ip_from fc00::/7;
  real_ip_header proxy_protocol;
  real_ip_recursive on;
}