feat: try rework
This commit is contained in:
parent
1ddbd3b8b6
commit
ecf10628c3
51 changed files with 1941 additions and 445 deletions
297
intermediate/nginx.nix
Normal file
297
intermediate/nginx.nix
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
let
|
||||
lib = import <nixpkgs/lib>;
|
||||
net = import ../config/network.nix;
|
||||
end = import ../config/endpoints.nix;
|
||||
endpointValidation = import ../validation/endpoints.nix;
|
||||
autoSshValidation = import ../validation/auto_ssh.nix;
|
||||
|
||||
endpoints = endpointValidation.validateEndpointsShape end;
|
||||
|
||||
subnet =
|
||||
if net ? network && builtins.isAttrs net.network && net.network ? subnet && builtins.isString net.network.subnet then
|
||||
net.network.subnet
|
||||
else
|
||||
throw "config/network.nix must define network.subnet as a string.";
|
||||
|
||||
localDomain =
|
||||
if net ? local_domain && builtins.isString net.local_domain then
|
||||
net.local_domain
|
||||
else
|
||||
throw "config/network.nix must define local_domain as a string.";
|
||||
|
||||
devices = autoSshValidation.getDevices net;
|
||||
autoSshDevices = autoSshValidation.getAutoSshDevices devices;
|
||||
autoSshDomains = autoSshValidation.getAutoSshDomains autoSshDevices;
|
||||
|
||||
matchesDomain = domain: candidate:
|
||||
domain == candidate || lib.hasSuffix ".${candidate}" domain;
|
||||
|
||||
classifyExposure = domain:
|
||||
let
|
||||
isLocal = matchesDomain domain localDomain;
|
||||
isRemote = lib.any (candidate: matchesDomain domain candidate) autoSshDomains;
|
||||
in
|
||||
if isLocal && isRemote then
|
||||
throw "Domain '${domain}' is both local and remote according to config/network.nix."
|
||||
else if isLocal then
|
||||
"local"
|
||||
else if isRemote then
|
||||
"external"
|
||||
else
|
||||
throw "Domain '${domain}' is neither local nor mapped to an auto_ssh remote domain.";
|
||||
|
||||
validateEndpointSemantics = index: endpoint:
|
||||
let
|
||||
_forceSslPort =
|
||||
if endpoint.force_ssl && endpoint.port == 80 then
|
||||
throw "Endpoint at index ${toString index} cannot use force_ssl=true on port 80."
|
||||
else
|
||||
null;
|
||||
_proxyType =
|
||||
if endpoint.type == "proxy" && endpoint.content.type != "service" then
|
||||
throw "Proxy endpoint at index ${toString index} must use content.type = 'service'."
|
||||
else
|
||||
null;
|
||||
in
|
||||
endpoint // {
|
||||
exposure = classifyExposure endpoint.domain;
|
||||
};
|
||||
|
||||
validatedEndpoints = lib.imap0 validateEndpointSemantics endpoints;
|
||||
|
||||
normalizeWebPrefix = endpoint:
|
||||
if endpoint == "/" then
|
||||
"/"
|
||||
else if lib.hasSuffix "/" endpoint then
|
||||
endpoint
|
||||
else
|
||||
throw "Web endpoint '${endpoint}' must end with '/' so it can be used as a file prefix.";
|
||||
|
||||
joinWebPath = endpointPrefix: relativePath:
|
||||
let
|
||||
prefix = normalizeWebPrefix endpointPrefix;
|
||||
relativeNoContext = builtins.unsafeDiscardStringContext relativePath;
|
||||
_ =
|
||||
if builtins.isString relativeNoContext && relativeNoContext != "" && builtins.substring 0 1 relativeNoContext != "/" then
|
||||
null
|
||||
else
|
||||
throw "Web store file path '${toString relativeNoContext}' must be a non-empty relative path.";
|
||||
in
|
||||
builtins.unsafeDiscardStringContext "${prefix}${relativeNoContext}";
|
||||
|
||||
expandWebEndpoint = endpoint:
|
||||
if endpoint.type != "web" then
|
||||
[ endpoint ]
|
||||
else
|
||||
let
|
||||
files =
|
||||
if endpoint.content ? files && builtins.isList endpoint.content.files then
|
||||
endpoint.content.files
|
||||
else
|
||||
throw "Web endpoint '${endpoint.domain}${endpoint.endpoint}' must define content.files as a list.";
|
||||
|
||||
_ =
|
||||
if files != [] then
|
||||
null
|
||||
else
|
||||
throw "Web endpoint '${endpoint.domain}${endpoint.endpoint}' resolves to zero files. Check config/web.nix roots and data files.";
|
||||
in
|
||||
map (file:
|
||||
endpoint // {
|
||||
endpoint = joinWebPath endpoint.endpoint file.path;
|
||||
content = {
|
||||
status = file.status;
|
||||
contentType = file.contentType;
|
||||
filePath = file.filePath;
|
||||
};
|
||||
}
|
||||
) files;
|
||||
|
||||
mappedEndpoints = lib.concatLists (map expandWebEndpoint validatedEndpoints);
|
||||
|
||||
hostKey = endpoint: "${endpoint.domain}|${toString endpoint.port}|${if endpoint.force_ssl then "tls" else "plain"}";
|
||||
hostKeyWithoutTls = endpoint: "${endpoint.domain}|${toString endpoint.port}";
|
||||
|
||||
groupedByHost = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = hostKey endpoint;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
groupedByHostWithoutTls = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = hostKeyWithoutTls endpoint;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint.force_ssl ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
groupedByDomain = lib.foldl' (acc: endpoint:
|
||||
let
|
||||
key = endpoint.domain;
|
||||
current = if acc ? ${key} then acc.${key} else [];
|
||||
in
|
||||
acc // { ${key} = current ++ [ endpoint.force_ssl ]; }
|
||||
) {} mappedEndpoints;
|
||||
|
||||
_tlsConsistency = lib.mapAttrs (_: tlsValues:
|
||||
let
|
||||
uniqueValues = lib.unique tlsValues;
|
||||
in
|
||||
if builtins.length uniqueValues > 1 then
|
||||
throw "Found incompatible TLS definitions for the same domain+port."
|
||||
else
|
||||
null
|
||||
) groupedByHostWithoutTls;
|
||||
|
||||
_domainTlsConsistency = lib.mapAttrs (domain: tlsValues:
|
||||
let
|
||||
uniqueValues = lib.unique tlsValues;
|
||||
in
|
||||
if builtins.length uniqueValues > 1 then
|
||||
throw "Found incompatible TLS definitions for domain '${domain}'."
|
||||
else
|
||||
null
|
||||
) groupedByDomain;
|
||||
|
||||
ensureUniquePaths = key: routes:
|
||||
let
|
||||
paths = map (route: route.endpoint) routes;
|
||||
uniquePaths = lib.unique paths;
|
||||
in
|
||||
if builtins.length paths != builtins.length uniquePaths then
|
||||
throw "Duplicate endpoint paths found for host key ${key}."
|
||||
else
|
||||
routes;
|
||||
|
||||
ensureExposureConsistency = key: routes:
|
||||
let
|
||||
exposures = lib.unique (map (route: route.exposure) routes);
|
||||
in
|
||||
if builtins.length exposures != 1 then
|
||||
throw "Mixed exposure policy found for host key ${key}."
|
||||
else
|
||||
routes;
|
||||
|
||||
sortRoutes = routes:
|
||||
builtins.sort (a: b: builtins.stringLength a.endpoint > builtins.stringLength b.endpoint) routes;
|
||||
|
||||
sanitizeHostKey = key:
|
||||
builtins.replaceStrings [ "." "|" ":" "/" ] [ "_" "_" "_" "_" ] key;
|
||||
|
||||
escaped = value:
|
||||
builtins.replaceStrings [ "\\" "\"" ] [ "\\\\" "\\\"" ] value;
|
||||
|
||||
mkLocation = route:
|
||||
if route.type == "proxy" then
|
||||
let
|
||||
localNodeIp =
|
||||
if devices ? self && devices.self ? ip && builtins.isString devices.self.ip then
|
||||
devices.self.ip
|
||||
else
|
||||
throw "config/network.nix must define devices.self.ip as a string.";
|
||||
proxyIp =
|
||||
if route.content.ip == localNodeIp then
|
||||
"127.0.0.1"
|
||||
else
|
||||
route.content.ip;
|
||||
in
|
||||
{
|
||||
name = route.endpoint;
|
||||
value = {
|
||||
proxyPass = "http://${proxyIp}:${toString route.content.port}";
|
||||
proxyWebsockets = route.content.proxyWebsockets;
|
||||
};
|
||||
}
|
||||
else
|
||||
let
|
||||
statusValue =
|
||||
if route.content ? status && builtins.isInt route.content.status then
|
||||
route.content.status
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.status as int.";
|
||||
contentTypeValue =
|
||||
if route.content ? contentType && builtins.isString route.content.contentType && route.content.contentType != "" then
|
||||
route.content.contentType
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.contentType as non-empty string.";
|
||||
filePathValue =
|
||||
if route.content ? filePath && (builtins.isString route.content.filePath || builtins.isPath route.content.filePath) then
|
||||
route.content.filePath
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must define content.filePath as string or path.";
|
||||
_status =
|
||||
if statusValue == 200 then
|
||||
null
|
||||
else
|
||||
throw "Web endpoint '${route.domain}${route.endpoint}' must use status 200 for static file serving.";
|
||||
in
|
||||
{
|
||||
name = "= ${route.endpoint}";
|
||||
value = {
|
||||
alias = toString filePathValue;
|
||||
extraConfig = ''
|
||||
types { }
|
||||
default_type ${contentTypeValue};
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
mkVirtualHost = key: routes:
|
||||
let
|
||||
checkedRoutes = ensureExposureConsistency key (ensureUniquePaths key routes);
|
||||
sortedRoutes = sortRoutes checkedRoutes;
|
||||
headRoute = builtins.head sortedRoutes;
|
||||
hostName = sanitizeHostKey key;
|
||||
locations = builtins.listToAttrs (map mkLocation sortedRoutes);
|
||||
base = {
|
||||
serverName = headRoute.domain;
|
||||
listen = [
|
||||
({
|
||||
addr = "0.0.0.0";
|
||||
port = headRoute.port;
|
||||
} // lib.optionalAttrs headRoute.force_ssl { ssl = true; })
|
||||
];
|
||||
locations = locations;
|
||||
};
|
||||
exposureConfig =
|
||||
if headRoute.exposure == "external" then
|
||||
{
|
||||
extraConfig = ''
|
||||
allow all;
|
||||
'';
|
||||
}
|
||||
else
|
||||
{
|
||||
extraConfig = ''
|
||||
allow ${subnet};
|
||||
deny all;
|
||||
'';
|
||||
};
|
||||
sslConfig =
|
||||
if headRoute.force_ssl then
|
||||
{
|
||||
enableACME = true;
|
||||
forceSSL = true;
|
||||
}
|
||||
else
|
||||
{};
|
||||
in
|
||||
{
|
||||
name = hostName;
|
||||
value = base // exposureConfig // sslConfig;
|
||||
};
|
||||
|
||||
virtualHostsData = builtins.listToAttrs (lib.mapAttrsToList mkVirtualHost groupedByHost);
|
||||
|
||||
nginxUsedPorts =
|
||||
lib.unique (map (route: route.port) mappedEndpoints);
|
||||
|
||||
acmeDomains =
|
||||
lib.unique (map (route: route.domain) (lib.filter (route: route.force_ssl) mappedEndpoints));
|
||||
in
|
||||
rec {
|
||||
inherit validatedEndpoints mappedEndpoints virtualHostsData nginxUsedPorts acmeDomains;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue