blob: d5b679e338aa1a1a2e78dd7c5684f8dc113afd6e [file] [log] [blame]
Skyler Greyd7e1acd2024-06-22 14:42:11 +00001# SPDX-FileCopyrightText: 2024 Clicks Codes
2#
3# SPDX-License-Identifier: GPL-3.0-only
4
5{ lib, inputs, ... }: {
6 nginx.http.internal = {
7 generateWwwRedirects = hosts: let
8 withWww = name: host: if host.www
9 then {
10 "${name}" = lib.attrsets.removeAttrs host [ "www" ];
11 "www.${name}" = {
Skyler Grey13068952024-08-20 22:56:49 +000012 inherit (host) dnsProvider;
Skyler Greyd7e1acd2024-06-22 14:42:11 +000013 routes."~ ^/?([^\r\n]*)$" = lib.home-manager.hm.dag.entryAnywhere (lib.clicks.nginx.http.redirectPermanent "https://${name}/$1");
14 service = null;
15 enableHttp = host.enableHttp;
16 authWith = null;
17 };
18 }
19 else {
20 "${name}" = lib.attrsets.removeAttrs host [ "www" ];
21 };
22 in lib.pipe hosts [
23 (lib.attrsets.mapAttrsToList withWww)
24 (lib.lists.foldr (host: merged: merged // host) {})
25 ];
26
27 assertExactlyOneOfServiceOrRoutes = hosts: let
28 validate = name: host: let
29 routesProvided = host.routes != null && host.routes != {};
30 serviceProvided = host.service != null;
31 in
32 if !serviceProvided && !routesProvided
33 then builtins.throw "${name}: You must provide one of `service` or `routes`"
34 else if serviceProvided && routesProvided
35 then builtins.throw "${name}: You may only provide one of `service` or `routes`"
36 else host;
37 in lib.attrsets.mapAttrs validate hosts;
38
39 assertTailscaleAuthServiceOnly = hosts: let
40 validate = name: host:
41 if host.authWith != "tailscale" || host.routes == null || host.routes == {}
42 then host
43 else builtins.throw "${name}: You may not use tailscale auth when manually specifying `routes`. Please use `service` instead";
44 in lib.attrsets.mapAttrs validate hosts;
45
46 translateServiceToRoutes = hosts: let
47 validate = name: value:
48 lib.trivial.throwIf (value.service != null && value.routes != null && value.routes != {}) "Both `service` and `routes` cannot be set at the same time" value;
49 translate = name: value:
50 lib.attrsets.removeAttrs (
51 if (value.service != null)
52 then value // {
53 routes."/" = lib.home-manager.hm.dag.entryAnywhere value.service;
54 }
55 else value
56 ) [ "service" ];
57 in lib.attrsets.mapAttrs translate
58 (lib.attrsets.mapAttrs validate hosts);
59
60 translateRoutesDagToList = hosts: let
61 hostTranslateRoutesDagToList = name: host: host // {
62 routes = let
63 HMDagRoutes = lib.home-manager.hm.dag.topoSort host.routes;
64 in map ({name, data}: { inherit name; value = data; }) HMDagRoutes.result;
65 };
66 in lib.attrsets.mapAttrs hostTranslateRoutesDagToList hosts;
67
68 unwrapHeaders = hosts: let
69 unwrapHeader = route:
70 if lib.types.isType "header" route.value
71 then unwrapHeader {
72 name = route.name;
73 value = lib.attrsets.recursiveUpdate route.value.content {
74 headers = (route.value.headers or {}) // {
75 "${route.value.name}" = "${route.value.value}";
76 };
77 };
78 }
79 else route;
80
81 hostUnwrapHeaders = name: host: host // {
82 routes = lib.lists.forEach host.routes unwrapHeader;
83 };
84 in lib.attrsets.mapAttrs hostUnwrapHeaders hosts;
85
86 translateRoutesToLocations = hosts: let
87 addHeaderToLocation = header: location:
88 location // {
89 extraConfig = (location.extraConfig or "") + "\nadd_header \"${header.name}\" \"${header.value}\";";
90 };
91 addHeadersToLocation = headers: location:
92 if headers == null
93 then location
94 else lib.trivial.pipe headers [
95 lib.attrsets.attrsToList
96 (lib.lists.foldr addHeaderToLocation location)
97 ];
98 serviceTypeHandlers = {
99 directory = service: {
100 root = service.root;
101 index = "index.html index.htm";
102 extraConfig = lib.strings.optionalString
103 service.listContents
104 "autoindex on;";
105 };
106 file = service: {
107 root = "/";
108 tryFiles = "${service.path} =404";
109 };
110 redirect = service: let
111 statusCode =
112 if service.permanent
113 then "307"
114 else "308";
115 in {
116 return = "${statusCode} ${service.to}";
117 };
118 reverseProxy = service: {
119 proxyPass = "${service.protocol}://${service.host}:${builtins.toString service.port}";
120 proxyWebsockets = true;
121 recommendedProxySettings = true;
122 };
123 status = service: {
124 return = builtins.toString service.statusCode;
125 };
126 };
127 translateServiceToLocation = service: lib.trivial.pipe service.value [
128 serviceTypeHandlers.${service.value._type}
129 (addHeadersToLocation service.value.headers)
130 (location: { name = service.name; value = location; })
131 ];
132 hostTranslateRoutesToLocations = name: host: lib.attrsets.removeAttrs (host // {
133 locations = lib.lists.forEach host.routes translateServiceToLocation;
134 }) [ "routes" ];
135 in lib.attrsets.mapAttrs hostTranslateRoutesToLocations hosts;
136
137 setLocationPriorities = hosts: let
138 priorityByIndex = index: 100 + index * 50;
139 locationWithPriority = index: location: {
140 name = location.name;
141 value = location.value // {
142 priority = location.value.priority or priorityByIndex index;
143 };
144 };
145 setHostLocationPriority = name: host: host // {
146 locations = lib.pipe host.locations [
147 (lib.lists.imap0 locationWithPriority)
148 lib.attrsets.listToAttrs
149 ];
150 };
151 in lib.attrsets.mapAttrs setHostLocationPriority hosts;
152
153 addListenDefaults = hosts: let
154 setDefaults = name: host: lib.attrsets.removeAttrs (host // {
155 onlySSL = !host.enableHttp;
156 addSSL = host.enableHttp;
157
158 useACMEHost = name;
159 }) [ "enableHttp" ];
160 in lib.attrsets.mapAttrs setDefaults hosts;
161
162 serviceTranslation = hosts: lib.trivial.pipe hosts [
163 lib.clicks.nginx.http.internal.assertExactlyOneOfServiceOrRoutes
164 lib.clicks.nginx.http.internal.assertTailscaleAuthServiceOnly
165 lib.clicks.nginx.http.internal.generateWwwRedirects
166 lib.clicks.nginx.http.internal.translateServiceToRoutes
167 lib.clicks.nginx.http.internal.translateRoutesDagToList
168 lib.clicks.nginx.http.internal.unwrapHeaders
169 lib.clicks.nginx.http.internal.translateRoutesToLocations
170 lib.clicks.nginx.http.internal.setLocationPriorities
171 lib.clicks.nginx.http.internal.addListenDefaults
172 ];
173 };
174}