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:!aNULL:!MD5:!EXP
177 SSLHonorCipherOrder on
178 '';
179
180
181 mimeConf = ''
182 TypesConfig ${httpd}/conf/mime.types
183
184 AddType application/x-x509-ca-cert .crt
185 AddType application/x-pkcs7-crl .crl
186 AddType application/x-httpd-php .php .phtml
187
188 <IfModule mod_mime_magic.c>
189 MIMEMagicFile ${httpd}/conf/magic
190 </IfModule>
191 '';
192
193
194 perServerConf = isMainServer: cfg: let
195
196 serverInfo = makeServerInfo cfg;
197
198 subservices = callSubservices serverInfo cfg.extraSubservices;
199
200 maybeDocumentRoot = fold (svc: acc:
201 if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc
202 ) null ([ cfg ] ++ subservices);
203
204 documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else
205 pkgs.runCommand "empty" {} "mkdir -p $out";
206
207 documentRootConf = ''
208 DocumentRoot "${documentRoot}"
209
210 <Directory "${documentRoot}">
211 Options Indexes FollowSymLinks
212 AllowOverride None
213 ${allGranted}
214 </Directory>
215 '';
216
217 robotsTxt =
218 concatStringsSep "\n" (filter (x: x != "") (
219 # If this is a vhost, the include the entries for the main server as well.
220 (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices)
221 ++ [cfg.robotsEntries]
222 ++ (map (svc: svc.robotsEntries) subservices)));
223
224 in ''
225 ServerName ${serverInfo.canonicalName}
226
227 ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases}
228
229 ${if cfg.sslServerCert != null then ''
230 SSLCertificateFile ${cfg.sslServerCert}
231 SSLCertificateKeyFile ${cfg.sslServerKey}
232 ${if cfg.sslServerChain != null then ''
233 SSLCertificateChainFile ${cfg.sslServerChain}
234 '' else ""}
235 '' else ""}
236
237 ${if cfg.enableSSL then ''
238 SSLEngine on
239 '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */
240 ''
241 SSLEngine off
242 '' else ""}
243
244 ${if isMainServer || cfg.adminAddr != null then ''
245 ServerAdmin ${cfg.adminAddr}
246 '' else ""}
247
248 ${if !isMainServer && mainCfg.logPerVirtualHost then ''
249 ErrorLog ${mainCfg.logDir}/error_log-${cfg.hostName}
250 CustomLog ${mainCfg.logDir}/access_log-${cfg.hostName} ${cfg.logFormat}
251 '' else ""}
252
253 ${optionalString (robotsTxt != "") ''
254 Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt}
255 ''}
256
257 ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""}
258
259 ${if cfg.enableUserDir then ''
260
261 UserDir public_html
262 UserDir disabled root
263
264 <Directory "/home/*/public_html">
265 AllowOverride FileInfo AuthConfig Limit Indexes
266 Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
267 <Limit GET POST OPTIONS>
268 ${allGranted}
269 </Limit>
270 <LimitExcept GET POST OPTIONS>
271 ${allDenied}
272 </LimitExcept>
273 </Directory>
274
275 '' else ""}
276
277 ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then ''
278 RedirectPermanent / ${cfg.globalRedirect}
279 '' else ""}
280
281 ${
282 let makeFileConf = elem: ''
283 Alias ${elem.urlPath} ${elem.file}
284 '';
285 in concatMapStrings makeFileConf cfg.servedFiles
286 }
287
288 ${
289 let makeDirConf = elem: ''
290 Alias ${elem.urlPath} ${elem.dir}/
291 <Directory ${elem.dir}>
292 Options +Indexes
293 ${allGranted}
294 AllowOverride All
295 </Directory>
296 '';
297 in concatMapStrings makeDirConf cfg.servedDirs
298 }
299
300 ${concatMapStrings (svc: svc.extraConfig) subservices}
301
302 ${cfg.extraConfig}
303 '';
304
305
306 confFile = pkgs.writeText "httpd.conf" ''
307
308 ServerRoot ${httpd}
309
310 ${optionalString version24 ''
311 DefaultRuntimeDir ${mainCfg.stateDir}/runtime
312 ''}
313
314 PidFile ${mainCfg.stateDir}/httpd.pid
315
316 ${optionalString (mainCfg.multiProcessingModule != "prefork") ''
317 # mod_cgid requires this.
318 ScriptSock ${mainCfg.stateDir}/cgisock
319 ''}
320
321 <IfModule prefork.c>
322 MaxClients ${toString mainCfg.maxClients}
323 MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild}
324 </IfModule>
325
326 ${let
327 ports = map getPort allHosts;
328 uniquePorts = uniqList {inputList = ports;};
329 in concatMapStrings (port: "Listen ${toString port}\n") uniquePorts
330 }
331
332 User ${mainCfg.user}
333 Group ${mainCfg.group}
334
335 ${let
336 load = {name, path}: "LoadModule ${name}_module ${path}\n";
337 allModules =
338 concatMap (svc: svc.extraModulesPre) allSubservices
339 ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules
340 ++ optional enablePHP { name = "php5"; path = "${php}/modules/libphp5.so"; }
341 ++ concatMap (svc: svc.extraModules) allSubservices
342 ++ extraForeignModules;
343 in concatMapStrings load allModules
344 }
345
346 AddHandler type-map var
347
348 <Files ~ "^\.ht">
349 ${allDenied}
350 </Files>
351
352 ${mimeConf}
353 ${loggingConf}
354 ${browserHacks}
355
356 Include ${httpd}/conf/extra/httpd-default.conf
357 Include ${httpd}/conf/extra/httpd-autoindex.conf
358 Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf
359 Include ${httpd}/conf/extra/httpd-languages.conf
360
361 ${if enableSSL then sslConf else ""}
362
363 # Fascist default - deny access to everything.
364 <Directory />
365 Options FollowSymLinks
366 AllowOverride None
367 ${allDenied}
368 </Directory>
369
370 # But do allow access to files in the store so that we don't have
371 # to generate <Directory> clauses for every generated file that we
372 # want to serve.
373 <Directory /nix/store>
374 ${allGranted}
375 </Directory>
376
377 # Generate directives for the main server.
378 ${perServerConf true mainCfg}
379
380 # Always enable virtual hosts; it doesn't seem to hurt.
381 ${let
382 ports = map getPort allHosts;
383 uniquePorts = uniqList {inputList = ports;};
384 directives = concatMapStrings (port: "NameVirtualHost *:${toString port}\n") uniquePorts;
385 in optionalString (!version24) directives
386 }
387
388 ${let
389 makeVirtualHost = vhost: ''
390 <VirtualHost *:${toString (getPort vhost)}>
391 ${perServerConf false vhost}
392 </VirtualHost>
393 '';
394 in concatMapStrings makeVirtualHost mainCfg.virtualHosts
395 }
396 '';
397
398
399 enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices;
400
401
402 # Generate the PHP configuration file. Should probably be factored
403 # out into a separate module.
404 phpIni = pkgs.runCommand "php.ini"
405 { options = concatStringsSep "\n"
406 ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices));
407 }
408 ''
409 cat ${php}/etc/php-recommended.ini > $out
410 echo "$options" >> $out
411 '';
412
413in
414
415
416{
417
418 ###### interface
419
420 options = {
421
422 services.httpd = {
423
424 enable = mkOption {
425 type = types.bool;
426 default = false;
427 description = "Whether to enable the Apache HTTP Server.";
428 };
429
430 package = mkOption {
431 type = types.package;
432 default = pkgs.apacheHttpd;
433 defaultText = "pkgs.apacheHttpd";
434 description = ''
435 Overridable attribute of the Apache HTTP Server package to use.
436 '';
437 };
438
439 configFile = mkOption {
440 type = types.path;
441 default = confFile;
442 defaultText = "confFile";
443 example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."'';
444 description = ''
445 Override the configuration file used by Apache. By default,
446 NixOS generates one automatically.
447 '';
448 };
449
450 extraConfig = mkOption {
451 type = types.lines;
452 default = "";
453 description = ''
454 Cnfiguration lines appended to the generated Apache
455 configuration file. Note that this mechanism may not work
456 when <option>configFile</option> is overridden.
457 '';
458 };
459
460 extraModules = mkOption {
461 type = types.listOf types.unspecified;
462 default = [];
463 example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]'';
464 description = ''
465 Additional Apache modules to be used. These can be
466 specified as a string in the case of modules distributed
467 with Apache, or as an attribute set specifying the
468 <varname>name</varname> and <varname>path</varname> of the
469 module.
470 '';
471 };
472
473 logPerVirtualHost = mkOption {
474 type = types.bool;
475 default = false;
476 description = ''
477 If enabled, each virtual host gets its own
478 <filename>access_log</filename> and
479 <filename>error_log</filename>, namely suffixed by the
480 <option>hostName</option> of the virtual host.
481 '';
482 };
483
484 user = mkOption {
485 type = types.str;
486 default = "wwwrun";
487 description = ''
488 User account under which httpd runs. The account is created
489 automatically if it doesn't exist.
490 '';
491 };
492
493 group = mkOption {
494 type = types.str;
495 default = "wwwrun";
496 description = ''
497 Group under which httpd runs. The account is created
498 automatically if it doesn't exist.
499 '';
500 };
501
502 logDir = mkOption {
503 type = types.path;
504 default = "/var/log/httpd";
505 description = ''
506 Directory for Apache's log files. It is created automatically.
507 '';
508 };
509
510 stateDir = mkOption {
511 type = types.path;
512 default = "/run/httpd";
513 description = ''
514 Directory for Apache's transient runtime state (such as PID
515 files). It is created automatically. Note that the default,
516 <filename>/run/httpd</filename>, is deleted at boot time.
517 '';
518 };
519
520 virtualHosts = mkOption {
521 type = types.listOf (types.submodule (
522 { options = import ./per-server-options.nix {
523 inherit lib;
524 forMainServer = false;
525 };
526 }));
527 default = [];
528 example = [
529 { hostName = "foo";
530 documentRoot = "/data/webroot-foo";
531 }
532 { hostName = "bar";
533 documentRoot = "/data/webroot-bar";
534 }
535 ];
536 description = ''
537 Specification of the virtual hosts served by Apache. Each
538 element should be an attribute set specifying the
539 configuration of the virtual host. The available options
540 are the non-global options permissible for the main host.
541 '';
542 };
543
544 enablePHP = mkOption {
545 type = types.bool;
546 default = false;
547 description = "Whether to enable the PHP module.";
548 };
549
550 phpOptions = mkOption {
551 type = types.lines;
552 default = "";
553 example =
554 ''
555 date.timezone = "CET"
556 '';
557 description =
558 "Options appended to the PHP configuration file <filename>php.ini</filename>.";
559 };
560
561 multiProcessingModule = mkOption {
562 type = types.str;
563 default = "prefork";
564 example = "worker";
565 description =
566 ''
567 Multi-processing module to be used by Apache. Available
568 modules are <literal>prefork</literal> (the default;
569 handles each request in a separate child process),
570 <literal>worker</literal> (hybrid approach that starts a
571 number of child processes each running a number of
572 threads) and <literal>event</literal> (a recent variant of
573 <literal>worker</literal> that handles persistent
574 connections more efficiently).
575 '';
576 };
577
578 maxClients = mkOption {
579 type = types.int;
580 default = 150;
581 example = 8;
582 description = "Maximum number of httpd processes (prefork)";
583 };
584
585 maxRequestsPerChild = mkOption {
586 type = types.int;
587 default = 0;
588 example = 500;
589 description =
590 "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited";
591 };
592 }
593
594 # Include the options shared between the main server and virtual hosts.
595 // (import ./per-server-options.nix {
596 inherit lib;
597 forMainServer = true;
598 });
599
600 };
601
602
603 ###### implementation
604
605 config = mkIf config.services.httpd.enable {
606
607 assertions = [ { assertion = mainCfg.enableSSL == true
608 -> mainCfg.sslServerCert != null
609 && mainCfg.sslServerKey != null;
610 message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; }
611 ];
612
613 users.extraUsers = optionalAttrs (mainCfg.user == "wwwrun") (singleton
614 { name = "wwwrun";
615 group = mainCfg.group;
616 description = "Apache httpd user";
617 uid = config.ids.uids.wwwrun;
618 });
619
620 users.extraGroups = optionalAttrs (mainCfg.group == "wwwrun") (singleton
621 { name = "wwwrun";
622 gid = config.ids.gids.wwwrun;
623 });
624
625 environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices;
626
627 services.httpd.phpOptions =
628 ''
629 ; Needed for PHP's mail() function.
630 sendmail_path = sendmail -t -i
631
632 ; Apparently PHP doesn't use $TZ.
633 date.timezone = "${config.time.timeZone}"
634 '';
635
636 systemd.services.httpd =
637 { description = "Apache HTTPD";
638
639 wantedBy = [ "multi-user.target" ];
640 wants = [ "keys.target" ];
641 after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ];
642
643 path =
644 [ httpd pkgs.coreutils pkgs.gnugrep ]
645 ++ # Needed for PHP's mail() function. !!! Probably the
646 # ssmtp module should export the path to sendmail in
647 # some way.
648 optional config.networking.defaultMailServer.directDelivery pkgs.ssmtp
649 ++ concatMap (svc: svc.extraServerPath) allSubservices;
650
651 environment =
652 optionalAttrs enablePHP { PHPRC = phpIni; }
653 // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices));
654
655 preStart =
656 ''
657 mkdir -m 0750 -p ${mainCfg.stateDir}
658 [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir}
659 ${optionalString version24 ''
660 mkdir -m 0750 -p "${mainCfg.stateDir}/runtime"
661 [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime"
662 ''}
663 mkdir -m 0700 -p ${mainCfg.logDir}
664
665 ${optionalString (mainCfg.documentRoot != null)
666 ''
667 # Create the document root directory if does not exists yet
668 mkdir -p ${mainCfg.documentRoot}
669 ''
670 }
671
672 # Get rid of old semaphores. These tend to accumulate across
673 # server restarts, eventually preventing it from restarting
674 # successfully.
675 for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do
676 ${pkgs.utillinux}/bin/ipcrm -s $i
677 done
678
679 # Run the startup hooks for the subservices.
680 for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do
681 echo Running Apache startup hook $i...
682 $i
683 done
684 '';
685
686 serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}";
687 serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop";
688 serviceConfig.Type = "forking";
689 serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid";
690 serviceConfig.Restart = "always";
691 serviceConfig.RestartSec = "5s";
692 };
693
694 };
695
696}