Extracting HTTP Host Header from nginx Stream 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?
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;
}