Skyler Grey | d7e1acd | 2024-06-22 14:42:11 +0000 | [diff] [blame] | 1 | # 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 Grey | 1306895 | 2024-08-20 22:56:49 +0000 | [diff] [blame^] | 12 | inherit (host) dnsProvider; |
Skyler Grey | d7e1acd | 2024-06-22 14:42:11 +0000 | [diff] [blame] | 13 | 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 | } |