1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6
7 cfg = config.services.prosody;
8
9 sslOpts = { ... }: {
10
11 options = {
12
13 key = mkOption {
14 type = types.path;
15 description = "Path to the key file.";
16 };
17
18 # TODO: rename to certificate to match the prosody config
19 cert = mkOption {
20 type = types.path;
21 description = "Path to the certificate file.";
22 };
23
24 extraOptions = mkOption {
25 type = types.attrs;
26 default = {};
27 description = "Extra SSL configuration options.";
28 };
29
30 };
31 };
32
33 moduleOpts = {
34 # Generally required
35 roster = mkOption {
36 type = types.bool;
37 default = true;
38 description = "Allow users to have a roster";
39 };
40
41 saslauth = mkOption {
42 type = types.bool;
43 default = true;
44 description = "Authentication for clients and servers. Recommended if you want to log in.";
45 };
46
47 tls = mkOption {
48 type = types.bool;
49 default = true;
50 description = "Add support for secure TLS on c2s/s2s connections";
51 };
52
53 dialback = mkOption {
54 type = types.bool;
55 default = true;
56 description = "s2s dialback support";
57 };
58
59 disco = mkOption {
60 type = types.bool;
61 default = true;
62 description = "Service discovery";
63 };
64
65 # Not essential, but recommended
66 carbons = mkOption {
67 type = types.bool;
68 default = true;
69 description = "Keep multiple clients in sync";
70 };
71
72 pep = mkOption {
73 type = types.bool;
74 default = true;
75 description = "Enables users to publish their mood, activity, playing music and more";
76 };
77
78 private = mkOption {
79 type = types.bool;
80 default = true;
81 description = "Private XML storage (for room bookmarks, etc.)";
82 };
83
84 blocklist = mkOption {
85 type = types.bool;
86 default = true;
87 description = "Allow users to block communications with other users";
88 };
89
90 vcard = mkOption {
91 type = types.bool;
92 default = true;
93 description = "Allow users to set vCards";
94 };
95
96 # Nice to have
97 version = mkOption {
98 type = types.bool;
99 default = true;
100 description = "Replies to server version requests";
101 };
102
103 uptime = mkOption {
104 type = types.bool;
105 default = true;
106 description = "Report how long server has been running";
107 };
108
109 time = mkOption {
110 type = types.bool;
111 default = true;
112 description = "Let others know the time here on this server";
113 };
114
115 ping = mkOption {
116 type = types.bool;
117 default = true;
118 description = "Replies to XMPP pings with pongs";
119 };
120
121 register = mkOption {
122 type = types.bool;
123 default = true;
124 description = "Allow users to register on this server using a client and change passwords";
125 };
126
127 mam = mkOption {
128 type = types.bool;
129 default = false;
130 description = "Store messages in an archive and allow users to access it";
131 };
132
133 # Admin interfaces
134 admin_adhoc = mkOption {
135 type = types.bool;
136 default = true;
137 description = "Allows administration via an XMPP client that supports ad-hoc commands";
138 };
139
140 admin_telnet = mkOption {
141 type = types.bool;
142 default = false;
143 description = "Opens telnet console interface on localhost port 5582";
144 };
145
146 # HTTP modules
147 bosh = mkOption {
148 type = types.bool;
149 default = false;
150 description = "Enable BOSH clients, aka 'Jabber over HTTP'";
151 };
152
153 websocket = mkOption {
154 type = types.bool;
155 default = false;
156 description = "Enable WebSocket support";
157 };
158
159 http_files = mkOption {
160 type = types.bool;
161 default = false;
162 description = "Serve static files from a directory over HTTP";
163 };
164
165 # Other specific functionality
166 limits = mkOption {
167 type = types.bool;
168 default = false;
169 description = "Enable bandwidth limiting for XMPP connections";
170 };
171
172 groups = mkOption {
173 type = types.bool;
174 default = false;
175 description = "Shared roster support";
176 };
177
178 server_contact_info = mkOption {
179 type = types.bool;
180 default = false;
181 description = "Publish contact information for this service";
182 };
183
184 announce = mkOption {
185 type = types.bool;
186 default = false;
187 description = "Send announcement to all online users";
188 };
189
190 welcome = mkOption {
191 type = types.bool;
192 default = false;
193 description = "Welcome users who register accounts";
194 };
195
196 watchregistrations = mkOption {
197 type = types.bool;
198 default = false;
199 description = "Alert admins of registrations";
200 };
201
202 motd = mkOption {
203 type = types.bool;
204 default = false;
205 description = "Send a message to users when they log in";
206 };
207
208 legacyauth = mkOption {
209 type = types.bool;
210 default = false;
211 description = "Legacy authentication. Only used by some old clients and bots";
212 };
213
214 proxy65 = mkOption {
215 type = types.bool;
216 default = false;
217 description = "Enables a file transfer proxy service which clients behind NAT can use";
218 };
219
220 };
221
222 toLua = x:
223 if builtins.isString x then ''"${x}"''
224 else if builtins.isBool x then (if x == true then "true" else "false")
225 else if builtins.isInt x then toString x
226 else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }''
227 else throw "Invalid Lua value";
228
229 createSSLOptsStr = o: ''
230 ssl = {
231 key = "${o.key}";
232 certificate = "${o.cert}";
233 ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)}
234 };
235 '';
236
237 vHostOpts = { ... }: {
238
239 options = {
240
241 # TODO: require attribute
242 domain = mkOption {
243 type = types.str;
244 description = "Domain name";
245 };
246
247 enabled = mkOption {
248 type = types.bool;
249 default = false;
250 description = "Whether to enable the virtual host";
251 };
252
253 ssl = mkOption {
254 type = types.nullOr (types.submodule sslOpts);
255 default = null;
256 description = "Paths to SSL files";
257 };
258
259 extraConfig = mkOption {
260 type = types.lines;
261 default = "";
262 description = "Additional virtual host specific configuration";
263 };
264
265 };
266
267 };
268
269in
270
271{
272
273 ###### interface
274
275 options = {
276
277 services.prosody = {
278
279 enable = mkOption {
280 type = types.bool;
281 default = false;
282 description = "Whether to enable the prosody server";
283 };
284
285 package = mkOption {
286 type = types.package;
287 description = "Prosody package to use";
288 default = pkgs.prosody;
289 defaultText = "pkgs.prosody";
290 example = literalExample ''
291 pkgs.prosody.override {
292 withExtraLibs = [ pkgs.luaPackages.lpty ];
293 withCommunityModules = [ "auth_external" ];
294 };
295 '';
296 };
297
298 dataDir = mkOption {
299 type = types.string;
300 description = "Directory where Prosody stores its data";
301 default = "/var/lib/prosody";
302 };
303
304 user = mkOption {
305 type = types.str;
306 default = "prosody";
307 description = "User account under which prosody runs.";
308 };
309
310 group = mkOption {
311 type = types.str;
312 default = "prosody";
313 description = "Group account under which prosody runs.";
314 };
315
316 allowRegistration = mkOption {
317 type = types.bool;
318 default = false;
319 description = "Allow account creation";
320 };
321
322 c2sRequireEncryption = mkOption {
323 type = types.bool;
324 default = true;
325 description = ''
326 Force clients to use encrypted connections? This option will
327 prevent clients from authenticating unless they are using encryption.
328 '';
329 };
330
331 s2sRequireEncryption = mkOption {
332 type = types.bool;
333 default = true;
334 description = ''
335 Force servers to use encrypted connections? This option will
336 prevent servers from authenticating unless they are using encryption.
337 Note that this is different from authentication.
338 '';
339 };
340
341 s2sSecureAuth = mkOption {
342 type = types.bool;
343 default = false;
344 description = ''
345 Force certificate authentication for server-to-server connections?
346 This provides ideal security, but requires servers you communicate
347 with to support encryption AND present valid, trusted certificates.
348 For more information see https://prosody.im/doc/s2s#security
349 '';
350 };
351
352 s2sInsecureDomains = mkOption {
353 type = types.listOf types.str;
354 default = [];
355 example = [ "insecure.example.com" ];
356 description = ''
357 Some servers have invalid or self-signed certificates. You can list
358 remote domains here that will not be required to authenticate using
359 certificates. They will be authenticated using DNS instead, even
360 when s2s_secure_auth is enabled.
361 '';
362 };
363
364 s2sSecureDomains = mkOption {
365 type = types.listOf types.str;
366 default = [];
367 example = [ "jabber.org" ];
368 description = ''
369 Even if you leave s2s_secure_auth disabled, you can still require valid
370 certificates for some domains by specifying a list here.
371 '';
372 };
373
374
375 modules = moduleOpts;
376
377 extraModules = mkOption {
378 type = types.listOf types.str;
379 default = [];
380 description = "Enable custom modules";
381 };
382
383 extraPluginPaths = mkOption {
384 type = types.listOf types.path;
385 default = [];
386 description = "Addtional path in which to look find plugins/modules";
387 };
388
389 virtualHosts = mkOption {
390
391 description = "Define the virtual hosts";
392
393 type = with types; loaOf (submodule vHostOpts);
394
395 example = {
396 myhost = {
397 domain = "my-xmpp-example-host.org";
398 enabled = true;
399 };
400 };
401
402 default = {
403 localhost = {
404 domain = "localhost";
405 enabled = true;
406 };
407 };
408
409 };
410
411 ssl = mkOption {
412 type = types.nullOr (types.submodule sslOpts);
413 default = null;
414 description = "Paths to SSL files";
415 };
416
417 admins = mkOption {
418 type = types.listOf types.str;
419 default = [];
420 example = [ "admin1@example.com" "admin2@example.com" ];
421 description = "List of administrators of the current host";
422 };
423
424 extraConfig = mkOption {
425 type = types.lines;
426 default = "";
427 description = "Additional prosody configuration";
428 };
429
430 };
431 };
432
433
434 ###### implementation
435
436 config = mkIf cfg.enable {
437
438 environment.systemPackages = [ cfg.package ];
439
440 environment.etc."prosody/prosody.cfg.lua".text = ''
441
442 pidfile = "/run/prosody/prosody.pid"
443
444 log = "*syslog"
445
446 data_path = "${cfg.dataDir}"
447 plugin_paths = {
448 ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) }
449 }
450
451 ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) }
452
453 admins = ${toLua cfg.admins}
454
455 -- we already build with libevent, so we can just enable it for a more performant server
456 use_libevent = true
457
458 modules_enabled = {
459
460 ${ lib.concatStringsSep "\n\ \ " (lib.mapAttrsToList
461 (name: val: optionalString val "${toLua name};")
462 cfg.modules) }
463 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
464 ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
465 };
466
467 allow_registration = ${toLua cfg.allowRegistration}
468
469 c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
470
471 s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
472
473 s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
474
475 s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
476
477 s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
478
479
480 ${ cfg.extraConfig }
481
482 ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: ''
483 VirtualHost "${v.domain}"
484 enabled = ${boolToString v.enabled};
485 ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) }
486 ${ v.extraConfig }
487 '') cfg.virtualHosts) }
488 '';
489
490 users.users.prosody = mkIf (cfg.user == "prosody") {
491 uid = config.ids.uids.prosody;
492 description = "Prosody user";
493 createHome = true;
494 inherit (cfg) group;
495 home = "${cfg.dataDir}";
496 };
497
498 users.groups.prosody = mkIf (cfg.group == "prosody") {
499 gid = config.ids.gids.prosody;
500 };
501
502 systemd.services.prosody = {
503 description = "Prosody XMPP server";
504 after = [ "network-online.target" ];
505 wants = [ "network-online.target" ];
506 wantedBy = [ "multi-user.target" ];
507 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
508 serviceConfig = {
509 User = cfg.user;
510 Group = cfg.group;
511 Type = "forking";
512 RuntimeDirectory = [ "prosody" ];
513 PIDFile = "/run/prosody/prosody.pid";
514 ExecStart = "${cfg.package}/bin/prosodyctl start";
515 };
516 };
517
518 };
519
520}