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