{ config, lib, pkgs, ... }: let lex_surf = pkgs.callPackage ./default.nix {}; cfg = config.services.lex-surf; in { options.services.lex-surf = { enable = lib.mkEnableOption "lex-surf"; domain = lib.mkOption { type = lib.types.str; description = "Domain under which lex-surf will be served."; }; fetchUser = lib.mkOption { type = lib.types.str; description = "User account used to run lex-fetch."; }; enableACME = lib.mkOption { type = lib.types.bool; description = "Whether to generate certificates."; default = false; }; nginx = let nginxOpts = (import { inherit config lib; }).options; in lib.mkOption { type = lib.types.submodule { options = lib.removeAttrs nginxOpts ["serverName" "useACMEHost" "acmeRoot"]; }; default = {}; }; }; config = lib.mkIf cfg.enable ( let socketPath = "/run/lex-serve/http.sock"; ccTLDs = lib.splitString "\n" (lib.strings.trim (builtins.readFile ./cc-tlds.txt)); chunkList = n: list: if list == [ ] then [ ] else [ (lib.lists.take n list) ] ++ (chunkList n (lib.lists.drop n list)); # Let's Encrypt only supports 100 domains per certificate. # We could also use a wildcard certificate but that requires # the dns-01 challenge which takes more effort to set up. ccChunks = chunkList 100 ccTLDs; in { systemd.services.lex-serve = { serviceConfig = { ExecStart = "${lex_surf}/bin/lex-serve"; DynamicUser = true; RuntimeDirectory = "lex-serve"; WorkingDirectory = lex_surf; LogExtraFields = "LOG_FORMAT=logfmt"; }; environment = { SOCKET_PATH = socketPath; DOMAIN = cfg.domain; LAWS_DIR = "/var/lib/lex-fetch"; }; wantedBy = ["multi-user.target"]; }; systemd.services."lex-fetch@" = { serviceConfig = { ExecStart = "${lex_surf}/bin/lex-fetch %i /var/lib/lex-fetch/%i.json"; User = cfg.fetchUser; StateDirectory = "lex-fetch"; # creates /var/lib/lex-fetch LogExtraFields = "LOG_FORMAT=logfmt"; }; environment = { SOCKET_PATH = socketPath; }; }; systemd.timers = let countries = lib.filter (name: lib.elem name ccTLDs) ( builtins.attrNames (builtins.readDir ./lex-fetch) ); in builtins.listToAttrs ( map (country: { name = "lex-fetch-${country}"; value = { wantedBy = ["timers.target"]; timerConfig = { OnCalendar = "daily"; Unit = "lex-fetch@${country}.service"; }; }; }) countries ); security.acme.certs = lib.mkIf cfg.enableACME ( builtins.listToAttrs ( lib.imap0 (i: ccTLDs: { name = "batch${toString i}.${cfg.domain}"; value = let domains = map (tld: "${tld}.${cfg.domain}") ccTLDs; in { domain = builtins.head domains; extraDomainNames = builtins.tail domains; group = config.services.nginx.group; webroot = "/var/lib/acme/acme-challenge"; }; }) ccChunks ) // { ${cfg.domain} = { group = config.services.nginx.group; webroot = "/var/lib/acme/acme-challenge"; }; } ); services.nginx = { enable = true; virtualHosts = { ${cfg.domain} = lib.mkMerge [ cfg.nginx { locations."/" = { proxyPass = "http://unix:${socketPath}"; recommendedProxySettings = true; }; locations."/assets/" = { root = lex_surf; tryFiles = "$uri =404"; }; useACMEHost = if cfg.enableACME then cfg.domain else null; } ]; } // (builtins.listToAttrs ( lib.imap0 (i: ccTLDs: { name = "host${toString i}.${cfg.domain}"; value = lib.mkMerge [ cfg.nginx { serverName = let escapedDomain = builtins.replaceStrings ["."] ["\\."] cfg.domain; in "~^(?${lib.strings.concatStringsSep "|" ccTLDs})\\.${escapedDomain}$"; useACMEHost = if cfg.enableACME then "batch${toString i}.${cfg.domain}" else null; locations."/" = { proxyPass = "http://unix:${socketPath}"; recommendedProxySettings = true; }; locations."=/laws.json" = { root = "/var/lib/lex-fetch"; tryFiles = "/$cc.json =404"; extraConfig = '' gzip on; gzip_types *; ''; }; } ]; }) ccChunks )); }; } ); }