-{ lib, config, pkgs, ... }: with lib;
-let
- cfg = config.services.websites;
-in
-{
- options.services.websites = with types; {
- certs = mkOption {
- description = "Default websites configuration for certificates as accepted by acme";
- };
- env = mkOption {
- default = {};
- description = "Each type of website to enable will target a distinct httpd server";
- type = attrsOf (submodule {
- options = {
- enable = mkEnableOption "Enable websites of this type";
- adminAddr = mkOption {
- type = str;
- description = "Admin e-mail address of the instance";
- };
- httpdName = mkOption {
- type = str;
- description = "Name of the httpd instance to assign this type to";
- };
- ips = mkOption {
- type = listOf str;
- default = [];
- description = "ips to listen to";
- };
- modules = mkOption {
- type = listOf str;
- default = [];
- description = "Additional modules to load in Apache";
- };
- extraConfig = mkOption {
- type = listOf lines;
- default = [];
- description = "Additional configuration to append to Apache";
- };
- nosslVhost = mkOption {
- description = "A default nossl vhost for captive portals";
- default = {};
- type = submodule {
- options = {
- enable = mkEnableOption "Add default no-ssl vhost for this instance";
- host = mkOption {
- type = str;
- description = "The hostname to use for this vhost";
- };
- root = mkOption {
- type = path;
- default = ./nosslVhost;
- description = "The root folder to serve";
- };
- indexFile = mkOption {
- type = str;
- default = "index.html";
- description = "The index file to show.";
- };
- };
- };
- };
- fallbackVhost = mkOption {
- description = "The fallback vhost that will be defined as first vhost in Apache";
- type = submodule {
- options = {
- certName = mkOption { type = str; };
- hosts = mkOption { type = listOf str; };
- root = mkOption { type = nullOr path; };
- forceSSL = mkOption {
- type = bool;
- default = true;
- description = ''
- Automatically create a corresponding non-ssl vhost
- that will only redirect to the ssl version
- '';
- };
- extraConfig = mkOption { type = listOf lines; default = []; };
- };
- };
- };
- vhostNoSSLConfs = mkOption {
- default = {};
- description = "List of no ssl vhosts to define for Apache";
- type = attrsOf (submodule {
- options = {
- hosts = mkOption { type = listOf str; };
- root = mkOption { type = nullOr path; };
- extraConfig = mkOption { type = listOf lines; default = []; };
- };
- });
- };
- vhostConfs = mkOption {
- default = {};
- description = "List of vhosts to define for Apache";
- type = attrsOf (submodule {
- options = {
- certName = mkOption { type = str; };
- addToCerts = mkOption {
- type = bool;
- default = false;
- description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null";
- };
- certMainHost = mkOption {
- type = nullOr str;
- description = "Use that host as 'main host' for acme certs";
- default = null;
- };
- hosts = mkOption { type = listOf str; };
- root = mkOption { type = nullOr path; };
- forceSSL = mkOption {
- type = bool;
- default = true;
- description = ''
- Automatically create a corresponding non-ssl vhost
- that will only redirect to the ssl version
- '';
- };
- extraConfig = mkOption { type = listOf lines; default = []; };
- };
- });
- };
- watchPaths = mkOption {
- type = listOf str;
- default = [];
- description = ''
- Paths to watch that should trigger a reload of httpd
- '';
- };
- };
- });
- };
- };
-
- config.services.httpd = let
- nosslVhost = ips: cfg: {
- listen = map (ip: { inherit ip; port = 80; }) ips;
- hostName = cfg.host;
- logFormat = "combinedVhost";
- documentRoot = cfg.root;
- extraConfig = ''
- <Directory ${cfg.root}>
- DirectoryIndex ${cfg.indexFile}
- AllowOverride None
- Require all granted
-
- RewriteEngine on
- RewriteRule ^/(.+) / [L]
- </Directory>
- '';
- };
- toVhost = ips: vhostConf: {
- forceSSL = vhostConf.forceSSL or true;
- useACMEHost = vhostConf.certName;
- logFormat = "combinedVhost";
- listen = if vhostConf.forceSSL
- then lists.flatten (map (ip: [{ inherit ip; port = 443; ssl = true; } { inherit ip; port = 80; }]) ips)
- else map (ip: { inherit ip; port = 443; ssl = true; }) ips;
- hostName = builtins.head vhostConf.hosts;
- serverAliases = builtins.tail vhostConf.hosts or [];
- documentRoot = vhostConf.root;
- extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
- };
- toVhostNoSSL = ips: vhostConf: {
- logFormat = "combinedVhost";
- listen = map (ip: { inherit ip; port = 80; }) ips;
- hostName = builtins.head vhostConf.hosts;
- serverAliases = builtins.tail vhostConf.hosts or [];
- documentRoot = vhostConf.root;
- extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
- };
- in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
- icfg.httpdName (mkIf icfg.enable {
- enable = true;
- logPerVirtualHost = true;
- multiProcessingModule = "worker";
- # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4
- # test with https://www.ssllabs.com/ssltest/analyze.html?d=www.immae.eu&s=176.9.151.154&latest
- sslProtocols = "all -SSLv3 -TLSv1 -TLSv1.1";
- sslCiphers = builtins.concatStringsSep ":" [
- "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256"
- "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384"
- "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305"
- "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384"
- ];
- inherit (icfg) adminAddr;
- logFormat = "combinedVhost";
- extraModules = lists.unique icfg.modules;
- extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
-
- virtualHosts = with attrsets; {
- ___fallbackVhost = toVhost icfg.ips icfg.fallbackVhost;
- } // (optionalAttrs icfg.nosslVhost.enable {
- nosslVhost = nosslVhost icfg.ips icfg.nosslVhost;
- }) // (mapAttrs' (n: v: nameValuePair ("nossl_" + n) (toVhostNoSSL icfg.ips v)) icfg.vhostNoSSLConfs)
- // (mapAttrs' (n: v: nameValuePair ("ssl_" + n) (toVhost icfg.ips v)) icfg.vhostConfs);
- })
- ) cfg.env;
-
- config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
- "httpd${icfg.httpdName}" {
- paths = icfg.watchPaths;
- waitTime = 5;
- }
- ) cfg.env;
-
- config.security.acme.certs = let
- typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env;
- flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
- attrValues v.vhostConfs
- ) typesToManage);
- groupedCerts = attrsets.filterAttrs
- (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
- (lists.groupBy (v: v.certName) flatVhosts);
- groupToDomain = group:
- let
- nonNull = builtins.filter (v: !isNull v.certMainHost) group;
- domains = lists.unique (map (v: v.certMainHost) nonNull);
- in
- if builtins.length domains == 0
- then null
- else assert (builtins.length domains == 1); (elemAt domains 0);
- extraDomains = group:
- let
- mainDomain = groupToDomain group;
- in
- lists.remove mainDomain (
- lists.unique (
- lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
- )
- );
- in attrsets.mapAttrs (k: g:
- if (!isNull (groupToDomain g))
- then cfg.certs // {
- domain = groupToDomain g;
- extraDomains = builtins.listToAttrs (
- map (d: attrsets.nameValuePair d null) (extraDomains g));
- }
- else {
- extraDomains = builtins.listToAttrs (
- map (d: attrsets.nameValuePair d null) (extraDomains g));
- }
- ) groupedCerts;
-
- config.systemd.services = let
- package = httpdName: config.services.httpd.${httpdName}.package.out;
- cfgFile = httpdName: config.services.httpd.${httpdName}.configFile;
- serviceChange = attrsets.mapAttrs' (name: icfg:
- attrsets.nameValuePair
- "httpd${icfg.httpdName}" {
- stopIfChanged = false;
- serviceConfig.ExecStart =
- lib.mkForce "@${package icfg.httpdName}/bin/httpd httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf";
- serviceConfig.ExecStop =
- lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful-stop";
- serviceConfig.ExecReload =
- lib.mkForce "${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -k graceful";
- }
- ) cfg.env;
- serviceReload = attrsets.mapAttrs' (name: icfg:
- attrsets.nameValuePair
- "httpd${icfg.httpdName}-config-reload" {
- wants = [ "httpd${icfg.httpdName}.service" ];
- wantedBy = [ "multi-user.target" ];
- restartTriggers = [ (cfgFile icfg.httpdName) ];
- # commented, because can cause extra delays during activate for this config:
- # services.nginx.virtualHosts."_".locations."/".proxyPass = "http://blabla:3000";
- # stopIfChanged = false;
- serviceConfig.Type = "oneshot";
- serviceConfig.TimeoutSec = 60;
- script = ''
- if ${pkgs.systemd}/bin/systemctl -q is-active httpd${icfg.httpdName}.service ; then
- ${package icfg.httpdName}/bin/httpd -f /etc/httpd/httpd_${icfg.httpdName}.conf -t && \
- ${pkgs.systemd}/bin/systemctl reload httpd${icfg.httpdName}.service
- fi
- '';
- serviceConfig.RemainAfterExit = true;
- }
- ) cfg.env;
- in
- serviceChange // serviceReload;
-}