1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 mainCfg = config.services.httpd;
8
9 httpd = mainCfg.package.out;
10
11 version24 = !versionOlder httpd.version "2.4";
12
13 httpdConf = mainCfg.configFile;
14
15 php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ };
16
17 phpMajorVersion = head (splitString "." php.version);
18
19 getPort = cfg: if cfg.port != 0 then cfg.port else if cfg.enableSSL then 443 else 80;
20
21 extraModules = attrByPath ["extraModules"] [] mainCfg;
22 extraForeignModules = filter isAttrs extraModules;
23 extraApacheModules = filter isString extraModules;
24
25
26 makeServerInfo = cfg: {
27 # Canonical name must not include a trailing slash.
28 canonicalName =
29 (if cfg.enableSSL then "https" else "http") + "://" +
30 cfg.hostName +
31 (if getPort cfg != (if cfg.enableSSL then 443 else 80) then ":${toString (getPort cfg)}" else "");
32
33 # Admin address: inherit from the main server if not specified for
34 # a virtual host.
35 adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr;
36
37 vhostConfig = cfg;
38 serverConfig = mainCfg;
39 fullConfig = config; # machine config
40 };
41
42
43 allHosts = [mainCfg] ++ mainCfg.virtualHosts;
44
45
46 callSubservices = serverInfo: defs:
47 let f = svc:
48 let
49 svcFunction =
50 if svc ? function then svc.function
51 else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix");
52 config = (evalModules
53 { modules = [ { options = res.options; config = svc.config or svc; } ];
54 check = false;
55 }).config;
56 defaults = {
57 extraConfig = "";
58 extraModules = [];
59 extraModulesPre = [];
60 extraPath = [];
61 extraServerPath = [];
62 globalEnvVars = [];
63 robotsEntries = "";
64 startupScript = "";
65 enablePHP = false;
66 phpOptions = "";
67 options = {};
68 documentRoot = null;
69 };
70 res = defaults // svcFunction { inherit config lib pkgs serverInfo php; };
71 in res;
72 in map f defs;
73
74
75 # !!! callSubservices is expensive
76 subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices;
77
78 mainSubservices = subservicesFor mainCfg;
79
80 allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts;
81
82
83 # !!! should be in lib
84 writeTextInDir = name: text:
85 pkgs.runCommand name {inherit text;} "mkdir -p $out; echo -n \"$text\" > $out/$name";
86
87
88 enableSSL = any (vhost: vhost.enableSSL) allHosts;
89
90
91 # Names of modules from ${httpd}/modules that we want to load.
92 apacheModules =
93 [ # HTTP authentication mechanisms: basic and digest.
94 "auth_basic" "auth_digest"
95
96 # Authentication: is the user who he claims to be?
97 "authn_file" "authn_dbm" "authn_anon"
98 (if version24 then "authn_core" else "authn_alias")
99
100 # Authorization: is the user allowed access?
101 "authz_user" "authz_groupfile" "authz_host"
102
103 # Other modules.
104 "ext_filter" "include" "log_config" "env" "mime_magic"
105 "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif"
106 "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs"
107 "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling"
108 "userdir" "alias" "rewrite" "proxy" "proxy_http"
109 ]
110 ++ optionals version24 [
111 "mpm_${mainCfg.multiProcessingModule}"
112 "authz_core"
113 "unixd"
114 "cache" "cache_disk"
115 "slotmem_shm"
116 "socache_shmcb"
117 # For compatibility with old configurations, the new module mod_access_compat is provided.
118 "access_compat"
119 ]
120 ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
121 ++ optional enableSSL "ssl"
122 ++ extraApacheModules;
123
124
125 allDenied = if version24 then ''
126 Require all denied
127 '' else ''
128 Order deny,allow
129 Deny from all
130 '';
131
132 allGranted = if version24 then ''
133 Require all granted
134 '' else ''
135 Order allow,deny
136 Allow from all
137 '';
138
139
140 loggingConf = (if mainCfg.logFormat != "none" then ''
141 ErrorLog ${mainCfg.logDir}/error_log
142
143 LogLevel notice
144
145 LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
146 LogFormat "%h %l %u %t \"%r\" %>s %b" common
147 LogFormat "%{Referer}i -> %U" referer
148 LogFormat "%{User-agent}i" agent
149
150 CustomLog ${mainCfg.logDir}/access_log ${mainCfg.logFormat}
151 '' else ''
152 ErrorLog /dev/null
153 '');
154
155
156 browserHacks = ''
157 BrowserMatch "Mozilla/2" nokeepalive
158 BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0
159 BrowserMatch "RealPlayer 4\.0" force-response-1.0
160 BrowserMatch "Java/1\.0" force-response-1.0
161 BrowserMatch "JDK/1\.0" force-response-1.0
162 BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully
163 BrowserMatch "^WebDrive" redirect-carefully
164 BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully
165 BrowserMatch "^gnome-vfs" redirect-carefully
166 '';
167
168
169 sslConf = ''
170 SSLSessionCache ${if version24 then "shmcb" else "shm"}:${mainCfg.stateDir}/ssl_scache(512000)
171
172 ${if version24 then "Mutex" else "SSLMutex"} posixsem
173
174 SSLRandomSeed startup builtin
175 SSLRandomSeed connect builtin
176
177 SSLProtocol All -SSLv2 -SSLv3
178 SSLCipherSuite HIGH:!aNULL:!MD5:!EXP
179 SSLHonorCipherOrder on
180 '';
181
182
183 mimeConf = ''
184 TypesConfig ${httpd}/conf/mime.types
185
186 AddType application/x-x509-ca-cert .crt
187 AddType application/x-pkcs7-crl .crl
188 AddType application/x-httpd-php .php .phtml
189
190 <IfModule mod_mime_magic.c>
191 MIMEMagicFile ${httpd}/conf/magic
192 </IfModule>
193 '';
194
195
196 perServerConf = isMainServer: cfg: let
197
198 serverInfo = makeServerInfo cfg;
199
200 subservices = callSubservices serverInfo cfg.extraSubservices;
201
202 maybeDocumentRoot = fold (svc: acc:
203 if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc
204 ) null ([ cfg ] ++ subservices);
205
206 documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else
207 pkgs.runCommand "empty" {} "mkdir -p $out";
208
209 documentRootConf = ''
210 DocumentRoot "${documentRoot}"
211
212 <Directory "${documentRoot}">
213 Options Indexes FollowSymLinks
214 AllowOverride None
215 ${allGranted}
216 </Directory>
217 '';
218
219 robotsTxt =
220 concatStringsSep "\n" (filter (x: x != "") (
221 # If this is a vhost, the include the entries for the main server as well.
222 (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices)
223 ++ [cfg.robotsEntries]
224 ++ (map (svc: svc.robotsEntries) subservices)));
225
226 in ''
227 ServerName ${serverInfo.canonicalName}
228
229 ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases}
230
231 ${if cfg.sslServerCert != null then ''
232 SSLCertificateFile ${cfg.sslServerCert}
233 SSLCertificateKeyFile ${cfg.sslServerKey}
234 ${if cfg.sslServerChain != null then ''
235 SSLCertificateChainFile ${cfg.sslServerChain}
236 '' else ""}
237 '' else ""}
238
239 ${if cfg.enableSSL then ''
240 SSLEngine on
241 '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */
242 ''
243 SSLEngine off
244 '' else ""}
245
246 ${if isMainServer || cfg.adminAddr != null then ''
247 ServerAdmin ${cfg.adminAddr}
248 '' else ""}
249
250 ${if !isMainServer && mainCfg.logPerVirtualHost then ''
251 ErrorLog ${mainCfg.logDir}/error_log-${cfg.hostName}
252 CustomLog ${mainCfg.logDir}/access_log-${cfg.hostName} ${cfg.logFormat}
253 '' else ""}
254
255 ${optionalString (robotsTxt != "") ''
256 Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt}
257 ''}
258
259 ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""}
260
261 ${if cfg.enableUserDir then ''
262
263 UserDir public_html
264 UserDir disabled root
265
266 <Directory "/home/*/public_html">
267 AllowOverride FileInfo AuthConfig Limit Indexes
268 Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
269 <Limit GET POST OPTIONS>
270 ${allGranted}
271 </Limit>
272 <LimitExcept GET POST OPTIONS>
273 ${allDenied}
274 </LimitExcept>
275 </Directory>
276
277 '' else ""}
278
279 ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then ''
280 RedirectPermanent / ${cfg.globalRedirect}
281 '' else ""}
282
283 ${
284 let makeFileConf = elem: ''
285 Alias ${elem.urlPath} ${elem.file}
286 '';
287 in concatMapStrings makeFileConf cfg.servedFiles
288 }
289
290 ${
291 let makeDirConf = elem: ''
292 Alias ${elem.urlPath} ${elem.dir}/
293 <Directory ${elem.dir}>
294 Options +Indexes
295 ${allGranted}
296 AllowOverride All
297 </Directory>
298 '';
299 in concatMapStrings makeDirConf cfg.servedDirs
300 }
301
302 ${concatMapStrings (svc: svc.extraConfig) subservices}
303
304 ${cfg.extraConfig}
305 '';
306
307
308 confFile = pkgs.writeText "httpd.conf" ''
309
310 ServerRoot ${httpd}
311
312 ${optionalString version24 ''
313 DefaultRuntimeDir ${mainCfg.stateDir}/runtime
314 ''}
315
316 PidFile ${mainCfg.stateDir}/httpd.pid
317
318 ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
319 # mod_cgid requires this.
320 ScriptSock ${mainCfg.stateDir}/cgisock
321 ''}
322
323 <IfModule prefork.c>
324 MaxClients ${toString mainCfg.maxClients}
325 MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild}
326 </IfModule>
327
328 ${let
329 ports = map getPort allHosts;
330 uniquePorts = uniqList {inputList = ports;};
331 in concatMapStrings (port: "Listen ${toString port}\n") uniquePorts
332 }
333
334 User ${mainCfg.user}
335 Group ${mainCfg.group}
336
337 ${let
338 load = {name, path}: "LoadModule ${name}_module ${path}\n";
339 allModules =
340 concatMap (svc: svc.extraModulesPre) allSubservices
341 ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules
342 ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
343 ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
344 ++ concatMap (svc: svc.extraModules) allSubservices
345 ++ extraForeignModules;
346 in concatMapStrings load allModules
347 }
348
349 AddHandler type-map var
350
351 <Files ~ "^\.ht">
352 ${allDenied}
353 </Files>
354
355 ${mimeConf}
356 ${loggingConf}
357 ${browserHacks}
358
359 Include ${httpd}/conf/extra/httpd-default.conf
360 Include ${httpd}/conf/extra/httpd-autoindex.conf
361 Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
362 Include ${httpd}/conf/extra/httpd-languages.conf
363
364 ${if enableSSL then sslConf else ""}
365
366 # Fascist default - deny access to everything.
367 <Directory />
368 Options FollowSymLinks
369 AllowOverride None
370 ${allDenied}
371 </Directory>
372
373 # But do allow access to files in the store so that we don't have
374 # to generate <Directory> clauses for every generated file that we
375 # want to serve.
376 <Directory /nix/store>
377 ${allGranted}
378 </Directory>
379
380 # Generate directives for the main server.
381 ${perServerConf true mainCfg}
382
383 # Always enable virtual hosts; it doesn't seem to hurt.
384 ${let
385 ports = map getPort allHosts;
386 uniquePorts = uniqList {inputList = ports;};
387 directives = concatMapStrings (port: "NameVirtualHost *:${toString port}\n") uniquePorts;
388 in optionalString (!version24) directives
389 }
390
391 ${let
392 makeVirtualHost = vhost: ''
393 <VirtualHost *:${toString (getPort vhost)}>
394 ${perServerConf false vhost}
395 </VirtualHost>
396 '';
397 in concatMapStrings makeVirtualHost mainCfg.virtualHosts
398 }
399 '';
400
401
402 enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices;
403
404
405 # Generate the PHP configuration file. Should probably be factored
406 # out into a separate module.
407 phpIni = pkgs.runCommand "php.ini"
408 { options = concatStringsSep "\n"
409 ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices));
410 }
411 ''
412 cat ${php}/etc/php.ini > $out
413 echo "$options" >> $out
414 '';
415
416in
417
418
419{
420
421 ###### interface
422
423 options = {
424
425 services.httpd = {
426
427 enable = mkOption {
428 type = types.bool;
429 default = false;
430 description = "Whether to enable the Apache HTTP Server.";
431 };
432
433 package = mkOption {
434 type = types.package;
435 default = pkgs.apacheHttpd;
436 defaultText = "pkgs.apacheHttpd";
437 description = ''
438 Overridable attribute of the Apache HTTP Server package to use.
439 '';
440 };
441
442 configFile = mkOption {
443 type = types.path;
444 default = confFile;
445 defaultText = "confFile";
446 example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
447 description = ''
448 Override the configuration file used by Apache. By default,
449 NixOS generates one automatically.
450 '';
451 };
452
453 extraConfig = mkOption {
454 type = types.lines;
455 default = "";
456 description = ''
457 Cnfiguration lines appended to the generated Apache
458 configuration file. Note that this mechanism may not work
459 when <option>configFile</option> is overridden.
460 '';
461 };
462
463 extraModules = mkOption {
464 type = types.listOf types.unspecified;
465 default = [];
466 example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]'';
467 description = ''
468 Additional Apache modules to be used. These can be
469 specified as a string in the case of modules distributed
470 with Apache, or as an attribute set specifying the
471 <varname>name</varname> and <varname>path</varname> of the
472 module.
473 '';
474 };
475
476 logPerVirtualHost = mkOption {
477 type = types.bool;
478 default = false;
479 description = ''
480 If enabled, each virtual host gets its own
481 <filename>access_log</filename> and
482 <filename>error_log</filename>, namely suffixed by the
483 <option>hostName</option> of the virtual host.
484 '';
485 };
486
487 user = mkOption {
488 type = types.str;
489 default = "wwwrun";
490 description = ''
491 User account under which httpd runs. The account is created
492 automatically if it doesn't exist.
493 '';
494 };
495
496 group = mkOption {
497 type = types.str;
498 default = "wwwrun";
499 description = ''
500 Group under which httpd runs. The account is created
501 automatically if it doesn't exist.
502 '';
503 };
504
505 logDir = mkOption {
506 type = types.path;
507 default = "/var/log/httpd";
508 description = ''
509 Directory for Apache's log files. It is created automatically.
510 '';
511 };
512
513 stateDir = mkOption {
514 type = types.path;
515 default = "/run/httpd";
516 description = ''
517 Directory for Apache's transient runtime state (such as PID
518 files). It is created automatically. Note that the default,
519 <filename>/run/httpd</filename>, is deleted at boot time.
520 '';
521 };
522
523 virtualHosts = mkOption {
524 type = types.listOf (types.submodule (
525 { options = import ./per-server-options.nix {
526 inherit lib;
527 forMainServer = false;
528 };
529 }));
530 default = [];
531 example = [
532 { hostName = "foo";
533 documentRoot = "/data/webroot-foo";
534 }
535 { hostName = "bar";
536 documentRoot = "/data/webroot-bar";
537 }
538 ];
539 description = ''
540 Specification of the virtual hosts served by Apache. Each
541 element should be an attribute set specifying the
542 configuration of the virtual host. The available options
543 are the non-global options permissible for the main host.
544 '';
545 };
546
547 enableMellon = mkOption {
548 type = types.bool;
549 default = false;
550 description = "Whether to enable the mod_auth_mellon module.";
551 };
552
553 enablePHP = mkOption {
554 type = types.bool;
555 default = false;
556 description = "Whether to enable the PHP module.";
557 };
558
559 phpPackage = mkOption {
560 type = types.package;
561 default = pkgs.php;
562 defaultText = "pkgs.php";
563 description = ''
564 Overridable attribute of the PHP package to use.
565 '';
566 };
567
568 phpOptions = mkOption {
569 type = types.lines;
570 default = "";
571 example =
572 ''
573 date.timezone = "CET"
574 '';
575 description =
576 "Options appended to the PHP configuration file <filename>php.ini</filename>.";
577 };
578
579 multiProcessingModule = mkOption {
580 type = types.str;
581 default = "prefork";
582 example = "worker";
583 description =
584 ''
585 Multi-processing module to be used by Apache. Available
586 modules are <literal>prefork</literal> (the default;
587 handles each request in a separate child process),
588 <literal>worker</literal> (hybrid approach that starts a
589 number of child processes each running a number of
590 threads) and <literal>event</literal> (a recent variant of
591 <literal>worker</literal> that handles persistent
592 connections more efficiently).
593 '';
594 };
595
596 maxClients = mkOption {
597 type = types.int;
598 default = 150;
599 example = 8;
600 description = "Maximum number of httpd processes (prefork)";
601 };
602
603 maxRequestsPerChild = mkOption {
604 type = types.int;
605 default = 0;
606 example = 500;
607 description =
608 "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited";
609 };
610 }
611
612 # Include the options shared between the main server and virtual hosts.
613 // (import ./per-server-options.nix {
614 inherit lib;
615 forMainServer = true;
616 });
617
618 };
619
620
621 ###### implementation
622
623 config = mkIf config.services.httpd.enable {
624
625 assertions = [ { assertion = mainCfg.enableSSL == true
626 -> mainCfg.sslServerCert != null
627 && mainCfg.sslServerKey != null;
628 message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; }
629 ];
630
631 users.extraUsers = optionalAttrs (mainCfg.user == "wwwrun") (singleton
632 { name = "wwwrun";
633 group = mainCfg.group;
634 description = "Apache httpd user";
635 uid = config.ids.uids.wwwrun;
636 });
637
638 users.extraGroups = optionalAttrs (mainCfg.group == "wwwrun") (singleton
639 { name = "wwwrun";
640 gid = config.ids.gids.wwwrun;
641 });
642
643 environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices;
644
645 services.httpd.phpOptions =
646 ''
647 ; Needed for PHP's mail() function.
648 sendmail_path = sendmail -t -i
649
650 ; Apparently PHP doesn't use $TZ.
651 date.timezone = "${config.time.timeZone}"
652 '';
653
654 systemd.services.httpd =
655 { description = "Apache HTTPD";
656
657 wantedBy = [ "multi-user.target" ];
658 wants = [ "keys.target" ];
659 after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ];
660
661 path =
662 [ httpd pkgs.coreutils pkgs.gnugrep ]
663 ++ # Needed for PHP's mail() function. !!! Probably the
664 # ssmtp module should export the path to sendmail in
665 # some way.
666 optional config.networking.defaultMailServer.directDelivery pkgs.ssmtp
667 ++ concatMap (svc: svc.extraServerPath) allSubservices;
668
669 environment =
670 optionalAttrs enablePHP { PHPRC = phpIni; }
671 // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; }
672 // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices));
673
674 preStart =
675 ''
676 mkdir -m 0750 -p ${mainCfg.stateDir}
677 [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir}
678 ${optionalString version24 ''
679 mkdir -m 0750 -p "${mainCfg.stateDir}/runtime"
680 [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime"
681 ''}
682 mkdir -m 0700 -p ${mainCfg.logDir}
683
684 ${optionalString (mainCfg.documentRoot != null)
685 ''
686 # Create the document root directory if does not exists yet
687 mkdir -p ${mainCfg.documentRoot}
688 ''
689 }
690
691 # Get rid of old semaphores. These tend to accumulate across
692 # server restarts, eventually preventing it from restarting
693 # successfully.
694 for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do
695 ${pkgs.utillinux}/bin/ipcrm -s $i
696 done
697
698 # Run the startup hooks for the subservices.
699 for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do
700 echo Running Apache startup hook $i...
701 $i
702 done
703 '';
704
705 serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
706 serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
707 serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful";
708 serviceConfig.Type = "forking";
709 serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid";
710 serviceConfig.Restart = "always";
711 serviceConfig.RestartSec = "5s";
712 };
713
714 };
715
716}