mirror of
https://github.com/CHN-beta/nixos.git
synced 2026-01-11 17:09:22 +08:00
394 lines
17 KiB
Nix
394 lines
17 KiB
Nix
inputs:
|
|
{
|
|
options.nixos.services.nginx.https = let inherit (inputs.lib) mkOption types; in mkOption
|
|
{
|
|
type = types.attrsOf (types.submodule (siteSubmoduleInputs: { options =
|
|
{
|
|
global =
|
|
{
|
|
configName = mkOption
|
|
{ type = types.nonEmptyStr; default = "https:${siteSubmoduleInputs.config._module.args.name}"; };
|
|
root = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
index = mkOption
|
|
{
|
|
type = types.nullOr (types.oneOf [ (types.enum [ "auto" ]) (types.nonEmptyListOf types.nonEmptyStr) ]);
|
|
default = null;
|
|
};
|
|
charset = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
detectAuth = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{
|
|
text = mkOption { type = types.nonEmptyStr; default = "Restricted Content"; };
|
|
users = mkOption { type = types.nonEmptyListOf types.nonEmptyStr; };
|
|
};});
|
|
default = null;
|
|
};
|
|
rewriteHttps = mkOption { type = types.bool; default = true; };
|
|
tlsCert = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
};
|
|
listen = mkOption
|
|
{
|
|
type = types.attrsOf (types.submodule { options =
|
|
{
|
|
http2 = mkOption { type = types.bool; default = true; };
|
|
proxyProtocol = mkOption { type = types.bool; default = true; };
|
|
# if proxyProtocol not enabled, add to transparentProxy only
|
|
# if proxyProtocol enabled, add to transparentProxy and streamProxy
|
|
addToTransparentProxy = mkOption { type = types.bool; default = true; };
|
|
};});
|
|
default.main = {};
|
|
};
|
|
location = mkOption
|
|
{
|
|
type = types.attrsOf (types.submodule { options =
|
|
let genericOptions =
|
|
{
|
|
# should be set to non null value if global root is null
|
|
root = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
detectAuth = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{
|
|
text = mkOption { type = types.nonEmptyStr; default = "Restricted Content"; };
|
|
users = mkOption { type = types.nonEmptyListOf types.nonEmptyStr; };
|
|
};});
|
|
default = null;
|
|
};
|
|
};
|
|
in
|
|
{
|
|
# only one should be specified
|
|
proxy = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{
|
|
inherit (genericOptions) detectAuth;
|
|
upstream = mkOption { type = types.nonEmptyStr; };
|
|
websocket = mkOption { type = types.bool; default = false; };
|
|
grpc = mkOption { type = types.bool; default = false; };
|
|
setHeaders = mkOption
|
|
{ type = types.attrsOf types.str; default.Host = siteSubmoduleInputs.config._module.args.name; };
|
|
# echo -n "username:password" | base64
|
|
addAuth = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
};});
|
|
default = null;
|
|
};
|
|
static = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{
|
|
inherit (genericOptions) detectAuth root;
|
|
index = mkOption
|
|
{
|
|
type = types.nullOr
|
|
(types.oneOf [ (types.enum [ "auto" ]) (types.nonEmptyListOf types.nonEmptyStr) ]);
|
|
default = null;
|
|
};
|
|
charset = mkOption { type = types.nullOr types.nonEmptyStr; default = null; };
|
|
tryFiles = mkOption { type = types.nullOr (types.nonEmptyListOf types.nonEmptyStr); default = null; };
|
|
webdav = mkOption { type = types.bool; default = false; };
|
|
};});
|
|
default = null;
|
|
};
|
|
php = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{ inherit (genericOptions) detectAuth root; fastcgiPass = mkOption { type = types.nonEmptyStr; };};});
|
|
default = null;
|
|
};
|
|
return = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options = { return = mkOption { type = types.nonEmptyStr; }; };});
|
|
default = null;
|
|
};
|
|
alias = mkOption
|
|
{
|
|
type = types.nullOr (types.submodule { options =
|
|
{
|
|
path = mkOption { type = types.nonEmptyStr; };
|
|
};});
|
|
default = null;
|
|
};
|
|
};});
|
|
default = {};
|
|
};
|
|
};}));
|
|
default = {};
|
|
};
|
|
config = let inherit (inputs.config.nixos.services) nginx; in inputs.lib.mkIf (nginx.https != {}) (inputs.lib.mkMerge
|
|
[
|
|
# https assertions
|
|
{
|
|
# only one type should be specified in each location
|
|
assertions =
|
|
(
|
|
(builtins.map
|
|
(location:
|
|
{
|
|
assertion = 1 >= (inputs.lib.count (x: x != null)
|
|
(builtins.map (type: location.value.${type}) nginx.global.httpsLocationTypes));
|
|
message = "Only one type shuold be specified in ${location.name}";
|
|
})
|
|
(builtins.concatLists (inputs.lib.mapAttrsToList
|
|
(sn: sv: (inputs.lib.mapAttrsToList (ln: lv: inputs.lib.nameValuePair "${sn} ${ln}" lv) sv.location))
|
|
nginx.https)))
|
|
# root should be specified either in global or in each location
|
|
++ (builtins.map
|
|
(location:
|
|
{
|
|
assertion = (location.value.root or "") != null;
|
|
message = "Root should be specified in ${location.name}";
|
|
})
|
|
(builtins.concatLists (builtins.map
|
|
(site: (inputs.lib.mapAttrsToList
|
|
(n: v: inputs.lib.nameValuePair "${site.name} ${n}" v)
|
|
site.value.location))
|
|
(builtins.filter (site: site.value.global.root == null) (inputs.localLib.attrsToList nginx.https)))))
|
|
);
|
|
}
|
|
# https
|
|
(
|
|
# merge different types of locations
|
|
let sites = inputs.lib.mapAttrsToList
|
|
(sn: sv: inputs.lib.nameValuePair sn
|
|
{
|
|
inherit (sv) global;
|
|
listens = builtins.attrValues sv.listen;
|
|
locations = inputs.lib.mapAttrsToList
|
|
(ln: lv: inputs.lib.nameValuePair ln
|
|
(
|
|
let _ = builtins.head (builtins.filter (type: type.value != null) (inputs.localLib.attrsToList lv));
|
|
in _.value // { type = _.name; }
|
|
))
|
|
sv.location;
|
|
})
|
|
nginx.https;
|
|
in
|
|
{
|
|
services.nginx.virtualHosts = builtins.listToAttrs (builtins.map
|
|
(site:
|
|
{
|
|
name = site.value.global.configName;
|
|
value =
|
|
{
|
|
serverName = site.name;
|
|
root = inputs.lib.mkIf (site.value.global.root != null) site.value.global.root;
|
|
basicAuthFile = inputs.lib.mkIf (site.value.global.detectAuth != null)
|
|
(
|
|
let secret = "nginx/templates/detectAuth/${inputs.lib.strings.escapeURL site.name}-global";
|
|
in inputs.config.nixos.system.sops.templates.${secret}.path
|
|
);
|
|
extraConfig = builtins.concatStringsSep "\n"
|
|
(
|
|
(
|
|
let inherit (site.value.global) index; in
|
|
if (builtins.typeOf index == "list") then [ "index ${builtins.concatStringsSep " " index};" ]
|
|
else if (index == "auto") then [ "autoindex on;" ]
|
|
else []
|
|
)
|
|
++ (
|
|
let inherit (site.value.global) detectAuth;
|
|
in inputs.lib.optionals (detectAuth != null) [ ''auth_basic "${detectAuth.text}"'' ]
|
|
)
|
|
++ (
|
|
let inherit (site.value.global) charset;
|
|
in inputs.lib.optionals (charset != null) [ "charset ${charset};" ]
|
|
)
|
|
);
|
|
listen = builtins.map
|
|
(listen:
|
|
{
|
|
addr = if listen.proxyProtocol then "0.0.0.0" else "127.0.0.1";
|
|
port = with nginx.global; httpsPort
|
|
+ (if listen.http2 then httpsPortShift.http2 else 0)
|
|
+ (if listen.proxyProtocol then httpsPortShift.proxyProtocol else 0);
|
|
ssl = true;
|
|
proxyProtocol = listen.proxyProtocol;
|
|
extraParameters = inputs.lib.mkIf listen.http2 [ "http2" ];
|
|
})
|
|
site.value.listens;
|
|
# do not automatically add http2 listen
|
|
http2 = false;
|
|
onlySSL = true;
|
|
useACMEHost = inputs.lib.mkIf (site.value.global.tlsCert == null) site.name;
|
|
sslCertificate = inputs.lib.mkIf (site.value.global.tlsCert != null)
|
|
"${site.value.global.tlsCert}/fullchain.pem";
|
|
sslCertificateKey = inputs.lib.mkIf (site.value.global.tlsCert != null)
|
|
"${site.value.global.tlsCert}/privkey.pem";
|
|
locations = builtins.listToAttrs (builtins.map
|
|
(location:
|
|
{
|
|
inherit (location) name;
|
|
value =
|
|
{
|
|
basicAuthFile = inputs.lib.mkIf (location.value.detectAuth or null != null)
|
|
(
|
|
let
|
|
inherit (inputs.lib.strings) escapeURL;
|
|
secret = "nginx/templates/detectAuth/${escapeURL site.name}/${escapeURL location.name}";
|
|
in inputs.config.nixos.system.sops.templates.${secret}.path
|
|
);
|
|
root = inputs.lib.mkIf (location.value.root or null != null) location.value.root;
|
|
}
|
|
// {
|
|
proxy =
|
|
{
|
|
proxyWebsockets = location.value.websocket;
|
|
extraConfig = builtins.concatStringsSep "\n"
|
|
(
|
|
[ "${if location.value.grpc then "grpc" else "proxy"}_pass ${location.value.upstream};" ]
|
|
++ (inputs.lib.mapAttrsToList (n: v: ''proxy_set_header ${n} "${v}";'')
|
|
location.value.setHeaders)
|
|
++ (inputs.lib.optionals
|
|
(location.value.detectAuth != null || site.value.global.detectAuth != null)
|
|
[ "proxy_hide_header Authorization;" ]
|
|
)
|
|
++ (inputs.lib.optionals (location.value.addAuth != null)
|
|
(
|
|
let authFile = "nginx/templates/addAuth/${location.value.addAuth}";
|
|
in [ "include ${inputs.config.nixos.system.sops.templates.${authFile}.path};" ]
|
|
))
|
|
);
|
|
};
|
|
static =
|
|
{
|
|
index = inputs.lib.mkIf (builtins.typeOf location.value.index == "list")
|
|
(builtins.concatStringsSep " " location.value.index);
|
|
tryFiles = inputs.lib.mkIf (location.value.tryFiles != null)
|
|
(builtins.concatStringsSep " " location.value.tryFiles);
|
|
extraConfig = inputs.lib.mkMerge
|
|
[
|
|
(inputs.lib.mkIf (location.value.index == "auto") "autoindex on;")
|
|
(inputs.lib.mkIf (location.value.charset != null) "charset ${location.value.charset};")
|
|
(inputs.lib.mkIf location.value.webdav
|
|
''
|
|
dav_access user:rw group:rw;
|
|
dav_methods PUT DELETE MKCOL COPY MOVE;
|
|
dav_ext_methods PROPFIND OPTIONS;
|
|
create_full_put_path on;
|
|
'')
|
|
];
|
|
};
|
|
php.extraConfig =
|
|
''
|
|
fastcgi_pass ${location.value.fastcgiPass};
|
|
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
include ${inputs.config.services.nginx.package}/conf/fastcgi.conf;
|
|
'';
|
|
return.return = location.value.return;
|
|
alias.alias = location.value.path;
|
|
}.${location.value.type};
|
|
})
|
|
site.value.locations);
|
|
};
|
|
})
|
|
sites);
|
|
nixos =
|
|
{
|
|
services =
|
|
{
|
|
nginx =
|
|
# { name = domain; value = listen = { http2 = xxx, proxyProtocol = xxx }; }
|
|
let listens = builtins.filter
|
|
(listen: listen.value.addToTransparentProxy)
|
|
(builtins.concatLists (builtins.map
|
|
(site: builtins.map (listen: { inherit (site) name; value = listen; }) site.value.listens)
|
|
sites));
|
|
in
|
|
{
|
|
transparentProxy.map = builtins.listToAttrs (builtins.map
|
|
(site:
|
|
{
|
|
inherit (site) name;
|
|
value = with nginx.global; httpsPort + (if site.value.http2 then httpsPortShift.http2 else 0);
|
|
})
|
|
(builtins.filter (listen: !listen.value.proxyProtocol) listens));
|
|
streamProxy.map = builtins.listToAttrs (builtins.map
|
|
(site:
|
|
{
|
|
inherit (site) name;
|
|
value =
|
|
{
|
|
upstream.port = with nginx.global; httpsPort + httpsPortShift.proxyProtocol
|
|
+ (if site.value.http2 then httpsPortShift.http2 else 0);
|
|
proxyProtocol = true;
|
|
rewriteHttps = inputs.lib.mkDefault false;
|
|
};
|
|
})
|
|
(builtins.filter (listen: listen.value.proxyProtocol) listens));
|
|
http = builtins.listToAttrs (builtins.map
|
|
(site: { inherit (site) name; value.rewriteHttps = {}; })
|
|
(builtins.filter (site: site.value.global.rewriteHttps) sites));
|
|
};
|
|
acme.cert = builtins.listToAttrs (builtins.map
|
|
(site: { inherit (site) name; value.group = inputs.config.services.nginx.group; })
|
|
sites);
|
|
};
|
|
system.sops =
|
|
let
|
|
inherit (inputs.lib.strings) escapeURL;
|
|
detectAuthUsers = builtins.concatLists (builtins.map
|
|
(site:
|
|
(
|
|
(builtins.map
|
|
(location:
|
|
{
|
|
name = "${escapeURL site.name}/${escapeURL location.name}";
|
|
value = location.value.detectAuth.users;
|
|
})
|
|
(builtins.filter (location: location.value.detectAuth or null != null) site.value.locations))
|
|
++ (inputs.lib.optionals (site.value.global.detectAuth != null)
|
|
[ { name = "${escapeURL site.name}-global"; value = site.value.global.detectAuth.users; } ])
|
|
))
|
|
sites);
|
|
addAuth = builtins.concatLists (builtins.map
|
|
(site: builtins.map
|
|
(location:
|
|
{
|
|
name = "${escapeURL site.name}/${escapeURL location.name}";
|
|
value = location.value.addAuth;
|
|
})
|
|
(builtins.filter (location: location.value.addAuth or null != null) site.value.locations)
|
|
)
|
|
sites);
|
|
in
|
|
{
|
|
templates = let inherit (inputs.config.nixos.system.sops) placeholder; in builtins.listToAttrs
|
|
(
|
|
(builtins.map
|
|
(detectAuth: inputs.lib.nameValuePair "nginx/templates/detectAuth/${detectAuth.name}"
|
|
{
|
|
owner = inputs.config.users.users.nginx.name;
|
|
content = builtins.concatStringsSep "\n" (builtins.map
|
|
(user: "${user}:{PLAIN}${placeholder."nginx/detectAuth/${user}"}")
|
|
detectAuth.value);
|
|
})
|
|
detectAuthUsers)
|
|
++ (builtins.map
|
|
(addAuth: inputs.lib.nameValuePair "nginx/templates/addAuth/${addAuth.name}"
|
|
{
|
|
owner = inputs.config.users.users.nginx.name;
|
|
content =
|
|
''proxy_set_header Authorization "Basic ${placeholder."nginx/addAuth/${addAuth.value}"}";'';
|
|
})
|
|
addAuth)
|
|
);
|
|
secrets = builtins.listToAttrs
|
|
(
|
|
(builtins.map
|
|
(secret: { name = "nginx/detectAuth/${secret}"; value = {}; })
|
|
(inputs.lib.unique (builtins.concatLists (builtins.map (detectAuth: detectAuth.value)
|
|
detectAuthUsers))))
|
|
++ (builtins.map
|
|
(secret: { name = "nginx/addAuth/${secret}"; value = {}; })
|
|
(inputs.lib.unique (builtins.map (addAuth: addAuth.value) addAuth)))
|
|
);
|
|
};
|
|
};
|
|
}
|
|
)
|
|
]);
|
|
}
|