feat: Add nginx module
Change-Id: I34fbb926c4b7eab344c1c14de4e4b5f82c6c30eb
Reviewed-on: https://git.clicks.codes/c/Infra/NixFiles/+/785
Reviewed-by: Samuel Shuert <coded@clicks.codes>
Tested-by: Skyler Grey <minion@clicks.codes>
diff --git a/lib/nginx/http/internal/default.nix b/lib/nginx/http/internal/default.nix
new file mode 100644
index 0000000..767bc6f
--- /dev/null
+++ b/lib/nginx/http/internal/default.nix
@@ -0,0 +1,173 @@
+# 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}" = {
+ 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
+ ];
+ };
+}