blob: d5b679e338aa1a1a2e78dd7c5684f8dc113afd6e [file] [log] [blame]
# 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}" = {
inherit (host) dnsProvider;
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
];
};
}