Pārlūkot izejas kodu

Add the nginx-proxy source to the directory
Going to change nginx.tmpl to allow LetsEncrypt to work for client ssl secured pages.
DockerCompose needs to be changed to build image locally.

hmetzner 7 gadi atpakaļ
vecāks
revīzija
506dbc3a8d
100 mainītis faili ar 4052 papildinājumiem un 0 dzēšanām
  1. 37 0
      nginx-proxy/Dockerfile
  2. 34 0
      nginx-proxy/Dockerfile.alpine
  3. 21 0
      nginx-proxy/LICENSE
  4. 16 0
      nginx-proxy/Makefile
  5. 2 0
      nginx-proxy/Procfile
  6. 432 0
      nginx-proxy/README.md
  7. 8 0
      nginx-proxy/dhparam.pem.default
  8. 23 0
      nginx-proxy/docker-compose-separate-containers.yml
  9. 14 0
      nginx-proxy/docker-compose.yml
  10. 33 0
      nginx-proxy/docker-entrypoint.sh
  11. 45 0
      nginx-proxy/generate-dhparam.sh
  12. 6 0
      nginx-proxy/network_internal.conf
  13. 361 0
      nginx-proxy/nginx.tmpl
  14. 107 0
      nginx-proxy/test/README.md
  15. 81 0
      nginx-proxy/test/certs/README.md
  16. 21 0
      nginx-proxy/test/certs/ca-root.crt
  17. 27 0
      nginx-proxy/test/certs/ca-root.key
  18. 183 0
      nginx-proxy/test/certs/create_server_certificate.sh
  19. 473 0
      nginx-proxy/test/conftest.py
  20. 8 0
      nginx-proxy/test/lib/ssl/dhparam.pem
  21. 3 0
      nginx-proxy/test/pytest.ini
  22. 24 0
      nginx-proxy/test/pytest.sh
  23. 10 0
      nginx-proxy/test/requirements/Dockerfile-nginx-proxy-tester
  24. 52 0
      nginx-proxy/test/requirements/README.md
  25. 6 0
      nginx-proxy/test/requirements/build.sh
  26. 5 0
      nginx-proxy/test/requirements/python-requirements.txt
  27. 8 0
      nginx-proxy/test/requirements/web/Dockerfile
  28. 15 0
      nginx-proxy/test/requirements/web/entrypoint.sh
  29. 38 0
      nginx-proxy/test/requirements/web/webserver.py
  30. 1 0
      nginx-proxy/test/stress_tests/README.md
  31. 5 0
      nginx-proxy/test/stress_tests/test_deleted_cert/README.md
  32. 70 0
      nginx-proxy/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.crt
  33. 27 0
      nginx-proxy/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.key
  34. 17 0
      nginx-proxy/test/stress_tests/test_deleted_cert/docker-compose.yml
  35. 73 0
      nginx-proxy/test/stress_tests/test_deleted_cert/test_restart_while_missing_cert.py
  36. 2 0
      nginx-proxy/test/stress_tests/test_deleted_cert/tmp_certs/.gitignore
  37. 59 0
      nginx-proxy/test/stress_tests/test_unreachable_network/README.md
  38. 35 0
      nginx-proxy/test/stress_tests/test_unreachable_network/docker-compose.yml
  39. 35 0
      nginx-proxy/test/stress_tests/test_unreachable_network/test_unreachable_net.py
  40. 15 0
      nginx-proxy/test/test_DOCKER_HOST_unix_socket.py
  41. 24 0
      nginx-proxy/test/test_DOCKER_HOST_unix_socket.yml
  42. 10 0
      nginx-proxy/test/test_composev2.py
  43. 15 0
      nginx-proxy/test/test_composev2.yml
  44. 1 0
      nginx-proxy/test/test_custom/my_custom_proxy_settings.conf
  45. 1 0
      nginx-proxy/test/test_custom/my_custom_proxy_settings_bar.conf
  46. 28 0
      nginx-proxy/test/test_custom/test_defaults-location.py
  47. 31 0
      nginx-proxy/test/test_custom/test_defaults-location.yml
  48. 20 0
      nginx-proxy/test/test_custom/test_defaults.py
  49. 24 0
      nginx-proxy/test/test_custom/test_defaults.yml
  50. 22 0
      nginx-proxy/test/test_custom/test_location-per-vhost.py
  51. 24 0
      nginx-proxy/test/test_custom/test_location-per-vhost.yml
  52. 19 0
      nginx-proxy/test/test_custom/test_per-vhost.py
  53. 24 0
      nginx-proxy/test/test_custom/test_per-vhost.yml
  54. 20 0
      nginx-proxy/test/test_custom/test_proxy-wide.py
  55. 24 0
      nginx-proxy/test/test_custom/test_proxy-wide.yml
  56. 7 0
      nginx-proxy/test/test_default-host.py
  57. 18 0
      nginx-proxy/test/test_default-host.yml
  58. 1 0
      nginx-proxy/test/test_dockergen/.gitignore
  59. 38 0
      nginx-proxy/test/test_dockergen/test_dockergen_v2.py
  60. 27 0
      nginx-proxy/test/test_dockergen/test_dockergen_v2.yml
  61. 66 0
      nginx-proxy/test/test_dockergen/test_dockergen_v3.py
  62. 28 0
      nginx-proxy/test/test_dockergen/test_dockergen_v3.yml
  63. 46 0
      nginx-proxy/test/test_events.py
  64. 5 0
      nginx-proxy/test/test_events.yml
  65. 70 0
      nginx-proxy/test/test_headers/certs/web.nginx-proxy.tld.crt
  66. 27 0
      nginx-proxy/test/test_headers/certs/web.nginx-proxy.tld.key
  67. 81 0
      nginx-proxy/test/test_headers/test_http.py
  68. 14 0
      nginx-proxy/test/test_headers/test_http.yml
  69. 82 0
      nginx-proxy/test/test_headers/test_https.py
  70. 16 0
      nginx-proxy/test/test_headers/test_https.yml
  71. 35 0
      nginx-proxy/test/test_ipv6.py
  72. 24 0
      nginx-proxy/test/test_ipv6.yml
  73. 16 0
      nginx-proxy/test/test_multiple-hosts.py
  74. 14 0
      nginx-proxy/test/test_multiple-hosts.yml
  75. 15 0
      nginx-proxy/test/test_multiple-networks.py
  76. 35 0
      nginx-proxy/test/test_multiple-networks.yml
  77. 7 0
      nginx-proxy/test/test_multiple-ports/test_VIRTUAL_PORT.py
  78. 15 0
      nginx-proxy/test/test_multiple-ports/test_VIRTUAL_PORT.yml
  79. 7 0
      nginx-proxy/test/test_multiple-ports/test_default-80.py
  80. 14 0
      nginx-proxy/test/test_multiple-ports/test_default-80.yml
  81. 7 0
      nginx-proxy/test/test_multiple-ports/test_single-port-not-80.py
  82. 14 0
      nginx-proxy/test/test_multiple-ports/test_single-port-not-80.yml
  83. 24 0
      nginx-proxy/test/test_nominal.py
  84. 22 0
      nginx-proxy/test/test_nominal.yml
  85. 70 0
      nginx-proxy/test/test_ssl/certs/nginx-proxy.tld.crt
  86. 27 0
      nginx-proxy/test/test_ssl/certs/nginx-proxy.tld.key
  87. 71 0
      nginx-proxy/test/test_ssl/certs/web2.nginx-proxy.tld.crt
  88. 27 0
      nginx-proxy/test/test_ssl/certs/web2.nginx-proxy.tld.key
  89. 71 0
      nginx-proxy/test/test_ssl/certs/web3.nginx-proxy.tld.crt
  90. 27 0
      nginx-proxy/test/test_ssl/certs/web3.nginx-proxy.tld.key
  91. 93 0
      nginx-proxy/test/test_ssl/test_dhparam.py
  92. 16 0
      nginx-proxy/test/test_ssl/test_dhparam.yml
  93. 44 0
      nginx-proxy/test/test_ssl/test_dhparam_generation.py
  94. 8 0
      nginx-proxy/test/test_ssl/test_dhparam_generation.yml
  95. 26 0
      nginx-proxy/test/test_ssl/test_hsts.py
  96. 32 0
      nginx-proxy/test/test_ssl/test_hsts.yml
  97. 18 0
      nginx-proxy/test/test_ssl/test_nohttp.py
  98. 16 0
      nginx-proxy/test/test_ssl/test_nohttp.yml
  99. 12 0
      nginx-proxy/test/test_ssl/test_nohttps.py
  100. 0 0
      nginx-proxy/test/test_ssl/test_nohttps.yml

+ 37 - 0
nginx-proxy/Dockerfile

@@ -0,0 +1,37 @@
+FROM nginx:1.14
+LABEL maintainer="Jason Wilder mail@jasonwilder.com"
+
+# Install wget and install/updates certificates
+RUN apt-get update \
+ && apt-get install -y -q --no-install-recommends \
+    ca-certificates \
+    wget \
+ && apt-get clean \
+ && rm -r /var/lib/apt/lists/*
+
+
+# Configure Nginx and apply fix for very long server names
+RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
+ && sed -i 's/worker_processes  1/worker_processes  auto/' /etc/nginx/nginx.conf
+
+# Install 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.7.4
+
+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 \
+ && rm /docker-gen-linux-amd64-$DOCKER_GEN_VERSION.tar.gz
+
+COPY network_internal.conf /etc/nginx/
+
+COPY . /app/
+WORKDIR /app/
+
+ENV DOCKER_HOST unix:///tmp/docker.sock
+
+VOLUME ["/etc/nginx/certs", "/etc/nginx/dhparam"]
+
+ENTRYPOINT ["/app/docker-entrypoint.sh"]
+CMD ["forego", "start", "-r"]

+ 34 - 0
nginx-proxy/Dockerfile.alpine

@@ -0,0 +1,34 @@
+FROM nginx:1.14-alpine
+LABEL maintainer="Jason Wilder mail@jasonwilder.com"
+
+# Install wget and install/updates certificates
+RUN apk add --no-cache --virtual .run-deps \
+    ca-certificates bash wget openssl \
+    && update-ca-certificates
+
+
+# Configure Nginx and apply fix for very long server names
+RUN echo "daemon off;" >> /etc/nginx/nginx.conf \
+ && sed -i 's/worker_processes  1/worker_processes  auto/' /etc/nginx/nginx.conf
+
+# Install 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.7.4
+
+RUN wget --quiet https://github.com/jwilder/docker-gen/releases/download/$DOCKER_GEN_VERSION/docker-gen-alpine-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \
+ && tar -C /usr/local/bin -xvzf docker-gen-alpine-linux-amd64-$DOCKER_GEN_VERSION.tar.gz \
+ && rm /docker-gen-alpine-linux-amd64-$DOCKER_GEN_VERSION.tar.gz
+
+COPY network_internal.conf /etc/nginx/
+
+COPY . /app/
+WORKDIR /app/
+
+ENV DOCKER_HOST unix:///tmp/docker.sock
+
+VOLUME ["/etc/nginx/certs", "/etc/nginx/dhparam"]
+
+ENTRYPOINT ["/app/docker-entrypoint.sh"]
+CMD ["forego", "start", "-r"]

+ 21 - 0
nginx-proxy/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Jason Wilder
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 16 - 0
nginx-proxy/Makefile

@@ -0,0 +1,16 @@
+.SILENT :
+.PHONY : test-debian test-alpine test
+
+
+update-dependencies:
+	test/requirements/build.sh
+
+test-debian: update-dependencies
+	docker build -t jwilder/nginx-proxy:test .
+	test/pytest.sh
+
+test-alpine: update-dependencies
+	docker build -f Dockerfile.alpine -t jwilder/nginx-proxy:test .
+	test/pytest.sh
+
+test: test-debian test-alpine

+ 2 - 0
nginx-proxy/Procfile

@@ -0,0 +1,2 @@
+dockergen: docker-gen -watch -notify "nginx -s reload" /app/nginx.tmpl /etc/nginx/conf.d/default.conf
+nginx: nginx

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 432 - 0
nginx-proxy/README.md


+ 8 - 0
nginx-proxy/dhparam.pem.default

@@ -0,0 +1,8 @@
+-----BEGIN DH PARAMETERS-----
+MIIBCAKCAQEAzB2nIGzpVq7afJnKBm1X0d64avwOlP2oneiKwxRHdDI/5+6TpH1P
+F8ipodGuZBUMmupoB3D34pu2Qq5boNW983sm18ww9LMz2i/pxhSdB+mYAew+A6h6
+ltQ5pNtyn4NaKw1SDFkqvde3GNPhaWoPDbZDJhpHGblR3w1b/ag+lTLZUvVwcD8L
+jYS9f9YWAC6T7WxAxh4zvu1Z0I1EKde8KYBxrreZNheXpXHqMNyJYZCaY2Hb/4oI
+EL65qZq1GCWezpWMjhk6pOnV5gbvqfhoazCv/4OdRv6RoWOIYBNs9BmGho4AtXqV
+FYLdYDhOvN4aVs9Ir+G8ouwiRnix24+UewIBAg==
+-----END DH PARAMETERS-----

+ 23 - 0
nginx-proxy/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

+ 14 - 0
nginx-proxy/docker-compose.yml

@@ -0,0 +1,14 @@
+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

+ 33 - 0
nginx-proxy/docker-entrypoint.sh

@@ -0,0 +1,33 @@
+#!/bin/bash
+set -e
+
+# Warn if the DOCKER_HOST socket does not exist
+if [[ $DOCKER_HOST = unix://* ]]; then
+	socket_file=${DOCKER_HOST#unix://}
+	if ! [ -S $socket_file ]; then
+		cat >&2 <<-EOT
+			ERROR: you need to share your Docker host socket with a volume at $socket_file
+			Typically you should run your jwilder/nginx-proxy with: \`-v /var/run/docker.sock:$socket_file:ro\`
+			See the documentation at http://git.io/vZaGJ
+		EOT
+		socketMissing=1
+	fi
+fi
+
+# Generate dhparam file if required
+# Note: if $DHPARAM_BITS is not defined, generate-dhparam.sh will use 2048 as a default
+/app/generate-dhparam.sh $DHPARAM_BITS
+
+# Compute the DNS resolvers for use in the templates - if the IP contains ":", it's IPv6 and must be enclosed in []
+export RESOLVERS=$(awk '$1 == "nameserver" {print ($2 ~ ":")? "["$2"]": $2}' ORS=' ' /etc/resolv.conf | sed 's/ *$//g')
+if [ "x$RESOLVERS" = "x" ]; then
+    echo "Warning: unable to determine DNS resolvers for nginx" >&2
+    unset RESOLVERS
+fi
+
+# If the user has run the default command and the socket doesn't exist, fail
+if [ "$socketMissing" = 1 -a "$1" = forego -a "$2" = start -a "$3" = '-r' ]; then
+	exit 1
+fi
+
+exec "$@"

+ 45 - 0
nginx-proxy/generate-dhparam.sh

@@ -0,0 +1,45 @@
+#!/bin/bash -e
+
+# The first argument is the bit depth of the dhparam, or 2048 if unspecified
+DHPARAM_BITS=${1:-2048}
+
+# If a dhparam file is not available, use the pre-generated one and generate a new one in the background.
+# Note that /etc/nginx/dhparam is a volume, so this dhparam will persist restarts.
+PREGEN_DHPARAM_FILE="/app/dhparam.pem.default"
+DHPARAM_FILE="/etc/nginx/dhparam/dhparam.pem"
+GEN_LOCKFILE="/tmp/dhparam_generating.lock"
+
+# The hash of the pregenerated dhparam file is used to check if the pregen dhparam is already in use
+PREGEN_HASH=$(md5sum $PREGEN_DHPARAM_FILE | cut -d" " -f1)
+if [[ -f $DHPARAM_FILE ]]; then
+    CURRENT_HASH=$(md5sum $DHPARAM_FILE | cut -d" " -f1)
+    if [[ $PREGEN_HASH != $CURRENT_HASH ]]; then
+        # There is already a dhparam, and it's not the default
+        echo "Custom dhparam.pem file found, generation skipped"
+        exit 0
+    fi
+
+    if [[ -f $GEN_LOCKFILE ]]; then
+        # Generation is already in progress
+        exit 0
+    fi
+fi
+
+cat >&2 <<-EOT
+WARNING: $DHPARAM_FILE was not found. A pre-generated dhparam.pem will be used for now while a new one
+is being generated in the background.  Once the new dhparam.pem is in place, nginx will be reloaded.
+EOT
+
+# Put the default dhparam file in place so we can start immediately
+cp $PREGEN_DHPARAM_FILE $DHPARAM_FILE
+touch $GEN_LOCKFILE
+
+# Generate a new dhparam in the background in a low priority and reload nginx when finished (grep removes the progress indicator).
+(
+    (
+        nice -n +5 openssl dhparam -out $DHPARAM_FILE $DHPARAM_BITS 2>&1 \
+        && echo "dhparam generation complete, reloading nginx" \
+        && nginx -s reload
+    ) | grep -vE '^[\.+]+'
+    rm $GEN_LOCKFILE
+) &disown

+ 6 - 0
nginx-proxy/network_internal.conf

@@ -0,0 +1,6 @@
+# Only allow traffic from internal clients
+allow 127.0.0.0/8;
+allow 10.0.0.0/8;
+allow 192.168.0.0/16;
+allow 172.16.0.0/12;
+deny all;

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 361 - 0
nginx-proxy/nginx.tmpl


+ 107 - 0
nginx-proxy/test/README.md

@@ -0,0 +1,107 @@
+Nginx proxy test suite
+======================
+
+Install requirements
+--------------------
+
+You need [python 2.7](https://www.python.org/) and [pip](https://pip.pypa.io/en/stable/installing/) installed. Then run the commands:
+
+    requirements/build.sh
+    pip install -r requirements/python-requirements.txt
+
+If you can't install those requirements on your computer, you can alternatively use the _pytest.sh_ script which will run the tests from a Docker container which has those requirements.
+
+
+Prepare the nginx-proxy test image
+----------------------------------
+
+    docker build -t jwilder/nginx-proxy:test ..
+
+or if you want to test the alpine flavor:
+
+    docker build -t jwilder/nginx-proxy:test -f Dockerfile.alpine ..
+
+make sure to tag that test image exactly `jwilder/nginx-proxy:test` or the test suite won't work.
+
+
+Run the test suite
+------------------
+
+    pytest
+
+need more verbosity ?
+
+    pytest -s
+
+
+Run one single test module
+--------------------------
+
+    pytest test_nominal.py
+
+
+Write a test module
+-------------------
+
+This test suite uses [pytest](http://doc.pytest.org/en/latest/). The [conftest.py](conftest.py) file will be automatically loaded by pytest and will provide you with two useful pytest [fixtures](http://doc.pytest.org/en/latest/fixture.html#fixture): 
+
+- docker_compose
+- nginxproxy
+
+
+### docker_compose fixture
+
+When using the `docker_compose` fixture in a test, pytest will try to find a yml file named after your test module filename. For instance, if your test module is `test_example.py`, then the `docker_compose` fixture will try to load a `test_example.yml` [docker compose file](https://docs.docker.com/compose/compose-file/).
+
+Once the docker compose file found, the fixture will remove all containers, run `docker-compose up`, and finally your test will be executed.
+
+The fixture will run the _docker-compose_ command with the `-f` option to load the given compose file. So you can test your docker compose file syntax by running it yourself with:
+
+    docker-compose -f test_example.yml up -d
+
+In the case you are running pytest from within a docker container, the `docker_compose` fixture will make sure the container running pytest is attached to all docker networks. That way, your test will be able to reach any of them.
+
+In your tests, you can use the `docker_compose` variable to query and command the docker daemon as it provides you with a [client from the docker python module](https://docker-py.readthedocs.io/en/2.0.2/client.html#client-reference).
+
+Also this fixture alters the way the python interpreter resolves domain names to IP addresses in the following ways:
+
+Any domain name containing the substring `nginx-proxy` will resolve to the IP address of the container that was created from the `jwilder/nginx-proxy:test` image. So all the following domain names will resolve to the nginx-proxy container in tests:
+- `nginx-proxy`
+- `nginx-proxy.com`
+- `www.nginx-proxy.com`
+- `www.nginx-proxy.test`
+- `www.nginx-proxy`
+- `whatever.nginx-proxyooooooo`
+- ...
+
+Any domain name ending with `XXX.container.docker` will resolve to the IP address of the XXX container.
+- `web1.container.docker` will resolve to the IP address of the `web1` container
+- `f00.web1.container.docker` will resolve to the IP address of the `web1` container
+- `anything.whatever.web2.container.docker` will resolve to the IP address of the `web2` container
+
+Otherwise, domain names are resoved as usual using your system DNS resolver.
+
+
+### nginxproxy fixture
+
+The `nginxproxy` fixture will provide you with a replacement for the python [requests](https://pypi.python.org/pypi/requests/) module. This replacement will just repeat up to 30 times a requests if it receives the HTTP error 404 or 502. This error occurs when you try to send queries to nginx-proxy too early after the container creation.
+
+Also this requests replacement is preconfigured to use the Certificate Authority root certificate [certs/ca-root.crt](certs/) to validate https connections.
+
+Furthermore, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not supported by the system or docker, that particular test will be skipped.
+
+    def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
+        r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
+        assert r.status_code == 200   
+        assert r.text == "answer from port 81\n"
+
+
+
+### The web docker image
+
+When you ran the `requirements/build.sh` script earlier, you built a [`web`](requirements/README.md) docker image which is convenient for running a small web server in a container. This image can produce containers that listens on multiple ports at the same time.
+
+
+### Testing TLS
+
+If you need to create server certificates, use the [`certs/create_server_certificate.sh`](certs/) script. Pytest will be able to validate any certificate issued from this script.

+ 81 - 0
nginx-proxy/test/certs/README.md

@@ -0,0 +1,81 @@
+create_server_certificate.sh
+============================
+
+`create_server_certificate.sh` is a script helping with issuing server certificates that can be used to provide TLS on web servers.
+
+It also creates a Certificate Authority (CA) root key and certificate. This CA root certificate can be used to validate the server certificates it generates.
+
+For instance, with _curl_:
+
+    curl --cacert /somewhere/ca-root.crt https://www.example.com/
+
+or with _wget_:
+
+    wget --certificate=/somewhere/ca-root.crt https://www.example.com/
+
+or with the python _requests_ module:
+
+    import requests
+    r = requests.get("https://www.example.com", verify="/somewhere/ca-root.crt")
+
+Usage
+-----
+
+### Simple domain
+
+Create a server certificate for domain `www.example.com`:
+
+    ./create_server_certificate.sh www.example.com
+
+Will produce:
+ - `www.example.com.key`
+ - `www.example.com.crt`
+
+
+### Multiple domains 
+
+Create a server certificate for main domain `www.example.com` and alternative domains `example.com`, `foo.com` and `bar.com`:
+
+    ./create_server_certificate.sh www.example.com foo.com bar.com
+
+Will produce:
+ - `www.example.com.key`
+ - `www.example.com.crt`
+ 
+### Wildcard domain
+
+Create a server certificate for wildcard domain `*.example.com`:
+
+    ./create_server_certificate.sh "*.example.com"
+
+Note that you need to use quotes around the domain string or the shell would expand `*`.
+
+Will produce:
+ - `*.example.com.key`
+ - `*.example.com.crt`
+
+Again, to prevent your shell from expanding `*`, use quotes. i.e.: `cat "*.example.com.crt"`.
+
+Such a server certificate would be valid for domains:
+- `foo.example.com` 
+- `bar.example.com`
+
+but not for domains:
+- `example.com`
+- `foo.bar.example.com`
+
+
+### Wildcard domain on multiple levels
+
+While you can technically create a server certificate for wildcard domain `*.example.com` and alternative name `*.*.example.com`, client implementations generally do not support multiple wildcards in a domain name.
+
+For instance, a python script using urllib3 would fail to validate domain `foo.bar.example.com` presenting a certificate with name `*.*.example.com`. It is advised to stay away from producing such certificates.
+
+If you want to give it a try:
+
+    ./create_server_certificate.sh "*.example.com" "*.*.example.com"
+
+Such a server certificate would be valid for domains:
+- `foo.example.com` 
+- `bar.example.com`
+- `foo.bar.example.com`

+ 21 - 0
nginx-proxy/test/certs/ca-root.crt

@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZDCCAkygAwIBAgIJAJmW6Ju6iJNNMA0GCSqGSIb3DQEBCwUAMD8xHzAdBgNV
+BAoMFm5naW54LXByb3h5IHRlc3Qgc3VpdGUxHDAaBgNVBAMME3d3dy5uZ2lueC1w
+cm94eS50bGQwHhcNMTcwMTEwMDAwODUxWhcNMjcwMTA4MDAwODUxWjA/MR8wHQYD
+VQQKDBZuZ2lueC1wcm94eSB0ZXN0IHN1aXRlMRwwGgYDVQQDDBN3d3cubmdpbngt
+cHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAndjE3OPr
+48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGTgBfxqq27fOPfqhE5bi1M5JDk
+KkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBehaDkPdamJ0i3dv6e4kZy41oI
+RqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68LO66bYiHlLNL7ENViSHhLyCmt
+qIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3HEVKBCismcwq80+BD5VS/yW18
+KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0Wscgt7UeggVYKDazjDSPvLUE
+FUN5wEmydkP2AQIDAQABo2MwYTAdBgNVHQ4EFgQUJL59pHomt+8dUNxv8HgrYjKf
+OA8wHwYDVR0jBBgwFoAUJL59pHomt+8dUNxv8HgrYjKfOA8wDwYDVR0TAQH/BAUw
+AwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggEBABALxY96YqsZ
+CL2crzY0FIGhfjLE7P3mtUGklUpFu7xyI6mGUyL1nJYSnHB5IEV6QLhVVUE/CojI
+crXorQWBDkx26AgCt/eIOdvPYC0JDeXiIhH6sld3yH7JGwGqJkfXaUUfUkuwMae7
+mMIEG9e6vfSh/YNTRxs0KBjBcXHHl5K+Dz4h9r14OqnQFqVFZaR6T6td44tDDNhn
+beW8iIfCWRqDsnvIcJzLa2QR4onmJSw5DaSeFFaKefhdHEzEBZntLfyFbjRYHT/O
++BRdewhg6rSDkGLcL8n/ZnRLOa+xmegjQ/Op94OmWO3TfXOITJAtkaO2YVZoyek8
+T6ckVovq4zU=
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/certs/ca-root.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAndjE3OPr48hIOQigk/HejrowsQDLNfkkc6vej0J983rJitGT
+gBfxqq27fOPfqhE5bi1M5JDkKkrOrSitxCJLgpq+4Ls9/RXg8skZFHRAQbNwuKBe
+haDkPdamJ0i3dv6e4kZy41oIRqxQ/MKdminC4LShFZvPoKeh9ae7w1MgB2/4E68L
+O66bYiHlLNL7ENViSHhLyCmtqIE7kdV9jgn2NuVJ37m6/6SNQ3GBiIjEW+ooRQ3H
+EVKBCismcwq80+BD5VS/yW18KqX8m4sBM+IgZbcOqrV+APMbGvd8iNJgQSSQC/r0
+Wscgt7UeggVYKDazjDSPvLUEFUN5wEmydkP2AQIDAQABAoIBAQCDM4zetix6lx1B
+GuSuVFrTc/vJBInkgQRFiVRi67fZS/R+CJl73WsonWO7+YUNzWdZJxpE2hJs/OUx
+lSBqaL8u/gUuszRhS3BBHdpU4BQRCF/ndpVaqVNN+z78ZDrrE9Vo63nPdCRw6gYf
+MnzhiVjMghdq6Kn6NZwvno45WrzCsIbrrQ4zU+S2PhG8MTA53jzqqQ8mUSJX0lAl
+6b9+1aWA0d0Jnk3M3doaFU/Dlnz3n6kkx0AdqNe8bdsFrPfwsrF+dwGx04SGgLmK
+V2OjIDFYYGtiHp3PJ9IYIA32ij+UloSDDZ2BxXkma8Zilw04ytY5l8tlk2ZDWTD9
+U2MXxjmBAoGBAMmmI19I/asTPjljlqzrOsrdRkklJvnCHgy/yw9u3nMfkJ0lLGAp
+mZoCqJIEsAqlLGM5bOjKy3KQ3n2SBX3mz7/RajnpJRTnNLeJIPAAXHN9TDyKcWRo
+Los6xHN7YMSLYKs4HMihXp9Yu4Ms88/8nO/01nufjN0rTgFnWdL0WfxJAoGBAMhk
+Qm92ukMmbrXSrV0WF+eFooHwgPmUWZ1oZY5ZHmO3FCuSBHiICGrWKmdbcG6H5zmZ
+oFZ0unsvk2Yjl+/+tntxr/dwp6Q+chsqkLms8GE76NWEO8qn4hQNywkFgpKlPci3
+n5IqpuQ2DpJ1PAQkwgZD/5rSscNidNMezXO5Uvv5AoGBALR291kjXcJpKlr6AbMn
+oipD9c8obMVBMNuAGh7pvjORoD7DMf+tu0XV8z8a6uHcCOmUTx/XvlP9yuDeegO/
+OVYV+NdzDDi04r0PAGdKK3NAQ6Y60Fhn1J/OLFqdpHDBu/X/9eKoaKJ7KvWumVUe
+YuVtXTauB8c4JkujTwQ4ov/hAoGAHxvhbGhkFhSbT0K7gx3w7BJE3iM2AojTOKqC
+SYzwOM6tJO5wHz4PAHbq8kyxsZcLgFenGoTYhlMmcM7JwYorThKiHKmyfL7s++ap
+vQlp785bIPp8RcO2RyK1CFuAn79jTgujjA9vBTKXJIlqncIPFOXtgl1/FzPrqvK3
+NmXoyhECgYEAje9hM9RYO0jbfmTZoQh+onMRz34SM9XWLH+NQGgfvsGtjeRnrUKK
+GuWQz/GQGJLy/Uc1KHIdrfPDjvQhZXmPL1v7pNfCrqyj+EnKCNDPPnYq5Zq4WLsB
+x1hKPH0LmfEBkXOiFGrD3h3KAuBK5nb0/EFBDR4JuMaySC5CpbOds9o=
+-----END RSA PRIVATE KEY-----

+ 183 - 0
nginx-proxy/test/certs/create_server_certificate.sh

@@ -0,0 +1,183 @@
+#!/bin/bash
+set -u
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+if [[ "$#" -eq 0 ]]; then
+	cat <<-EOF
+
+	To generate a server certificate, provide the domain name as a parameter:
+	    $(basename $0) www.my-domain.tdl
+	    $(basename $0) www.my-domain.tdl alternate.domain.tld
+
+	You can also create certificates for wildcard domains:
+	    $(basename $0) '*.my-domain.tdl'
+
+	EOF
+	exit 0
+else
+	DOMAIN="$1"
+	ALTERNATE_DOMAINS="DNS:$( echo "$@" | sed 's/ /,DNS:/g')"
+fi
+
+
+###############################################################################
+# Create a nginx container (which conveniently provides the `openssl` command)
+###############################################################################
+
+CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.14)
+# Configure openssl
+docker exec $CONTAINER bash -c '
+	mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null
+	echo 1000 > /ca/serial
+	touch /ca/index.txt
+	cat > /ca/openssl.cnf <<-"OESCRIPT"
+		[ ca ]
+		# `man ca`
+		default_ca = CA_default
+
+		[ CA_default ]
+		# Directory and file locations.
+		dir               = /ca
+		certs             = $dir/certs
+		crl_dir           = $dir/crl
+		new_certs_dir     = $dir/newcerts
+		database          = $dir/index.txt
+		serial            = $dir/serial
+		RANDFILE          = $dir/private/.rand
+
+		# The root key and root certificate.
+		private_key       = /work/ca-root.key
+		certificate       = /work/ca-root.crt
+
+		# SHA-1 is deprecated, so use SHA-2 instead.
+		default_md        = sha256
+
+		name_opt          = ca_default
+		cert_opt          = ca_default
+		default_days      = 10000
+		preserve          = no
+		policy            = policy_loose
+
+		[ policy_loose ]
+		countryName             = optional
+		stateOrProvinceName     = optional
+		localityName            = optional
+		organizationName        = optional
+		organizationalUnitName  = optional
+		commonName              = supplied
+		emailAddress            = optional
+
+		[ req ]
+		# Options for the `req` tool (`man req`).
+		default_bits        = 2048
+		distinguished_name  = req_distinguished_name
+		string_mask         = utf8only
+
+		# SHA-1 is deprecated, so use SHA-2 instead.
+		default_md          = sha256
+
+		# Extension to add when the -x509 option is used.
+		x509_extensions     = v3_ca
+
+		[ req_distinguished_name ]
+		# See <https://en.wikipedia.org/wiki/Certificate_signing_request>.
+		countryName                     = Country Name (2 letter code)
+		stateOrProvinceName             = State or Province Name
+		localityName                    = Locality Name
+		0.organizationName              = Organization Name
+		organizationalUnitName          = Organizational Unit Name
+		commonName                      = Common Name
+		emailAddress                    = Email Address
+
+		[ v3_ca ]
+		# Extensions for a typical CA (`man x509v3_config`).
+		subjectKeyIdentifier = hash
+		authorityKeyIdentifier = keyid:always,issuer
+		basicConstraints = critical, CA:true
+		keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+
+		[ server_cert ]
+		# Extensions for server certificates (`man x509v3_config`).
+		basicConstraints = CA:FALSE
+		nsCertType = server
+		nsComment = server certificate generated for test purpose (nginx-proxy test suite)
+		subjectKeyIdentifier = hash
+		authorityKeyIdentifier = keyid,issuer:always
+		keyUsage = critical, digitalSignature, keyEncipherment
+		extendedKeyUsage = serverAuth
+
+		[ san_env ]
+		subjectAltName=${ENV::SAN}
+	OESCRIPT
+'
+
+# shortcut for calling `openssl` inside the container
+function openssl {
+	docker exec $CONTAINER openssl "$@"
+}
+
+function exitfail {
+		echo
+		echo ERROR: "$@"
+		docker rm -f $CONTAINER
+		exit 1
+}
+
+
+###############################################################################
+# Setup Certificate authority
+###############################################################################
+
+if ! [[ -f "$DIR/ca-root.key" ]]; then
+	echo
+	echo "> Create a Certificate Authority root key: $DIR/ca-root.key"
+	openssl genrsa -out ca-root.key 2048
+	[[ $? -eq 0 ]] || exitfail failed to generate CA root key
+fi
+
+# Create a CA root certificate
+if ! [[ -f "$DIR/ca-root.crt" ]]; then
+	echo
+	echo "> Create a CA root certificate: $DIR/ca-root.crt"
+	openssl req -config /ca/openssl.cnf \
+	-key ca-root.key \
+	-new -x509 -days 3650 -subj "/O=nginx-proxy test suite/CN=www.nginx-proxy.tld" -extensions v3_ca \
+	-out ca-root.crt
+	[[ $? -eq 0 ]] || exitfail failed to generate CA root certificate
+
+	# Verify certificate
+	openssl x509 -noout -text -in ca-root.crt
+fi
+
+
+###############################################################################
+# create server key and certificate signed by the certificate authority
+###############################################################################
+
+echo
+echo "> Create a host key: $DIR/$DOMAIN.key"
+openssl genrsa -out "$DOMAIN.key" 2048
+
+echo
+echo "> Create a host certificate signing request"
+
+SAN="$ALTERNATE_DOMAINS" openssl req -config /ca/openssl.cnf \
+	-key "$DOMAIN.key" \
+	-new -out "/ca/$DOMAIN.csr" -days 1000 -extensions san_env -subj "/CN=$DOMAIN"
+	[[ $? -eq 0 ]] || exitfail failed to generate server certificate signing request
+
+echo
+echo "> Create server certificate: $DIR/$DOMAIN.crt"
+SAN="$ALTERNATE_DOMAINS" openssl ca -config /ca/openssl.cnf -batch \
+		-extensions server_cert \
+		-extensions san_env \
+		-in "/ca/$DOMAIN.csr" \
+		-out "$DOMAIN.crt"
+	[[ $? -eq 0 ]] || exitfail failed to generate server certificate
+
+
+# Verify host certificate
+#openssl x509 -noout -text -in "$DOMAIN.crt"
+
+
+docker rm -f $CONTAINER >/dev/null

+ 473 - 0
nginx-proxy/test/conftest.py

@@ -0,0 +1,473 @@
+from __future__ import print_function
+import contextlib
+import logging
+import os
+import shlex
+import socket
+import subprocess
+import time
+import re
+
+import backoff
+import docker
+import pytest
+import requests
+from _pytest._code.code import ReprExceptionInfo
+from requests.packages.urllib3.util.connection import HAS_IPV6
+
+logging.basicConfig(level=logging.INFO)
+logging.getLogger('backoff').setLevel(logging.INFO)
+logging.getLogger('DNS').setLevel(logging.DEBUG)
+logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.WARN)
+
+CA_ROOT_CERTIFICATE = os.path.join(os.path.dirname(__file__), 'certs/ca-root.crt')
+I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER = os.path.isfile("/.dockerenv")
+FORCE_CONTAINER_IPV6 = False  # ugly global state to consider containers' IPv6 address instead of IPv4
+
+
+docker_client = docker.from_env()
+
+
+###############################################################################
+# 
+# utilities
+# 
+###############################################################################
+
+@contextlib.contextmanager
+def ipv6(force_ipv6=True):
+    """
+    Meant to be used as a context manager to force IPv6 sockets:
+
+        with ipv6():
+            nginxproxy.get("http://something.nginx-proxy.local")  # force use of IPv6
+
+        with ipv6(False):
+            nginxproxy.get("http://something.nginx-proxy.local")  # legacy behavior
+
+
+    """
+    global FORCE_CONTAINER_IPV6
+    FORCE_CONTAINER_IPV6 = force_ipv6
+    yield
+    FORCE_CONTAINER_IPV6 = False
+
+
+class requests_for_docker(object):
+    """
+    Proxy for calling methods of the requests module. 
+    When a HTTP response failed due to HTTP Error 404 or 502, retry a few times.
+    Provides method `get_conf` to extract the nginx-proxy configuration content.
+    """
+    def __init__(self):
+        self.session = requests.Session()
+        if os.path.isfile(CA_ROOT_CERTIFICATE):
+            self.session.verify = CA_ROOT_CERTIFICATE
+
+    def get_conf(self):
+        """
+        Return the nginx config file
+        """
+        nginx_proxy_containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
+        if len(nginx_proxy_containers) > 1:
+            pytest.fail("Too many running jwilder/nginx-proxy:test containers", pytrace=False)
+        elif len(nginx_proxy_containers) == 0:
+            pytest.fail("No running jwilder/nginx-proxy:test container", pytrace=False)
+        return get_nginx_conf_from_container(nginx_proxy_containers[0])
+
+    def get(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _get(*args, **kwargs):
+                return self.session.get(*args, **kwargs)
+            return _get(*args, **kwargs)
+
+    def post(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _post(*args, **kwargs):
+                return self.session.post(*args, **kwargs)
+            return _post(*args, **kwargs)
+
+    def put(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _put(*args, **kwargs):
+                return self.session.put(*args, **kwargs)
+            return _put(*args, **kwargs)
+
+    def head(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _head(*args, **kwargs):
+                return self.session.head(*args, **kwargs)
+            return _head(*args, **kwargs)
+
+    def delete(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _delete(*args, **kwargs):
+                return self.session.delete(*args, **kwargs)
+            return _delete(*args, **kwargs)
+
+    def options(self, *args, **kwargs):
+        with ipv6(kwargs.pop('ipv6', False)):
+            @backoff.on_predicate(backoff.constant, lambda r: r.status_code in (404, 502), interval=.3, max_tries=30, jitter=None)
+            def _options(*args, **kwargs):
+                return self.session.options(*args, **kwargs)
+            return _options(*args, **kwargs)
+
+    def __getattr__(self, name):
+        return getattr(requests, name)
+
+
+def container_ip(container):
+    """
+    return the IP address of a container.
+
+    If the global FORCE_CONTAINER_IPV6 flag is set, return the IPv6 address
+    """
+    global FORCE_CONTAINER_IPV6
+    if FORCE_CONTAINER_IPV6:
+        if not HAS_IPV6:
+            pytest.skip("This system does not support IPv6")
+        ip = container_ipv6(container)
+        if ip == '':
+            pytest.skip("Container %s has no IPv6 address" % container.name)
+        else:
+            return ip
+    else:
+        net_info = container.attrs["NetworkSettings"]["Networks"]
+        if "bridge" in net_info:
+            return net_info["bridge"]["IPAddress"]
+
+        # not default bridge network, fallback on first network defined
+        network_name = net_info.keys()[0]
+        return net_info[network_name]["IPAddress"]
+
+
+def container_ipv6(container):
+    """
+    return the IPv6 address of a container.
+    """
+    net_info = container.attrs["NetworkSettings"]["Networks"]
+    if "bridge" in net_info:
+        return net_info["bridge"]["GlobalIPv6Address"]
+
+    # not default bridge network, fallback on first network defined
+    network_name = net_info.keys()[0]
+    return net_info[network_name]["GlobalIPv6Address"]
+
+
+def nginx_proxy_dns_resolver(domain_name):
+    """
+    if "nginx-proxy" if found in host, return the ip address of the docker container
+    issued from the docker image jwilder/nginx-proxy:test.
+
+    :return: IP or None
+    """
+    log = logging.getLogger('DNS')
+    log.debug("nginx_proxy_dns_resolver(%r)" % domain_name)
+    if 'nginx-proxy' in domain_name:
+        nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "jwilder/nginx-proxy:test"})
+        if len(nginxproxy_containers) == 0:
+            log.warn("no container found from image jwilder/nginx-proxy:test while resolving %r", domain_name)
+            return
+        nginxproxy_container = nginxproxy_containers[0]
+        ip = container_ip(nginxproxy_container)
+        log.info("resolving domain name %r as IP address %s of nginx-proxy container %s" % (domain_name, ip, nginxproxy_container.name))
+        return ip
+
+def docker_container_dns_resolver(domain_name):
+    """
+    if domain name is of the form "XXX.container.docker" or "anything.XXX.container.docker", return the ip address of the docker container
+    named XXX.
+
+    :return: IP or None
+    """
+    log = logging.getLogger('DNS')
+    log.debug("docker_container_dns_resolver(%r)" % domain_name)
+
+    match = re.search('(^|.+\.)(?P<container>[^.]+)\.container\.docker$', domain_name)
+    if not match:
+        log.debug("%r does not match" % domain_name)
+        return
+
+    container_name = match.group('container')
+    log.debug("looking for container %r" % container_name)
+    try:
+        container = docker_client.containers.get(container_name)
+    except docker.errors.NotFound:
+        log.warn("container named %r not found while resolving %r" % (container_name, domain_name))
+        return
+    log.debug("container %r found (%s)" % (container.name, container.short_id))
+
+    ip = container_ip(container)
+    log.info("resolving domain name %r as IP address %s of container %s" % (domain_name, ip, container.name))
+    return ip 
+
+
+def monkey_patch_urllib_dns_resolver():
+    """
+    Alter the behavior of the urllib DNS resolver so that any domain name
+    containing substring 'nginx-proxy' will resolve to the IP address
+    of the container created from image 'jwilder/nginx-proxy:test'.
+    """
+    prv_getaddrinfo = socket.getaddrinfo
+    dns_cache = {}
+    def new_getaddrinfo(*args):
+        logging.getLogger('DNS').debug("resolving domain name %s" % repr(args))
+        _args = list(args)
+
+        # custom DNS resolvers
+        ip = nginx_proxy_dns_resolver(args[0])
+        if ip is None:
+            ip = docker_container_dns_resolver(args[0])
+        if ip is not None:
+            _args[0] = ip
+
+        # call on original DNS resolver, with eventually the original host changed to the wanted IP address
+        try:
+            return dns_cache[tuple(_args)]
+        except KeyError:
+            res = prv_getaddrinfo(*_args)
+            dns_cache[tuple(_args)] = res
+            return res
+    socket.getaddrinfo = new_getaddrinfo
+    return prv_getaddrinfo
+
+def restore_urllib_dns_resolver(getaddrinfo_func):
+    socket.getaddrinfo = getaddrinfo_func
+
+
+def remove_all_containers():
+    for container in docker_client.containers.list(all=True):
+        if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and container.id.startswith(socket.gethostname()):
+            continue  # pytest is running within a Docker container, so we do not want to remove that particular container
+        logging.info("removing container %s" % container.name)
+        container.remove(v=True, force=True)
+
+
+def get_nginx_conf_from_container(container):
+    """
+    return the nginx /etc/nginx/conf.d/default.conf file content from a container
+    """
+    import tarfile
+    from cStringIO import StringIO
+    strm, stat = container.get_archive('/etc/nginx/conf.d/default.conf')
+    with tarfile.open(fileobj=StringIO(strm.read())) as tf:
+        conffile = tf.extractfile('default.conf')
+        return conffile.read()
+
+
+def docker_compose_up(compose_file='docker-compose.yml'):
+    logging.info('docker-compose -f %s up -d' % compose_file)
+    try:
+        subprocess.check_output(shlex.split('docker-compose -f %s up -d' % compose_file), stderr=subprocess.STDOUT)
+    except subprocess.CalledProcessError, e:
+        pytest.fail("Error while runninng 'docker-compose -f %s up -d':\n%s" % (compose_file, e.output), pytrace=False)
+
+
+def docker_compose_down(compose_file='docker-compose.yml'):
+    logging.info('docker-compose -f %s down' % compose_file)
+    try:
+        subprocess.check_output(shlex.split('docker-compose -f %s down' % compose_file), stderr=subprocess.STDOUT)
+    except subprocess.CalledProcessError, e:
+        pytest.fail("Error while runninng 'docker-compose -f %s down':\n%s" % (compose_file, e.output), pytrace=False)
+
+
+def wait_for_nginxproxy_to_be_ready():
+    """
+    If one (and only one) container started from image jwilder/nginx-proxy:test is found, 
+    wait for its log to contain substring "Watching docker events"
+    """
+    containers = docker_client.containers.list(filters={"ancestor": "jwilder/nginx-proxy:test"})
+    if len(containers) != 1:
+        return
+    container = containers[0]
+    for line in container.logs(stream=True):
+        if "Watching docker events" in line:
+            logging.debug("nginx-proxy ready")
+            break
+
+def find_docker_compose_file(request):
+    """
+    helper for fixture functions to figure out the name of the docker-compose file to consider.
+
+    - if the test module provides a `docker_compose_file` variable, take that
+    - else, if a yaml file exists with the same name as the test module (but for the `.yml` extension), use that
+    - otherwise use `docker-compose.yml`.
+    """
+    test_module_dir = os.path.dirname(request.module.__file__)
+    yml_file = os.path.join(test_module_dir, request.module.__name__ + '.yml')
+    yaml_file = os.path.join(test_module_dir, request.module.__name__ + '.yaml')
+    default_file = os.path.join(test_module_dir, 'docker-compose.yml')
+
+    docker_compose_file_module_variable = getattr(request.module, "docker_compose_file", None)
+    if docker_compose_file_module_variable is not None:
+        docker_compose_file = os.path.join( test_module_dir, docker_compose_file_module_variable)
+        if not os.path.isfile(docker_compose_file):
+            raise ValueError("docker compose file %r could not be found. Check your test module `docker_compose_file` variable value." % docker_compose_file)
+    else:
+        if os.path.isfile(yml_file):
+            docker_compose_file = yml_file
+        elif os.path.isfile(yaml_file):
+            docker_compose_file = yaml_file
+        else:
+            docker_compose_file = default_file
+
+    if not os.path.isfile(docker_compose_file):
+        logging.error("Could not find any docker-compose file named either '{0}.yml', '{0}.yaml' or 'docker-compose.yml'".format(request.module.__name__))
+
+    logging.debug("using docker compose file %s" % docker_compose_file)
+    return docker_compose_file
+
+
+def connect_to_network(network):
+    """
+    If we are running from a container, connect our container to the given network
+
+    :return: the name of the network we were connected to, or None
+    """
+    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
+        try:
+            my_container = docker_client.containers.get(socket.gethostname())
+        except docker.errors.NotFound:
+            logging.warn("container %r not found" % socket.gethostname())
+            return
+
+        # figure out our container networks
+        my_networks = my_container.attrs["NetworkSettings"]["Networks"].keys()
+
+        # make sure our container is connected to the nginx-proxy's network
+        if network not in my_networks:
+            logging.info("Connecting to docker network: %s" % network.name)
+            network.connect(my_container)
+            return network
+
+
+def disconnect_from_network(network=None):
+    """
+    If we are running from a container, disconnect our container from the given network.
+
+    :param network: name of a docker network to disconnect from
+    """
+    if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None:
+        try:
+            my_container = docker_client.containers.get(socket.gethostname())
+        except docker.errors.NotFound:
+            logging.warn("container %r not found" % socket.gethostname())
+            return
+
+        # figure out our container networks
+        my_networks_names = my_container.attrs["NetworkSettings"]["Networks"].keys()
+
+        # disconnect our container from the given network
+        if network.name in my_networks_names:
+            logging.info("Disconnecting from network %s" % network.name)
+            network.disconnect(my_container)
+
+
+def connect_to_all_networks():
+    """
+    If we are running from a container, connect our container to all current docker networks.
+
+    :return: a list of networks we connected to
+    """
+    if not I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER:
+        return []
+    else:
+        # find the list of docker networks
+        networks = filter(lambda network: len(network.containers) > 0 and network.name != 'bridge', docker_client.networks.list())
+        return [connect_to_network(network) for network in networks]
+
+
+###############################################################################
+# 
+# Py.test fixtures
+# 
+###############################################################################
+
+@pytest.yield_fixture(scope="module")
+def docker_compose(request):
+    """
+    pytest fixture providing containers described in a docker compose file. After the tests, remove the created containers
+    
+    A custom docker compose file name can be defined in a variable named `docker_compose_file`.
+    
+    Also, in the case where pytest is running from a docker container, this fixture makes sure
+    our container will be attached to all the docker networks.
+    """
+    docker_compose_file = find_docker_compose_file(request)
+    original_dns_resolver = monkey_patch_urllib_dns_resolver()
+    remove_all_containers()
+    docker_compose_up(docker_compose_file)
+    networks = connect_to_all_networks()
+    wait_for_nginxproxy_to_be_ready()
+    time.sleep(3)  # give time to containers to be ready
+    yield docker_client
+    for network in networks:
+        disconnect_from_network(network)
+    docker_compose_down(docker_compose_file)
+    restore_urllib_dns_resolver(original_dns_resolver)
+
+
+@pytest.yield_fixture()
+def nginxproxy():
+    """
+    Provides the `nginxproxy` object that can be used in the same way the requests module is:
+
+    r = nginxproxy.get("http://foo.com")
+
+    The difference is that in case an HTTP requests has status code 404 or 502 (which mostly
+    indicates that nginx has just reloaded), we retry up to 30 times the query.
+
+    Also, the nginxproxy methods accept an additional keyword parameter: `ipv6` which forces requests
+    made against containers to use the containers IPv6 address when set to `True`. If IPv6 is not
+    supported by the system or docker, that particular test will be skipped.
+    """
+    yield requests_for_docker()
+
+
+###############################################################################
+# 
+# Py.test hooks
+# 
+###############################################################################
+
+# pytest hook to display additionnal stuff in test report
+def pytest_runtest_logreport(report):
+    if report.failed:
+        if isinstance(report.longrepr, ReprExceptionInfo):
+            test_containers = docker_client.containers.list(all=True, filters={"ancestor": "jwilder/nginx-proxy:test"})
+            for container in test_containers:
+                report.longrepr.addsection('nginx-proxy logs', container.logs())
+                report.longrepr.addsection('nginx-proxy conf', get_nginx_conf_from_container(container))
+
+
+# Py.test `incremental` marker, see http://stackoverflow.com/a/12579625/107049
+def pytest_runtest_makereport(item, call):
+    if "incremental" in item.keywords:
+        if call.excinfo is not None:
+            parent = item.parent
+            parent._previousfailed = item
+
+
+def pytest_runtest_setup(item):
+    previousfailed = getattr(item.parent, "_previousfailed", None)
+    if previousfailed is not None:
+        pytest.xfail("previous test failed (%s)" % previousfailed.name)
+
+###############################################################################
+# 
+# Check requirements
+# 
+###############################################################################
+
+try:
+    docker_client.images.get('jwilder/nginx-proxy:test')
+except docker.errors.ImageNotFound:
+    pytest.exit("The docker image 'jwilder/nginx-proxy:test' is missing")
+
+if docker.__version__ != "2.1.0":
+    pytest.exit("This test suite is meant to work with the python docker module v2.1.0")

+ 8 - 0
nginx-proxy/test/lib/ssl/dhparam.pem

@@ -0,0 +1,8 @@
+-----BEGIN DH PARAMETERS-----
+MIIBCAKCAQEA1cae6HqPSgicEuAuSCf6Ii3d6qMX9Ta8lnwoX0JQ0CWK7mzaiiIi
+dY7oHmc4cq0S3SH+g0tdLP9yqygFS9hdUGINwS2VV6poj2/vdL/dUshegyxpEH58
+nofCPnFDeKkcPDMYAlGS8zjp60TsBkRJKcrxxwnjod1Q5mWuMN5KH3sxs842udKH
+0nHFE9kKW/NfXb+EGsjpocGpf786cGuCO2d00THsoItOEcM9/aI8DX1QcyxAHR6D
+HaYTFJnyyx8Q44u27M15idI4pbNoKORlotiuOwCTGYCfbN14aOV+Ict7aSF8FWpP
+48j9SMNuIu2DlF9pNLo6fsrOjYY3c9X12wIBAg==
+-----END DH PARAMETERS-----

+ 3 - 0
nginx-proxy/test/pytest.ini

@@ -0,0 +1,3 @@
+[pytest]
+# disable the creation of the `.cache` folders
+addopts = -p no:cacheprovider --ignore=requirements --ignore=certs -r s -v

+ 24 - 0
nginx-proxy/test/pytest.sh

@@ -0,0 +1,24 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+# This script is meant to run the test suite from a Docker container.         #
+#                                                                             #
+# This is usefull when you want to run the test suite from Mac or             #
+# Docker Toolbox.                                                             #
+#                                                                             #
+###############################################################################
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+ARGS="$@"
+
+# check requirements
+echo "> Building nginx-proxy-tester image..."
+docker build -t nginx-proxy-tester -f $DIR/requirements/Dockerfile-nginx-proxy-tester $DIR/requirements
+
+# run the nginx-proxy-tester container setting the correct value for the working dir in order for 
+# docker-compose to work properly when run from within that container.
+exec docker run --rm -it \
+	-v ${DIR}:/${DIR} \
+	-w ${DIR} \
+	-v /var/run/docker.sock:/var/run/docker.sock \
+	nginx-proxy-tester ${ARGS}

+ 10 - 0
nginx-proxy/test/requirements/Dockerfile-nginx-proxy-tester

@@ -0,0 +1,10 @@
+FROM python:2.7-alpine
+
+# Note: we're using alpine because it has openssl 1.0.2, which we need for testing
+RUN apk add --update bash openssl curl && rm -rf /var/cache/apk/*
+
+COPY python-requirements.txt /requirements.txt
+RUN pip install -r /requirements.txt
+
+WORKDIR /test
+ENTRYPOINT ["pytest"]

+ 52 - 0
nginx-proxy/test/requirements/README.md

@@ -0,0 +1,52 @@
+This directory contains resources to build Docker images tests depend on
+
+# Build images
+
+    ./build.sh   
+
+
+# python-requirements.txt
+
+If you want to run the test suite from your computer, you need python and a few python modules.
+The _python-requirements.txt_ file describes the python modules required. To install them, use
+pip:
+
+    pip install -r python-requirements.txt
+
+If you don't want to run the test from your computer, you can run the tests from a docker container, see the _pytest.sh_ script.
+
+
+# Images
+
+## web
+
+This container will run one or many webservers, each of them listening on a single port.
+
+Ports are specified using the `WEB_PORTS` environment variable:
+
+    docker run -d -e WEB_PORTS=80 web  # will create a container running one webserver listening on port 80
+    docker run -d -e WEB_PORTS="80 81" web  # will create a container running two webservers, one listening on port 80 and a second one listening on port 81
+
+The webserver answers on two paths:
+
+- `/headers`
+- `/port`
+
+```
+$ docker run -d -e WEB_PORTS=80 -p 80:80 web
+$ curl http://127.0.0.1:80/headers
+Host: 127.0.0.1
+User-Agent: curl/7.47.0
+Accept: */*
+
+$ curl http://127.0.0.1:80/port
+answer from port 80
+
+```
+
+
+## nginx-proxy-tester
+
+This is an optional requirement which is usefull if you cannot (or don't want to) install pytest and its requirements on your computer. In this case, you can use the `nginx-proxy-tester` docker image to run the test suite from a Docker container.
+
+To use this image, it is mandatory to run the container using the `pytest.sh` shell script. The script will build the image and run a container from it with the appropriate volumes and settings.

+ 6 - 0
nginx-proxy/test/requirements/build.sh

@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+
+docker build -t web $DIR/web

+ 5 - 0
nginx-proxy/test/requirements/python-requirements.txt

@@ -0,0 +1,5 @@
+backoff==1.3.2
+docker-compose==1.11.2
+docker==2.1.0
+pytest==3.0.5
+requests==2.11.1

+ 8 - 0
nginx-proxy/test/requirements/web/Dockerfile

@@ -0,0 +1,8 @@
+# Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable
+
+FROM python:3
+COPY ./webserver.py /
+COPY ./entrypoint.sh /
+WORKDIR /opt
+ENTRYPOINT ["/bin/bash", "/entrypoint.sh"]
+

+ 15 - 0
nginx-proxy/test/requirements/web/entrypoint.sh

@@ -0,0 +1,15 @@
+#!/bin/bash
+set -u
+
+trap '[ ${#PIDS[@]} -gt 0 ] && kill -TERM ${PIDS[@]}' TERM
+declare -a PIDS
+
+for port in $WEB_PORTS; do
+	echo starting a web server listening on port $port;
+	/webserver.py $port &
+	PIDS+=($!)
+done
+
+wait ${PIDS[@]}
+trap - TERM
+wait ${PIDS[@]}

+ 38 - 0
nginx-proxy/test/requirements/web/webserver.py

@@ -0,0 +1,38 @@
+#!/usr/bin/env python3
+
+import os, sys, re
+import http.server
+import socketserver
+
+class Handler(http.server.SimpleHTTPRequestHandler):
+    def do_GET(self):
+
+        response_body = ""
+        response_code = 200
+
+        if self.path == "/headers":
+            response_body += self.headers.as_string()
+        elif self.path == "/port":
+            response_body += "answer from port %s\n" % PORT
+        elif re.match("/status/(\d+)", self.path):
+            result = re.match("/status/(\d+)", self.path)
+            response_code = int(result.group(1))
+            response_body += "answer with response code %s\n" % response_code
+        elif self.path == "/":
+            response_body += "I'm %s\n" % os.environ['HOSTNAME']
+        else:
+            response_body += "No route for this path!\n"
+            response_code = 404
+
+        self.send_response(response_code)
+        self.send_header("Content-Type", "text/plain")
+        self.end_headers()
+
+        if (len(response_body)):
+            self.wfile.write(response_body.encode())
+
+if __name__ == '__main__':
+    PORT = int(sys.argv[1])
+    socketserver.TCPServer.allow_reuse_address = True
+    httpd = socketserver.TCPServer(('0.0.0.0', PORT), Handler)
+    httpd.serve_forever()

+ 1 - 0
nginx-proxy/test/stress_tests/README.md

@@ -0,0 +1 @@
+This directory contains tests that showcase scenarios known to break the expected behavior of nginx-proxy.

+ 5 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/README.md

@@ -0,0 +1,5 @@
+Test the behavior of nginx-proxy when restarted after deleting a certificate file is was using.
+
+1. nginx-proxy is created with a virtual host having a certificate
+1. while nginx-proxy is running, the certificate file is deleted
+1. nginx-proxy is then restarted (without removing the container)

+ 70 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.crt

@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Feb 17 23:20:54 2017 GMT
+            Not After : Jul  5 23:20:54 2044 GMT
+        Subject: CN=web.nginx-proxy
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:b6:27:63:a5:c6:e8:f4:7a:94:0e:cc:a2:62:76:
+                    6d:5d:33:6f:cf:19:fc:e7:e5:bb:0e:0e:d0:7c:4f:
+                    73:4c:48:2b:17:d1:4d:d5:9f:42:08:73:84:54:8c:
+                    86:d2:c5:da:59:01:3f:42:22:e0:36:f0:dc:ab:de:
+                    0a:bd:26:2b:22:13:87:a6:1f:23:ef:0e:99:27:8b:
+                    15:4a:1b:ef:93:c9:6b:91:de:a0:02:0c:62:bb:cc:
+                    56:37:e8:25:92:c3:1f:f1:69:d8:7c:a8:33:e0:89:
+                    ce:14:67:a0:39:77:88:91:e6:a3:07:97:90:22:88:
+                    d0:79:18:63:fb:6f:7e:ee:2b:42:7e:23:f5:e7:da:
+                    e9:ee:6a:fa:96:65:9f:e1:2b:15:49:c8:cd:2d:ce:
+                    86:4f:2c:2a:67:79:bf:41:30:14:cc:f6:0f:14:74:
+                    9e:b6:d3:d0:3b:f0:1b:b8:e8:19:2a:fd:d6:fd:dc:
+                    4b:4e:65:7d:9b:bf:37:7e:2d:35:22:2e:74:90:ce:
+                    41:35:3d:41:a0:99:db:97:1f:bf:3e:18:3c:48:fb:
+                    da:df:c6:4e:4e:b9:67:b8:10:d5:a5:13:03:c4:b7:
+                    65:e7:aa:f0:14:4b:d3:4d:ea:fe:8f:69:cf:50:21:
+                    63:27:cf:9e:4c:67:15:7b:3f:3b:da:cb:17:80:61:
+                    1e:25
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web.nginx-proxy
+    Signature Algorithm: sha256WithRSAEncryption
+         09:31:be:db:4e:b0:b6:68:da:ae:5b:16:51:29:fc:9f:61:b6:
+         5a:2f:3c:35:ef:67:76:97:b0:34:4e:3b:b4:d6:88:19:4f:84:
+         2e:73:d3:c0:3a:4c:41:54:6c:bb:67:89:67:ad:25:55:d7:d4:
+         80:fe:a7:3f:3d:9e:f1:34:96:d8:da:5a:78:51:c0:63:f1:52:
+         29:35:55:f4:7d:70:1c:d3:96:62:7f:64:86:81:52:27:c4:c6:
+         10:13:c6:73:56:4d:32:d0:b3:c3:c8:2c:25:83:e4:2b:1d:d4:
+         74:30:e5:85:af:2d:b6:a5:6b:fe:5d:d3:3c:00:58:94:f4:6a:
+         f5:a6:1d:cf:f9:ed:d5:27:ed:13:24:b2:4f:2b:f3:b8:e4:af:
+         0c:1d:fe:e0:6a:01:5e:a2:44:ff:3e:96:fa:6c:39:a3:51:37:
+         f3:72:55:d8:2d:29:6e:de:95:b9:d8:e3:1e:65:a5:9c:0d:79:
+         2d:39:ab:c7:ac:16:b6:a5:71:4b:35:a4:6c:72:47:1b:72:9c:
+         67:58:c1:fc:f6:7f:a7:73:50:7b:d6:27:57:74:a1:31:38:a7:
+         31:e3:b9:d4:c9:45:33:ec:ed:16:cf:c5:bd:d0:03:b1:45:3f:
+         68:0d:91:5c:26:4e:37:05:74:ed:3e:75:5e:ca:5e:ee:e2:51:
+         4b:da:08:99
+-----BEGIN CERTIFICATE-----
+MIIC8zCCAdugAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAyMTcyMzIwNTRaFw00NDA3MDUyMzIwNTRaMBoxGDAWBgNVBAMMD3dl
+Yi5uZ2lueC1wcm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALYn
+Y6XG6PR6lA7MomJ2bV0zb88Z/Ofluw4O0HxPc0xIKxfRTdWfQghzhFSMhtLF2lkB
+P0Ii4Dbw3KveCr0mKyITh6YfI+8OmSeLFUob75PJa5HeoAIMYrvMVjfoJZLDH/Fp
+2HyoM+CJzhRnoDl3iJHmoweXkCKI0HkYY/tvfu4rQn4j9efa6e5q+pZln+ErFUnI
+zS3Ohk8sKmd5v0EwFMz2DxR0nrbT0DvwG7joGSr91v3cS05lfZu/N34tNSIudJDO
+QTU9QaCZ25cfvz4YPEj72t/GTk65Z7gQ1aUTA8S3Zeeq8BRL003q/o9pz1AhYyfP
+nkxnFXs/O9rLF4BhHiUCAwEAAaMeMBwwGgYDVR0RBBMwEYIPd2ViLm5naW54LXBy
+b3h5MA0GCSqGSIb3DQEBCwUAA4IBAQAJMb7bTrC2aNquWxZRKfyfYbZaLzw172d2
+l7A0Tju01ogZT4Quc9PAOkxBVGy7Z4lnrSVV19SA/qc/PZ7xNJbY2lp4UcBj8VIp
+NVX0fXAc05Zif2SGgVInxMYQE8ZzVk0y0LPDyCwlg+QrHdR0MOWFry22pWv+XdM8
+AFiU9Gr1ph3P+e3VJ+0TJLJPK/O45K8MHf7gagFeokT/Ppb6bDmjUTfzclXYLSlu
+3pW52OMeZaWcDXktOavHrBa2pXFLNaRsckcbcpxnWMH89n+nc1B71idXdKExOKcx
+47nUyUUz7O0Wz8W90AOxRT9oDZFcJk43BXTtPnVeyl7u4lFL2giZ
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/certs/web.nginx-proxy.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAtidjpcbo9HqUDsyiYnZtXTNvzxn85+W7Dg7QfE9zTEgrF9FN
+1Z9CCHOEVIyG0sXaWQE/QiLgNvDcq94KvSYrIhOHph8j7w6ZJ4sVShvvk8lrkd6g
+Agxiu8xWN+glksMf8WnYfKgz4InOFGegOXeIkeajB5eQIojQeRhj+29+7itCfiP1
+59rp7mr6lmWf4SsVScjNLc6GTywqZ3m/QTAUzPYPFHSettPQO/AbuOgZKv3W/dxL
+TmV9m783fi01Ii50kM5BNT1BoJnblx+/Phg8SPva38ZOTrlnuBDVpRMDxLdl56rw
+FEvTTer+j2nPUCFjJ8+eTGcVez872ssXgGEeJQIDAQABAoIBAGQCMFW+ZfyEqHGP
+rMA+oUEAkqy0agSwPwky3QjDXlxNa0uCYSeebtTRB6CcHxHuCzm+04puN4gyqhW6
+rU64fAoTivCMPGBuNWxekmvD9r+/YM4P2u4E+th9EgFT9f0kII+dO30FpKXtQzY0
+xuWGWXcxl+T9M+eiEkPKPmq4BoqgTDo5ty7qDv0ZqksGotKFmdYbtSvgBAueJdwu
+VWJvenI9F42ExBRKOW1aldiRiaYBCLiCVPKJtOg9iuOP9RHUL1SE8xy5I5mm78g3
+a13ji3BNq3yS+VhGjQ7zDy1V1jGupLoJw4I7OThu8hy+B8Vt8EN/iqakufOkjlTN
+xTJ33CkCgYEA5Iymg0NTjWk6aEkFa9pERjfUWqdVp9sWSpFFZZgi55n7LOx6ohi3
+vuLim3is/gYfK2kU/kHGZZLPnT0Rdx0MbOB4XK0CAUlqtUd0IyO4jMZ06g4/kn3N
+e2jLdCCIBoEQuLk4ELxj2mHsLQhEvDrg7nzU2WpTHHhvJbIbDWOAxhsCgYEAzAgv
+rKpanF+QDf4yeKHxAj2rrwRksTw4Pe7ZK/bog/i+HIVDA70vMapqftHbual/IRrB
+JL7hxskoJ/h9c1w4xkWDjqkSKz8/Ihr4dyPfWyGINWbx/rarT/m5MU5SarScoK7o
+Xgb25x+W+61rtI+2JhVRGO86+JiAeT4LkAX88L8CgYAwHHug/jdEeXZWJakCfzwI
+HBCT1M3vO+uBXvtg25ndb0i0uENIhDOJ93EEkW65Osis9r34mBgPocwaqZRXosHO
+2aH8wF6/rpjL+HK2QvrCh7Rs4Pr494qeA/1wQLjhxaGjgToQK9hJTHvPLwJpLWvU
+SGr2Ka+9Oo0LPmb7dorRKQKBgQCLsNcjOodLJMp2KiHYIdfmlt6itzlRd09yZ8Nc
+rHHJWVagJEUbnD1hnbHIHlp3pSqbObwfMmlWNoc9xo3tm6hrZ1CJLgx4e5b3/Ms8
+ltznge/F0DPDFsH3wZwfu+YFlJ7gDKCfL9l/qEsxCS0CtJobPOEHV1NivNbJK8ey
+1ca19QKBgDTdMOUsobAmDEkPQIpxfK1iqYAB7hpRLi79OOhLp23NKeyRNu8FH9fo
+G3DZ4xUi6hP2bwiYugMXDyLKfvxbsXwQC84kGF8j+bGazKNhHqEC1OpYwmaTB3kg
+qL9cHbjWySeRdIsRY/eWmiKjUwmiO54eAe1HWUdcsuz8yM3xf636
+-----END RSA PRIVATE KEY-----

+ 17 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/docker-compose.yml

@@ -0,0 +1,17 @@
+web:
+  image: web
+  expose:
+  - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web.nginx-proxy
+
+
+reverseproxy:
+  image: jwilder/nginx-proxy:test
+  container_name: reverseproxy
+  environment:
+    DEBUG: "true"
+  volumes:
+  - /var/run/docker.sock:/tmp/docker.sock:ro
+  - ./tmp_certs:/etc/nginx/certs:ro

+ 73 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/test_restart_while_missing_cert.py

@@ -0,0 +1,73 @@
+import logging
+import os
+from os.path import join, isfile
+from shutil import copy
+from time import sleep
+
+import pytest
+from requests import ConnectionError
+
+script_dir = os.path.dirname(__file__)
+
+pytestmark = pytest.mark.xfail()  # TODO delete this marker once those issues are fixed
+
+
+@pytest.yield_fixture(scope="module", autouse=True)
+def certs():
+    """
+    pytest fixture that provides cert and key files into the tmp_certs directory
+    """
+    file_names = ("web.nginx-proxy.crt", "web.nginx-proxy.key")
+    logging.info("copying server cert and key files into tmp_certs")
+    for f_name in file_names:
+        copy(join(script_dir, "certs", f_name), join(script_dir, "tmp_certs"))
+    yield
+    logging.info("cleaning up the tmp_cert directory")
+    for f_name in file_names:
+        if isfile(join(script_dir, "tmp_certs", f_name)):
+            os.remove(join(script_dir, "tmp_certs", f_name))
+
+###############################################################################
+
+
+def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://foo.nginx-proxy/")
+    assert r.status_code == 503
+
+
+def test_http_web_is_301(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy/port", allow_redirects=False)
+    assert r.status_code == 301
+
+
+def test_https_web_is_200(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy/port")
+    assert r.status_code == 200
+    assert 'answer from port 81\n' in r.text
+
+
+@pytest.mark.incremental
+def test_delete_cert_and_restart_reverseproxy(docker_compose):
+    os.remove(join(script_dir, "tmp_certs", "web.nginx-proxy.crt"))
+    docker_compose.containers.get("reverseproxy").restart()
+    sleep(3)  # give time for the container to initialize
+    assert "running" == docker_compose.containers.get("reverseproxy").status
+
+
+@pytest.mark.incremental
+def test_unknown_virtual_host_is_still_503(nginxproxy):
+    r = nginxproxy.get("http://foo.nginx-proxy/")
+    assert r.status_code == 503
+
+
+@pytest.mark.incremental
+def test_http_web_is_now_200(nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 81\n" == r.text
+
+
+@pytest.mark.incremental
+def test_https_web_is_now_broken_since_there_is_no_cert(nginxproxy):
+    with pytest.raises(ConnectionError):
+        nginxproxy.get("https://web.nginx-proxy/port")

+ 2 - 0
nginx-proxy/test/stress_tests/test_deleted_cert/tmp_certs/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 59 - 0
nginx-proxy/test/stress_tests/test_unreachable_network/README.md

@@ -0,0 +1,59 @@
+# nginx-proxy template is not considered when a container is not reachable
+
+Having a container with the `VIRTUAL_HOST` environment variable set but on a network not reachable from the nginx-proxy container will result in nginx-proxy serving the default nginx welcome page for all requests.
+
+Furthermore, if the nginx-proxy in such state is restarted, the nginx process will crash and the container stops.
+
+In the generated nginx config file, we can notice the presence of an empty `upstream {}` block.
+
+This can be fixed by merging [PR-585](https://github.com/jwilder/nginx-proxy/pull/585).
+
+## How to reproduce
+
+1. a first web container is created on network `netA`
+1. a second web container is created on network `netB`
+1. nginx-proxy is created with access to `netA` only
+
+
+## Erratic behavior
+
+- nginx serves the default welcome page for all requests to `/` and error 404 for any other path
+- nginx-container crash on restart
+
+Log shows:
+
+```
+webB_1          | starting a web server listening on port 82
+webA_1          | starting a web server listening on port 81
+reverseproxy    | forego     | starting dockergen.1 on port 5000
+reverseproxy    | forego     | starting nginx.1 on port 5100
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Generated '/etc/nginx/conf.d/default.conf' from 3 containers
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Running 'nginx -s reload'
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Error running notify command: nginx -s reload, exit status 1
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Watching docker events
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Contents of /etc/nginx/conf.d/default.conf did not change. Skipping notification 'nginx -s reload'
+reverseproxy    | reverseproxy    | forego     | starting dockergen.1 on port 5000  <---- nginx-proxy container restarted
+reverseproxy    | forego     | starting nginx.1 on port 5100
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Generated '/etc/nginx/conf.d/default.conf' from 3 containers
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Running 'nginx -s reload'
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Error running notify command: nginx -s reload, exit status 1
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Watching docker events
+reverseproxy    | dockergen.1 | 2017/02/20 01:10:24 Contents of /etc/nginx/conf.d/default.conf did not change. Skipping notification 'nginx -s reload'
+reverseproxy    | forego     | starting dockergen.1 on port 5000
+reverseproxy    | forego     | starting nginx.1 on port 5100
+reverseproxy    | nginx.1    | 2017/02/20 01:11:02 [emerg] 17#17: no servers are inside upstream in /etc/nginx/conf.d/default.conf:64
+reverseproxy    | forego     | starting nginx.1 on port 5200
+reverseproxy    | forego     | sending SIGTERM to nginx.1
+reverseproxy    | forego     | sending SIGTERM to dockergen.1
+reverseproxy exited with code 0
+reverseproxy exited with code 0
+
+```
+
+## Expected behavior
+
+- no default nginx welcome page should be served
+- nginx is able to forward requests to containers of `netA`
+- nginx respond with error 503 for unknown virtual hosts
+- nginx is not able to forward requests to containers of `netB` and responds with an error
+- nginx should survive restarts

+ 35 - 0
nginx-proxy/test/stress_tests/test_unreachable_network/docker-compose.yml

@@ -0,0 +1,35 @@
+version: "2"
+
+networks:
+  netA:
+  netB:
+
+services:
+  reverseproxy:
+    container_name: reverseproxy
+    networks:
+      - netA
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+
+  webA:
+    networks:
+      - netA
+    image: web
+    expose:
+      - 81
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: webA.nginx-proxy
+
+  webB:
+    networks:
+      - netB
+    image: web
+    expose:
+      - 82
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: webB.nginx-proxy
+

+ 35 - 0
nginx-proxy/test/stress_tests/test_unreachable_network/test_unreachable_net.py

@@ -0,0 +1,35 @@
+from time import sleep
+
+import pytest
+import requests
+
+pytestmark = pytest.mark.xfail()  # TODO delete this marker once #585 is merged
+
+
+def test_default_nginx_welcome_page_should_not_be_served(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://whatever.nginx-proxy/", allow_redirects=False)
+    assert "<title>Welcome to nginx!</title>" not in r.text
+
+
+def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx-proxy/", allow_redirects=False)
+    assert r.status_code == 503
+
+
+def test_http_web_a_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webA.nginx-proxy/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 81\n" == r.text
+
+
+def test_http_web_b_gets_an_error(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webB.nginx-proxy/", allow_redirects=False)
+    assert "<title>Welcome to nginx!</title>" not in r.text
+    with pytest.raises(requests.exceptions.HTTPError):
+        r.raise_for_status()
+
+
+def test_reverseproxy_survive_restart(docker_compose):
+    docker_compose.containers.get("reverseproxy").restart()
+    sleep(2)  # give time for the container to initialize
+    assert docker_compose.containers.get("reverseproxy").status == "running"

+ 15 - 0
nginx-proxy/test/test_DOCKER_HOST_unix_socket.py

@@ -0,0 +1,15 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 

+ 24 - 0
nginx-proxy/test/test_DOCKER_HOST_unix_socket.yml

@@ -0,0 +1,24 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/f00.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+  environment:
+    DOCKER_HOST: unix:///f00.sock

+ 10 - 0
nginx-proxy/test/test_composev2.py

@@ -0,0 +1,10 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+def test_forwards_to_whoami(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"

+ 15 - 0
nginx-proxy/test/test_composev2.yml

@@ -0,0 +1,15 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+
+  web:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web.nginx-proxy.local

+ 1 - 0
nginx-proxy/test/test_custom/my_custom_proxy_settings.conf

@@ -0,0 +1 @@
+add_header  X-test  f00;

+ 1 - 0
nginx-proxy/test/test_custom/my_custom_proxy_settings_bar.conf

@@ -0,0 +1 @@
+add_header  X-test  bar;

+ 28 - 0
nginx-proxy/test/test_custom/test_defaults-location.py

@@ -0,0 +1,28 @@
+import pytest
+
+def test_custom_default_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_default_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_default_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+
+def test_custom_default_conf_is_overriden_for_web3(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web3.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 83\n"
+    assert "X-test" in r.headers
+    assert "bar" == r.headers["X-test"]

+ 31 - 0
nginx-proxy/test/test_custom/test_defaults-location.yml

@@ -0,0 +1,31 @@
+nginx-proxy:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+    - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/default_location:ro
+    - ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/web3.nginx-proxy.local_location:ro
+
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.local
+
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.local
+
+web3:
+  image: web
+  expose:
+    - "83"
+  environment:
+    WEB_PORTS: 83
+    VIRTUAL_HOST: web3.nginx-proxy.local

+ 20 - 0
nginx-proxy/test/test_custom/test_defaults.py

@@ -0,0 +1,20 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]

+ 24 - 0
nginx-proxy/test/test_custom/test_defaults.yml

@@ -0,0 +1,24 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/proxy.conf:ro
+
+  web1:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 22 - 0
nginx-proxy/test/test_custom/test_location-per-vhost.py

@@ -0,0 +1,22 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" not in r.headers
+
+def test_custom_block_is_present_in_nginx_generated_conf(docker_compose, nginxproxy):
+    assert "include /etc/nginx/vhost.d/web1.nginx-proxy.local_location;" in nginxproxy.get_conf()

+ 24 - 0
nginx-proxy/test/test_custom/test_location-per-vhost.yml

@@ -0,0 +1,24 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local_location:ro
+
+  web1:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 19 - 0
nginx-proxy/test/test_custom/test_per-vhost.py

@@ -0,0 +1,19 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" not in r.headers

+ 24 - 0
nginx-proxy/test/test_custom/test_per-vhost.yml

@@ -0,0 +1,24 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.local:ro
+
+  web1:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 20 - 0
nginx-proxy/test/test_custom/test_proxy-wide.py

@@ -0,0 +1,20 @@
+import pytest
+
+def test_custom_conf_does_not_apply_to_unknown_vhost(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+    assert "X-test" not in r.headers
+
+def test_custom_conf_applies_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]
+
+def test_custom_conf_applies_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"
+    assert "X-test" in r.headers
+    assert "f00" == r.headers["X-test"]

+ 24 - 0
nginx-proxy/test/test_custom/test_proxy-wide.yml

@@ -0,0 +1,24 @@
+version: '2'
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+      - ./my_custom_proxy_settings.conf:/etc/nginx/conf.d/my_custom_proxy_settings.conf:ro
+
+  web1:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+
+  web2:
+    image: web
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local

+ 7 - 0
nginx-proxy/test/test_default-host.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_fallback_on_default(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"

+ 18 - 0
nginx-proxy/test/test_default-host.yml

@@ -0,0 +1,18 @@
+# GIVEN a webserver with VIRTUAL_HOST set to web1.tld
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.tld
+
+
+# WHEN nginx-proxy runs with DEFAULT_HOST set to web1.tld
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+  environment:
+    DEFAULT_HOST: web1.tld

+ 1 - 0
nginx-proxy/test/test_dockergen/.gitignore

@@ -0,0 +1 @@
+nginx.tmpl

+ 38 - 0
nginx-proxy/test/test_dockergen/test_dockergen_v2.py

@@ -0,0 +1,38 @@
+import os
+import docker
+import logging
+import pytest
+
+
+@pytest.yield_fixture(scope="module")
+def nginx_tmpl():
+    """
+    pytest fixture which extracts the the nginx config template from
+    the jwilder/nginx-proxy:test image
+    """
+    script_dir = os.path.dirname(__file__)
+    logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
+    docker_client = docker.from_env()
+    print(docker_client.containers.run(
+        image='jwilder/nginx-proxy:test',
+        remove=True,
+        volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
+        entrypoint='sh',
+        command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
+            current_dir=script_dir),
+        stderr=True))
+    yield
+    logging.info("removing nginx.tmpl")
+    os.remove(os.path.join(script_dir, "nginx.tmpl"))
+
+
+def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx.container.docker/")
+    assert r.status_code == 503
+
+
+def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://whoami.nginx.container.docker/")
+    assert r.status_code == 200
+    whoami_container = docker_compose.containers.get("whoami")
+    assert r.text == "I'm %s\n" % whoami_container.id[:12]

+ 27 - 0
nginx-proxy/test/test_dockergen/test_dockergen_v2.yml

@@ -0,0 +1,27 @@
+version: '2'
+
+services:
+  nginx:
+    image: nginx
+    container_name: nginx
+    volumes:
+      - /etc/nginx/conf.d
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+
+  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
+
+  web:
+    image: web
+    container_name: whoami
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: whoami.nginx.container.docker

+ 66 - 0
nginx-proxy/test/test_dockergen/test_dockergen_v3.py

@@ -0,0 +1,66 @@
+import os
+import docker
+import logging
+import pytest
+import re
+
+def versiontuple(v):
+    """
+    >>> versiontuple("1.12.3")
+    (1, 12, 3)
+
+    >>> versiontuple("1.13.0")
+    (1, 13, 0)
+
+    >>> versiontuple("17.03.0-ce")
+    (17, 3, 0)
+
+    >>> versiontuple("17.03.0-ce") < (1, 13)
+    False
+    """
+    return tuple(map(int, (v.split('-')[0].split("."))))
+
+
+raw_version = docker.from_env().version()['Version']
+pytestmark = pytest.mark.skipif(
+    versiontuple(raw_version) < (1, 13),
+    reason="Docker compose syntax v3 requires docker engine v1.13 or later (got %s)" % raw_version)
+
+
+@pytest.yield_fixture(scope="module")
+def nginx_tmpl():
+    """
+    pytest fixture which extracts the the nginx config template from
+    the jwilder/nginx-proxy:test image
+    """
+    script_dir = os.path.dirname(__file__)
+    logging.info("extracting nginx.tmpl from jwilder/nginx-proxy:test")
+    docker_client = docker.from_env()
+    print(docker_client.containers.run(
+        image='jwilder/nginx-proxy:test',
+        remove=True,
+        volumes=['{current_dir}:{current_dir}'.format(current_dir=script_dir)],
+        entrypoint='sh',
+        command='-xc "cp /app/nginx.tmpl {current_dir} && chmod 777 {current_dir}/nginx.tmpl"'.format(
+            current_dir=script_dir),
+        stderr=True))
+    yield
+    logging.info("removing nginx.tmpl")
+    os.remove(os.path.join(script_dir, "nginx.tmpl"))
+
+
+def test_unknown_virtual_host_is_503(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx.container.docker/")
+    assert r.status_code == 503
+
+
+def test_forwards_to_whoami(nginx_tmpl, docker_compose, nginxproxy):
+    r = nginxproxy.get("http://whoami.nginx.container.docker/")
+    assert r.status_code == 200
+    whoami_container = docker_compose.containers.get("whoami")
+    assert r.text == "I'm %s\n" % whoami_container.id[:12]
+
+
+if __name__ == '__main__':
+    import doctest
+    doctest.testmod()

+ 28 - 0
nginx-proxy/test/test_dockergen/test_dockergen_v3.yml

@@ -0,0 +1,28 @@
+version: '3'
+services:
+  nginx:
+    image: nginx
+    container_name: nginx
+    volumes:
+      - nginx_conf:/etc/nginx/conf.d
+      - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+
+  dockergen:
+    image: jwilder/docker-gen
+    command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
+      - nginx_conf:/etc/nginx/conf.d
+
+  web:
+    image: web
+    container_name: whoami
+    expose:
+      - "80"
+    environment:
+      WEB_PORTS: 80
+      VIRTUAL_HOST: whoami.nginx.container.docker
+
+volumes:
+  nginx_conf: {}

+ 46 - 0
nginx-proxy/test/test_events.py

@@ -0,0 +1,46 @@
+"""
+Test that nginx-proxy detects new containers
+"""
+from time import sleep
+
+import pytest
+from docker.errors import NotFound
+
+
+@pytest.yield_fixture()
+def web1(docker_compose):
+    """
+    pytest fixture creating a web container with `VIRTUAL_HOST=web1.nginx-proxy` listening on port 81.
+    """
+    container = docker_compose.containers.run(
+        name="web1",
+        image="web",
+        detach=True,
+        environment={
+            "WEB_PORTS": "81",
+            "VIRTUAL_HOST": "web1.nginx-proxy"
+        },
+        ports={"81/tcp": None}
+    )
+    sleep(2)  # give it some time to initialize and for docker-gen to detect it
+    yield container
+    try:
+        docker_compose.containers.get("web1").remove(force=True)
+    except NotFound:
+        pass
+
+
+def test_nginx_proxy_behavior_when_alone(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+
+def test_new_container_is_detected(web1, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy/port")
+    assert r.status_code == 200
+    assert "answer from port 81\n" == r.text
+
+    web1.remove(force=True)
+    sleep(2)
+    r = nginxproxy.get("http://web1.nginx-proxy/port")
+    assert r.status_code == 503

+ 5 - 0
nginx-proxy/test/test_events.yml

@@ -0,0 +1,5 @@
+nginxproxy:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 70 - 0
nginx-proxy/test/test_headers/certs/web.nginx-proxy.tld.crt

@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 13 03:06:39 2017 GMT
+            Not After : May 31 03:06:39 2044 GMT
+        Subject: CN=web.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:95:56:c7:0d:48:a5:2b:3c:65:49:3f:26:e1:38:
+                    2b:61:30:56:e4:92:d7:63:e0:eb:ad:ac:f9:33:9b:
+                    b2:31:f1:39:13:0b:e5:43:7b:c5:bd:8a:85:c8:d9:
+                    3d:d8:ac:71:ba:16:e7:81:96:b2:ab:ae:c6:c0:bd:
+                    be:a7:d1:96:8f:b2:9b:df:ba:f9:4d:a1:3b:7e:21:
+                    4a:cd:b6:45:f9:6d:79:50:bf:24:8f:c1:6b:c1:09:
+                    19:5b:62:cb:96:e8:04:14:20:e8:d4:16:62:6a:f2:
+                    37:c1:96:e2:9d:53:05:0b:52:1d:e7:68:92:db:8b:
+                    36:68:cd:8d:5b:02:ff:12:f0:ac:5d:0c:c4:e0:7a:
+                    55:a2:49:60:9f:ff:47:1f:52:73:55:4d:d4:f2:d1:
+                    62:a2:f4:50:9d:c9:f6:f1:43:b3:dc:57:e1:31:76:
+                    b4:e0:a4:69:7e:f2:6d:34:ae:b9:8d:74:26:7b:d9:
+                    f6:07:00:ef:4b:36:61:b3:ef:7a:a1:36:3a:b6:d0:
+                    9e:f8:b8:a9:0d:4c:30:a2:ed:eb:ab:6b:eb:2e:e2:
+                    0b:28:be:f7:04:b1:e9:e0:84:d6:5d:31:77:7c:dc:
+                    d2:1f:d4:1d:71:6f:6f:6c:6d:1b:bf:31:e2:5b:c3:
+                    52:d0:14:fc:8b:fb:45:ea:41:ec:ca:c7:3b:67:12:
+                    c4:df
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         4e:48:7d:81:66:ba:2f:50:3d:24:42:61:3f:1f:de:cf:ec:1b:
+         1b:bd:0a:67:b6:62:c8:79:9d:31:a0:fd:a9:61:ce:ff:69:bf:
+         0e:f4:f7:e6:15:2b:b0:f0:e4:f2:f4:d2:8f:74:02:b1:1e:4a:
+         a8:6f:26:0a:77:32:29:cf:dc:b5:61:82:3e:58:47:61:92:f0:
+         0c:20:25:f8:41:4d:34:09:44:bc:39:9e:aa:82:06:83:13:8b:
+         1e:2c:3d:cf:cd:1a:f7:77:39:38:e0:a3:a7:f3:09:da:02:8d:
+         73:75:38:b4:dd:24:a7:f9:03:db:98:c6:88:54:87:dc:e0:65:
+         4c:95:c5:39:9c:00:30:dc:f0:d3:2c:19:ca:f1:f4:6c:c6:d9:
+         b5:c4:4a:c7:bc:a1:2e:88:7b:b5:33:d0:ff:fb:48:5e:3e:29:
+         fa:58:e5:03:de:d8:17:de:ed:96:fc:7e:1f:fe:98:f6:be:99:
+         38:87:51:c0:d3:b7:9a:0f:26:92:e5:53:1b:d6:25:4c:ac:48:
+         f3:29:fc:74:64:9d:07:6a:25:57:24:aa:a7:70:fa:8f:6c:a7:
+         2b:b7:9d:81:46:10:32:93:b9:45:6d:0f:16:18:b2:21:1f:f3:
+         30:24:62:3f:e1:6c:07:1d:71:28:cb:4c:bb:f5:39:05:f9:b2:
+         5b:a0:05:1b
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTMwMzA2MzlaFw00NDA1MzEwMzA2MzlaMB4xHDAaBgNVBAMME3dl
+Yi5uZ2lueC1wcm94eS50bGQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQCVVscNSKUrPGVJPybhOCthMFbkktdj4OutrPkzm7Ix8TkTC+VDe8W9ioXI2T3Y
+rHG6FueBlrKrrsbAvb6n0ZaPspvfuvlNoTt+IUrNtkX5bXlQvySPwWvBCRlbYsuW
+6AQUIOjUFmJq8jfBluKdUwULUh3naJLbizZozY1bAv8S8KxdDMTgelWiSWCf/0cf
+UnNVTdTy0WKi9FCdyfbxQ7PcV+ExdrTgpGl+8m00rrmNdCZ72fYHAO9LNmGz73qh
+Njq20J74uKkNTDCi7eura+su4gsovvcEsenghNZdMXd83NIf1B1xb29sbRu/MeJb
+w1LQFPyL+0XqQezKxztnEsTfAgMBAAGjIjAgMB4GA1UdEQQXMBWCE3dlYi5uZ2lu
+eC1wcm94eS50bGQwDQYJKoZIhvcNAQELBQADggEBAE5IfYFmui9QPSRCYT8f3s/s
+Gxu9Cme2Ysh5nTGg/alhzv9pvw709+YVK7Dw5PL00o90ArEeSqhvJgp3MinP3LVh
+gj5YR2GS8AwgJfhBTTQJRLw5nqqCBoMTix4sPc/NGvd3OTjgo6fzCdoCjXN1OLTd
+JKf5A9uYxohUh9zgZUyVxTmcADDc8NMsGcrx9GzG2bXESse8oS6Ie7Uz0P/7SF4+
+KfpY5QPe2Bfe7Zb8fh/+mPa+mTiHUcDTt5oPJpLlUxvWJUysSPMp/HRknQdqJVck
+qqdw+o9spyu3nYFGEDKTuUVtDxYYsiEf8zAkYj/hbAcdcSjLTLv1OQX5slugBRs=
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/test_headers/certs/web.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAlVbHDUilKzxlST8m4TgrYTBW5JLXY+Drraz5M5uyMfE5Ewvl
+Q3vFvYqFyNk92KxxuhbngZayq67GwL2+p9GWj7Kb37r5TaE7fiFKzbZF+W15UL8k
+j8FrwQkZW2LLlugEFCDo1BZiavI3wZbinVMFC1Id52iS24s2aM2NWwL/EvCsXQzE
+4HpVoklgn/9HH1JzVU3U8tFiovRQncn28UOz3FfhMXa04KRpfvJtNK65jXQme9n2
+BwDvSzZhs+96oTY6ttCe+LipDUwwou3rq2vrLuILKL73BLHp4ITWXTF3fNzSH9Qd
+cW9vbG0bvzHiW8NS0BT8i/tF6kHsysc7ZxLE3wIDAQABAoIBAEmK7IecKMq7+V0y
+3mC3GpXICmKR9cRX9XgX4LkLiZuSoXrBtuuevmhzGSMp6I0VjwQHV4a3wdFORs6Q
+Ip3eVvj5Ck4Jc9BJAFVC6+WWR6tnwACFwOmSZRAw/O3GH2B3bdrDwiT/yQPFuLN7
+LKoxQiCrFdLp6rh3PBosb9pMBXU7k/HUazIdgmSKg6/JIoo/4Gwyid04TF/4MI2l
+RscxtP5/ANtS8VgwBEqhgdafRJ4KnLEpgvswgIQvUKmduVhZQlzd0LMY8FbhKVqz
+Utg8gsXaTyH6df/nmgUIInxLMz/MKPnMkv99fS6Sp/hvYlGpLZFWBJ6unMq3lKEr
+LMbHfIECgYEAxB+5QWdVqG2r9loJlf8eeuNeMPml4P8Jmi5RKyJC7Cww6DMlMxOS
+78ZJfl4b3ZrWuyvhjOfX/aTq7kQaF1BI9o3KJBH8k6EtO4gI8KeNmDONyQk9zsrn
+ru8Zwr7hVbAo8fCXxCnmPzhDLsYg6f3BVOsQWoX2SFYKZ1GvkPfIReECgYEAwu6G
+qtgFb57Vim10ecfWGM6vrPxvyfqP+zlH/p4nR+aQ+2sFbt27D0B1byWBRZe4KQyw
+Vq6XiQ09Fk6MJr8E8iAr9GXPPHcqlYI6bbNc6YOP3jVSKut0tQdTUOHll4kYIY+h
+RS3VA3+BA//ADpWpywu+7RZRbaIECA+U2a224r8CgYB5PCMIixgoRaNHZeEHF+1/
+iY1wOOKRcxY8eOU0BLnZxHd3EiasrCzoi2pi80nGczDKAxYqRCcAZDHVl8OJJdf0
+kTGjmnrHx5pucmkUWn7s1vGOlGfgrQ0K1kLWX6hrj7m/1Tn7yOrLqbvd7hvqiTI5
+jBVP3/+eN5G2zIf61TC4AQKBgCX2Q92jojNhsF58AHHy+/vqzIWYx8CC/mVDe4TX
+kfjLqzJ7XhyAK/zFZdlWaX1/FYtRAEpxR+uV226rr1mgW7s3jrfS1/ADmRRyvyQ8
+CP0k9PCmW7EmF51lptEanRbMyRlIGnUZfuFmhF6eAO4WMXHsgKs1bHg4VCapuihG
+T1aLAoGACRGn1UxFuBGqtsh2zhhsBZE7GvXKJSk/eP7QJeEXUNpNjCpgm8kIZM5K
+GorpL7PSB8mwVlDl18TpMm3P7nz6YkJYte+HdjO7pg59H39Uvtg3tZnIrFxNxVNb
+YF62/yHfk2AyTgjQZQUSmDS84jq1zUK4oS90lxr+u8qwELTniMs=
+-----END RSA PRIVATE KEY-----

+ 81 - 0
nginx-proxy/test/test_headers/test_http.py

@@ -0,0 +1,81 @@
+import pytest
+
+def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
+    assert r.status_code == 200
+    assert "Foo: Bar\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-For #####
+
+def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-For:" in r.text
+
+def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
+    assert r.status_code == 200
+    assert "X-Forwarded-For: 1.2.3.4, " in r.text
+
+
+##### Testing the handling of X-Forwarded-Proto #####
+
+def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: http" in r.text
+
+def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: f00\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Port #####
+
+def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 80\n" in r.text
+
+def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 1234\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Ssl #####
+
+def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: off\n" in r.text
+
+def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: off\n" in r.text
+
+
+##### Other headers
+
+def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Real-IP: " in r.text
+
+def test_Host_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "Host: web.nginx-proxy.tld" in r.text
+
+def test_httpoxy_safe(docker_compose, nginxproxy):
+    """
+    See https://httpoxy.org/
+    nginx-proxy should suppress the `Proxy` header
+    """
+    r = nginxproxy.get("http://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
+    assert r.status_code == 200
+    assert "Proxy:" not in r.text
+    

+ 14 - 0
nginx-proxy/test/test_headers/test_http.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "80"
+  environment:
+    WEB_PORTS: 80
+    VIRTUAL_HOST: web.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 82 - 0
nginx-proxy/test/test_headers/test_https.py

@@ -0,0 +1,82 @@
+import pytest
+
+
+def test_arbitrary_headers_are_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Foo': 'Bar'})
+    assert r.status_code == 200
+    assert "Foo: Bar\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-For #####
+
+def test_X_Forwarded_For_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-For:" in r.text
+
+def test_X_Forwarded_For_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-For': '1.2.3.4'})
+    assert r.status_code == 200
+    assert "X-Forwarded-For: 1.2.3.4, " in r.text
+
+
+##### Testing the handling of X-Forwarded-Proto #####
+
+def test_X_Forwarded_Proto_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: https" in r.text
+
+def test_X_Forwarded_Proto_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Proto': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Proto: f00\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Port #####
+
+def test_X_Forwarded_Port_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 443\n" in r.text
+
+def test_X_Forwarded_Port_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Port': '1234'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Port: 1234\n" in r.text
+
+
+##### Testing the handling of X-Forwarded-Ssl #####
+
+def test_X_Forwarded_Ssl_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: on\n" in r.text
+
+def test_X_Forwarded_Ssl_is_overwritten(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'X-Forwarded-Ssl': 'f00'})
+    assert r.status_code == 200
+    assert "X-Forwarded-Ssl: on\n" in r.text
+
+
+##### Other headers
+
+def test_X_Real_IP_is_generated(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "X-Real-IP: " in r.text
+
+def test_Host_is_passed_on(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers")
+    assert r.status_code == 200
+    assert "Host: web.nginx-proxy.tld" in r.text
+
+def test_httpoxy_safe(docker_compose, nginxproxy):
+    """
+    See https://httpoxy.org/
+    nginx-proxy should suppress the `Proxy` header
+    """
+    r = nginxproxy.get("https://web.nginx-proxy.tld/headers", headers={'Proxy': 'tcp://some.hacker.com'})
+    assert r.status_code == 200
+    assert "Proxy:" not in r.text
+    

+ 16 - 0
nginx-proxy/test/test_headers/test_https.yml

@@ -0,0 +1,16 @@
+web:
+  image: web
+  expose:
+    - "80"
+  environment:
+    WEB_PORTS: 80
+    VIRTUAL_HOST: web.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs/web.nginx-proxy.tld.crt:/etc/nginx/certs/web.nginx-proxy.tld.crt:ro
+    - ./certs/web.nginx-proxy.tld.key:/etc/nginx/certs/web.nginx-proxy.tld.key:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 35 - 0
nginx-proxy/test/test_ipv6.py

@@ -0,0 +1,35 @@
+import pytest
+
+
+def test_unknown_virtual_host_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2_ipv4(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 
+
+
+def test_unknown_virtual_host_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port", ipv6=True)
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port", ipv6=True)
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2_ipv6(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port", ipv6=True)
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 

+ 24 - 0
nginx-proxy/test/test_ipv6.yml

@@ -0,0 +1,24 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+  environment:
+    ENABLE_IPV6: "true"

+ 16 - 0
nginx-proxy/test/test_multiple-hosts.py

@@ -0,0 +1,16 @@
+import pytest
+
+
+def test_unknown_virtual_host_is_503(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://unknown.nginx-proxy.tld/port")
+    assert r.status_code == 503
+
+def test_webA_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webA.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"
+
+def test_webB_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://webB.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 81\n"

+ 14 - 0
nginx-proxy/test/test_multiple-hosts.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: webA.nginx-proxy.tld,webB.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 15 - 0
nginx-proxy/test/test_multiple-networks.py

@@ -0,0 +1,15 @@
+import pytest
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/")
+    assert r.status_code == 503
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.local/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 82\n"

+ 35 - 0
nginx-proxy/test/test_multiple-networks.yml

@@ -0,0 +1,35 @@
+version: '2'
+
+networks:
+  net1: {}
+  net2: {}
+
+services:
+  nginx-proxy:
+    image: jwilder/nginx-proxy:test
+    volumes:
+      - /var/run/docker.sock:/tmp/docker.sock:ro
+      - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+    networks:
+      - net1
+      - net2
+
+  web1:
+    image: web
+    expose:
+      - "81"
+    environment:
+      WEB_PORTS: 81
+      VIRTUAL_HOST: web1.nginx-proxy.local
+    networks:
+      - net1
+
+  web2:
+    image: web
+    expose:
+      - "82"
+    environment:
+      WEB_PORTS: 82
+      VIRTUAL_HOST: web2.nginx-proxy.local
+    networks:
+      - net2

+ 7 - 0
nginx-proxy/test/test_multiple-ports/test_VIRTUAL_PORT.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_chosen_port(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 90\n" in r.text

+ 15 - 0
nginx-proxy/test/test_multiple-ports/test_VIRTUAL_PORT.yml

@@ -0,0 +1,15 @@
+web:
+  image: web
+  expose:
+    - "80"
+    - "90"
+  environment:
+    WEB_PORTS: "80 90"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+    VIRTUAL_PORT: 90
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 7 - 0
nginx-proxy/test/test_multiple-ports/test_default-80.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 80\n" in r.text

+ 14 - 0
nginx-proxy/test/test_multiple-ports/test_default-80.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "80"
+    - "81"
+  environment:
+    WEB_PORTS: "80 81"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 7 - 0
nginx-proxy/test/test_multiple-ports/test_single-port-not-80.py

@@ -0,0 +1,7 @@
+import pytest
+
+
+def test_answer_is_served_from_exposed_port_even_if_not_80(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert "answer from port 81\n" in r.text

+ 14 - 0
nginx-proxy/test/test_multiple-ports/test_single-port-not-80.yml

@@ -0,0 +1,14 @@
+web:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "web.nginx-proxy.tld"
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 24 - 0
nginx-proxy/test/test_nominal.py

@@ -0,0 +1,24 @@
+import pytest
+from requests import ConnectionError
+
+
+def test_unknown_virtual_host(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://nginx-proxy/port")
+    assert r.status_code == 503
+
+
+def test_forwards_to_web1(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web1.nginx-proxy.tld/port")
+    assert r.status_code == 200   
+    assert r.text == "answer from port 81\n"
+
+
+def test_forwards_to_web2(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/port")
+    assert r.status_code == 200
+    assert r.text == "answer from port 82\n" 
+
+
+def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy):
+    with pytest.raises(ConnectionError):
+        nginxproxy.get("http://nginx-proxy/port", ipv6=True)

+ 22 - 0
nginx-proxy/test/test_nominal.yml

@@ -0,0 +1,22 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: 81
+    VIRTUAL_HOST: web1.nginx-proxy.tld
+
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: 82
+    VIRTUAL_HOST: web2.nginx-proxy.tld
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro

+ 70 - 0
nginx-proxy/test/test_ssl/certs/nginx-proxy.tld.crt

@@ -0,0 +1,70 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:08:52 2017 GMT
+            Not After : May 28 00:08:52 2044 GMT
+        Subject: CN=*.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:cb:45:f4:14:9b:fe:64:85:79:4a:36:8d:3d:d1:
+                    27:d0:7c:36:28:30:e6:73:80:6f:7c:49:23:d0:6c:
+                    17:e4:44:c0:77:4d:9a:c2:bc:24:84:e3:a5:4d:ba:
+                    d2:da:51:7b:a1:2a:12:d4:c0:19:55:69:2c:22:27:
+                    2d:1a:f6:fc:4b:7f:e9:cb:a8:3c:e8:69:b8:d2:4f:
+                    de:4e:50:e2:d0:74:30:7c:42:5a:ae:aa:85:a5:b1:
+                    71:4d:c9:7e:86:8b:62:8c:3e:0d:e3:3b:c3:f5:81:
+                    0b:8c:68:79:fe:bf:10:fb:ae:ec:11:49:6d:64:5e:
+                    1a:7d:b3:92:93:4e:96:19:3a:98:04:a7:66:b2:74:
+                    61:2d:41:13:0c:a4:54:0d:2c:78:fd:b4:a3:e8:37:
+                    78:9a:de:fa:bc:2e:a8:0f:67:14:58:ce:c3:87:d5:
+                    14:0e:8b:29:7d:48:19:b2:a9:f5:b4:e8:af:32:21:
+                    67:15:7e:43:52:8b:20:cf:9f:38:43:bf:fd:c8:24:
+                    7f:52:a3:88:f2:f1:4a:14:91:2a:6e:91:6f:fb:7d:
+                    6a:78:c6:6d:2e:dd:1e:4c:2b:63:bb:3a:43:9c:91:
+                    f9:df:d3:08:13:63:86:7d:ce:e8:46:cf:f1:6c:1f:
+                    ca:f7:4c:de:d8:4b:e0:da:bc:06:d9:87:0f:ff:96:
+                    45:85
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:*.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         6e:a5:0e:e4:d3:cc:d5:b7:fc:34:75:89:4e:98:8c:e7:08:06:
+         a8:5b:ec:13:7d:83:99:a2:61:b8:d5:12:6e:c5:b4:53:4e:9a:
+         22:cd:ad:14:30:6a:7d:58:d7:23:d9:a4:2a:96:a0:40:9e:50:
+         9f:ce:f2:fe:8c:dd:9a:ac:99:39:5b:89:2d:ca:e5:3e:c3:bc:
+         03:04:1c:12:d9:6e:b8:9f:f0:3a:be:12:44:7e:a4:21:86:73:
+         af:d5:00:51:3f:2c:56:70:34:8f:26:b0:7f:b0:cf:cf:7f:f9:
+         40:6f:00:29:c4:cf:c3:b7:c2:49:3d:3f:b0:26:78:87:b9:c7:
+         6c:1b:aa:6a:1a:dd:c5:eb:f2:69:ba:6d:46:0b:92:49:b5:11:
+         3c:eb:48:c7:2f:fb:33:a6:6a:82:a2:ab:f8:1e:5f:7d:e3:b7:
+         f2:fd:f5:88:a5:09:4d:a0:bc:f4:3b:cd:d2:8b:d7:57:1f:86:
+         3b:d2:3e:a4:92:21:b0:02:0b:e9:e0:c4:1c:f1:78:e2:58:a7:
+         26:5f:4c:29:c8:23:f0:6e:12:3f:bd:ad:44:7b:0b:bd:db:ba:
+         63:8d:07:c6:9d:dc:46:cc:63:40:ba:5e:45:82:dd:9a:e5:50:
+         e8:e7:d7:27:88:fc:6f:1d:8a:e7:5c:49:28:aa:10:29:75:28:
+         c7:52:de:f9
+-----BEGIN CERTIFICATE-----
+MIIC9zCCAd+gAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDA4NTJaFw00NDA1MjgwMDA4NTJaMBwxGjAYBgNVBAMMESou
+bmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
+y0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02awrwkhOOlTbrS2lF7
+oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJarqqFpbFxTcl+hoti
+jD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdmsnRhLUETDKRUDSx4
+/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFnFX5DUosgz584Q7/9
+yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MIE2OGfc7oRs/xbB/K
+90ze2Evg2rwG2YcP/5ZFhQIDAQABoyAwHjAcBgNVHREEFTATghEqLm5naW54LXBy
+b3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAbqUO5NPM1bf8NHWJTpiM5wgGqFvs
+E32DmaJhuNUSbsW0U06aIs2tFDBqfVjXI9mkKpagQJ5Qn87y/ozdmqyZOVuJLcrl
+PsO8AwQcEtluuJ/wOr4SRH6kIYZzr9UAUT8sVnA0jyawf7DPz3/5QG8AKcTPw7fC
+ST0/sCZ4h7nHbBuqahrdxevyabptRguSSbURPOtIxy/7M6ZqgqKr+B5ffeO38v31
+iKUJTaC89DvN0ovXVx+GO9I+pJIhsAIL6eDEHPF44linJl9MKcgj8G4SP72tRHsL
+vdu6Y40Hxp3cRsxjQLpeRYLdmuVQ6OfXJ4j8bx2K51xJKKoQKXUox1Le+Q==
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/test_ssl/certs/nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAy0X0FJv+ZIV5SjaNPdEn0Hw2KDDmc4BvfEkj0GwX5ETAd02a
+wrwkhOOlTbrS2lF7oSoS1MAZVWksIictGvb8S3/py6g86Gm40k/eTlDi0HQwfEJa
+rqqFpbFxTcl+hotijD4N4zvD9YELjGh5/r8Q+67sEUltZF4afbOSk06WGTqYBKdm
+snRhLUETDKRUDSx4/bSj6Dd4mt76vC6oD2cUWM7Dh9UUDospfUgZsqn1tOivMiFn
+FX5DUosgz584Q7/9yCR/UqOI8vFKFJEqbpFv+31qeMZtLt0eTCtjuzpDnJH539MI
+E2OGfc7oRs/xbB/K90ze2Evg2rwG2YcP/5ZFhQIDAQABAoIBAQCjAro2PNLJMfCO
+fyjNRgmzu6iCmpR0U68T8GN0JPsT576g7e8J828l0pkhuIyW33lRSThIvLSUNf9a
+dChL032H3lBTLduKVh4NKleQXnVFzaeEPoISSFVdButiAhAhPW4OIUVp0OfY3V+x
+fac3j2nDLAfL5SKAtqZv363Py9m66EBYm5BmGTQqT/frQWeCEBvlErQef5RIaU8p
+e2zMWgSNNojVai8U3nKNRvYHWeWXM6Ck7lCvkHhMF+RpbmCZuqhbEARVnehU/Jdn
+QHJ3nxeA2OWpoWKXvAHtSnno49yxq1UIstiQvY+ng5C5i56UlB60UiU2NJ6doZkB
+uQ7/1MaBAoGBAORdcFtgdgRALjXngFWhpCp0CseyUehn1KhxDCG+D1pJ142/ymcf
+oJOzKJPMRNDdDUBMnR1GBfy7rmwvYevI/SMNy2Qs7ofcXPbdtwwvTCToZ1V9/54k
+VfuPBFT+3QzWRvG1tjTV3E4L2VV3nrl2qNPhE5DlfIaU3nQq5Fl0HprJAoGBAOPf
+MWOTGev61CdODO5KN3pLAoamiPs5lEUlz3kM3L1Q52YLITxNDjRj9hWBUATJZOS2
+pLOoYRwmhD7vrnimMc41+NuuFX+4T7hWPc8uSuOxX0VijYtULyNRK57mncG1Fq9M
+RMLbOJ7FD+8jdXNsSMqpQ+pxLJRX/A10O2fOQnbdAoGAL5hV4YWSM0KZHvz332EI
+ER0MXiCJN7HkPZMKH0I4eu3m8hEmAyYxVndBnsQ1F37q0xrkqAQ/HTSUntGlS/og
+4Bxw5pkCwegoq/77tpto+ExDtSrEitYx4XMmSPyxX4qNULU5m3tzJgUML+b1etwD
+Rd2kMU/TC02dq4KBAy/TbRkCgYAl1xN5iJz+XenLGR/2liZ+TWR+/bqzlU006mF4
+pZUmbv/uJxz+yYD5XDwqOA4UrWjuvhG9r9FoflDprp2XdWnB556KxG7XhcDfSJr9
+A5/2DadXe1Ur9O/a+oi2228JEsxQkea9QPA3FVxfBtFjOHEiDlez39VaUP4PMeUH
+iO3qlQKBgFQhdTb7HeYnApYIDHLmd1PvjRvp8XKR1CpEN0nkw8HpHcT1q1MUjQCr
+iT6FQupULEvGmO3frQsgVeRIQDbEdZK3C5xCtn6qOw70sYATVf361BbTtidmU9yV
+THFxwDSVLiVZgFryoY/NtAc27sVdJnGsPRjjaeVgALAsLbmZ1K/H
+-----END RSA PRIVATE KEY-----

+ 71 - 0
nginx-proxy/test/test_ssl/certs/web2.nginx-proxy.tld.crt

@@ -0,0 +1,71 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:37:02 2017 GMT
+            Not After : May 28 00:37:02 2044 GMT
+        Subject: CN=web2.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:da:ee:46:2d:44:7c:f1:e6:91:11:bf:34:d6:02:
+                    4e:fe:43:23:fb:6d:20:f7:8d:1b:c6:9c:cd:1c:1a:
+                    07:95:c2:ed:b9:23:73:c1:02:2b:50:51:3f:33:cf:
+                    8e:aa:f1:13:58:4c:ff:7f:7d:4a:87:fc:f0:0f:54:
+                    af:8c:eb:ba:b4:0f:71:5e:12:1f:64:b1:3d:83:88:
+                    dd:9c:09:50:2d:37:1d:03:3b:18:30:36:f4:82:94:
+                    87:7f:31:27:28:84:0c:99:6d:77:b5:b8:4f:ca:83:
+                    58:d5:d8:4e:36:73:1c:1a:5c:ed:05:ef:95:60:03:
+                    28:0c:9f:d8:6f:98:a8:cd:08:be:af:b1:95:5a:60:
+                    96:fe:2a:d0:98:74:9b:4a:c0:48:66:73:67:54:33:
+                    11:22:20:ea:11:05:ba:a6:ed:74:12:05:d8:de:4f:
+                    01:46:39:74:d8:34:1a:f2:2c:c2:df:6d:94:57:52:
+                    c1:e4:2e:1b:8e:12:0e:43:e7:6f:1f:da:51:80:35:
+                    c2:8a:9b:b6:2a:30:39:6b:a0:77:fa:37:11:b7:ec:
+                    de:6e:8c:6f:93:81:5e:2d:90:69:1b:4b:a4:80:ca:
+                    f4:e5:5b:c0:13:45:b9:76:70:ed:d3:4e:dd:66:98:
+                    99:9f:9d:f0:1e:fd:dd:04:4f:9a:55:bc:38:ad:42:
+                    b9:dd
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web2.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         38:d6:8c:be:3c:5e:5d:61:02:77:eb:5b:6e:a7:1d:4f:69:0d:
+         54:bd:dd:3a:1a:8e:9d:a0:c2:f3:a5:31:91:3e:ec:7a:69:48:
+         40:27:45:a5:c6:b9:af:6d:d9:76:24:97:ec:c5:cf:4d:cc:49:
+         93:ab:bc:4f:01:7e:7a:57:73:4d:27:62:a6:68:bf:4c:00:2c:
+         c0:f3:7b:b3:32:81:6b:96:20:0a:73:a0:85:b5:f8:07:10:88:
+         e8:62:85:41:63:df:43:c5:f9:4b:90:89:6a:16:d9:a6:85:4a:
+         04:1b:5e:75:d8:84:ae:69:c7:62:8f:f1:53:c8:c6:31:71:6d:
+         0c:05:2d:bf:6e:b8:b8:7a:8c:73:6f:17:bb:5c:5a:67:51:12:
+         af:e2:17:9c:40:0e:35:f6:59:93:69:45:14:74:33:c6:aa:04:
+         76:8e:3c:a6:ac:f4:60:cb:53:eb:d6:63:1a:47:7b:be:11:8d:
+         74:de:e8:e5:bc:de:1b:09:db:1b:87:89:b7:6a:20:0a:5e:fb:
+         35:04:ab:b2:f7:4d:a1:86:65:1d:c7:c3:aa:30:15:3a:6e:66:
+         f8:43:33:63:ac:fc:c1:58:48:5b:ec:a0:00:bf:d4:f1:06:03:
+         79:ca:d5:b6:f2:39:0b:62:b4:fd:27:27:e9:37:52:21:ce:a4:
+         32:8a:bb:c7
+-----BEGIN CERTIFICATE-----
+MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDM3MDJaFw00NDA1MjgwMDM3MDJaMB8xHTAbBgNVBAMMFHdl
+YjIubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNzwQIrUFE/M8+O
+qvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsYMDb0gpSHfzEn
+KIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GVWmCW/irQmHSb
+SsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB5C4bjhIOQ+dv
+H9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvAE0W5dnDt007d
+ZpiZn53wHv3dBE+aVbw4rUK53QIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIyLm5n
+aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAONaMvjxeXWECd+tbbqcd
+T2kNVL3dOhqOnaDC86UxkT7semlIQCdFpca5r23ZdiSX7MXPTcxJk6u8TwF+eldz
+TSdipmi/TAAswPN7szKBa5YgCnOghbX4BxCI6GKFQWPfQ8X5S5CJahbZpoVKBBte
+ddiErmnHYo/xU8jGMXFtDAUtv264uHqMc28Xu1xaZ1ESr+IXnEAONfZZk2lFFHQz
+xqoEdo48pqz0YMtT69ZjGkd7vhGNdN7o5bzeGwnbG4eJt2ogCl77NQSrsvdNoYZl
+HcfDqjAVOm5m+EMzY6z8wVhIW+ygAL/U8QYDecrVtvI5C2K0/Scn6TdSIc6kMoq7
+xw==
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/test_ssl/certs/web2.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEA2u5GLUR88eaREb801gJO/kMj+20g940bxpzNHBoHlcLtuSNz
+wQIrUFE/M8+OqvETWEz/f31Kh/zwD1SvjOu6tA9xXhIfZLE9g4jdnAlQLTcdAzsY
+MDb0gpSHfzEnKIQMmW13tbhPyoNY1dhONnMcGlztBe+VYAMoDJ/Yb5iozQi+r7GV
+WmCW/irQmHSbSsBIZnNnVDMRIiDqEQW6pu10EgXY3k8BRjl02DQa8izC322UV1LB
+5C4bjhIOQ+dvH9pRgDXCipu2KjA5a6B3+jcRt+zeboxvk4FeLZBpG0ukgMr05VvA
+E0W5dnDt007dZpiZn53wHv3dBE+aVbw4rUK53QIDAQABAoIBABeTCsl7E30017Ay
+h6z3yKvGbQx43tDpR/FmFwwMnX555AFImQFSi3l1ljmtAu7TUML0X5rJ0gm8qdjs
+xI6HH66d7xYzG2BLWZVdWoef1RtZUO11IpCmikO5XLHMiCvrtDOdPwO5WhYzeJBm
+X12rnX4VPYyjFNGm5Vwepj62EI8rS9G3NG00pDYPmN/vUQJiV/iTRIlvXgFm4Hl2
+zJhVi+NhyiptFEycdg65JwVfLKtmUXRmwGFiSxQi1FX8YS5EdIV2S8yDwXlWSxmq
+4R1eSID7pKxtzyRtBqZJggzfqtY8cMpoOC12FbLAvzagOavs4ntMgAVy5k2T15G2
+syQyLSECgYEA+1NIRF3CxNUaPvpcR8Y4PWhwDEzqn5ZscnXaFrUp1W72f3bpwSCa
+/t9lXe9O53R5/yt4nCbwvVM0UWYPHGZGQr+5AS7WWDVWVcwkXX1NIjALi0TXQ0Ty
+zeuViXDofUS31yhwFFmgGa52pd+edXaGRvxzGyPVdtwpSLZP/gBLQykCgYEA3wC9
+sHNPKMxi6vI2VBvmBXHoCSDAkuRLmQEGDmjjt0Ve4fmwGRbBJxniOlNtufNQRfRg
+67qaeM4BTrP03cilZ71yXvLN5G2opY5c/I0dYTXRhV3IV6XwlCC+0xmn+ro7kB8x
+J4wAj/h/tJ8T0+0TpnbyjmPH4KTJ9GjRTKwe65UCgYBszIHlbr2JXkONbe6S98GS
+++o9uPJ9Aa6S4mf2Gpkwl2fIiF7rR0Ux/t2wC5AZ7Ld/en8tAkKHg0SL1GXIQpI6
+BSt+0prh9r0YSVaYzkyc9zWYJcYWjfuan1jN9f3/dMctMolKlf4UAA3HAwZjDVtV
+0aW24w1e9jI9EweQCuqJ+QKBgBwZec18GiNn7abxMktS4J8bBUPxLpLT1XrIGD1E
+lj0HrrcGwVvH9Dq7FjiHPrJJqHnIG1ZYwxIp0xxZrKctmzoBMyInsi3wa2nBEJJ6
+LZOMNoR5lr8El9XyclkjSHldchfs9kKnb4K0q1LVIKh5nRpCrrmmdQ8ndJMpigYB
+QjwpAoGAWIhFrN0Acdq7Xc9pScqnAohPMWCTBUbeKrOm0ZwN4Q9D+lLeeggbWlWy
+AqR4WHQMHc6B+p4Ncg3XBCFw0V62PkYhSCdaLNH3CPyFT0qoeY8VuMjmqS7yoKvp
+uMMHfzMmyGg0dyplVGgANafb/it6Dp8T0pnCmzxhe57jf9xsVBo=
+-----END RSA PRIVATE KEY-----

+ 71 - 0
nginx-proxy/test/test_ssl/certs/web3.nginx-proxy.tld.crt

@@ -0,0 +1,71 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 4096 (0x1000)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: O=nginx-proxy test suite, CN=www.nginx-proxy.tld
+        Validity
+            Not Before: Jan 10 00:58:11 2017 GMT
+            Not After : May 28 00:58:11 2044 GMT
+        Subject: CN=web3.nginx-proxy.tld
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:ac:9b:90:dc:2c:0d:c3:ae:f4:a0:cc:40:15:d2:
+                    c4:c2:2c:8c:15:b6:70:28:cf:32:c4:03:ce:b3:87:
+                    30:5d:a6:12:96:69:7a:fe:67:29:1c:8c:24:bb:6a:
+                    c8:86:13:19:91:00:3e:ef:00:67:50:b0:ea:c9:93:
+                    c6:8a:73:82:d8:37:9f:8e:6e:12:13:ec:fa:08:0f:
+                    ac:73:6e:42:96:67:9f:20:c5:1c:a3:b1:4a:83:36:
+                    0e:0a:31:93:76:b1:b6:37:4f:e0:88:3c:46:dc:c1:
+                    53:60:df:28:ae:3e:20:d8:d9:53:a2:be:09:38:f0:
+                    ff:4a:91:45:cb:cb:b5:b3:3d:7d:09:98:47:dc:0d:
+                    5e:83:73:b6:c9:f3:fb:9a:f2:bb:b0:62:ca:aa:af:
+                    6b:42:e5:08:b2:14:87:f4:fc:f1:14:f8:cc:76:b3:
+                    c0:49:df:66:c6:21:a0:bc:5e:0c:bb:de:e9:9c:e7:
+                    fb:31:a1:cf:c4:e9:bb:bd:c3:5a:0d:23:52:c6:b3:
+                    84:77:f1:0c:3d:ca:c3:60:48:f9:7e:a6:dc:4f:f7:
+                    d2:5b:7c:02:4d:38:09:64:33:7e:bb:b1:65:bb:e2:
+                    2b:1d:9a:49:d4:34:21:42:7a:49:3e:11:6c:10:66:
+                    b4:91:db:bd:3a:c2:8d:f4:e4:03:b1:b4:6e:5c:98:
+                    bf:7d
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Subject Alternative Name: 
+                DNS:web3.nginx-proxy.tld
+    Signature Algorithm: sha256WithRSAEncryption
+         9a:f6:b9:c2:08:a4:b4:d7:e4:b2:d3:22:e9:fe:69:4a:e8:76:
+         18:60:11:1b:3b:7c:4b:c3:72:66:95:b7:7c:de:c7:34:32:58:
+         aa:5d:e0:12:f0:df:27:b6:3f:dd:f1:8c:ed:ce:bd:73:50:fc:
+         9b:e1:8c:c2:7f:ac:6b:54:9d:23:0a:d9:a6:25:cc:99:94:73:
+         2b:69:e8:f7:07:40:37:d3:d4:0b:14:86:6a:a1:01:53:4b:ae:
+         85:2d:12:13:bd:23:1e:09:cf:20:50:24:76:a6:5f:b4:d6:d6:
+         e1:34:40:93:4d:42:f7:d0:95:98:77:53:16:e7:ce:cc:4c:35:
+         b8:30:3b:62:95:e2:40:0c:a1:73:84:92:93:63:df:c6:21:d5:
+         eb:1d:a1:d0:f2:ec:a5:cf:d6:10:c9:8f:ac:11:16:39:cd:b0:
+         38:04:29:cf:e1:1c:dd:21:df:1f:95:35:a5:a4:61:2b:3d:8b:
+         8a:76:02:6d:31:a1:e8:6b:c5:3b:eb:90:40:34:16:5a:07:93:
+         96:56:cd:8b:52:ca:65:51:78:d3:f2:af:40:44:43:ec:fe:a2:
+         c8:d4:6d:21:c7:1f:d2:45:28:0d:d2:51:0f:d1:a5:db:00:2a:
+         3a:10:88:9e:5a:76:a2:f7:e2:f0:fe:14:a3:e8:ec:ef:00:f0:
+         35:87:eb:7c
+-----BEGIN CERTIFICATE-----
+MIIC/TCCAeWgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwPzEfMB0GA1UECgwWbmdp
+bngtcHJveHkgdGVzdCBzdWl0ZTEcMBoGA1UEAwwTd3d3Lm5naW54LXByb3h5LnRs
+ZDAeFw0xNzAxMTAwMDU4MTFaFw00NDA1MjgwMDU4MTFaMB8xHTAbBgNVBAMMFHdl
+YjMubmdpbngtcHJveHkudGxkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6/mcpHIwku2rI
+hhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUco7FKgzYOCjGT
+drG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH3A1eg3O2yfP7
+mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7MaHPxOm7vcNa
+DSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ1DQhQnpJPhFs
+EGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABoyMwITAfBgNVHREEGDAWghR3ZWIzLm5n
+aW54LXByb3h5LnRsZDANBgkqhkiG9w0BAQsFAAOCAQEAmva5wgiktNfkstMi6f5p
+Suh2GGARGzt8S8NyZpW3fN7HNDJYql3gEvDfJ7Y/3fGM7c69c1D8m+GMwn+sa1Sd
+IwrZpiXMmZRzK2no9wdAN9PUCxSGaqEBU0uuhS0SE70jHgnPIFAkdqZftNbW4TRA
+k01C99CVmHdTFufOzEw1uDA7YpXiQAyhc4SSk2PfxiHV6x2h0PLspc/WEMmPrBEW
+Oc2wOAQpz+Ec3SHfH5U1paRhKz2LinYCbTGh6GvFO+uQQDQWWgeTllbNi1LKZVF4
+0/KvQERD7P6iyNRtIccf0kUoDdJRD9Gl2wAqOhCInlp2ovfi8P4Uo+js7wDwNYfr
+fA==
+-----END CERTIFICATE-----

+ 27 - 0
nginx-proxy/test/test_ssl/certs/web3.nginx-proxy.tld.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEArJuQ3CwNw670oMxAFdLEwiyMFbZwKM8yxAPOs4cwXaYSlml6
+/mcpHIwku2rIhhMZkQA+7wBnULDqyZPGinOC2Defjm4SE+z6CA+sc25ClmefIMUc
+o7FKgzYOCjGTdrG2N0/giDxG3MFTYN8orj4g2NlTor4JOPD/SpFFy8u1sz19CZhH
+3A1eg3O2yfP7mvK7sGLKqq9rQuUIshSH9PzxFPjMdrPASd9mxiGgvF4Mu97pnOf7
+MaHPxOm7vcNaDSNSxrOEd/EMPcrDYEj5fqbcT/fSW3wCTTgJZDN+u7Flu+IrHZpJ
+1DQhQnpJPhFsEGa0kdu9OsKN9OQDsbRuXJi/fQIDAQABAoIBAA14DjPAFEriyiAK
+EC4jxkrIox3GoLXuhS2ahnSn5fRI00Z9cKWNcz3RCcS+LmuX7fTMqhyIUYeQZqHY
+MDP5k4o/vOmmWS7I3THn1zMitXt7FoW+G+ACI6hdfXb6K2GluGxUhVbcLUNoqpLy
+lwARxQpm2wnl/l49IA63i1S9zq3vy5HSvxBv3jq8xp2PX3Sho33LhsvW+miCJe+R
+etKSV4mAjvR62XVgUGJ40FciVMK3pzwwIKPLI7k9sa7WHZr6fNHeDxIWA6AsVBTH
+O+2l8Ufd79KesOD6VqdHlxg2a79s3NoCOflQQAbOSSR9ioCE7Ykgvw0wVl57xNoB
+WZVAY0ECgYEA2I6+a56NMzkEMxr0w9ZRqgocXCgbqxZx1v7newDyO5J3X4jYhmMJ
+abNZtnVs1Pz0IAgCMCf+N4D+RAi9/ahYxq7jmhIkT/IUHseh93XLgd/x9Q9ZJOGm
+9+NI3aIBgWOsy4orKxfwzRAVEakOCChYUCy/GUDDD1MPcjxC8ma5abUCgYEAzAua
+15Nayr9Sg0QsHqNvgTVkVlA6u+TwN+vfI8cH5nmXIMm5ShwCc9+Pm8mpcFwUo4uE
+vOzQ+NwG9CBbVu6/i1/aR+ZlbctdhpsW51v18B9eFVXSZUvEv1ONGdoJZhq0tW7W
+V4Zjf+UwdTcrSZKVpd5woUbRkByROPdir/3Ie6kCgYBhf5LX3SBxSWBMqfw9F4bY
+6YhvLVeXpZlHVKhfRsPIcl7wUio6Bui8ABWKAkAnfGNk8HYbvEXGM3tGojD3vQ2L
+Fj4+paBXpgPM/9A6G3yuUmcbD/fwlO+Zd2jc8A2BdaDcWq6ozjSJ/o2dz+ETZyar
+ohm/gtrPUXQI2HzDqeAcaQKBgEBMd+LvAHFbkPjkhrKw9fZViOTaK2gCYOB+Z7ay
+hX7PWhxu9QCxiuRQ0sRY7BgILEjNMmsGhWOmklpjx+TBH4MgFX0K0XOj3jkIrlMB
+26JrgA5hGQfqtHlGLvSyjLusNr3ly42RP9GRu490byOkGZxHWF66Hle3aNv2uRaU
+dpThAoGBAMIPpf4E6xGzurhgYdXMit3jGAYD85BbNUIm9jOym2lxS63ipoF08QhH
+NoMVRf/AUoS4VDGXsuABjMffTZbE8L+DaL1cWSuPJAoF9AXUXtz6A8LRc9Mn2WjS
+L8BIs9xZCzrJ5XzY4PSnjjyAU81z6E80azWglkmh8rRDzi/o9O79
+-----END RSA PRIVATE KEY-----

+ 93 - 0
nginx-proxy/test/test_ssl/test_dhparam.py

@@ -0,0 +1,93 @@
+import re
+import subprocess
+
+import backoff
+import docker
+import pytest
+
+docker_client = docker.from_env()
+
+
+###############################################################################
+#
+# Tests helpers
+#
+###############################################################################
+
+@backoff.on_exception(backoff.constant, AssertionError, interval=2, max_tries=15, jitter=None)
+def assert_log_contains(expected_log_line):
+    """
+    Check that the nginx-proxy container log contains a given string.
+    The backoff decorator will retry the check 15 times with a 2 seconds delay.
+
+    :param expected_log_line: string to search for
+    :return: None
+    :raises: AssertError if the expected string is not found in the log
+    """
+    sut_container = docker_client.containers.get("nginxproxy")
+    docker_logs = sut_container.logs(stdout=True, stderr=True, stream=False, follow=False)
+    assert expected_log_line in docker_logs
+
+
+def require_openssl(required_version):
+    """
+    This function checks that the required version of OpenSSL is present, and skips the test if not.
+    Use it as a test function decorator:
+
+        @require_openssl("2.3.4")
+        def test_something():
+            ...
+
+    :param required_version: minimal required version as a string: "1.2.3"
+    """
+
+    def versiontuple(v):
+        clean_v = re.sub("[^\d\.]", "", v)
+        return tuple(map(int, (clean_v.split("."))))
+
+    try:
+        command_output = subprocess.check_output(["openssl", "version"])
+    except OSError:
+        return pytest.mark.skip("openssl command is not available in test environment")
+    else:
+        if not command_output:
+            raise Exception("Could not get openssl version")
+        openssl_version = command_output.split()[1]
+        return pytest.mark.skipif(
+            versiontuple(openssl_version) < versiontuple(required_version),
+            reason="openssl v%s is less than required version %s" % (openssl_version, required_version))
+
+
+###############################################################################
+#
+# Tests
+#
+###############################################################################
+
+def test_dhparam_is_not_generated_if_present(docker_compose):
+    sut_container = docker_client.containers.get("nginxproxy")
+    assert sut_container.status == "running"
+
+    assert_log_contains("Custom dhparam.pem file found, generation skipped")
+
+    # Make sure the dhparam in use is not the default, pre-generated one
+    default_checksum = sut_container.exec_run("md5sum /app/dhparam.pem.default").split()
+    current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").split()
+    assert default_checksum[0] != current_checksum[0]
+
+
+def test_web5_https_works(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web5.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 85\n" in r.text
+
+
+@require_openssl("1.0.2")
+def test_web5_dhparam_is_used(docker_compose):
+    sut_container = docker_client.containers.get("nginxproxy")
+    assert sut_container.status == "running"
+
+    host = "%s:443" % sut_container.attrs["NetworkSettings"]["IPAddress"]
+    r = subprocess.check_output(
+        "echo '' | openssl s_client -connect %s -cipher 'EDH' | grep 'Server Temp Key'" % host, shell=True)
+    assert "Server Temp Key: DH, 2048 bits\n" == r

+ 16 - 0
nginx-proxy/test/test_ssl/test_dhparam.yml

@@ -0,0 +1,16 @@
+web5:
+  image: web
+  expose:
+    - "85"
+  environment:
+    WEB_PORTS: "85"
+    VIRTUAL_HOST: "web5.nginx-proxy.tld"
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  container_name: nginxproxy
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+    - ./certs:/etc/nginx/certs:ro

+ 44 - 0
nginx-proxy/test/test_ssl/test_dhparam_generation.py

@@ -0,0 +1,44 @@
+import backoff
+import docker
+
+docker_client = docker.from_env()
+
+
+###############################################################################
+#
+# Tests helpers
+#
+###############################################################################
+
+@backoff.on_exception(backoff.constant, AssertionError, interval=2, max_tries=15, jitter=None)
+def assert_log_contains(expected_log_line):
+    """
+    Check that the nginx-proxy container log contains a given string.
+    The backoff decorator will retry the check 15 times with a 2 seconds delay.
+
+    :param expected_log_line: string to search for
+    :return: None
+    :raises: AssertError if the expected string is not found in the log
+    """
+    sut_container = docker_client.containers.get("nginxproxy")
+    docker_logs = sut_container.logs(stdout=True, stderr=True, stream=False, follow=False)
+    assert expected_log_line in docker_logs
+
+
+###############################################################################
+#
+# Tests
+#
+###############################################################################
+
+def test_dhparam_is_generated_if_missing(docker_compose):
+    sut_container = docker_client.containers.get("nginxproxy")
+    assert sut_container.status == "running"
+
+    assert_log_contains("Generating DH parameters")
+    assert_log_contains("dhparam generation complete, reloading nginx")
+
+    # Make sure the dhparam in use is not the default, pre-generated one
+    default_checksum = sut_container.exec_run("md5sum /app/dhparam.pem.default").split()
+    generated_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").split()
+    assert default_checksum[0] != generated_checksum[0]

+ 8 - 0
nginx-proxy/test/test_ssl/test_dhparam_generation.yml

@@ -0,0 +1,8 @@
+sut:
+  image: jwilder/nginx-proxy:test
+  container_name: nginxproxy
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ./certs:/etc/nginx/certs:ro
+  environment:
+    - DHPARAM_BITS=256

+ 26 - 0
nginx-proxy/test/test_ssl/test_hsts.py

@@ -0,0 +1,26 @@
+import pytest
+
+
+def test_web1_HSTS_default(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web1.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 81\n" in r.text
+    assert "Strict-Transport-Security" in r.headers
+    assert "max-age=31536000" == r.headers["Strict-Transport-Security"]
+
+# Regression test to ensure HSTS is enabled even when the upstream sends an error in response
+# Issue #1073 https://github.com/jwilder/nginx-proxy/pull/1073
+def test_web1_HSTS_error(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web1.nginx-proxy.tld/status/500", allow_redirects=False)
+    assert "Strict-Transport-Security" in r.headers
+    assert "max-age=31536000" == r.headers["Strict-Transport-Security"]
+
+def test_web2_HSTS_off(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 81\n" in r.text
+    assert "Strict-Transport-Security" not in r.headers
+
+def test_web3_HSTS_custom(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 81\n" in r.text
+    assert "Strict-Transport-Security" in r.headers
+    assert "max-age=86400; includeSubDomains; preload" == r.headers["Strict-Transport-Security"]

+ 32 - 0
nginx-proxy/test/test_ssl/test_hsts.yml

@@ -0,0 +1,32 @@
+web1:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "web1.nginx-proxy.tld"
+
+web2:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "web2.nginx-proxy.tld"
+    HSTS: "off"
+
+web3:
+  image: web
+  expose:
+    - "81"
+  environment:
+    WEB_PORTS: "81"
+    VIRTUAL_HOST: "web3.nginx-proxy.tld"
+    HSTS: "max-age=86400; includeSubDomains; preload"
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+    - ./certs:/etc/nginx/certs:ro

+ 18 - 0
nginx-proxy/test/test_ssl/test_nohttp.py

@@ -0,0 +1,18 @@
+import pytest
+
+
+def test_web2_http_is_not_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False)
+    assert r.status_code == 503
+
+
+def test_web2_https_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 82\n" in r.text
+
+
+def test_web2_HSTS_policy_is_active(docker_compose, nginxproxy):
+    r = nginxproxy.get("https://web2.nginx-proxy.tld/port", allow_redirects=False)
+    assert "answer from port 82\n" in r.text
+    assert "Strict-Transport-Security" in r.headers

+ 16 - 0
nginx-proxy/test/test_ssl/test_nohttp.yml

@@ -0,0 +1,16 @@
+web2:
+  image: web
+  expose:
+    - "82"
+  environment:
+    WEB_PORTS: "82"
+    VIRTUAL_HOST: "web2.nginx-proxy.tld"
+    HTTPS_METHOD: nohttp
+
+
+sut:
+  image: jwilder/nginx-proxy:test
+  volumes:
+    - /var/run/docker.sock:/tmp/docker.sock:ro
+    - ../lib/ssl/dhparam.pem:/etc/nginx/dhparam/dhparam.pem:ro
+    - ./certs:/etc/nginx/certs:ro

+ 12 - 0
nginx-proxy/test/test_ssl/test_nohttps.py

@@ -0,0 +1,12 @@
+import pytest
+from requests import ConnectionError
+
+def test_http_is_forwarded(docker_compose, nginxproxy):
+    r = nginxproxy.get("http://web.nginx-proxy.tld/port", allow_redirects=False)
+    assert r.status_code == 200
+    assert "answer from port 83\n" in r.text
+
+
+def test_https_is_disabled(docker_compose, nginxproxy):
+    with pytest.raises(ConnectionError):
+        nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False)

+ 0 - 0
nginx-proxy/test/test_ssl/test_nohttps.yml


Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels