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