Force download certain mime types, is there a non-template approach?

Caddy v2.6.2, docker-compose, ubuntu 22.04

Hi. I’m new to Caddy. I’m not a developer nor do I have much CSS or html knowledge however the care, thought and logic that’s gone into building Caddy makes it possible even for people like me (!) to use it. Thanks to all involved!

I’m looking for a simple way to force a one-click download of specific mime types from a page with a directory listing. I’m aware that this can be achieved with file_browser and templates (File_server default template force download) but was wondering whether a more simple approach is possible?

The specific page is located at: /srv/domain/source/versions/public

The relevant Caddyfile entry is:

{   
    root * /srv/domain/source
    file_server
    redir /public /public/
        handle_path /public* {
                root * /srv/domain/source/versions/public
                file_server browse
        }
}

Is there some directive or other syntax I can add under handle_path or file_server perhaps (the ‘commands’ below are pseudo versions) which identifies certain mime-types for one-click downloading:

  • http_header…[???]
  • mime_type…[]
  • etc?

Any suggestions with (easy) examples would be most appreciated.

I think you’re looking to set the Content-Disposition: attachment header.

You can use the header directive to set this. Pair it with a matcher to make it only apply to certain request paths.

Say you want .pdf files to always get downloaded, this should work:

@pdf path *.pdf
header @pdf Content-Disposition attachment

Thanks for getting back to me so quickly.

I put these lines within the handle_path:

redir /public /public/
        handle_path /public* {
                root *  /srv/domain/source/versions/public
                @mp4  /srv/domain/source/versions/public/ *.mp4
                header @mp4 Content-Disposition attachment
                file_server browse
        }

I received the following error message:

{"level":"info","ts":1675392237.6160033,"msg":"using provided configuration","config_file":"/etc/caddy/Caddyfile","config_adapter":"caddyfile"}
Error: adapting config using caddyfile: parsing caddyfile tokens for 'handle_path': getting matcher module '/srv/domain/source/versions/public/: module not registered: http.matchers./srv/domain/source/versions/public/

I removed the named matcher (putting that problem aside for a moment) and tried this to see if a download would be forced:

redir /pubfiles /pubfiles/
        handle_path /pubfiles* {
                root *  /srv/domain/source/versions/public
                header  /srv/domain/source/versions/public/ *.mp4 Content-Disposition attachment
                file_server browse
        }

The error went away but the browser streamed the file rather than downloaded it. I moved the header directive to outside handle_path and also ran a scenario where I made the header path the same as the domain root. In all cases, the file didn’t download. Here’s an excerpt from the access log.

http.log.access.log1    handled request {"request": {"remote_ip": "", "remote_port": "47218", "proto": "HTTP/2.0", "method": "GET", "host": "", "uri": "/pubfiles/1.mp4", "headers": {"X-Forwarded-Proto": ["https"], "Sec-Fetch-Dest": ["document"], "Sec-Fetch-Mode": ["navigate"], "Cf-Ipcountry": [""], "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"], "Upgrade-Insecure-Requests": ["1"], "X-Forwarded-For": [""], "Cf-Ray": ["7937f1018ef57572-LHR"], "Priority": ["u=1"], "User-Agent": ["Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/109.0"], "Accept-Language": ["en-GB,en;q=0.5"], "Dnt": ["1"], "Sec-Fetch-Site": ["same-origin"], "Sec-Gpc": ["1"], "Accept-Encoding": ["gzip"], "Cf-Visitor": ["{\"scheme\":\"https\"}"], "Sec-Fetch-User": ["?1"], "Cookie": [], "Cf-Connecting-Ip": [""], "Cdn-Loop": ["cloudflare"]}, "tls": {"resumed": false, "version": 772, "cipher_suite": 4865, "proto": "h2", "server_name": ""}}, "user_id": "", "duration": 0.223048749, "size": 3971279, "status": 200, "resp_headers": {"X-Frame-Options": ["SAMEORIGIN"], "Content-Length": ["3971279"], "X-Permitted-Cross-Domain-Policies": ["none"], "Alt-Svc": ["h3=\":443\"; ma=2592000"], "Accept-Ranges": ["bytes"], "Server": ["Caddy"], "Content-Security-Policy": ["frame-ancestors 'self'"], "Referrer-Policy": ["no-referrer"], "Content-Type": ["video/mp4"], "Last-Modified": ["Fri, 03 Feb 2023 02:18:48 GMT"], "Permissions-Policy": ["geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"], "X-Robots-Tag": ["none"], "Strict-Transport-Security": ["max-age=630720000;"], "X-Download-Options": ["noopen"], "Etag": ["\"rphfrc2d49b\""], "X-Content-Type-Options": ["nosniff"]}}

I guess, then, why is the file not downloading and what is the matcher module error about?

N.B. As you can, I proxy via Cloudflare but that’s never stopped me getting this working before with different servers.

Please review the request matching syntax:

Don’t use the webroot in the path matcher. And you need to literally use the word path there, because that’s the name of that matcher.

Ok, noted re. ‘path’ and I will review the syntax. Am I correct in placing these lines in the handle_path section?

I fixed the matcher syntax and there’s no error in the logs anymore. The designated files still don’t download though. To refresh, here’s the relevant section from the Caddyfile:

redir /public /public/
        handle_path /public* {
                root * /srv/domain/source/versions/public
                @mp4 {
                        path *.mp4
                }
                header @mp4 Content-Disposition "attachment"
                file_server browse
        }

Any suggestions as to how best debug this?

Another thing you could try is setting the content type to something browsers won’t recognize as something they can display embedded. Try Content-Type application/octet-stream

Hmm, still no change. Anyway, you’ve set me on the right track. I will carry on tinkering and post back here if I get it working. Many thanks.

Change header to

header @mp4 Content-Disposition "attachment; filename=\"{http.request.uri.path.1}\""

You’ll have to place all your videos below the public folder though. In my chrome it prompts me to download the video file.

1 Like

It worked! Any idea why the files have to go in the subfolder? I’m just curious.

Anyway, thank you very much and to @francislavoie also.

UPDATE:

Any idea why the files have to go in the subfolder? I’m just curious.

Using the config text you gave me, I was able to one-click download directly from public without using a subfolder.

1 Like

As far as I know, Content-Dispostion header is usually accomplied by the filename you want it to download by. Putting the files in the public folder is unfortunate as it is a limitation of caddy. It set the filename to the actually filename you want it. Perhaps there is another method involving cel expression to extrace the last path component.

In case it’s helpful, here’s the final working config. To summarise:

  • file listings activated for a specific directory - “/srv/domain/source/versions/public”
  • wish to force ‘one-click’ download for media files in this directory
  • the directory is nested so will also use redir and handle_path to create a shorter url

Caddyfile [EDITED TO REFLECT COMMENTS FROM @francislavoie BELOW]

domain {
	root * /srv/domain
	file_server

	@download {
		path *.mp4 *.avi *.mp3 *.flac
        path_regexp download .*/(.*)$
	}

	redir /public /public/
	handle_path /public* {
		root * /srv/domain/source/versions/public
		header @download Content-Disposition `attachment; filename="{re.download.1}"`
		file_server browse
	}
}

Couple things.

You can avoid escaping the double quotes by using backticks instead:

header @download Content-Disposition `attachment; filename="{http.request.uri.path.1}"`

Also, you can use the path_regexp matcher to grab the last path segment:

	@download {
		path *.mp4 *.avi *.mp3 *.flac
		path_regexp download .*/(.*)$
	}

Then use it later:

header @download Content-Disposition `attachment; filename="{re.download.1}"`

I’m surprised filename would be necessary though. Content-Disposition - HTTP | MDN shows that it should be optional.

1 Like

Thank you. That worked fine. I edited the config in my previous post accordingly.

1 Like

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