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