Serve static files matching string from domain

1. Output of caddy version:

v2.5.0 h1:eRHzZ4l3X6Ag3kUt8nj5IxATprhqKq/wToP7OHlXWA0=

2. How I run Caddy:

a. System environment:

Docker caddy:2.5.0-alpine

b. Command:

docker run -d -p 8021:80 [mycaddycontainer]:latest

d. My complete Caddy config:

:80 {
	# Set this path to your site's directory.
	root * /usr/share/caddy/myapp-desktop

	# Enable the static file server.
	file_server
}

:8080 {
	root * /usr/share/caddy/myapp-mobile
	file_server
}

3. The problem I’m having:

The previous Caddyfile works to test my app’s mobile and desktop versions based on two different ports.
The two paths contain static files previously created by Angular.
My question: I would like now to achieve the same based on a string in the domain (not port-based anymore), like as follows:

[long-string]-desktop.myappdomain.com {
	root * /usr/share/caddy/myapp-desktop  ##folder with Angular compiled files
	file_server
}

[long-string]-mobile.myappdomain.com  {
	root * /usr/share/caddy/myapp-mobile    #folder with Angular compiled files
	file_server
}

I guess the regex for the domain should be tested to Go’s standard, which I could do. But I have no idea on how to accomplish two different folders containing static files being served by Caddy depending on a string in the domain. Is this even possible?

Any clue would be greatly appreciated.

Please upgrade to v2.5.2. There were some significant bug fixes.

I’m not sure I understand. Do you have an unlimited number of these “long strings” in your domain that you want to serve? Why?

If you want Caddy to serve HTTPS for those domains, you need a certificate that matches that domain. If it’s not always the same domain, then that significantly complicates things.

There’s ways to work around it, but it really depends on what your requirements are.

Please be more specific about your setup, and we can give a more specific answer in turn.

1 Like

Hi and thanks for your quick reply. Those strings are based on the name of the feature I am testing. So every time I create a new feature, it gets exposed to a URL like the following:

my-new-future-mobile.myappdomain.com
and
my-new-future-desktop.myappdomain.com

Once I am done testing, I remove the whole thing. The most unknown part for me is to serve those static files from two different sources:

[long-string]-desktop.myappdomain.com {
	root * /usr/share/caddy/myapp-desktop  ##folder with Angular compiled files
	file_server
}

[long-string]-mobile.myappdomain.com  {
	root * /usr/share/caddy/myapp-mobile    #folder with Angular compiled files
	file_server
}

I would appreciate any help.

Would something like this work for you?

*.example.com {
  tls {
    dns [provider]
  }
  root * /usr/share/caddy
  rewrite * /{labels.2}{uri}
  file_server
}

Requests for myapp-desktop.example.com are served from a wildcard certificate (requires DNS validation for automation) and rewritten to be served from /usr/share/caddy/myapp-desktop/ on disk.

Likewise requests for long-string-mobile.example.com are served from /usr/share/caddy/long-string-mobile/.

The benefit to this setup is no changes to Caddyfile config when prototyping features; requests for nonexistent features on disk result in 404s. You simply put the files in /usr/share/caddy/ when you’re testing and remove them when you’re done, naming them appropriately and then accessing by the same name you use for the directory containing the files.

For reference, I used a very similar trick back in the day (for Caddy v1!) for the purposes of local web development: [Guide] Local web development setup on a Mac (Caddy v1)

2 Likes

Aaaaactually, maybe not… we can try to hack on_demand_tls to test a local file server to determine if we should get a certificate on demand for a given feature only if it exists on disk, knowing that file_server returns 200 only when it finds an existing file.

We’ll need a few sites; ask will query the first site, which will be a local only HTTP listener whose only job is to translate the URI query coming from ask into a full blown host-based request for our second site.

Thus, requests from ask and requests to the external site will reference the same file server, so we know for sure that ask will only procure certs for the features you’ve actually put the files on disk for. We can add the external HTTP(S) hosts to the file server site, with on demand TLS enabled for those HTTPS requests.

{
  on_demand_tls {
    ask localhost:8080
    interval 2m
    burst 5
  }
}

# Translates ask requests to file server
http://localhost:8080 {
  reverse_proxy localhost:8081 {
    header_up Host {http.request.uri.query.domain}
  }
}

# File server with local listener for pre-TLS ask
http://*.example.com:8081, *.example.com {
  tls {
    on_demand
  }
  root * /usr/share/caddy
  rewrite * /{labels.2}{uri}
  file_server
}

The first request will result in a double request to the file server; the ‘preflight’-style ask, followed by a request via the actual external HTTPS site itself after the TLS validation is complete, but given that this will only occur whenever Caddy needs to requisition (or renew) the cert for this site, and I assume the scale of these feature tests will be quite manageable, I consider this quite reasonable.

Of note is that another LAN actor could technically hit localhost:8080 with queries including bad domains, in order to cause bad behavior in ask which will then proceed because Caddy responds with 200s by default to unconfigured requests, which is very hard for us to mitigate. However, the damage in this case is limited by the fact that it requires a bad actor on LAN or misconfigured firewall/external networking, and also mitigated by interval 2m and burst 5, so we at least won’t demolish the poor ACME servers if we’re taken advantage of. The interval and burst can be brought way down depending on, well, how often you deploy new features to test.

This is all extremely overengineered if you can just get DNS validation for a wildcard cert, but… I had the idea and wanted to explore it, so. ¯\_(ツ)_/¯

1 Like

Hi @Whitestrake, quite interesting post. Caddy is definitely powerful. I ended up doing the following for now:

:80 {
        @hostdesktop header_regexp host Host .*-desktop.myappdomain.com
        @hostmobile header_regexp host Host .*-mobile.myappdomain.com

        handle @hostdesktop {
                root * /usr/share/caddy/myapp-desktop
                file_server
        }
        handle @hostmobile {
                root * /usr/share/caddy/myapp-mobile
                file_server
        }
}

One thing, when you say the following would rewrite and enable to be served from " /usr/share/caddy/myapp-desktop/":

  root * /usr/share/caddy
  rewrite * /{labels.2}{uri}

Could you explain a little bit the magic behind it?

Thanks for the valuable info.

Sure thing!

I would not recommend this with :80 at all. The version I wrote is highly dependent on the *.example.com site label for security reasons.

Here’s why: the {labels.N} placeholder reads, from right-to-left, the elements of the requested Host.

{http.request.host.labels.*}
Request host labels (0-based from right); e.g. for foo.example.com: 0=com, 1=example, 2=foo

JSON Config Structure - Caddy Documentation

(And then we use the placeholder shorthand.)

That means, for *.example.com, {labels.2} will expand to the value of *. For a request to some-feature-12345.example.com/some-path/index.html that means the rewrite will produce /some-feature-12345/some-path/index.html.

Combined with the directory root /usr/share/caddy (P.S.: We strongly advise you use /srv or some other conventional path instead, as the Caddy package owns this directory and could cause some issues for you!), the file server looks for /usr/share/caddy/some-feature-12345/some-path/index.html for this request and serves it if it exists.

So, essentially, to add or remove features being served by Caddy, you just mkdir some-feature-12345-desktop some-feature-12345-mobile and then put the site files inside and you’re off to the races.

Having the site label explicitly match the host *.example.com ensures that you will always have three labels in your Host for this site; this prevents cases where a request has no 3rd label, which would produce an empty string for {labels.2}, which would collapse to // and then be sanitized to /. This would enable someone to walk the root directory, which may be undesirable (but may also be fine, since every directory within will be accessible if they know the name of the directory in advance and request the appropriate subdomain).

2 Likes

Amazing. Thanks for your useful information. Everything makes sense now.
I will apply now the security tips.
Thanks @Whitestrake and @francislavoie for your help.

2 Likes

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