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/README.md b/lib/nginx/http/internal/README.md
new file mode 100644
index 0000000..7b0813c
--- /dev/null
+++ b/lib/nginx/http/internal/README.md
@@ -0,0 +1,7 @@
+<!--
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: GPL-3.0-only
+-->
+
+dag ordering should start at 100 and should be spaced 50 apart
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
+ ];
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix
new file mode 100644
index 0000000..946ef61
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.assertion.spec.nix
@@ -0,0 +1,71 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ tailscaleAuthWithRoute = {
+ "calibre.clicks.codes" = {
+ routes."/somePath" = lib.home-manager.hm.dag.entryAnywhere {
+ host = "generic";
+ port = 1024;
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ service = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ serviceAndRoutes = {
+ "calibre.clicks.codes" = {
+ routes."/somePath" = lib.home-manager.hm.dag.entryAnywhere {
+ host = "generic";
+ port = 1024;
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ service = {
+ host = "generic";
+ port = 1024;
+ _type = "reverseProxy";
+ headers = null;
+ };
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ neitherServiceOrRoutes = {
+ "calibre.clicks.codes" = {
+ routes = null;
+ dnsProvider = "cloudflare";
+ service = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+in {
+ testAssertTailscaleAuthServiceOnlyFail = {
+ /* This is NOT considered a step, here we test that it passes through input */
+ expr = lib.clicks.nginx.http.internal.assertTailscaleAuthServiceOnly tailscaleAuthWithRoute;
+ expectedError = {
+ type = "ThrownError";
+ msg = "calibre.clicks.codes: You may not use tailscale auth when manually specifying `routes`. Please use `service` instead";
+ };
+ };
+ testAssertExactlyOneOServiceOrRoutesBothProvidedFail = {
+ expr = lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes serviceAndRoutes;
+ expectedError = {
+ type = "ThrownError";
+ msg = "calibre.clicks.codes: You may only provide one of `service` or `routes`";
+ };
+ };
+ testAssertExactlyOneOfServiceOrRoutesNeitherProvidedFail = {
+ expr = lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes neitherServiceOrRoutes;
+ expectedError = {
+ type = "ThrownError";
+ msg = "calibre.clicks.codes: You must provide one of `service` or `routes`";
+ };
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix
new file mode 100644
index 0000000..6710523
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.design.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ step0 = {
+ "calibre.clicks.codes" = {
+ service = lib.clicks.nginx.http.reverseProxy "generic" 1024;
+ www = true;
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ step1 = {
+ "calibre.clicks.codes" = {
+ service = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ service = null;
+ enableHttp = false;
+ };
+ };
+ step2 = {
+ "calibre.clicks.codes" = {
+ routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step3 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step4 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step5 = {
+ "calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "/";
+ value = {
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ return = "307 https://calibre.clicks.codes/$1";
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step6 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step7 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "calibre.clicks.codes";
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "www.calibre.clicks.codes";
+ authWith = null;
+ };
+ };
+in {
+ testGenerateWwwRedirects = {
+ expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+ expected = step1;
+ };
+ testTranslateServiceToRoutes = {
+ expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+ expected = step2;
+ };
+ testTranslateRoutesDagToList = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+ expected = step3;
+ };
+ testUnwrapHeaderPassthrough = {
+ expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+ expected = step4;
+ };
+ testTranslateRoutesToLocations = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+ expected = step5;
+ };
+ testSetLocationPriorities = {
+ expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+ expected = step6;
+ };
+ testAddListenDefaults = {
+ expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+ expected = step7;
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix
new file mode 100644
index 0000000..97756f4
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.header.spec.nix
@@ -0,0 +1,258 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ step0 = {
+ "calibre.clicks.codes" = {
+ service = lib.clicks.nginx.http.unsafeAddCrossOriginHeader (lib.clicks.nginx.http.reverseProxy "generic" 1024);
+ www = true;
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ step1 = {
+ "calibre.clicks.codes" = {
+ service = {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ service = null;
+ enableHttp = false;
+ };
+ };
+ step2 = {
+ "calibre.clicks.codes" = {
+ routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step3 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step4 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = {
+ "Access-Control-Allow-Origin" = "*";
+ };
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step5 = {
+ "calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "/";
+ value = {
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ return = "307 https://calibre.clicks.codes/$1";
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step6 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step7 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "calibre.clicks.codes";
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "www.calibre.clicks.codes";
+ authWith = null;
+ };
+ };
+in {
+ testGenerateWwwRedirects = {
+ expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+ expected = step1;
+ };
+ testTranslateServiceToRoutes = {
+ expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+ expected = step2;
+ };
+ testTranslateRoutesDagToList = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+ expected = step3;
+ };
+ testUnwrapHeader = {
+ expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+ expected = step4;
+ };
+ testTranslateRoutesToLocations = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+ expected = step5;
+ };
+ testSetLocationPriorities = {
+ expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+ expected = step6;
+ };
+ testAddListenDefaults = {
+ expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+ expected = step7;
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix
new file mode 100644
index 0000000..66ced1d
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.headers.spec.nix
@@ -0,0 +1,277 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ step0 = {
+ "calibre.clicks.codes" = {
+ service = lib.pipe (lib.clicks.nginx.http.reverseProxy "generic" 1024) [
+ lib.clicks.nginx.http.unsafeAddCrossOriginHeader
+ (lib.clicks.nginx.http.addHeader "X-Clacks-Overhead" "GNU Terry Pratchett")
+ ];
+ www = true;
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ step1 = {
+ "calibre.clicks.codes" = {
+ service = {
+ _type = "header";
+ name = "X-Clacks-Overhead";
+ value = "GNU Terry Pratchett";
+ content = {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ };
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ service = null;
+ enableHttp = false;
+ };
+ };
+ step2 = {
+ "calibre.clicks.codes" = {
+ routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+ _type = "header";
+ name = "X-Clacks-Overhead";
+ value = "GNU Terry Pratchett";
+ content = {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step3 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ _type = "header";
+ name = "X-Clacks-Overhead";
+ value = "GNU Terry Pratchett";
+ content = {
+ _type = "header";
+ name = "Access-Control-Allow-Origin";
+ value = "*";
+ content = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ };
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step4 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = {
+ "Access-Control-Allow-Origin" = "*";
+ "X-Clacks-Overhead" = "GNU Terry Pratchett";
+ };
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step5 = {
+ "calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "/";
+ value = {
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ return = "307 https://calibre.clicks.codes/$1";
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step6 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step7 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ extraConfig = "\nadd_header \"X-Clacks-Overhead\" \"GNU Terry Pratchett\";\nadd_header \"Access-Control-Allow-Origin\" \"*\";";
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "calibre.clicks.codes";
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "www.calibre.clicks.codes";
+ authWith = null;
+ };
+ };
+in {
+ testGenerateWwwRedirects = {
+ expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+ expected = step1;
+ };
+ testTranslateServiceToRoutes = {
+ expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+ expected = step2;
+ };
+ testTranslateRoutesDagToList = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+ expected = step3;
+ };
+ testUnwrapHeader = {
+ expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+ expected = step4;
+ };
+ testTranslateRoutesToLocations = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+ expected = step5;
+ };
+ testSetLocationPriorities = {
+ expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+ expected = step6;
+ };
+ testAddListenDefaults = {
+ expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+ expected = step7;
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix
new file mode 100644
index 0000000..4f56d96
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.http.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ step0 = {
+ "calibre.clicks.codes" = {
+ service = lib.clicks.nginx.http.reverseProxy "generic" 1024;
+ www = true;
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ };
+ step1 = {
+ "calibre.clicks.codes" = {
+ service = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ service = null;
+ enableHttp = true;
+ };
+ };
+ step2 = {
+ "calibre.clicks.codes" = {
+ routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ enableHttp = true;
+ };
+ };
+ step3 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = true;
+ };
+ };
+ step4 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "http";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = true;
+ };
+ };
+ step5 = {
+ "calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "/";
+ value = {
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ return = "307 https://calibre.clicks.codes/$1";
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = true;
+ };
+ };
+ step6 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = true;
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ authWith = null;
+ enableHttp = true;
+ };
+ };
+ step7 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "http://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ onlySSL = false;
+ addSSL = true;
+ useACMEHost = "calibre.clicks.codes";
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ onlySSL = false;
+ addSSL = true;
+ useACMEHost = "www.calibre.clicks.codes";
+ authWith = null;
+ };
+ };
+in {
+ testGenerateWwwRedirects = {
+ expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+ expected = step1;
+ };
+ testTranslateServiceToRoutes = {
+ expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+ expected = step2;
+ };
+ testTranslateRoutesDagToList = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+ expected = step3;
+ };
+ testUnwrapHeaderPassthrough = {
+ expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+ expected = step4;
+ };
+ testTranslateRoutesToLocations = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+ expected = step5;
+ };
+ testSetLocationPriorities = {
+ expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+ expected = step6;
+ };
+ testAddListenDefaults = {
+ expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+ expected = step7;
+ };
+}
diff --git a/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix b/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix
new file mode 100644
index 0000000..77ad490
--- /dev/null
+++ b/lib/nginx/http/internal/lib.nginx.http.internal.https-proxy.spec.nix
@@ -0,0 +1,238 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: let
+ step0 = {
+ "calibre.clicks.codes" = {
+ service = lib.clicks.nginx.http.reverseProxySecure "generic" 1024;
+ www = true;
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ };
+ step1 = {
+ "calibre.clicks.codes" = {
+ service = {
+ host = "generic";
+ port = 1024;
+ protocol = "https";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ routes = null;
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ service = null;
+ enableHttp = false;
+ };
+ };
+ step2 = {
+ "calibre.clicks.codes" = {
+ routes."/" = lib.home-manager.hm.dag.entryAnywhere {
+ host = "generic";
+ port = 1024;
+ protocol = "https";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step3 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "https";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step4 = {
+ "calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "/";
+ value = {
+ host = "generic";
+ port = 1024;
+ protocol = "https";
+ _type = "reverseProxy";
+ headers = null;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ routes = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ to = "https://calibre.clicks.codes/$1";
+ permanent = true;
+ _type = "redirect";
+ headers = null;
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step5 = {
+ "calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "/";
+ value = {
+ proxyPass = "https://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ }
+ ];
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations = [
+ {
+ name = "~ ^/?([^\r\n]*)$";
+ value = {
+ return = "307 https://calibre.clicks.codes/$1";
+ };
+ }
+ ];
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step6 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "https://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ enableHttp = false;
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ authWith = null;
+ enableHttp = false;
+ };
+ };
+ step7 = {
+ "calibre.clicks.codes" = {
+ locations."/" = {
+ priority = 100;
+ proxyPass = "https://generic:1024";
+ proxyWebsockets = true;
+ recommendedProxySettings = true;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "calibre.clicks.codes";
+ dnsProvider = "cloudflare";
+ authWith = "tailscale";
+ };
+ "www.calibre.clicks.codes" = {
+ locations."~ ^/?([^\r\n]*)$" = {
+ return = "307 https://calibre.clicks.codes/$1";
+ priority = 100;
+ };
+ onlySSL = true;
+ addSSL = false;
+ useACMEHost = "www.calibre.clicks.codes";
+ authWith = null;
+ };
+ };
+in {
+ testGenerateWwwRedirects = {
+ expr = lib.clicks.nginx.http.internal.generateWwwRedirects step0;
+ expected = step1;
+ };
+ testTranslateServiceToRoutes = {
+ expr = lib.clicks.nginx.http.internal.translateServiceToRoutes step1;
+ expected = step2;
+ };
+ testTranslateRoutesDagToList = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesDagToList step2;
+ expected = step3;
+ };
+ testUnwrapHeaderPassthrough = {
+ expr = lib.clicks.nginx.http.internal.unwrapHeaders step3;
+ expected = step4;
+ };
+ testTranslateRoutesToLocations = {
+ expr = lib.clicks.nginx.http.internal.translateRoutesToLocations step4;
+ expected = step5;
+ };
+ testSetLocationPriorities = {
+ expr = lib.clicks.nginx.http.internal.setLocationPriorities step5;
+ expected = step6;
+ };
+ testAddListenDefaults = {
+ expr = lib.clicks.nginx.http.internal.addListenDefaults step6;
+ expected = step7;
+ };
+}