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}" = { |
| 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 | } |