{ config, lib, my, ... }@args: let cfg = config.modules.reverse-proxy; registry = args.registry or config.registry; routeWithBackend = hostName: routeName: route: let facts = my.registry.lookup registry hostName routeName; in if route.backend != null then route else if facts ? address && route.port != null then route // { backend = "${route.scheme}://${facts.address}:${toString route.port}"; } else route; allRoutes = lib.concatLists ( lib.mapAttrsToList ( hostName: routes: lib.mapAttrsToList (routeName: route: routeWithBackend hostName routeName route) routes ) registry.route ); enabledRoutes = builtins.filter (route: route.enable) allRoutes; routesByHost = lib.groupBy (route: route.host) enabledRoutes; rootRoutesByHost = lib.mapAttrs ( host: routes: let roots = builtins.filter (route: route.path == "/") routes; in if roots == [ ] then null else builtins.head roots ) routesByHost; hasDuplicatePaths = routes: let paths = map (route: route.path) routes; in lib.length paths != lib.length (lib.unique paths); hasUnresolvedBackend = route: route.backend == null; mkLocation = route: { proxyPass = route.backend; proxyWebsockets = route.websockets; }; mkVirtualHost = host: routes: let rootRoute = rootRoutesByHost.${host}; in lib.mkIf (rootRoute != null) { forceSSL = rootRoute.tls; enableACME = rootRoute.tls; serverAliases = rootRoute.aliases; locations = lib.listToAttrs ( map (route: lib.nameValuePair route.path (mkLocation route)) ( builtins.filter (route: route.backend != null) routes ) ); }; in { options.modules.reverse-proxy = { enable = lib.mkEnableOption "the Nginx reverse proxy route realizer"; }; config = lib.mkIf cfg.enable { assertions = lib.mapAttrsToList (host: rootRoute: { assertion = rootRoute != null; message = "registry.route entries for ${host} must include a primary / route."; }) rootRoutesByHost ++ lib.mapAttrsToList (host: routes: { assertion = !hasDuplicatePaths routes; message = "registry.route entries for ${host} must not declare the same path more than once."; }) routesByHost ++ map (route: { assertion = !hasUnresolvedBackend route; message = "registry.route entry for ${route.host}${route.path} must resolve to a backend URL before actualization."; }) (builtins.filter hasUnresolvedBackend enabledRoutes); networking.firewall.allowedTCPPorts = [ 80 443 ]; security.acme = { acceptTerms = true; defaults.email = "contact@alexion.dev"; }; services.nginx = { enable = true; commonHttpConfig = '' server { listen 0.0.0.0:443 ssl default_server; listen [::0]:443 ssl default_server; ssl_reject_handshake on; } ''; recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; virtualHosts = lib.mapAttrs mkVirtualHost routesByHost; }; }; }