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