| # SPDX-FileCopyrightText: 2024 Clicks Codes |
| # |
| # SPDX-License-Identifier: GPL-3.0-only |
| |
| { lib, inputs, ... }: { |
| nginx.http.internal = { |
| generateWwwRedirects = hosts: let |
| withWww = name: host: if host.www |
| then { |
| "${name}" = lib.attrsets.removeAttrs host [ "www" ]; |
| "www.${name}" = { |
| inherit (host) dnsProvider; |
| routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere (lib.clicks.nginx.http.redirectPermanent "https://${name}/$1"); |
| service = null; |
| enableHttp = host.enableHttp; |
| authWith = null; |
| }; |
| } |
| else { |
| "${name}" = lib.attrsets.removeAttrs host [ "www" ]; |
| }; |
| in lib.pipe hosts [ |
| (lib.attrsets.mapAttrsToList withWww) |
| (lib.lists.foldr (host: merged: merged // host) {}) |
| ]; |
| |
| assertExactlyOneOfServiceOrRoutes = hosts: let |
| validate = name: host: let |
| routesProvided = host.routes != null && host.routes != {}; |
| serviceProvided = host.service != null; |
| in |
| if !serviceProvided && !routesProvided |
| then builtins.throw "${name}: You must provide one of `service` or `routes`" |
| else if serviceProvided && routesProvided |
| then builtins.throw "${name}: You may only provide one of `service` or `routes`" |
| else host; |
| in lib.attrsets.mapAttrs validate hosts; |
| |
| assertTailscaleAuthServiceOnly = hosts: let |
| validate = name: host: |
| if host.authWith != "tailscale" || host.routes == null || host.routes == {} |
| then host |
| else builtins.throw "${name}: You may not use tailscale auth when manually specifying `routes`. Please use `service` instead"; |
| in lib.attrsets.mapAttrs validate hosts; |
| |
| translateServiceToRoutes = hosts: let |
| validate = name: value: |
| lib.trivial.throwIf (value.service != null && value.routes != null && value.routes != {}) "Both `service` and `routes` cannot be set at the same time" value; |
| translate = name: value: |
| lib.attrsets.removeAttrs ( |
| if (value.service != null) |
| then value // { |
| routes."/" = lib.home-manager.hm.dag.entryAnywhere value.service; |
| } |
| else value |
| ) [ "service" ]; |
| in lib.attrsets.mapAttrs translate |
| (lib.attrsets.mapAttrs validate hosts); |
| |
| translateRoutesDagToList = hosts: let |
| hostTranslateRoutesDagToList = name: host: host // { |
| routes = let |
| HMDagRoutes = lib.home-manager.hm.dag.topoSort host.routes; |
| in map ({name, data}: { inherit name; value = data; }) HMDagRoutes.result; |
| }; |
| in lib.attrsets.mapAttrs hostTranslateRoutesDagToList hosts; |
| |
| unwrapHeaders = hosts: let |
| unwrapHeader = route: |
| if lib.types.isType "header" route.value |
| then unwrapHeader { |
| name = route.name; |
| value = lib.attrsets.recursiveUpdate route.value.content { |
| headers = (route.value.headers or {}) // { |
| "${route.value.name}" = "${route.value.value}"; |
| }; |
| }; |
| } |
| else route; |
| |
| hostUnwrapHeaders = name: host: host // { |
| routes = lib.lists.forEach host.routes unwrapHeader; |
| }; |
| in lib.attrsets.mapAttrs hostUnwrapHeaders hosts; |
| |
| translateRoutesToLocations = hosts: let |
| addHeaderToLocation = header: location: |
| location // { |
| extraConfig = (location.extraConfig or "") + "\nadd_header \"${header.name}\" \"${header.value}\";"; |
| }; |
| addHeadersToLocation = headers: location: |
| if headers == null |
| then location |
| else lib.trivial.pipe headers [ |
| lib.attrsets.attrsToList |
| (lib.lists.foldr addHeaderToLocation location) |
| ]; |
| serviceTypeHandlers = { |
| directory = service: { |
| root = service.root; |
| index = "index.html index.htm"; |
| extraConfig = lib.strings.optionalString |
| service.listContents |
| "autoindex on;"; |
| }; |
| file = service: { |
| root = "/"; |
| tryFiles = "${service.path} =404"; |
| }; |
| redirect = service: let |
| statusCode = |
| if service.permanent |
| then "307" |
| else "308"; |
| in { |
| return = "${statusCode} ${service.to}"; |
| }; |
| reverseProxy = service: { |
| proxyPass = "${service.protocol}://${service.host}:${builtins.toString service.port}"; |
| proxyWebsockets = true; |
| recommendedProxySettings = true; |
| }; |
| status = service: { |
| return = builtins.toString service.statusCode; |
| }; |
| }; |
| translateServiceToLocation = service: lib.trivial.pipe service.value [ |
| serviceTypeHandlers.${service.value._type} |
| (addHeadersToLocation service.value.headers) |
| (location: { name = service.name; value = location; }) |
| ]; |
| hostTranslateRoutesToLocations = name: host: lib.attrsets.removeAttrs (host // { |
| locations = lib.lists.forEach host.routes translateServiceToLocation; |
| }) [ "routes" ]; |
| in lib.attrsets.mapAttrs hostTranslateRoutesToLocations hosts; |
| |
| setLocationPriorities = hosts: let |
| priorityByIndex = index: 100 + index * 50; |
| locationWithPriority = index: location: { |
| name = location.name; |
| value = location.value // { |
| priority = location.value.priority or priorityByIndex index; |
| }; |
| }; |
| setHostLocationPriority = name: host: host // { |
| locations = lib.pipe host.locations [ |
| (lib.lists.imap0 locationWithPriority) |
| lib.attrsets.listToAttrs |
| ]; |
| }; |
| in lib.attrsets.mapAttrs setHostLocationPriority hosts; |
| |
| addListenDefaults = hosts: let |
| setDefaults = name: host: lib.attrsets.removeAttrs (host // { |
| onlySSL = !host.enableHttp; |
| addSSL = host.enableHttp; |
| |
| useACMEHost = name; |
| }) [ "enableHttp" ]; |
| in lib.attrsets.mapAttrs setDefaults hosts; |
| |
| serviceTranslation = hosts: lib.trivial.pipe hosts [ |
| lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes |
| lib.clicks.nginx.http.internal.assertTailscaleAuthServiceOnly |
| lib.clicks.nginx.http.internal.generateWwwRedirects |
| lib.clicks.nginx.http.internal.translateServiceToRoutes |
| lib.clicks.nginx.http.internal.translateRoutesDagToList |
| lib.clicks.nginx.http.internal.unwrapHeaders |
| lib.clicks.nginx.http.internal.translateRoutesToLocations |
| lib.clicks.nginx.http.internal.setLocationPriorities |
| lib.clicks.nginx.http.internal.addListenDefaults |
| ]; |
| }; |
| } |