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/modules/default.nix b/lib/modules/default.nix
new file mode 100644
index 0000000..f0b6092
--- /dev/null
+++ b/lib/modules/default.nix
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: {
+ modules = {
+ unset = lib.modules.mkIf false null;
+ };
+}
diff --git a/lib/nginx/http/default.nix b/lib/nginx/http/default.nix
new file mode 100644
index 0000000..ad0e822
--- /dev/null
+++ b/lib/nginx/http/default.nix
@@ -0,0 +1,53 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, inputs, ... }:
+{
+ nginx.http = let
+ _directory = listContents: root: {
+ inherit root listContents;
+ _type = "directory";
+ headers = null;
+ };
+ _redirect = permanent: to: {
+ inherit to permanent;
+ _type = "redirect";
+ headers = null;
+ };
+ _reverseProxy = protocol: host: port: {
+ inherit protocol host port;
+ _type = "reverseProxy";
+ headers = null;
+ };
+ in {
+ # Header Manipulation
+ addHeader = name: value: content: {
+ inherit name value content;
+ _type = "header";
+ };
+ unsafeAddCrossOriginHeader = lib.clicks.nginx.http.addHeader "Access-Control-Allow-Origin" "*";
+
+ # Location translatable directives
+
+ directory = _directory true;
+ privateDirectory = _directory false;
+
+ file = path: {
+ inherit path;
+ _type = "file";
+ };
+
+ redirect = _redirect false;
+ redirectPermanent = _redirect true;
+
+ reverseProxySecure = _reverseProxy "https";
+ reverseProxy = _reverseProxy "http";
+
+ status = code: {
+ inherit code;
+ _type = "status";
+ headers = null;
+ };
+ };
+}
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;
+ };
+}
diff --git a/lib/nginx/http/lib.nginx.http.status-code.spec.nix b/lib/nginx/http/lib.nginx.http.status-code.spec.nix
new file mode 100644
index 0000000..67b4d86
--- /dev/null
+++ b/lib/nginx/http/lib.nginx.http.status-code.spec.nix
@@ -0,0 +1,22 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, ... }: {
+ testSuccess = {
+ expr = lib.clicks.nginx.http.status 200;
+ expected = {
+ code = 200;
+ _type = "status";
+ headers = null;
+ };
+ };
+ testNotFound = {
+ expr = lib.clicks.nginx.http.status 404;
+ expected = {
+ code = 404;
+ _type = "status";
+ headers = null;
+ };
+ };
+}
diff --git a/lib/nginx/stream/default.nix b/lib/nginx/stream/default.nix
new file mode 100644
index 0000000..d6a3183
--- /dev/null
+++ b/lib/nginx/stream/default.nix
@@ -0,0 +1,17 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{ lib, inputs, ... }:
+{
+ nginx.stream = {
+ proxyStream = external: internal: protocol: {
+ inherit external internal protocol;
+ haproxy = true;
+ };
+ stream = external: internal: protocol: {
+ inherit external internal protocol;
+ haproxy = false;
+ };
+ };
+}
diff --git a/lib/types/modules/README.md b/lib/types/modules/README.md
new file mode 100644
index 0000000..b13a8af
--- /dev/null
+++ b/lib/types/modules/README.md
@@ -0,0 +1,60 @@
+<!--
+Copyright (c) 2003-2016 Eelco Dolstra and the Nixpkgs/NixOS contributors
+SPDX-FileCopyrightText: 2024 Clicks Codes
+
+SPDX-License-Identifier: MIT
+-->
+
+# lib.clicks.types.modules.taggedSubmodule
+
+This is a cleaned-up version of the type proposed in https://github.com/NixOS/nixpkgs/pull/254790, and as-such is under the same license as nixpkgs
+
+{ *`types`*, *`specialArgs`* ? {} }
+
+This is like `types.oneOf`, but takes an attrsSet of submodule in types. Those need to have a type option
+ which is used to find the correct submodule.
+
+::: {#ex-tagged-submodule .example}
+### Tagged submodules
+```nix
+let
+ submoduleA = submodule {
+ options = {
+ _type = mkOption {
+ type = str;
+ };
+ foo = mkOption {
+ type = int;
+ };
+ };
+ };
+ submoduleB = submodule {
+ options = {
+ _type = mkOption {
+ type = str;
+ };
+ bar = mkOption {
+ type = int;
+ };
+ };
+ };
+in
+options.mod = mkOption {
+ type = attrsOf (taggedSubmodule {
+ types = {
+ a = submoduleA;
+ b = submoduleB;
+ };
+ });
+};
+config.mod = {
+ someA = {
+ _type = "a";
+ foo = 123;
+ };
+ someB = {
+ _type = "b";
+ bar = 456;
+ };
+};
+```
diff --git a/lib/types/modules/default.nix b/lib/types/modules/default.nix
new file mode 100644
index 0000000..d9d11d3
--- /dev/null
+++ b/lib/types/modules/default.nix
@@ -0,0 +1,26 @@
+# Copyright (c) 2003-2016 Eelco Dolstra and the Nixpkgs/NixOS contributors
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: MIT
+
+{
+ lib,
+ ...
+}: {
+ types.modules = {
+ taggedSubmodule =
+ { types
+ , specialArgs ? {}
+ }: lib.types.mkOptionType rec {
+ name = "taggedSubmodule";
+ description = "submodules tagged by ._type";
+ check = x: if x ? _type then types.${x._type}.check x else throw "No ._type option set in:\n${lib.generators.toPretty {} x}";
+ merge = loc: lib.lists.foldl'
+ (res: def: types.${def.value._type}.merge loc [
+ (lib.attrsets.recursiveUpdate { value._module.args = specialArgs; } def)
+ ])
+ { };
+ nestedTypes = types;
+ };
+ };
+}
diff --git a/lib/types/nginx/README.md b/lib/types/nginx/README.md
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/types/nginx/README.md
diff --git a/lib/types/nginx/default.nix b/lib/types/nginx/default.nix
new file mode 100644
index 0000000..3b48b7a
--- /dev/null
+++ b/lib/types/nginx/default.nix
@@ -0,0 +1,157 @@
+# SPDX-FileCopyrightText: 2024 Clicks Codes
+#
+# SPDX-License-Identifier: GPL-3.0-only
+
+{
+ lib,
+ ...
+}: {
+ types.nginx = {
+ host = config: lib.types.submodule (
+ { ... }@submodule: {
+ options = {
+ dnsProvider = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ description = "DNS Provider for this host's / these hosts certificate";
+ default = config.security.acme.defaults.dnsProvider;
+ };
+ www = lib.mkOption {
+ type = lib.types.bool;
+ description = "Automatically create www.<address>";
+ default = true;
+ };
+ service = lib.mkOption {
+ type = lib.types.nullOr (lib.clicks.types.modules.taggedSubmodule {
+ types = lib.clicks.types.nginx.services;
+ });
+ description = "Service which sits at \"/\"";
+ default = null;
+ };
+ routes = lib.mkOption {
+ type = lib.types.nullOr (lib.home-manager.hm.types.dagOf (lib.clicks.types.modules.taggedSubmodule {
+ types = lib.clicks.types.nginx.services;
+ }));
+ description = "An attrset of paths to the service wanted";
+ default = null;
+ };
+ authWith = lib.mkOption {
+ type = lib.types.nullOr (lib.types.enum [ "tailscale" ]);
+ description = "Authenticate with this service on the nginx level";
+ default = null;
+ };
+ enableHttp = lib.mkEnableOption "Listen additionally on port 80";
+ };
+ }
+ );
+ services = {
+ reverseProxy = lib.types.submodule {
+ options = {
+ host = lib.mkOption {
+ type = lib.types.str;
+ description = "Local host of the service";
+ default = "127.0.0.1";
+ };
+ port = lib.mkOption {
+ type = lib.types.port;
+ description = "Port the service is running on";
+ };
+ protocol = lib.mkOption {
+ type = lib.types.str;
+ description = "Protocol to connect with, normally 'http'";
+ default = "http";
+ };
+ _type = lib.mkOption {
+ type = lib.types.str;
+ internal = true;
+ description = "The type of service";
+ default = "reverseProxy";
+ };
+ headers = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Headers and their values";
+ default = null;
+ };
+ };
+ };
+ redirect = lib.types.submodule {
+ options = {
+ to = lib.mkOption {
+ type = lib.types.str;
+ description = "Where to redirect to";
+ };
+ permanent = lib.mkEnableOption "Make this redirect with a HTTP 308 Permanent Redirect rather than 307 Temporary Redirect";
+ _type = lib.mkOption {
+ type = lib.types.str;
+ internal = true;
+ description = "The type of service";
+ default = "redirect";
+ };
+ headers = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Headers and their values";
+ default = null;
+ };
+ };
+ };
+ directory = lib.types.submodule {
+ options = {
+ root = lib.mkOption {
+ type = lib.types.str;
+ description = "The root path of the directory";
+ };
+ listContents = lib.mkEnableOption "Make this directory and subdirectories give a listing when there's no index file";
+ _type = lib.mkOption {
+ type = lib.types.str;
+ internal = true;
+ description = "The type of service";
+ default = "directory";
+ };
+ headers = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Headers and their values";
+ default = null;
+ };
+ };
+ };
+ file = lib.types.submodule {
+ options = {
+ path = lib.mkOption {
+ type = lib.types.str;
+ description = "Path to the file";
+ };
+ _type = lib.mkOption {
+ type = lib.types.str;
+ internal = true;
+ description = "The type of service";
+ default = "file";
+ };
+ headers = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Headers and their values";
+ default = null;
+ };
+ };
+ };
+ status = lib.types.submodule {
+ options = {
+ code = lib.mkOption {
+ type = lib.types.number;
+ description = "HTTP status code to return";
+ default = 200;
+ };
+ _type = lib.mkOption {
+ type = lib.types.str;
+ internal = true;
+ description = "The type of service";
+ default = "status";
+ };
+ headers = lib.mkOption {
+ type = lib.types.nullOr (lib.types.attrsOf lib.types.str);
+ description = "Headers and their values";
+ default = null;
+ };
+ };
+ };
+ };
+ };
+}