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