{ config, inputs, lib, namespace, ... }: let inherit (lib) mkIf mkEnableOption mapAttrsToList listToAttrs replaceStrings mkMerge concatStringsSep filterAttrs optionalString; inherit (lib.${namespace}) getAttrByNamespace mkOptionsWithNamespace resolveHostIP isGateway flattenHostServices; inherit (config.networking) hostName; inherit (config.sops) secrets; base = "${namespace}.services.networking.httpd"; cfg = getAttrByNamespace config base; networkingCfg = getAttrByNamespace config "${namespace}.services.networking"; miasmaCfg = getAttrByNamespace config "${namespace}.services.apps.miasma"; networkServices = networkingCfg.network-services; networkServicesFlat = flattenHostServices networkServices; mkLocalhostConfig = name: service: let p = toString service.port; in '' # --- ${name} (localhost access) --- RewriteCond %{HTTP_HOST} ^${name}\.localhost$ [NC] RewriteRule ^/(.*) http://localhost:${p}/$1 [P,L] ProxyPassReverse / http://localhost:${p}/ ''; mkVirtualHost = { domain, useSSL, enableAcme ? false, injectHoneypot ? false, }: host: name: service: let hostIP = resolveHostIP networkingCfg.devices host; p = toString service.port; certs = replaceStrings ["*" "."] ["wildcard" "_"] domain; sslConfig = if useSSL then { forceSSL = true; } // ( if enableAcme then { useACMEHost = certs; } else { sslServerCert = secrets."ssl/${certs}/cert".path; sslServerKey = secrets."ssl/${certs}/key".path; } ) else {}; miasma = networkServicesFlat.miasma; honeypotConfig = optionalString injectHoneypot '' AddOutputFilterByType SUBSTITUTE text/html SubstituteMaxLineLength 30m Substitute 's|||i' ProxyPass ${miasmaCfg.linkPrefix}/ http://${resolveHostIP networkingCfg.devices miasma.host}:${toString miasma.port}/ ProxyPassReverse ${miasmaCfg.linkPrefix}/ http://${resolveHostIP networkingCfg.devices miasma.host}:${toString miasma.port}/ ProxyPass /robots.txt ! Alias /robots.txt ${inputs.dotfiles}/httpd/robots/${name}.txt ''; websocketConfig = optionalString service.websocket.enable '' # --- ${name} (websocket access) --- ProxyPass ${service.websocket.path} ws://${hostIP}:${p}${service.websocket.path} connectiontimeout=30 timeout=300 retry=0 ProxyPassReverse ${service.websocket.path} ws://${hostIP}:${p}${service.websocket.path} ''; in { name = replaceStrings ["*"] [name] domain; value = { acmeRoot = null; documentRoot = "/var/empty"; extraConfig = '' UseCanonicalName Off KeepAlive On MaxKeepAliveRequests 100 KeepAliveTimeout 5 RequestHeader set X-Forwarded-Proto "https" RequestHeader set X-Forwarded-Port "443" RequestHeader set X-Forwarded-For %{REMOTE_ADDR}s RewriteEngine On ProxyTimeout 300 ProxyPreserveHost On ${honeypotConfig} ${websocketConfig} # --- ${name} (subdomain access) --- ProxyPass / http://${hostIP}:${p}/ connectiontimeout=30 timeout=300 retry=0 ProxyPassReverse / http://${hostIP}:${p}/ ''; } // sslConfig; }; in { options = mkOptionsWithNamespace base { enable = mkEnableOption "Apache HTTPD"; }; config = mkIf cfg.enable { services.httpd = { enable = true; extraModules = [ "filter" "proxy" "proxy_http" "proxy_wstunnel" "rewrite" "substitute" ]; extraConfig = '' ErrorDocument 400 /400.html ErrorDocument 401 /401.html ErrorDocument 403 /403.html ErrorDocument 404 /404.html ErrorDocument 500 /500.html ErrorDocument 503 /503.html ''; virtualHosts = mkMerge [ { "localhost" = let localhostProxyConfig = networkServices.${hostName} |> mapAttrsToList mkLocalhostConfig |> concatStringsSep "\n"; in { acmeRoot = null; serverAliases = ["*.localhost"]; documentRoot = "/var/empty"; servedDirs = [ { dir = "/var/www/error"; urlPath = "/"; } ]; extraConfig = '' UseCanonicalName Off RewriteEngine On ${localhostProxyConfig} RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500|503)\.html$ RewriteRule ^ - [L,R=404] ''; }; } (networkServices.${hostName} |> filterAttrs (_: svc: svc.internal) |> mapAttrsToList (name: svc: (mkVirtualHost { domain = "*.yumeami.sh"; useSSL = true; }) hostName name svc) |> listToAttrs) (mkIf (isGateway networkingCfg.devices hostName) ( networkServicesFlat |> filterAttrs (_: svc: svc.public) |> mapAttrsToList (name: svc: (mkVirtualHost { domain = "*.cpatino.com"; useSSL = true; enableAcme = true; injectHoneypot = true; }) svc.host name svc) |> listToAttrs )) { "_default_" = { documentRoot = "/var/empty"; servedDirs = [ { dir = "/var/www/error"; urlPath = "/"; } ]; extraConfig = '' RewriteEngine On RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500|503)\.html$ RewriteRule ^ - [L,R=404] ''; }; } ( mkIf (isGateway networkingCfg.devices hostName) { "zzz-undefined.cpatino.com" = { serverAliases = ["*.cpatino.com"]; documentRoot = "/var/empty"; servedDirs = [ { dir = "/var/www/error"; urlPath = "/"; } ]; extraConfig = '' RewriteEngine On RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500|503)\.html$ RewriteRule ^ - [L,R=404] ''; forceSSL = true; useACMEHost = "wildcard_cpatino_com"; }; } ) { "zzz-undefined.yumeami.sh" = { serverAliases = ["*.yumeami.sh"]; documentRoot = "/var/empty"; servedDirs = [ { dir = "/var/www/error"; urlPath = "/"; } ]; extraConfig = '' RewriteEngine On RewriteCond %{REQUEST_URI} !^/(400|401|403|404|500|503)\.html$ RewriteRule ^ - [L,R=404] ''; forceSSL = true; sslServerCert = secrets."ssl/wildcard_yumeami_sh/cert".path; sslServerKey = secrets."ssl/wildcard_yumeami_sh/key".path; }; } ]; }; networking.firewall.allowedTCPPorts = [80 443]; systemd.tmpfiles.rules = [ "L+ /var/www 555 root root - ${inputs.dotfiles + "/httpd"}" ]; sops.secrets = let inherit (config.networking) hostName; inherit (config.users.users) wwwrun; hostSpecific = [ "ssl/wildcard_yumeami_sh/cert" "ssl/wildcard_yumeami_sh/key" ] |> map (name: { inherit name; value = { sopsFile = "${inputs.self}/secrets/sops/${hostName}.yaml"; owner = wwwrun.name; group = wwwrun.group; }; }); in listToAttrs hostSpecific; }; }