{ config, lib, pkgs, ... }:
with lib;
let
mainCfg = config.services.httpdTools;
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}
# 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.httpdTools = {
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.httpdTools.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);
users.users = optionalAttrs (mainCfg.user == "wwwrun") (singleton
{ name = "wwwrun";
group = mainCfg.group;
description = "Apache httpd user";
uid = config.ids.uids.wwwrun;
});
users.groups = optionalAttrs (mainCfg.group == "wwwrun") (singleton
{ name = "wwwrun";
gid = config.ids.gids.wwwrun;
});
environment.systemPackages = [httpd] ++ concatMap (svc: svc.extraPath) allSubservices;
services.httpdTools.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.httpdTools =
{ 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";
};
};
}