]>
Commit | Line | Data |
---|---|---|
daf64e3f IB |
1 | { lib, config, ... }: with lib; |
2 | let | |
29f8cb85 | 3 | cfg = config.services.websites; |
daf64e3f IB |
4 | in |
5 | { | |
29f8cb85 IB |
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 | }; | |
daf64e3f IB |
83 | }; |
84 | }; | |
85 | }; | |
29f8cb85 IB |
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 | }; | |
daf64e3f IB |
95 | }; |
96 | }; | |
29f8cb85 IB |
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 = []; }; | |
7df420c2 | 116 | }; |
29f8cb85 IB |
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 | }; | |
17f6eae9 | 126 | }; |
29f8cb85 IB |
127 | }); |
128 | }; | |
daf64e3f IB |
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"; | |
9ade8f6e | 138 | documentRoot = "${config.security.acme.directory}/acme-challenge"; |
daf64e3f IB |
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; | |
9ade8f6e IB |
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"; | |
daf64e3f IB |
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 | }) | |
f4da0504 | 193 | ) cfg.env; |
7df420c2 | 194 | |
17f6eae9 IB |
195 | config.services.filesWatcher = attrsets.mapAttrs' (name: icfg: attrsets.nameValuePair |
196 | "httpd${icfg.httpdName}" { | |
197 | paths = icfg.watchPaths; | |
198 | waitTime = 5; | |
199 | } | |
f4da0504 | 200 | ) cfg.env; |
17f6eae9 | 201 | |
7df420c2 | 202 | config.security.acme.certs = let |
f4da0504 | 203 | typesToManage = attrsets.filterAttrs (k: v: v.enable) cfg.env; |
7df420c2 IB |
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)) | |
f4da0504 | 229 | then cfg.certs // { |
7df420c2 IB |
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; | |
f4da0504 IB |
239 | |
240 | config.system.extraSystemBuilderCmds = lib.mkIf (builtins.length (builtins.attrValues cfg.webappDirs) > 0) '' | |
29f8cb85 IB |
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 | } | |
f4da0504 | 246 | ''; |
daf64e3f | 247 | } |