diff --git a/Dockerfile.alpine b/Dockerfile.alpine index d4d5c9d2d..244ecb76c 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,9 +1,9 @@ -FROM nginxproxy/docker-gen:0.12.1 AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.14.0 AS docker-gen -FROM nginxproxy/forego:0.18.1 AS forego +FROM docker.io/nginxproxy/forego:0.18.1 AS forego # Build the final image -FROM nginx:1.26.0-alpine +FROM docker.io/library/nginx:1.27.0-alpine ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because @@ -17,10 +17,13 @@ ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ RUN apk add --no-cache --virtual .run-deps bash openssl # Configure Nginx -RUN sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ +RUN echo -e "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \ + && sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ + && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ - && mkdir -p '/etc/nginx/certs' + && mkdir -p '/etc/nginx/certs' \ + && mkdir -p '/usr/share/nginx/html/errors' # Install Forego + docker-gen COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego diff --git a/Dockerfile.debian b/Dockerfile.debian index c91f5765f..d3f5945ce 100644 --- a/Dockerfile.debian +++ b/Dockerfile.debian @@ -1,9 +1,9 @@ -FROM nginxproxy/docker-gen:0.12.1-debian AS docker-gen +FROM docker.io/nginxproxy/docker-gen:0.14.0-debian AS docker-gen -FROM nginxproxy/forego:0.18.1-debian AS forego +FROM docker.io/nginxproxy/forego:0.18.1-debian AS forego # Build the final image -FROM nginx:1.26.0 +FROM docker.io/library/nginx:1.27.0 ARG NGINX_PROXY_VERSION # Add DOCKER_GEN_VERSION environment variable because @@ -14,10 +14,13 @@ ENV NGINX_PROXY_VERSION=${NGINX_PROXY_VERSION} \ DOCKER_HOST=unix:///tmp/docker.sock # Configure Nginx -RUN sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ +RUN echo "\ninclude /etc/nginx/toplevel.conf.d/*.conf;" >> /etc/nginx/nginx.conf \ + && sed -i 's/worker_connections.*;$/worker_connections 10240;/' /etc/nginx/nginx.conf \ && sed -i -e '/^\}$/{s//\}\nworker_rlimit_nofile 20480;/;:a' -e '$!N;$!ba' -e '}' /etc/nginx/nginx.conf \ + && mkdir -p '/etc/nginx/toplevel.conf.d' \ && mkdir -p '/etc/nginx/dhparam' \ - && mkdir -p '/etc/nginx/certs' + && mkdir -p '/etc/nginx/certs' \ + && mkdir -p '/usr/share/nginx/html/errors' # Install Forego + docker-gen COPY --from=forego /usr/local/bin/forego /usr/local/bin/forego diff --git a/README.md b/README.md index 09e81d09f..d29c15d38 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [](https://github.com/nginx-proxy/nginx-proxy/actions/workflows/test.yml) [](https://github.com/nginx-proxy/nginx-proxy/releases) - + [](https://hub.docker.com/r/nginxproxy/nginx-proxy "Click to view the image on Docker Hub") [](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub") [](https://hub.docker.com/r/nginxproxy/nginx-proxy "DockerHub") diff --git a/docs/README.md b/docs/README.md index 6e29e03ff..6dea7c8e6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ - [HTTP/2 and HTTP/3](#http2-and-http3) - [Headers](#headers) - [Custom Nginx Configuration](#custom-nginx-configuration) +- [TCP and UDP stream](#tcp-and-udp-stream) - [Unhashed vs SHA1 upstream names](#unhashed-vs-sha1-upstream-names) - [Separate Containers](#separate-containers) - [Docker Compose](#docker-compose) @@ -53,6 +54,105 @@ For each host defined into `VIRTUAL_HOST`, the associated virtual port is retrie 1. From the container's exposed port if there is only one 1. From the default port 80 when none of the above methods apply +### Multiple ports + +If your container expose more than one service on different ports and those services need to be proxied, you'll need to use the `VIRTUAL_HOST_MULTIPORTS` environment variable. This variable takes virtual host, path, port and dest definition in YAML (or JSON) form, and completely override the `VIRTUAL_HOST`, `VIRTUAL_PORT`, `VIRTUAL_PATH` and `VIRTUAL_DEST` environment variables on this container. + +The YAML syntax should be easier to write on Docker compose files, while the JSON syntax can be used for CLI invocation. + +The expected format is the following: + +```yaml +hostname: + path: + port: int + dest: string +``` + +For each hostname entry, `path`, `port` and `dest` are optional and are assigned default values when missing: + +- `path` = "/" +- `port` = default port +- `dest` = "" + +The following examples use an hypothetical container running services on port 80, 8000 and 9000: + +#### Multiple ports routed to different hostnames + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: + service1.example.org: + "/": + port: 8000 + service2.example.org: + "/": + port: 9000 + +# There is no path dict specified for www.example.org, so it get the default values: +# www.example.org: +# "/": +# port: 80 (default port) +# dest: "" + +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": {}, +# "service1.example.org": { "/": { "port": 8000, "dest": "" } }, +# "service2.example.org": { "/": { "port": 9000, "dest": "" } } +# } +``` + +This would result in the following proxy config: + +- `www.example.org` -> `multiport-container:80` +- `service1.example.org` -> `multiport-container:8000` +- `service2.example.org` -> `multiport-container:9000` + +#### Multiple ports routed to same hostname and different paths + +```yaml +services: + multiport-container: + image: somerepo/somecontainer + container_name: multiport-container + environment: + VIRTUAL_HOST_MULTIPORTS: |- + www.example.org: + "/": + "/service1": + port: 8000 + dest: "/" + "/service2": + port: 9000 + dest: "/" + +# port and dest are not specified on the / path, so this path is routed +# to the default port with the default dest value (empty string) + +# JSON equivalent: +# VIRTUAL_HOST_MULTIPORTS: |- +# { +# "www.example.org": { +# "/": {}, +# "/service1": { "port": 8000, "dest": "/" }, +# "/service2": { "port": 9000, "dest": "/" } +# } +# } +``` + +This would result in the following proxy config: + +- `www.example.org` -> `multiport-container:80` +- `www.example.org/service1` -> `multiport-container:8000` +- `www.example.org/service2` -> `multiport-container:9000` + ⬆️ [back to table of contents](#table-of-contents) ## Path-based Routing @@ -321,6 +421,12 @@ If you are running the container in a virtualized environment (Hyper-V, VirtualB [acme-companion](https://github.com/nginx-proxy/acme-companion) is a lightweight companion container for the nginx-proxy. It allows the automated creation/renewal of SSL certificates using the ACME protocol. +By default nginx-proxy generates location blocks to handle ACME HTTP Challenge. This behavior can be changed with environment variable `ACME_HTTP_CHALLENGE_LOCATION`. It accepts these values: + +- `true`: default behavior, handle ACME HTTP Challenge in all cases. +- `false`: do not handle ACME HTTP Challenge at all. +- `legacy`: legacy behavior for compatibility with older (<= `2.3`) versions of acme-companion, only handle ACME HTTP challenge when there is a certificate for the domain and `HTTPS_METHOD=redirect`. + ### Diffie-Hellman Groups [RFC7919 groups](https://datatracker.ietf.org/doc/html/rfc7919#appendix-A) with key lengths of 2048, 3072, and 4096 bits are [provided by `nginx-proxy`](https://github.com/nginx-proxy/nginx-proxy/dhparam). The ENV `DHPARAM_BITS` can be set to `2048` or `3072` to change from the default 4096-bit key. The DH key file will be located in the container at `/etc/nginx/dhparam/dhparam.pem`. Mounting a different `dhparam.pem` file at that location will override the RFC7919 key. @@ -472,7 +578,11 @@ _WARNING_: HSTS will force your users to visit the HTTPS version of your site fo ### Missing Certificate -If HTTPS is enabled for a virtual host but its certificate is missing, nginx-proxy will configure nginx to use the default certificate (`default.crt` with `default.key`) and return a 500 error. +If no matching certificate is found for a given virtual host, nginx-proxy will: + +- configure nginx to use the default certificate (`default.crt` with `default.key`) and return a 500 error for HTTPS, +- force enable HTTP; i.e. `HTTPS_METHOD` will switch to `noredirect` if it was set to `nohttp` or `redirect`. + If this switch to HTTP is not wanted set `ENABLE_HTTP_ON_MISSING_CERT=false` (default is `true`). If the default certificate is also missing, nginx-proxy will configure nginx to accept HTTPS connections but fail the TLS negotiation. Client browsers will render a TLS error page. As of March 2023, web browsers display the following error messages: @@ -697,6 +807,76 @@ location / { Per virtual-host `servers_tokens` directive can be configured by passing appropriate value to the `SERVER_TOKENS` environment variable. Please see the [nginx http_core module configuration](https://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens) for more details. +### Custom error page + +To override the default error page displayed on 50x errors, mount your custom HTML error page inside the container at `/usr/share/nginx/html/errors/50x.html`: + +```console +docker run --detach \ + --name nginx-proxy \ + --publish 80:80 \ + --volume /var/run/docker.sock:/tmp/docker.sock:ro \ + --volume /path/to/error.html:/usr/share/nginx/html/errors/50x.html:ro \ + nginxproxy/nginx-proxy:1.5 +``` + +Note that this will not replace your own services error pages. + +⬆️ [back to table of contents](#table-of-contents) + +## TCP and UDP stream + +If you want to proxy non-HTTP traffic, you can use nginx's stream module. Write a configuration file and mount it inside `/etc/nginx/toplevel.conf.d`. + +```nginx +# stream.conf +stream { + upstream stream_backend { + server backend1.example.com:12345; + server backend2.example.com:12345; + server backend3.example.com:12346; + # ... + } + server { + listen 12345; + #TCP traffic will be forwarded to the "stream_backend" upstream group + proxy_pass stream_backend; + } + + server { + listen 12346; + #TCP traffic will be forwarded to the specified server + proxy_pass backend.example.com:12346; + } + + upstream dns_servers { + server 192.168.136.130:53; + server 192.168.136.131:53; + # ... + } + server { + listen 53 udp; + #UDP traffic will be forwarded to the "dns_servers" upstream group + proxy_pass dns_servers; + } + # ... +} +``` + +```console +docker run --detach \ + --name nginx-proxy \ + --publish 80:80 \ + --publish 12345:12345 \ + --publish 12346:12346 \ + --publish 53:53:udp \ + --volume /var/run/docker.sock:/tmp/docker.sock:ro \ + --volume ./stream.conf:/etc/nginx/toplevel.conf.d/stream.conf:ro \ + nginxproxy/nginx-proxy:1.5 +``` + +Please note that TCP and UDP stream are not core features of nginx-proxy, so the above is provided as an example only, without any guarantee. + ⬆️ [back to table of contents](#table-of-contents) ## Unhashed vs SHA1 upstream names diff --git a/nginx.tmpl b/nginx.tmpl index fefb07f1a..a7f4a0c18 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -128,7 +128,7 @@ # exposed ports:{{ range sortObjectsByKeysAsc $.container.Addresses "Port" }} {{ .Port }}/{{ .Proto }}{{ else }} (none){{ end }} {{- $default_port := when (eq (len $.container.Addresses) 1) (first $.container.Addresses).Port "80" }} # default port: {{ $default_port }} - {{- $port := when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port }} + {{- $port := when (eq $.port "default") $default_port (when (eq $.port "legacy") (or $.container.Env.VIRTUAL_PORT $default_port) $.port) }} # using port: {{ $port }} {{- $addr_obj := where $.container.Addresses "Port" $port | first }} {{- if and $addr_obj $addr_obj.HostPort }} @@ -265,7 +265,7 @@ uwsgi_pass {{ trim $proto }}://{{ trim $upstream }}; {{- else if eq $proto "fastcgi" }} root {{ trim .VhostRoot }}; - include fastcgi_params; + include fastcgi.conf; fastcgi_pass {{ trim $upstream }}; {{- if ne $keepalive "disabled" }} fastcgi_keep_conn on; @@ -338,49 +338,6 @@ upstream {{ $vpath.upstream }} { } {{- end }} -{{- /* - * Template used as a function to collect virtual path properties from - * the given containers. These properties are "returned" by storing their - * values into the provided dot dict. - * - * The provided dot dict is expected to have the following entries: - * - "Containers": List of container's RuntimeContainer struct. - * - "Upstream_name" - * - "Has_virtual_paths": boolean - * - "Path" - * - * The return values will be added to the dot dict with keys: - * - "dest" - * - "proto" - * - "network_tag" - * - "upstream" - * - "loadbalance" - * - "keepalive" - */}} -{{- define "get_path_info" }} - {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, 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" }} - - {{- $loadbalance := first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} - {{- $keepalive := coalesce (first (keys (groupByLabel $.Containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} - - {{- $upstream := $.Upstream_name }} - {{- $dest := "" }} - {{- if $.Has_virtual_paths }} - {{- $sum := sha1 $.Path }} - {{- $upstream = printf "%s-%s" $upstream $sum }} - {{- $dest = or (first (groupByKeys $.Containers "Env.VIRTUAL_DEST")) "" }} - {{- end }} - {{- $_ := set $ "proto" $proto }} - {{- $_ := set $ "network_tag" $network_tag }} - {{- $_ := set $ "upstream" $upstream }} - {{- $_ := set $ "dest" $dest }} - {{- $_ := set $ "loadbalance" $loadbalance }} - {{- $_ := set $ "keepalive" $keepalive }} -{{- 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 { @@ -500,15 +457,131 @@ proxy_set_header X-Original-URI $request_uri; proxy_set_header Proxy ""; {{- end }} -{{- /* Precompute some information about each vhost. */}} +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST_MULTIPORTS. */}} +{{- range $vhosts_yaml, $containers := groupBy $globals.containers "Env.VIRTUAL_HOST_MULTIPORTS" }} + {{- /* Print a warning in the config if VIRTUAL_HOST_MULTIPORTS can't be parsed. */}} + {{- $parsedVhosts := fromYaml $vhosts_yaml }} + {{- if (empty $parsedVhosts) }} + {{- $containerNames := list }} + {{- range $container := $containers }} + {{- $containerNames = append $containerNames $container.Name }} + {{- end }} +# /!\ WARNING: the VIRTUAL_HOST_MULTIPORTS environment variable used for {{ len $containerNames | plural "this container" "those containers" }} is not a valid YAML string: +# {{ $containerNames | join ", " }} + {{- continue }} + {{- end }} + + {{- range $hostname, $vhost := $parsedVhosts }} + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + + {{- if (empty $vhost) }} + {{ $vhost = dict "/" (dict) }} + {{- end }} + + {{- range $path, $vpath := $vhost }} + {{- if (empty $vpath) }} + {{- $vpath = dict "dest" "" "port" "default" }} + {{- end }} + {{- $dest := coalesce $vpath.dest "" }} + {{- $port := when (hasKey $vpath "port") (toString $vpath.port) "default" }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" $dest }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- end }} + {{- $_ := set $vhost_data "paths" $paths }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} + {{- end }} +{{- end }} + +{{- /* Precompute and store some information about vhost that use VIRTUAL_HOST. */}} {{- range $hostname, $containers := groupByMulti $globals.containers "Env.VIRTUAL_HOST" "," }} + {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- $hostname = trim $hostname }} {{- if not $hostname }} - {{- /* Ignore containers with VIRTUAL_HOST set to the empty string. */}} {{- continue }} {{- end }} - {{- $certName := first (groupByKeys $containers "Env.CERT_NAME") }} + {{- /* Drop containers with both VIRTUAL_HOST and VIRTUAL_HOST_MULTIPORTS set + * (VIRTUAL_HOST_MULTIPORTS takes precedence thanks to the previous loop). + */}} + {{- range $_, $containers_to_drop := groupBy $containers "Env.VIRTUAL_HOST_MULTIPORTS" }} + {{- range $container := $containers_to_drop }} + {{- $containers = without $containers $container }} + {{- end }} + {{- end }} + {{- if (eq (len $containers) 0) }} + {{- continue }} + {{- end }} + + {{- $vhost_data := when (hasKey $globals.vhosts $hostname) (get $globals.vhosts $hostname) (dict) }} + {{- $paths := coalesce $vhost_data.paths (dict) }} + + {{- $tmp_paths := groupByWithDefault $containers "Env.VIRTUAL_PATH" "/" }} + + {{- range $path, $containers := $tmp_paths }} + {{- $dest := or (first (groupByKeys $containers "Env.VIRTUAL_DEST")) "" }} + {{- $port := "legacy" }} + {{- $path_data := when (hasKey $paths $path) (get $paths $path) (dict) }} + {{- $path_ports := when (hasKey $path_data "ports") (get $path_data "ports") (dict) }} + {{- $path_port_containers := when (hasKey $path_ports $port) (get $path_ports $port) (list) }} + {{- $path_port_containers = concat $path_port_containers $containers }} + {{- $_ := set $path_ports $port $path_port_containers }} + {{- $_ := set $path_data "ports" $path_ports }} + {{- if (not (hasKey $path_data "dest")) }} + {{- $_ := set $path_data "dest" $dest }} + {{- end }} + {{- $_ := set $paths $path $path_data }} + {{- end }} + {{- $_ := set $vhost_data "paths" $paths }} + {{- $is_regexp := hasPrefix "~" $hostname }} + {{- $_ := set $vhost_data "upstream_name" (when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname) }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} +{{- end }} + +{{- /* Loop over $globals.vhosts and update it with the remaining informations about each vhost. */}} +{{- range $hostname, $vhost_data := $globals.vhosts }} + {{- $vhost_containers := list }} + {{- range $path, $vpath_data := $vhost_data.paths }} + {{- $vpath_containers := list }} + {{- range $port, $vport_containers := $vpath_data.ports }} + {{ $vpath_containers = concat $vpath_containers $vport_containers }} + {{- end }} + + {{- /* Get the VIRTUAL_PROTO defined by containers w/ the same vhost-vpath, falling back to "http". */}} + {{- $proto := trim (or (first (groupByKeys $vpath_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 $vpath_containers "Env.NETWORK_ACCESS")) "external" }} + + {{- $loadbalance := first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.loadbalance")) }} + {{- $keepalive := coalesce (first (keys (groupByLabel $vpath_containers "com.github.nginx-proxy.nginx-proxy.keepalive"))) "disabled" }} + + {{- $upstream := $vhost_data.upstream_name }} + {{- if (not (eq $path "/")) }} + {{- $sum := sha1 $path }} + {{- $upstream = printf "%s-%s" $upstream $sum }} + {{- end }} + + {{- $_ := set $vpath_data "proto" $proto }} + {{- $_ := set $vpath_data "network_tag" $network_tag }} + {{- $_ := set $vpath_data "upstream" $upstream }} + {{- $_ := set $vpath_data "loadbalance" $loadbalance }} + {{- $_ := set $vpath_data "keepalive" $keepalive }} + {{- $_ := set $vhost_data.paths $path $vpath_data }} + + {{ $vhost_containers = concat $vhost_containers $vpath_containers }} + {{- end }} + + {{- $certName := first (groupByKeys $vhost_containers "Env.CERT_NAME") }} {{- $vhostCert := closest (dir "/etc/nginx/certs") (printf "%s.crt" $hostname) }} {{- $vhostCert = trimSuffix ".crt" $vhostCert }} {{- $vhostCert = trimSuffix ".key" $vhostCert }} @@ -516,49 +589,34 @@ proxy_set_header Proxy ""; {{- $cert_ok := and (ne $cert "") (exists (printf "/etc/nginx/certs/%s.crt" $cert)) (exists (printf "/etc/nginx/certs/%s.key" $cert)) }} {{- $default := eq $globals.Env.DEFAULT_HOST $hostname }} - {{- $https_method := or (first (groupByKeys $containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} - {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} - {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} - - {{- $is_regexp := hasPrefix "~" $hostname }} - {{- $upstream_name := when (or $is_regexp $globals.sha1_upstream_name) (sha1 $hostname) $hostname }} + {{- $https_method := or (first (groupByKeys $vhost_containers "Env.HTTPS_METHOD")) $globals.Env.HTTPS_METHOD "redirect" }} + {{- $enable_http_on_missing_cert := parseBool (or (first (groupByKeys $vhost_containers "Env.ENABLE_HTTP_ON_MISSING_CERT")) $globals.Env.ENABLE_HTTP_ON_MISSING_CERT "true") }} + {{- /* When the certificate is missing we want to ensure that HTTP is enabled; hence switching from 'nohttp' or 'redirect' to 'noredirect' */}} + {{- if (and $enable_http_on_missing_cert (not $cert_ok) (or (eq $https_method "nohttp") (eq $https_method "redirect"))) }} + {{- $https_method = "noredirect" }} + {{- end }} + {{- $http2_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http2.enable"))) $globals.Env.ENABLE_HTTP2 "true")}} + {{- $http3_enabled := parseBool (or (first (keys (groupByLabel $vhost_containers "com.github.nginx-proxy.nginx-proxy.http3.enable"))) $globals.Env.ENABLE_HTTP3 "false")}} + {{- $acme_http_challenge := or (first (groupByKeys $vhost_containers "Env.ACME_HTTP_CHALLENGE_LOCATION")) $globals.Env.ACME_HTTP_CHALLENGE_LOCATION "true" }} + {{- $acme_http_challenge_legacy := eq $acme_http_challenge "legacy" }} + {{- $acme_http_challenge_enabled := false }} + {{- if (not $acme_http_challenge_legacy) }} + {{- $acme_http_challenge_enabled = parseBool $acme_http_challenge }} + {{- end }} {{- /* Get the SERVER_TOKENS defined by containers w/ the same vhost, falling back to "". */}} - {{- $server_tokens := trim (or (first (groupByKeys $containers "Env.SERVER_TOKENS")) "") }} + {{- $server_tokens := trim (or (first (groupByKeys $vhost_containers "Env.SERVER_TOKENS")) "") }} {{- /* Get the SSL_POLICY defined by containers w/ the same vhost, falling back to empty string (use default). */}} - {{- $ssl_policy := or (first (groupByKeys $containers "Env.SSL_POLICY")) "" }} + {{- $ssl_policy := or (first (groupByKeys $vhost_containers "Env.SSL_POLICY")) "" }} {{- /* Get the HSTS defined by containers w/ the same vhost, falling back to "max-age=31536000". */}} - {{- $hsts := or (first (groupByKeys $containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} + {{- $hsts := or (first (groupByKeys $vhost_containers "Env.HSTS")) (or $globals.Env.HSTS "max-age=31536000") }} {{- /* Get the VIRTUAL_ROOT By containers w/ use fastcgi root */}} - {{- $vhost_root := or (first (groupByKeys $containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - + {{- $vhost_root := or (first (groupByKeys $vhost_containers "Env.VIRTUAL_ROOT")) "/var/www/public" }} - {{- $tmp_paths := groupBy $containers "Env.VIRTUAL_PATH" }} - {{- $has_virtual_paths := gt (len $tmp_paths) 0}} - {{- if not $has_virtual_paths }} - {{- $tmp_paths = dict "/" $containers }} - {{- end }} - - {{- $paths := dict }} - - {{- range $path, $containers := $tmp_paths }} - {{- $args := dict "Containers" $containers "Path" $path "Upstream_name" $upstream_name "Has_virtual_paths" $has_virtual_paths }} - {{- template "get_path_info" $args }} - {{- $_ := set $paths $path (dict - "ports" (dict "legacy" $containers) - "dest" $args.dest - "proto" $args.proto - "network_tag" $args.network_tag - "upstream" $args.upstream - "loadbalance" $args.loadbalance - "keepalive" $args.keepalive - ) }} - {{- end }} - - {{- $_ := set $globals.vhosts $hostname (dict + {{- $vhost_data = merge $vhost_data (dict "cert" $cert "cert_ok" $cert_ok "default" $default @@ -566,13 +624,16 @@ proxy_set_header Proxy ""; "https_method" $https_method "http2_enabled" $http2_enabled "http3_enabled" $http3_enabled - "paths" $paths + "acme_http_challenge_legacy" $acme_http_challenge_legacy + "acme_http_challenge_enabled" $acme_http_challenge_enabled "server_tokens" $server_tokens "ssl_policy" $ssl_policy "vhost_root" $vhost_root ) }} + {{- $_ := set $globals.vhosts $hostname $vhost_data }} {{- end }} + {{- /* * If needed, create a catch-all fallback server to send an error code to * clients that request something from an unknown vhost. @@ -594,7 +655,7 @@ proxy_set_header Proxy ""; {{- $default_https_exists := false }} {{- $http3_enabled := false }} {{- range $vhost := $globals.vhosts }} - {{- $http := or (ne $vhost.https_method "nohttp") (not $vhost.cert_ok) }} + {{- $http := ne $vhost.https_method "nohttp" }} {{- $https := ne $vhost.https_method "nohttps" }} {{- $http_exists = or $http_exists $http }} {{- $https_exists = or $https_exists $https }} @@ -602,7 +663,7 @@ proxy_set_header Proxy ""; {{- $default_https_exists = or $default_https_exists (and $https $vhost.default) }} {{- $http3_enabled = or $http3_enabled $vhost.http3_enabled }} {{- end }} - {{- $fallback_http := and $http_exists (not $default_http_exists) }} + {{- $fallback_http := not $default_http_exists }} {{- $fallback_https := and $https_exists (not $default_https_exists) }} {{- /* * If there are no vhosts at all, create fallbacks for both plain http @@ -610,7 +671,6 @@ proxy_set_header Proxy ""; * refused error. */}} {{- if and (not $http_exists) (not $https_exists) }} - {{- $fallback_http = true }} {{- $fallback_https = true }} {{- end }} {{- if or $fallback_http $fallback_https }} @@ -655,7 +715,17 @@ server { return 444; } {{- end }} - return 503; + + {{- if (exists "/usr/share/nginx/html/errors/50x.html") }} + error_page 500 502 503 504 /50x.html; + location /50x.html { + root /usr/share/nginx/html/errors; + internal; + } + {{- end }} + location ^~ / { + return 503; + } } {{- end }} {{- end }} @@ -668,7 +738,7 @@ server { {{ template "upstream" (dict "globals" $globals "Path" $path "VPath" $vpath) }} {{- end }} - {{- if and $vhost.cert_ok (eq $vhost.https_method "redirect") }} + {{- if (eq $vhost.https_method "redirect") }} server { server_name {{ $hostname }}; {{- if $vhost.server_tokens }} @@ -680,6 +750,7 @@ server { listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; {{- end }} + {{- if (or $vhost.acme_http_challenge_legacy $vhost.acme_http_challenge_enabled) }} # Do not HTTPS redirect Let's Encrypt ACME challenge location ^~ /.well-known/acme-challenge/ { auth_basic off; @@ -689,6 +760,7 @@ server { try_files $uri =404; break; } + {{- end }} location / { {{- if eq $globals.external_https_port "443" }} @@ -709,11 +781,21 @@ server { {{- if $vhost.http2_enabled }} http2 on; {{- end }} - {{- if or (eq $vhost.https_method "nohttps") (not $vhost.cert_ok) (eq $vhost.https_method "noredirect") }} + {{- if or (eq $vhost.https_method "nohttps") (eq $vhost.https_method "noredirect") }} listen {{ $globals.external_http_port }} {{ $default_server }}; {{- if $globals.enable_ipv6 }} listen [::]:{{ $globals.external_http_port }} {{ $default_server }}; {{- end }} + + {{- if (and (eq $vhost.https_method "noredirect") $vhost.acme_http_challenge_enabled) }} + location /.well-known/acme-challenge/ { + auth_basic off; + allow all; + root /usr/share/nginx/html; + try_files $uri =404; + break; + } + {{- end }} {{- end }} {{- if ne $vhost.https_method "nohttps" }} listen {{ $globals.external_https_port }} ssl {{ $default_server }}; diff --git a/test/certs/create_server_certificate.sh b/test/certs/create_server_certificate.sh index 0789a22e7..f9d6b9764 100755 --- a/test/certs/create_server_certificate.sh +++ b/test/certs/create_server_certificate.sh @@ -24,7 +24,7 @@ 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.25.3) +CONTAINER=$(docker run -d -v $DIR:/work -w /work -e SAN="$ALTERNATE_DOMAINS" nginx:1.27.0) # Configure openssl docker exec $CONTAINER bash -c ' mkdir -p /ca/{certs,crl,private,newcerts} 2>/dev/null diff --git a/test/conftest.py b/test/conftest.py index 7fa269a61..dda20f60b 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -510,6 +510,13 @@ def nginxproxy(): yield requests_for_docker() +@pytest.fixture() +def acme_challenge_path(): + """ + Provides fake Let's Encrypt ACME challenge path used in certain tests + """ + return ".well-known/acme-challenge/test-filename" + ############################################################################### # # Py.test hooks diff --git a/test/requirements/python-requirements.txt b/test/requirements/python-requirements.txt index f22c000dd..c3e2218a1 100644 --- a/test/requirements/python-requirements.txt +++ b/test/requirements/python-requirements.txt @@ -1,4 +1,4 @@ backoff==2.2.1 -docker==7.0.0 -pytest==8.2.0 -requests==2.31.0 +docker==7.1.0 +pytest==8.2.1 +requests==2.32.3 diff --git a/test/requirements/web/Dockerfile b/test/requirements/web/Dockerfile index 923ed79c4..da755534c 100644 --- a/test/requirements/web/Dockerfile +++ b/test/requirements/web/Dockerfile @@ -1,6 +1,7 @@ # Docker Image running one (or multiple) webservers listening on all given ports from WEB_PORTS environment variable -FROM python:3 +FROM python:3-alpine +RUN apk add --no-cache bash COPY ./webserver.py / COPY ./entrypoint.sh / WORKDIR /opt diff --git a/test/requirements/web/entrypoint.sh b/test/requirements/web/entrypoint.sh index 3015c115d..be9f9c365 100644 --- a/test/requirements/web/entrypoint.sh +++ b/test/requirements/web/entrypoint.sh @@ -5,11 +5,11 @@ 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 & + echo starting a web server listening on port "$port"; + /webserver.py "$port" & PIDS+=($!) done -wait ${PIDS[@]} +wait "${PIDS[@]}" trap - TERM -wait ${PIDS[@]} +wait "${PIDS[@]}" diff --git a/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename b/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_acme_http_challenge_location/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.crt new file mode 100644 index 000000000..cd7284b06 --- /dev/null +++ b/test/test_acme_http_challenge_location/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----- diff --git a/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key b/test/test_acme_http_challenge_location/certs/nginx-proxy.tld.key new file mode 100644 index 000000000..91adb14e1 --- /dev/null +++ b/test/test_acme_http_challenge_location/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----- diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py new file mode 100644 index 000000000..acbc8feb9 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.py @@ -0,0 +1,30 @@ +import pytest + + +def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web1.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 301 + +def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web2.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_noredirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 + +def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web4.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml new file mode 100644 index 000000000..3cd4f2d62 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_disabled.yml @@ -0,0 +1,47 @@ +version: "2" + +services: + 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" + ACME_HTTP_CHALLENGE_LOCATION: "true" + + web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "web3.nginx-proxy.tld" + HTTPS_METHOD: noredirect + + web4: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: "web4.nginx-proxy.tld" + HTTPS_METHOD: noredirect + ACME_HTTP_CHALLENGE_LOCATION: "true" + + sut: + image: nginxproxy/nginx-proxy:test + environment: + ACME_HTTP_CHALLENGE_LOCATION: "false" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.py new file mode 100644 index 000000000..fd06e847d --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.py @@ -0,0 +1,30 @@ +import pytest + + +def test_redirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web1.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_redirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web2.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 301 + +def test_noredirect_acme_challenge_location_enabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_noredirect_acme_challenge_location_disabled(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web4.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml new file mode 100644 index 000000000..41439e30c --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_enabled_is_default.yml @@ -0,0 +1,45 @@ +version: "2" + +services: + 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" + ACME_HTTP_CHALLENGE_LOCATION: "false" + + web3: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: "web3.nginx-proxy.tld" + HTTPS_METHOD: noredirect + + web4: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: "web4.nginx-proxy.tld" + HTTPS_METHOD: noredirect + ACME_HTTP_CHALLENGE_LOCATION: "false" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.py b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.py new file mode 100644 index 000000000..d2051d0aa --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.py @@ -0,0 +1,16 @@ +import pytest + + +def test_redirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web1.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + +def test_noredirect_acme_challenge_location_legacy(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web2.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 diff --git a/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml new file mode 100644 index 000000000..693f9e013 --- /dev/null +++ b/test/test_acme_http_challenge_location/test_acme_challenge_location_legacy.yml @@ -0,0 +1,28 @@ +version: "2" + +services: + 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" + HTTPS_METHOD: noredirect + + sut: + image: nginxproxy/nginx-proxy:test + environment: + ACME_HTTP_CHALLENGE_LOCATION: "legacy" + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_build.py b/test/test_build.py index 9c798082e..4052d1ee0 100644 --- a/test/test_build.py +++ b/test/test_build.py @@ -4,13 +4,14 @@ import pytest import docker import re +import os client = docker.from_env() @pytest.fixture(scope = "session") def docker_build(request): # Define Dockerfile path - dockerfile_path = "requirements/" + dockerfile_path = os.path.join(os.path.dirname(__file__), "requirements/") dockerfile_name = "Dockerfile-nginx-proxy-tester" # Build the Docker image diff --git a/test/test_composev2.py b/test/test_composev2.py deleted file mode 100644 index 695857efe..000000000 --- a/test/test_composev2.py +++ /dev/null @@ -1,10 +0,0 @@ -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.example/port") - assert r.status_code == 200 - assert r.text == "answer from port 81\n" diff --git a/test/test_composev2.yml b/test/test_composev2.yml deleted file mode 100644 index 3c36022b5..000000000 --- a/test/test_composev2.yml +++ /dev/null @@ -1,15 +0,0 @@ -version: "2" - -services: - nginx-proxy: - image: nginxproxy/nginx-proxy:test - volumes: - - /var/run/docker.sock:/tmp/docker.sock:ro - - web: - image: web - expose: - - "81" - environment: - WEB_PORTS: 81 - VIRTUAL_HOST: web.nginx-proxy.example diff --git a/test/test_custom-error-page/50x.html b/test/test_custom-error-page/50x.html new file mode 100644 index 000000000..63a299ec0 --- /dev/null +++ b/test/test_custom-error-page/50x.html @@ -0,0 +1,23 @@ + + +
++ Our apologies for this temporary inconvenience. Regular service + performance will be re-established shortly. +
+ + diff --git a/test/test_custom-error-page/test_custom-error-page.py b/test/test_custom-error-page/test_custom-error-page.py new file mode 100644 index 000000000..32cb0b542 --- /dev/null +++ b/test/test_custom-error-page/test_custom-error-page.py @@ -0,0 +1,8 @@ +import pytest +import re + + +def test_custom_error_page(docker_compose, nginxproxy): + r = nginxproxy.get("http://unknown.nginx-proxy.tld") + assert r.status_code == 503 + assert re.search(r"Damn, there's some maintenance in progress.", r.text) diff --git a/test/test_custom-error-page/test_custom-error-page.yml b/test/test_custom-error-page/test_custom-error-page.yml new file mode 100644 index 000000000..419b7eb8d --- /dev/null +++ b/test/test_custom-error-page/test_custom-error-page.yml @@ -0,0 +1,8 @@ +version: "2" + +services: + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./50x.html:/usr/share/nginx/html/errors/50x.html:ro diff --git a/test/test_enable_http_on_missing_cert.py b/test/test_enable_http_on_missing_cert.py new file mode 100644 index 000000000..cdedc8a2c --- /dev/null +++ b/test/test_enable_http_on_missing_cert.py @@ -0,0 +1,18 @@ +import pytest + + +def test_nohttp_missing_cert_disabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://nohttp-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 503 + +def test_nohttp_missing_cert_enabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://nohttp-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 200 + +def test_redirect_missing_cert_disabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://redirect-missing-cert-disabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 301 + +def test_redirect_missing_cert_enabled(docker_compose, nginxproxy): + r = nginxproxy.get("http://redirect-missing-cert-enabled.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 200 diff --git a/test/test_enable_http_on_missing_cert.yml b/test/test_enable_http_on_missing_cert.yml new file mode 100644 index 000000000..1149ef720 --- /dev/null +++ b/test/test_enable_http_on_missing_cert.yml @@ -0,0 +1,46 @@ +version: "2" + +services: + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./withdefault.certs:/etc/nginx/certs:ro + environment: + ENABLE_HTTP_ON_MISSING_CERT: "false" + + nohttp-missing-cert-disabled: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: nohttp-missing-cert-disabled.nginx-proxy.tld + HTTPS_METHOD: nohttp + + nohttp-missing-cert-enabled: + image: web + expose: + - "82" + environment: + WEB_PORTS: "82" + VIRTUAL_HOST: nohttp-missing-cert-enabled.nginx-proxy.tld + HTTPS_METHOD: nohttp + ENABLE_HTTP_ON_MISSING_CERT: "true" + + redirect-missing-cert-disabled: + image: web + expose: + - "83" + environment: + WEB_PORTS: "83" + VIRTUAL_HOST: redirect-missing-cert-disabled.nginx-proxy.tld + + redirect-missing-cert-enabled: + image: web + expose: + - "84" + environment: + WEB_PORTS: "84" + VIRTUAL_HOST: redirect-missing-cert-enabled.nginx-proxy.tld + ENABLE_HTTP_ON_MISSING_CERT: "true" diff --git a/test/test_fallback.py b/test/test_fallback.py index 1ee923ac1..16da3d7d6 100644 --- a/test/test_fallback.py +++ b/test/test_fallback.py @@ -60,19 +60,17 @@ def _get(url): ("nodefault.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nodefault.yml", "https://unknown.nginx-proxy.test/", None, INTERNAL_ERR_RE), # HTTPS_METHOD=nohttp on nginx-proxy, HTTPS_METHOD unset on the app container. - ("nohttp.yml", "http://https-only.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("nohttp.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp.yml", "https://https-only.nginx-proxy.test/", 200, None), - ("nohttp.yml", "http://unknown.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("nohttp.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nohttp.yml", "https://unknown.nginx-proxy.test/", 503, None), # HTTPS_METHOD=redirect on nginx-proxy, HTTPS_METHOD=nohttp on the app container. - ("nohttp-on-app.yml", "http://https-only.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("nohttp-on-app.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-on-app.yml", "https://https-only.nginx-proxy.test/", 200, None), - ("nohttp-on-app.yml", "http://unknown.nginx-proxy.test/", None, CONNECTION_REFUSED_RE), + ("nohttp-on-app.yml", "http://unknown.nginx-proxy.test/", 503, None), ("nohttp-on-app.yml", "https://unknown.nginx-proxy.test/", 503, None), # Same as nohttp.yml, except there is a vhost with a missing cert. This causes its - # HTTPS_METHOD=nohttp setting to effectively become HTTPS_METHOD=noredirect. This means that - # there will be a plain http server solely to support that vhost, so http requests to other - # vhosts get a 503, not a connection refused error. + # HTTPS_METHOD=nohttp setting to effectively become HTTPS_METHOD=noredirect. ("nohttp-with-missing-cert.yml", "http://https-only.nginx-proxy.test/", 503, None), ("nohttp-with-missing-cert.yml", "https://https-only.nginx-proxy.test/", 200, None), ("nohttp-with-missing-cert.yml", "http://missing-cert.nginx-proxy.test/", 200, None), diff --git a/test/test_log_format.py b/test/test_logs/test_log_format.py similarity index 100% rename from test/test_log_format.py rename to test/test_logs/test_log_format.py diff --git a/test/test_log_format.yml b/test/test_logs/test_log_format.yml similarity index 100% rename from test/test_log_format.yml rename to test/test_logs/test_log_format.yml diff --git a/test/test_log_json.py b/test/test_logs/test_log_json.py similarity index 100% rename from test/test_log_json.py rename to test/test_logs/test_log_json.py diff --git a/test/test_log_json.yml b/test/test_logs/test_log_json.yml similarity index 100% rename from test/test_log_json.yml rename to test/test_logs/test_log_json.yml diff --git a/test/test_log_json_format.py b/test/test_logs/test_log_json_format.py similarity index 100% rename from test/test_log_json_format.py rename to test/test_logs/test_log_json_format.py diff --git a/test/test_log_json_format.yml b/test/test_logs/test_log_json_format.yml similarity index 100% rename from test/test_log_json_format.yml rename to test/test_logs/test_log_json_format.yml diff --git a/test/test_multiports/test_multiports-base-json.py b/test/test_multiports/test_multiports-base-json.py new file mode 100644 index 000000000..7f1ef0fb0 --- /dev/null +++ b/test/test_multiports/test_multiports-base-json.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base-json.yml b/test/test_multiports/test_multiports-base-json.yml new file mode 100644 index 000000000..fc0d0fa81 --- /dev/null +++ b/test/test_multiports/test_multiports-base-json.yml @@ -0,0 +1,77 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_MULTIPORTS: |- + { + "notskipped.nginx-proxy.tld": {} + } + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port80.a.nginx-proxy.tld": {}, + "port80.b.nginx-proxy.tld": {}, + "port80.c.nginx-proxy.tld": { + "/": {} + } + } + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + { + "port8080.nginx-proxy.tld": { + "/": { + "port": 8080 + } + }, + "port9000.nginx-proxy.tld": { + "/": { + "port": 9000 + } + } + } + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_MULTIPORTS: |- + { + "virtualpaths.nginx-proxy.tld": { + "/rootdest": { + "port": 10001, + "dest": "/" + }, + "/customdest": { + "port": 10002, + "dest": "/port" + } + } + } + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiports/test_multiports-base-yaml.py b/test/test_multiports/test_multiports-base-yaml.py new file mode 100644 index 000000000..7f1ef0fb0 --- /dev/null +++ b/test/test_multiports/test_multiports-base-yaml.py @@ -0,0 +1,39 @@ +import pytest + + +def test_virtual_host_is_dropped_when_using_multiports(docker_compose, nginxproxy): + r = nginxproxy.get("http://notskipped.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 81\n" in r.text + r = nginxproxy.get("http://skipped.nginx-proxy.tld/") + assert r.status_code == 503 + + +def test_answer_is_served_from_port_80_by_default(docker_compose, nginxproxy): + r = nginxproxy.get("http://port80.a.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.b.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + r = nginxproxy.get("http://port80.c.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 80\n" in r.text + + +def test_answer_is_served_from_chosen_ports(docker_compose, nginxproxy): + r = nginxproxy.get("http://port8080.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 8080\n" in r.text + r = nginxproxy.get("http://port9000.nginx-proxy.tld/port") + assert r.status_code == 200 + assert "answer from port 9000\n" in r.text + + +def test_answer_is_served_from_chosen_ports_and_dest(docker_compose, nginxproxy): + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/rootdest/port") + assert r.status_code == 200 + assert "answer from port 10001\n" in r.text + r = nginxproxy.get("http://virtualpaths.nginx-proxy.tld/customdest") + assert r.status_code == 200 + assert "answer from port 10002\n" in r.text diff --git a/test/test_multiports/test_multiports-base-yaml.yml b/test/test_multiports/test_multiports-base-yaml.yml new file mode 100644 index 000000000..8bd58030c --- /dev/null +++ b/test/test_multiports/test_multiports-base-yaml.yml @@ -0,0 +1,61 @@ +version: "2" + +services: + skipvirtualhost: + image: web + expose: + - "81" + environment: + WEB_PORTS: "81" + VIRTUAL_HOST: skipped.nginx-proxy.tld + VIRTUAL_HOST_MULTIPORTS: |- + notskipped.nginx-proxy.tld: + + defaultport: + image: web + expose: + - "80" + - "8080" + environment: + WEB_PORTS: "80 8080" + VIRTUAL_HOST_MULTIPORTS: |- + port80.a.nginx-proxy.tld: + port80.b.nginx-proxy.tld: + port80.c.nginx-proxy.tld: + "/": + + multiports: + image: web + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + port8080.nginx-proxy.tld: + "/": + port: 8080 + port9000.nginx-proxy.tld: + "/": + port: 9000 + + virtualpath: + image: web + expose: + - "10001" + - "10002" + environment: + WEB_PORTS: "10001 10002" + VIRTUAL_HOST_MULTIPORTS: |- + virtualpaths.nginx-proxy.tld: + "/rootdest": + port: 10001 + dest: "/" + "/customdest": + port: 10002 + dest: "/port" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiports/test_multiports-invalid-syntax.py b/test/test_multiports/test_multiports-invalid-syntax.py new file mode 100644 index 000000000..ed1c77338 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.py @@ -0,0 +1,18 @@ +import pytest +import re + + +def test_virtual_hosts_with_syntax_error_should_not_be_reachable(docker_compose, nginxproxy): + r = nginxproxy.get("http://test1.nginx-proxy.tld") + assert r.status_code == 503 + r = nginxproxy.get("http://test2.nginx-proxy.tld") + assert r.status_code == 503 + + +def test_config_should_have_multiports_warning_comments(docker_compose, nginxproxy): + conf = nginxproxy.get_conf().decode('ASCII') + matches = re.findall(r"the VIRTUAL_HOST_MULTIPORTS environment variable used for this container is not a valid YAML string", conf) + assert len(matches) == 3 + assert "# invalidsyntax" in conf + assert "# hostnamerepeat" in conf + assert "# pathrepeat" in conf diff --git a/test/test_multiports/test_multiports-invalid-syntax.yml b/test/test_multiports/test_multiports-invalid-syntax.yml new file mode 100644 index 000000000..9f4022061 --- /dev/null +++ b/test/test_multiports/test_multiports-invalid-syntax.yml @@ -0,0 +1,44 @@ +version: "2" + +services: + invalidsyntax: + image: web + container_name: invalidsyntax + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld + test2.nginx-proxy.tld: + + hostnamerepeat: + image: web + container_name: hostnamerepeat + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + test1.nginx-proxy.tld: + + pathrepeat: + image: web + container_name: pathrepeat + expose: + - "8080" + - "9000" + environment: + WEB_PORTS: "8080 9000" + VIRTUAL_HOST_MULTIPORTS: |- + test1.nginx-proxy.tld: + "/": + port: 8080 + "/": + port: 9000 + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiports/test_multiports-merge.py b/test/test_multiports/test_multiports-merge.py new file mode 100644 index 000000000..f5aa69782 --- /dev/null +++ b/test/test_multiports/test_multiports-merge.py @@ -0,0 +1,14 @@ +import backoff +import pytest + + +def test_multiports_and_legacy_configs_should_be_merged(docker_compose, nginxproxy): + @backoff.on_predicate(backoff.constant, lambda r: r == False, interval=.5, max_tries=20, jitter=None) + def answer_contains(answer, url): + return answer in nginxproxy.get(url).text + + assert answer_contains("80", "http://merged.nginx-proxy.tld/port") + assert answer_contains("81", "http://merged.nginx-proxy.tld/port") + + assert answer_contains("9090", "http://merged.nginx-proxy.tld/foo/port") + assert answer_contains("9191", "http://merged.nginx-proxy.tld/foo/port") diff --git a/test/test_multiports/test_multiports-merge.yml b/test/test_multiports/test_multiports-merge.yml new file mode 100644 index 000000000..5c5cd8bd0 --- /dev/null +++ b/test/test_multiports/test_multiports-merge.yml @@ -0,0 +1,41 @@ +version: "2" + +services: + merged-singleport: + image: web + expose: + - "80" + environment: + WEB_PORTS: "80" + VIRTUAL_HOST: merged.nginx-proxy.tld + + merged-singleport-virtual-path: + image: web + expose: + - "9090" + environment: + WEB_PORTS: "9090" + VIRTUAL_HOST: merged.nginx-proxy.tld + VIRTUAL_PORT: "9090" + VIRTUAL_PATH: "/foo" + VIRTUAL_DEST: "/" + + merged-multiports: + image: web + expose: + - "81" + - "9191" + environment: + WEB_PORTS: "81 9191" + VIRTUAL_HOST_MULTIPORTS: |- + merged.nginx-proxy.tld: + "/": + port: 81 + "/foo": + port: 9191 + dest: "/" + + sut: + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.py rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml b/test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT-single-different-from-single-port.yml rename to test/test_ports/test_VIRTUAL_PORT-single-different-from-single-port.yml diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.py b/test/test_ports/test_VIRTUAL_PORT.py similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.py rename to test/test_ports/test_VIRTUAL_PORT.py diff --git a/test/test_multiple-ports/test_VIRTUAL_PORT.yml b/test/test_ports/test_VIRTUAL_PORT.yml similarity index 100% rename from test/test_multiple-ports/test_VIRTUAL_PORT.yml rename to test/test_ports/test_VIRTUAL_PORT.yml diff --git a/test/test_multiple-ports/test_default-80.py b/test/test_ports/test_default-80.py similarity index 100% rename from test/test_multiple-ports/test_default-80.py rename to test/test_ports/test_default-80.py diff --git a/test/test_multiple-ports/test_default-80.yml b/test/test_ports/test_default-80.yml similarity index 100% rename from test/test_multiple-ports/test_default-80.yml rename to test/test_ports/test_default-80.yml diff --git a/test/test_multiple-ports/test_single-port-not-80.py b/test/test_ports/test_single-port-not-80.py similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.py rename to test/test_ports/test_single-port-not-80.py diff --git a/test/test_multiple-ports/test_single-port-not-80.yml b/test/test_ports/test_single-port-not-80.yml similarity index 100% rename from test/test_multiple-ports/test_single-port-not-80.yml rename to test/test_ports/test_single-port-not-80.yml diff --git a/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename b/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_ssl/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_ssl/test_https_port.py b/test/test_ssl/test_https_port.py index 27a42eff6..ebe305f70 100644 --- a/test/test_ssl/test_https_port.py +++ b/test/test_ssl/test_https_port.py @@ -17,3 +17,12 @@ def test_nonstandardport_Host_header(docker_compose, nginxproxy): r = nginxproxy.get("https://web.nginx-proxy.tld:8443/headers") assert r.status_code == 200 assert "Host: web.nginx-proxy.tld:8443" in r.text + +@pytest.mark.parametrize("subdomain", ["foo", "bar"]) +def test_web1_acme_challenge_works(docker_compose, nginxproxy, acme_challenge_path, subdomain): + r = nginxproxy.get( + f"http://{subdomain}.nginx-proxy.tld:8080/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + assert "challenge-teststring\n" in r.text diff --git a/test/test_ssl/test_https_port.yml b/test/test_ssl/test_https_port.yml index 047054a39..b6541acb0 100644 --- a/test/test_ssl/test_https_port.yml +++ b/test/test_ssl/test_https_port.yml @@ -14,6 +14,7 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro environment: HTTP_PORT: 8080 HTTPS_PORT: 8443 diff --git a/test/test_ssl/test_nohttp.py b/test/test_ssl/test_nohttp.py index 5b650db4f..032f60fd4 100644 --- a/test/test_ssl/test_nohttp.py +++ b/test/test_ssl/test_nohttp.py @@ -3,8 +3,15 @@ def test_web2_http_is_connection_refused(docker_compose, nginxproxy): - with pytest.raises(requests.exceptions.RequestException, match="Connection refused"): - nginxproxy.get("http://web2.nginx-proxy.tld/") + r = nginxproxy.get("http://web2.nginx-proxy.tld/", allow_redirects=False) + assert r.status_code == 503 + + +def test_web2_http_is_connection_refused_for_acme_challenge( + docker_compose, nginxproxy, acme_challenge_path +): + r = nginxproxy.get(f"http://web2.nginx-proxy.tld/{acme_challenge_path}", allow_redirects=False) + assert r.status_code == 503 def test_web2_https_is_forwarded(docker_compose, nginxproxy): diff --git a/test/test_ssl/test_nohttp.yml b/test/test_ssl/test_nohttp.yml index 40b393ad6..0c21bf9ca 100644 --- a/test/test_ssl/test_nohttp.yml +++ b/test/test_ssl/test_nohttp.yml @@ -15,3 +15,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_nohttps.py b/test/test_ssl/test_nohttps.py index 1cedf8218..23e822477 100644 --- a/test/test_ssl/test_nohttps.py +++ b/test/test_ssl/test_nohttps.py @@ -10,3 +10,11 @@ def test_http_is_forwarded(docker_compose, nginxproxy): def test_https_is_disabled(docker_compose, nginxproxy): with pytest.raises(ConnectionError): nginxproxy.get("https://web.nginx-proxy.tld/", allow_redirects=False) + + +def test_http_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 diff --git a/test/test_ssl/test_nohttps.yml b/test/test_ssl/test_nohttps.yml index f2b07574b..209f57a8c 100644 --- a/test/test_ssl/test_nohttps.yml +++ b/test/test_ssl/test_nohttps.yml @@ -14,3 +14,4 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_noredirect.py b/test/test_ssl/test_noredirect.py index 62df28b13..1d956d198 100644 --- a/test/test_ssl/test_noredirect.py +++ b/test/test_ssl/test_noredirect.py @@ -16,4 +16,12 @@ def test_web3_https_is_forwarded(docker_compose, nginxproxy): def test_web2_HSTS_policy_is_inactive(docker_compose, nginxproxy): r = nginxproxy.get("https://web3.nginx-proxy.tld/port", allow_redirects=False) assert "answer from port 83\n" in r.text - assert "Strict-Transport-Security" not in r.headers \ No newline at end of file + assert "Strict-Transport-Security" not in r.headers + + +def test_web3_acme_challenge_does_work(docker_compose, nginxproxy, acme_challenge_path): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 diff --git a/test/test_ssl/test_noredirect.yml b/test/test_ssl/test_noredirect.yml index 8ee845525..7610ae212 100644 --- a/test/test_ssl/test_noredirect.yml +++ b/test/test_ssl/test_noredirect.yml @@ -15,3 +15,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_virtual_path.py b/test/test_ssl/test_virtual_path.py index 508653f24..72ac433b9 100644 --- a/test/test_ssl/test_virtual_path.py +++ b/test/test_ssl/test_virtual_path.py @@ -1,4 +1,5 @@ import pytest +from requests import ConnectionError @pytest.mark.parametrize("path", ["web1", "web2"]) def test_web1_http_redirects_to_https(docker_compose, nginxproxy, path): @@ -13,3 +14,11 @@ def test_web1_https_is_forwarded(docker_compose, nginxproxy, path, port): assert r.status_code == 200 assert "answer from port %d\n" % port in r.text + +@pytest.mark.parametrize("port", [81, 82]) +def test_acme_challenge_does_not_work(docker_compose, nginxproxy, acme_challenge_path, port): + with pytest.raises(ConnectionError): + nginxproxy.get( + f"http://www.nginx-proxy.tld:{port}/{acme_challenge_path}", + allow_redirects=False + ) diff --git a/test/test_ssl/test_virtual_path.yml b/test/test_ssl/test_virtual_path.yml index 2494d35d8..eb09ef0f6 100644 --- a/test/test_ssl/test_virtual_path.yml +++ b/test/test_ssl/test_virtual_path.yml @@ -26,3 +26,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/test_wildcard.py b/test/test_ssl/test_wildcard.py index 202ba247e..f019f68ef 100644 --- a/test/test_ssl/test_wildcard.py +++ b/test/test_ssl/test_wildcard.py @@ -21,3 +21,13 @@ def test_web1_HSTS_policy_is_active(docker_compose, nginxproxy, subdomain): r = nginxproxy.get(f"https://{subdomain}.nginx-proxy.tld/port", allow_redirects=False) assert "answer from port 81\n" in r.text assert "Strict-Transport-Security" in r.headers + + +@pytest.mark.parametrize("subdomain", ["foo", "bar"]) +def test_web1_acme_challenge_works(docker_compose, nginxproxy, acme_challenge_path, subdomain): + r = nginxproxy.get( + f"http://web3.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 200 + assert "challenge-teststring\n" in r.text diff --git a/test/test_ssl/test_wildcard.yml b/test/test_ssl/test_wildcard.yml index ea8c596cc..b101e9f11 100644 --- a/test/test_ssl/test_wildcard.yml +++ b/test/test_ssl/test_wildcard.yml @@ -14,3 +14,4 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro diff --git a/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename b/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename new file mode 100644 index 000000000..5b45dff28 --- /dev/null +++ b/test/test_ssl/wildcard_cert_and_nohttps/acme_root/.well-known/acme-challenge/test-filename @@ -0,0 +1 @@ +challenge-teststring diff --git a/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml b/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml index 98f41a0a2..7cc64e76d 100644 --- a/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml +++ b/test/test_ssl/wildcard_cert_and_nohttps/docker-compose.yml @@ -7,6 +7,7 @@ services: volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./certs:/etc/nginx/certs:ro + - ./acme_root:/usr/share/nginx/html:ro web1: image: web diff --git a/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py b/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py index 445377912..d07437d76 100644 --- a/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py +++ b/test/test_ssl/wildcard_cert_and_nohttps/test_wildcard_cert_nohttps.py @@ -1,5 +1,6 @@ import pytest from ssl import CertificateError +from requests import ConnectionError from requests.exceptions import SSLError @@ -32,3 +33,25 @@ def test_https_request_to_nohttps_vhost_goes_to_fallback_server(docker_compose, r = nginxproxy.get("https://3.web.nginx-proxy.tld/port", verify=False) assert r.status_code == 503 + + +@pytest.mark.parametrize("subdomain,acme_should_work", [ + (1, True), + (2, True), + (3, False), +]) +def test_acme_challenge_works( + docker_compose, nginxproxy, acme_challenge_path, subdomain, acme_should_work +): + if acme_should_work: + r = nginxproxy.get( + f"https://{subdomain}.web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) + assert r.status_code == 404 + else: + with pytest.raises(ConnectionError): + nginxproxy.get( + f"https://{subdomain}.web.nginx-proxy.tld/{acme_challenge_path}", + allow_redirects=False + ) diff --git a/test/test_default-root-none.py b/test/test_virtual-path/test_default-root-none.py similarity index 100% rename from test/test_default-root-none.py rename to test/test_virtual-path/test_default-root-none.py diff --git a/test/test_default-root-none.yml b/test/test_virtual-path/test_default-root-none.yml similarity index 100% rename from test/test_default-root-none.yml rename to test/test_virtual-path/test_default-root-none.yml