Unable to use map variables with url strip_prefix

1. The problem I’m having:

I have a caddyfile to manages a bunch of subdomains and have worked out a map directive that works for me, to map hosts to backends.
I am needed to add a new backend that requires uri strip_prefix /alexa to work. It is possible that in the future I might have another backend that needs uri strip_prefix /something_else. So I want to add this as an optional map variable. But it doesn’t seem to work.

Request I am making, with the output (from http-echo server mendhak/http-https-echo):

curl -vL https://dorewrite.realsam.co/alexa/hello
*   Trying 38.68.134.37:443...
* TCP_NODELAY set
* Connected to dorewrite.realsam.co (38.68.134.37) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: /etc/ssl/certs
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=realsam.co
*  start date: Oct 22 01:16:02 2024 GMT
*  expire date: Jan 20 01:16:01 2025 GMT
*  subjectAltName: host "dorewrite.realsam.co" matched cert's "*.realsam.co"
*  issuer: C=US; O=Let's Encrypt; CN=E6
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x556877c600e0)
> GET /alexa/hello HTTP/2
> Host: dorewrite.realsam.co
> user-agent: curl/7.68.0
> accept: */*
>
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
< HTTP/2 200
< alt-svc: h3=":443"; ma=2592000
< content-type: application/json; charset=utf-8
< date: Thu, 24 Oct 2024 03:39:19 GMT
< etag: W/"257-wixQIzjyKMDXL23PdyEMjJo8TZA"
< server: Caddy
< x-powered-by: Express
< content-length: 599
<
{
  "path": "/alexa/hello",
  "headers": {
    "host": "dorewrite.realsam.co",
    "user-agent": "curl/7.68.0",
    "accept": "*/*",
    "x-forwarded-for": "202.144.161.92",
    "x-forwarded-host": "dorewrite.realsam.co",
    "x-forwarded-proto": "https",
    "accept-encoding": "gzip"
  },
  "method": "GET",
  "body": "",
  "fresh": false,
  "hostname": "dorewrite.realsam.co",
  "ip": "202.144.161.92",
  "ips": [
    "202.144.161.92"
  ],
  "protocol": "https",
  "query": {},
  "subdomains": [
    "dorewrite"
  ],
  "xhr": false,
  "os": {
    "hostname": "http-echo"
  },
  "connection": {}
* Connection #0 to host dorewrite.realsam.co left intact
}%

I am expecting to see:

{
  "path": "/hello",
   ...
}

2. Error messages and/or full log output:

Nothing wrong seen in caddy debug logs:

{"level":"info","ts":1729741151.2381005,"logger":"admin.api","msg":"received request","method":"POST","host":"localhost:2019","uri":"/load","remote_ip":"127.0.0.1","remote_port":"59950","headers":{"Accept-Encoding":["gzip"],"Content-Length":["3364"],"Content-Type":["application/json"],"Origin":["http://localhost:2019"],"User-Agent":["Go-http-client/1.1"]}}
{"level":"info","ts":1729741151.238755,"msg":"config is unchanged"}
{"level":"info","ts":1729741151.2387686,"logger":"admin.api","msg":"load complete"}
{"level":"debug","ts":1729741159.3723786,"logger":"events","msg":"event","name":"tls_get_certificate","id":"5682b465-5d6a-46a0-b198-6e6d4857ae46","origin":"tls","data":{"client_hello":{"CipherSuites":[4866,4867,4865,49196,49200,159,52393,52392,52394,49195,49199,158,49188,49192,107,49187,49191,103,49162,49172,57,49161,49171,51,157,156,61,60,53,47,255],"ServerName":"dorewrite.realsam.co","SupportedCurves":[29,23,30,25,24],"SupportedPoints":"AAEC","SignatureSchemes":[1027,1283,1539,2055,2056,2057,2058,2059,2052,2053,2054,1025,1281,1537,771,769,770,1026,1282,1538],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"202.144.161.92","Port":30792,"Zone":""},"LocalAddr":{"IP":"10.62.191.2","Port":443,"Zone":""}}}}
{"level":"debug","ts":1729741159.3724205,"logger":"tls.handshake","msg":"no matching certificate; will choose from all certificates","identifier":"dorewrite.realsam.co"}
{"level":"debug","ts":1729741159.3724356,"logger":"tls.handshake","msg":"choosing certificate","identifier":"dorewrite.realsam.co","num_choices":2}
{"level":"debug","ts":1729741159.3725164,"logger":"tls.handshake","msg":"custom certificate selection results","identifier":"dorewrite.realsam.co","subjects":["realsam.co","*.nlsspeaker.realsam.co","*.realsam.co"],"managed":false,"issuer_key":"","hash":"fb5c3f239850649a597ca3a211d2bb54bab8bdabb35c4f526929772ba926d390"}
{"level":"debug","ts":1729741159.3725343,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"202.144.161.92","remote_port":"30792","subjects":["realsam.co","*.nlsspeaker.realsam.co","*.realsam.co"],"managed":false,"expiration":1737335762,"hash":"fb5c3f239850649a597ca3a211d2bb54bab8bdabb35c4f526929772ba926d390"}
{"level":"debug","ts":1729741159.5844364,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"http-echo:8080","total_upstreams":1}
{"level":"debug","ts":1729741159.5920455,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"{backend}","duration":0.007529887,"request":{"remote_ip":"202.144.161.92","remote_port":"30792","client_ip":"202.144.161.92","proto":"HTTP/2.0","method":"GET","host":"dorewrite.realsam.co","uri":"/alexa/hello","headers":{"User-Agent":["curl/7.68.0"],"Accept":["*/*"],"X-Forwarded-For":["202.144.161.92"],"X-Forwarded-Proto":["https"],"X-Forwarded-Host":["dorewrite.realsam.co"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"dorewrite.realsam.co"}},"headers":{"Content-Type":["application/json; charset=utf-8"],"Content-Length":["599"],"Etag":["W/\"257-wixQIzjyKMDXL23PdyEMjJo8TZA\""],"Date":["Thu, 24 Oct 2024 03:39:19 GMT"],"Connection":["keep-alive"],"Keep-Alive":["timeout=5"],"X-Powered-By":["Express"]},"status":200}

3. Caddy version:

root@us-docker3:~/docker-servers#  docker exec ds-caddy caddy version
v2.8.4 h1:q3pe0wpBj1OcHFZ3n/1nl4V4bxBrYoSoab7rL9BMYNk=

4. How I installed and ran Caddy:

Docker Compose

a. System environment:

.env:

NET_DOT=10.62.191
NET_DASH=10-62-191
MYIP_DASH=38-68-134-37

OS: Ubuntu 22.04.2 LTS
Arch: x64
Docker Version:

root@us-docker3:~/docker-servers# docker version
Client: Docker Engine - Community
 Version:           24.0.4
 API version:       1.43
 Go version:        go1.20.5
 Git commit:        3713ee1
 Built:             Fri Jul  7 14:50:55 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.4
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.5
  Git commit:       4ffc614
  Built:            Fri Jul  7 14:50:55 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

b. Command:

root@us-docker3:~/docker-servers#  docker exec -it ds-caddy ps
PID   USER     TIME  COMMAND
    1 root      1:19 caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

To reload my config, I use:

root@us-docker3:~/docker-servers# docker exec -it ds-caddy caddy reload --config /etc/caddy/Caddyfile
2024/10/24 03:52:46.908 INFO    using config from file  {"file": "/etc/caddy/Caddyfile"}
2024/10/24 03:52:46.913 INFO    adapted config to JSON  {"adapter": "caddyfile"}
2024/10/24 03:52:46.913 WARN    Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix inconsistencies    {"adapter": "caddyfile", "file": "/etc/caddy/Caddyfile", "line": 8}

c. Service/unit/compose file:

services:
  ds-caddy:
    image: caddy:2-alpine
    container_name: ds-caddy
    hostname: ds-caddy
    restart: on-failure
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./vols/caddy_data:/data
      - ./vols/caddy_config:/config
      - /root/sslcerts:/root/sslcerts
    environment:
        - NET_DASH=${NET_DASH}
        - NET_DOT=${NET_DOT}
        - MYIP_DASH=${MYIP_DASH}
    networks:
      ds-net:
        ipv4_address: ${NET_DOT}.2

d. My complete Caddy config:

I haven’t run caddy fmt because it gets rid of the spacing in the map directive.

Pls see: ## NEW SERVICE TO ADD

{
	debug
}

*.realsam.co *.*.realsam.co {
	tls /root/sslcerts/realsam.co/caddy/realsam_co_cert.pem /root/sslcerts/realsam.co/caddy/realsam_co_key.pem

	map "{labels.3}.{labels.2}"         {backend}                       {slash_redir}   {strip_alexa} {
		## NEW SERVICE TO ADD
		.dorewrite                       http-echo:8080                  -               /alexa

		# all DS
		~.ds-{$NET_DASH}-([3456789])$   "{$NET_DOT}.${1}:1900${1}"
		~.ds-{$NET_DASH}-([1-9][0-9])$  "{$NET_DOT}.${1}:190${1}"

		# streamers under our control, match st-(name) with streamer-(name)
		~.st-([a-zA-Z]*)-{$MYIP_DASH}$  "streamer-${1}:32003"

		# webhooks - preset
		.w1-mtb-alexa                   webhook-mytalkingbooks:32001
		.realpaolo                      webhook-nlspaolo:32001
		realbooks.nlsspeaker            webhook-realbooks:32001
		develop.nlsspeaker              webhook-qa:32001

		# authproxy - preset
		.auth-mtb-nlsspeaker            authproxy-mytalkingbooks:7779
		.mytalkingbookslogin            authproxy-mytalkingbooks:7779   webcontact
		.auth-smrtdev                   authproxy-smrtdev:7779
		.nlsloginsmrtdev                authproxy-smrtdev:7779          webcontact
		.auth-realbooks-nlsspeaker      authproxy-realbooks:7779
		.realbookslogin                 authproxy-realbooks:7779        webcontact
		.auth-nlsspeaker                authproxy-nls:7779
		.nlslogin                       authproxy-nls:7779              webcontact

		# default
		default - - -
	}

	@slash_redir_set not vars {slash_redir} -
	handle @slash_redir_set {
		rewrite /  /{slash_redir}
		rewrite /*  /{slash_redir}{path}?{query}
		reverse_proxy {backend}
	}

	@slash_strip_alexa_set not vars {strip_alexa} -
	handle @slash_strip_alexa_set {
		uri strip_prefix {strip_alexa}
		reverse_proxy {backend}
		# reverse_proxy http-echo:8080
	}

	@backend_set not vars {backend} -
	handle @backend_set {
		reverse_proxy {backend}
	}

	## Everything else
	handle {
		respond "{host} Not Found!" 404
	}
}

:443 {
	respond "Bad Domain {host}!" 404
}

5. Links to relevant resources:

6. Other questions/thoughts:

Any suggestions to improve this caddyfile are highly appereciated!
I am sure there are better ways to do things, I would love to know more, as long as I get to keep the same behaviour.

Ah, I see a bug. Basically, the Caddyfile adapter is checking whether the strip_prefix argument has a / and if it doesn’t it adds one. So this results in the argument being /{strip_alexa} (you can see by running caddy adapt -p). So ultimately it tries to strip //alexa or whatever which fails.

Workaround for now, just use alexa as your map value and it’ll be fine because the / will be added by the Caddyfile adapter.

The slash should be added at runtime after the replacer is run, not at Caddyfile adapt time, I think. I’ll fix that for next release. rewrite: Don't add `/` in Caddyfile, do it after replacer by francislavoie · Pull Request #6662 · caddyserver/caddy · GitHub

1 Like

That was fast, thanks a lot!

I have made it uri strip_prefix /{strip_alexa} to be safe with upgrades, considering it will be fixed in a later release ?

While we are here, is it possible to also have a look at what happens with the handle_path side of things ? It gave me an error, but prepending / didn’t help.
For ref:

	@slash_strip_alexa_set not vars {strip_alexa} -
	handle @slash_strip_alexa_set {
		handle_path /{strip_alexa} {
			reverse_proxy {backend}
		}
		# uri strip_prefix /{strip_alexa}
		# reverse_proxy http-echo:8080
	}

handle_path as a shortcut has to set up both a path matcher and a uri strip_prefix handler. I don’t think it can be smart enough to handle this case. So you’ll just have to use the slightly longer way (1 more line lol) for your case.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.