{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.httpd;
runtimeDir = "/run/httpd";
pkg = cfg.package.out;
httpdConf = cfg.configFile;
php = cfg.phpPackage.override { apacheHttpd = pkg.dev; /* otherwise it only gets .out */ };
phpMajorVersion = lib.versions.major (lib.getVersion php);
mod_perl = pkgs.apacheHttpdPackages.mod_perl.override { apacheHttpd = pkg; };
vhosts = attrValues cfg.virtualHosts;
mkListenInfo = hostOpts:
if hostOpts.listen != [] then hostOpts.listen
else (
optional (hostOpts.onlySSL || hostOpts.addSSL || hostOpts.forceSSL) { ip = "*"; port = 443; ssl = true; } ++
optional (!hostOpts.onlySSL) { ip = "*"; port = 80; ssl = false; }
);
listenInfo = unique (concatMap mkListenInfo vhosts);
enableHttp2 = any (vhost: vhost.http2) vhosts;
enableSSL = any (listen: listen.ssl) listenInfo;
enableUserDir = any (vhost: vhost.enableUserDir) vhosts;
# NOTE: generally speaking order of modules is very important
modules =
[ # required apache modules our httpd service cannot run without
"authn_core" "authz_core"
"log_config"
"mime" "autoindex" "negotiation" "dir"
"alias" "rewrite"
"unixd" "slotmem_shm" "socache_shmcb"
"mpm_${cfg.multiProcessingModule}"
]
++ (if cfg.multiProcessingModule == "prefork" then [ "cgi" ] else [ "cgid" ])
++ optional enableHttp2 "http2"
++ optional enableSSL "ssl"
++ optional enableUserDir "userdir"
++ optional cfg.enableMellon { name = "auth_mellon"; path = "${pkgs.apacheHttpdPackages.mod_auth_mellon}/modules/mod_auth_mellon.so"; }
++ optional cfg.enablePHP { name = "php${phpMajorVersion}"; path = "${php}/modules/libphp${phpMajorVersion}.so"; }
++ optional cfg.enablePerl { name = "perl"; path = "${mod_perl}/modules/mod_perl.so"; }
++ cfg.extraModules;
loggingConf = (if cfg.logFormat != "none" then ''
ErrorLog ${cfg.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 ${cfg.logDir}/access.log ${cfg.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 shmcb:${runtimeDir}/ssl_scache(512000)
Mutex posixsem
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLProtocol ${cfg.sslProtocols}
SSLCipherSuite ${cfg.sslCiphers}
SSLHonorCipherOrder on
'';
mimeConf = ''
TypesConfig ${pkg}/conf/mime.types
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl .crl
AddType application/x-httpd-php .php .phtml
MIMEMagicFile ${pkg}/conf/magic
'';
mkVHostConf = hostOpts:
let
adminAddr = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
listen = filter (listen: !listen.ssl) (mkListenInfo hostOpts);
listenSSL = filter (listen: listen.ssl) (mkListenInfo hostOpts);
useACME = hostOpts.enableACME || hostOpts.useACMEHost != null;
sslCertDir =
if hostOpts.enableACME then config.security.acme.certs.${hostOpts.hostName}.directory
else if hostOpts.useACMEHost != null then config.security.acme.certs.${hostOpts.useACMEHost}.directory
else abort "This case should never happen.";
sslServerCert = if useACME then "${sslCertDir}/full.pem" else hostOpts.sslServerCert;
sslServerKey = if useACME then "${sslCertDir}/key.pem" else hostOpts.sslServerKey;
sslServerChain = if useACME then "${sslCertDir}/fullchain.pem" else hostOpts.sslServerChain;
acmeChallenge = optionalString useACME ''
Alias /.well-known/acme-challenge/ "${hostOpts.acmeRoot}/.well-known/acme-challenge/"
AllowOverride None
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
Require method GET POST OPTIONS
Require all granted
'';
in
optionalString (listen != []) ''
ServerName ${hostOpts.hostName}
${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
ServerAdmin ${adminAddr}
SSLEngine off
${acmeChallenge}
${if hostOpts.forceSSL then ''
RewriteEngine on
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge [NC]
RewriteCond %{HTTPS} off
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}
'' else mkVHostCommonConf hostOpts}
'' +
optionalString (listenSSL != []) ''
ServerName ${hostOpts.hostName}
${concatMapStrings (alias: "ServerAlias ${alias}\n") hostOpts.serverAliases}
ServerAdmin ${adminAddr}
SSLEngine on
SSLCertificateFile ${sslServerCert}
SSLCertificateKeyFile ${sslServerKey}
${optionalString (sslServerChain != null) "SSLCertificateChainFile ${sslServerChain}"}
${optionalString hostOpts.http2 "Protocols h2 h2c http/1.1"}
${acmeChallenge}
${mkVHostCommonConf hostOpts}
''
;
mkVHostCommonConf = hostOpts:
let
documentRoot = if hostOpts.documentRoot != null
then hostOpts.documentRoot
else pkgs.runCommand "empty" { preferLocalBuild = true; } "mkdir -p $out"
;
mkLocations = locations: concatStringsSep "\n" (map (config: ''
${optionalString (config.proxyPass != null) ''
ProxyPass ${config.proxyPass}
ProxyPassReverse ${config.proxyPass}
''}
${optionalString (config.index != null) ''
DirectoryIndex ${config.index}
''}
${optionalString (config.alias != null) ''
Alias "${config.alias}"
''}
${config.extraConfig}
'') (sortProperties (mapAttrsToList (k: v: v // { location = k; }) locations)));
in
''
${optionalString cfg.logPerVirtualHost ''
ErrorLog ${cfg.logDir}/error-${hostOpts.hostName}.log
CustomLog ${cfg.logDir}/access-${hostOpts.hostName}.log ${hostOpts.logFormat}
''}
${optionalString (hostOpts.robotsEntries != "") ''
Alias /robots.txt ${pkgs.writeText "robots.txt" hostOpts.robotsEntries}
''}
DocumentRoot "${documentRoot}"
Options Indexes FollowSymLinks
AllowOverride None
Require all granted
${optionalString hostOpts.enableUserDir ''
UserDir public_html
UserDir disabled root
AllowOverride FileInfo AuthConfig Limit Indexes
Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
Require all granted
Require all denied
''}
${optionalString (hostOpts.globalRedirect != null && hostOpts.globalRedirect != "") ''
RedirectPermanent / ${hostOpts.globalRedirect}
''}
${
let makeDirConf = elem: ''
Alias ${elem.urlPath} ${elem.dir}/
Options +Indexes
Require all granted
AllowOverride All
'';
in concatMapStrings makeDirConf hostOpts.servedDirs
}
${mkLocations hostOpts.locations}
${hostOpts.extraConfig}
''
;
confFile = pkgs.writeText "httpd.conf" ''
ServerRoot ${pkg}
ServerName ${config.networking.hostName}
DefaultRuntimeDir ${runtimeDir}/runtime
PidFile ${runtimeDir}/httpd.pid
${optionalString (cfg.multiProcessingModule != "prefork") ''
# mod_cgid requires this.
ScriptSock ${runtimeDir}/cgisock
''}
MaxClients ${toString cfg.maxClients}
MaxRequestsPerChild ${toString cfg.maxRequestsPerChild}
${let
toStr = listen: "Listen ${listen.ip}:${toString listen.port} ${if listen.ssl then "https" else "http"}";
uniqueListen = uniqList {inputList = map toStr listenInfo;};
in concatStringsSep "\n" uniqueListen
}
User ${cfg.user}
Group ${cfg.group}
${let
mkModule = module:
if isString module then { name = module; path = "${pkg}/modules/mod_${module}.so"; }
else if isAttrs module then { inherit (module) name path; }
else throw "Expecting either a string or attribute set including a name and path.";
in
concatMapStringsSep "\n" (module: "LoadModule ${module.name}_module ${module.path}") (unique (map mkModule modules))
}
AddHandler type-map var
Require all denied
${mimeConf}
${loggingConf}
${browserHacks}
Include ${pkg}/conf/extra/httpd-default.conf
Include ${pkg}/conf/extra/httpd-autoindex.conf
Include ${pkg}/conf/extra/httpd-multilang-errordoc.conf
Include ${pkg}/conf/extra/httpd-languages.conf
TraceEnable off
${sslConf}
# Fascist default - deny access to everything.
Options FollowSymLinks
AllowOverride None
Require all denied
# 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.
Require all granted
${cfg.extraConfig}
${concatMapStringsSep "\n" mkVHostConf vhosts}
'';
# Generate the PHP configuration file. Should probably be factored
# out into a separate module.
phpIni = pkgs.runCommand "php.ini"
{ options = cfg.phpOptions;
preferLocalBuild = true;
}
''
cat ${php}/etc/php.ini > $out
cat ${php}/lib/custom-php.ini > $out
echo "$options" >> $out
'';
in
{
imports = [
(mkRemovedOptionModule [ "services" "httpd" "extraSubservices" ] "Most existing subservices have been ported to the NixOS module system. Please update your configuration accordingly.")
(mkRemovedOptionModule [ "services" "httpd" "stateDir" ] "The httpd module now uses /run/httpd as a runtime directory.")
# virtualHosts options
(mkRemovedOptionModule [ "services" "httpd" "documentRoot" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "enableSSL" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "enableUserDir" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "globalRedirect" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "hostName" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "listen" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "robotsEntries" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "servedDirs" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "servedFiles" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "serverAliases" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "sslServerCert" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "sslServerChain" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
(mkRemovedOptionModule [ "services" "httpd" "sslServerKey" ] "Please define a virtual host using `services.httpd.virtualHosts`.")
];
# interface
options = {
services.httpd = {
enable = mkEnableOption "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 = ''
Configuration lines appended to the generated Apache
configuration file. Note that this mechanism will not work
when is overridden.
'';
};
extraModules = mkOption {
type = types.listOf types.unspecified;
default = [];
example = literalExample ''
[
"proxy_connect"
{ name = "jk"; path = "''${pkgs.tomcat_connectors}/modules/mod_jk.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.
'';
};
adminAddr = mkOption {
type = types.str;
example = "admin@example.org";
description = "E-mail address of the server administrator.";
};
logFormat = mkOption {
type = types.str;
default = "common";
example = "combined";
description = ''
Log format for log files. Possible values are: combined, common, referer, agent.
See for more details.
'';
};
logPerVirtualHost = mkOption {
type = types.bool;
default = true;
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.
'';
};
group = mkOption {
type = types.str;
default = "wwwrun";
description = ''
Group under which httpd runs.
'';
};
logDir = mkOption {
type = types.path;
default = "/var/log/httpd";
description = ''
Directory for Apache's log files. It is created automatically.
'';
};
virtualHosts = mkOption {
type = with types; attrsOf (submodule (import ./vhost-options.nix));
default = {
localhost = {
documentRoot = "${pkg}/htdocs";
};
};
example = literalExample ''
{
"foo.example.com" = {
forceSSL = true;
documentRoot = "/var/www/foo.example.com"
};
"bar.example.com" = {
addSSL = true;
documentRoot = "/var/www/bar.example.com";
};
}
'';
description = ''
Specification of the virtual hosts served by Apache. Each
element should be an attribute set specifying the
configuration of the virtual 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.enum [ "event" "prefork" "worker" ];
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.
'';
};
sslCiphers = mkOption {
type = types.str;
default = "HIGH:!aNULL:!MD5:!EXP";
description = "Cipher Suite available for negotiation in SSL proxy handshake.";
};
sslProtocols = mkOption {
type = types.str;
default = "All -SSLv2 -SSLv3 -TLSv1 -TLSv1.1";
example = "All -SSLv2 -SSLv3";
description = "Allowed SSL/TLS protocol versions.";
};
};
};
# implementation
config = mkIf cfg.enable {
assertions = [
{
assertion = all (hostOpts: !hostOpts.enableSSL) vhosts;
message = ''
The option `services.httpd.virtualHosts..enableSSL` no longer has any effect; please remove it.
Select one of `services.httpd.virtualHosts..addSSL`, `services.httpd.virtualHosts..forceSSL`,
or `services.httpd.virtualHosts..onlySSL`.
'';
}
{
assertion = all (hostOpts: with hostOpts; !(addSSL && onlySSL) && !(forceSSL && onlySSL) && !(addSSL && forceSSL)) vhosts;
message = ''
Options `services.httpd.virtualHosts..addSSL`,
`services.httpd.virtualHosts..onlySSL` and `services.httpd.virtualHosts..forceSSL`
are mutually exclusive.
'';
}
{
assertion = all (hostOpts: !(hostOpts.enableACME && hostOpts.useACMEHost != null)) vhosts;
message = ''
Options `services.httpd.virtualHosts..enableACME` and
`services.httpd.virtualHosts..useACMEHost` are mutually exclusive.
'';
}
];
warnings =
mapAttrsToList (name: hostOpts: ''
Using config.services.httpd.virtualHosts."${name}".servedFiles is deprecated and will become unsupported in a future release. Your configuration will continue to work as is but please migrate your configuration to config.services.httpd.virtualHosts."${name}".locations before the 20.09 release of NixOS.
'') (filterAttrs (name: hostOpts: hostOpts.servedFiles != []) cfg.virtualHosts);
users.users = optionalAttrs (cfg.user == "wwwrun") {
wwwrun = {
group = cfg.group;
description = "Apache httpd user";
uid = config.ids.uids.wwwrun;
};
};
users.groups = optionalAttrs (cfg.group == "wwwrun") {
wwwrun.gid = config.ids.gids.wwwrun;
};
security.acme.certs = mapAttrs (name: hostOpts: {
user = cfg.user;
group = mkDefault cfg.group;
email = if hostOpts.adminAddr != null then hostOpts.adminAddr else cfg.adminAddr;
webroot = hostOpts.acmeRoot;
extraDomains = genAttrs hostOpts.serverAliases (alias: null);
postRun = "systemctl reload httpd.service";
}) (filterAttrs (name: hostOpts: hostOpts.enableACME) cfg.virtualHosts);
environment.systemPackages = [ pkg ];
# required for "apachectl configtest"
environment.etc."httpd/httpd.conf".source = httpdConf;
services.httpd.phpOptions =
''
; Needed for PHP's mail() function.
sendmail_path = sendmail -t -i
; Don't advertise PHP
expose_php = off
'' + optionalString (config.time.timeZone != null) ''
; Apparently PHP doesn't use $TZ.
date.timezone = "${config.time.timeZone}"
'';
services.httpd.extraModules = mkBefore [
# 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"
# Authorization: is the user allowed access?
"authz_user" "authz_groupfile" "authz_host"
# Other modules.
"ext_filter" "include" "env" "mime_magic"
"cern_meta" "expires" "headers" "usertrack" "setenvif"
"dav" "status" "asis" "info" "dav_fs"
"vhost_alias" "imagemap" "actions" "speling"
"proxy" "proxy_http"
"cache" "cache_disk"
# For compatibility with old configurations, the new module mod_access_compat is provided.
"access_compat"
];
systemd.tmpfiles.rules =
let
svc = config.systemd.services.httpd.serviceConfig;
in
[
"d '${cfg.logDir}' 0700 ${svc.User} ${svc.Group}"
"Z '${cfg.logDir}' - ${svc.User} ${svc.Group}"
];
systemd.services.httpd =
let
vhostsACME = filter (hostOpts: hostOpts.enableACME) vhosts;
in
{ description = "Apache HTTPD";
wantedBy = [ "multi-user.target" ];
wants = concatLists (map (hostOpts: [ "acme-${hostOpts.hostName}.service" "acme-selfsigned-${hostOpts.hostName}.service" ]) vhostsACME);
after = [ "network.target" "fs.target" ] ++ map (hostOpts: "acme-selfsigned-${hostOpts.hostName}.service") vhostsACME;
path =
[ pkg pkgs.coreutils pkgs.gnugrep ]
++ optional cfg.enablePHP pkgs.system-sendmail; # Needed for PHP's mail() function.
environment =
optionalAttrs cfg.enablePHP { PHPRC = phpIni; }
// optionalAttrs cfg.enableMellon { LD_LIBRARY_PATH = "${pkgs.xmlsec}/lib"; };
preStart =
''
# 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 ' ${cfg.user} ' | cut -f2 -d ' '); do
${pkgs.utillinux}/bin/ipcrm -s $i
done
'';
serviceConfig = {
ExecStart = "@${pkg}/bin/httpd httpd -f ${httpdConf}";
ExecStop = "${pkg}/bin/httpd -f ${httpdConf} -k graceful-stop";
ExecReload = "${pkg}/bin/httpd -f ${httpdConf} -k graceful";
User = "root";
Group = cfg.group;
Type = "forking";
PIDFile = "${runtimeDir}/httpd.pid";
Restart = "always";
RestartSec = "5s";
RuntimeDirectory = "httpd httpd/runtime";
RuntimeDirectoryMode = "0750";
};
};
};
}