{ config, lib, pkgs, ... }: with lib; let mainCfg = config.services.httpd; runtimeDir = "/run/httpd"; httpd = mainCfg.package.out; httpdConf = mainCfg.configFile; php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ }; phpMajorVersion = lib.versions.major (lib.getVersion php); mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; }; defaultListen = cfg: if cfg.enableSSL then [{ip = "*"; port = 443;}] else [{ip = "*"; port = 80;}]; getListen = cfg: if cfg.listen == [] then defaultListen cfg else cfg.listen; listenToString = l: "${l.ip}:${toString l.port}"; allHosts = [mainCfg] ++ mainCfg.virtualHosts; enableSSL = any (vhost: vhost.enableSSL) allHosts; enableUserDir = any (vhost: vhost.enableUserDir) allHosts; # 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_${mainCfg.multiProcessingModule}" ] ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) ++ optional enableSSL "ssl" ++ optional enableUserDir "userdir" ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } ++ optional mainCfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } ++ optional mainCfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } ++ mainCfg.extraModules; allDenied = "Require all denied"; allGranted = "Require all granted"; loggingConf = (if mainCfg.logFormat != "none" then '' ErrorLog ${mainCfg.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 ${mainCfg.logDir}/access.log ${mainCfg.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 ''; sslConf = '' SSLSessionCache shmcb:${runtimeDir}/ssl_scache(512000) Mutex posixsem SSLRandomSeed startup builtin SSLRandomSeed connect builtin SSLProtocol ${mainCfg.sslProtocols} SSLCipherSuite ${mainCfg.sslCiphers} SSLHonorCipherOrder on ''; mimeConf = '' TypesConfig ${httpd}/conf/mime.types AddType application/x-x509-ca-cert .crt AddType application/x-pkcs7-crl .crl AddType application/x-httpd-php .php .phtml MIMEMagicFile ${httpd}/conf/magic ''; perServerConf = isMainServer: cfg: let # 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)); maybeDocumentRoot = fold (svc: acc: if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc ) null ([ cfg ]); documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"; documentRootConf = '' DocumentRoot "${documentRoot}" Options Indexes FollowSymLinks AllowOverride None ${allGranted} ''; # If this is a vhost, the include the entries for the main server as well. robotsTxt = concatStringsSep "\n" (filter (x: x != "") ([ cfg.robotsEntries ] ++ lib.optional (!isMainServer) mainCfg.robotsEntries)); in '' ${concatStringsSep "\n" (map (n: "ServerName ${n}") canonicalNames)} ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases} ${if cfg.sslServerCert != null then '' SSLCertificateFile ${cfg.sslServerCert} SSLCertificateKeyFile ${cfg.sslServerKey} ${if cfg.sslServerChain != null then '' SSLCertificateChainFile ${cfg.sslServerChain} '' else ""} '' else ""} ${if cfg.enableSSL then '' SSLEngine on '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */ '' SSLEngine off '' else ""} ${if isMainServer || cfg.adminAddr != null then '' ServerAdmin ${cfg.adminAddr} '' else ""} ${if !isMainServer && mainCfg.logPerVirtualHost then '' ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat} '' else ""} ${optionalString (robotsTxt != "") '' Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt} ''} ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""} ${if cfg.enableUserDir then '' UserDir public_html UserDir disabled root AllowOverride FileInfo AuthConfig Limit Indexes Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec ${allGranted} ${allDenied} '' else ""} ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then '' RedirectPermanent / ${cfg.globalRedirect} '' else ""} ${ let makeFileConf = elem: '' Alias ${elem.urlPath} ${elem.file} ''; in concatMapStrings makeFileConf cfg.servedFiles } ${ let makeDirConf = elem: '' Alias ${elem.urlPath} ${elem.dir}/ Options +Indexes ${allGranted} AllowOverride All ''; in concatMapStrings makeDirConf cfg.servedDirs } ${cfg.extraConfig} ''; confFile = pkgs.writeText "httpd.conf" '' ServerRoot ${httpd} DefaultRuntimeDir ${runtimeDir}/runtime PidFile ${runtimeDir}/httpd.pid ${optionalString (mainCfg.multiProcessingModule != "prefork") '' # mod_cgid requires this. ScriptSock ${runtimeDir}/cgisock ''} MaxClients ${toString mainCfg.maxClients} MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild} ${let listen = concatMap getListen allHosts; toStr = listen: "Listen ${listenToString listen}\n"; uniqueListen = uniqList {inputList = map toStr listen;}; in concatStrings uniqueListen } User ${mainCfg.user} Group ${mainCfg.group} ${let mkModule = module: if isString module then { name = module; path = "${httpd}/modules/mod_${module}.so"; } else if isAttrs module then { inherit (module) name path; } else throw "Expecting either a string or attribute set including a name and path."; in concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules)) } AddHandler type-map var ${allDenied} ${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 TraceEnable off ${if enableSSL then sslConf else ""} # Fascist default - deny access to everything. Options FollowSymLinks AllowOverride None ${allDenied} # But do allow access to files in the store so that we don't have # to generate clauses for every generated file that we # want to serve. ${allGranted} # Generate directives for the main server. ${perServerConf true mainCfg} ${let makeVirtualHost = vhost: '' ${perServerConf false vhost} ''; in concatMapStrings makeVirtualHost mainCfg.virtualHosts } ''; # Generate the PHP configuration file. Should probably be factored # out into a separate module. phpIni = pkgs.runCommand "php.ini" { options = mainCfg.phpOptions; preferLocalBuild = true; } '' cat ${php}/etc/php.ini > $out echo "$options" >> $out ''; in { 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.") ]; ###### interface options = { services.httpd = { enable = mkOption { type = types.bool; default = false; description = "Whether to enable 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 = '' Cnfiguration lines appended to the generated Apache configuration file. Note that this mechanism may not work when 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 name and path of the module. ''; }; logPerVirtualHost = mkOption { type = types.bool; default = false; description = '' If enabled, each virtual host gets its own access.log and error.log, namely suffixed by the of the virtual host. ''; }; user = mkOption { type = types.str; default = "wwwrun"; description = '' User account under which httpd runs. The account is created automatically if it doesn't exist. ''; }; group = mkOption { type = types.str; default = "wwwrun"; description = '' Group under which httpd runs. The account is created automatically if it doesn't exist. ''; }; logDir = mkOption { type = types.path; default = "/var/log/httpd"; description = '' Directory for Apache's log files. It is created automatically. ''; }; virtualHosts = mkOption { type = types.listOf (types.submodule ( { options = import ./per-server-options.nix { inherit lib; forMainServer = false; }; })); default = []; example = [ { hostName = "foo"; documentRoot = "/data/webroot-foo"; } { hostName = "bar"; documentRoot = "/data/webroot-bar"; } ]; description = '' 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. ''; }; 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 php.ini."; }; multiProcessingModule = mkOption { type = types.str; default = "prefork"; example = "worker"; description = '' Multi-processing module to be used by Apache. Available modules are prefork (the default; handles each request in a separate child process), worker (hybrid approach that starts a number of child processes each running a number of threads) and event (a recent variant of worker 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"; example = "All -SSLv2 -SSLv3"; description = "Allowed SSL/TLS protocol versions."; }; } # Include the options shared between the main server and virtual hosts. // (import ./per-server-options.nix { inherit lib; forMainServer = true; }); }; ###### implementation config = mkIf config.services.httpd.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."; } ]; users.users = optionalAttrs (mainCfg.user == "wwwrun") (singleton { name = "wwwrun"; group = mainCfg.group; description = "Apache httpd user"; uid = config.ids.uids.wwwrun; }); users.groups = optionalAttrs (mainCfg.group == "wwwrun") (singleton { name = "wwwrun"; gid = config.ids.gids.wwwrun; }); environment.systemPackages = [httpd]; services.httpd.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}" ''; services.httpd.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.services.httpd = { description = "Apache HTTPD"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "fs.target" ]; path = [ httpd pkgs.coreutils pkgs.gnugrep ] ++ optional mainCfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function. environment = optionalAttrs mainCfg.enablePHP { PHPRC = phpIni; } // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }; preStart = '' 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 ${pkgs.utillinux}/bin/ipcrm -s $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.Group = mainCfg.group; serviceConfig.Type = "forking"; serviceConfig.PIDFile = "${runtimeDir}/httpd.pid"; serviceConfig.Restart = "always"; serviceConfig.RestartSec = "5s"; serviceConfig.RuntimeDirectory = "httpd httpd/runtime"; serviceConfig.RuntimeDirectoryMode = "0750"; }; }; }