diff --git a/.dockerignore b/.dockerignore index 8fafbb0..aad7a31 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,9 @@ .git +.github +test .dockerignore -circle.yml +.gitignore +*.yml +Dockerfile* Makefile README.md -test diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 99d688b..9824d63 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,16 +1,35 @@ -# !!!PLEASE READ!!! +# ⚠️ PLEASE READ ⚠️ -## Questions +## Questions or Features -If you have a question, DO NOT SUBMIT a new issue. +If you have a question or want to request a feature, please **DO NOT SUBMIT** a new issue. -Please ask the question on the Discussions section: https://github.com/nginx-proxy/nginx-proxy/discussions +Instead please use the relevant Discussions section's category: +- 🙏 [Ask a question](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/q-a) +- 💡 [Request a feature](https://github.com/nginx-proxy/nginx-proxy/discussions/categories/ideas) -## Bugs or Features +## Bugs -If you are logging a bug or feature request, please search the current open issues to see if there is already a bug or feature opened. +If you are logging a bug, please search the current open issues first to see if there is already a bug opened. -For bugs, the easier you make it to reproduce the issue you see, the easier and faster it can get fixed. If you can provide a script or docker-compose file that reproduces the problems, that is very helpful. +For bugs, the easier you make it to reproduce the issue you see and the more initial information you provide, the easier and faster the bug can be identified and can get fixed. + +Please at least provide: +- the exact nginx-proxy version you're using (if using `latest` please make sure it is up to date and provide the version number printed at container startup). +- complete configuration (compose file, command line, etc) of both your nginx-proxy container(s) and proxied containers. You should redact sensitive info if needed but please provide **full** configurations. +- generated nginx configuration obtained with `docker exec nameofyournginxproxycontainer nginx -T` + +If you can provide a script or docker-compose file that reproduces the problems, that is very helpful. + +## General advice about `latest` + +Do not use the `latest` tag for production setups. + +`latest` is nothing more than a convenient default used by Docker if no specific tag is provided, there isn't any strict convention on what goes into this tag over different projects, and it does not carry any promise of stability. + +Using `latest` will most certainly put you at risk of experiencing uncontrolled updates to non backward compatible versions (or versions with breaking changes) and makes it harder for maintainers to track which exact version of the container you are experiencing an issue with. + +This recommendation stands for pretty much every Docker image in existence, not just nginx-proxy's ones. Thanks, -Jason +Nicolas diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 5d4cfba..706d298 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -12,7 +12,6 @@ on: paths-ignore: - 'test/*' - '.gitignore' - - '.travis.yml' - 'docker-compose-separate-containers.yml' - 'docker-compose.yml' - 'LICENSE' @@ -28,18 +27,25 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + + - name: Retrieve version + run: echo "GIT_DESCRIBE=$(git describe --tags)" >> $GITHUB_ENV - name: Get Docker tags for Debian based image id: docker_meta_debian - uses: crazy-max/ghaction-docker-meta@v2 + uses: docker/metadata-action@v3 with: images: | + ghcr.io/nginx-proxy/nginx-proxy nginxproxy/nginx-proxy jwilder/nginx-proxy tags: | type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + labels: | + org.opencontainers.image.authors=Nicolas Duchon (@buchdag), Jason Wilder + org.opencontainers.image.version=${{ env.GIT_DESCRIBE }} - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -52,6 +58,13 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the Debian based image id: docker_build_debian @@ -59,6 +72,7 @@ jobs: with: context: . file: Dockerfile + build-args: NGINX_PROXY_VERSION=${{ env.GIT_DESCRIBE }} platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: ${{ steps.docker_meta_debian.outputs.tags }} @@ -75,18 +89,25 @@ jobs: uses: actions/checkout@v2 with: fetch-depth: 0 + + - name: Retrieve version + run: echo "GIT_DESCRIBE=$(git describe --tags)" >> $GITHUB_ENV - name: Get Docker tags for Alpine based image id: docker_meta_alpine - uses: crazy-max/ghaction-docker-meta@v2 + uses: docker/metadata-action@v3 with: images: | + ghcr.io/nginx-proxy/nginx-proxy nginxproxy/nginx-proxy jwilder/nginx-proxy tags: | type=semver,suffix=-alpine,pattern={{version}} type=semver,suffix=-alpine,pattern={{major}}.{{minor}} - type=raw,value=alpine,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} + type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/main' }} + labels: | + org.opencontainers.image.authors=Nicolas Duchon (@buchdag), Jason Wilder + org.opencontainers.image.version=${{ env.GIT_DESCRIBE }} flavor: latest=false - name: Set up QEMU @@ -100,6 +121,13 @@ jobs: with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push the Alpine based image id: docker_build_alpine @@ -107,6 +135,7 @@ jobs: with: context: . file: Dockerfile.alpine + build-args: NGINX_PROXY_VERSION=${{ env.GIT_DESCRIBE }} platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: ${{ steps.docker_meta_alpine.outputs.tags }} diff --git a/Dockerfile b/Dockerfile index 8005b95..dcd0285 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,16 @@ # setup build arguments for version of dependencies to use -ARG DOCKER_GEN_VERSION=0.7.7 +ARG DOCKER_GEN_VERSION=0.9.0 ARG FOREGO_VERSION=v0.17.0 # Use a specific version of golang to build both binaries -FROM golang:1.16.7 as gobuilder +FROM golang:1.18.1 as gobuilder # Build docker-gen from scratch FROM gobuilder as dockergen ARG DOCKER_GEN_VERSION -RUN git clone https://github.com/jwilder/docker-gen \ +RUN git clone https://github.com/nginx-proxy/docker-gen \ && cd /go/docker-gen \ && git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \ && go mod download \ @@ -36,8 +36,15 @@ RUN git clone https://github.com/nginx-proxy/forego/ \ && rm -rf /go/forego # Build the final image -FROM nginx:1.21.4 -LABEL maintainer="Nicolas Duchon (@buchdag)" +FROM nginx:1.21.6 + +ARG NGINX_PROXY_VERSION +# Add DOCKER_GEN_VERSION environment variable +# Because some external projects rely on it +ARG DOCKER_GEN_VERSION +ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ + DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \ + DOCKER_HOST=unix:///tmp/docker.sock # Install wget and install/updates certificates RUN apt-get update \ @@ -48,7 +55,7 @@ RUN apt-get update \ && rm -r /var/lib/apt/lists/* -# Configure Nginx and apply fix for very long server names +# Configure Nginx RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ && sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \ && sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \ @@ -58,17 +65,10 @@ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen -# Add DOCKER_GEN_VERSION environment variable -# Because some external projects rely on it -ARG DOCKER_GEN_VERSION -ENV DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} - COPY network_internal.conf /etc/nginx/ -COPY . /app/ +COPY app nginx.tmpl LICENSE /app/ WORKDIR /app/ -ENV DOCKER_HOST unix:///tmp/docker.sock - ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["forego", "start", "-r"] diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 75250f6..51cafd9 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,9 +1,9 @@ # setup build arguments for version of dependencies to use -ARG DOCKER_GEN_VERSION=0.7.7 +ARG DOCKER_GEN_VERSION=0.9.0 ARG FOREGO_VERSION=v0.17.0 # Use a specific version of golang to build both binaries -FROM golang:1.16.7-alpine as gobuilder +FROM golang:1.18.1-alpine as gobuilder RUN apk add --no-cache git musl-dev # Build docker-gen from scratch @@ -11,7 +11,7 @@ FROM gobuilder as dockergen ARG DOCKER_GEN_VERSION -RUN git clone https://github.com/jwilder/docker-gen \ +RUN git clone https://github.com/nginx-proxy/docker-gen \ && cd /go/docker-gen \ && git -c advice.detachedHead=false checkout $DOCKER_GEN_VERSION \ && go mod download \ @@ -37,15 +37,22 @@ RUN git clone https://github.com/nginx-proxy/forego/ \ && rm -rf /go/forego # Build the final image -FROM nginx:1.21.4-alpine -LABEL maintainer="Nicolas Duchon (@buchdag)" +FROM nginx:1.21.6-alpine + +ARG NGINX_PROXY_VERSION +# Add DOCKER_GEN_VERSION environment variable +# Because some external projects rely on it +ARG DOCKER_GEN_VERSION +ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ + DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} \ + DOCKER_HOST=unix:///tmp/docker.sock # 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 +# Configure Nginx RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ && sed -i 's/worker_processes 1/worker_processes auto/' /etc/nginx/nginx.conf \ && sed -i 's/worker_connections 1024/worker_connections 10240/' /etc/nginx/nginx.conf \ @@ -55,17 +62,10 @@ RUN echo "daemon off;" >> /etc/nginx/nginx.conf \ COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego COPY --from=dockergen /usr/local/bin/docker-gen /usr/local/bin/docker-gen -# Add DOCKER_GEN_VERSION environment variable -# Because some external projects rely on it -ARG DOCKER_GEN_VERSION -ENV DOCKER_GEN_VERSION=${DOCKER_GEN_VERSION} - COPY network_internal.conf /etc/nginx/ -COPY . /app/ +COPY app nginx.tmpl LICENSE /app/ WORKDIR /app/ -ENV DOCKER_HOST unix:///tmp/docker.sock - ENTRYPOINT ["/app/docker-entrypoint.sh"] CMD ["forego", "start", "-r"] diff --git a/LICENSE b/LICENSE index fc926a8..ce7d86c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT License (MIT) -Copyright (c) 2014 Jason Wilder +Copyright (c) 2014-2020 Jason Wilder +Copyright (c) 2021-2022 Nicolas Duchon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 18fcd33..ab44880 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,10 @@ build-webserver: docker build -t web test/requirements/web build-nginx-proxy-test-debian: - docker build -t nginxproxy/nginx-proxy:test . + docker build --build-arg NGINX_PROXY_VERSION="test" -t nginxproxy/nginx-proxy:test . build-nginx-proxy-test-alpine: - docker build -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test . + docker build --build-arg NGINX_PROXY_VERSION="test" -f Dockerfile.alpine -t nginxproxy/nginx-proxy:test . test-debian: build-webserver build-nginx-proxy-test-debian test/pytest.sh diff --git a/README.md b/README.md index 13d8c2d..acf1aca 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Test](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml/badge.svg)](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml) [![GitHub release](https://img.shields.io/github/v/release/nginx-proxy/nginx-proxy)](https://github.com/nginx-proxy/nginx-proxy/releases) -![nginx 1.21.4](https://img.shields.io/badge/nginx-1.21.4-brightgreen.svg) +![nginx 1.21.6](https://img.shields.io/badge/nginx-1.21.6-brightgreen.svg) [![Docker Image Size](https://img.shields.io/docker/image-size/nginxproxy/nginx-proxy?sort=semver)](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub") [![Docker stars](https://img.shields.io/docker/stars/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub') [![Docker pulls](https://img.shields.io/docker/pulls/nginxproxy/nginx-proxy.svg)](https://hub.docker.com/r/nginxproxy/nginx-proxy 'DockerHub') @@ -115,7 +115,51 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie ### Wildcard Hosts -You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [xip.io](http://xip.io), using `~^foo\.bar\..*\.xip\.io` will match `foo.bar.127.0.0.1.xip.io`, `foo.bar.10.0.2.2.xip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html). +You can also use wildcards at the beginning and the end of host name, like `*.bar.com` or `foo.bar.*`. Or even a regular expression, which can be very useful in conjunction with a wildcard DNS service like [nip.io](https://nip.io) or [sslip.io](https://sslip.io), using `~^foo\.bar\..*\.nip\.io` will match `foo.bar.127.0.0.1.nip.io`, `foo.bar.10.0.2.2.nip.io` and all other given IPs. More information about this topic can be found in the nginx documentation about [`server_names`](http://nginx.org/en/docs/http/server_names.html). + +### Path-based Routing + +You can have multiple containers proxied by the same `VIRTUAL_HOST` by adding a `VIRTUAL_PATH` environment variable containing the absolute path to where the container should be mounted. For example with `VIRTUAL_HOST=foo.example.com` and `VIRTUAL_PATH=/api/v2/service`, then requests to http://foo.example.com/api/v2/service will be routed to the container. If you wish to have a container serve the root while other containers serve other paths, give the root container a `VIRTUAL_PATH` of `/`. Unmatched paths will be served by the container at `/` or will return the default nginx error page if no container has been assigned `/`. +It is also possible to specify multiple paths with regex locations like `VIRTUAL_PATH=~^/(app1|alternative1)/`. For further details see the nginx documentation on location blocks. This is not compatible with `VIRTUAL_DEST`. + +The full request URI will be forwarded to the serving container in the `X-Forwarded-Path` header. + +**NOTE**: Your application needs to be able to generate links starting with `VIRTUAL_PATH`. This can be achieved by it being natively on this path or having an option to prepend this path. The application does not need to expect this path in the request. + +#### VIRTUAL_DEST + +This environment variable can be used to rewrite the `VIRTUAL_PATH` part of the requested URL to proxied application. The default value is empty (off). +Make sure that your settings won't result in the slash missing or being doubled. Both these versions can cause troubles. + +If the application runs natively on this sub-path or has a setting to do so, `VIRTUAL_DEST` should not be set or empty. +If the requests are expected to not contain a sub-path and the generated links contain the sub-path, `VIRTUAL_DEST=/` should be used. + +```console +$ docker run -d -e VIRTUAL_HOST=example.tld -e VIRTUAL_PATH=/app1/ -e VIRTUAL_DEST=/ --name app1 app +``` + +In this example, the incoming request `http://example.tld/app1/foo` will be proxied as `http://app1/foo` instead of `http://app1/app1/foo`. + +#### Per-VIRTUAL_PATH location configuration + +The same options as from [Per-VIRTUAL_HOST location configuration](#Per-VIRTUAL_HOST-location-configuration) are available on a `VIRTUAL_PATH` basis. +The only difference is that the filename gets an additional block `HASH=$(echo -n $VIRTUAL_PATH | sha1sum | awk '{ print $1 }')`. This is the sha1-hash of the `VIRTUAL_PATH` (no newline). This is done filename sanitization purposes. +The used filename is `${VIRTUAL_HOST}_${HASH}_location` + +The filename of the previous example would be `example.tld_8610f6c344b4096614eab6e09d58885349f42faf_location`. + +#### DEFAULT_ROOT + +This environment variable of the nginx proxy container can be used to customize the return error page if no matching path is found. Furthermore it is possible to use anything which is compatible with the `return` statement of nginx. + +For example `DEFAUL_ROOT=418` will return a 418 error page instead of the normal 404 one. +Another example is `DEFAULT_ROOT="301 https://github.com/nginx-proxy/nginx-proxy/blob/main/README.md"` which would redirect an invalid request to this documentation. +Nginx variables such as $scheme, $host, and $request_uri can be used. However, care must be taken to make sure the $ signs are escaped properly. +If you want to use `301 $scheme://$host/myapp1$request_uri` you should use: + +* Bash: `DEFAULT_ROOT='301 $scheme://$host/myapp1$request_uri'` +* Docker Compose yaml: `- DEFAULT_ROOT: 301 $$scheme://$$host/myapp1$$request_uri` + ### Virtual Host Aliases @@ -218,7 +262,7 @@ docker run -d -e VIRTUAL_HOST=foo.bar.com nginx ### Separate Containers -nginx-proxy can also be run as two separate containers using the [jwilder/docker-gen](https://hub.docker.com/r/jwilder/docker-gen) image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image. +nginx-proxy can also be run as two separate containers using the [nginxproxy/docker-gen](https://hub.docker.com/r/nginxproxy/docker-gen) image and the official [nginx](https://registry.hub.docker.com/_/nginx/) image. You may want to do this to prevent having the docker socket bound to a publicly exposed container service. @@ -249,7 +293,7 @@ Then start the docker-gen container with the shared volume and template: docker run --volumes-from nginx \ -v /var/run/docker.sock:/tmp/docker.sock:ro \ -v $(pwd):/etc/docker-gen/templates \ - -t jwilder/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf + -t nginxproxy/docker-gen -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf ``` Finally, start your containers with `VIRTUAL_HOST` environment variables. @@ -284,7 +328,7 @@ To use custom `dhparam.pem` files per-virtual-host, the files should be named af > COMPATIBILITY WARNING: The default generated `dhparam.pem` key is 4096 bits for A+ security. Some older clients (like Java 6 and 7) do not support DH keys with over 1024 bits. In order to support these clients, you must provide your own `dhparam.pem`. -In the separate container setup, no pre-generated key will be available and neither the [jwilder/docker-gen](https://hub.docker.com/r/jwilder/docker-gen) image, nor the offical [nginx](https://registry.hub.docker.com/_/nginx/) image will provide one. If you still want A+ security in a separate container setup, you should mount an RFC7919 DH key file to the nginx container at `/etc/nginx/dhparam/dhparam.pem`. +In the separate container setup, no pre-generated key will be available and neither the [nginxproxy/docker-gen](https://hub.docker.com/r/nginxproxy/docker-gen) image, nor the offical [nginx](https://registry.hub.docker.com/_/nginx/) image will provide one. If you still want A+ security in a separate container setup, you should mount an RFC7919 DH key file to the nginx container at `/etc/nginx/dhparam/dhparam.pem`. Set `DHPARAM_SKIP` environment variable to `true` to disable using default Diffie-Hellman parameters. The default value is `false`. @@ -362,6 +406,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; +proxy_set_header X-Forwarded-Path $request_uri; # Mitigate httpoxy attack (see README for details) proxy_set_header Proxy ""; @@ -448,10 +493,10 @@ Please note that using regular expressions in `VIRTUAL_HOST` will always result ### Troubleshooting -In case you can't access your VIRTUAL_HOST, set `DEBUG=true` in the client container's environment and have a look at the generated nginx configuration file `/etc/nginx/conf.d/default`: +In case you can't access your VIRTUAL_HOST, set `DEBUG=true` in the client container's environment and have a look at the generated nginx configuration file `/etc/nginx/conf.d/default.conf`: ```console -docker exec cat /etc/nginx/conf.d/default +docker exec cat /etc/nginx/conf.d/default.conf ``` Especially at `upstream` definition blocks which should look like: diff --git a/Procfile b/app/Procfile similarity index 100% rename from Procfile rename to app/Procfile diff --git a/dhparam/ffdhe2048.pem b/app/dhparam/ffdhe2048.pem similarity index 100% rename from dhparam/ffdhe2048.pem rename to app/dhparam/ffdhe2048.pem diff --git a/dhparam/ffdhe3072.pem b/app/dhparam/ffdhe3072.pem similarity index 100% rename from dhparam/ffdhe3072.pem rename to app/dhparam/ffdhe3072.pem diff --git a/dhparam/ffdhe4096.pem b/app/dhparam/ffdhe4096.pem similarity index 100% rename from dhparam/ffdhe4096.pem rename to app/dhparam/ffdhe4096.pem diff --git a/docker-entrypoint.sh b/app/docker-entrypoint.sh similarity index 94% rename from docker-entrypoint.sh rename to app/docker-entrypoint.sh index 45d6cd2..8f4ed7a 100755 --- a/docker-entrypoint.sh +++ b/app/docker-entrypoint.sh @@ -29,6 +29,12 @@ function _parse_false() { esac } +function _print_version { + if [[ -n "${NGINX_PROXY_VERSION:-}" ]]; then + echo "Info: running nginx-proxy version ${NGINX_PROXY_VERSION}" + fi +} + function _check_unix_socket() { # Warn if the DOCKER_HOST socket does not exist if [[ ${DOCKER_HOST} == unix://* ]]; then @@ -96,6 +102,8 @@ function _setup_dhparam() { # Run the init logic if the default CMD was provided if [[ $* == 'forego start -r' ]]; then + _print_version + _check_unix_socket _resolvers diff --git a/docker-compose-separate-containers.yml b/docker-compose-separate-containers.yml index a4edb94..254b742 100644 --- a/docker-compose-separate-containers.yml +++ b/docker-compose-separate-containers.yml @@ -9,8 +9,9 @@ services: - /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 + image: nginxproxy/docker-gen + command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl + /etc/nginx/conf.d/default.conf volumes_from: - nginx volumes: diff --git a/network_internal.conf b/network_internal.conf index cdf3c9c..bacceb1 100644 --- a/network_internal.conf +++ b/network_internal.conf @@ -3,4 +3,5 @@ allow 127.0.0.0/8; allow 10.0.0.0/8; allow 192.168.0.0/16; allow 172.16.0.0/12; +allow fc00::/7; # IPv6 local address range deny all; diff --git a/nginx.tmpl b/nginx.tmpl index 5118d4c..0e4d908 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -1,9 +1,11 @@ {{ $CurrentContainer := where $ "ID" .Docker.CurrentContainerID | first }} +{{ $nginx_proxy_version := coalesce $.Env.NGINX_PROXY_VERSION "" }} {{ $external_http_port := coalesce $.Env.HTTP_PORT "80" }} {{ $external_https_port := coalesce $.Env.HTTPS_PORT "443" }} {{ $debug_all := $.Env.DEBUG }} {{ $sha1_upstream_name := parseBool (coalesce $.Env.SHA1_UPSTREAM_NAME "false") }} +{{ $default_root_response := coalesce $.Env.DEFAULT_ROOT "404" }} {{ define "ssl_policy" }} {{ if eq .ssl_policy "Mozilla-Modern" }} @@ -48,6 +50,103 @@ {{ end }} {{ end }} +{{ define "location" }} + location {{ .Path }} { + {{ if eq .NetworkTag "internal" }} + # Only allow traffic from internal clients + include /etc/nginx/network_internal.conf; + {{ end }} + + {{ if eq .Proto "uwsgi" }} + include uwsgi_params; + uwsgi_pass {{ trim .Proto }}://{{ trim .Upstream }}; + {{ else if eq .Proto "fastcgi" }} + root {{ trim .VhostRoot }}; + include fastcgi_params; + fastcgi_pass {{ trim .Upstream }}; + {{ else if eq .Proto "grpc" }} + grpc_pass {{ trim .Proto }}://{{ trim .Upstream }}; + {{ else }} + proxy_pass {{ trim .Proto }}://{{ trim .Upstream }}{{ trim .Dest }}; + {{ end }} + + {{ if (exists (printf "/etc/nginx/htpasswd/%s" .Host)) }} + auth_basic "Restricted {{ .Host }}"; + auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" .Host) }}; + {{ end }} + + {{ if (exists (printf "/etc/nginx/vhost.d/%s_%s_location" .Host (sha1 .Path) )) }} + include {{ printf "/etc/nginx/vhost.d/%s_%s_location" .Host (sha1 .Path) }}; + {{ else if (exists (printf "/etc/nginx/vhost.d/%s_location" .Host)) }} + include {{ printf "/etc/nginx/vhost.d/%s_location" .Host}}; + {{ else if (exists "/etc/nginx/vhost.d/default_location") }} + include /etc/nginx/vhost.d/default_location; + {{ end }} +} +{{ end }} + +{{ define "upstream" }} + {{ $networks := .Networks }} + {{ $debug_all := .Debug }} +upstream {{ .Upstream }} { + {{ $server_found := "false" }} + {{ range $container := .Containers }} + {{ $debug := (eq (coalesce $container.Env.DEBUG $debug_all "false") "true") }} + {{/* If only 1 port exposed, use that as a default, else 80 */}} + {{ $defaultPort := (when (eq (len $container.Addresses) 1) (first $container.Addresses) (dict "Port" "80")).Port }} + {{ $port := (coalesce $container.Env.VIRTUAL_PORT $defaultPort) }} + {{ $address := where $container.Addresses "Port" $port | first }} + {{ if $debug }} + # Exposed ports: {{ $container.Addresses }} + # Default virtual port: {{ $defaultPort }} + # VIRTUAL_PORT: {{ $container.Env.VIRTUAL_PORT }} + {{ if not $address }} + # /!\ Virtual port not exposed + {{ end }} + {{ end }} + {{ range $knownNetwork := $networks }} + {{ range $containerNetwork := $container.Networks }} + {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} + ## Can be connected with "{{ $containerNetwork.Name }}" network + {{ if $address }} + {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} + {{ if and $container.Node.ID $address.HostPort }} + {{ $server_found = "true" }} + # {{ $container.Node.Name }}/{{ $container.Name }} + server {{ $container.Node.Address.IP }}:{{ $address.HostPort }}; + {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} + {{ else if $containerNetwork }} + {{ $server_found = "true" }} + # {{ $container.Name }} + server {{ $containerNetwork.IP }}:{{ $address.Port }}; + {{ end }} + {{ else if $containerNetwork }} + # {{ $container.Name }} + {{ if $containerNetwork.IP }} + {{ $server_found = "true" }} + server {{ $containerNetwork.IP }}:{{ $port }}; + {{ else }} + # /!\ No IP for this network! + {{ end }} + {{ end }} + {{ else }} + # Cannot connect to network '{{ $containerNetwork.Name }}' of this container + {{ end }} + {{ end }} + {{ end }} + {{ end }} + {{/* nginx-proxy/nginx-proxy#1105 */}} + {{ if (eq $server_found "false") }} + # Fallback entry + server 127.0.0.1 down; + {{ end }} +} +{{ end }} + +{{ if ne $nginx_proxy_version "" }} +# nginx-proxy version : {{ $nginx_proxy_version }} +{{ end }} + # If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the # scheme used to connect to this server map $http_x_forwarded_proto $proxy_x_forwarded_proto { @@ -95,6 +194,7 @@ access_log off; {{/* Get the SSL_POLICY defined by this container, falling back to "Mozilla-Intermediate" */}} {{ $ssl_policy := or ($.Env.SSL_POLICY) "Mozilla-Intermediate" }} {{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }} +error_log /dev/stderr; {{ if $.Env.RESOLVERS }} resolver {{ $.Env.RESOLVERS }}; @@ -114,6 +214,7 @@ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; +proxy_set_header X-Original-URI $request_uri; # Mitigate httpoxy attack (see README for details) proxy_set_header Proxy ""; @@ -157,73 +258,27 @@ server { {{ $is_regexp := hasPrefix "~" $host }} {{ $upstream_name := when (or $is_regexp $sha1_upstream_name) (sha1 $host) $host }} -# {{ $host }} -upstream {{ $upstream_name }} { +{{ $paths := groupBy $containers "Env.VIRTUAL_PATH" }} +{{ $nPaths := len $paths }} -{{ $server_found := "false" }} -{{ range $container := $containers }} - {{ $debug := (eq (coalesce $container.Env.DEBUG $debug_all "false") "true") }} - {{/* If only 1 port exposed, use that as a default, else 80 */}} - {{ $defaultPort := (when (eq (len $container.Addresses) 1) (first $container.Addresses) (dict "Port" "80")).Port }} - {{ $port := (coalesce $container.Env.VIRTUAL_PORT $defaultPort) }} - {{ $address := where $container.Addresses "Port" $port | first }} - {{ if $debug }} - # Exposed ports: {{ $container.Addresses }} - # Default virtual port: {{ $defaultPort }} - # VIRTUAL_PORT: {{ $container.Env.VIRTUAL_PORT }} - {{ if not $address }} - # /!\ Virtual port not exposed - {{ end }} - {{ end }} - {{ range $knownNetwork := $CurrentContainer.Networks }} - {{ range $containerNetwork := $container.Networks }} - {{ if (and (ne $containerNetwork.Name "ingress") (or (eq $knownNetwork.Name $containerNetwork.Name) (eq $knownNetwork.Name "host"))) }} - ## Can be connected with "{{ $containerNetwork.Name }}" network - {{ if $address }} - {{/* If we got the containers from swarm and this container's port is published to host, use host IP:PORT */}} - {{ if and $container.Node.ID $address.HostPort }} - {{ $server_found = "true" }} - # {{ $container.Node.Name }}/{{ $container.Name }} - server {{ $container.Node.Address.IP }}:{{ $address.HostPort }}; - {{/* If there is no swarm node or the port is not published on host, use container's IP:PORT */}} - {{ else if $containerNetwork }} - {{ $server_found = "true" }} - # {{ $container.Name }} - server {{ $containerNetwork.IP }}:{{ $address.Port }}; - {{ end }} - {{ else if $containerNetwork }} - # {{ $container.Name }} - {{ if $containerNetwork.IP }} - {{ $server_found = "true" }} - server {{ $containerNetwork.IP }}:{{ $port }}; - {{ else }} - # /!\ No IP for this network! - {{ end }} - {{ end }} - {{ else }} - # Cannot connect to network '{{ $containerNetwork.Name }}' of this container - {{ end }} - {{ end }} +{{ if eq $nPaths 0 }} + # {{ $host }} + {{ template "upstream" (dict "Upstream" $upstream_name "Containers" $containers "Networks" $CurrentContainer.Networks "Debug" $debug_all) }} +{{ else }} + {{ range $path, $containers := $paths }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $upstream_name $sum }} + # {{ $host }}{{ $path }} + {{ template "upstream" (dict "Upstream" $upstream "Containers" $containers "Networks" $CurrentContainer.Networks "Debug" $debug_all) }} {{ end }} {{ end }} -{{/* nginx-proxy/nginx-proxy#1105 */}} -{{ if (eq $server_found "false") }} - # Fallback entry - server 127.0.0.1 down; -{{ end }} -} {{ $default_host := or ($.Env.DEFAULT_HOST) "" }} {{ $default_server := index (dict $host "" $default_host "default_server") $host }} -{{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} -{{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} - {{/* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "" */}} {{ $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} -{{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} -{{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }} {{/* Get the HTTPS_METHOD defined by containers w/ the same vhost, falling back to "redirect" */}} {{ $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) (or $.Env.HTTPS_METHOD "redirect") }} @@ -298,11 +353,6 @@ server { {{ end }} {{ $access_log }} - {{ if eq $network_tag "internal" }} - # Only allow traffic from internal clients - include /etc/nginx/network_internal.conf; - {{ end }} - {{ template "ssl_policy" (dict "ssl_policy" $ssl_policy) }} ssl_session_timeout 5m; @@ -332,30 +382,31 @@ server { include /etc/nginx/vhost.d/default; {{ end }} - location / { - {{ if eq $proto "uwsgi" }} - include uwsgi_params; - uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else if eq $proto "fastcgi" }} - root {{ trim $vhost_root }}; - include fastcgi_params; - fastcgi_pass {{ trim $upstream_name }}; - {{ else if eq $proto "grpc" }} - grpc_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else }} - proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ end }} + {{ if eq $nPaths 0 }} + {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} + {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} - {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} - auth_basic "Restricted {{ $host }}"; - auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; + {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} + {{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }} + {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $upstream_name "Host" $host "VhostRoot" $vhost_root "Dest" "" "NetworkTag" $network_tag) }} + {{ else }} + {{ range $path, $container := $paths }} + {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}} + {{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }} + + {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} + {{ $network_tag := or (first (groupByKeys $container "Env.NETWORK_ACCESS")) "external" }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $upstream_name $sum }} + {{ $dest := (or (first (groupByKeys $container "Env.VIRTUAL_DEST")) "") }} + {{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag) }} {{ end }} - {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} - include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; - {{ else if (exists "/etc/nginx/vhost.d/default_location") }} - include /etc/nginx/vhost.d/default_location; + {{ if (not (contains $paths "/")) }} + location / { + return {{ $default_root_response }}; + } {{ end }} - } + {{ end }} } {{ end }} @@ -369,44 +420,41 @@ server { {{ end }} listen {{ $external_http_port }} {{ $default_server }}; {{ if $enable_ipv6 }} - listen [::]:80 {{ $default_server }}; + listen [::]:{{ $external_http_port }} {{ $default_server }}; {{ end }} {{ $access_log }} - {{ if eq $network_tag "internal" }} - # Only allow traffic from internal clients - include /etc/nginx/network_internal.conf; - {{ end }} - {{ if (exists (printf "/etc/nginx/vhost.d/%s" $host)) }} include {{ printf "/etc/nginx/vhost.d/%s" $host }}; {{ else if (exists "/etc/nginx/vhost.d/default") }} include /etc/nginx/vhost.d/default; {{ end }} - location / { - {{ if eq $proto "uwsgi" }} - include uwsgi_params; - uwsgi_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else if eq $proto "fastcgi" }} - root {{ trim $vhost_root }}; - include fastcgi_params; - fastcgi_pass {{ trim $upstream_name }}; - {{ else if eq $proto "grpc" }} - grpc_pass {{ trim $proto }}://{{ trim $upstream_name }}; - {{ else }} - proxy_pass {{ trim $proto }}://{{ trim $upstream_name }}; + {{ if eq $nPaths 0 }} + {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost, falling back to "http" */}} + {{ $proto := trim (or (first (groupByKeys $containers "Env.VIRTUAL_PROTO")) "http") }} + + {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} + {{ $network_tag := or (first (groupByKeys $containers "Env.NETWORK_ACCESS")) "external" }} + {{ template "location" (dict "Path" "/" "Proto" $proto "Upstream" $upstream_name "Host" $host "VhostRoot" $vhost_root "Dest" "" "NetworkTag" $network_tag) }} + {{ else }} + {{ range $path, $container := $paths }} + {{/* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http" */}} + {{ $proto := trim (or (first (groupByKeys $container "Env.VIRTUAL_PROTO")) "http") }} + + {{/* Get the NETWORK_ACCESS defined by containers w/ the same vhost, falling back to "external" */}} + {{ $network_tag := or (first (groupByKeys $container "Env.NETWORK_ACCESS")) "external" }} + {{ $sum := sha1 $path }} + {{ $upstream := printf "%s-%s" $upstream_name $sum }} + {{ $dest := (or (first (groupByKeys $container "Env.VIRTUAL_DEST")) "") }} + {{ template "location" (dict "Path" $path "Proto" $proto "Upstream" $upstream "Host" $host "VhostRoot" $vhost_root "Dest" $dest "NetworkTag" $network_tag) }} {{ end }} - {{ if (exists (printf "/etc/nginx/htpasswd/%s" $host)) }} - auth_basic "Restricted {{ $host }}"; - auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" $host) }}; + {{ if (not (contains $paths "/")) }} + location / { + return {{ $default_root_response }}; + } {{ end }} - {{ if (exists (printf "/etc/nginx/vhost.d/%s_location" $host)) }} - include {{ printf "/etc/nginx/vhost.d/%s_location" $host}}; - {{ else if (exists "/etc/nginx/vhost.d/default_location") }} - include /etc/nginx/vhost.d/default_location; - {{ end }} - } + {{ end }} } {{ if (and (not $is_https) (exists "/etc/nginx/certs/default.crt") (exists "/etc/nginx/certs/default.key")) }} diff --git a/test/conftest.py b/test/conftest.py index 3e0f3af..1121e96 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -23,17 +23,20 @@ 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") +PYTEST_RUNNING_IN_CONTAINER = os.environ.get('PYTEST_RUNNING_IN_CONTAINER') == "1" FORCE_CONTAINER_IPV6 = False # ugly global state to consider containers' IPv6 address instead of IPv4 docker_client = docker.from_env() +# Name of pytest container to reference if it's being used for running tests +test_container = 'nginx-proxy-pytest' + ############################################################################### -# +# # utilities -# +# ############################################################################### @contextlib.contextmanager @@ -57,7 +60,7 @@ def ipv6(force_ipv6=True): class requests_for_docker(object): """ - Proxy for calling methods of the requests module. + 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. """ @@ -189,6 +192,10 @@ def nginx_proxy_dns_resolver(domain_name): nginxproxy_containers = docker_client.containers.list(filters={"status": "running", "ancestor": "nginxproxy/nginx-proxy:test"}) if len(nginxproxy_containers) == 0: log.warn(f"no container found from image nginxproxy/nginx-proxy:test while resolving {domain_name!r}") + exited_nginxproxy_containers = docker_client.containers.list(filters={"status": "exited", "ancestor": "nginxproxy/nginx-proxy:test"}) + if len(exited_nginxproxy_containers) > 0: + exited_nginxproxy_container_logs = exited_nginxproxy_containers[0].logs() + log.warn(f"nginxproxy/nginx-proxy:test container might have exited unexpectedly. Container logs: " + "\n" + exited_nginxproxy_container_logs.decode()) return nginxproxy_container = nginxproxy_containers[0] ip = container_ip(nginxproxy_container) @@ -221,7 +228,7 @@ def docker_container_dns_resolver(domain_name): ip = container_ip(container) log.info(f"resolving domain name {domain_name!r} as IP address {ip} of container {container.name}") - return ip + return ip def monkey_patch_urllib_dns_resolver(): @@ -236,6 +243,11 @@ def monkey_patch_urllib_dns_resolver(): logging.getLogger('DNS').debug(f"resolving domain name {repr(args)}") _args = list(args) + # Fail early when querying IP directly and it is forced ipv6 when not supported, + # Otherwise a pytest container not using the host network fails to pass `test_raw-ip-vhost`. + if FORCE_CONTAINER_IPV6 and not HAS_IPV6: + pytest.skip("This system does not support IPv6") + # custom DNS resolvers ip = nginx_proxy_dns_resolver(args[0]) if ip is None: @@ -259,7 +271,7 @@ def restore_urllib_dns_resolver(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()): + if PYTEST_RUNNING_IN_CONTAINER and container.name == test_container: continue # pytest is running within a Docker container, so we do not want to remove that particular container logging.info(f"removing container {container.name}") container.remove(v=True, force=True) @@ -298,7 +310,7 @@ def docker_compose_down(compose_file='docker-compose.yml'): def wait_for_nginxproxy_to_be_ready(): """ - If one (and only one) container started from image nginxproxy/nginx-proxy:test is found, + If one (and only one) container started from image nginxproxy/nginx-proxy:test is found, wait for its log to contain substring "Watching docker events" """ containers = docker_client.containers.list(filters={"ancestor": "nginxproxy/nginx-proxy:test"}) @@ -349,18 +361,23 @@ def connect_to_network(network): :return: the name of the network we were connected to, or None """ - if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER: + if PYTEST_RUNNING_IN_CONTAINER: try: - my_container = docker_client.containers.get(socket.gethostname()) + my_container = docker_client.containers.get(test_container) except docker.errors.NotFound: - logging.warn(f"container {socket.gethostname()!r} not found") + logging.warn(f"container {test_container} not found") return # figure out our container networks my_networks = list(my_container.attrs["NetworkSettings"]["Networks"].keys()) - # make sure our container is connected to the nginx-proxy's network - if network not in my_networks: + # If the pytest container is using host networking, it cannot connect to container networks (not required with host network) + if 'host' in my_networks: + return None + + # Make sure our container is connected to the nginx-proxy's network, + # but avoid connecting to `none` network (not valid) with `test_server-down` tests + if network.name not in my_networks and network.name != 'none': logging.info(f"Connecting to docker network: {network.name}") network.connect(my_container) return network @@ -372,11 +389,11 @@ def disconnect_from_network(network=None): :param network: name of a docker network to disconnect from """ - if I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER and network is not None: + if PYTEST_RUNNING_IN_CONTAINER and network is not None: try: - my_container = docker_client.containers.get(socket.gethostname()) + my_container = docker_client.containers.get(test_container) except docker.errors.NotFound: - logging.warn(f"container {socket.gethostname()!r} not found") + logging.warn(f"container {test_container} not found") return # figure out our container networks @@ -394,27 +411,27 @@ def connect_to_all_networks(): :return: a list of networks we connected to """ - if not I_AM_RUNNING_INSIDE_A_DOCKER_CONTAINER: + if not PYTEST_RUNNING_IN_CONTAINER: return [] else: # find the list of docker networks - networks = [network for network in docker_client.networks.list() if len(network.containers) > 0 and network.name != 'bridge'] + networks = [network for network in docker_client.networks.list(greedy=True) if len(network.containers) > 0 and network.name != 'bridge'] return [connect_to_network(network) for network in networks] ############################################################################### -# +# # Py.test fixtures -# +# ############################################################################### @pytest.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. """ @@ -450,9 +467,9 @@ def nginxproxy(): ############################################################################### -# +# # Py.test hooks -# +# ############################################################################### # pytest hook to display additionnal stuff in test report @@ -479,9 +496,9 @@ def pytest_runtest_setup(item): pytest.xfail(f"previous test failed ({previousfailed.name})") ############################################################################### -# +# # Check requirements -# +# ############################################################################### try: diff --git a/test/pytest.sh b/test/pytest.sh index 99c054c..28275e5 100755 --- a/test/pytest.sh +++ b/test/pytest.sh @@ -18,8 +18,8 @@ docker build -t nginx-proxy-tester -f "${DIR}/requirements/Dockerfile-nginx-prox # 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 \ ---volume /var/run/docker.sock:/var/run/docker.sock \ ---volume "${DIR}:${DIR}" \ ---workdir "${DIR}" \ -nginx-proxy-tester "${ARGS[@]}" \ No newline at end of file +exec docker run --rm -it --name "nginx-proxy-pytest" \ + --volume "/var/run/docker.sock:/var/run/docker.sock" \ + --volume "${DIR}:${DIR}" \ + --workdir "${DIR}" \ + nginx-proxy-tester "${ARGS[@]}" \ No newline at end of file diff --git a/test/requirements/Dockerfile-nginx-proxy-tester b/test/requirements/Dockerfile-nginx-proxy-tester index 3c25c0c..36984fe 100644 --- a/test/requirements/Dockerfile-nginx-proxy-tester +++ b/test/requirements/Dockerfile-nginx-proxy-tester @@ -1,5 +1,7 @@ FROM python:3.9 +ENV PYTEST_RUNNING_IN_CONTAINER=1 + COPY python-requirements.txt /requirements.txt RUN pip install -r /requirements.txt diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index 1a2ad1e..7aa8731 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,5 +1,5 @@ backoff==1.11.1 docker-compose==1.29.2 docker==5.0.3 -pytest==6.2.5 -requests==2.26.0 +pytest==7.1.2 +requests==2.27.1 diff --git a/test/test_dockergen/test_dockergen_v2.yml b/test/test_dockergen/test_dockergen_v2.yml index 919461d..b1f443c 100644 --- a/test/test_dockergen/test_dockergen_v2.yml +++ b/test/test_dockergen/test_dockergen_v2.yml @@ -8,7 +8,7 @@ services: - /etc/nginx/conf.d dockergen: - image: jwilder/docker-gen + image: nginxproxy/docker-gen command: -notify-sighup nginx -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf volumes_from: - nginx diff --git a/test/test_dockergen/test_dockergen_v3.yml b/test/test_dockergen/test_dockergen_v3.yml index 5bc4bff..8339273 100644 --- a/test/test_dockergen/test_dockergen_v3.yml +++ b/test/test_dockergen/test_dockergen_v3.yml @@ -7,7 +7,7 @@ services: - nginx_conf:/etc/nginx/conf.d dockergen: - image: jwilder/docker-gen + image: nginxproxy/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 diff --git a/test/test_events.py b/test/test_events.py index 201917f..b5da3dd 100644 --- a/test/test_events.py +++ b/test/test_events.py @@ -29,13 +29,36 @@ def web1(docker_compose): except NotFound: pass +@pytest.fixture() +def web2(docker_compose): + """ + pytest fixture creating a web container with `VIRTUAL_HOST=nginx-proxy`, `VIRTUAL_PATH=/web2/` and `VIRTUAL_DEST=/` listening on port 82. + """ + container = docker_compose.containers.run( + name="web2", + image="web", + detach=True, + environment={ + "WEB_PORTS": "82", + "VIRTUAL_HOST": "nginx-proxy", + "VIRTUAL_PATH": "/web2/", + "VIRTUAL_DEST": "/", + }, + ports={"82/tcp": None} + ) + sleep(2) # give it some time to initialize and for docker-gen to detect it + yield container + try: + docker_compose.containers.get("web2").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): +def test_new_container_is_detected_vhost(web1, nginxproxy): r = nginxproxy.get("http://web1.nginx-proxy/port") assert r.status_code == 200 assert "answer from port 81\n" == r.text @@ -44,3 +67,16 @@ def test_new_container_is_detected(web1, nginxproxy): sleep(2) r = nginxproxy.get("http://web1.nginx-proxy/port") assert r.status_code == 503 + +def test_new_container_is_detected_vpath(web2, nginxproxy): + r = nginxproxy.get("http://nginx-proxy/web2/port") + assert r.status_code == 200 + assert "answer from port 82\n" == r.text + r = nginxproxy.get("http://nginx-proxy/port") + assert r.status_code in [404, 503] + + web2.remove(force=True) + sleep(2) + r = nginxproxy.get("http://nginx-proxy/web2/port") + assert r.status_code == 503 + diff --git a/test/test_internal/network_internal.conf b/test/test_internal/network_internal.conf new file mode 100644 index 0000000..496e569 --- /dev/null +++ b/test/test_internal/network_internal.conf @@ -0,0 +1,11 @@ +# 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; +allow fc00::/7; # IPv6 local address range +deny all; + +# Dummy header for testing +add_header X-network internal; + diff --git a/test/test_internal/test_internal-per-vhost.py b/test/test_internal/test_internal-per-vhost.py new file mode 100644 index 0000000..4586cc0 --- /dev/null +++ b/test/test_internal/test_internal-per-vhost.py @@ -0,0 +1,14 @@ +import pytest + +def test_network_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-network" in r.headers + assert "internal" == r.headers["X-network"] + +def test_network_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-network" not in r.headers diff --git a/test/test_internal/test_internal-per-vhost.yml b/test/test_internal/test_internal-per-vhost.yml new file mode 100644 index 0000000..5c732ee --- /dev/null +++ b/test/test_internal/test_internal-per-vhost.yml @@ -0,0 +1,23 @@ +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: web1.nginx-proxy.local + NETWORK_ACCESS: internal + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: 82 + VIRTUAL_HOST: web2.nginx-proxy.local + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./network_internal.conf:/etc/nginx/network_internal.conf:ro + diff --git a/test/test_internal/test_internal-per-vpath.py b/test/test_internal/test_internal-per-vpath.py new file mode 100644 index 0000000..e95fe00 --- /dev/null +++ b/test/test_internal/test_internal-per-vpath.py @@ -0,0 +1,14 @@ +import pytest + +def test_network_web1(docker_compose, nginxproxy): + r = nginxproxy.get("http://nginx-proxy.local/web1/port") + assert r.status_code == 200 + assert r.text == "answer from port 81\n" + assert "X-network" in r.headers + assert "internal" == r.headers["X-network"] + +def test_network_web2(docker_compose, nginxproxy): + r = nginxproxy.get("http://nginx-proxy.local/web2/port") + assert r.status_code == 200 + assert r.text == "answer from port 82\n" + assert "X-network" not in r.headers diff --git a/test/test_internal/test_internal-per-vpath.yml b/test/test_internal/test_internal-per-vpath.yml new file mode 100644 index 0000000..f5bac55 --- /dev/null +++ b/test/test_internal/test_internal-per-vpath.yml @@ -0,0 +1,27 @@ +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: 81 + VIRTUAL_HOST: nginx-proxy.local + VIRTUAL_PATH: /web1/ + VIRTUAL_DEST: / + NETWORK_ACCESS: internal + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: 82 + VIRTUAL_HOST: nginx-proxy.local + VIRTUAL_PATH: /web2/ + VIRTUAL_DEST: / + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./network_internal.conf:/etc/nginx/network_internal.conf:ro + diff --git a/test/test_nominal.py b/test/test_nominal.py index cce7c94..a3f9c87 100644 --- a/test/test_nominal.py +++ b/test/test_nominal.py @@ -22,3 +22,8 @@ def test_forwards_to_web2(docker_compose, nginxproxy): def test_ipv6_is_disabled_by_default(docker_compose, nginxproxy): with pytest.raises(ConnectionError): nginxproxy.get("http://nginx-proxy/port", ipv6=True) + + +def test_container_version_is_displayed(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + assert "# nginx-proxy version : test" in conf diff --git a/test/test_ssl/certs/web2.nginx-proxy.tld.dhparam.pem b/test/test_ssl/certs/web2.nginx-proxy.tld.dhparam.pem new file mode 100644 index 0000000..088f967 --- /dev/null +++ b/test/test_ssl/certs/web2.nginx-proxy.tld.dhparam.pem @@ -0,0 +1,8 @@ +-----BEGIN DH PARAMETERS----- +MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz ++8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a +87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 +YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi +7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD +ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== +-----END DH PARAMETERS----- \ No newline at end of file diff --git a/test/test_ssl/test_dhparam.py b/test/test_ssl/test_dhparam.py index 6de92b2..2f69ad3 100644 --- a/test/test_ssl/test_dhparam.py +++ b/test/test_ssl/test_dhparam.py @@ -1,5 +1,6 @@ import re import subprocess +import os import backoff import docker @@ -80,12 +81,17 @@ def negotiate_cipher(sut_container, additional_params='', grep='Cipher is'): raise Exception("Failed to process CLI request:\n" + e.stderr) from None -def can_negotiate_dhe_ciphersuite(sut_container): - r = negotiate_cipher(sut_container, "-cipher 'EDH'") +# The default `dh_bits` can vary due to configuration. +# `additional_params` allows for adjusting the request to a specific `VIRTUAL_HOST`, +# where DH size can differ from the configured global default DH size. +def can_negotiate_dhe_ciphersuite(sut_container, dh_bits=4096, additional_params=''): + openssl_params = f"-cipher 'EDH' {additional_params}" + + r = negotiate_cipher(sut_container, openssl_params) assert "New, TLSv1.2, Cipher is DHE-RSA-AES256-GCM-SHA384\n" == r - r2 = negotiate_cipher(sut_container, "-cipher 'EDH'", "Server Temp Key") - assert "DH" in r2 + r2 = negotiate_cipher(sut_container, openssl_params, "Server Temp Key") + assert f"Server Temp Key: DH, {dh_bits} bits" in r2 def cannot_negotiate_dhe_ciphersuite(sut_container): @@ -101,6 +107,29 @@ def cannot_negotiate_dhe_ciphersuite(sut_container): assert "X25519" in r3 +# To verify self-signed certificates, the file path to their CA cert must be provided. +# Use the `fqdn` arg to specify the `VIRTUAL_HOST` to request for verification for that cert. +# +# Resolves the following stderr warnings regarding self-signed cert verification and missing SNI: +# `Can't use SSL_get_servername` +# `verify error:num=20:unable to get local issuer certificate` +# `verify error:num=21:unable to verify the first certificate` +# +# The stderr output is hidden due to running the openssl command with `stderr=subprocess.PIPE`. +def can_verify_chain_of_trust(sut_container, ca_cert, fqdn): + openssl_params = f"-CAfile '{ca_cert}' -servername '{fqdn}'" + + r = negotiate_cipher(sut_container, openssl_params, "Verify return code") + assert "Verify return code: 0 (ok)" in r + + +def should_be_equivalent_content(sut_container, expected, actual): + expected_checksum = sut_container.exec_run(f"md5sum {expected}").output.split()[0] + actual_checksum = sut_container.exec_run(f"md5sum {actual}").output.split()[0] + + assert expected_checksum == actual_checksum + + # Parse array of container ENV, splitting at the `=` and returning the value, otherwise `None` def get_env(sut_container, var): env = sut_container.attrs['Config']['Env'] @@ -125,14 +154,17 @@ def test_default_dhparam_is_ffdhe4096(docker_compose): assert_log_contains("Setting up DH Parameters..", container_name) - # Make sure the dhparam file used is the default ffdhe4096.pem: - default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe4096.pem").output.split() - current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() - assert default_checksum[0] == current_checksum[0] + # `dhparam.pem` contents should match the default (ffdhe4096.pem): + should_be_equivalent_content( + sut_container, + "/app/dhparam/ffdhe4096.pem", + "/etc/nginx/dhparam/dhparam.pem" + ) - can_negotiate_dhe_ciphersuite(sut_container) + can_negotiate_dhe_ciphersuite(sut_container, 4096) +# Overrides default DH group via ENV `DHPARAM_BITS=3072`: def test_can_change_dhparam_group(docker_compose): container_name="dh-env" sut_container = docker_client.containers.get(container_name) @@ -140,12 +172,14 @@ def test_can_change_dhparam_group(docker_compose): assert_log_contains("Setting up DH Parameters..", container_name) - # Make sure the dhparam file used is ffdhe2048.pem, not the default (ffdhe4096.pem): - default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe2048.pem").output.split() - current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() - assert default_checksum[0] == current_checksum[0] + # `dhparam.pem` contents should not match the default (ffdhe4096.pem): + should_be_equivalent_content( + sut_container, + "/app/dhparam/ffdhe3072.pem", + "/etc/nginx/dhparam/dhparam.pem" + ) - can_negotiate_dhe_ciphersuite(sut_container) + can_negotiate_dhe_ciphersuite(sut_container, 3072) def test_fail_if_dhparam_group_not_supported(docker_compose): @@ -162,6 +196,7 @@ def test_fail_if_dhparam_group_not_supported(docker_compose): ) +# Overrides default DH group by providing a custom `/etc/nginx/dhparam/dhparam.pem`: def test_custom_dhparam_is_supported(docker_compose): container_name="dh-file" sut_container = docker_client.containers.get(container_name) @@ -172,14 +207,49 @@ def test_custom_dhparam_is_supported(docker_compose): container_name ) - # Make sure the dhparam file used is not the default (ffdhe4096.pem): - default_checksum = sut_container.exec_run("md5sum /app/dhparam/ffdhe4096.pem").output.split() - current_checksum = sut_container.exec_run("md5sum /etc/nginx/dhparam/dhparam.pem").output.split() - assert default_checksum[0] != current_checksum[0] + # `dhparam.pem` contents should not match the default (ffdhe4096.pem): + should_be_equivalent_content( + sut_container, + "/app/dhparam/ffdhe3072.pem", + "/etc/nginx/dhparam/dhparam.pem" + ) - can_negotiate_dhe_ciphersuite(sut_container) + can_negotiate_dhe_ciphersuite(sut_container, 3072) +# Only `web2` has a site-specific DH param file (which overrides all other DH config) +# Other tests here use `web5` explicitly, or implicitly (via ENV `DEFAULT_HOST`, otherwise first HTTPS server) +def test_custom_dhparam_is_supported_per_site(docker_compose): + container_name="dh-file" + sut_container = docker_client.containers.get(container_name) + assert sut_container.status == "running" + + # A site specific `dhparam.pem` with DH group size of 2048-bit. + # DH group size should not match the: + # - 4096-bit default. + # - 3072-bit default, overriden by file. + should_be_equivalent_content( + sut_container, + "/app/dhparam/ffdhe2048.pem", + "/etc/nginx/certs/web2.nginx-proxy.tld.dhparam.pem" + ) + + # `-servername` required for nginx-proxy to respond with site-specific DH params used: + can_negotiate_dhe_ciphersuite(sut_container, 2048, '-servername web2.nginx-proxy.tld') + + # --Unrelated to DH support-- + # - `web5` is missing a certificate, but falls back to available `/etc/nginx/certs/nginx-proxy.tld.crt` via `nginx.tmpl` "closest" result. + # - `web2` has it's own cert provisioned at `/etc/nginx/certs/web2.nginx-proxy.tld.crt`. + can_verify_chain_of_trust( + sut_container, + ca_cert = f"{os.getcwd()}/certs/ca-root.crt", + fqdn = 'web2.nginx-proxy.tld' + ) + + +# NOTE: These two tests will fail without the ENV `DEFAULT_HOST` to prevent +# accidentally falling back to `web2` as the default server, which has explicit DH params configured. +# Only copying DH params is skipped, not explicit usage via user providing custom files. def test_can_skip_dhparam(docker_compose): container_name="dh-skip" sut_container = docker_client.containers.get(container_name) @@ -189,6 +259,7 @@ def test_can_skip_dhparam(docker_compose): cannot_negotiate_dhe_ciphersuite(sut_container) + def test_can_skip_dhparam_backward_compatibility(docker_compose): container_name="dh-skip-backward" sut_container = docker_client.containers.get(container_name) diff --git a/test/test_ssl/test_dhparam.yml b/test/test_ssl/test_dhparam.yml index d49afc9..505ac4c 100644 --- a/test/test_ssl/test_dhparam.yml +++ b/test/test_ssl/test_dhparam.yml @@ -6,12 +6,27 @@ web5: WEB_PORTS: "85" VIRTUAL_HOST: "web5.nginx-proxy.tld" +# Intended for testing with `dh-file` container. +# VIRTUAL_HOST is paired with site-specific DH param file. +# DEFAULT_HOST is required to avoid defaulting to web2, +# if not specifying FQDN (`-servername`) in openssl queries. +web2: + image: web + expose: + - "85" + environment: + WEB_PORTS: "85" + VIRTUAL_HOST: "web2.nginx-proxy.tld" + + # sut - System Under Test # `docker.sock` required for functionality # `certs` required to enable HTTPS via template with_default_group: container_name: dh-default image: &img-nginxproxy nginxproxy/nginx-proxy:test + environment: &env-common + - &default-host DEFAULT_HOST=web5.nginx-proxy.tld volumes: &vols-common - &docker-sock /var/run/docker.sock:/tmp/docker.sock:ro - &nginx-certs ./certs:/etc/nginx/certs:ro @@ -19,7 +34,8 @@ with_default_group: with_alternative_group: container_name: dh-env environment: - - DHPARAM_BITS=2048 + - DHPARAM_BITS=3072 + - *default-host image: *img-nginxproxy volumes: *vols-common @@ -27,21 +43,24 @@ with_invalid_group: container_name: invalid-group-1024 environment: - DHPARAM_BITS=1024 + - *default-host image: *img-nginxproxy volumes: *vols-common with_custom_file: container_name: dh-file image: *img-nginxproxy - volumes: + environment: *env-common + volumes: - *docker-sock - *nginx-certs - - ../../dhparam/ffdhe3072.pem:/etc/nginx/dhparam/dhparam.pem:ro + - ../../app/dhparam/ffdhe3072.pem:/etc/nginx/dhparam/dhparam.pem:ro with_skip: container_name: dh-skip environment: - DHPARAM_SKIP=true + - *default-host image: *img-nginxproxy volumes: *vols-common @@ -49,5 +68,6 @@ with_skip_backward: container_name: dh-skip-backward environment: - DHPARAM_GENERATION=false + - *default-host image: *img-nginxproxy - volumes: *vols-common \ No newline at end of file + volumes: *vols-common diff --git a/test/test_ssl/test_virtual_path.py b/test/test_ssl/test_virtual_path.py new file mode 100644 index 0000000..508653f --- /dev/null +++ b/test/test_ssl/test_virtual_path.py @@ -0,0 +1,15 @@ +import pytest + +@pytest.mark.parametrize("path", ["web1", "web2"]) +def test_web1_http_redirects_to_https(docker_compose, nginxproxy, path): + r = nginxproxy.get("http://www.nginx-proxy.tld/%s/port" % path, allow_redirects=False) + assert r.status_code == 301 + assert "Location" in r.headers + assert "https://www.nginx-proxy.tld/%s/port" % path == r.headers['Location'] + +@pytest.mark.parametrize("path,port", [("web1", 81), ("web2", 82)]) +def test_web1_https_is_forwarded(docker_compose, nginxproxy, path, port): + r = nginxproxy.get("https://www.nginx-proxy.tld/%s/port" % path, allow_redirects=False) + assert r.status_code == 200 + assert "answer from port %d\n" % port in r.text + diff --git a/test/test_ssl/test_virtual_path.yml b/test/test_ssl/test_virtual_path.yml new file mode 100644 index 0000000..2260321 --- /dev/null +++ b/test/test_ssl/test_virtual_path.yml @@ -0,0 +1,26 @@ +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "www.nginx-proxy.tld" + VIRTUAL_PATH: "/web1/" + VIRTUAL_DEST: "/" + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: "www.nginx-proxy.tld" + VIRTUAL_PATH: "/web2/" + VIRTUAL_DEST: "/" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + diff --git a/test/test_virtual-path/alternate.conf b/test/test_virtual-path/alternate.conf new file mode 100644 index 0000000..541332e --- /dev/null +++ b/test/test_virtual-path/alternate.conf @@ -0,0 +1 @@ +rewrite ^/(web3|alt)/(.*) /$2 break; diff --git a/test/test_virtual-path/bar.conf b/test/test_virtual-path/bar.conf new file mode 100644 index 0000000..e8b0827 --- /dev/null +++ b/test/test_virtual-path/bar.conf @@ -0,0 +1 @@ +add_header X-test bar; diff --git a/test/test_virtual-path/default.conf b/test/test_virtual-path/default.conf new file mode 100644 index 0000000..087e66c --- /dev/null +++ b/test/test_virtual-path/default.conf @@ -0,0 +1 @@ +add_header X-test-default true; diff --git a/test/test_virtual-path/foo.conf b/test/test_virtual-path/foo.conf new file mode 100644 index 0000000..8d8502d --- /dev/null +++ b/test/test_virtual-path/foo.conf @@ -0,0 +1 @@ +add_header X-test f00; \ No newline at end of file diff --git a/test/test_virtual-path/host.conf b/test/test_virtual-path/host.conf new file mode 100644 index 0000000..fe05265 --- /dev/null +++ b/test/test_virtual-path/host.conf @@ -0,0 +1 @@ +add_header X-test-host true; diff --git a/test/test_virtual-path/path.conf b/test/test_virtual-path/path.conf new file mode 100644 index 0000000..6c23b9a --- /dev/null +++ b/test/test_virtual-path/path.conf @@ -0,0 +1 @@ +add_header X-test-path true; diff --git a/test/test_virtual-path/test_custom_conf.py b/test/test_virtual-path/test_custom_conf.py new file mode 100644 index 0000000..eec149f --- /dev/null +++ b/test/test_virtual-path/test_custom_conf.py @@ -0,0 +1,38 @@ +import pytest + +def test_default_root_response(docker_compose, nginxproxy): + r = nginxproxy.get("http://nginx-proxy.test/") + assert r.status_code == 418 + +@pytest.mark.parametrize("stub,header", [ + ("nginx-proxy.test/web1", "bar"), + ("foo.nginx-proxy.test", "f00"), +]) +def test_custom_applies(docker_compose, nginxproxy, stub, header): + r = nginxproxy.get(f"http://{stub}/port") + assert r.status_code == 200 + assert "X-test" in r.headers + assert header == r.headers["X-test"] + +@pytest.mark.parametrize("stub,code", [ + ("nginx-proxy.test/foo", 418), + ("nginx-proxy.test/web2", 200), + ("nginx-proxy.test/web3", 200), + ("bar.nginx-proxy.test", 503), +]) +def test_custom_does_not_apply(docker_compose, nginxproxy, stub, code): + r = nginxproxy.get(f"http://{stub}/port") + assert r.status_code == code + assert "X-test" not in r.headers + +@pytest.mark.parametrize("stub,port", [ + ("nginx-proxy.test/web1", 81), + ("nginx-proxy.test/web2", 82), + ("nginx-proxy.test/web3", 83), + ("nginx-proxy.test/alt", 83), +]) +def test_alternate(docker_compose, nginxproxy, stub, port): + r = nginxproxy.get(f"http://{stub}/port") + assert r.status_code == 200 + assert r.text == f"answer from port {port}\n" + diff --git a/test/test_virtual-path/test_custom_conf.yml b/test/test_virtual-path/test_custom_conf.yml new file mode 100644 index 0000000..40ab512 --- /dev/null +++ b/test/test_virtual-path/test_custom_conf.yml @@ -0,0 +1,48 @@ + +foo: + image: web + expose: + - "42" + environment: + WEB_PORTS: "42" + VIRTUAL_HOST: "foo.nginx-proxy.test" + +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "/web1/" + VIRTUAL_DEST: "/" + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "/web2/" + VIRTUAL_DEST: "/" + +web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "~ ^/(web3|alt)/" + +sut: + image: nginxproxy/nginx-proxy:test + environment: + DEFAULT_ROOT: 418 + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./foo.conf:/etc/nginx/vhost.d/foo.nginx-proxy.test:ro + - ./bar.conf:/etc/nginx/vhost.d/nginx-proxy.test_918d687a929083edd0c7224ee2293e0e7c062ab4_location:ro + - ./alternate.conf:/etc/nginx/vhost.d/nginx-proxy.test_7fb22b74bbdf907425dbbad18e4462565cada230_location:ro + diff --git a/test/test_virtual-path/test_forwarding.py b/test/test_virtual-path/test_forwarding.py new file mode 100644 index 0000000..062dd6c --- /dev/null +++ b/test/test_virtual-path/test_forwarding.py @@ -0,0 +1,18 @@ +import pytest + +def test_root_redirects_to_web1(docker_compose, nginxproxy): + r = nginxproxy.get("http://www.nginx-proxy.tld/port", allow_redirects=False) + assert r.status_code == 301 + assert "Location" in r.headers + assert "http://www.nginx-proxy.tld/web1/port" == r.headers['Location'] + +def test_direct_access(docker_compose, nginxproxy): + r = nginxproxy.get("http://www.nginx-proxy.tld/web1/port", allow_redirects=False) + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + +def test_root_is_forwarded(docker_compose, nginxproxy): + r = nginxproxy.get("http://www.nginx-proxy.tld/port", allow_redirects=True) + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + diff --git a/test/test_virtual-path/test_forwarding.yml b/test/test_virtual-path/test_forwarding.yml new file mode 100644 index 0000000..ee87e8d --- /dev/null +++ b/test/test_virtual-path/test_forwarding.yml @@ -0,0 +1,17 @@ +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "www.nginx-proxy.tld" + VIRTUAL_PATH: "/web1/" + VIRTUAL_DEST: "/" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + environment: + - DEFAULT_ROOT=301 http://$$host/web1$$request_uri diff --git a/test/test_virtual-path/test_location_precedence.py b/test/test_virtual-path/test_location_precedence.py new file mode 100644 index 0000000..415c6c1 --- /dev/null +++ b/test/test_virtual-path/test_location_precedence.py @@ -0,0 +1,32 @@ +import pytest + +def test_location_precedence_case1(docker_compose, nginxproxy): + r = nginxproxy.get(f"http://foo.nginx-proxy.test/web1/port") + assert r.status_code == 200 + + assert "X-test-default" in r.headers + assert "X-test-host" not in r.headers + assert "X-test-path" not in r.headers + + assert r.headers["X-test-default"] == "true" + +def test_location_precedence_case2(docker_compose, nginxproxy): + r = nginxproxy.get(f"http://bar.nginx-proxy.test/web2/port") + assert r.status_code == 200 + + assert "X-test-default" not in r.headers + assert "X-test-host" in r.headers + assert "X-test-path" not in r.headers + + assert r.headers["X-test-host"] == "true" + +def test_location_precedence_case3(docker_compose, nginxproxy): + r = nginxproxy.get(f"http://bar.nginx-proxy.test/web3/port") + assert r.status_code == 200 + + assert "X-test-default" not in r.headers + assert "X-test-host" not in r.headers + assert "X-test-path" in r.headers + + assert r.headers["X-test-path"] == "true" + diff --git a/test/test_virtual-path/test_location_precedence.yml b/test/test_virtual-path/test_location_precedence.yml new file mode 100644 index 0000000..be3248c --- /dev/null +++ b/test/test_virtual-path/test_location_precedence.yml @@ -0,0 +1,37 @@ +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "foo.nginx-proxy.test" + VIRTUAL_PATH: "/web1/" + VIRTUAL_DEST: "/" + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: "bar.nginx-proxy.test" + VIRTUAL_PATH: "/web2/" + VIRTUAL_DEST: "/" + +web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "bar.nginx-proxy.test" + VIRTUAL_PATH: "/web3/" + VIRTUAL_DEST: "/" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./default.conf:/etc/nginx/vhost.d/default_location:ro + - ./host.conf:/etc/nginx/vhost.d/bar.nginx-proxy.test_location:ro + - ./path.conf:/etc/nginx/vhost.d/bar.nginx-proxy.test_99f2db0ed8aa95dbb5b87fca79c7eff2ff6bb8bd_location:ro diff --git a/test/test_virtual-path/test_virtual_paths.py b/test/test_virtual-path/test_virtual_paths.py new file mode 100644 index 0000000..115d47f --- /dev/null +++ b/test/test_virtual-path/test_virtual_paths.py @@ -0,0 +1,59 @@ +from time import sleep + +import pytest +from docker.errors import NotFound + +@pytest.mark.parametrize("stub,expected_port", [ + ("nginx-proxy.test/web1", 81), + ("nginx-proxy.test/web2", 82), + ("nginx-proxy.test", 83), + ("foo.nginx-proxy.test", 42), +]) +def test_valid_path(docker_compose, nginxproxy, stub, expected_port): + r = nginxproxy.get(f"http://{stub}/port") + assert r.status_code == 200 + assert r.text == f"answer from port {expected_port}\n" + +@pytest.mark.parametrize("stub", [ + "nginx-proxy.test/foo", + "bar.nginx-proxy.test", +]) +def test_invalid_path(docker_compose, nginxproxy, stub): + r = nginxproxy.get(f"http://{stub}/port") + assert r.status_code in [404, 503] + +@pytest.fixture() +def web4(docker_compose): + """ + pytest fixture creating a web container with `VIRTUAL_HOST=nginx-proxy.test`, `VIRTUAL_PATH=/web4/` and `VIRTUAL_DEST=/` listening on port 84. + """ + container = docker_compose.containers.run( + name="web4", + image="web", + detach=True, + environment={ + "WEB_PORTS": "84", + "VIRTUAL_HOST": "nginx-proxy.test", + "VIRTUAL_PATH": "/web4/", + "VIRTUAL_DEST": "/", + }, + ports={"84/tcp": None} + ) + sleep(2) # give it some time to initialize and for docker-gen to detect it + yield container + try: + docker_compose.containers.get("web4").remove(force=True) + except NotFound: + pass + +""" +Test if we can add and remove a single virtual_path from multiple ones on the same subdomain. +""" +def test_container_hotplug(web4, nginxproxy): + r = nginxproxy.get(f"http://nginx-proxy.test/web4/port") + assert r.status_code == 200 + assert r.text == f"answer from port 84\n" + web4.remove(force=True) + sleep(2) + r = nginxproxy.get(f"http://nginx-proxy.test/web4/port") + assert r.status_code == 404 diff --git a/test/test_virtual-path/test_virtual_paths.yml b/test/test_virtual-path/test_virtual_paths.yml new file mode 100644 index 0000000..9f6a54f --- /dev/null +++ b/test/test_virtual-path/test_virtual_paths.yml @@ -0,0 +1,42 @@ + +foo: + image: web + expose: + - "42" + environment: + WEB_PORTS: "42" + VIRTUAL_HOST: "foo.nginx-proxy.test" + +web1: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "/web1/" + VIRTUAL_DEST: "/" + +web2: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "/web2/" + VIRTUAL_DEST: "/" + +web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "nginx-proxy.test" + VIRTUAL_PATH: "/" + +sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro