summaryrefslogblamecommitdiff
path: root/modules/websites/httpd-service-builder.nix
blob: c5f72f96c56a6a08c787c46b32d4784bc5a726b2 (plain) (tree)
1
2
3
4
5
6
7
8
9








                                                                      
                                             
 
                                         
 
                        
 
                             
 
                                                                                             
 
                                                            
 
                                                                               
 
                                       
 





                                                                                                                  
 
                                                      
 


                                                          
 








                                                                    
     

                                                                                 
                               




                                                                                                                                           
 

                                                   







                                                                                     
                                                       





                      










                                                                                            



              

                                                              
 
                      
 

                                     
 



                                        



               
                                      





                                                  
                                       


               














































































































































                                                                                                              



                                           


                                            
 
                                   
 
                                                                
                               
                                      


                        

                                                                


               


                                                                                                                

     

                      

         





                                                                                                                            




                           
                          





                   



                                                           


                   
              




                                                  
                          

                
                      
 
                                                  

     


                                                                     
                               











                                   




















                                                                                                                                                                                                      




                                     
                                                       
























                                                                                               

                                                                    






                                                         





                                                                                    
                        
                                                            






                                                                    















                                                                                                

                                    
                       











                                                                
                                              






                           
                                       






                                   
                                                                        



                               
                                                                                                                                    













                                                        
              
           
           
                        
                                                                   
                                                           
                                            




































                                                                      


                                                                                      


                                        
                                                         



                            
                                                                   




















                                                                      


                                                                                                 









                                                                                       
                                                      


                                                           
      


    
                  
 
                            
 
























                                                                                                                                  
 



                                                                                                                                                                                                                                                                                                                       
 


                                                                     

                                          














                                                                                       
 
                                         
 

                                                                       
 
                                                  





                                           
                                                           




                                                 































                                                                                                
                                          


                                                                   


                                           

                                                                                                                                                

              

                                                                                             

                     

                                                                                         


                  


                                                                         
                                                                                                 

                                                 

             












                                                                             



        
# 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."${httpdName}";

  runtimeDir = "/run/httpd_${httpdName}";

  pkg = cfg.package.out;

  httpdConf = cfg.configFile;

  php = cfg.phpPackage.override { apacheHttpd = pkg.dev; /* otherwise it only gets .out */ };

  phpMajorVersion = lib.versions.major (lib.getVersion php);

  mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };

  vhosts = attrValues cfg.virtualHosts;

  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; }
    );

  listenInfo = unique (concatMap mkListenInfo vhosts);

  enableHttp2 = any (vhost: vhost.http2) vhosts;
  enableSSL = any (listen: listen.ssl) listenInfo;
  enableUserDir = any (vhost: vhost.enableUserDir) vhosts;

  # 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 cfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
    ++ optional enableHttp2 "http2"
    ++ optional enableSSL "ssl"
    ++ 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 cfg.logFormat != "none" then ''
    ErrorLog ${cfg.logDir}/error.log

    LogLevel notice

    LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
    LogFormat "%h %l %u %t \"%r\" %>s %b" common
    LogFormat "%{Referer}i -> %U" referer
    LogFormat "%{User-agent}i" agent

    CustomLog ${cfg.logDir}/access.log ${cfg.logFormat}
  '' else ''
    ErrorLog /dev/null
  '');


  browserHacks = ''
    <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 = ''
    <IfModule mod_ssl.c>
        SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000)

        Mutex posixsem

        SSLRandomSeed startup builtin
        SSLRandomSeed connect builtin

        SSLProtocol ${cfg.sslProtocols}
        SSLCipherSuite ${cfg.sslCiphers}
        SSLHonorCipherOrder on
    </IfModule>
  '';


  mimeConf = ''
    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 ${pkg}/conf/magic
    </IfModule>
  '';

  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 ${pkg}
    ServerName ${config.networking.hostName}
    DefaultRuntimeDir ${runtimeDir}/runtime

    PidFile ${runtimeDir}/httpd.pid

    ${optionalString (cfg.multiProcessingModule != "prefork") ''
      # mod_cgid requires this.
      ScriptSock ${runtimeDir}/cgisock
    ''}

    <IfModule prefork.c>
        MaxClients           ${toString cfg.maxClients}
        MaxRequestsPerChild  ${toString cfg.maxRequestsPerChild}
    </IfModule>

    ${let
        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 ${cfg.user}
    Group ${cfg.group}

    ${let
        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">
        Require all denied
    </Files>

    ${mimeConf}
    ${loggingConf}
    ${browserHacks}

    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

    ${sslConf}

    # Fascist default - deny access to everything.
    <Directory />
        Options FollowSymLinks
        AllowOverride None
        Require all denied
    </Directory>

    ${cfg.extraConfig}

    ${concatMapStringsSep "\n" mkVHostConf vhosts}
  '';

  # Generate the PHP configuration file.  Should probably be factored
  # out into a separate module.
  phpIni = pkgs.runCommand "php.ini"
    { options = cfg.phpOptions;
      preferLocalBuild = true;
    }
    ''
      cat ${php}/etc/php.ini > $out
      echo "$options" >> $out
    '';

in


{

  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 = mkEnableOption "the Apache HTTP Server";

      package = mkOption {
        type = types.package;
        default = pkgs.apacheHttpd;
        defaultText = "pkgs.apacheHttpd";
        description = ''
          Overridable attribute of the Apache HTTP Server package to use.
        '';
      };

      configFile = mkOption {
        type = types.path;
        default = confFile;
        defaultText = "confFile";
        example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
        description = ''
          Override the configuration file used by Apache. By default,
          NixOS generates one automatically.
        '';
      };

      extraConfig = mkOption {
        type = types.lines;
        default = "";
        description = ''
          Configuration lines appended to the generated Apache
          configuration file. Note that this mechanism will not work
          when <option>configFile</option> is overridden.
        '';
      };

      extraModules = mkOption {
        type = types.listOf types.unspecified;
        default = [];
        example = literalExample ''
          [
            "proxy_connect"
            { name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.so"; }
          ]
        '';
        description = ''
          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
          module.
        '';
      };

      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 = true;
        description = ''
          If enabled, each virtual host gets its own
          <filename>access.log</filename> and
          <filename>error.log</filename>, namely suffixed by the
          <option>hostName</option> of the virtual host.
        '';
      };

      user = mkOption {
        type = types.str;
        default = "wwwrun";
        description = ''
          User account under which httpd runs.
        '';
      };

      group = mkOption {
        type = types.str;
        default = "wwwrun";
        description = ''
          Group under which httpd runs.
        '';
      };

      logDir = mkOption {
        type = types.path;
        default = "/var/log/httpd";
        description = ''
          Directory for Apache's log files. It is created automatically.
        '';
      };

      virtualHosts = mkOption {
        type = with types; attrsOf (submodule (import <nixpkgs/nixos/modules/services/web-servers/apache-httpd/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";
            };
          }
        '';
        description = ''
          Specification of the virtual hosts served by Apache. Each
          element should be an attribute set specifying the
          configuration of the virtual host.
        '';
      };

      enableMellon = mkOption {
        type = types.bool;
        default = false;
        description = "Whether to enable the mod_auth_mellon module.";
      };

      enablePHP = mkOption {
        type = types.bool;
        default = false;
        description = "Whether to enable the PHP module.";
      };

      phpPackage = mkOption {
        type = types.package;
        default = pkgs.php;
        defaultText = "pkgs.php";
        description = ''
          Overridable attribute of the PHP package to use.
        '';
      };

      enablePerl = mkOption {
        type = types.bool;
        default = false;
        description = "Whether to enable the Perl module (mod_perl).";
      };

      phpOptions = mkOption {
        type = types.lines;
        default = "";
        example =
          ''
            date.timezone = "CET"
          '';
        description = ''
          Options appended to the PHP configuration file <filename>php.ini</filename>.
        '';
      };

      multiProcessingModule = mkOption {
        type = types.enum [ "event" "prefork" "worker" ];
        default = "prefork";
        example = "worker";
        description =
          ''
            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
            number of child processes each running a number of
            threads) and <literal>event</literal> (a recent variant of
            <literal>worker</literal> that handles persistent
            connections more efficiently).
          '';
      };

      maxClients = mkOption {
        type = types.int;
        default = 150;
        example = 8;
        description = "Maximum number of httpd processes (prefork)";
      };

      maxRequestsPerChild = mkOption {
        type = types.int;
        default = 0;
        example = 500;
        description = ''
          Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited.
        '';
      };

      sslCiphers = mkOption {
        type = types.str;
        default = "HIGH:!aNULL:!MD5:!EXP";
        description = "Cipher Suite available for negotiation in SSL proxy handshake.";
      };

      sslProtocols = mkOption {
        type = types.str;
        default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1";
        example = "All -SSLv2 -SSLv3";
        description = "Allowed SSL/TLS protocol versions.";
      };
    };

  };

  # implementation

  config = mkIf cfg.enable {

    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 =
      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 && 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);

    environment.systemPackages = [ pkg ];

    # required for "apachectl configtest"
    environment.etc."httpd/httpd_${httpdName}.conf".source = httpdConf;

    services.httpd."${httpdName}" = { phpOptions =
      ''
        ; Needed for PHP's mail() function.
        sendmail_path = sendmail -t -i

        ; Don't advertise PHP
        expose_php = off
      '' + optionalString (config.time.timeZone != null) ''

        ; Apparently PHP doesn't use $TZ.
        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 = 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 =
          [ pkg pkgs.coreutils pkgs.gnugrep ]
          ++ optional cfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.

        environment =
          optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
          // optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH  = "${pkgs.xmlsec}/lib"; };

        preStart =
          ''
            # 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 ' ${cfg.user} ' | cut -f2 -d ' '); do
                ${pkgs.utillinux}/bin/ipcrm -s $i
            done
          '';

        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";
        };
      };

  };
}