diff --git a/docs/README.md b/docs/README.md index 080b86b..3990a7a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -181,8 +181,9 @@ In this example, the incoming request `http://example.tld/app1/foo` will be prox ### 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 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 for filename sanitization purposes. + +The used filename is `${VIRTUAL_HOST}_${PATH_HASH}_location`, or when `VIRTUAL_HOST` is a regex, `${VIRTUAL_HOST_HASH}_${PATH_HASH}_location`. The filename of the previous example would be `example.tld_8610f6c344b4096614eab6e09d58885349f42faf_location`. @@ -328,7 +329,7 @@ See the [nginx keepalive documentation](https://nginx.org/en/docs/http/ngx_http_ ## Basic Authentication Support -In order to be able to secure your virtual host, you have to create a file named as its equivalent `VIRTUAL_HOST` variable in directory +In order to be able to secure your virtual host, you have to create a file named as its equivalent `VIRTUAL_HOST` variable (or if using a regex `VIRTUAL_HOST`, as the sha1 hash of the regex) in directory `/etc/nginx/htpasswd/{$VIRTUAL_HOST}` ```console @@ -738,7 +739,7 @@ docker run -d -p 80:80 -p 443:443 -v /path/to/my_proxy.conf:/etc/nginx/conf.d/my ### Per-VIRTUAL_HOST -To add settings on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d`. Unlike in the proxy-wide case, which allows multiple config files with any name ending in `.conf`, the per-`VIRTUAL_HOST` file must be named exactly after the `VIRTUAL_HOST`. +To add settings on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d`. Unlike in the proxy-wide case, which allows multiple config files with any name ending in `.conf`, the per-`VIRTUAL_HOST` file must be named exactly after the `VIRTUAL_HOST`, or if `VIRTUAL_HOST` is a regex, after the sha1 hash of the regex. In order to allow virtual hosts to be dynamically configured as backends are added and removed, it makes the most sense to mount an external directory as `/etc/nginx/vhost.d` as opposed to using derived images or mounting individual configuration files. @@ -762,7 +763,7 @@ If you want most of your virtual hosts to use a default single configuration and ### Per-VIRTUAL_HOST location configuration -To add settings to the "location" block on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d` just like the previous section except with the suffix `_location`. +To add settings to the "location" block on a per-`VIRTUAL_HOST` basis, add your configuration file under `/etc/nginx/vhost.d` just like the per-`VIRTUAL_HOST` section except with the suffix `_location` (like this section, if your `VIRTUAl_HOST` is a regex, use the sha1 hash of the regex instead, with the suffix `_location` appended). For example, if you have a virtual host named `app.example.com` and you have configured a proxy_cache `my-cache` in another custom file, you could tell it to use a proxy cache as follows: @@ -790,7 +791,7 @@ The `${VIRTUAL_HOST}_${PATH_HASH}_location`, `${VIRTUAL_HOST}_location`, and `de /etc/nginx/vhost.d/${VIRTUAL_HOST}_${PATH_HASH}_location_override ``` -where `${VIRTUAL_HOST}` is the name of the virtual host (the `VIRTUAL_HOST` environment variable) and `${PATH_HASH}` is the SHA-1 hash of the path, as [described above](#per-virtual_path-location-configuration). +where `${VIRTUAL_HOST}` is the name of the virtual host (the `VIRTUAL_HOST` environment variable), or the sha1 hash of `VIRTUAL_HOST` when it's a regex, and `${PATH_HASH}` is the SHA-1 hash of the path, as [described above](#per-virtual_path-location-configuration). For convenience, the `_${PATH_HASH}` part can be omitted if the path is `/`: diff --git a/nginx.tmpl b/nginx.tmpl index 59e3573..2348ea7 100644 --- a/nginx.tmpl +++ b/nginx.tmpl @@ -289,7 +289,7 @@ auth_basic "Restricted {{ .Host }}{{ .Path }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s_%s" .Host (sha1 .Path)) }}; {{- else if (exists (printf "/etc/nginx/htpasswd/%s" .Host)) }} - auth_basic "Restricted {{ .Host }}"; + auth_basic "Restricted {{ .HostIsRegexp | ternary "access" .Host }}"; auth_basic_user_file {{ (printf "/etc/nginx/htpasswd/%s" .Host) }}; {{- end }} @@ -570,7 +570,9 @@ proxy_set_header Proxy ""; {{- /* Loop over $globals.vhosts and update it with the remaining informations about each vhost. */}} {{- range $hostname, $vhost_data := $globals.vhosts }} + {{- $is_regexp := hasPrefix "~" $hostname }} {{- $vhost_containers := list }} + {{- range $path, $vpath_data := $vhost_data.paths }} {{- $vpath_containers := list }} {{- range $port, $vport_containers := $vpath_data.ports }} @@ -644,6 +646,7 @@ proxy_set_header Proxy ""; "https_method" $https_method "http2_enabled" $http2_enabled "http3_enabled" $http3_enabled + "is_regexp" $is_regexp "acme_http_challenge_legacy" $acme_http_challenge_legacy "acme_http_challenge_enabled" $acme_http_challenge_enabled "server_tokens" $server_tokens @@ -785,6 +788,23 @@ server { {{- end }} server { + {{- if $vhost.is_regexp }} + {{- if or + (printf "/etc/nginx/vhost.d/%s" $hostname | exists) + (printf "/etc/nginx/vhost.d/%s_location" $hostname | exists) + (printf "/etc/nginx/vhost.d/%s_location_override" $hostname | exists) + (printf "/etc/nginx/htpasswd/%s" $hostname | exists) + }} + # https://github.com/nginx-proxy/nginx-proxy/issues/2529#issuecomment-2437609249 + # Support for vhost config file(s) named like a regexp ({{ $hostname }}) has been removed from nginx-proxy. + # Please name your vhost config file(s) with the sha1 of the regexp instead ({{ $hostname }} -> {{ sha1 $hostname }}) : + # - /etc/nginx/vhost.d/{{ sha1 $hostname }} + # - /etc/nginx/vhost.d/{{ sha1 $hostname }}_location + # - /etc/nginx/vhost.d/{{ sha1 $hostname }}_location_override + # - /etc/nginx/htpasswd/{{ sha1 $hostname }} + {{- end }} + {{- end }} + server_name {{ $hostname }}; {{- if $vhost.server_tokens }} server_tokens {{ $vhost.server_tokens }}; @@ -865,8 +885,10 @@ server { {{- end }} {{- end }} - {{- if (exists (printf "/etc/nginx/vhost.d/%s" $hostname)) }} - include {{ printf "/etc/nginx/vhost.d/%s" $hostname }}; + {{- $vhostFileName := $vhost.is_regexp | ternary (sha1 $hostname) $hostname }} + + {{- if (exists (printf "/etc/nginx/vhost.d/%s" $vhostFileName)) }} + include {{ printf "/etc/nginx/vhost.d/%s" $vhostFileName }}; {{- else if (exists "/etc/nginx/vhost.d/default") }} include /etc/nginx/vhost.d/default; {{- end }} @@ -874,7 +896,8 @@ server { {{- range $path, $vpath := $vhost.paths }} {{- template "location" (dict "Path" $path - "Host" $hostname + "Host" $vhostFileName + "HostIsRegexp" $vhost.is_regexp "VhostRoot" $vhost.vhost_root "VPath" $vpath ) }} diff --git a/test/test_custom/my_custom_proxy_settings.conf b/test/test_custom/my_custom_proxy_settings_f00.conf similarity index 100% rename from test/test_custom/my_custom_proxy_settings.conf rename to test/test_custom/my_custom_proxy_settings_f00.conf diff --git a/test/test_custom/test_defaults-location.yml b/test/test_custom/test_defaults-location.yml index 9a3ab44..6e89650 100644 --- a/test/test_custom/test_defaults-location.yml +++ b/test/test_custom/test_defaults-location.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/default_location:ro + - ./my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/default_location:ro - ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/web3.nginx-proxy.example_location:ro web1: diff --git a/test/test_custom/test_defaults.yml b/test/test_custom/test_defaults.yml index d6a959a..2f25387 100644 --- a/test/test_custom/test_defaults.yml +++ b/test/test_custom/test_defaults.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./my_custom_proxy_settings.conf:/etc/nginx/proxy.conf:ro + - ./my_custom_proxy_settings_f00.conf:/etc/nginx/proxy.conf:ro web1: image: web diff --git a/test/test_custom/test_location-per-vhost.py b/test/test_custom/test_location-per-vhost.py index 53a146b..8218ed0 100644 --- a/test/test_custom/test_location-per-vhost.py +++ b/test/test_custom/test_location-per-vhost.py @@ -12,6 +12,13 @@ def test_custom_conf_applies_to_web1(docker_compose, nginxproxy): assert "X-test" in r.headers assert "f00" == r.headers["X-test"] +def test_custom_conf_applies_to_regex(docker_compose, nginxproxy): + r = nginxproxy.get("http://regex.foo.nginx-proxy.example/port") + assert r.status_code == 200 + assert r.text == "answer from port 83\n" + assert "X-test" in r.headers + assert "bar" == r.headers["X-test"] + def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy): r = nginxproxy.get("http://web2.nginx-proxy.example/port") assert r.status_code == 200 diff --git a/test/test_custom/test_location-per-vhost.yml b/test/test_custom/test_location-per-vhost.yml index 5294308..71e606a 100644 --- a/test/test_custom/test_location-per-vhost.yml +++ b/test/test_custom/test_location-per-vhost.yml @@ -5,7 +5,8 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example_location:ro + - ./my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example_location:ro + - ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/561032515ede3ab3a015edfb244608b72409c430_location:ro web1: image: web @@ -22,3 +23,11 @@ services: environment: WEB_PORTS: 82 VIRTUAL_HOST: web2.nginx-proxy.example + + regex: + image: web + expose: + - "83" + environment: + WEB_PORTS: 83 + VIRTUAL_HOST: ~^regex.*\.nginx-proxy\.example$ diff --git a/test/test_custom/test_per-vhost.py b/test/test_custom/test_per-vhost.py index 6a85e69..7394472 100644 --- a/test/test_custom/test_per-vhost.py +++ b/test/test_custom/test_per-vhost.py @@ -12,6 +12,13 @@ def test_custom_conf_applies_to_web1(docker_compose, nginxproxy): assert "X-test" in r.headers assert "f00" == r.headers["X-test"] +def test_custom_conf_applies_to_regex(docker_compose, nginxproxy): + r = nginxproxy.get("http://regex.foo.nginx-proxy.example/port") + assert r.status_code == 200 + assert r.text == "answer from port 83\n" + assert "X-test" in r.headers + assert "bar" == r.headers["X-test"] + def test_custom_conf_does_not_apply_to_web2(docker_compose, nginxproxy): r = nginxproxy.get("http://web2.nginx-proxy.example/port") assert r.status_code == 200 diff --git a/test/test_custom/test_per-vhost.yml b/test/test_custom/test_per-vhost.yml index 63d33b2..0795cef 100644 --- a/test/test_custom/test_per-vhost.yml +++ b/test/test_custom/test_per-vhost.yml @@ -5,7 +5,8 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./my_custom_proxy_settings.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example:ro + - ./my_custom_proxy_settings_f00.conf:/etc/nginx/vhost.d/web1.nginx-proxy.example:ro + - ./my_custom_proxy_settings_bar.conf:/etc/nginx/vhost.d/561032515ede3ab3a015edfb244608b72409c430:ro web1: image: web @@ -22,3 +23,11 @@ services: environment: WEB_PORTS: 82 VIRTUAL_HOST: web2.nginx-proxy.example + + regex: + image: web + expose: + - "83" + environment: + WEB_PORTS: 83 + VIRTUAL_HOST: ~^regex.*\.nginx-proxy\.example$ diff --git a/test/test_custom/test_proxy-wide.yml b/test/test_custom/test_proxy-wide.yml index 1322bcd..22e5c18 100644 --- a/test/test_custom/test_proxy-wide.yml +++ b/test/test_custom/test_proxy-wide.yml @@ -5,7 +5,7 @@ services: image: nginxproxy/nginx-proxy:test volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - - ./my_custom_proxy_settings.conf:/etc/nginx/conf.d/my_custom_proxy_settings.conf:ro + - ./my_custom_proxy_settings_f00.conf:/etc/nginx/conf.d/my_custom_proxy_settings_f00.conf:ro web1: image: web diff --git a/test/test_htpasswd/htpasswd/561032515ede3ab3a015edfb244608b72409c430 b/test/test_htpasswd/htpasswd/561032515ede3ab3a015edfb244608b72409c430 new file mode 100644 index 0000000..336275a --- /dev/null +++ b/test/test_htpasswd/htpasswd/561032515ede3ab3a015edfb244608b72409c430 @@ -0,0 +1 @@ +vhost:$2a$13$/aPYmoK0mmgyAI4TpKdFY.6441Ugo39MdXjhpm.Pp6D15rbz9tvz. diff --git a/test/test_htpasswd/test_htpasswd_regex_virtual_host.py b/test/test_htpasswd/test_htpasswd_regex_virtual_host.py new file mode 100644 index 0000000..1b169d0 --- /dev/null +++ b/test/test_htpasswd/test_htpasswd_regex_virtual_host.py @@ -0,0 +1,13 @@ +import pytest + +def test_htpasswd_regex_virtual_host_is_restricted(docker_compose, nginxproxy): + r = nginxproxy.get("http://regex.htpasswd.nginx-proxy.example/port") + assert r.status_code == 401 + assert "WWW-Authenticate" in r.headers + assert r.headers["WWW-Authenticate"] == 'Basic realm="Restricted access"' + + +def test_htpasswd_regex_virtual_host_basic_auth(docker_compose, nginxproxy): + r = nginxproxy.get("http://regex.htpasswd.nginx-proxy.example/port", auth=("vhost", "password")) + assert r.status_code == 200 + assert r.text == "answer from port 80\n" diff --git a/test/test_htpasswd/test_htpasswd_regex_virtual_host.yml b/test/test_htpasswd/test_htpasswd_regex_virtual_host.yml new file mode 100644 index 0000000..7f0d1bc --- /dev/null +++ b/test/test_htpasswd/test_htpasswd_regex_virtual_host.yml @@ -0,0 +1,17 @@ +version: "2" + +services: + regex: + image: web + expose: + - "80" + environment: + WEB_PORTS: 80 + VIRTUAL_HOST: ~^regex.*\.nginx-proxy\.example$ + + sut: + container_name: sut + image: nginxproxy/nginx-proxy:test + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./htpasswd:/etc/nginx/htpasswd:ro