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