diff --git a/module.nix b/module.nix index cf6866d..1f3a9eb 100644 --- a/module.nix +++ b/module.nix @@ -98,6 +98,52 @@ let 'map(.["trigger-rule"].match.secret = $s)' \ ${hooksTemplate} > /run/fdroid-webhook/hooks.json ''; + # Served as a Caddyfile.d fragment rather than a virtualHosts entry + # because bucur's Caddyfile is a hand-written file, not the NixOS + # module's attrset. Hoisted into a binding so reloadTriggers below can + # depend on its content. + caddyFragment = '' + ${cfg.domain} { + handle /hooks/* { + reverse_proxy localhost:${toString cfg.webhookPort} + } + + # F-Droid's convention is /fdroid/repo, and the fingerprint + # clients pin is tied to that exact address via the signed index, + # so it is not a path that can be tidied up later. + # + # Rooted at repo/ rather than at the state directory with /fdroid + # stripped, which would have served the whole fdroidserver working + # tree: config.yml and metadata/ are not secret, but they are not + # part of the repo and nothing should be able to fetch them. + redir /fdroid/repo /fdroid/repo/ permanent + handle_path /fdroid/repo/* { + root * ${cfg.stateDir}/repo + # No `browse`: repo/index.html is the landing page, and a + # directory listing for everything else only advertises paths + # that no client needs to discover. + file_server + } + + # `fdroid update` writes repo/index.html: the repo's own landing + # page, listing every app and carrying the QR code that adds the + # repo to a phone. Better than anything hand-written here, and it + # can never fall out of step with the index. + redir / /fdroid/repo/ permanent + + # Caddy answers a request that matched no route with an empty 200, so + # without this every stray path — /config.yml included — looks like a + # file that exists and happens to be empty. Nothing is served either + # way, but "not found" is the honest answer and the one a client can + # act on. + handle { + respond 404 + } + + encode zstd gzip + } + ''; + in { options.services.fdroidRepo = { @@ -272,38 +318,16 @@ in }; environment.etc = lib.mkIf cfg.serveCaddy { - "caddy/Caddyfile.d/fdroid.caddyfile".text = '' - ${cfg.domain} { - handle /hooks/* { - reverse_proxy localhost:${toString cfg.webhookPort} - } - - # F-Droid's convention is /fdroid/repo, and the fingerprint - # clients pin is tied to that exact address via the signed index, - # so it is not a path that can be tidied up later. - # - # Rooted at repo/ rather than at the state directory with /fdroid - # stripped, which would have served the whole fdroidserver working - # tree: config.yml and metadata/ are not secret, but they are not - # part of the repo and nothing should be able to fetch them. - redir /fdroid/repo /fdroid/repo/ permanent - handle_path /fdroid/repo/* { - root * ${cfg.stateDir}/repo - # No `browse`: repo/index.html is the landing page, and a - # directory listing for everything else only advertises paths - # that no client needs to discover. - file_server - } - - # `fdroid update` writes repo/index.html: the repo's own landing - # page, listing every app and carrying the QR code that adds the - # repo to a phone. Better than anything hand-written here, and it - # can never fall out of step with the index. - redir / /fdroid/repo/ permanent - - encode zstd gzip - } - ''; + "caddy/Caddyfile.d/fdroid.caddyfile".text = caddyFragment; }; + + # Caddy glob-imports Caddyfile.d/ once, at startup. Its own unit does not + # change when a fragment does, so NixOS leaves it running and the vhost + # silently does not exist — no certificate, no listener, nothing in the log + # to say why, until something unrelated happens to restart it. Which is + # exactly what happened on the first deploy of this module. + systemd.services.caddy.reloadTriggers = + lib.mkIf cfg.serveCaddy [ caddyFragment ]; }; } +