at v192 21 kB view raw
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}