1 { config, lib, pkgs, ... }:
7 mainCfg = config.services.httpdTools;
9 httpd = mainCfg.package.out;
11 version24 = !versionOlder httpd.version "2.4";
13 httpdConf = mainCfg.configFile;
15 php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ };
17 phpMajorVersion = head (splitString "." php.version);
19 mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; };
21 defaultListen = cfg: if cfg.enableSSL
22 then [{ip = "*"; port = 443;}]
23 else [{ip = "*"; port = 80;}];
26 let list = (lib.optional (cfg.port != 0) {ip = "*"; port = cfg.port;}) ++ cfg.listen;
28 then defaultListen cfg
31 listenToString = l: "${l.ip}:${toString l.port}";
33 extraModules = attrByPath ["extraModules"] [] mainCfg;
34 extraForeignModules = filter isAttrs extraModules;
35 extraApacheModules = filter isString extraModules;
38 makeServerInfo = cfg: {
39 # Canonical name must not include a trailing slash.
41 let defaultPort = (head (defaultListen cfg)).port; in
43 (if cfg.enableSSL then "https" else "http") + "://" +
45 (if port != defaultPort then ":${toString port}" else "")
46 ) (map (x: x.port) (getListen cfg));
48 # Admin address: inherit from the main server if not specified for
50 adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr;
53 serverConfig = mainCfg;
54 fullConfig = config; # machine config
58 allHosts = [mainCfg] ++ mainCfg.virtualHosts;
61 callSubservices = serverInfo: defs:
65 if svc ? function then svc.function
66 # instead of using serviceType="mediawiki"; you can copy mediawiki.nix to any location outside nixpkgs, modify it at will, and use serviceExpression=./mediawiki.nix;
67 else if svc ? serviceExpression then import (toString svc.serviceExpression)
68 else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix");
70 { modules = [ { options = res.options; config = svc.config or svc; } ];
88 res = defaults // svcFunction { inherit config lib pkgs serverInfo php; };
93 # !!! callSubservices is expensive
94 subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices;
96 mainSubservices = subservicesFor mainCfg;
98 allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts;
101 enableSSL = any (vhost: vhost.enableSSL) allHosts;
104 # Names of modules from ${httpd}/modules that we want to load.
106 [ # HTTP authentication mechanisms: basic and digest.
107 "auth_basic" "auth_digest"
109 # Authentication: is the user who he claims to be?
110 "authn_file" "authn_dbm" "authn_anon"
111 (if version24 then "authn_core" else "authn_alias")
113 # Authorization: is the user allowed access?
114 "authz_user" "authz_groupfile" "authz_host"
117 "ext_filter" "include" "log_config" "env" "mime_magic"
118 "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif"
119 "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs"
120 "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling"
121 "userdir" "alias" "rewrite" "proxy" "proxy_http"
123 ++ optionals version24 [
124 "mpm_${mainCfg.multiProcessingModule}"
130 # For compatibility with old configurations, the new module mod_access_compat is provided.
133 ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
134 ++ optional enableSSL "ssl"
135 ++ extraApacheModules;
138 allDenied = if version24 then ''
145 allGranted = if version24 then ''
153 loggingConf = (if mainCfg.logFormat != "none" then ''
154 ErrorLog ${mainCfg.logDir}/error.log
158 LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
159 LogFormat "%h %l %u %t \"%r\" %>s %b" common
160 LogFormat "%{Referer}i -> %U" referer
161 LogFormat "%{User-agent}i" agent
163 CustomLog ${mainCfg.logDir}/access.log ${mainCfg.logFormat}
170 BrowserMatch "Mozilla/2" nokeepalive
171 BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
172 BrowserMatch "RealPlayer 4\.0" force-response-1.0
173 BrowserMatch "Java/1\.0" force-response-1.0
174 BrowserMatch "JDK/1\.0" force-response-1.0
175 BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
176 BrowserMatch "^WebDrive" redirect-carefully
177 BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
178 BrowserMatch "^gnome-vfs" redirect-carefully
183 SSLSessionCache ${if version24 then "shmcb" else "shm"}:${mainCfg.stateDir}/ssl_scache(512000)
185 ${if version24 then "Mutex" else "SSLMutex"} posixsem
187 SSLRandomSeed startup builtin
188 SSLRandomSeed connect builtin
190 SSLProtocol ${mainCfg.sslProtocols}
191 SSLCipherSuite ${mainCfg.sslCiphers}
192 SSLHonorCipherOrder on
197 TypesConfig ${httpd}/conf/mime.types
199 AddType application/x-x509-ca-cert .crt
200 AddType application/x-pkcs7-crl .crl
201 AddType application/x-httpd-php .php .phtml
203 <IfModule mod_mime_magic.c>
204 MIMEMagicFile ${httpd}/conf/magic
209 perServerConf = isMainServer: cfg: let
211 serverInfo = makeServerInfo cfg;
213 subservices = callSubservices serverInfo cfg.extraSubservices;
215 maybeDocumentRoot = fold (svc: acc:
216 if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc
217 ) null ([ cfg ] ++ subservices);
219 documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else
220 pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out";
222 documentRootConf = ''
223 DocumentRoot "${documentRoot}"
225 <Directory "${documentRoot}">
226 Options Indexes FollowSymLinks
233 concatStringsSep "\n" (filter (x: x != "") (
234 # If this is a vhost, the include the entries for the main server as well.
235 (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices)
236 ++ [cfg.robotsEntries]
237 ++ (map (svc: svc.robotsEntries) subservices)));
240 ${concatStringsSep "\n" (map (n: "ServerName ${n}") serverInfo.canonicalNames)}
242 ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases}
244 ${if cfg.sslServerCert != null then ''
245 SSLCertificateFile ${cfg.sslServerCert}
246 SSLCertificateKeyFile ${cfg.sslServerKey}
247 ${if cfg.sslServerChain != null then ''
248 SSLCertificateChainFile ${cfg.sslServerChain}
252 ${if cfg.enableSSL then ''
254 '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */
259 ${if isMainServer || cfg.adminAddr != null then ''
260 ServerAdmin ${cfg.adminAddr}
263 ${if !isMainServer && mainCfg.logPerVirtualHost then ''
264 ErrorLog ${mainCfg.logDir}/error-${cfg.hostName}.log
265 CustomLog ${mainCfg.logDir}/access-${cfg.hostName}.log ${cfg.logFormat}
268 ${optionalString (robotsTxt != "") ''
269 Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt}
272 ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""}
274 ${if cfg.enableUserDir then ''
277 UserDir disabled root
279 <Directory "/home/*/public_html">
280 AllowOverride FileInfo AuthConfig Limit Indexes
281 Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
282 <Limit GET POST OPTIONS>
285 <LimitExcept GET POST OPTIONS>
292 ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then ''
293 RedirectPermanent / ${cfg.globalRedirect}
297 let makeFileConf = elem: ''
298 Alias ${elem.urlPath} ${elem.file}
300 in concatMapStrings makeFileConf cfg.servedFiles
304 let makeDirConf = elem: ''
305 Alias ${elem.urlPath} ${elem.dir}/
306 <Directory ${elem.dir}>
312 in concatMapStrings makeDirConf cfg.servedDirs
315 ${concatMapStrings (svc: svc.extraConfig) subservices}
321 confFile = pkgs.writeText "httpd.conf" ''
325 ${optionalString version24 ''
326 DefaultRuntimeDir ${mainCfg.stateDir}/runtime
329 PidFile ${mainCfg.stateDir}/httpd.pid
331 ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
332 # mod_cgid requires this.
333 ScriptSock ${mainCfg.stateDir}/cgisock
337 MaxClients ${toString mainCfg.maxClients}
338 MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild}
342 listen = concatMap getListen allHosts;
343 toStr = listen: "Listen ${listenToString listen}\n";
344 uniqueListen = uniqList {inputList = map toStr listen;};
345 in concatStrings uniqueListen
349 Group ${mainCfg.group}
352 load = {name, path}: "LoadModule ${name}_module ${path}\n";
354 concatMap (svc: svc.extraModulesPre) allSubservices
355 ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules
356 ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
357 ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
358 ++ optional enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
359 ++ concatMap (svc: svc.extraModules) allSubservices
360 ++ extraForeignModules;
361 in concatMapStrings load allModules
364 AddHandler type-map var
374 Include ${httpd}/conf/extra/httpd-default.conf
375 Include ${httpd}/conf/extra/httpd-autoindex.conf
376 Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
377 Include ${httpd}/conf/extra/httpd-languages.conf
381 ${if enableSSL then sslConf else ""}
383 # Fascist default - deny access to everything.
385 Options FollowSymLinks
390 # Generate directives for the main server.
391 ${perServerConf true mainCfg}
393 # Always enable virtual hosts; it doesn't seem to hurt.
395 listen = concatMap getListen allHosts;
396 uniqueListen = uniqList {inputList = listen;};
397 directives = concatMapStrings (listen: "NameVirtualHost ${listenToString listen}\n") uniqueListen;
398 in optionalString (!version24) directives
402 makeVirtualHost = vhost: ''
403 <VirtualHost ${concatStringsSep " " (map listenToString (getListen vhost))}>
404 ${perServerConf false vhost}
407 in concatMapStrings makeVirtualHost mainCfg.virtualHosts
412 enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices;
414 enablePerl = mainCfg.enablePerl || any (svc: svc.enablePerl) allSubservices;
417 # Generate the PHP configuration file. Should probably be factored
418 # out into a separate module.
419 phpIni = pkgs.runCommand "php.ini"
420 { options = concatStringsSep "\n"
421 ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices));
422 preferLocalBuild = true;
425 cat ${php}/etc/php.ini > $out
426 echo "$options" >> $out
438 services.httpdTools = {
443 description = "Whether to enable the Apache HTTP Server.";
447 type = types.package;
448 default = pkgs.apacheHttpd;
449 defaultText = "pkgs.apacheHttpd";
451 Overridable attribute of the Apache HTTP Server package to use.
455 configFile = mkOption {
458 defaultText = "confFile";
459 example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
461 Override the configuration file used by Apache. By default,
462 NixOS generates one automatically.
466 extraConfig = mkOption {
470 Cnfiguration lines appended to the generated Apache
471 configuration file. Note that this mechanism may not work
472 when <option>configFile</option> is overridden.
476 extraModules = mkOption {
477 type = types.listOf types.unspecified;
479 example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]'';
481 Additional Apache modules to be used. These can be
482 specified as a string in the case of modules distributed
483 with Apache, or as an attribute set specifying the
484 <varname>name</varname> and <varname>path</varname> of the
489 logPerVirtualHost = mkOption {
493 If enabled, each virtual host gets its own
494 <filename>access.log</filename> and
495 <filename>error.log</filename>, namely suffixed by the
496 <option>hostName</option> of the virtual host.
504 User account under which httpd runs. The account is created
505 automatically if it doesn't exist.
513 Group under which httpd runs. The account is created
514 automatically if it doesn't exist.
520 default = "/var/log/httpd";
522 Directory for Apache's log files. It is created automatically.
526 stateDir = mkOption {
528 default = "/run/httpd";
530 Directory for Apache's transient runtime state (such as PID
531 files). It is created automatically. Note that the default,
532 <filename>/run/httpd</filename>, is deleted at boot time.
536 virtualHosts = mkOption {
537 type = types.listOf (types.submodule (
538 { options = import <nixpkgs/nixos/modules/services/web-servers/apache-httpd/per-server-options.nix> {
540 forMainServer = false;
546 documentRoot = "/data/webroot-foo";
549 documentRoot = "/data/webroot-bar";
553 Specification of the virtual hosts served by Apache. Each
554 element should be an attribute set specifying the
555 configuration of the virtual host. The available options
556 are the non-global options permissible for the main host.
560 enableMellon = mkOption {
563 description = "Whether to enable the mod_auth_mellon module.";
566 enablePHP = mkOption {
569 description = "Whether to enable the PHP module.";
572 phpPackage = mkOption {
573 type = types.package;
575 defaultText = "pkgs.php";
577 Overridable attribute of the PHP package to use.
581 enablePerl = mkOption {
584 description = "Whether to enable the Perl module (mod_perl).";
587 phpOptions = mkOption {
592 date.timezone = "CET"
595 "Options appended to the PHP configuration file <filename>php.ini</filename>.";
598 multiProcessingModule = mkOption {
604 Multi-processing module to be used by Apache. Available
605 modules are <literal>prefork</literal> (the default;
606 handles each request in a separate child process),
607 <literal>worker</literal> (hybrid approach that starts a
608 number of child processes each running a number of
609 threads) and <literal>event</literal> (a recent variant of
610 <literal>worker</literal> that handles persistent
611 connections more efficiently).
615 maxClients = mkOption {
619 description = "Maximum number of httpd processes (prefork)";
622 maxRequestsPerChild = mkOption {
627 "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited";
630 sslCiphers = mkOption {
632 default = "HIGH:!aNULL:!MD5:!EXP";
633 description = "Cipher Suite available for negotiation in SSL proxy handshake.";
636 sslProtocols = mkOption {
638 default = "All -SSLv2 -SSLv3 -TLSv1";
639 example = "All -SSLv2 -SSLv3";
640 description = "Allowed SSL/TLS protocol versions.";
644 # Include the options shared between the main server and virtual hosts.
645 // (import ./per-server-options.nix {
647 forMainServer = true;
653 ###### implementation
655 config = mkIf config.services.httpdTools.enable {
657 assertions = [ { assertion = mainCfg.enableSSL == true
658 -> mainCfg.sslServerCert != null
659 && mainCfg.sslServerKey != null;
660 message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; }
663 warnings = map (cfg: ''apache-httpd's port option is deprecated. Use listen = [{/*ip = "*"; */ port = ${toString cfg.port};}]; instead'' ) (lib.filter (cfg: cfg.port != 0) allHosts);
665 users.users = optionalAttrs (mainCfg.user == "wwwrun") (singleton
667 group = mainCfg.group;
668 description = "Apache httpd user";
669 uid = config.ids.uids.wwwrun;
672 users.groups = optionalAttrs (mainCfg.group == "wwwrun") (singleton
674 gid = config.ids.gids.wwwrun;
677 environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices;
679 services.httpdTools.phpOptions =
681 ; Needed for PHP's mail() function.
682 sendmail_path = sendmail -t -i
684 ; Don't advertise PHP
686 '' + optionalString (!isNull config.time.timeZone) ''
688 ; Apparently PHP doesn't use $TZ.
689 date.timezone = "${config.time.timeZone}"
692 systemd.services.httpdTools =
693 { description = "Apache HTTPD";
695 wantedBy = [ "multi-user.target" ];
696 wants = [ "keys.target" ];
697 after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ];
700 [ httpd pkgs.coreutils pkgs.gnugrep ]
701 ++ optional enablePHP pkgs.system-sendmail # Needed for PHP's mail() function.
702 ++ concatMap (svc: svc.extraServerPath) allSubservices;
705 optionalAttrs enablePHP { PHPRC = phpIni; }
706 // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }
707 // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices));
711 mkdir -m 0750 -p ${mainCfg.stateDir}
712 [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir}
713 ${optionalString version24 ''
714 mkdir -m 0750 -p "${mainCfg.stateDir}/runtime"
715 [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime"
717 mkdir -m 0700 -p ${mainCfg.logDir}
719 # Get rid of old semaphores. These tend to accumulate across
720 # server restarts, eventually preventing it from restarting
722 for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do
723 ${pkgs.utillinux}/bin/ipcrm -s $i
726 # Run the startup hooks for the subservices.
727 for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do
728 echo Running Apache startup hook $i...
733 serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
734 serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
735 serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful";
736 serviceConfig.Type = "forking";
737 serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid";
738 serviceConfig.Restart = "always";
739 serviceConfig.RestartSec = "5s";