diff --git a/.dockerignore b/.dockerignore index 35cdaf3..8fafbb0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,6 @@ .git +.dockerignore +circle.yml +Makefile README.md +test diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e850f08 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,22 @@ +sudo: required +services: + - docker + +env: + global: + - DOCKER_VERSION=1.12.1-0~trusty + +before_install: + # list docker-engine versions + - apt-cache madison docker-engine + # upgrade docker-engine to specific version + - sudo apt-get -o Dpkg::Options::="--force-confnew" install -y docker-engine=${DOCKER_VERSION} + - docker version + - docker info + - sudo add-apt-repository ppa:duggan/bats --yes + - sudo apt-get update -qq + - sudo apt-get install -qq bats + - make update-dependencies + +script: + - make test diff --git a/Dockerfile b/Dockerfile index cc80e08..6d5ce9b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM nginx:1.9.5 -MAINTAINER Jason Wilder jwilder@litl.com +FROM nginx:1.11.3 +MAINTAINER Jason Wilder mail@jasonwilder.com # Install wget and install/updates certificates RUN apt-get update \ @@ -14,10 +14,10 @@ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ && sed -i 's/^http {/&\n server_names_hash_bucket_size 128;/g' /etc/nginx/nginx.conf # Install Forego -RUN wget -P /usr/local/bin https://godist.herokuapp.com/projects/ddollar/forego/releases/current/linux-amd64/forego \ - && chmod u+x /usr/local/bin/forego +ADD https://github.com/jwilder/forego/releases/download/v0.16.1/forego /usr/local/bin/forego +RUN chmod u+x /usr/local/bin/forego -ENV DOCKER_GEN_VERSION 0.4.1 +ENV DOCKER_GEN_VERSION 0.7.3 RUN wget https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ && tar -C /usr/local/bin -xvzf docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..74ae6bf --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +.SILENT : +.PHONY : test + +update-dependencies: + docker pull jwilder/docker-gen:0.7.3 + docker pull nginx:1.11.3 + docker pull python:3 + docker pull rancher/socat-docker:latest + docker pull appropriate/curl:latest + docker pull docker:1.10 + +test: + docker build -t jwilder/nginx-proxy:bats . + bats test diff --git a/Procfile b/Procfile index 8547156..0fa56e7 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,2 @@ nginx: nginx -dockergen: docker-gen -watch -only-exposed -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf +dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf diff --git a/README.md b/README.md index 22cae63..52ab6e4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -![nginx 1.9.5](https://img.shields.io/badge/nginx-1.9.5-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) ![Build](https://circleci.com/gh/jwilder/nginx-proxy.svg?&style=shield&circle-token=2da3ee844076a47371bd45da81cf27409ca7306a) +![nginx 1.11.3](https://img.shields.io/badge/nginx-1.11.3-brightgreen.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) [![Build Status](https://travis-ci.org/jwilder/nginx-proxy.svg?branch=master)](https://travis-ci.org/jwilder/nginx-proxy) [![](https://img.shields.io/docker/stars/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') [![](https://img.shields.io/docker/pulls/jwilder/nginx-proxy.svg)](https://hub.docker.com/r/jwilder/nginx-proxy 'DockerHub') + nginx-proxy sets up a container running nginx and [docker-gen][1]. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped. @@ -18,6 +19,32 @@ The containers being proxied must [expose](https://docs.docker.com/reference/run Provided your DNS is setup to forward foo.bar.com to the a host running nginx-proxy, the request will be routed to a container with the VIRTUAL_HOST env var set. +### Docker Compose + +```yaml +version: '2' +services: + nginx-proxy: + image: jwilder/nginx-proxy + container_name: nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + + whoami: + image: jwilder/whoami + container_name: whoami + environment: + - VIRTUAL_HOST=whoami.local +``` + +```shell +$ docker-compose up +$ curl -H "Host: whoami.local" localhost +I'm 5b129ab83266 +``` + ### Multiple Ports If your container exposes multiple ports, nginx-proxy will default to the service running on port 80. If you need to specify a different port, you can set a VIRTUAL_PORT env var to select a different one. If your container only exposes one port and it has a VIRTUAL_HOST env var set, that port will be selected. @@ -33,10 +60,30 @@ If you need to support multiple virtual hosts for a container, you can separate You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [xip.io](http://xip.io), using `~^foo\.bar\..*\.xip\.io` will match `foo.bar.127.0.0.1.xip.io`, `foo.bar.10.0.2.2.xip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html). +### Multiple Networks + +With the addition of [overlay networking](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) in Docker 1.9, your `nginx-proxy` container may need to connect to backend containers on multiple networks. By default, if you don't pass the `--net` flag when your `nginx-proxy` container is created, it will only be attached to the default `bridge` network. This means that it will not be able to connect to containers on networks other than `bridge`. + +If you want your `nginx-proxy` container to be attached to a different network, you must pass the `--net=my-network` option in your `docker create` or `docker run` command. At the time of this writing, only a single network can be specified at container creation time. To attach to other networks, you can use the `docker network connect` command after your container is created: + +```console +$ docker run -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro \ + --name my-nginx-proxy --net my-network jwilder/nginx-proxy +$ docker network connect my-other-network my-nginx-proxy +``` + +In this example, the `my-nginx-proxy` container will be connected to `my-network` and `my-other-network` and will be able to proxy to other containers attached to those networks. + ### SSL Backends If you would like to connect to your backend using HTTPS instead of HTTP, set `VIRTUAL_PROTO=https` on the backend container. +### uWSGI Backends + +If you would like to connect to uWSGI backend, set `VIRTUAL_PROTO=uwsgi` on the +backend container. Your backend container should than listen on a port rather +than a socket and expose that port. + ### Default Host To set the default host for nginx use the env var `DEFAULT_HOST=foo.bar.com` for example @@ -51,6 +98,14 @@ image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image. You may want to do this to prevent having the docker socket bound to a publicly exposed container service. +You can demo this pattern with docker-compose: + +```console +$ docker-compose --file docker-compose-separate-containers.yml up +$ curl -H "Host: whoami.local" localhost +I'm 5b129ab83266 +``` + To run nginx proxy as a separate container you'll need to have [nginx.tmpl](https://github.com/jwilder/nginx-proxy/blob/master/nginx.tmpl) on your host system. First start nginx with a volume: @@ -64,7 +119,7 @@ Then start the docker-gen container with the shared volume and template: $ docker run --volumes-from nginx \ -v /var/run/docker.sock:/tmp/docker.sock:ro \ -v $(pwd):/etc/docker-gen/templates \ - -t jwilder/docker-gen -notify-sighup nginx -watch -only-exposed /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf + -t jwilder/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf ``` Finally, start your containers with `VIRTUAL_HOST` environment variables. @@ -93,7 +148,7 @@ should have a `foo.bar.com.dhparam.pem` file in the certs directory. #### Wildcard Certificates -Wildcard certificates and keys should be name after the domain name with a `.crt` and `.key` extension. +Wildcard certificates and keys should be named after the domain name with a `.crt` and `.key` extension. For example `VIRTUAL_HOST=foo.bar.com` would use cert name `bar.com.crt` and `bar.com.key`. #### SNI @@ -110,7 +165,7 @@ should provide compatibility with clients back to Firefox 1, Chrome 1, IE 7, Ope Windows XP IE8, Android 2.3, Java 7. The configuration also enables HSTS, and SSL session caches. -The behavior for the proxy when port 80 and 443 are exposed is as follows: +The default behavior for the proxy when port 80 and 443 are exposed is as follows: * If a container has a usable cert, port 80 will redirect to 443 for that container so that HTTPS is always preferred when available. @@ -121,6 +176,15 @@ to establish a connection. A self-signed or generic cert named `default.crt` an will allow a client browser to make a SSL connection (likely w/ a warning) and subsequently receive a 503. +To serve traffic in both SSL and non-SSL modes without redirecting to SSL, you can include the +environment variable `HTTPS_METHOD=noredirect` (the default is `HTTPS_METHOD=redirect`). You can also +disable the non-SSL site entirely with `HTTPS_METHOD=nohttp`. `HTTPS_METHOD` must be specified +on each container for which you want to override the default behavior. If `HTTPS_METHOD=noredirect` is +used, Strict Transport Security (HSTS) is disabled to prevent HTTPS users from being redirected by the +client. If you cannot get to the HTTP site after changing this setting, your browser has probably cached +the HSTS policy and is automatically redirecting you back to HTTPS. You will need to clear your browser's +HSTS cache or use an incognito window / different browser. + ### Basic Authentication Support In order to be able to secure your virtual host, you have to create a file named as its equivalent VIRTUAL_HOST variable on directory @@ -155,10 +219,15 @@ proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + +# Mitigate httpoxy attack (see README for details) +proxy_set_header Proxy ""; ``` ***NOTE***: If you provide this file it will replace the defaults; you may want to check the .tmpl file to make sure you have all of the needed options. +***NOTE***: The default configuration blocks the `Proxy` HTTP request header from being sent to downstream servers. This prevents attackers from using the so-called [httpoxy attack](http://httpoxy.org). There is no legitimate reason for a client to send this header, and there are many vulnerable languages / platforms (`CVE-2016-5385`, `CVE-2016-5386`, `CVE-2016-5387`, `CVE-2016-5388`, `CVE-2016-1000109`, `CVE-2016-1000110`, `CERT-VU#797896`). + #### Proxy-wide To add settings on a proxy-wide basis, add your configuration file under `/etc/nginx/conf.d` using a name ending in `.conf`. @@ -218,3 +287,12 @@ If you are using multiple hostnames for a single container (e.g. `VIRTUAL_HOST=e If you want most of your virtual hosts to use a default single `location` block configuration and then override on a few specific ones, add those settings to the `/etc/nginx/vhost.d/default_location` file. This file will be used on any virtual host which does not have a `/etc/nginx/vhost.d/{VIRTUAL_HOST}` file associated with it. +### Contributing + +Before submitting pull requests or issues, please check github to make sure an existing issue or pull request is not already open. + +#### Running Tests Locally + +To run tests, you'll need to install [bats 0.4.0](https://github.com/sstephenson/bats). + + make test diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 8deabde..0000000 --- a/circle.yml +++ /dev/null @@ -1,21 +0,0 @@ -machine: - pre: - # install docker 1.7.1 - - sudo curl -L -o /usr/bin/docker 'https://s3-external-1.amazonaws.com/circle-downloads/docker-1.7.1-circleci'; sudo chmod 0755 /usr/bin/docker; true - services: - - docker - -dependencies: - override: - - sudo add-apt-repository ppa:duggan/bats --yes - - sudo apt-get update -qq - - sudo apt-get install -qq bats - - docker pull jwilder/docker-gen - - docker pull nginx - - docker pull python:3 - - docker pull rancher/socat-docker - -test: - override: - - docker build -t jwilder/nginx-proxy:bats . - - bats test \ No newline at end of file diff --git a/docker-compose-separate-containers.yml b/docker-compose-separate-containers.yml new file mode 100644 index 0000000..a4edb94 --- /dev/null +++ b/docker-compose-separate-containers.yml @@ -0,0 +1,23 @@ +version: '2' +services: + nginx: + image: nginx + container_name: nginx + ports: + - "80:80" + volumes: + - /etc/nginx/conf.d + + dockergen: + image: jwilder/docker-gen + command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf + volumes_from: + - nginx + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl + + whoami: + image: jwilder/whoami + environment: + - VIRTUAL_HOST=whoami.local diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..044f022 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +version: '2' +services: + nginx-proxy: + image: jwilder/nginx-proxy + container_name: nginx-proxy + ports: + - "80:80" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + + whoami: + image: jwilder/whoami + environment: + - VIRTUAL_HOST=whoami.local + diff --git a/nginx.tmpl b/nginx.tmpl index 93d57b5..c48c652 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -1,3 +1,5 @@ +{{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} + {{ define "upstream" }} {{ if .Address }} {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} @@ -5,13 +7,13 @@ # {{ .Container.Node.Name }}/{{ .Container.Name }} server {{ .Container.Node.Address.IP }}:{{ .Address.HostPort }} weight={{ .Weight }}; {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} - {{ else }} + {{ else if .Network }} # {{ .Container.Name }} - server {{ .Address.IP }}:{{ .Address.Port }} weight={{ .Weight }}; + server {{ .Network.IP }}:{{ .Address.Port }} weight={{ .Weight }}; {{ end }} - {{ else }} + {{ else if .Network }} # {{ .Container.Name }} - server {{ .Container.IP }} down; + server {{ .Network.IP }} down; {{ end }} {{ end }} @@ -49,6 +51,9 @@ proxy_set_header Connection $proxy_connection; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; + +# Mitigate httpoxy attack (see README for details) +proxy_set_header Proxy ""; {{ end }} server { @@ -65,6 +70,7 @@ server { access_log /var/log/nginx/access.log vhost; return 503; + ssl_session_tickets off; ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; } @@ -76,15 +82,24 @@ upstream {{ $host }} { {{ range $container := $containers }} {{ $weight := or ($container.Env.VIRTUAL_WEIGHT) "1" }} {{ $addrLen := len $container.Addresses }} - {{/* If only 1 port exposed, use that */}} - {{ if eq $addrLen 1 }} - {{ $address := index $container.Addresses 0 }} - {{ template "upstream" (dict "Container" $container "Address" $address "Weight" $weight) }} - {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} - {{ else }} - {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ template "upstream" (dict "Container" $container "Address" $address "Weight" $weight) }} + + {{ range $knownNetwork := $CurrentContainer.Networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if eq $knownNetwork.Name $containerNetwork.Name }} + ## Can be connect with "{{ $containerNetwork.Name }}" network + + {{/* If only 1 port exposed, use that */}} + {{ if eq $addrLen 1 }} + {{ $address := index $container.Addresses 0 }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork "Weight" $weight) }} + {{/* If more than one port exposed, use the one matching VIRTUAL_PORT env var, falling back to standard web port 80 */}} + {{ else }} + {{ $port := coalesce $container.Env.VIRTUAL_PORT "80" }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ template "upstream" (dict "Container" $container "Address" $address "Network" $containerNetwork "Weight" $weight) }} + {{ end }} + {{ end }} + {{ end }} {{ end }} {{ end }} } @@ -95,6 +110,9 @@ upstream {{ $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 HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} +{{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) "redirect" }} + {{/* Get the first cert name defined by containers w/ the same vhost */}} {{ $certName := (first (groupByKeys $containers "Env.CERT_NAME")) }} @@ -102,20 +120,24 @@ upstream {{ $host }} { {{ $vhostCert := (closest (dir "/etc/nginx/certs") (printf "%s.crt" $host))}} {{/* vhostCert is actually a filename so remove any suffixes since they are added later */}} -{{ $vhostCert := replace $vhostCert ".crt" "" -1 }} -{{ $vhostCert := replace $vhostCert ".key" "" -1 }} +{{ $vhostCert := trimSuffix ".crt" $vhostCert }} +{{ $vhostCert := trimSuffix ".key" $vhostCert }} -{{/* Use the cert specifid on the container or fallback to the best vhost match */}} +{{/* Use the cert specified on the container or fallback to the best vhost match */}} {{ $cert := (coalesce $certName $vhostCert) }} -{{ if (and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }} +{{ $is_https := (and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert))) }} +{{ if $is_https }} + +{{ if eq $https_method "redirect" }} server { server_name {{ $host }}; listen 80 {{ $default_server }}; access_log /var/log/nginx/access.log vhost; return 301 https://$host$request_uri; } +{{ end }} server { server_name {{ $host }}; @@ -123,11 +145,12 @@ server { access_log /var/log/nginx/access.log vhost; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA; + ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS'; ssl_prefer_server_ciphers on; ssl_session_timeout 5m; ssl_session_cache shared:SSL:50m; + ssl_session_tickets off; ssl_certificate /etc/nginx/certs/{{ (printf "%s.crt" $cert) }}; ssl_certificate_key /etc/nginx/certs/{{ (printf "%s.key" $cert) }}; @@ -136,7 +159,9 @@ server { ssl_dhparam {{ printf "/etc/nginx/certs/%s.dhparam.pem" $cert }}; {{ end }} + {{ if (ne $https_method "noredirect") }} add_header Strict-Transport-Security "max-age=31536000"; + {{ end }} {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} include {{ printf "/etc/nginx/vhost.d/%s" $host }}; @@ -145,7 +170,12 @@ server { {{ end }} location / { - proxy_pass {{ $proto }}://{{ $host }}; + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} + proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} auth_basic "Restricted {{ $host }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; @@ -157,7 +187,10 @@ server { {{ end }} } } -{{ else }} + +{{ end }} + +{{ if or (not $is_https) (eq $https_method "noredirect") }} server { server_name {{ $host }}; @@ -171,7 +204,12 @@ server { {{ end }} location / { - proxy_pass {{ $proto }}://{{ $host }}; + {{ if eq $proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim $proto }}://{{ trim $host }}; + {{ else }} + proxy_pass {{ trim $proto }}://{{ trim $host }}; + {{ end }} {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} auth_basic "Restricted {{ $host }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; @@ -184,12 +222,12 @@ server { } } -{{ if (and (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} +{{ 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 }}; access_log /var/log/nginx/access.log vhost; - return 503; + return 500; ssl_certificate /etc/nginx/certs/default.crt; ssl_certificate_key /etc/nginx/certs/default.key; diff --git a/test/default-host.bats b/test/default-host.bats index 503bb9f..acdffc6 100644 --- a/test/default-host.bats +++ b/test/default-host.bats @@ -4,7 +4,7 @@ load test_helpers function setup { # make sure to stop any web container before each test so we don't # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set - docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 + stop_bats_containers web } @@ -17,7 +17,7 @@ function setup { # 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 assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" # THEN querying the proxy without Host header → 200 run curl_container $SUT_CONTAINER / --head @@ -27,3 +27,7 @@ function setup { run curl_container $SUT_CONTAINER / --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" { + stop_bats_containers +} diff --git a/test/docker.bats b/test/docker.bats index 44d31b9..fc10226 100644 --- a/test/docker.bats +++ b/test/docker.bats @@ -11,10 +11,10 @@ load test_helpers @test "[$TEST_FILE] -v /var/run/docker.sock:/tmp/docker.sock:ro" { SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-1 - # WHEN nginx-proxy runs on our docker host using the default unix socket + # WHEN nginx-proxy runs on our docker host using the default unix socket run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" # THEN assert_nginxproxy_behaves $SUT_CONTAINER @@ -24,10 +24,10 @@ load test_helpers @test "[$TEST_FILE] -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock" { SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE}-2 - # WHEN nginx-proxy runs on our docker host using a custom unix socket + # WHEN nginx-proxy runs on our docker host using a custom unix socket run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/f00.sock:ro -e DOCKER_HOST=unix:///f00.sock assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" # THEN assert_nginxproxy_behaves $SUT_CONTAINER @@ -44,8 +44,8 @@ load test_helpers # WHEN nginx-proxy runs on our docker host using tcp to connect to our docker host run nginxproxy $SUT_CONTAINER -e DOCKER_HOST="tcp://bats-docker-tcp:2375" --link bats-docker-tcp:bats-docker-tcp assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" - + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" + # THEN assert_nginxproxy_behaves $SUT_CONTAINER } @@ -54,33 +54,36 @@ load test_helpers @test "[$TEST_FILE] separated containers (nginx + docker-gen + nginx.tmpl)" { docker_clean bats-nginx docker_clean bats-docker-gen - + # GIVEN a simple nginx container run docker run -d \ + --label bats-type="nginx" \ --name bats-nginx \ -v /etc/nginx/conf.d/ \ -v /etc/nginx/certs/ \ nginx:latest assert_success - run retry 5 1s curl --silent --fail --head http://$(docker_ip bats-nginx)/ + run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail --head http://$(docker_ip bats-nginx)/ assert_output -l 0 $'HTTP/1.1 200 OK\r' # WHEN docker-gen runs on our docker host run docker run -d \ + --label bats-type="docker-gen" \ --name bats-docker-gen \ -v /var/run/docker.sock:/tmp/docker.sock:ro \ -v $BATS_TEST_DIRNAME/../nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl:ro \ --volumes-from bats-nginx \ - jwilder/docker-gen:latest \ + --expose 80 \ + jwilder/docker-gen:0.7.3 \ -notify-sighup bats-nginx \ -watch \ -only-exposed \ /etc/docker-gen/templates/nginx.tmpl \ /etc/nginx/conf.d/default.conf assert_success - docker_wait_for_log bats-docker-gen 6 "Watching docker events" - - # Give some time to the docker-gen container to notify bats-nginx so it + docker_wait_for_log bats-docker-gen 9 "Watching docker events" + + # Give some time to the docker-gen container to notify bats-nginx so it # reloads its config sleep 2s @@ -89,11 +92,15 @@ load test_helpers docker logs bats-docker-gen false } >&2 - + # THEN assert_nginxproxy_behaves bats-nginx } +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} + # $1 nginx-proxy container function assert_nginxproxy_behaves { @@ -109,9 +116,8 @@ function assert_nginxproxy_behaves { run curl_container $container /data --header "Host: web2.bats" assert_output "answer from port 82" - + # Querying the proxy with unknown Host header → 503 run curl_container $container /data --header "Host: webFOO.bats" --head assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' } - diff --git a/test/lib/docker_helpers.bash b/test/lib/docker_helpers.bash index bbaf27e..221234e 100644 --- a/test/lib/docker_helpers.bash +++ b/test/lib/docker_helpers.bash @@ -13,6 +13,11 @@ function docker_ip { docker inspect --format '{{ .NetworkSettings.IPAddress }}' $1 } +# get the ip of docker container $1 +function docker_id { + docker inspect --format '{{ .ID }}' $1 +} + # get the running state of container $1 # → true/false # fails if the container does not exist @@ -52,9 +57,10 @@ function docker_tcp { local container_name="$1" docker_clean $container_name docker run -d \ + --label bats-type="socat" \ --name $container_name \ --expose 2375 \ -v /var/run/docker.sock:/var/run/docker.sock \ rancher/socat-docker - docker -H tcp://$(docker_ip $container_name):2375 version + docker run --label bats-type="docker" --link "$container_name:docker" docker:1.10 version } diff --git a/test/lib/ssl/nginx-proxy.bats.crt b/test/lib/ssl/nginx-proxy.bats.crt new file mode 100644 index 0000000..cf42bd7 --- /dev/null +++ b/test/lib/ssl/nginx-proxy.bats.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID7TCCAtWgAwIBAgIJAOGkf5EnexJVMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD +VQQGEwJVUzERMA8GA1UECAwIVmlyZ2luaWExDzANBgNVBAcMBlJlc3RvbjERMA8G +A1UECgwIRmFrZSBPcmcxGzAZBgNVBAMMEioubmdpbngtcHJveHkuYmF0czEpMCcG +CSqGSIb3DQEJARYad2VibWFzdGVyQG5naW54LXByb3h5LmJhdHMwHhcNMTYwNDIw +MTUzOTUxWhcNMjYwNDE4MTUzOTUxWjCBjDELMAkGA1UEBhMCVVMxETAPBgNVBAgM +CFZpcmdpbmlhMQ8wDQYDVQQHDAZSZXN0b24xETAPBgNVBAoMCEZha2UgT3JnMRsw +GQYDVQQDDBIqLm5naW54LXByb3h5LmJhdHMxKTAnBgkqhkiG9w0BCQEWGndlYm1h +c3RlckBuZ2lueC1wcm94eS5iYXRzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA0Amkj3iaQn8Z2CW6n24zSuWu2OoLCkHZAk8eprkI4kKoPBvjusynkm8E +phq65jebToHoldfuQ0wM61DzhD15bHwS3x9CrOVbShsmdnGALz+wdR0/4Likx50I +YZdecTOAlkoZudnX5FZ4ngOxjqcym7p5T8TrSS97a0fx99gitZY0p+Nu2tip4o3t +WBMs+SoPWTlQ1SrSmL8chC8O2knyBl/w1nHmDnMuR6FGcHdhLncApw9t5spgfv7p +OrMF4tQxJQNk10TnflmEMkGmy+pfk2e0cQ1Kwp3Nmzm7ECkggxxyjU3ihKiFK+09 +8aSCi7gDAY925+mV6LZ5oLMpO3KJvQIDAQABo1AwTjAdBgNVHQ4EFgQU+NvFo37z +9Dyq8Mu82SPtV7q1gYQwHwYDVR0jBBgwFoAU+NvFo37z9Dyq8Mu82SPtV7q1gYQw +DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAI1ityDV0UsCVHSpB2LN+ +QXlk8XS0ACIJ8Q0hbOj3BmYrdAVglG4P6upDEueaaxwsaBTagkTP8nxZ9dhfZHyZ +5YLNwYsiG5iqb8e0ecHx3uJT/0YiXn/8rBvxEZna4Fl8seGdp7BjOWUAS2Nv8tn4 +EJJvRdfX/O8XgPc95DM4lwQ/dvyWmavMI4lnl0n1IQV9WPGaIQhYPU9WEQK6iMUB +o1kx8YbOJQD0ZBRfqpriNt1/8ylkkSYYav8QT9JFvQFCWEvaX71QF+cuOwC7ZYBH +4ElXwEUrYBHKiPo0q0VsTtMvLh7h/T5czrIhG/NpfVJPtQOk8aVwNScL3/n+TGU8 +6g== +-----END CERTIFICATE----- diff --git a/test/lib/ssl/nginx-proxy.bats.key b/test/lib/ssl/nginx-proxy.bats.key new file mode 100644 index 0000000..24d8dc8 --- /dev/null +++ b/test/lib/ssl/nginx-proxy.bats.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQCaSPeJpCfxnY +JbqfbjNK5a7Y6gsKQdkCTx6muQjiQqg8G+O6zKeSbwSmGrrmN5tOgeiV1+5DTAzr +UPOEPXlsfBLfH0Ks5VtKGyZ2cYAvP7B1HT/guKTHnQhhl15xM4CWShm52dfkVnie +A7GOpzKbunlPxOtJL3trR/H32CK1ljSn427a2Knije1YEyz5Kg9ZOVDVKtKYvxyE +Lw7aSfIGX/DWceYOcy5HoUZwd2EudwCnD23mymB+/uk6swXi1DElA2TXROd+WYQy +QabL6l+TZ7RxDUrCnc2bObsQKSCDHHKNTeKEqIUr7T3xpIKLuAMBj3bn6ZXotnmg +syk7com9AgMBAAECggEAa7wCp3XqVPNjW+c1/ShhkbDeWmDhtL8i9aopkmeSbTHd +07sRtQQU56Vsf+Sp010KpZ5q52Z6cglpS1eRtHLtdbvPPhL/QXBJVVg4E/B1VIKk +DBJIqUSVuPXeiEOOWgs01R+ssO1ae1o4foQlKF33vGPWPPQacL0RKh6I9TPNzcD7 +n4rujlHk72N/bNydyK2rnyKB4vAI5TbZPLps+Xe123CmgZnW3JClcWV9B4foRmiu +a5Iq1WYAK2GYKbYwgqDRyYBC27m91a7U31pE4GQD+xQdlz6kcOlCU5hAcPK3h7j0 +fLQqn8g+YAtc0nBKKB4NZe3QEzTiVMorT0VitxI71QKBgQDnirardZaXOFzYGzB3 +j+FGB9BUW54hnHr5BxOYrfmEJ5umJjJWaGupfYrQsPArrJP1//WbqVZIPvdQParD +mQhLmSp1r/VNzGB6pISmzU1ZGDHsmBxYseh366om5YBQUFU2vmbil9VkrkM4fsJG +tcS9V/nVY/EM7Yp3PzjfLlhC1wKBgQDmA1YJmnZvIbLp3PoKqM69QiCLKztVm7nX +xpu3b3qbXEzXkt2sP5PHmr+s13hOPQFKRJ2hk4UN9WqpnFoHw5E5eWWhSa/peUZm +r10Y5XspiFtRHHiu6ABXB49eB4fen+vHEZHKyRJ4rFthKjjBHdNPC8bmwnT3jE85 +/8a26FLZiwKBgQDXEi8JZslBn9YF2oOTm28KCLoHka551AsaA+u892T8z3mxxGsf +fhD7N6TYonIEb2Jkr6OpOortwqcgvpc+5oghCJ27AX2fDUdUxDp/YdYF+wZsmQJD +lMW1lo7PYIBmmaf9mLCiq5xIz+GauYul+LNNmUl0YEgI1SC4EV63WCodswKBgDMX +GJxHd/kVViVGFTAa8NjvAEWJU8OfNHduQRZMp8IsjVDw6VYiRRP4Fo0wyyMtv8Sc +WxsRpmNEWO3VsdW5pd9LTLy3nmBQtMeIOjiWeHXwOMBaf5/yHmk2X6z2JULY6Mkt +6OFPKlAtkJqTg0m58z7Ckeqd1NdLjimG27+y+PwjAoGAFt0cbC1Ust2BE6YEspSX +ofpAnJsyKrbF9iVUyXDUP99sdqYQfPJ5uqPGkP59lJGkTLtebuitqi6FCyrsT6Fq +AWLiExbqebAqcuAZw2S+iuK27S4rrkjVGF53J7vH3rOzCBUXaRx6GKfTjUqedHdg +9Kw+LP6IFnMTb+EGLo+GqHs= +-----END PRIVATE KEY----- diff --git a/test/multiple-hosts.bats b/test/multiple-hosts.bats index c5c5749..10487ae 100644 --- a/test/multiple-hosts.bats +++ b/test/multiple-hosts.bats @@ -5,19 +5,21 @@ SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} function setup { # make sure to stop any web container before each test so we don't # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set - docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 + stop_bats_containers web } @test "[$TEST_FILE] start a nginx-proxy container" { run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" } @test "[$TEST_FILE] nginx-proxy forwards requests for 2 hosts" { # WHEN a container runs a web server with VIRTUAL_HOST set for multiple hosts prepare_web_container bats-multiple-hosts-1 80 -e VIRTUAL_HOST=multiple-hosts-1-A.bats,multiple-hosts-1-B.bats + 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 @@ -35,3 +37,7 @@ function setup { run curl_container $SUT_CONTAINER /data --header 'Host: multiple-hosts-1-B.bats' assert_output "answer from port 80" } + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} diff --git a/test/multiple-ports.bats b/test/multiple-ports.bats index a711056..a3c6fd0 100644 --- a/test/multiple-ports.bats +++ b/test/multiple-ports.bats @@ -1,54 +1,64 @@ -#!/usr/bin/env bats -load test_helpers -SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} - -function setup { - # make sure to stop any web container before each test so we don't - # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set - docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 -} - - -@test "[$TEST_FILE] start a nginx-proxy container" { - # GIVEN nginx-proxy - run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro - assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" -} - - -@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" { - # WHEN - prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats - - # THEN - assert_response_is_from_port 80 -} - - -@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" { - # GIVEN - prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90 - - # THEN - assert_response_is_from_port 90 -} - - -@test "[$TEST_FILE] single exposed port != 80" { - # GIVEN - prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats - - # THEN - assert_response_is_from_port 1234 -} - - -# assert querying nginx-proxy provides a response from the expected port of the web container -# $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" - assert_output "answer from port $port" -} - +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + stop_bats_containers web +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + # GIVEN nginx-proxy + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" +} + + +@test "[$TEST_FILE] nginx-proxy defaults to the service running on port 80" { + # WHEN + prepare_web_container bats-web-${TEST_FILE}-1 "80 90" -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-1 + sleep 1 + + # THEN + assert_response_is_from_port 80 +} + + +@test "[$TEST_FILE] VIRTUAL_PORT=90 while port 80 is also exposed" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-2 "80 90" -e VIRTUAL_HOST=web.bats -e VIRTUAL_PORT=90 + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-2 + sleep 1 + + # THEN + assert_response_is_from_port 90 +} + + +@test "[$TEST_FILE] single exposed port != 80" { + # GIVEN + prepare_web_container bats-web-${TEST_FILE}-3 1234 -e VIRTUAL_HOST=web.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-web-${TEST_FILE}-3 + sleep 1 + + # THEN + assert_response_is_from_port 1234 +} + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} + + +# assert querying nginx-proxy provides a response from the expected port of the web container +# $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" + assert_output "answer from port $port" +} + diff --git a/test/ssl.bats b/test/ssl.bats new file mode 100644 index 0000000..e7e0eae --- /dev/null +++ b/test/ssl.bats @@ -0,0 +1,146 @@ +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + stop_bats_containers web +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro -v ${DIR}/lib/ssl:/etc/nginx/certs:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" +} + +@test "[$TEST_FILE] test SSL for VIRTUAL_HOST=*.nginx-proxy.bats" { + # WHEN + prepare_web_container bats-ssl-hosts-1 "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 + sleep 1 + + # THEN + assert_301 test.nginx-proxy.bats + assert_200_https test.nginx-proxy.bats +} + +@test "[$TEST_FILE] test HTTPS_METHOD=nohttp" { + # WHEN + prepare_web_container bats-ssl-hosts-2 "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e CERT_NAME=nginx-proxy.bats \ + -e HTTPS_METHOD=nohttp + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-2 + sleep 1 + + # THEN + assert_503 test.nginx-proxy.bats + assert_200_https test.nginx-proxy.bats +} + +@test "[$TEST_FILE] test HTTPS_METHOD=noredirect" { + # WHEN + prepare_web_container bats-ssl-hosts-3 "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e CERT_NAME=nginx-proxy.bats \ + -e HTTPS_METHOD=noredirect + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3 + sleep 1 + + # THEN + assert_200 test.nginx-proxy.bats + assert_200_https test.nginx-proxy.bats +} + +@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 + sleep 1 + + # THEN + assert_301 test.nginx-proxy.bats + assert_200_https test.nginx-proxy.bats + assert_output -p "Strict-Transport-Security: max-age=31536000" +} + +@test "[$TEST_FILE] test HTTPS_METHOD=noredirect disables Strict-Transport-Security" { + # WHEN + prepare_web_container bats-ssl-hosts-5 "80 443" \ + -e VIRTUAL_HOST=*.nginx-proxy.bats \ + -e CERT_NAME=nginx-proxy.bats \ + -e HTTPS_METHOD=noredirect + dockergen_wait_for_event $SUT_CONTAINER start bats-ssl-hosts-3 + sleep 1 + + # THEN + assert_200 test.nginx-proxy.bats + assert_200_https test.nginx-proxy.bats + refute_output -p "Strict-Transport-Security: max-age=31536000" +} + + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} + + +# 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 +function assert_200 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + 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 +function assert_503 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + 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 +# $1 Host HTTP header to use when querying nginx-proxy +function assert_301 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + 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 +function assert_200_https { + local -r host=$1 + + run curl_container_https $SUT_CONTAINER / --head --header "Host: $host" + 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 +function assert_503_https { + local -r host=$1 + + run curl_container_https $SUT_CONTAINER / --head --header "Host: $host" + 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 +# $1 Host HTTP header to use when querying nginx-proxy +function assert_301_https { + local -r host=$1 + + run curl_container_https $SUT_CONTAINER / --head --header "Host: $host" + assert_output -l 0 $'HTTP/1.1 301 Moved Permanently\r' +} diff --git a/test/test_helpers.bash b/test/test_helpers.bash index 9063736..9b35b3c 100644 --- a/test/test_helpers.bash +++ b/test/test_helpers.bash @@ -1,7 +1,6 @@ # Test if requirements are met ( type docker &>/dev/null || ( echo "docker is not available"; exit 1 ) - type curl &>/dev/null || ( echo "curl is not available"; exit 1 ) )>&2 @@ -34,6 +33,7 @@ function nginxproxy { shift docker_clean $container_name \ && docker run -d \ + --label bats-type="nginx-proxy" \ --name $container_name \ "$@" \ $SUT_IMAGE \ @@ -67,13 +67,30 @@ function curl_container { local -r container=$1 local -r path=$2 shift 2 - curl --silent \ + docker run --label bats-type="curl" appropriate/curl --silent \ --connect-timeout 5 \ --max-time 20 \ "$@" \ http://$(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 +# $@ additional options to pass to the curl command +function curl_container_https { + local -r container=$1 + local -r path=$2 + shift 2 + docker run --label bats-type="curl" appropriate/curl --silent \ + --connect-timeout 5 \ + --max-time 20 \ + --insecure \ + "$@" \ + https://$(docker_ip $container)${path} +} # start a container running (one or multiple) webservers listening on given ports # @@ -87,6 +104,7 @@ function prepare_web_container { local -r options="$@" local expose_option="" + IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89 for port in $ports; do expose_option="${expose_option}--expose=$port " done @@ -108,21 +126,59 @@ function prepare_web_container { -w /var/www/ \ $options \ -e PYTHON_PORTS="$ports" \ - python:3 sh -c " + python:3 bash -c " + trap '[ \${#PIDS[@]} -gt 0 ] && kill -TERM \${PIDS[@]}' TERM + declare -a PIDS for port in \$PYTHON_PORTS; do echo starting a web server listening on port \$port; mkdir /var/www/\$port cd /var/www/\$port echo \"answer from port \$port\" > data python -m http.server \$port & + PIDS+=(\$!) done - wait + wait \${PIDS[@]} + trap - TERM + wait \${PIDS[@]} " assert_success # THEN querying directly port works + IFS=$' \t\n' # See https://github.com/sstephenson/bats/issues/89 for port in $ports; do - run retry 5 1s curl --silent --fail http://$(docker_ip $container_name):$port/data + run retry 5 1s docker run --label bats-type="curl" appropriate/curl --silent --fail http://$(docker_ip $container_name):$port/data assert_output "answer from port $port" done -} \ No newline at end of file +} + +# stop all containers with the "bats-type" label (matching the optionally supplied value) +# +# $1 optional label value +function stop_bats_containers { + local -r value=$1 + + if [ -z "$value" ]; then + CIDS=( $(docker ps -q --filter "label=bats-type") ) + else + CIDS=( $(docker ps -q --filter "label=bats-type=$value") ) + fi + + if [ ${#CIDS[@]} -gt 0 ]; then + docker stop ${CIDS[@]} >&2 + fi +} + +# wait for a docker-gen container to receive a specified event from a +# container with the specified ID/name +# +# $1 docker-gen container name +# $2 event +# $3 ID/name of container to receive event from +function dockergen_wait_for_event { + local -r container=$1 + local -r event=$2 + local -r other=$3 + local -r did=$(docker_id "$other") + docker_wait_for_log "$container" 9 "Received event $event for container ${did:0:12}" +} + diff --git a/test/wildcard-hosts.bats b/test/wildcard-hosts.bats index 8242fc1..8491e4b 100644 --- a/test/wildcard-hosts.bats +++ b/test/wildcard-hosts.bats @@ -1,68 +1,78 @@ -#!/usr/bin/env bats -load test_helpers -SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} - -function setup { - # make sure to stop any web container before each test so we don't - # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set - docker ps -q --filter "label=bats-type=web" | xargs -r docker stop >&2 -} - - -@test "[$TEST_FILE] start a nginx-proxy container" { - # GIVEN - run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro - assert_success - docker_wait_for_log $SUT_CONTAINER 3 "Watching docker events" -} - - -@test "[$TEST_FILE] VIRTUAL_HOST=*.wildcard.bats" { - # WHEN - prepare_web_container bats-wildcard-hosts-1 80 -e VIRTUAL_HOST=*.wildcard.bats - - # THEN - assert_200 f00.wildcard.bats - assert_200 bar.wildcard.bats - assert_503 unexpected.host.bats -} - -@test "[$TEST_FILE] VIRTUAL_HOST=wildcard.bats.*" { - # WHEN - prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=wildcard.bats.* - - # THEN - assert_200 wildcard.bats.f00 - assert_200 wildcard.bats.bar - assert_503 unexpected.host.bats -} - -@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats" { - # WHEN - prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats - - # THEN - assert_200 foo.bar.whatever.bats - assert_200 foo.bar.why.not.bats - assert_503 unexpected.host.bats - -} - - -# 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 -function assert_200 { - local -r host=$1 - - run curl_container $SUT_CONTAINER / --head --header "Host: $host" - 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 -function assert_503 { - local -r host=$1 - - run curl_container $SUT_CONTAINER / --head --header "Host: $host" - assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' -} \ No newline at end of file +#!/usr/bin/env bats +load test_helpers +SUT_CONTAINER=bats-nginx-proxy-${TEST_FILE} + +function setup { + # make sure to stop any web container before each test so we don't + # have any unexpected contaiener running with VIRTUAL_HOST or VIRUTAL_PORT set + stop_bats_containers web +} + + +@test "[$TEST_FILE] start a nginx-proxy container" { + # GIVEN + run nginxproxy $SUT_CONTAINER -v /var/run/docker.sock:/tmp/docker.sock:ro + assert_success + docker_wait_for_log $SUT_CONTAINER 9 "Watching docker events" +} + + +@test "[$TEST_FILE] VIRTUAL_HOST=*.wildcard.bats" { + # WHEN + prepare_web_container bats-wildcard-hosts-1 80 -e VIRTUAL_HOST=*.wildcard.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-1 + sleep 1 + + # THEN + assert_200 f00.wildcard.bats + assert_200 bar.wildcard.bats + assert_503 unexpected.host.bats +} + +@test "[$TEST_FILE] VIRTUAL_HOST=wildcard.bats.*" { + # WHEN + prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=wildcard.bats.* + dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-2 + sleep 1 + + # THEN + assert_200 wildcard.bats.f00 + assert_200 wildcard.bats.bar + assert_503 unexpected.host.bats +} + +@test "[$TEST_FILE] VIRTUAL_HOST=~^foo\.bar\..*\.bats" { + # WHEN + prepare_web_container bats-wildcard-hosts-2 80 -e VIRTUAL_HOST=~^foo\.bar\..*\.bats + dockergen_wait_for_event $SUT_CONTAINER start bats-wildcard-hosts-2 + sleep 1 + + # THEN + assert_200 foo.bar.whatever.bats + assert_200 foo.bar.why.not.bats + assert_503 unexpected.host.bats + +} + +@test "[$TEST_FILE] stop all bats containers" { + stop_bats_containers +} + + +# 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 +function assert_200 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + 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 +function assert_503 { + local -r host=$1 + + run curl_container $SUT_CONTAINER / --head --header "Host: $host" + assert_output -l 0 $'HTTP/1.1 503 Service Temporarily Unavailable\r' +}