From: Ismaël Bouya Date: Sat, 4 Apr 2020 01:18:40 +0000 (+0200) Subject: Prepare upgrade to nixos 20.03 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=27794e1507ab5bd4b0f31278cf8049854790e4a7;p=perso%2FImmae%2FConfig%2FNix%2FNUR.git Prepare upgrade to nixos 20.03 --- diff --git a/modules/websites/default.nix b/modules/websites/default.nix index 3f46e65d..d5a0f635 100644 --- a/modules/websites/default.nix +++ b/modules/websites/default.nix @@ -82,6 +82,14 @@ in 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 = []; }; }; }; @@ -115,6 +123,14 @@ in }; 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 = []; }; }; }); @@ -143,26 +159,9 @@ in }; config.services.httpd = let - redirectVhost = ips: { # Should go last, catchall http -> https redirect - listen = map (ip: { inherit ip; port = 80; }) ips; - hostName = "redirectSSL"; - serverAliases = [ "*" ]; - enableSSL = false; - logFormat = "combinedVhost"; - documentRoot = "/var/lib/acme/acme-challenge"; - extraConfig = '' - RewriteEngine on - RewriteCond "%{REQUEST_URI}" "!^/\.well-known" - RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301] - # To redirect in specific "VirtualHost *:80", do - # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1 - # rather than rewrite - ''; - }; nosslVhost = ips: cfg: { listen = map (ip: { inherit ip; port = 80; }) ips; hostName = cfg.host; - enableSSL = false; logFormat = "combinedVhost"; documentRoot = cfg.root; extraConfig = '' @@ -177,19 +176,18 @@ in ''; }; toVhost = ips: vhostConf: { - enableSSL = true; - sslServerCert = "${config.security.acme.certs."${vhostConf.certName}".directory}/cert.pem"; - sslServerKey = "${config.security.acme.certs."${vhostConf.certName}".directory}/key.pem"; - sslServerChain = "${config.security.acme.certs."${vhostConf.certName}".directory}/chain.pem"; + forceSSL = vhostConf.forceSSL or true; + useACMEHost = vhostConf.certName; logFormat = "combinedVhost"; - listen = map (ip: { inherit ip; port = 443; }) ips; + 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: { - enableSSL = false; logFormat = "combinedVhost"; listen = map (ip: { inherit ip; port = 80; }) ips; hostName = builtins.head vhostConf.hosts; @@ -200,8 +198,6 @@ in in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair icfg.httpdName (mkIf icfg.enable { enable = true; - listen = map (ip: { inherit ip; port = 443; }) icfg.ips; - stateDir = "/run/httpd_${name}"; logPerVirtualHost = true; multiProcessingModule = "worker"; # https://ssl-config.mozilla.org/#server=apache&version=2.4.41&config=intermediate&openssl=1.0.2t&guideline=5.4 @@ -216,11 +212,13 @@ in logFormat = "combinedVhost"; extraModules = lists.unique icfg.modules; extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig; - virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ] - ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ] - ++ (attrsets.mapAttrsToList (n: v: toVhostNoSSL icfg.ips v) icfg.vhostNoSSLConfs) - ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs) - ++ [ (redirectVhost icfg.ips) ]; + + 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; diff --git a/modules/websites/httpd-service-builder.nix b/modules/websites/httpd-service-builder.nix index f0208ab5..ec79a90c 100644 --- a/modules/websites/httpd-service-builder.nix +++ b/modules/websites/httpd-service-builder.nix @@ -7,134 +7,56 @@ with lib; let - mainCfg = config.services.httpd."${httpdName}"; + cfg = config.services.httpd."${httpdName}"; - httpd = mainCfg.package.out; + runtimeDir = "/run/httpd_${httpdName}"; - httpdConf = mainCfg.configFile; + pkg = cfg.package.out; - php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ }; + httpdConf = cfg.configFile; - phpMajorVersion = head (splitString "." php.version); + php = cfg.phpPackage.override { apacheHttpd = pkg.dev; /* otherwise it only gets .out */ }; - mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; }; + phpMajorVersion = lib.versions.major (lib.getVersion php); - defaultListen = cfg: if cfg.enableSSL - then [{ip = "*"; port = 443;}] - else [{ip = "*"; port = 80;}]; + mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; }; - getListen = cfg: - if cfg.listen == [] - then defaultListen cfg - else cfg.listen; + vhosts = attrValues cfg.virtualHosts; - listenToString = l: "${l.ip}:${toString l.port}"; + mkListenInfo = hostOpts: + if hostOpts.listen != [] then hostOpts.listen + else ( + optional (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) { ip = "*"; port = 443; ssl = true; } ++ + optional (!hostOpts.onlySSL) { ip = "*"; port = 80; ssl = false; } + ); - extraModules = attrByPath ["extraModules"] [] mainCfg; - extraForeignModules = filter isAttrs extraModules; - extraApacheModules = filter isString extraModules; + listenInfo = unique (concatMap mkListenInfo vhosts); + enableHttp2 = any (vhost: vhost.http2) vhosts; + enableSSL = any (listen: listen.ssl) listenInfo; + enableUserDir = any (vhost: vhost.enableUserDir) vhosts; - makeServerInfo = cfg: { - # Canonical name must not include a trailing slash. - canonicalNames = - let defaultPort = (head (defaultListen cfg)).port; in - map (port: - (if cfg.enableSSL then "https" else "http") + "://" + - cfg.hostName + - (if port != defaultPort then ":${toString port}" else "") - ) (map (x: x.port) (getListen cfg)); - - # Admin address: inherit from the main server if not specified for - # a virtual host. - adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr; - - vhostConfig = cfg; - serverConfig = mainCfg; - fullConfig = config; # machine config - }; - - - allHosts = [mainCfg] ++ mainCfg.virtualHosts; - - - callSubservices = serverInfo: defs: - let f = svc: - let - svcFunction = - if svc ? function then svc.function - # instead of using serviceType="mediawiki"; you can copy mediawiki.nix to any location outside nixpkgs, modify it at will, and use serviceExpression=./mediawiki.nix; - else if svc ? serviceExpression then import (toString svc.serviceExpression) - else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix"); - config = (evalModules - { modules = [ { options = res.options; config = svc.config or svc; } ]; - check = false; - }).config; - defaults = { - extraConfig = ""; - extraModules = []; - extraModulesPre = []; - extraPath = []; - extraServerPath = []; - globalEnvVars = []; - robotsEntries = ""; - startupScript = ""; - enablePHP = false; - enablePerl = false; - phpOptions = ""; - options = {}; - documentRoot = null; - }; - res = defaults // svcFunction { inherit config lib pkgs serverInfo php; }; - in res; - in map f defs; - - - # !!! callSubservices is expensive - subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices; - - mainSubservices = subservicesFor mainCfg; - - allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts; - - - enableSSL = any (vhost: vhost.enableSSL) allHosts; - - - # Names of modules from ${httpd}/modules that we want to load. - apacheModules = - [ # HTTP authentication mechanisms: basic and digest. - "auth_basic" "auth_digest" - - # Authentication: is the user who he claims to be? - "authn_file" "authn_dbm" "authn_anon" "authn_core" - - # Authorization: is the user allowed access? - "authz_user" "authz_groupfile" "authz_host" "authz_core" - - # Other modules. - "ext_filter" "include" "log_config" "env" "mime_magic" - "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif" - "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs" - "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling" - "userdir" "alias" "rewrite" "proxy" "proxy_http" - "unixd" "cache" "cache_disk" "slotmem_shm" "socache_shmcb" - "mpm_${mainCfg.multiProcessingModule}" - - # For compatibility with old configurations, the new module mod_access_compat is provided. - "access_compat" + # NOTE: generally speaking order of modules is very important + modules = + [ # required apache modules our httpd service cannot run without + "authn_core" "authz_core" + "log_config" + "mime" "autoindex" "negotiation" "dir" + "alias" "rewrite" + "unixd" "slotmem_shm" "socache_shmcb" + "mpm_${cfg.multiProcessingModule}" ] - ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) + ++ (if cfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) + ++ optional enableHttp2 "http2" ++ optional enableSSL "ssl" - ++ extraApacheModules; - - - allDenied = "Require all denied"; - allGranted = "Require all granted"; - + ++ optional enableUserDir "userdir" + ++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } + ++ optional cfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } + ++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } + ++ cfg.extraModules; - loggingConf = (if mainCfg.logFormat != "none" then '' - ErrorLog ${mainCfg.logDir}/error.log + loggingConf = (if cfg.logFormat != "none" then '' + ErrorLog ${cfg.logDir}/error.log LogLevel notice @@ -143,255 +65,271 @@ let LogFormat "%{Referer}i -> %U" referer LogFormat "%{User-agent}i" agent - CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat} + CustomLog ${cfg.logDir}/access.log ${cfg.logFormat} '' else '' ErrorLog /dev/null ''); browserHacks = '' - BrowserMatch "Mozilla/2" nokeepalive - BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 - BrowserMatch "RealPlayer 4\.0" force-response-1.0 - BrowserMatch "Java/1\.0" force-response-1.0 - BrowserMatch "JDK/1\.0" force-response-1.0 - BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully - BrowserMatch "^WebDrive" redirect-carefully - BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully - BrowserMatch "^gnome-vfs" redirect-carefully + + BrowserMatch "Mozilla/2" nokeepalive + BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 + BrowserMatch "RealPlayer 4\.0" force-response-1.0 + BrowserMatch "Java/1\.0" force-response-1.0 + BrowserMatch "JDK/1\.0" force-response-1.0 + BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully + BrowserMatch "^WebDrive" redirect-carefully + BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully + BrowserMatch "^gnome-vfs" redirect-carefully + ''; sslConf = '' - SSLSessionCache shmcb:${mainCfg.stateDir}/ssl_scache(512000) + + SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000) - Mutex posixsem + Mutex posixsem - SSLRandomSeed startup builtin - SSLRandomSeed connect builtin + SSLRandomSeed startup builtin + SSLRandomSeed connect builtin - SSLProtocol ${mainCfg.sslProtocols} - SSLCipherSuite ${mainCfg.sslCiphers} - SSLHonorCipherOrder on + SSLProtocol ${cfg.sslProtocols} + SSLCipherSuite ${cfg.sslCiphers} + SSLHonorCipherOrder on + ''; mimeConf = '' - TypesConfig ${httpd}/conf/mime.types + TypesConfig ${pkg}/conf/mime.types AddType application/x-x509-ca-cert .crt AddType application/x-pkcs7-crl .crl AddType application/x-httpd-php .php .phtml - MIMEMagicFile ${httpd}/conf/magic + MIMEMagicFile ${pkg}/conf/magic ''; - - perServerConf = isMainServer: cfg: let - - serverInfo = makeServerInfo cfg; - - subservices = callSubservices serverInfo cfg.extraSubservices; - - maybeDocumentRoot = fold (svc: acc: - if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc - ) null ([ cfg ] ++ subservices); - - documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else - pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"; - - documentRootConf = '' - DocumentRoot "${documentRoot}" - - - Options Indexes FollowSymLinks - AllowOverride None - ${allGranted} - - ''; - - robotsTxt = - concatStringsSep "\n" (filter (x: x != "") ( - # If this is a vhost, the include the entries for the main server as well. - (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices) - ++ [cfg.robotsEntries] - ++ (map (svc: svc.robotsEntries) subservices))); - - in '' - ${concatStringsSep "\n" (map (n: "ServerName ${n}") serverInfo.canonicalNames)} - - ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases} - - ${if cfg.sslServerCert != null then '' - SSLCertificateFile ${cfg.sslServerCert} - SSLCertificateKeyFile ${cfg.sslServerKey} - ${if cfg.sslServerChain != null then '' - SSLCertificateChainFile ${cfg.sslServerChain} - '' else ""} - '' else ""} - - ${if cfg.enableSSL then '' - SSLEngine on - '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */ - '' - SSLEngine off - '' else ""} - - ${if isMainServer || cfg.adminAddr != null then '' - ServerAdmin ${cfg.adminAddr} - '' else ""} - - ${if !isMainServer && mainCfg.logPerVirtualHost then '' - ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log - CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat} - '' else ""} - - ${optionalString (robotsTxt != "") '' - Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt} - ''} - - ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""} - - ${if cfg.enableUserDir then '' - - UserDir public_html - UserDir disabled root - - - AllowOverride FileInfo AuthConfig Limit Indexes - Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec - - ${allGranted} - - - ${allDenied} - - - - '' else ""} - - ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then '' - RedirectPermanent / ${cfg.globalRedirect} - '' else ""} - - ${ - let makeFileConf = elem: '' - Alias ${elem.urlPath} ${elem.file} - ''; - in concatMapStrings makeFileConf cfg.servedFiles - } - - ${ - let makeDirConf = elem: '' - Alias ${elem.urlPath} ${elem.dir}/ - - Options +Indexes - ${allGranted} - AllowOverride All - - ''; - in concatMapStrings makeDirConf cfg.servedDirs - } - - ${concatMapStrings (svc: svc.extraConfig) subservices} - - ${cfg.extraConfig} - ''; + mkVHostConf = hostOpts: + let + adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr; + listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts); + listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts); + + useACME = hostOpts.enableACME || hostOpts.useACMEHost != null; + sslCertDir = + if hostOpts.enableACME then config.security.acme.certs.${hostOpts.hostName}.directory + else if hostOpts.useACMEHost != null then config.security.acme.certs.${hostOpts.useACMEHost}.directory + else abort "This case should never happen."; + + sslServerCert = if useACME then "${sslCertDir}/full.pem" else hostOpts.sslServerCert; + sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey; + sslServerChain = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerChain; + + acmeChallenge = optionalString useACME '' + Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/" + + AllowOverride None + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + Require method GET POST OPTIONS + Require all granted + + ''; + in + optionalString (listen != []) '' + + ServerName ${hostOpts.hostName} + ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} + ServerAdmin ${adminAddr} + + SSLEngine off + + ${acmeChallenge} + ${if hostOpts.forceSSL then '' + + RewriteEngine on + RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC] + RewriteCond %{HTTPS} off + RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} + + '' else mkVHostCommonConf hostOpts} + + '' + + optionalString (listenSSL != []) '' + + ServerName ${hostOpts.hostName} + ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases} + ServerAdmin ${adminAddr} + SSLEngine on + SSLCertificateFile ${sslServerCert} + SSLCertificateKeyFile ${sslServerKey} + ${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"} + ${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"} + ${acmeChallenge} + ${mkVHostCommonConf hostOpts} + + '' + ; + + mkVHostCommonConf = hostOpts: + let + documentRoot = if hostOpts.documentRoot != null + then hostOpts.documentRoot + else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out" + ; + + mkLocations = locations: concatStringsSep "\n" (map (config: '' + + ${optionalString (config.proxyPass != null) '' + + ProxyPass ${config.proxyPass} + ProxyPassReverse ${config.proxyPass} + + ''} + ${optionalString (config.index != null) '' + + DirectoryIndex ${config.index} + + ''} + ${optionalString (config.alias != null) '' + + Alias "${config.alias}" + + ''} + ${config.extraConfig} + + '') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations))); + in + '' + ${optionalString cfg.logPerVirtualHost '' + ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log + CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat} + ''} + + ${optionalString (hostOpts.robotsEntries != "") '' + Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries} + ''} + + DocumentRoot "${documentRoot}" + + + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + + ${optionalString hostOpts.enableUserDir '' + UserDir public_html + UserDir disabled root + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + Require all granted + + + Require all denied + + + ''} + + ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") '' + RedirectPermanent / ${hostOpts.globalRedirect} + ''} + + ${ + let makeDirConf = elem: '' + Alias ${elem.urlPath} ${elem.dir}/ + + Options +Indexes + Require all granted + AllowOverride All + + ''; + in concatMapStrings makeDirConf hostOpts.servedDirs + } + + ${mkLocations hostOpts.locations} + ${hostOpts.extraConfig} + '' + ; confFile = pkgs.writeText "httpd.conf" '' - ServerRoot ${httpd} + ServerRoot ${pkg} + ServerName ${config.networking.hostName} + DefaultRuntimeDir ${runtimeDir}/runtime - DefaultRuntimeDir ${mainCfg.stateDir}/runtime + PidFile ${runtimeDir}/httpd.pid - PidFile ${mainCfg.stateDir}/httpd.pid - - ${optionalString (mainCfg.multiProcessingModule != "prefork") '' + ${optionalString (cfg.multiProcessingModule != "prefork") '' # mod_cgid requires this. - ScriptSock ${mainCfg.stateDir}/cgisock + ScriptSock ${runtimeDir}/cgisock ''} - MaxClients ${toString mainCfg.maxClients} - MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild} + MaxClients ${toString cfg.maxClients} + MaxRequestsPerChild ${toString cfg.maxRequestsPerChild} ${let - listen = concatMap getListen allHosts; - toStr = listen: "Listen ${listenToString listen}\n"; - uniqueListen = uniqList {inputList = map toStr listen;}; - in concatStrings uniqueListen + toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}"; + uniqueListen = uniqList {inputList = map toStr listenInfo;}; + in concatStringsSep "\n" uniqueListen } - User ${mainCfg.user} - Group ${mainCfg.group} + User ${cfg.user} + Group ${cfg.group} ${let - load = {name, path}: "LoadModule ${name}_module ${path}\n"; - allModules = - concatMap (svc: svc.extraModulesPre) allSubservices - ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules - ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } - ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } - ++ optional enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } - ++ concatMap (svc: svc.extraModules) allSubservices - ++ extraForeignModules; - in concatMapStrings load (unique allModules) + mkModule = module: + if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; } + else if isAttrs module then { inherit (module) name path; } + else throw "Expecting either a string or attribute set including a name and path."; + in + concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules)) } AddHandler type-map var - ${allDenied} + Require all denied ${mimeConf} ${loggingConf} ${browserHacks} - Include ${httpd}/conf/extra/httpd-default.conf - Include ${httpd}/conf/extra/httpd-autoindex.conf - Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf - Include ${httpd}/conf/extra/httpd-languages.conf + Include ${pkg}/conf/extra/httpd-default.conf + Include ${pkg}/conf/extra/httpd-autoindex.conf + Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf + Include ${pkg}/conf/extra/httpd-languages.conf TraceEnable off - ${if enableSSL then sslConf else ""} + ${sslConf} # Fascist default - deny access to everything. Options FollowSymLinks AllowOverride None - ${allDenied} + Require all denied - # Generate directives for the main server. - ${perServerConf true mainCfg} + ${cfg.extraConfig} - ${let - makeVirtualHost = vhost: '' - - ${perServerConf false vhost} - - ''; - in concatMapStrings makeVirtualHost mainCfg.virtualHosts - } + ${concatMapStringsSep "\n" mkVHostConf vhosts} ''; - - enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices; - - enablePerl = mainCfg.enablePerl || any (svc: svc.enablePerl) allSubservices; - - # Generate the PHP configuration file. Should probably be factored # out into a separate module. phpIni = pkgs.runCommand "php.ini" - { options = concatStringsSep "\n" - ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices)); + { options = cfg.phpOptions; preferLocalBuild = true; } '' @@ -404,17 +342,33 @@ in { - ###### interface + imports = [ + (mkRemovedOptionModule [ "services" "httpd" httpdName "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.") + + # virtualHosts options + (mkRemovedOptionModule [ "services" "httpd" httpdName "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + ]; + + # interface options = { services.httpd."${httpdName}" = { - enable = mkOption { - type = types.bool; - default = false; - description = "Whether to enable the Apache HTTP Server."; - }; + enable = mkEnableOption "the Apache HTTP Server"; package = mkOption { type = types.package; @@ -440,8 +394,8 @@ in type = types.lines; default = ""; description = '' - Cnfiguration lines appended to the generated Apache - configuration file. Note that this mechanism may not work + Configuration lines appended to the generated Apache + configuration file. Note that this mechanism will not work when is overridden. ''; }; @@ -449,9 +403,14 @@ in extraModules = mkOption { type = types.listOf types.unspecified; default = []; - example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]''; + example = literalExample '' + [ + "proxy_connect" + { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; } + ] + ''; description = '' - Additional Apache modules to be used. These can be + Additional Apache modules to be used. These can be specified as a string in the case of modules distributed with Apache, or as an attribute set specifying the name and path of the @@ -459,9 +418,25 @@ in ''; }; + adminAddr = mkOption { + type = types.str; + example = "admin@example.org"; + description = "E-mail address of the server administrator."; + }; + + logFormat = mkOption { + type = types.str; + default = "common"; + example = "combined"; + description = '' + Log format for log files. Possible values are: combined, common, referer, agent. + See for more details. + ''; + }; + logPerVirtualHost = mkOption { type = types.bool; - default = false; + default = true; description = '' If enabled, each virtual host gets its own access.log and @@ -474,8 +449,7 @@ in type = types.str; default = "wwwrun"; description = '' - User account under which httpd runs. The account is created - automatically if it doesn't exist. + User account under which httpd runs. ''; }; @@ -483,8 +457,7 @@ in type = types.str; default = "wwwrun"; description = '' - Group under which httpd runs. The account is created - automatically if it doesn't exist. + Group under which httpd runs. ''; }; @@ -492,41 +465,33 @@ in type = types.path; default = "/var/log/httpd"; description = '' - Directory for Apache's log files. It is created automatically. - ''; - }; - - stateDir = mkOption { - type = types.path; - default = "/run/httpd"; - description = '' - Directory for Apache's transient runtime state (such as PID - files). It is created automatically. Note that the default, - /run/httpd, is deleted at boot time. + Directory for Apache's log files. It is created automatically. ''; }; virtualHosts = mkOption { - type = types.listOf (types.submodule ( - { options = import { - inherit lib; - forMainServer = false; + type = with types; attrsOf (submodule (import ./vhost-options.nix)); + default = { + localhost = { + documentRoot = "${pkg}/htdocs"; + }; + }; + example = literalExample '' + { + "foo.example.com" = { + forceSSL = true; + documentRoot = "/var/www/foo.example.com" + }; + "bar.example.com" = { + addSSL = true; + documentRoot = "/var/www/bar.example.com"; }; - })); - default = []; - example = [ - { hostName = "foo"; - documentRoot = "/data/webroot-foo"; - } - { hostName = "bar"; - documentRoot = "/data/webroot-bar"; } - ]; + ''; description = '' - Specification of the virtual hosts served by Apache. Each + Specification of the virtual hosts served by Apache. Each element should be an attribute set specifying the - configuration of the virtual host. The available options - are the non-global options permissible for the main host. + configuration of the virtual host. ''; }; @@ -564,17 +529,18 @@ in '' date.timezone = "CET" ''; - description = - "Options appended to the PHP configuration file php.ini."; + description = '' + Options appended to the PHP configuration file php.ini. + ''; }; multiProcessingModule = mkOption { - type = types.str; + type = types.enum [ "event" "prefork" "worker" ]; default = "prefork"; example = "worker"; description = '' - Multi-processing module to be used by Apache. Available + Multi-processing module to be used by Apache. Available modules are prefork (the default; handles each request in a separate child process), worker (hybrid approach that starts a @@ -596,8 +562,9 @@ in type = types.int; default = 0; example = 500; - description = - "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited"; + description = '' + Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited. + ''; }; sslCiphers = mkOption { @@ -608,48 +575,76 @@ in sslProtocols = mkOption { type = types.str; - default = "All -SSLv2 -SSLv3 -TLSv1"; + default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1"; example = "All -SSLv2 -SSLv3"; description = "Allowed SSL/TLS protocol versions."; }; - } - - # Include the options shared between the main server and virtual hosts. - // (import { - inherit lib; - forMainServer = true; - }); + }; }; + # implementation - ###### implementation - - config = mkIf config.services.httpd."${httpdName}".enable { + config = mkIf cfg.enable { - assertions = [ { assertion = mainCfg.enableSSL == true - -> mainCfg.sslServerCert != null - && mainCfg.sslServerKey != null; - message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; } - ]; + assertions = [ + { + assertion = all (hostOpts: !hostOpts.enableSSL) vhosts; + message = '' + The option `services.httpd.virtualHosts..enableSSL` no longer has any effect; please remove it. + Select one of `services.httpd.virtualHosts..addSSL`, `services.httpd.virtualHosts..forceSSL`, + or `services.httpd.virtualHosts..onlySSL`. + ''; + } + { + assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts; + message = '' + Options `services.httpd.virtualHosts..addSSL`, + `services.httpd.virtualHosts..onlySSL` and `services.httpd.virtualHosts..forceSSL` + are mutually exclusive. + ''; + } + { + assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts; + message = '' + Options `services.httpd.virtualHosts..enableACME` and + `services.httpd.virtualHosts..useACMEHost` are mutually exclusive. + ''; + } + ]; - warnings = map (cfg: "apache-httpd's extraSubservices option is deprecated. Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") (lib.filter (cfg: cfg.extraSubservices != []) allHosts); + warnings = + mapAttrsToList (name: hostOpts: '' + Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS. + '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts); - users.users = optionalAttrs (withUsers && mainCfg.user == "wwwrun") (singleton - { name = "wwwrun"; - group = mainCfg.group; + users.users = optionalAttrs (withUsers && cfg.user == "wwwrun") { + wwwrun = { + group = cfg.group; description = "Apache httpd user"; uid = config.ids.uids.wwwrun; - }); + }; + }; + + users.groups = optionalAttrs (withUsers && cfg.group == "wwwrun") { + wwwrun.gid = config.ids.gids.wwwrun; + }; + + security.acme.certs = mapAttrs (name: hostOpts: { + user = cfg.user; + group = mkDefault cfg.group; + email = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr; + webroot = hostOpts.acmeRoot; + extraDomains = genAttrs hostOpts.serverAliases (alias: null); + postRun = "systemctl reload httpd.service"; + }) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts); - users.groups = optionalAttrs (withUsers && mainCfg.group == "wwwrun") (singleton - { name = "wwwrun"; - gid = config.ids.gids.wwwrun; - }); + environment.systemPackages = [ pkg ]; - environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices; + # required for "apachectl configtest" + environment.etc."httpd/httpd_${httpdName}.conf".source = httpdConf; - services.httpd."${httpdName}".phpOptions = + services.httpd."${httpdName}" = { phpOptions = '' ; Needed for PHP's mail() function. sendmail_path = sendmail -t -i @@ -662,54 +657,79 @@ in date.timezone = "${config.time.timeZone}" ''; + extraModules = mkBefore [ + # HTTP authentication mechanisms: basic and digest. + "auth_basic" "auth_digest" + + # Authentication: is the user who he claims to be? + "authn_file" "authn_dbm" "authn_anon" + + # Authorization: is the user allowed access? + "authz_user" "authz_groupfile" "authz_host" + + # Other modules. + "ext_filter" "include" "env" "mime_magic" + "cern_meta" "expires" "headers" "usertrack" "setenvif" + "dav" "status" "asis" "info" "dav_fs" + "vhost_alias" "imagemap" "actions" "speling" + "proxy" "proxy_http" + "cache" "cache_disk" + + # For compatibility with old configurations, the new module mod_access_compat is provided. + "access_compat" + ]; + }; + + systemd.tmpfiles.rules = + let + svc = config.systemd.services."httpd${httpdName}".serviceConfig; + in + [ + "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}" + "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}" + ]; + systemd.services."httpd${httpdName}" = + let + vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts; + in { description = "Apache HTTPD"; wantedBy = [ "multi-user.target" ]; - wants = [ "keys.target" ]; - after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ]; + wants = concatLists (map (hostOpts: [ "acme-${hostOpts.hostName}.service" "acme-selfsigned-${hostOpts.hostName}.service" ]) vhostsACME); + after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME; path = - [ httpd pkgs.coreutils pkgs.gnugrep ] - ++ optional enablePHP pkgs.system-sendmail # Needed for PHP's mail() function. - ++ concatMap (svc: svc.extraServerPath) allSubservices; + [ pkg pkgs.coreutils pkgs.gnugrep ] + ++ optional cfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function. environment = - optionalAttrs enablePHP { PHPRC = phpIni; } - // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; } - // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices)); + optionalAttrs cfg.enablePHP { PHPRC = phpIni; } + // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }; preStart = '' - mkdir -m 0750 -p ${mainCfg.stateDir} - [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir} - - mkdir -m 0750 -p "${mainCfg.stateDir}/runtime" - [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime" - - mkdir -m 0700 -p ${mainCfg.logDir} - # Get rid of old semaphores. These tend to accumulate across # server restarts, eventually preventing it from restarting # successfully. - for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do + for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${cfg.user} ' | cut -f2 -d ' '); do ${pkgs.utillinux}/bin/ipcrm -s $i done - - # Run the startup hooks for the subservices. - for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do - echo Running Apache startup hook $i... - $i - done ''; - serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}"; - serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop"; - serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful"; - serviceConfig.Type = "forking"; - serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid"; - serviceConfig.Restart = "always"; - serviceConfig.RestartSec = "5s"; + serviceConfig = { + ExecStart = "@${pkg}/bin/httpd httpd -f ${httpdConf}"; + ExecStop = "${pkg}/bin/httpd -f ${httpdConf} -k graceful-stop"; + ExecReload = "${pkg}/bin/httpd -f ${httpdConf} -k graceful"; + User = "root"; + Group = cfg.group; + Type = "forking"; + PIDFile = "${runtimeDir}/httpd.pid"; + Restart = "always"; + RestartSec = "5s"; + RuntimeDirectory = "httpd_${httpdName} httpd_${httpdName}/runtime"; + RuntimeDirectoryMode = "0750"; + }; }; }; diff --git a/modules/websites/httpd-service-builder.patch b/modules/websites/httpd-service-builder.patch new file mode 100644 index 00000000..f0ad8366 --- /dev/null +++ b/modules/websites/httpd-service-builder.patch @@ -0,0 +1,150 @@ +--- /nix/store/xj651aslybfsma20hpbi5nznfcffq8ky-nixexprs.tar.xz/nixos/modules/services/web-servers/apache-httpd/default.nix 1970-01-01 01:00:01.000000000 +0100 ++++ modules/websites/httpd-service-builder.nix 2020-04-04 03:08:29.068490345 +0200 +@@ -1,12 +1,15 @@ ++# to help backporting this builder should stay as close as possible to ++# nixos/modules/services/web-servers/apache-httpd/default.nix ++{ httpdName, withUsers ? true }: + { config, lib, pkgs, ... }: + + with lib; + + let + +- cfg = config.services.httpd; ++ cfg = config.services.httpd."${httpdName}"; + +- runtimeDir = "/run/httpd"; ++ runtimeDir = "/run/httpd_${httpdName}"; + + pkg = cfg.package.out; + +@@ -318,13 +321,6 @@ + Require all denied + + +- # But do allow access to files in the store so that we don't have +- # to generate clauses for every generated file that we +- # want to serve. +- +- Require all granted +- +- + ${cfg.extraConfig} + + ${concatMapStringsSep "\n" mkVHostConf vhosts} +@@ -347,30 +343,30 @@ + { + + imports = [ +- (mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") +- (mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.") + + # virtualHosts options +- (mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.") +- (mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.") ++ (mkRemovedOptionModule [ "services" "httpd" httpdName "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.") + ]; + + # interface + + options = { + +- services.httpd = { ++ services.httpd."${httpdName}" = { + + enable = mkEnableOption "the Apache HTTP Server"; + +@@ -622,7 +618,7 @@ + Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS. + '') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts); + +- users.users = optionalAttrs (cfg.user == "wwwrun") { ++ users.users = optionalAttrs (withUsers && cfg.user == "wwwrun") { + wwwrun = { + group = cfg.group; + description = "Apache httpd user"; +@@ -630,7 +626,7 @@ + }; + }; + +- users.groups = optionalAttrs (cfg.group == "wwwrun") { ++ users.groups = optionalAttrs (withUsers && cfg.group == "wwwrun") { + wwwrun.gid = config.ids.gids.wwwrun; + }; + +@@ -646,9 +642,9 @@ + environment.systemPackages = [ pkg ]; + + # required for "apachectl configtest" +- environment.etc."httpd/httpd.conf".source = httpdConf; ++ environment.etc."httpd/httpd_${httpdName}.conf".source = httpdConf; + +- services.httpd.phpOptions = ++ services.httpd."${httpdName}" = { phpOptions = + '' + ; Needed for PHP's mail() function. + sendmail_path = sendmail -t -i +@@ -661,7 +657,7 @@ + date.timezone = "${config.time.timeZone}" + ''; + +- services.httpd.extraModules = mkBefore [ ++ extraModules = mkBefore [ + # HTTP authentication mechanisms: basic and digest. + "auth_basic" "auth_digest" + +@@ -682,17 +678,18 @@ + # For compatibility with old configurations, the new module mod_access_compat is provided. + "access_compat" + ]; ++ }; + + systemd.tmpfiles.rules = + let +- svc = config.systemd.services.httpd.serviceConfig; ++ svc = config.systemd.services."httpd${httpdName}".serviceConfig; + in + [ + "d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}" + "Z '${cfg.logDir}' - ${svc.User} ${svc.Group}" + ]; + +- systemd.services.httpd = ++ systemd.services."httpd${httpdName}" = + let + vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts; + in +@@ -730,7 +727,7 @@ + PIDFile = "${runtimeDir}/httpd.pid"; + Restart = "always"; + RestartSec = "5s"; +- RuntimeDirectory = "httpd httpd/runtime"; ++ RuntimeDirectory = "httpd_${httpdName} httpd_${httpdName}/runtime"; + RuntimeDirectoryMode = "0750"; + }; + }; diff --git a/modules/websites/location-options.nix b/modules/websites/location-options.nix new file mode 100644 index 00000000..8ea88f94 --- /dev/null +++ b/modules/websites/location-options.nix @@ -0,0 +1,54 @@ +{ config, lib, name, ... }: +let + inherit (lib) mkOption types; +in +{ + options = { + + proxyPass = mkOption { + type = with types; nullOr str; + default = null; + example = "http://www.example.org/"; + description = '' + Sets up a simple reverse proxy as described by . + ''; + }; + + index = mkOption { + type = with types; nullOr str; + default = null; + example = "index.php index.html"; + description = '' + Adds DirectoryIndex directive. See . + ''; + }; + + alias = mkOption { + type = with types; nullOr path; + default = null; + example = "/your/alias/directory"; + description = '' + Alias directory for requests. See . + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + These lines go to the end of the location verbatim. + ''; + }; + + priority = mkOption { + type = types.int; + default = 1000; + description = '' + Order of this location block in relation to the others in the vhost. + The semantics are the same as with `lib.mkOrder`. Smaller values have + a greater priority. + ''; + }; + + }; +} diff --git a/modules/websites/vhost-options.nix b/modules/websites/vhost-options.nix new file mode 100644 index 00000000..263980ad --- /dev/null +++ b/modules/websites/vhost-options.nix @@ -0,0 +1,275 @@ +{ config, lib, name, ... }: +let + inherit (lib) literalExample mkOption nameValuePair types; +in +{ + options = { + + hostName = mkOption { + type = types.str; + default = name; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = types.listOf types.str; + default = []; + example = ["www.example.org" "www.example.org:8080" "example.org"]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + listen = mkOption { + type = with types; listOf (submodule ({ + options = { + port = mkOption { + type = types.port; + description = "Port to listen on"; + }; + ip = mkOption { + type = types.str; + default = "*"; + description = "IP to listen on. 0.0.0.0 for IPv4 only, * for all."; + }; + ssl = mkOption { + type = types.bool; + default = false; + description = "Whether to enable SSL (https) support."; + }; + }; + })); + default = []; + example = [ + { ip = "195.154.1.1"; port = 443; ssl = true;} + { ip = "192.154.1.1"; port = 80; } + { ip = "*"; port = 8080; } + ]; + description = '' + Listen addresses and ports for this virtual host. + + This option overrides addSSL, forceSSL and onlySSL. + + ''; + }; + + enableSSL = mkOption { + type = types.bool; + visible = false; + default = false; + }; + + addSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS in addition to plain HTTP. This will set defaults for + listen to listen on all interfaces on the respective default + ports (80, 443). + ''; + }; + + onlySSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTPS and reject plain HTTP connections. This will set + defaults for listen to listen on all interfaces on port 443. + ''; + }; + + forceSSL = mkOption { + type = types.bool; + default = false; + description = '' + Whether to add a separate nginx server block that permanently redirects (301) + all plain HTTP traffic to HTTPS. This will set defaults for + listen to listen on all interfaces on the respective default + ports (80, 443), where the non-SSL listens are used for the redirect vhosts. + ''; + }; + + enableACME = mkOption { + type = types.bool; + default = false; + description = '' + Whether to ask Let's Encrypt to sign a certificate for this vhost. + Alternately, you can use an existing certificate through . + ''; + }; + + useACMEHost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A host of an existing Let's Encrypt certificate to use. + This is useful if you have many subdomains and want to avoid hitting the + rate limit. + Alternately, you can generate a certificate through . + Note that this option does not create any certificates, nor it does add subdomains to existing ones – you will need to create them manually using . + ''; + }; + + acmeRoot = mkOption { + type = types.str; + default = "/var/lib/acme/acme-challenges"; + description = "Directory for the acme challenge which is PUBLIC, don't put certs or keys in here"; + }; + + sslServerCert = mkOption { + type = types.path; + example = "/var/host.cert"; + description = "Path to server SSL certificate."; + }; + + sslServerKey = mkOption { + type = types.path; + example = "/var/host.key"; + description = "Path to server SSL certificate key."; + }; + + sslServerChain = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/ca.pem"; + description = "Path to server SSL chain file."; + }; + + http2 = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable HTTP 2. HTTP/2 is supported in all multi-processing modules that come with httpd. However, if you use the prefork mpm, there will + be severe restrictions. Refer to for details. + ''; + }; + + adminAddr = mkOption { + type = types.nullOr types.str; + default = null; + example = "admin@example.org"; + description = "E-mail address of the server administrator."; + }; + + documentRoot = mkOption { + type = types.nullOr types.path; + default = null; + example = "/data/webserver/docs"; + description = '' + The path of Apache's document root directory. If left undefined, + an empty directory in the Nix store will be used as root. + ''; + }; + + servedDirs = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/nix"; + dir = "/home/eelco/Dev/nix-homepage"; + } + ]; + description = '' + This option provides a simple way to serve static directories. + ''; + }; + + servedFiles = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/foo/bar.png"; + file = "/home/eelco/some-file.png"; + } + ]; + description = '' + This option provides a simple way to serve individual, static files. + + + This option has been deprecated and will be removed in a future + version of NixOS. You can achieve the same result by making use of + the locations.<name>.alias option. + + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + + Options FollowSymlinks + AllowOverride All + + ''; + description = '' + These lines go to httpd.conf verbatim. They will go after + directories and directory aliases defined by default. + ''; + }; + + enableUserDir = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable serving ~/public_html as + /~username. + ''; + }; + + globalRedirect = mkOption { + type = types.nullOr types.str; + default = null; + example = http://newserver.example.org/; + description = '' + If set, all requests for this host are redirected permanently to + the given URL. + ''; + }; + + logFormat = mkOption { + type = types.str; + default = "common"; + example = "combined"; + description = '' + Log format for Apache's log files. Possible values are: combined, common, referer, agent. + ''; + }; + + robotsEntries = mkOption { + type = types.lines; + default = ""; + example = "Disallow: /foo/"; + description = '' + Specification of pages to be ignored by web crawlers. See for details. + ''; + }; + + locations = mkOption { + type = with types; attrsOf (submodule (import ./location-options.nix)); + default = {}; + example = literalExample '' + { + "/" = { + proxyPass = "http://localhost:3000"; + }; + "/foo/bar.png" = { + alias = "/home/eelco/some-file.png"; + }; + }; + ''; + description = '' + Declarative location config. See for details. + ''; + }; + + }; + + config = { + + locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles); + + }; +}