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