Dynamic site root from response header

I have a music streaming application (Koel) which allows several streaming methods. Among them is x-accel-redirect, which utilizes nginx’s X-Accel module. This is how it works:

  1. The application (PHP) sets a custom response header X-Media-Root to the media_path setting value. Note that this value is configured and changeable by the end-user.
    header('X-Media-Root: '.Setting::get('media_path'));
    
  2. A standard X-Accel-Redirect header is also set to the media’s relative path, prefixed by a /media/ root:
    header("X-Accel-Redirect: /media/$relativePath");
    
  3. Nginx is configured to serve /media/ as internal and aliased by the custom X-Media-Root header’s value:
    location /media/ {
      internal;
      alias $upstream_http_x_media_root;
    }
    

This way, a request to the streamer endpoint will be redirected to e.g. /media/foo.mp3 internally, and then served by Nginx from the actual file /real/user-configured/path/foo.mp3 (as far as my understanding goes).

Question: How do I replicate this functionality in Caddy, given its support for X-Accel-Redirect (or x-Sendfile)? I tried:

localhost:8088/media/ {
  root {<X-Media-Root}
}

without success (Caddy responds 200 OK, but the response is empty). I also tried asking on Twitter and got this answer from @matt:

Site roots are static in Caddy 1. You can use a single root that is common to all requests and then rewrite the URI path to a specific sub-path easily enough. Caddy 2 allows dynamic roots.

He then suggested me to forward the question to the forum, so here I am. Any help would be appreciated!

Howdy!

Caddy only supports X-Accel-Redirect when the internal directive is in use. Without it, Caddy does not perform the check for the upstream header and does not re-evaluate the request.

https://caddyserver.com/docs/internal

He’s also not wrong about the static root. Caddy won’t support a dynamic one like that.

I’d suggest you don’t bother with a separate site (i.e. localhost:8088/media/) and just add:

internal /media/

to your Caddyfile for the main site.

Thanks for the answer. As a matter of fact, I forgot to mention that internal /media/ was my first attempt:

localhost:8088 {
  tls self_signed
  root ./

  fastcgi / 127.0.0.1:9000 php {
    index index.php
  }

  rewrite {
    to {path} {path}/ /index.php?{query}
  }
  
  internal /media/
}

The problem with this config (which doesn’t work) is, I couldn’t figure out a way to alias /media/ to the dynamic X-Media-Root header, if that’s still how it should go for Caddy.

This isn’t really possible - the root must be specified in the Caddyfile and can’t really be changed once it’s locked in. This feature (allowing the end-user to change a media path setting) isn’t supported in this manner by Caddy because it doesn’t allow a dynamic site root.

The closest approximation would be to have the site root be a common ancestor of the web root and the media path, and then rewriting requests to the right media path as appropriate.

The closest approximation would be to have the site root be a common ancestor of the web root and the media path, and then rewriting requests to the right media path as appropriate.

This does sound similar to @matt’s answer :slight_smile: so I guess it’s the way to go. I’m still not sure how “having the site root to be the common ancestor” looks exactly though. Could you give an example? Cheers!

For a given public html folder /path/to/website, a common ancestor might be /path.

So you might have root /path, and then rewrite /media requests to {<X-Media-Root}, and rewrite normal requests to /to/website{uri}.

This would result in the end user being able to select any relative path under /path as their “Media Root”, although it would be blind to the /path prefix (i.e. the user could select /to/media as their media root, but Caddy would be looking in /path/to/media).

Actually, this wouldn’t work because the {<Header} placeholder format is for response placeholders and the rewrite code executes before the proxy does, so rewrite has no access to this placeholder coming back from upstream and thus can’t act on it.

I have to say, having the end user configure the folder media is served from, and relying on the web server to do that with dynamic web roots (rather than handling this custom… handling… in PHP?), seems very awkward and clunky to me. If you could set it globally once and configure Caddy to the same path, that’d be fine, but Caddy definitely can’t support allowing you to change it on the fly, not without modifying the Caddyfile and reloading.

Edit: Not even the Apache variant does this kind of dynamic site root stuff, it seems to have the XSendFilePath hard-coded.

Edit 2: So, internal's X-Accel-Redirect support functions by executing the tail of the middleware chain and inspecting the result for the header. If it gets the header, it re-executes the middleware chain with the new location. Because it’s never treated as a new request, it continually executes the middleware chain it was given as part of the original request, I’ve got a feeling that using more than one site definition (i.e. example.com/media with a different root) isn’t going to work at all, because it’s going to be executing the middleware chain where the root is the main website’s root.

Edit 3: The more I look at it, the more ways this kind of logical loop doesn’t really seem possible in Caddy 1. Caddy 2 will almost definitely be able to handle this, though. Although it doesn’t seem very secure to have end users determining arbitrary absolute paths to media to serve. Seems like a great way to get your server pwned in a flash.

1 Like

Thanks for the very thoughtful feedback @Whitestrake! Regarding your concerns:

I have to say, having the end user configure the folder media is served from, and relying on the web server to do that with dynamic web roots (rather than handling this custom… handling… in PHP?), seems very awkward and clunky to me.

I OTOH see this as a convenience. Sure, I can let the user manually edit the config and reload the server every time they modify the media path (XSendFile method works this way actually), but it’s obviously more work to do. Not to mention, such an approach will render a multi-tenant system, where each user can have their own media folder, impossible.

Although it doesn’t seem very secure to have end users determining arbitrary absolute paths to media to serve. Seems like a great way to get your server pwned in a flash.

Koel is a self-hosted solution, so the end-user is responsible for their server, including choosing where to store the media :). Also, only legit audio files are recognized – the client can’t just request an arbitrary file.

With all that said, again, thanks for the great help! I guess I’ll have to go for the hard-coded route for now while waiting for Caddy 2.

1 Like