diff --git a/nginx.tmpl b/nginx.tmpl index 9eb9520..e3cd246 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -56,9 +56,17 @@ proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header Proxy ""; {{ end }} +{{ $http_ports := (groupByKeys $ "Env.VIRTUAL_LISTEN_PORT_HTTP") }} +{{ $https_ports := (groupByKeys $ "Env.VIRTUAL_LISTEN_PORT_HTTPS") }} + server { server_name _; # This is just an invalid value which will never trigger on a real hostname. listen 80; + {{ range $port := $http_ports }} + {{ if ne $port "80" }} + listen {{ $port }}; + {{ end }} + {{ end }} access_log /var/log/nginx/access.log vhost; return 503; } @@ -67,6 +75,11 @@ server { server { server_name _; # This is just an invalid value which will never trigger on a real hostname. listen 443 ssl http2; + {{ range $port := $https_ports }} + {{ if ne $port "443" }} + listen {{ $port }} ssl http2; + {{ end }} + {{ end }} access_log /var/log/nginx/access.log vhost; return 503; @@ -104,11 +117,17 @@ upstream {{ $host }} { } {{ $default_host := or ($.Env.DEFAULT_HOST) "" }} -{{ $default_server := index (dict $host "" $default_host "default_server") $host }} +{{ $default_server := index (dict $host 0 $default_host 1) $host }} {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} {{ $proto := or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http" }} +{{/* Get the port to listen on for HTTP connections, falling back to 80 */}} +{{ $http_port := or (first (groupByKeys $containers "Env.VIRTUAL_LISTEN_PORT_HTTP")) "80" }} + +{{/* Get the port to listen on for HTTPS connections, falling back to 443 */}} +{{ $https_port := or (first (groupByKeys $containers "Env.VIRTUAL_LISTEN_PORT_HTTPS")) "443" }} + {{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} {{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) "redirect" }} @@ -132,7 +151,16 @@ upstream {{ $host }} { {{ if eq $https_method "redirect" }} server { server_name {{ $host }}; - listen 80 {{ $default_server }}; + {{ if eq $default_server 1 }} + listen 80 default_server; + {{ range $port := $http_ports }} + {{ if ne $port "80" }} + listen {{ $port }} default_server; + {{ end }} + {{ end }} + {{ else }} + listen {{ $http_port }}; + {{ end }} access_log /var/log/nginx/access.log vhost; return 301 https://$host$request_uri; } @@ -140,7 +168,16 @@ server { server { server_name {{ $host }}; - listen 443 ssl http2 {{ $default_server }}; + {{ if eq $default_server 1 }} + listen 443 ssl http2 default_server; + {{ range $port := $http_ports }} + {{ if ne $port "443" }} + listen {{ $port }} ssl http2 default_server; + {{ end }} + {{ end }} + {{ else }} + listen {{ $https_port }} ssl http2; + {{ end }} access_log /var/log/nginx/access.log vhost; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; @@ -193,7 +230,16 @@ server { server { server_name {{ $host }}; - listen 80 {{ $default_server }}; + {{ if eq $default_server 1 }} + listen 80 default_server; + {{ range $port := $http_ports }} + {{ if ne $port "80" }} + listen {{ $port }} default_server; + {{ end }} + {{ end }} + {{ else }} + listen {{ $http_port }}; + {{ end }} access_log /var/log/nginx/access.log vhost; {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} @@ -224,7 +270,16 @@ server { {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} server { server_name {{ $host }}; - listen 443 ssl http2 {{ $default_server }}; + {{ if eq $default_server 1 }} + listen 443 ssl http2 default_server; + {{ range $port := $https_ports }} + {{ if ne $port "443" }} + listen {{ $port }} ssl http2 default_server; + {{ end }} + {{ end }} + {{ else }} + listen {{ $https_port }} ssl http2; + {{ end }} access_log /var/log/nginx/access.log vhost; return 500; diff --git a/test/default-host.bats b/test/default-host.bats index acdffc6..57fde0b 100644 --- a/test/default-host.bats +++ b/test/default-host.bats @@ -11,8 +11,8 @@ function setup { @test "[$TEST_FILE] DEFAULT_HOST=web1.bats" { SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1 - # GIVEN a webserver with VIRTUAL_HOST set to web.bats - prepare_web_container bats-web 80 -e VIRTUAL_HOST=web.bats + # GIVEN a webserver with VIRTUAL_HOST set to web.bats and VIRTUAL_LISTEN_PORT_HTTP set to 81 + prepare_web_container bats-web 80 -e VIRTUAL_HOST=web.bats -e VIRTUAL_LISTEN_PORT_HTTP=81 # WHEN nginx-proxy runs with DEFAULT_HOST set to web.bats run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -e DEFAULT_HOST=web.bats @@ -26,6 +26,14 @@ function setup { # THEN querying the proxy with any other Host header → 200 run curl_container $SUT_CONTAINER / --head --header "Host: something.I.just.made.up" assert_output -l 0 $'HTTP/1.1 200 OK\r' + + # THEN querying the proxy without Host header on port 81 → 200 + run curl_container_port $SUT_CONTAINER / 81 --head + assert_output -l 0 $'HTTP/1.1 200 OK\r' + + # THEN querying the proxy with any other Host header on port 81 → 200 + run curl_container_port $SUT_CONTAINER / 81 --head --header "Host: something.I.just.made.up" + assert_output -l 0 $'HTTP/1.1 200 OK\r' } @test "[$TEST_FILE] stop all bats containers" { diff --git a/test/multiple-hosts.bats b/test/multiple-hosts.bats index 10487ae..a37122e 100644 --- a/test/multiple-hosts.bats +++ b/test/multiple-hosts.bats @@ -38,6 +38,45 @@ function setup { assert_output "answer from port 80" } +@test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts on another port" { + # WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts and a VIRTUAL_LISTEN_PORT_HTTP + prepare_web_container bats-multiple-hosts-1 80 -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats -e VIRTUAL_LISTEN_PORT_HTTP=81 + dockergen_wait_for_event $SUT_CONTAINER start bats-multiple-hosts-1 + sleep 1 + + # THEN querying the proxy without Host header → 503 + run curl_container $SUT_CONTAINER / --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy without Host header on port 81 → 503 + run curl_container_port $SUT_CONTAINER / 81 --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy with unknown Host header → 503 + run curl_container $SUT_CONTAINER /data --header "Host: webFOO.bats" --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy with unknown Host header on port 81 → 503 + run curl_container_port $SUT_CONTAINER /data 81 --header "Host: webFOO.bats" --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy with known Host header on wrong port → 503 + run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-A.bats' --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN querying the proxy with known Host header on wrong port → 503 + run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-B.bats' --head + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' + + # THEN + run curl_container_port $SUT_CONTAINER /data 81 --header 'Host: multiple-hosts-1-A.bats' + assert_output "answer from port 80" # 80 because we forward 81 to 80 + + # THEN + run curl_container_port $SUT_CONTAINER /data 81 --header 'Host: multiple-hosts-1-B.bats' + assert_output "answer from port 80" # 80 because we forward 81 to 80 +} + @test "[$TEST_FILE] stop all bats containers" { stop_bats_containers } diff --git a/test/multiple-ports.bats b/test/multiple-ports.bats index a3c6fd0..31a9264 100644 --- a/test/multiple-ports.bats +++ b/test/multiple-ports.bats @@ -27,6 +27,16 @@ function setup { assert_response_is_from_port 80 } +@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80 when listening on another port" { + # WHEN + prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_LISTEN_PORT_HTTP=90 + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-1 + sleep 1 + + # THEN + assert_response_is_from_port 80 90 +} + @test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" { # GIVEN @@ -38,6 +48,15 @@ function setup { assert_response_is_from_port 90 } +@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed when listening on another port" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90 -e VIRTUAL_LISTEN_PORT_HTTP=90 + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-2 + sleep 1 + + # THEN + assert_response_is_from_port 90 90 +} @test "[$TEST_FILE] single exposed port != 80" { # GIVEN @@ -49,6 +68,16 @@ function setup { assert_response_is_from_port 1234 } +@test "[$TEST_FILE] single exposed port != 80 when listening on another port" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats -e VIRTUAL_LISTEN_PORT_HTTP=1234 + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-3 + sleep 1 + + # THEN + assert_response_is_from_port 1234 1234 +} + @test "[$TEST_FILE] stop all bats containers" { stop_bats_containers } @@ -58,7 +87,12 @@ function setup { # $1 port we are expecting an response from function assert_response_is_from_port { local -r port=$1 - run curl_container $SUT_CONTAINER /data --header "Host: web.bats" + local -r curl_port=$2 + + if [ -z "${curl_port}" ]; then + run curl_container $SUT_CONTAINER /data --header "Host: web.bats" + else + run curl_container_port $SUT_CONTAINER /data ${curl_port} --header "Host: web.bats" + fi assert_output "answer from port $port" } - diff --git a/test/ssl.bats b/test/ssl.bats index e7e0eae..1191e8c 100644 --- a/test/ssl.bats +++ b/test/ssl.bats @@ -28,6 +28,22 @@ function setup { assert_200_https test.nginx-proxy.bats } +@test "[$TEST_FILE] test SSL for VIRTUAL_HOST=*.nginx-proxy.bats with different ports" { + # WHEN + prepare_web_container bats-ssl-hosts-1-ports "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e VIRTUAL_LISTEN_PORT_HTTP=81 \ + -e VIRTUAL_LISTEN_PORT_HTTPS=444 \ + -e CERT_NAME=nginx-proxy.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-1-ports + sleep 1 + + # THEN + assert_503 test.nginx-proxy.bats + assert_301 test.nginx-proxy.bats 81 + assert_200_https test.nginx-proxy.bats 444 +} + @test "[$TEST_FILE] test HTTPS_METHOD=nohttp" { # WHEN prepare_web_container bats-ssl-hosts-2 "80 443" \ @@ -42,6 +58,23 @@ function setup { assert_200_https test.nginx-proxy.bats } +@test "[$TEST_FILE] test HTTPS_METHOD=nohttp with different ports" { + # WHEN + prepare_web_container bats-ssl-hosts-2-ports "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e CERT_NAME=nginx-proxy.bats \ + -e VIRTUAL_LISTEN_PORT_HTTP=82 \ + -e VIRTUAL_LISTEN_PORT_HTTPS=445 \ + -e HTTPS_METHOD=nohttp + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-2-ports + sleep 1 + + # THEN + assert_503 test.nginx-proxy.bats + assert_503 test.nginx-proxy.bats 82 + assert_200_https test.nginx-proxy.bats 445 +} + @test "[$TEST_FILE] test HTTPS_METHOD=noredirect" { # WHEN prepare_web_container bats-ssl-hosts-3 "80 443" \ @@ -56,12 +89,29 @@ function setup { assert_200_https test.nginx-proxy.bats } +@test "[$TEST_FILE] test HTTPS_METHOD=noredirect with different ports" { + # WHEN + prepare_web_container bats-ssl-hosts-3-ports "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e CERT_NAME=nginx-proxy.bats \ + -e VIRTUAL_LISTEN_PORT_HTTP=81 \ + -e VIRTUAL_LISTEN_PORT_HTTPS=444 \ + -e HTTPS_METHOD=noredirect + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3-ports + sleep 1 + + # THEN + assert_503 test.nginx-proxy.bats + assert_200 test.nginx-proxy.bats 81 + assert_200_https test.nginx-proxy.bats 444 +} + @test "[$TEST_FILE] test SSL Strict-Transport-Security" { # WHEN prepare_web_container bats-ssl-hosts-4 "80 443" \ -e VIRTUAL_HOST=*.nginx-proxy.bats \ -e CERT_NAME=nginx-proxy.bats - dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-1 + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-4 sleep 1 # THEN @@ -93,37 +143,61 @@ function setup { # assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response # $1 Host HTTP header to use when querying nginx-proxy +# $2 (optional) HTTP port to use function assert_200 { local -r host=$1 + local -r port=$2 - run curl_container $SUT_CONTAINER / --head --header "Host: $host" + if [ -z "$port" ]; then + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + else + run curl_container_port $SUT_CONTAINER / ${port} --head --header "Host: $host" + fi assert_output -l 0 $'HTTP/1.1 200 OK\r' } # assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response # $1 Host HTTP header to use when querying nginx-proxy +# $2 (optional) HTTP port to use function assert_503 { local -r host=$1 + local -r port=$2 - run curl_container $SUT_CONTAINER / --head --header "Host: $host" + if [ -z "$port" ]; then + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + else + run curl_container $SUT_CONTAINER / ${port} --head --header "Host: $host" + fi assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' } -# assert that querying nginx-proxy with the given Host header produces a `HTTP 503` response +# assert that querying nginx-proxy with the given Host header produces a `HTTP 301` response # $1 Host HTTP header to use when querying nginx-proxy +# $2 (optional) HTTP port to use function assert_301 { local -r host=$1 + local -r port=$2 - run curl_container $SUT_CONTAINER / --head --header "Host: $host" + if [ -z "$port" ]; then + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + else + run curl_container_port $SUT_CONTAINER / ${port} --head --header "Host: $host" + fi assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r' } # assert that querying nginx-proxy with the given Host header produces a `HTTP 200` response # $1 Host HTTP header to use when querying nginx-proxy +# $2 (optional) HTTPS port to use function assert_200_https { local -r host=$1 + local -r port=$2 - run curl_container_https $SUT_CONTAINER / --head --header "Host: $host" + if [ -z "$port" ]; then + run curl_container_https $SUT_CONTAINER / --head --header "Host: $host" + else + run curl_container_https_port $SUT_CONTAINER / ${port} --head --header "Host: $host" + fi assert_output -l 0 $'HTTP/1.1 200 OK\r' } diff --git a/test/test_helpers.bash b/test/test_helpers.bash index 9b35b3c..aa458b8 100644 --- a/test/test_helpers.bash +++ b/test/test_helpers.bash @@ -22,7 +22,7 @@ load ${DIR}/lib/docker_helpers.bash # Define functions specific to our test suite -# run the SUT docker container +# run the SUT docker container # and makes sure it remains started # and displays the nginx-proxy start logs # @@ -57,7 +57,7 @@ function wait_for_nginxproxy_container_to_start { } -# Send a HTTP request to container $1 for path $2 and +# Send a HTTP request to container $1 for path $2 and # Additional curl options can be passed as $@ # # $1 container name @@ -74,7 +74,26 @@ function curl_container { http://$(docker_ip $container)${path} } -# Send a HTTPS request to container $1 for path $2 and +# Send a HTTP request to container $1 for path $2 and port $3 +# Additional curl options can be passed as $@ +# +# $1 container name +# $2 HTTP path to query +# $3 HTTP port +# $@ additional options to pass to the curl command +function curl_container_port { + local -r container=$1 + local -r path=$2 + local -r port=$3 + shift 3 + docker run --label bats-type="curl" appropriate/curl --silent \ + --connect-timeout 5 \ + --max-time 20 \ + "$@" \ + http://$(docker_ip $container):${port}${path} +} + +# Send a HTTPS request to container $1 for path $2 and # Additional curl options can be passed as $@ # # $1 container name @@ -92,6 +111,26 @@ function curl_container_https { https://$(docker_ip $container)${path} } +# Send a HTTPS request to container $1 for path $2 and +# Additional curl options can be passed as $@ +# +# $1 container name +# $2 HTTPS path to query +# $3 HTTP port +# $@ additional options to pass to the curl command +function curl_container_https_port { + local -r container=$1 + local -r path=$2 + local -r port=$3 + shift 3 + docker run --label bats-type="curl" appropriate/curl --silent \ + --connect-timeout 5 \ + --max-time 20 \ + --insecure \ + "$@" \ + https://$(docker_ip $container):${port}${path} +} + # start a container running (one or multiple) webservers listening on given ports # # $1 container name @@ -181,4 +220,3 @@ function dockergen_wait_for_event { local -r did=$(docker_id "$other") docker_wait_for_log "$container" 9 "Received event $event for container ${did:0:12}" } -