]> git.immae.eu Git - perso/Immae/Config/Nix/NUR.git/blame - modules/websites/default.nix
Add webapp dirs in websites module
[perso/Immae/Config/Nix/NUR.git] / modules / websites / default.nix
CommitLineData
24fd1fe6
IB
1{ lib, config, ... }: with lib;
2let
ea5b3f51
IB
3 cfg = {
4 certs = config.services.websitesCerts;
5 webappDirs = config.services.websitesWebappDirs;
6 env = config.services.websites;
7 };
24fd1fe6
IB
8in
9{
10 options.services.websitesCerts = mkOption {
11 description = "Default websites configuration for certificates as accepted by acme";
12 };
ea5b3f51
IB
13 options.services.websitesWebappDirs = mkOption {
14 description = ''
15 Defines a symlink between /run/current-system/webapps and a store
16 app directory to be used in http configuration. Permits to avoid
17 restarting httpd when only the folder name changes.
18 '';
19 type = types.attrsOf types.path;
20 default = {};
21 };
22 # TODO: ajouter /run/current-system/webapps (RO) et webapps (RW)
24fd1fe6
IB
23 options.services.websites = with types; mkOption {
24 default = {};
25 description = "Each type of website to enable will target a distinct httpd server";
26 type = attrsOf (submodule {
27 options = {
28 enable = mkEnableOption "Enable websites of this type";
29 adminAddr = mkOption {
30 type = str;
31 description = "Admin e-mail address of the instance";
32 };
33 httpdName = mkOption {
34 type = str;
35 description = "Name of the httpd instance to assign this type to";
36 };
37 ips = mkOption {
38 type = listOf string;
39 default = [];
40 description = "ips to listen to";
41 };
42 modules = mkOption {
43 type = listOf str;
44 default = [];
45 description = "Additional modules to load in Apache";
46 };
47 extraConfig = mkOption {
48 type = listOf lines;
49 default = [];
50 description = "Additional configuration to append to Apache";
51 };
52 nosslVhost = mkOption {
53 description = "A default nossl vhost for captive portals";
54 default = {};
55 type = submodule {
56 options = {
57 enable = mkEnableOption "Add default no-ssl vhost for this instance";
58 host = mkOption {
59 type = string;
60 description = "The hostname to use for this vhost";
61 };
62 root = mkOption {
63 type = path;
64 default = ./nosslVhost;
65 description = "The root folder to serve";
66 };
67 indexFile = mkOption {
68 type = string;
69 default = "index.html";
70 description = "The index file to show.";
71 };
72 };
73 };
74 };
75 fallbackVhost = mkOption {
76 description = "The fallback vhost that will be defined as first vhost in Apache";
77 type = submodule {
78 options = {
79 certName = mkOption { type = string; };
80 hosts = mkOption { type = listOf string; };
81 root = mkOption { type = nullOr path; };
82 extraConfig = mkOption { type = listOf lines; default = []; };
83 };
84 };
85 };
86 vhostConfs = mkOption {
87 default = {};
88 description = "List of vhosts to define for Apache";
89 type = attrsOf (submodule {
90 options = {
91 certName = mkOption { type = string; };
92 addToCerts = mkOption {
93 type = bool;
94 default = false;
95 description = "Use these to certificates. Is ignored (considered true) if certMainHost is not null";
96 };
97 certMainHost = mkOption {
98 type = nullOr string;
99 description = "Use that host as 'main host' for acme certs";
100 default = null;
101 };
102 hosts = mkOption { type = listOf string; };
103 root = mkOption { type = nullOr path; };
104 extraConfig = mkOption { type = listOf lines; default = []; };
105 };
106 });
107 };
06782a20
IB
108 watchPaths = mkOption {
109 type = listOf string;
110 default = [];
111 description = ''
112 Paths to watch that should trigger a reload of httpd
113 '';
114 };
24fd1fe6
IB
115 };
116 });
117 };
118
119 config.services.httpd = let
120 redirectVhost = ips: { # Should go last, catchall http -> https redirect
121 listen = map (ip: { inherit ip; port = 80; }) ips;
122 hostName = "redirectSSL";
123 serverAliases = [ "*" ];
124 enableSSL = false;
125 logFormat = "combinedVhost";
126 documentRoot = "${config.security.acme.directory}/acme-challenge";
127 extraConfig = ''
128 RewriteEngine on
129 RewriteCond "%{REQUEST_URI}" "!^/\.well-known"
130 RewriteRule ^(.+) https://%{HTTP_HOST}$1 [R=301]
131 # To redirect in specific "VirtualHost *:80", do
132 # RedirectMatch 301 ^/((?!\.well-known.*$).*)$ https://host/$1
133 # rather than rewrite
134 '';
135 };
136 nosslVhost = ips: cfg: {
137 listen = map (ip: { inherit ip; port = 80; }) ips;
138 hostName = cfg.host;
139 enableSSL = false;
140 logFormat = "combinedVhost";
141 documentRoot = cfg.root;
142 extraConfig = ''
143 <Directory ${cfg.root}>
144 DirectoryIndex ${cfg.indexFile}
145 AllowOverride None
146 Require all granted
147
148 RewriteEngine on
149 RewriteRule ^/(.+) / [L]
150 </Directory>
151 '';
152 };
153 toVhost = ips: vhostConf: {
154 enableSSL = true;
155 sslServerCert = "${config.security.acme.directory}/${vhostConf.certName}/cert.pem";
156 sslServerKey = "${config.security.acme.directory}/${vhostConf.certName}/key.pem";
157 sslServerChain = "${config.security.acme.directory}/${vhostConf.certName}/chain.pem";
158 logFormat = "combinedVhost";
159 listen = map (ip: { inherit ip; port = 443; }) ips;
160 hostName = builtins.head vhostConf.hosts;
161 serverAliases = builtins.tail vhostConf.hosts or [];
162 documentRoot = vhostConf.root;
163 extraConfig = builtins.concatStringsSep "\n" vhostConf.extraConfig;
164 };
165 in attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
166 icfg.httpdName (mkIf icfg.enable {
167 enable = true;
168 listen = map (ip: { inherit ip; port = 443; }) icfg.ips;
169 stateDir = "/run/httpd_${name}";
170 logPerVirtualHost = true;
171 multiProcessingModule = "worker";
172 inherit (icfg) adminAddr;
173 logFormat = "combinedVhost";
174 extraModules = lists.unique icfg.modules;
175 extraConfig = builtins.concatStringsSep "\n" icfg.extraConfig;
176 virtualHosts = [ (toVhost icfg.ips icfg.fallbackVhost) ]
177 ++ optionals (icfg.nosslVhost.enable) [ (nosslVhost icfg.ips icfg.nosslVhost) ]
178 ++ (attrsets.mapAttrsToList (n: v: toVhost icfg.ips v) icfg.vhostConfs)
179 ++ [ (redirectVhost icfg.ips) ];
180 })
ea5b3f51 181 ) cfg.env;
24fd1fe6 182
06782a20
IB
183 config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair
184 "httpd${icfg.httpdName}" {
185 paths = icfg.watchPaths;
186 waitTime = 5;
187 }
ea5b3f51 188 ) cfg.env;
06782a20 189
24fd1fe6 190 config.security.acme.certs = let
ea5b3f51 191 typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env;
24fd1fe6
IB
192 flatVhosts = lists.flatten (attrsets.mapAttrsToList (k: v:
193 attrValues v.vhostConfs
194 ) typesToManage);
195 groupedCerts = attrsets.filterAttrs
196 (_: group: builtins.any (v: v.addToCerts || !isNull v.certMainHost) group)
197 (lists.groupBy (v: v.certName) flatVhosts);
198 groupToDomain = group:
199 let
200 nonNull = builtins.filter (v: !isNull v.certMainHost) group;
201 domains = lists.unique (map (v: v.certMainHost) nonNull);
202 in
203 if builtins.length domains == 0
204 then null
205 else assert (builtins.length domains == 1); (elemAt domains 0);
206 extraDomains = group:
207 let
208 mainDomain = groupToDomain group;
209 in
210 lists.remove mainDomain (
211 lists.unique (
212 lists.flatten (map (c: optionals (c.addToCerts || !isNull c.certMainHost) c.hosts) group)
213 )
214 );
215 in attrsets.mapAttrs (k: g:
216 if (!isNull (groupToDomain g))
ea5b3f51 217 then cfg.certs // {
24fd1fe6
IB
218 domain = groupToDomain g;
219 extraDomains = builtins.listToAttrs (
220 map (d: attrsets.nameValuePair d null) (extraDomains g));
221 }
222 else {
223 extraDomains = builtins.listToAttrs (
224 map (d: attrsets.nameValuePair d null) (extraDomains g));
225 }
226 ) groupedCerts;
ea5b3f51
IB
227
228 config.system.extraSystemBuilderCmds = lib.mkIf (builtins.length (builtins.attrValues cfg.webappDirs) > 0) ''
229 mkdir -p $out/webapps
230 ${builtins.concatStringsSep "\n" (attrsets.mapAttrsToList (name: path: "ln -s ${path} $out/webapps/${name}") cfg.webappDirs)}
231 '';
24fd1fe6 232}