]> git.immae.eu Git - perso/Immae/Config/Nix/NUR.git/commitdiff
Prepare upgrade to nixos 20.03
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Sat, 4 Apr 2020 01:18:40 +0000 (03:18 +0200)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Fri, 24 Apr 2020 22:04:53 +0000 (00:04 +0200)
modules/websites/default.nix
modules/websites/httpd-service-builder.nix
modules/websites/httpd-service-builder.patch [new file with mode: 0644]
modules/websites/location-options.nix [new file with mode: 0644]
modules/websites/vhost-options.nix [new file with mode: 0644]

index 3f46e65dfb4890dabf2cc071b7c123ae764c03cd..d5a0f635b0354db0ae775c7a5b877dded3db133d 100644 (file)
@@ -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;
 
index f0208ab5310bbc59feb5fd7ffd0dfacffbed07f2..ec79a90c0f0dbd583d06af8bdc8b75744c91cf9d 100644 (file)
@@ -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
+    <IfModule mod_setenvif.c>
+        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
+    </IfModule>
   '';
 
 
   sslConf = ''
-    SSLSessionCache shmcb:${mainCfg.stateDir}/ssl_scache(512000)
+    <IfModule mod_ssl.c>
+        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
+    </IfModule>
   '';
 
 
   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
 
     <IfModule mod_mime_magic.c>
-        MIMEMagicFile ${httpd}/conf/magic
+        MIMEMagicFile ${pkg}/conf/magic
     </IfModule>
   '';
 
-
-  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}"
-
-      <Directory "${documentRoot}">
-          Options Indexes FollowSymLinks
-          AllowOverride None
-          ${allGranted}
-      </Directory>
-    '';
-
-    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
-
-      <Directory "/home/*/public_html">
-          AllowOverride FileInfo AuthConfig Limit Indexes
-          Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
-          <Limit GET POST OPTIONS>
-              ${allGranted}
-          </Limit>
-          <LimitExcept GET POST OPTIONS>
-              ${allDenied}
-          </LimitExcept>
-      </Directory>
-
-    '' 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}/
-            <Directory ${elem.dir}>
-                Options +Indexes
-                ${allGranted}
-                AllowOverride All
-            </Directory>
-          '';
-      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/"
+        <Directory "${hostOpts.acmeRoot}">
+            AllowOverride None
+            Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+            Require method GET POST OPTIONS
+            Require all granted
+        </Directory>
+      '';
+    in
+      optionalString (listen != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") listen}>
+            ServerName ${hostOpts.hostName}
+            ${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
+            ServerAdmin ${adminAddr}
+            <IfModule mod_ssl.c>
+                SSLEngine off
+            </IfModule>
+            ${acmeChallenge}
+            ${if hostOpts.forceSSL then ''
+              <IfModule mod_rewrite.c>
+                  RewriteEngine on
+                  RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC]
+                  RewriteCond %{HTTPS} off
+                  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
+              </IfModule>
+            '' else mkVHostCommonConf hostOpts}
+        </VirtualHost>
+      '' +
+      optionalString (listenSSL != []) ''
+        <VirtualHost ${concatMapStringsSep " " (listen: "${listen.ip}:${toString listen.port}") 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}
+        </VirtualHost>
+      ''
+  ;
+
+  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: ''
+        <Location ${config.location}>
+          ${optionalString (config.proxyPass != null) ''
+            <IfModule mod_proxy.c>
+                ProxyPass ${config.proxyPass}
+                ProxyPassReverse ${config.proxyPass}
+            </IfModule>
+          ''}
+          ${optionalString (config.index != null) ''
+            <IfModule mod_dir.c>
+                DirectoryIndex ${config.index}
+            </IfModule>
+          ''}
+          ${optionalString (config.alias != null) ''
+            <IfModule mod_alias.c>
+                Alias "${config.alias}"
+            </IfModule>
+          ''}
+          ${config.extraConfig}
+        </Location>
+      '') (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}"
+
+        <Directory "${documentRoot}">
+            Options Indexes FollowSymLinks
+            AllowOverride None
+            Require all granted
+        </Directory>
+
+        ${optionalString hostOpts.enableUserDir ''
+          UserDir public_html
+          UserDir disabled root
+          <Directory "/home/*/public_html">
+              AllowOverride FileInfo AuthConfig Limit Indexes
+              Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
+              <Limit GET POST OPTIONS>
+                  Require all granted
+              </Limit>
+              <LimitExcept GET POST OPTIONS>
+                  Require all denied
+              </LimitExcept>
+          </Directory>
+        ''}
+
+        ${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
+          RedirectPermanent / ${hostOpts.globalRedirect}
+        ''}
+
+        ${
+          let makeDirConf = elem: ''
+                Alias ${elem.urlPath} ${elem.dir}/
+                <Directory ${elem.dir}>
+                    Options +Indexes
+                    Require all granted
+                    AllowOverride All
+                </Directory>
+              '';
+          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
     ''}
 
     <IfModule prefork.c>
-        MaxClients           ${toString mainCfg.maxClients}
-        MaxRequestsPerChild  ${toString mainCfg.maxRequestsPerChild}
+        MaxClients           ${toString cfg.maxClients}
+        MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
     </IfModule>
 
     ${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
 
     <Files ~ "^\.ht">
-        ${allDenied}
+        Require all denied
     </Files>
 
     ${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.
     <Directory />
         Options FollowSymLinks
         AllowOverride None
-        ${allDenied}
+        Require all denied
     </Directory>
 
-    # Generate directives for the main server.
-    ${perServerConf true mainCfg}
+    ${cfg.extraConfig}
 
-    ${let
-        makeVirtualHost = vhost: ''
-          <VirtualHost ${concatStringsSep " " (map listenToString (getListen vhost))}>
-              ${perServerConf false vhost}
-          </VirtualHost>
-        '';
-      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 <option>configFile</option> 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
           <varname>name</varname> and <varname>path</varname> 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 <link xlink:href="https://httpd.apache.org/docs/2.4/logs.html"/> for more details.
+        '';
+      };
+
       logPerVirtualHost = mkOption {
         type = types.bool;
-        default = false;
+        default = true;
         description = ''
           If enabled, each virtual host gets its own
           <filename>access.log</filename> 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,
-          <filename>/run/httpd</filename>, is deleted at boot time.
+          Directory for Apache's log files. It is created automatically.
         '';
       };
 
       virtualHosts = mkOption {
-        type = types.listOf (types.submodule (
-          { options = import <nixpkgs/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix> {
-              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 <filename>php.ini</filename>.";
+        description = ''
+          Options appended to the PHP configuration file <filename>php.ini</filename>.
+        '';
       };
 
       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 <literal>prefork</literal> (the default;
             handles each request in a separate child process),
             <literal>worker</literal> (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 <nixpkgs/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix> {
-      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.<name>.enableSSL` no longer has any effect; please remove it.
+          Select one of `services.httpd.virtualHosts.<name>.addSSL`, `services.httpd.virtualHosts.<name>.forceSSL`,
+          or `services.httpd.virtualHosts.<name>.onlySSL`.
+        '';
+      }
+      {
+        assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.addSSL`,
+          `services.httpd.virtualHosts.<name>.onlySSL` and `services.httpd.virtualHosts.<name>.forceSSL`
+          are mutually exclusive.
+        '';
+      }
+      {
+        assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts;
+        message = ''
+          Options `services.httpd.virtualHosts.<name>.enableACME` and
+          `services.httpd.virtualHosts.<name>.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 (file)
index 0000000..f0ad836
--- /dev/null
@@ -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
+     </Directory>
+-    # But do allow access to files in the store so that we don't have
+-    # to generate <Directory> clauses for every generated file that we
+-    # want to serve.
+-    <Directory /nix/store>
+-        Require all granted
+-    </Directory>
+-
+     ${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 (file)
index 0000000..8ea88f9
--- /dev/null
@@ -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 <link xlink:href="https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html#simple" />.
+      '';
+    };
+
+    index = mkOption {
+      type = with types; nullOr str;
+      default = null;
+      example = "index.php index.html";
+      description = ''
+        Adds DirectoryIndex directive. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_dir.html#directoryindex" />.
+      '';
+    };
+
+    alias = mkOption {
+      type = with types; nullOr path;
+      default = null;
+      example = "/your/alias/directory";
+      description = ''
+        Alias directory for requests. See <link xlink:href="https://httpd.apache.org/docs/2.4/mod/mod_alias.html#alias" />.
+      '';
+    };
+
+    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 (file)
index 0000000..263980a
--- /dev/null
@@ -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.
+        <note><para>
+          This option overrides <literal>addSSL</literal>, <literal>forceSSL</literal> and <literal>onlySSL</literal>.
+        </para></note>
+      '';
+    };
+
+    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
+        <literal>listen</literal> 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 <literal>listen</literal> 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
+        <literal>listen</literal> 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 <option>useACMEHost</option>.
+      '';
+    };
+
+    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
+        <link xlink:href="https://letsencrypt.org/docs/rate-limits/">rate limit</link>.
+        Alternately, you can generate a certificate through <option>enableACME</option>.
+        <emphasis>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  <xref linkend="opt-security.acme.certs"/>.</emphasis>
+      '';
+    };
+
+    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. <emphasis>However, if you use the prefork mpm, there will
+        be severe restrictions.</emphasis> Refer to <link xlink:href="https://httpd.apache.org/docs/2.4/howto/http2.html#mpm-config"/> 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.
+
+        <note><para>
+          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 <literal>locations.&lt;name&gt;.alias</literal> option.
+        </para></note>
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = ''
+        <Directory /home>
+          Options FollowSymlinks
+          AllowOverride All
+        </Directory>
+      '';
+      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 <filename>~/public_html</filename> as
+        <literal>/~<replaceable>username</replaceable></literal>.
+      '';
+    };
+
+    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 <link
+        xlink:href='http://www.robotstxt.org/'/> 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 <link
+        xlink:href="https://httpd.apache.org/docs/2.4/mod/core.html#location"/> for details.
+      '';
+    };
+
+  };
+
+  config = {
+
+    locations = builtins.listToAttrs (map (elem: nameValuePair elem.urlPath { alias = elem.file; }) config.servedFiles);
+
+  };
+}