From f8bde3d6d31da84b5e81bdfc4f96efdf6bec3df2 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Thu, 10 Jan 2019 20:56:44 +0100 Subject: [PATCH] Add http configuration to modules and separate production from integration --- virtual/eldiron.nix | 57 +- virtual/modules/websites.nix | 81 +- .../modules/websites/apache/httpd_inte.nix | 722 ++++++++++++++++++ .../modules/websites/apache/httpd_prod.nix | 722 ++++++++++++++++++ .../websites/apache/per-server-options.nix | 188 +++++ virtual/modules/websites/aten.nix | 15 +- virtual/modules/websites/chloe.nix | 16 +- virtual/modules/websites/connexionswing.nix | 16 +- virtual/modules/websites/ludivine.nix | 15 +- virtual/modules/websites/piedsjaloux.nix | 16 +- 10 files changed, 1792 insertions(+), 56 deletions(-) create mode 100644 virtual/modules/websites/apache/httpd_inte.nix create mode 100644 virtual/modules/websites/apache/httpd_prod.nix create mode 100644 virtual/modules/websites/apache/per-server-options.nix diff --git a/virtual/eldiron.nix b/virtual/eldiron.nix index efaa068..a1e6909 100644 --- a/virtual/eldiron.nix +++ b/virtual/eldiron.nix @@ -4,7 +4,7 @@ enableRollback = true; }; - eldiron = { config, pkgs, mylibs, ... }: + eldiron = { config, pkgs, mylibs, myconfig, ... }: with mylibs; let mypkgs = pkgs.callPackage ./packages.nix { @@ -14,6 +14,13 @@ { _module.args = { mylibs = import ../libs.nix; + myconfig = { + ips = { + main = "176.9.151.89"; + production = "176.9.151.154"; + integration = "176.9.151.155"; + }; + }; }; imports = [ @@ -47,6 +54,11 @@ enable = true; allowedTCPPorts = [ 22 80 443 9418 ]; }; + interfaces."eth0".ipv4.addresses = [ + # 176.9.151.89 declared in nixops -> infra / tools + { address = myconfig.ips.production; prefixLength = 32; } + { address = myconfig.ips.integration; prefixLength = 32; } + ]; }; deployment = { @@ -54,7 +66,7 @@ hetzner = { #robotUser = "defined in HETZNER_ROBOT_USER"; #robotPass = "defined in HETZNER_ROBOT_PASS"; - mainIPv4 = "176.9.151.89"; + mainIPv4 = myconfig.ips.main; partitions = '' clearpart --all --initlabel --drives=sda,sdb @@ -138,7 +150,6 @@ install -d -m 0750 -o wwwrun -g wwwrun /var/lib/php/sessions install -d -m 0750 -o wwwrun -g wwwrun /var/lib/php/sessions/adminer install -d -m 0750 -o wwwrun -g wwwrun /var/lib/php/sessions/mantisbt - install -d -m 0750 -o wwwrun -g wwwrun /var/lib/php/sessions/ttrss install -d -m 0750 -o wwwrun -g wwwrun /var/lib/php/sessions/davical ''; # FIXME: initial sync @@ -187,7 +198,9 @@ sslServerKey = "/var/lib/acme/${domain}/key.pem"; sslServerChain = "/var/lib/acme/${domain}/fullchain.pem"; logFormat = "combinedVhost"; - listen = [ { ip = "*"; port = 443; } ]; + listen = [ + { ip = "176.9.151.89"; port = 443; } + ]; }; apacheConfig = config.services.myWebsites.apacheConfig; in rec { @@ -240,14 +253,6 @@ mypkgs.davical.apache.vhostConf ]; }) - (withConf "eldiron" // { - hostName = "connexionswing.immae.eu"; - serverAliases = [ "sandetludo.immae.eu" ]; - documentRoot = mypkgs.connexionswing_dev.webRoot; - extraConfig = builtins.concatStringsSep "\n" [ - mypkgs.connexionswing_dev.apache.vhostConf - ]; - }) (withConf "connexionswing" // { hostName = "connexionswing.com"; serverAliases = [ "sandetludo.com" "www.connexionswing.com" "www.sandetludo.com" ]; @@ -256,13 +261,6 @@ mypkgs.connexionswing_prod.apache.vhostConf ]; }) - (withConf "eldiron" // { - hostName = "ludivine.immae.eu"; - documentRoot = mypkgs.ludivinecassal_dev.webRoot; - extraConfig = builtins.concatStringsSep "\n" [ - mypkgs.ludivinecassal_dev.apache.vhostConf - ]; - }) (withConf "ludivinecassal" // { hostName = "ludivinecassal.com"; serverAliases = [ "www.ludivinecassal.com" ]; @@ -271,13 +269,6 @@ mypkgs.ludivinecassal_prod.apache.vhostConf ]; }) - (withConf "eldiron" // { - hostName = "piedsjaloux.immae.eu"; - documentRoot = mypkgs.piedsjaloux_dev.webRoot; - extraConfig = builtins.concatStringsSep "\n" [ - mypkgs.piedsjaloux_dev.apache.vhostConf - ]; - }) (withConf "piedsjaloux" // { hostName = "piedsjaloux.fr"; serverAliases = [ "www.piedsjaloux.fr" ]; @@ -286,13 +277,6 @@ mypkgs.piedsjaloux_prod.apache.vhostConf ]; }) - (withConf "eldiron" // { - hostName = "chloe.immae.eu"; - documentRoot = mypkgs.chloe_dev.webRoot; - extraConfig = builtins.concatStringsSep "\n" [ - mypkgs.chloe_dev.apache.vhostConf - ]; - }) (withConf "chloe" // { hostName = "osteopathe-cc.fr"; serverAliases = [ "www.osteopathe-cc.fr" ]; @@ -301,13 +285,6 @@ mypkgs.chloe_prod.apache.vhostConf ]; }) - (withConf "eldiron" // { - hostName = "dev.aten.pro"; - documentRoot = mypkgs.aten_dev.webRoot; - extraConfig = builtins.concatStringsSep "\n" [ - mypkgs.aten_dev.apache.vhostConf - ]; - }) (withConf "aten" // { hostName = "aten.pro"; serverAliases = [ "www.aten.pro" ]; diff --git a/virtual/modules/websites.nix b/virtual/modules/websites.nix index 62f45d9..cbd7de0 100644 --- a/virtual/modules/websites.nix +++ b/virtual/modules/websites.nix @@ -1,6 +1,61 @@ -{ lib, pkgs, config, mylibs, ... }: +{ lib, pkgs, config, mylibs, myconfig, ... }: let cfg = config.services.myWebsites; + makeService = name: cfg: let + toVhost = vhostConf: { + enableSSL = true; + sslServerCert = "/var/lib/acme/${vhostConf.certName}/cert.pem"; + sslServerKey = "/var/lib/acme/${vhostConf.certName}/key.pem"; + sslServerChain = "/var/lib/acme/${vhostConf.certName}/fullchain.pem"; + logFormat = "combinedVhost"; + listen = [ + { ip = cfg.ip; port = 443; } + ]; + hostName = builtins.head vhostConf.hosts; + serverAliases = builtins.tail vhostConf.hosts or []; + documentRoot = vhostConf.root; + extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig; + }; + in rec { + enable = true; + listen = [ + { ip = cfg.ip; port = 443; } + ]; + stateDir = "/run/httpd_${name}"; + logPerVirtualHost = true; + multiProcessingModule = "worker"; + adminAddr = "httpd@immae.eu"; + logFormat = "combinedVhost"; + extraModules = pkgs.lib.lists.unique (pkgs.lib.lists.flatten cfg.modules); + extraConfig = builtins.concatStringsSep "\n" cfg.extraConfig; + virtualHosts = pkgs.lib.attrsets.mapAttrsToList (n: v: toVhost v) cfg.vhostConfs; + }; + makeServiceOptions = name: ip: { + enable = lib.mkEnableOption "enable websites in ${name}"; + ip = lib.mkOption { + type = lib.types.string; + default = ip; + description = "${name} ip to listen to"; + }; + modules = lib.mkOption { + type = lib.types.listOf (lib.types.str); + default = []; + }; + extraConfig = lib.mkOption { + type = lib.types.listOf (lib.types.lines); + default = []; + }; + vhostConfs = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options = { + certName = lib.mkOption { type = lib.types.string; }; + hosts = lib.mkOption { type = lib.types.listOf lib.types.string; }; + root = lib.mkOption { type = lib.types.nullOr lib.types.path; }; + extraConfig = lib.mkOption { type = lib.types.listOf lib.types.lines; default = []; }; + }; + }); + }; + }; in { imports = [ @@ -9,16 +64,16 @@ in ./websites/aten.nix ./websites/piedsjaloux.nix ./websites/connexionswing.nix + # built using: + # sed -e "s/services\.httpd/services\.httpdProd/g" .nix-defexpr/channels/nixpkgs/nixos/modules/services/web-servers/apache-httpd/default.nix + # And removed users / groups + ./websites/apache/httpd_prod.nix + ./websites/apache/httpd_inte.nix ]; options.services.myWebsites = { - production = { - enable = lib.mkEnableOption "enable websites in production"; - }; - - integration = { - enable = lib.mkEnableOption "enable websites in integration"; - }; + production = makeServiceOptions "production" myconfig.ips.production; + integration = makeServiceOptions "integration" myconfig.ips.integration; apacheConfig = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule { @@ -111,5 +166,15 @@ in ''; }; }; + + # FIXME: logrotate + # FIXME: ipv6 + services.httpdProd = makeService "production" config.services.myWebsites.production; + services.myWebsites.production.modules = pkgs.lib.lists.flatten (pkgs.lib.attrsets.mapAttrsToList (n: v: v.modules or []) cfg.apacheConfig); + services.myWebsites.production.extraConfig = (builtins.filter (x: x != null) (pkgs.lib.attrsets.mapAttrsToList (n: v: v.extraConfig or null) cfg.apacheConfig)); + + services.httpdInte = makeService "integration" config.services.myWebsites.integration; + services.myWebsites.integration.modules = pkgs.lib.lists.flatten (pkgs.lib.attrsets.mapAttrsToList (n: v: v.modules or []) cfg.apacheConfig); + services.myWebsites.integration.extraConfig = (builtins.filter (x: x != null) (pkgs.lib.attrsets.mapAttrsToList (n: v: v.extraConfig or null) cfg.apacheConfig)); }; } diff --git a/virtual/modules/websites/apache/httpd_inte.nix b/virtual/modules/websites/apache/httpd_inte.nix new file mode 100644 index 0000000..83d8ab8 --- /dev/null +++ b/virtual/modules/websites/apache/httpd_inte.nix @@ -0,0 +1,722 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + mainCfg = config.services.httpdInte; + + httpd = mainCfg.package.out; + + version24 = !versionOlder httpd.version "2.4"; + + httpdConf = mainCfg.configFile; + + php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ }; + + phpMajorVersion = head (splitString "." php.version); + + mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; }; + + defaultListen = cfg: if cfg.enableSSL + then [{ip = "*"; port = 443;}] + else [{ip = "*"; port = 80;}]; + + getListen = cfg: + let list = (lib.optional (cfg.port != 0) {ip = "*"; port = cfg.port;}) ++ cfg.listen; + in if list == [] + then defaultListen cfg + else list; + + listenToString = l: "${l.ip}:${toString l.port}"; + + extraModules = attrByPath ["extraModules"] [] mainCfg; + extraForeignModules = filter isAttrs extraModules; + extraApacheModules = filter isString extraModules; + + + makeServerInfo = cfg: { + # Canonical name must not include a trailing slash. + canonicalNames = + let defaultPort = (head (defaultListen cfg)).port; in + map (port: + (if cfg.enableSSL then "https" else "http") + "://" + + cfg.hostName + + (if port != defaultPort then ":${toString port}" else "") + ) (map (x: x.port) (getListen cfg)); + + # Admin address: inherit from the main server if not specified for + # a virtual host. + adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr; + + vhostConfig = cfg; + serverConfig = mainCfg; + fullConfig = config; # machine config + }; + + + allHosts = [mainCfg] ++ mainCfg.virtualHosts; + + + callSubservices = serverInfo: defs: + let f = svc: + let + svcFunction = + if svc ? function then svc.function + # instead of using serviceType="mediawiki"; you can copy mediawiki.nix to any location outside nixpkgs, modify it at will, and use serviceExpression=./mediawiki.nix; + else if svc ? serviceExpression then import (toString svc.serviceExpression) + else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix"); + config = (evalModules + { modules = [ { options = res.options; config = svc.config or svc; } ]; + check = false; + }).config; + defaults = { + extraConfig = ""; + extraModules = []; + extraModulesPre = []; + extraPath = []; + extraServerPath = []; + globalEnvVars = []; + robotsEntries = ""; + startupScript = ""; + enablePHP = false; + enablePerl = false; + phpOptions = ""; + options = {}; + documentRoot = null; + }; + res = defaults // svcFunction { inherit config lib pkgs serverInfo php; }; + in res; + in map f defs; + + + # !!! callSubservices is expensive + subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices; + + mainSubservices = subservicesFor mainCfg; + + allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts; + + + enableSSL = any (vhost: vhost.enableSSL) allHosts; + + + # Names of modules from ${httpd}/modules that we want to load. + apacheModules = + [ # HTTP authentication mechanisms: basic and digest. + "auth_basic" "auth_digest" + + # Authentication: is the user who he claims to be? + "authn_file" "authn_dbm" "authn_anon" + (if version24 then "authn_core" else "authn_alias") + + # Authorization: is the user allowed access? + "authz_user" "authz_groupfile" "authz_host" + + # Other modules. + "ext_filter" "include" "log_config" "env" "mime_magic" + "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif" + "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs" + "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling" + "userdir" "alias" "rewrite" "proxy" "proxy_http" + ] + ++ optionals version24 [ + "mpm_${mainCfg.multiProcessingModule}" + "authz_core" + "unixd" + "cache" "cache_disk" + "slotmem_shm" + "socache_shmcb" + # For compatibility with old configurations, the new module mod_access_compat is provided. + "access_compat" + ] + ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) + ++ optional enableSSL "ssl" + ++ extraApacheModules; + + + allDenied = if version24 then '' + Require all denied + '' else '' + Order deny,allow + Deny from all + ''; + + allGranted = if version24 then '' + Require all granted + '' else '' + Order allow,deny + Allow from all + ''; + + + loggingConf = (if mainCfg.logFormat != "none" then '' + ErrorLog ${mainCfg.logDir}/error_log + + LogLevel notice + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + + CustomLog ${mainCfg.logDir}/access_log ${mainCfg.logFormat} + '' else '' + ErrorLog /dev/null + ''); + + + browserHacks = '' + BrowserMatch "Mozilla/2" nokeepalive + BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 + BrowserMatch "RealPlayer 4\.0" force-response-1.0 + BrowserMatch "Java/1\.0" force-response-1.0 + BrowserMatch "JDK/1\.0" force-response-1.0 + BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully + BrowserMatch "^WebDrive" redirect-carefully + BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully + BrowserMatch "^gnome-vfs" redirect-carefully + ''; + + + sslConf = '' + SSLSessionCache ${if version24 then "shmcb" else "shm"}:${mainCfg.stateDir}/ssl_scache(512000) + + ${if version24 then "Mutex" else "SSLMutex"} posixsem + + SSLRandomSeed startup builtin + SSLRandomSeed connect builtin + + SSLProtocol All -SSLv2 -SSLv3 + SSLCipherSuite HIGH:!aNULL:!MD5:!EXP + SSLHonorCipherOrder on + ''; + + + mimeConf = '' + TypesConfig ${httpd}/conf/mime.types + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + AddType application/x-httpd-php .php .phtml + + + MIMEMagicFile ${httpd}/conf/magic + + ''; + + + perServerConf = isMainServer: cfg: let + + serverInfo = makeServerInfo cfg; + + subservices = callSubservices serverInfo cfg.extraSubservices; + + maybeDocumentRoot = fold (svc: acc: + if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc + ) null ([ cfg ] ++ subservices); + + documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else + pkgs.runCommand "empty" {} "mkdir -p $out"; + + documentRootConf = '' + DocumentRoot "${documentRoot}" + + + Options Indexes FollowSymLinks + AllowOverride None + ${allGranted} + + ''; + + robotsTxt = + concatStringsSep "\n" (filter (x: x != "") ( + # If this is a vhost, the include the entries for the main server as well. + (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices) + ++ [cfg.robotsEntries] + ++ (map (svc: svc.robotsEntries) subservices))); + + in '' + ${concatStringsSep "\n" (map (n: "ServerName ${n}") serverInfo.canonicalNames)} + + ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases} + + ${if cfg.sslServerCert != null then '' + SSLCertificateFile ${cfg.sslServerCert} + SSLCertificateKeyFile ${cfg.sslServerKey} + ${if cfg.sslServerChain != null then '' + SSLCertificateChainFile ${cfg.sslServerChain} + '' else ""} + '' else ""} + + ${if cfg.enableSSL then '' + SSLEngine on + '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */ + '' + SSLEngine off + '' else ""} + + ${if isMainServer || cfg.adminAddr != null then '' + ServerAdmin ${cfg.adminAddr} + '' else ""} + + ${if !isMainServer && mainCfg.logPerVirtualHost then '' + ErrorLog ${mainCfg.logDir}/error_log-${cfg.hostName} + CustomLog ${mainCfg.logDir}/access_log-${cfg.hostName} ${cfg.logFormat} + '' else ""} + + ${optionalString (robotsTxt != "") '' + Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt} + ''} + + ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""} + + ${if cfg.enableUserDir then '' + + UserDir public_html + UserDir disabled root + + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + ${allGranted} + + + ${allDenied} + + + + '' else ""} + + ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then '' + RedirectPermanent / ${cfg.globalRedirect} + '' else ""} + + ${ + let makeFileConf = elem: '' + Alias ${elem.urlPath} ${elem.file} + ''; + in concatMapStrings makeFileConf cfg.servedFiles + } + + ${ + let makeDirConf = elem: '' + Alias ${elem.urlPath} ${elem.dir}/ + + Options +Indexes + ${allGranted} + AllowOverride All + + ''; + in concatMapStrings makeDirConf cfg.servedDirs + } + + ${concatMapStrings (svc: svc.extraConfig) subservices} + + ${cfg.extraConfig} + ''; + + + confFile = pkgs.writeText "httpd.conf" '' + + ServerRoot ${httpd} + + ${optionalString version24 '' + DefaultRuntimeDir ${mainCfg.stateDir}/runtime + ''} + + PidFile ${mainCfg.stateDir}/httpd.pid + + ${optionalString (mainCfg.multiProcessingModule != "prefork") '' + # mod_cgid requires this. + ScriptSock ${mainCfg.stateDir}/cgisock + ''} + + + MaxClients ${toString mainCfg.maxClients} + MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild} + + + ${let + listen = concatMap getListen allHosts; + toStr = listen: "Listen ${listenToString listen}\n"; + uniqueListen = uniqList {inputList = map toStr listen;}; + in concatStrings uniqueListen + } + + User ${mainCfg.user} + Group ${mainCfg.group} + + ${let + load = {name, path}: "LoadModule ${name}_module ${path}\n"; + allModules = + concatMap (svc: svc.extraModulesPre) allSubservices + ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules + ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } + ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } + ++ optional enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } + ++ concatMap (svc: svc.extraModules) allSubservices + ++ extraForeignModules; + in concatMapStrings load allModules + } + + AddHandler type-map var + + + ${allDenied} + + + ${mimeConf} + ${loggingConf} + ${browserHacks} + + Include ${httpd}/conf/extra/httpd-default.conf + Include ${httpd}/conf/extra/httpd-autoindex.conf + Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf + Include ${httpd}/conf/extra/httpd-languages.conf + + ${if enableSSL then sslConf else ""} + + # Fascist default - deny access to everything. + + Options FollowSymLinks + AllowOverride None + ${allDenied} + + + # But do allow access to files in the store so that we don't have + # to generate clauses for every generated file that we + # want to serve. + + ${allGranted} + + + # Generate directives for the main server. + ${perServerConf true mainCfg} + + # Always enable virtual hosts; it doesn't seem to hurt. + ${let + listen = concatMap getListen allHosts; + uniqueListen = uniqList {inputList = listen;}; + directives = concatMapStrings (listen: "NameVirtualHost ${listenToString listen}\n") uniqueListen; + in optionalString (!version24) directives + } + + ${let + makeVirtualHost = vhost: '' + + ${perServerConf false vhost} + + ''; + in concatMapStrings makeVirtualHost mainCfg.virtualHosts + } + ''; + + + enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices; + + enablePerl = mainCfg.enablePerl || any (svc: svc.enablePerl) allSubservices; + + + # Generate the PHP configuration file. Should probably be factored + # out into a separate module. + phpIni = pkgs.runCommand "php.ini" + { options = concatStringsSep "\n" + ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices)); + } + '' + cat ${php}/etc/php.ini > $out + echo "$options" >> $out + ''; + +in + + +{ + + ###### interface + + options = { + + services.httpdInte = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Apache HTTP Server."; + }; + + package = mkOption { + type = types.package; + default = pkgs.apacheHttpd; + defaultText = "pkgs.apacheHttpd"; + description = '' + Overridable attribute of the Apache HTTP Server package to use. + ''; + }; + + configFile = mkOption { + type = types.path; + default = confFile; + defaultText = "confFile"; + example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."''; + description = '' + Override the configuration file used by Apache. By default, + NixOS generates one automatically. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Cnfiguration lines appended to the generated Apache + configuration file. Note that this mechanism may not work + when is overridden. + ''; + }; + + extraModules = mkOption { + type = types.listOf types.unspecified; + default = []; + example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]''; + description = '' + Additional Apache modules to be used. These can be + specified as a string in the case of modules distributed + with Apache, or as an attribute set specifying the + name and path of the + module. + ''; + }; + + logPerVirtualHost = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, each virtual host gets its own + access_log and + error_log, namely suffixed by the + of the virtual host. + ''; + }; + + user = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + User account under which httpd runs. The account is created + automatically if it doesn't exist. + ''; + }; + + group = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + Group under which httpd runs. The account is created + automatically if it doesn't exist. + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/httpd"; + description = '' + Directory for Apache's log files. It is created automatically. + ''; + }; + + stateDir = mkOption { + type = types.path; + default = "/run/httpd"; + description = '' + Directory for Apache's transient runtime state (such as PID + files). It is created automatically. Note that the default, + /run/httpd, is deleted at boot time. + ''; + }; + + virtualHosts = mkOption { + type = types.listOf (types.submodule ( + { options = import ./per-server-options.nix { + inherit lib; + forMainServer = false; + }; + })); + default = []; + example = [ + { hostName = "foo"; + documentRoot = "/data/webroot-foo"; + } + { hostName = "bar"; + documentRoot = "/data/webroot-bar"; + } + ]; + description = '' + Specification of the virtual hosts served by Apache. Each + element should be an attribute set specifying the + configuration of the virtual host. The available options + are the non-global options permissible for the main host. + ''; + }; + + enableMellon = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the mod_auth_mellon module."; + }; + + enablePHP = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the PHP module."; + }; + + phpPackage = mkOption { + type = types.package; + default = pkgs.php; + defaultText = "pkgs.php"; + description = '' + Overridable attribute of the PHP package to use. + ''; + }; + + enablePerl = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Perl module (mod_perl)."; + }; + + phpOptions = mkOption { + type = types.lines; + default = ""; + example = + '' + date.timezone = "CET" + ''; + description = + "Options appended to the PHP configuration file php.ini."; + }; + + multiProcessingModule = mkOption { + type = types.str; + default = "prefork"; + example = "worker"; + description = + '' + Multi-processing module to be used by Apache. Available + modules are prefork (the default; + handles each request in a separate child process), + worker (hybrid approach that starts a + number of child processes each running a number of + threads) and event (a recent variant of + worker that handles persistent + connections more efficiently). + ''; + }; + + maxClients = mkOption { + type = types.int; + default = 150; + example = 8; + description = "Maximum number of httpd processes (prefork)"; + }; + + maxRequestsPerChild = mkOption { + type = types.int; + default = 0; + example = 500; + description = + "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited"; + }; + } + + # Include the options shared between the main server and virtual hosts. + // (import ./per-server-options.nix { + inherit lib; + forMainServer = true; + }); + + }; + + + ###### implementation + + config = mkIf config.services.httpdInte.enable { + + assertions = [ { assertion = mainCfg.enableSSL == true + -> mainCfg.sslServerCert != null + && mainCfg.sslServerKey != null; + message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; } + ]; + + 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); + + environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices; + + services.httpdInte.phpOptions = + '' + ; Needed for PHP's mail() function. + sendmail_path = sendmail -t -i + '' + optionalString (!isNull config.time.timeZone) '' + + ; Apparently PHP doesn't use $TZ. + date.timezone = "${config.time.timeZone}" + ''; + + systemd.services.httpdInte = + { description = "Apache HTTPD"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "keys.target" ]; + after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ]; + + path = + [ httpd pkgs.coreutils pkgs.gnugrep ] + ++ # Needed for PHP's mail() function. !!! Probably the + # ssmtp module should export the path to sendmail in + # some way. + optional config.networking.defaultMailServer.directDelivery pkgs.ssmtp + ++ concatMap (svc: svc.extraServerPath) allSubservices; + + environment = + optionalAttrs enablePHP { PHPRC = phpIni; } + // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; } + // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices)); + + preStart = + '' + mkdir -m 0750 -p ${mainCfg.stateDir} + [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir} + ${optionalString version24 '' + mkdir -m 0750 -p "${mainCfg.stateDir}/runtime" + [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime" + ''} + mkdir -m 0700 -p ${mainCfg.logDir} + + # Get rid of old semaphores. These tend to accumulate across + # server restarts, eventually preventing it from restarting + # successfully. + for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do + ${pkgs.utillinux}/bin/ipcrm -s $i + done + + # Run the startup hooks for the subservices. + for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do + echo Running Apache startup hook $i... + $i + done + ''; + + serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}"; + serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop"; + serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful"; + serviceConfig.Type = "forking"; + serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid"; + serviceConfig.Restart = "always"; + serviceConfig.RestartSec = "5s"; + }; + + }; +} diff --git a/virtual/modules/websites/apache/httpd_prod.nix b/virtual/modules/websites/apache/httpd_prod.nix new file mode 100644 index 0000000..576a305 --- /dev/null +++ b/virtual/modules/websites/apache/httpd_prod.nix @@ -0,0 +1,722 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + mainCfg = config.services.httpdProd; + + httpd = mainCfg.package.out; + + version24 = !versionOlder httpd.version "2.4"; + + httpdConf = mainCfg.configFile; + + php = mainCfg.phpPackage.override { apacheHttpd = httpd.dev; /* otherwise it only gets .out */ }; + + phpMajorVersion = head (splitString "." php.version); + + mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = httpd; }; + + defaultListen = cfg: if cfg.enableSSL + then [{ip = "*"; port = 443;}] + else [{ip = "*"; port = 80;}]; + + getListen = cfg: + let list = (lib.optional (cfg.port != 0) {ip = "*"; port = cfg.port;}) ++ cfg.listen; + in if list == [] + then defaultListen cfg + else list; + + listenToString = l: "${l.ip}:${toString l.port}"; + + extraModules = attrByPath ["extraModules"] [] mainCfg; + extraForeignModules = filter isAttrs extraModules; + extraApacheModules = filter isString extraModules; + + + makeServerInfo = cfg: { + # Canonical name must not include a trailing slash. + canonicalNames = + let defaultPort = (head (defaultListen cfg)).port; in + map (port: + (if cfg.enableSSL then "https" else "http") + "://" + + cfg.hostName + + (if port != defaultPort then ":${toString port}" else "") + ) (map (x: x.port) (getListen cfg)); + + # Admin address: inherit from the main server if not specified for + # a virtual host. + adminAddr = if cfg.adminAddr != null then cfg.adminAddr else mainCfg.adminAddr; + + vhostConfig = cfg; + serverConfig = mainCfg; + fullConfig = config; # machine config + }; + + + allHosts = [mainCfg] ++ mainCfg.virtualHosts; + + + callSubservices = serverInfo: defs: + let f = svc: + let + svcFunction = + if svc ? function then svc.function + # instead of using serviceType="mediawiki"; you can copy mediawiki.nix to any location outside nixpkgs, modify it at will, and use serviceExpression=./mediawiki.nix; + else if svc ? serviceExpression then import (toString svc.serviceExpression) + else import (toString "${toString ./.}/${if svc ? serviceType then svc.serviceType else svc.serviceName}.nix"); + config = (evalModules + { modules = [ { options = res.options; config = svc.config or svc; } ]; + check = false; + }).config; + defaults = { + extraConfig = ""; + extraModules = []; + extraModulesPre = []; + extraPath = []; + extraServerPath = []; + globalEnvVars = []; + robotsEntries = ""; + startupScript = ""; + enablePHP = false; + enablePerl = false; + phpOptions = ""; + options = {}; + documentRoot = null; + }; + res = defaults // svcFunction { inherit config lib pkgs serverInfo php; }; + in res; + in map f defs; + + + # !!! callSubservices is expensive + subservicesFor = cfg: callSubservices (makeServerInfo cfg) cfg.extraSubservices; + + mainSubservices = subservicesFor mainCfg; + + allSubservices = mainSubservices ++ concatMap subservicesFor mainCfg.virtualHosts; + + + enableSSL = any (vhost: vhost.enableSSL) allHosts; + + + # Names of modules from ${httpd}/modules that we want to load. + apacheModules = + [ # HTTP authentication mechanisms: basic and digest. + "auth_basic" "auth_digest" + + # Authentication: is the user who he claims to be? + "authn_file" "authn_dbm" "authn_anon" + (if version24 then "authn_core" else "authn_alias") + + # Authorization: is the user allowed access? + "authz_user" "authz_groupfile" "authz_host" + + # Other modules. + "ext_filter" "include" "log_config" "env" "mime_magic" + "cern_meta" "expires" "headers" "usertrack" /* "unique_id" */ "setenvif" + "mime" "dav" "status" "autoindex" "asis" "info" "dav_fs" + "vhost_alias" "negotiation" "dir" "imagemap" "actions" "speling" + "userdir" "alias" "rewrite" "proxy" "proxy_http" + ] + ++ optionals version24 [ + "mpm_${mainCfg.multiProcessingModule}" + "authz_core" + "unixd" + "cache" "cache_disk" + "slotmem_shm" + "socache_shmcb" + # For compatibility with old configurations, the new module mod_access_compat is provided. + "access_compat" + ] + ++ (if mainCfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ]) + ++ optional enableSSL "ssl" + ++ extraApacheModules; + + + allDenied = if version24 then '' + Require all denied + '' else '' + Order deny,allow + Deny from all + ''; + + allGranted = if version24 then '' + Require all granted + '' else '' + Order allow,deny + Allow from all + ''; + + + loggingConf = (if mainCfg.logFormat != "none" then '' + ErrorLog ${mainCfg.logDir}/error_log + + LogLevel notice + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + LogFormat "%{Referer}i -> %U" referer + LogFormat "%{User-agent}i" agent + + CustomLog ${mainCfg.logDir}/access_log ${mainCfg.logFormat} + '' else '' + ErrorLog /dev/null + ''); + + + browserHacks = '' + BrowserMatch "Mozilla/2" nokeepalive + BrowserMatch "MSIE 4\.0b2;" nokeepalive downgrade-1.0 force-response-1.0 + BrowserMatch "RealPlayer 4\.0" force-response-1.0 + BrowserMatch "Java/1\.0" force-response-1.0 + BrowserMatch "JDK/1\.0" force-response-1.0 + BrowserMatch "Microsoft Data Access Internet Publishing Provider" redirect-carefully + BrowserMatch "^WebDrive" redirect-carefully + BrowserMatch "^WebDAVFS/1.[012]" redirect-carefully + BrowserMatch "^gnome-vfs" redirect-carefully + ''; + + + sslConf = '' + SSLSessionCache ${if version24 then "shmcb" else "shm"}:${mainCfg.stateDir}/ssl_scache(512000) + + ${if version24 then "Mutex" else "SSLMutex"} posixsem + + SSLRandomSeed startup builtin + SSLRandomSeed connect builtin + + SSLProtocol All -SSLv2 -SSLv3 + SSLCipherSuite HIGH:!aNULL:!MD5:!EXP + SSLHonorCipherOrder on + ''; + + + mimeConf = '' + TypesConfig ${httpd}/conf/mime.types + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + AddType application/x-httpd-php .php .phtml + + + MIMEMagicFile ${httpd}/conf/magic + + ''; + + + perServerConf = isMainServer: cfg: let + + serverInfo = makeServerInfo cfg; + + subservices = callSubservices serverInfo cfg.extraSubservices; + + maybeDocumentRoot = fold (svc: acc: + if acc == null then svc.documentRoot else assert svc.documentRoot == null; acc + ) null ([ cfg ] ++ subservices); + + documentRoot = if maybeDocumentRoot != null then maybeDocumentRoot else + pkgs.runCommand "empty" {} "mkdir -p $out"; + + documentRootConf = '' + DocumentRoot "${documentRoot}" + + + Options Indexes FollowSymLinks + AllowOverride None + ${allGranted} + + ''; + + robotsTxt = + concatStringsSep "\n" (filter (x: x != "") ( + # If this is a vhost, the include the entries for the main server as well. + (if isMainServer then [] else [mainCfg.robotsEntries] ++ map (svc: svc.robotsEntries) mainSubservices) + ++ [cfg.robotsEntries] + ++ (map (svc: svc.robotsEntries) subservices))); + + in '' + ${concatStringsSep "\n" (map (n: "ServerName ${n}") serverInfo.canonicalNames)} + + ${concatMapStrings (alias: "ServerAlias ${alias}\n") cfg.serverAliases} + + ${if cfg.sslServerCert != null then '' + SSLCertificateFile ${cfg.sslServerCert} + SSLCertificateKeyFile ${cfg.sslServerKey} + ${if cfg.sslServerChain != null then '' + SSLCertificateChainFile ${cfg.sslServerChain} + '' else ""} + '' else ""} + + ${if cfg.enableSSL then '' + SSLEngine on + '' else if enableSSL then /* i.e., SSL is enabled for some host, but not this one */ + '' + SSLEngine off + '' else ""} + + ${if isMainServer || cfg.adminAddr != null then '' + ServerAdmin ${cfg.adminAddr} + '' else ""} + + ${if !isMainServer && mainCfg.logPerVirtualHost then '' + ErrorLog ${mainCfg.logDir}/error_log-${cfg.hostName} + CustomLog ${mainCfg.logDir}/access_log-${cfg.hostName} ${cfg.logFormat} + '' else ""} + + ${optionalString (robotsTxt != "") '' + Alias /robots.txt ${pkgs.writeText "robots.txt" robotsTxt} + ''} + + ${if isMainServer || maybeDocumentRoot != null then documentRootConf else ""} + + ${if cfg.enableUserDir then '' + + UserDir public_html + UserDir disabled root + + + AllowOverride FileInfo AuthConfig Limit Indexes + Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec + + ${allGranted} + + + ${allDenied} + + + + '' else ""} + + ${if cfg.globalRedirect != null && cfg.globalRedirect != "" then '' + RedirectPermanent / ${cfg.globalRedirect} + '' else ""} + + ${ + let makeFileConf = elem: '' + Alias ${elem.urlPath} ${elem.file} + ''; + in concatMapStrings makeFileConf cfg.servedFiles + } + + ${ + let makeDirConf = elem: '' + Alias ${elem.urlPath} ${elem.dir}/ + + Options +Indexes + ${allGranted} + AllowOverride All + + ''; + in concatMapStrings makeDirConf cfg.servedDirs + } + + ${concatMapStrings (svc: svc.extraConfig) subservices} + + ${cfg.extraConfig} + ''; + + + confFile = pkgs.writeText "httpd.conf" '' + + ServerRoot ${httpd} + + ${optionalString version24 '' + DefaultRuntimeDir ${mainCfg.stateDir}/runtime + ''} + + PidFile ${mainCfg.stateDir}/httpd.pid + + ${optionalString (mainCfg.multiProcessingModule != "prefork") '' + # mod_cgid requires this. + ScriptSock ${mainCfg.stateDir}/cgisock + ''} + + + MaxClients ${toString mainCfg.maxClients} + MaxRequestsPerChild ${toString mainCfg.maxRequestsPerChild} + + + ${let + listen = concatMap getListen allHosts; + toStr = listen: "Listen ${listenToString listen}\n"; + uniqueListen = uniqList {inputList = map toStr listen;}; + in concatStrings uniqueListen + } + + User ${mainCfg.user} + Group ${mainCfg.group} + + ${let + load = {name, path}: "LoadModule ${name}_module ${path}\n"; + allModules = + concatMap (svc: svc.extraModulesPre) allSubservices + ++ map (name: {inherit name; path = "${httpd}/modules/mod_${name}.so";}) apacheModules + ++ optional mainCfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; } + ++ optional enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; } + ++ optional enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; } + ++ concatMap (svc: svc.extraModules) allSubservices + ++ extraForeignModules; + in concatMapStrings load allModules + } + + AddHandler type-map var + + + ${allDenied} + + + ${mimeConf} + ${loggingConf} + ${browserHacks} + + Include ${httpd}/conf/extra/httpd-default.conf + Include ${httpd}/conf/extra/httpd-autoindex.conf + Include ${httpd}/conf/extra/httpd-multilang-errordoc.conf + Include ${httpd}/conf/extra/httpd-languages.conf + + ${if enableSSL then sslConf else ""} + + # Fascist default - deny access to everything. + + Options FollowSymLinks + AllowOverride None + ${allDenied} + + + # But do allow access to files in the store so that we don't have + # to generate clauses for every generated file that we + # want to serve. + + ${allGranted} + + + # Generate directives for the main server. + ${perServerConf true mainCfg} + + # Always enable virtual hosts; it doesn't seem to hurt. + ${let + listen = concatMap getListen allHosts; + uniqueListen = uniqList {inputList = listen;}; + directives = concatMapStrings (listen: "NameVirtualHost ${listenToString listen}\n") uniqueListen; + in optionalString (!version24) directives + } + + ${let + makeVirtualHost = vhost: '' + + ${perServerConf false vhost} + + ''; + in concatMapStrings makeVirtualHost mainCfg.virtualHosts + } + ''; + + + enablePHP = mainCfg.enablePHP || any (svc: svc.enablePHP) allSubservices; + + enablePerl = mainCfg.enablePerl || any (svc: svc.enablePerl) allSubservices; + + + # Generate the PHP configuration file. Should probably be factored + # out into a separate module. + phpIni = pkgs.runCommand "php.ini" + { options = concatStringsSep "\n" + ([ mainCfg.phpOptions ] ++ (map (svc: svc.phpOptions) allSubservices)); + } + '' + cat ${php}/etc/php.ini > $out + echo "$options" >> $out + ''; + +in + + +{ + + ###### interface + + options = { + + services.httpdProd = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Apache HTTP Server."; + }; + + package = mkOption { + type = types.package; + default = pkgs.apacheHttpd; + defaultText = "pkgs.apacheHttpd"; + description = '' + Overridable attribute of the Apache HTTP Server package to use. + ''; + }; + + configFile = mkOption { + type = types.path; + default = confFile; + defaultText = "confFile"; + example = literalExample ''pkgs.writeText "httpd.conf" "# my custom config file ..."''; + description = '' + Override the configuration file used by Apache. By default, + NixOS generates one automatically. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Cnfiguration lines appended to the generated Apache + configuration file. Note that this mechanism may not work + when is overridden. + ''; + }; + + extraModules = mkOption { + type = types.listOf types.unspecified; + default = []; + example = literalExample ''[ "proxy_connect" { name = "php5"; path = "''${pkgs.php}/modules/libphp5.so"; } ]''; + description = '' + Additional Apache modules to be used. These can be + specified as a string in the case of modules distributed + with Apache, or as an attribute set specifying the + name and path of the + module. + ''; + }; + + logPerVirtualHost = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, each virtual host gets its own + access_log and + error_log, namely suffixed by the + of the virtual host. + ''; + }; + + user = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + User account under which httpd runs. The account is created + automatically if it doesn't exist. + ''; + }; + + group = mkOption { + type = types.str; + default = "wwwrun"; + description = '' + Group under which httpd runs. The account is created + automatically if it doesn't exist. + ''; + }; + + logDir = mkOption { + type = types.path; + default = "/var/log/httpd"; + description = '' + Directory for Apache's log files. It is created automatically. + ''; + }; + + stateDir = mkOption { + type = types.path; + default = "/run/httpd"; + description = '' + Directory for Apache's transient runtime state (such as PID + files). It is created automatically. Note that the default, + /run/httpd, is deleted at boot time. + ''; + }; + + virtualHosts = mkOption { + type = types.listOf (types.submodule ( + { options = import ./per-server-options.nix { + inherit lib; + forMainServer = false; + }; + })); + default = []; + example = [ + { hostName = "foo"; + documentRoot = "/data/webroot-foo"; + } + { hostName = "bar"; + documentRoot = "/data/webroot-bar"; + } + ]; + description = '' + Specification of the virtual hosts served by Apache. Each + element should be an attribute set specifying the + configuration of the virtual host. The available options + are the non-global options permissible for the main host. + ''; + }; + + enableMellon = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the mod_auth_mellon module."; + }; + + enablePHP = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the PHP module."; + }; + + phpPackage = mkOption { + type = types.package; + default = pkgs.php; + defaultText = "pkgs.php"; + description = '' + Overridable attribute of the PHP package to use. + ''; + }; + + enablePerl = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Perl module (mod_perl)."; + }; + + phpOptions = mkOption { + type = types.lines; + default = ""; + example = + '' + date.timezone = "CET" + ''; + description = + "Options appended to the PHP configuration file php.ini."; + }; + + multiProcessingModule = mkOption { + type = types.str; + default = "prefork"; + example = "worker"; + description = + '' + Multi-processing module to be used by Apache. Available + modules are prefork (the default; + handles each request in a separate child process), + worker (hybrid approach that starts a + number of child processes each running a number of + threads) and event (a recent variant of + worker that handles persistent + connections more efficiently). + ''; + }; + + maxClients = mkOption { + type = types.int; + default = 150; + example = 8; + description = "Maximum number of httpd processes (prefork)"; + }; + + maxRequestsPerChild = mkOption { + type = types.int; + default = 0; + example = 500; + description = + "Maximum number of httpd requests answered per httpd child (prefork), 0 means unlimited"; + }; + } + + # Include the options shared between the main server and virtual hosts. + // (import ./per-server-options.nix { + inherit lib; + forMainServer = true; + }); + + }; + + + ###### implementation + + config = mkIf config.services.httpdProd.enable { + + assertions = [ { assertion = mainCfg.enableSSL == true + -> mainCfg.sslServerCert != null + && mainCfg.sslServerKey != null; + message = "SSL is enabled for httpd, but sslServerCert and/or sslServerKey haven't been specified."; } + ]; + + 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); + + environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices; + + services.httpdProd.phpOptions = + '' + ; Needed for PHP's mail() function. + sendmail_path = sendmail -t -i + '' + optionalString (!isNull config.time.timeZone) '' + + ; Apparently PHP doesn't use $TZ. + date.timezone = "${config.time.timeZone}" + ''; + + systemd.services.httpdProd = + { description = "Apache HTTPD"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "keys.target" ]; + after = [ "network.target" "fs.target" "postgresql.service" "keys.target" ]; + + path = + [ httpd pkgs.coreutils pkgs.gnugrep ] + ++ # Needed for PHP's mail() function. !!! Probably the + # ssmtp module should export the path to sendmail in + # some way. + optional config.networking.defaultMailServer.directDelivery pkgs.ssmtp + ++ concatMap (svc: svc.extraServerPath) allSubservices; + + environment = + optionalAttrs enablePHP { PHPRC = phpIni; } + // optionalAttrs mainCfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; } + // (listToAttrs (concatMap (svc: svc.globalEnvVars) allSubservices)); + + preStart = + '' + mkdir -m 0750 -p ${mainCfg.stateDir} + [ $(id -u) != 0 ] || chown root.${mainCfg.group} ${mainCfg.stateDir} + ${optionalString version24 '' + mkdir -m 0750 -p "${mainCfg.stateDir}/runtime" + [ $(id -u) != 0 ] || chown root.${mainCfg.group} "${mainCfg.stateDir}/runtime" + ''} + mkdir -m 0700 -p ${mainCfg.logDir} + + # Get rid of old semaphores. These tend to accumulate across + # server restarts, eventually preventing it from restarting + # successfully. + for i in $(${pkgs.utillinux}/bin/ipcs -s | grep ' ${mainCfg.user} ' | cut -f2 -d ' '); do + ${pkgs.utillinux}/bin/ipcrm -s $i + done + + # Run the startup hooks for the subservices. + for i in ${toString (map (svn: svn.startupScript) allSubservices)}; do + echo Running Apache startup hook $i... + $i + done + ''; + + serviceConfig.ExecStart = "@${httpd}/bin/httpd httpd -f ${httpdConf}"; + serviceConfig.ExecStop = "${httpd}/bin/httpd -f ${httpdConf} -k graceful-stop"; + serviceConfig.ExecReload = "${httpd}/bin/httpd -f ${httpdConf} -k graceful"; + serviceConfig.Type = "forking"; + serviceConfig.PIDFile = "${mainCfg.stateDir}/httpd.pid"; + serviceConfig.Restart = "always"; + serviceConfig.RestartSec = "5s"; + }; + + }; +} diff --git a/virtual/modules/websites/apache/per-server-options.nix b/virtual/modules/websites/apache/per-server-options.nix new file mode 100644 index 0000000..4bbd041 --- /dev/null +++ b/virtual/modules/websites/apache/per-server-options.nix @@ -0,0 +1,188 @@ +# This file defines the options that can be used both for the Apache +# main server configuration, and for the virtual hosts. (The latter +# has additional options that affect the web server as a whole, like +# the user/group to run under.) + +{ forMainServer, lib }: + +with lib; + +{ + + hostName = mkOption { + type = types.str; + default = "localhost"; + description = "Canonical hostname for the server."; + }; + + serverAliases = mkOption { + type = types.listOf types.str; + default = []; + example = ["www.example.org" "www.example.org:8080" "example.org"]; + description = '' + Additional names of virtual hosts served by this virtual host configuration. + ''; + }; + + port = mkOption { + type = types.int; + default = 0; + description = '' + Port for the server. Option will be removed, use instead. + ''; + }; + + listen = mkOption { + type = types.listOf (types.submodule ( + { + options = { + port = mkOption { + type = types.int; + description = "port to listen on"; + }; + ip = mkOption { + type = types.string; + default = "*"; + description = "Ip to listen on. 0.0.0.0 for ipv4 only, * for all."; + }; + }; + } )); + description = '' + List of { /* ip: "*"; */ port = 80;} to listen on + ''; + + default = []; + }; + + enableSSL = mkOption { + type = types.bool; + default = false; + description = "Whether to enable SSL (https) support."; + }; + + # Note: sslServerCert and sslServerKey can be left empty, but this + # only makes sense for virtual hosts (they will inherit from the + # main server). + + sslServerCert = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/host.cert"; + description = "Path to server SSL certificate."; + }; + + sslServerKey = mkOption { + type = types.path; + example = "/var/host.key"; + description = "Path to server SSL certificate key."; + }; + + sslServerChain = mkOption { + type = types.nullOr types.path; + default = null; + example = "/var/ca.pem"; + description = "Path to server SSL chain file."; + }; + + adminAddr = mkOption ({ + type = types.nullOr types.str; + example = "admin@example.org"; + description = "E-mail address of the server administrator."; + } // (if forMainServer then {} else {default = null;})); + + documentRoot = mkOption { + type = types.nullOr types.path; + default = null; + example = "/data/webserver/docs"; + description = '' + The path of Apache's document root directory. If left undefined, + an empty directory in the Nix store will be used as root. + ''; + }; + + servedDirs = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/nix"; + dir = "/home/eelco/Dev/nix-homepage"; + } + ]; + description = '' + This option provides a simple way to serve static directories. + ''; + }; + + servedFiles = mkOption { + type = types.listOf types.attrs; + default = []; + example = [ + { urlPath = "/foo/bar.png"; + file = "/home/eelco/some-file.png"; + } + ]; + description = '' + This option provides a simple way to serve individual, static files. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + + Options FollowSymlinks + AllowOverride All + + ''; + description = '' + These lines go to httpd.conf verbatim. They will go after + directories and directory aliases defined by default. + ''; + }; + + extraSubservices = mkOption { + type = types.listOf types.unspecified; + default = []; + description = "Extra subservices to enable in the webserver."; + }; + + enableUserDir = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable serving ~/public_html as + /~username. + ''; + }; + + globalRedirect = mkOption { + type = types.nullOr types.str; + default = null; + example = http://newserver.example.org/; + description = '' + If set, all requests for this host are redirected permanently to + the given URL. + ''; + }; + + logFormat = mkOption { + type = types.str; + default = "common"; + example = "combined"; + description = '' + Log format for Apache's log files. Possible values are: combined, common, referer, agent. + ''; + }; + + robotsEntries = mkOption { + type = types.lines; + default = ""; + example = "Disallow: /foo/"; + description = '' + Specification of pages to be ignored by web crawlers. See for details. + ''; + }; + +} diff --git a/virtual/modules/websites/aten.nix b/virtual/modules/websites/aten.nix index 1a65389..53db03a 100644 --- a/virtual/modules/websites/aten.nix +++ b/virtual/modules/websites/aten.nix @@ -27,12 +27,25 @@ in { services.phpfpm.poolConfigs.aten_prod = aten_prod.phpFpm.pool; system.activationScripts.aten_prod = aten_prod.activationScript; services.myWebsites.apacheConfig.aten_prod.modules = aten_prod.apache.modules; + services.myWebsites.production.modules = aten_prod.apache.modules; + services.myWebsites.production.vhostConfs.aten = { + certName = "aten"; + hosts = [ "aten.pro" "www.aten.pro" ]; + root = aten_prod.webRoot; + extraConfig = [ aten_prod.apache.vhostConf ]; + }; }) (lib.mkIf cfg.integration.enable { security.acme.certs."eldiron".extraDomains."dev.aten.pro" = null; services.phpfpm.poolConfigs.aten_dev = aten_dev.phpFpm.pool; system.activationScripts.aten_dev = aten_dev.activationScript; - services.myWebsites.apacheConfig.aten_dev.modules = aten_dev.apache.modules; + services.myWebsites.integration.modules = aten_dev.apache.modules; + services.myWebsites.integration.vhostConfs.aten = { + certName = "eldiron"; + hosts = [ "dev.aten.pro" ]; + root = aten_dev.webRoot; + extraConfig = [ aten_dev.apache.vhostConf ]; + }; }) ]; } diff --git a/virtual/modules/websites/chloe.nix b/virtual/modules/websites/chloe.nix index d54c42d..67e8e1f 100644 --- a/virtual/modules/websites/chloe.nix +++ b/virtual/modules/websites/chloe.nix @@ -26,13 +26,25 @@ in { services.phpfpm.poolConfigs.chloe_prod = chloe_prod.phpFpm.pool; system.activationScripts.chloe_prod = chloe_prod.activationScript; - services.myWebsites.apacheConfig.chloe_prod.modules = chloe_prod.apache.modules; + services.myWebsites.production.modules = chloe_prod.apache.modules; + services.myWebsites.production.vhostConfs.chloe = { + certName = "chloe"; + hosts = ["osteopathe-cc.fr" "www.osteopathe-cc.fr" ]; + root = chloe_prod.webRoot; + extraConfig = [ chloe_prod.apache.vhostConf ]; + }; }) (lib.mkIf cfg.integration.enable { security.acme.certs."eldiron".extraDomains."chloe.immae.eu" = null; services.phpfpm.poolConfigs.chloe_dev = chloe_dev.phpFpm.pool; system.activationScripts.chloe_dev = chloe_dev.activationScript; - services.myWebsites.apacheConfig.chloe_dev.modules = chloe_dev.apache.modules; + services.myWebsites.integration.modules = chloe_dev.apache.modules; + services.myWebsites.integration.vhostConfs.chloe = { + certName = "eldiron"; + hosts = ["chloe.immae.eu" ]; + root = chloe_dev.webRoot; + extraConfig = [ chloe_dev.apache.vhostConf ]; + }; }) ]; } diff --git a/virtual/modules/websites/connexionswing.nix b/virtual/modules/websites/connexionswing.nix index 8bf63a8..dcc7264 100644 --- a/virtual/modules/websites/connexionswing.nix +++ b/virtual/modules/websites/connexionswing.nix @@ -28,14 +28,26 @@ in { services.phpfpm.poolConfigs.connexionswing_prod = connexionswing_prod.phpFpm.pool; system.activationScripts.connexionswing_prod = connexionswing_prod.activationScript; - services.myWebsites.apacheConfig.connexionswing_prod.modules = connexionswing_prod.apache.modules; + services.myWebsites.production.modules = connexionswing_prod.apache.modules; + services.myWebsites.production.vhostConfs.connexionswing = { + certName = "connexionswing"; + hosts = ["connexionswing.com" "sandetludo.com" "www.connexionswing.com" "www.sandetludo.com" ]; + root = connexionswing_prod.webRoot; + extraConfig = [ connexionswing_prod.apache.vhostConf ]; + }; }) (lib.mkIf cfg.integration.enable { security.acme.certs."eldiron".extraDomains."sandetludo.immae.eu" = null; security.acme.certs."eldiron".extraDomains."connexionswing.immae.eu" = null; services.phpfpm.poolConfigs.connexionswing_dev = connexionswing_dev.phpFpm.pool; system.activationScripts.connexionswing_dev = connexionswing_dev.activationScript; - services.myWebsites.apacheConfig.connexionswing_dev.modules = connexionswing_dev.apache.modules; + services.myWebsites.integration.modules = connexionswing_dev.apache.modules; + services.myWebsites.integration.vhostConfs.connexionswing = { + certName = "eldiron"; + hosts = ["connexionswing.immae.eu" "sandetludo.immae.eu" ]; + root = connexionswing_dev.webRoot; + extraConfig = [ connexionswing_dev.apache.vhostConf ]; + }; }) ]; } diff --git a/virtual/modules/websites/ludivine.nix b/virtual/modules/websites/ludivine.nix index f06e41a..6eb98e7 100644 --- a/virtual/modules/websites/ludivine.nix +++ b/virtual/modules/websites/ludivine.nix @@ -26,7 +26,13 @@ in { services.phpfpm.poolConfigs.ludivinecassal_prod = ludivinecassal_prod.phpFpm.pool; system.activationScripts.ludivinecassal_prod = ludivinecassal_prod.activationScript; - services.myWebsites.apacheConfig.ludivinecassal_prod.modules = ludivinecassal_prod.apache.modules; + services.myWebsites.production.modules = ludivinecassal_prod.apache.modules; + services.myWebsites.production.vhostConfs.ludivine = { + certName = "ludivinecassal"; + hosts = ["ludivinecassal.com" "www.ludivinecassal.com" ]; + root = ludivinecassal_prod.webRoot; + extraConfig = [ ludivinecassal_prod.apache.vhostConf ]; + }; }) (lib.mkIf cfg.integration.enable { security.acme.certs."eldiron".extraDomains."ludivine.immae.eu" = null; @@ -34,6 +40,13 @@ in { services.phpfpm.poolConfigs.ludivinecassal_dev = ludivinecassal_dev.phpFpm.pool; system.activationScripts.ludivinecassal_dev = ludivinecassal_dev.activationScript; services.myWebsites.apacheConfig.ludivinecassal_dev.modules = ludivinecassal_dev.apache.modules; + services.myWebsites.integration.modules = ludivinecassal_dev.apache.modules; + services.myWebsites.integration.vhostConfs.ludivine = { + certName = "eldiron"; + hosts = [ "ludivine.immae.eu" ]; + root = ludivinecassal_dev.webRoot; + extraConfig = [ ludivinecassal_dev.apache.vhostConf ]; + }; }) ]; } diff --git a/virtual/modules/websites/piedsjaloux.nix b/virtual/modules/websites/piedsjaloux.nix index 285fd18..46161c7 100644 --- a/virtual/modules/websites/piedsjaloux.nix +++ b/virtual/modules/websites/piedsjaloux.nix @@ -26,13 +26,25 @@ in { services.phpfpm.poolConfigs.piedsjaloux_prod = piedsjaloux_prod.phpFpm.pool; system.activationScripts.piedsjaloux_prod = piedsjaloux_prod.activationScript; - services.myWebsites.apacheConfig.piedsjaloux_prod.modules = piedsjaloux_prod.apache.modules; + services.myWebsites.production.modules = piedsjaloux_prod.apache.modules; + services.myWebsites.production.vhostConfs.piedsjaloux = { + certName = "piedsjaloux"; + hosts = [ "piedsjaloux.fr" "www.piedsjaloux.fr" ]; + root = piedsjaloux_prod.webRoot; + extraConfig = [ piedsjaloux_prod.apache.vhostConf ]; + }; }) (lib.mkIf cfg.integration.enable { security.acme.certs."eldiron".extraDomains."piedsjaloux.immae.eu" = null; services.phpfpm.poolConfigs.piedsjaloux_dev = piedsjaloux_dev.phpFpm.pool; system.activationScripts.piedsjaloux_dev = piedsjaloux_dev.activationScript; - services.myWebsites.apacheConfig.piedsjaloux_dev.modules = piedsjaloux_dev.apache.modules; + services.myWebsites.integration.modules = piedsjaloux_dev.apache.modules; + services.myWebsites.integration.vhostConfs.piedsjaloux = { + certName = "eldiron"; + hosts = [ "piedsjaloux.immae.eu" ]; + root = piedsjaloux_dev.webRoot; + extraConfig = [ piedsjaloux_dev.apache.vhostConf ]; + }; }) ]; } -- 2.41.0