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}