infrastructure/modules/nixos/nginx/proxy.nix
2024-04-27 11:45:40 -07:00

346 lines
12 KiB
Nix

let
proxyModule = {config, name, options, gensokyo-zone, lib, ...}: let
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkMerge mkAfter mkOptionDefault;
inherit (lib.strings) optionalString;
cfg = config.proxy;
in {
options = with lib.types; {
proxy = {
enable = mkEnableOption "proxy_pass";
bind = {
enable = mkEnableOption "proxy_bind";
transparent = mkEnableOption "proxy_bind transparent";
address = mkOption {
type = str;
};
};
url = mkOption {
type = str;
};
};
};
config = {
proxy = {
bind.address = mkIf cfg.bind.transparent (mkOptionDefault "$remote_addr");
};
extraConfig = mkIf cfg.enable (mkMerge [
(mkIf cfg.bind.enable (mkAfter (
"proxy_bind ${cfg.bind.address}" + optionalString cfg.bind.transparent " transparent" + ";"
)))
]);
};
};
serverModule = {config, name, options, gensokyo-zone, lib, ...}: let
inherit (lib.modules) mkIf mkAfter;
cfg = config.proxy;
in {
imports = [ proxyModule ];
config = let
warnProxy = lib.warnIf (!cfg.enable && options.proxy.url.isDefined) "nginx.stream.servers.${name}.proxy.url set without proxy.enable";
in {
streamConfig = warnProxy (mkIf cfg.enable (mkAfter
"proxy_pass ${cfg.url};"
));
};
};
locationModule = { config, nixosConfig, name, virtualHost, xvars, gensokyo-zone, lib, ... }: let
inherit (gensokyo-zone.lib) mkJustBefore mkJustAfter mkAlmostOptionDefault mapOptionDefaults coalesce parseUrl;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkMerge mkBefore mkOptionDefault;
inherit (lib.attrsets) filterAttrs mapAttrsToList;
inherit (lib.lists) optional;
inherit (lib.strings) hasPrefix removeSuffix optionalString concatStringsSep;
inherit (lib.trivial) mapNullable;
inherit (nixosConfig.services) nginx;
cfg = config.proxy;
in {
imports = [ proxyModule ];
options = with lib.types; {
proxy = {
enabled = mkOption {
type = bool;
readOnly = true;
};
inheritServerDefaults = mkOption {
type = bool;
default = true;
};
path = mkOption {
type = str;
};
host = mkOption {
type = nullOr str;
};
websocket.enable = mkEnableOption "websocket proxy" // {
default = cfg.inheritServerDefaults && virtualHost.proxy.websocket.enable;
};
parsed = {
scheme = mkOption {
type = nullOr str;
};
path = mkOption {
type = nullOr str;
};
host = mkOption {
type = nullOr str;
};
port = mkOption {
type = nullOr int;
};
};
headers = {
enableRecommended = mkOption {
type = enum [ true false "nixpkgs" ];
};
rewriteReferer.enable = mkEnableOption "rewrite referer host";
set = mkOption {
type = attrsOf (nullOr str);
};
hide = mkOption {
type = attrsOf bool;
default = { };
};
};
redirect = {
enable = mkEnableOption "proxy_redirect";
fromHost = mkOption {
type = str;
default = xvars.get.host;
example = "xvars.get.proxy_host";
};
fromScheme = mkOption {
type = str;
default = xvars.get.scheme;
example = "xvars.get.proxy_scheme";
};
};
};
};
config = let
emitHeaders = setHeaders' != { };
url = parseUrl config.proxyPass;
upstream = nginx.upstreams'.${cfg.upstream};
upstreamServer = upstream.servers.${upstream.defaultServerName};
dynamicUpstream = hasPrefix "$" cfg.upstream;
hasUpstream = cfg.upstream != null && !dynamicUpstream;
hasUpstreamServer = upstream.defaultServerName != null;
recommendedHeaders = {
Host = if cfg.host == null then xvars.get.proxy_hostport else cfg.host;
Referer = xvars.get.referer;
X-Real-IP = xvars.get.remote_addr;
X-Forwarded-For = xvars.get.forwarded_for;
X-Forwarded-Proto = xvars.get.scheme;
X-Forwarded-Host = xvars.get.host;
X-Forwarded-Server = xvars.get.forwarded_server;
};
schemePort = {
http = 80;
https = 443;
}.${cfg.parsed.scheme} or (throw "unsupported proxy_scheme ${toString cfg.parsed.scheme}");
upstreamHost = coalesce ([ upstream.host ] ++ optional hasUpstreamServer upstreamServer.addr);
port = coalesce [ cfg.parsed.port schemePort ];
hostport = cfg.parsed.host + optionalString (port != schemePort) ":${toString port}";
initProxyVars = let
initScheme = xvars.init "proxy_scheme" config.xvars.defaults.proxy_scheme;
initHost = xvars.init "proxy_host" config.xvars.defaults.proxy_host;
initPort = xvars.init "proxy_port" config.xvars.defaults.proxy_port;
initHostPort = xvars.init "proxy_hostport" config.xvars.defaults.proxy_hostport;
initUpstream = ''
${initScheme}
${initHost}
${initPort}
${initHostPort}
'';
initDynamic = ''
${initScheme}
${xvars.init "proxy_host" "$proxy_host"}
if (${xvars.get.proxy_host} = "") {
${initHost}
}
${xvars.init "proxy_port" "$proxy_port"}
if (${xvars.get.proxy_port} = "") {
${initPort}
}
${xvars.init "proxy_hostport" "${xvars.get.proxy_host}:${xvars.get.proxy_port}"}
if (${xvars.get.proxy_port} = ${toString schemePort}) {
${xvars.init "proxy_hostport" xvars.get.proxy_host}
}
if (${xvars.get.proxy_port} = "") {
${xvars.init "proxy_hostport" xvars.get.proxy_host}
}
'';
init = if cfg.upstream != null then initUpstream else initDynamic;
in init;
hostHeader = coalesce [
cfg.headers.set.Host or null
cfg.host
xvars.get.proxy_hostport
];
rewriteReferer = ''
if (${xvars.get.referer_host} = $host) {
${xvars.init "referer" "${xvars.get.proxy_scheme}://${hostHeader}${xvars.get.referer_path}"}
}
'';
redirect = ''
proxy_redirect ${cfg.redirect.fromScheme}://${cfg.redirect.fromHost}/ ${xvars.get.scheme}://${xvars.get.host}/;
'';
setHeaders' = filterAttrs (_: header: header != null) cfg.headers.set;
setHeaders = concatStringsSep "\n" (mapAttrsToList (
name: value: "proxy_set_header ${name} ${xvars.escapeString value};"
) setHeaders');
hideHeaders = mapAttrsToList (header: hide: mkIf hide "proxy_hide_header ${xvars.escapeString header};") cfg.headers.hide;
in {
xvars = {
enable = mkIf cfg.headers.rewriteReferer.enable true;
defaults = mkIf cfg.enabled (mapOptionDefaults {
proxy_scheme = cfg.parsed.scheme;
proxy_host = cfg.parsed.host;
proxy_port = toString port;
proxy_hostport = hostport;
});
};
proxy = {
enabled = mkOptionDefault (config.proxyPass != null);
path = mkIf (hasPrefix "/" name) (mkOptionDefault name);
url = mkIf (cfg.inheritServerDefaults && virtualHost.proxy.url != null) (mkOptionDefault virtualHost.proxy.url);
headers = {
enableRecommended = mkOptionDefault (
if cfg.enable && (!cfg.inheritServerDefaults || virtualHost.proxy.headers.enableRecommended != false) then true
else if cfg.inheritServerDefaults then virtualHost.proxy.headers.enableRecommended
else if nginx.recommendedProxySettings then "nixpkgs" else false
);
set = mkMerge [
(mkOptionDefault { })
(mkIf (cfg.headers.enableRecommended == true) (mapOptionDefaults recommendedHeaders))
(mkIf (cfg.host != null) {
Host = mkIf (cfg.headers.enableRecommended != "nixpkgs") (mkAlmostOptionDefault cfg.host);
})
(mkIf cfg.headers.rewriteReferer.enable {
Referer = mkAlmostOptionDefault xvars.get.referer;
})
(mkIf cfg.websocket.enable (mapOptionDefaults {
Upgrade = "$http_upgrade";
Connection = "upgrade";
}))
];
};
host = mkOptionDefault (
if cfg.inheritServerDefaults && virtualHost.proxy.host != null then virtualHost.proxy.host
else if cfg.headers.enableRecommended == false then null
else xvars.get.host
);
parsed = {
scheme = mkOptionDefault (
mapNullable (_: url.scheme) config.proxyPass
);
path = mkOptionDefault (
mapNullable (_: url.path) config.proxyPass
);
host = mkOptionDefault (
if hasUpstream then assert url.host == upstream.name; upstreamHost
else mapNullable (_: url.host) config.proxyPass
);
port = mkOptionDefault (
if hasUpstream && hasUpstreamServer && url.port == null then assert url.host == upstream.name; upstreamServer.port
else mapNullable (_: url.port) config.proxyPass
);
};
};
proxyPass = mkIf cfg.enable (mkAlmostOptionDefault (removeSuffix "/" cfg.url + cfg.path));
recommendedProxySettings = mkAlmostOptionDefault (cfg.headers.enableRecommended == "nixpkgs");
extraConfig = mkIf cfg.enabled (mkMerge ([
(mkIf virtualHost.xvars.enable (mkJustBefore initProxyVars))
(mkIf (cfg.headers.rewriteReferer.enable) (mkJustBefore rewriteReferer))
(mkIf (cfg.redirect.enable) (mkBefore redirect))
(mkIf (emitHeaders) (mkJustAfter setHeaders))
(mkIf cfg.websocket.enable "proxy_cache_bypass $http_upgrade;")
] ++ hideHeaders));
};
};
hostModule = { config, nixosConfig, gensokyo-zone, lib, ... }: let
inherit (gensokyo-zone.lib) mapOptionDefaults mapAlmostOptionDefaults;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.modules) mkIf mkOptionDefault;
inherit (lib.attrsets) attrValues;
inherit (lib.lists) any;
inherit (nixosConfig.services) nginx;
cfg = config.proxy;
anyLocations = f: any (loc: loc.enable && f loc) (attrValues config.locations);
in {
options = with lib.types; {
proxy = {
host = mkOption {
type = nullOr str;
default = null;
};
url = mkOption {
type = nullOr str;
default = null;
};
copyFromVhost = mkOption {
type = nullOr str;
default = null;
};
websocket.enable = mkEnableOption "websocket proxy";
headers.enableRecommended = mkOption {
type = enum [ true false "nixpkgs" ];
default = if nginx.recommendedProxySettings then "nixpkgs" else false;
};
};
locations = mkOption {
type = attrsOf (submoduleWith {
modules = [ locationModule ];
shorthandOnlyDefinesConfig = true;
});
};
};
config = let
needsReferer = loc: loc.proxy.enabled && loc.proxy.headers.rewriteReferer.enable;
confCopy = let
proxyHost = nginx.virtualHosts.${cfg.copyFromVhost};
in mapAlmostOptionDefaults {
inherit (proxyHost.proxy) host url upstream;
} // {
websocket = mapAlmostOptionDefaults {
inherit (proxyHost.proxy.websocket) enable;
};
headers = mapAlmostOptionDefaults {
inherit (proxyHost.proxy.headers) enableRecommended;
};
};
in {
xvars = {
parseReferer = mkIf (anyLocations needsReferer) true;
defaults = mkIf (anyLocations (loc: loc.proxy.enabled)) (mkOptionDefault (mapOptionDefaults rec {
proxy_scheme = null;
proxy_host = "$proxy_host";
proxy_port = "$proxy_port";
proxy_hostport = "${proxy_host}:${proxy_port}";
}));
};
proxy = mkIf (cfg.copyFromVhost != null) confCopy;
};
};
in {
lib,
...
}: let
inherit (lib.options) mkOption;
in {
options.services.nginx = with lib.types; {
virtualHosts = mkOption {
type = attrsOf (submodule [hostModule]);
};
stream.servers = mkOption {
type = attrsOf (submoduleWith {
modules = [serverModule];
shorthandOnlyDefinesConfig = false;
});
};
};
}