1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9let
10 cfg = config.services.prosody;
11
12 sslOpts = _: {
13 options = {
14 key = mkOption {
15 type = types.path;
16 description = "Path to the key file.";
17 };
18
19 # TODO: rename to certificate to match the prosody config
20 cert = mkOption {
21 type = types.path;
22 description = "Path to the certificate file.";
23 };
24
25 extraOptions = mkOption {
26 type = types.attrs;
27 default = { };
28 description = "Extra SSL configuration options.";
29 };
30 };
31 };
32
33 discoOpts = {
34 options = {
35 url = mkOption {
36 type = types.str;
37 description = "URL of the endpoint you want to make discoverable";
38 };
39 description = mkOption {
40 type = types.str;
41 description = "A short description of the endpoint you want to advertise";
42 };
43 };
44 };
45
46 moduleOpts = {
47 # Required for compliance with https://compliance.conversations.im/about/
48 roster = mkOption {
49 type = types.bool;
50 default = true;
51 description = "Allow users to have a roster";
52 };
53
54 saslauth = mkOption {
55 type = types.bool;
56 default = true;
57 description = "Authentication for clients and servers. Recommended if you want to log in.";
58 };
59
60 tls = mkOption {
61 type = types.bool;
62 default = true;
63 description = "Add support for secure TLS on c2s/s2s connections";
64 };
65
66 dialback = mkOption {
67 type = types.bool;
68 default = true;
69 description = "s2s dialback support";
70 };
71
72 disco = mkOption {
73 type = types.bool;
74 default = true;
75 description = "Service discovery";
76 };
77
78 # Not essential, but recommended
79 carbons = mkOption {
80 type = types.bool;
81 default = true;
82 description = "Keep multiple clients in sync";
83 };
84
85 csi = mkOption {
86 type = types.bool;
87 default = true;
88 description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server";
89 };
90
91 cloud_notify = mkOption {
92 type = types.bool;
93 default = true;
94 description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online";
95 };
96
97 pep = mkOption {
98 type = types.bool;
99 default = true;
100 description = "Enables users to publish their mood, activity, playing music and more";
101 };
102
103 private = mkOption {
104 type = types.bool;
105 default = true;
106 description = "Private XML storage (for room bookmarks, etc.)";
107 };
108
109 blocklist = mkOption {
110 type = types.bool;
111 default = true;
112 description = "Allow users to block communications with other users";
113 };
114
115 vcard = mkOption {
116 type = types.bool;
117 default = false;
118 description = "Allow users to set vCards";
119 };
120
121 vcard_legacy = mkOption {
122 type = types.bool;
123 default = true;
124 description = "Converts users profiles and Avatars between old and new formats";
125 };
126
127 bookmarks = mkOption {
128 type = types.bool;
129 default = true;
130 description = "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP";
131 };
132
133 # Nice to have
134 version = mkOption {
135 type = types.bool;
136 default = true;
137 description = "Replies to server version requests";
138 };
139
140 uptime = mkOption {
141 type = types.bool;
142 default = true;
143 description = "Report how long server has been running";
144 };
145
146 time = mkOption {
147 type = types.bool;
148 default = true;
149 description = "Let others know the time here on this server";
150 };
151
152 ping = mkOption {
153 type = types.bool;
154 default = true;
155 description = "Replies to XMPP pings with pongs";
156 };
157
158 register = mkOption {
159 type = types.bool;
160 default = true;
161 description = "Allow users to register on this server using a client and change passwords";
162 };
163
164 mam = mkOption {
165 type = types.bool;
166 default = true;
167 description = "Store messages in an archive and allow users to access it";
168 };
169
170 smacks = mkOption {
171 type = types.bool;
172 default = true;
173 description = "Allow a client to resume a disconnected session, and prevent message loss";
174 };
175
176 # Admin interfaces
177 admin_adhoc = mkOption {
178 type = types.bool;
179 default = true;
180 description = "Allows administration via an XMPP client that supports ad-hoc commands";
181 };
182
183 http_files = mkOption {
184 type = types.bool;
185 default = false;
186 description = "Serve static files from a directory over HTTP";
187 };
188
189 proxy65 = mkOption {
190 type = types.bool;
191 default = true;
192 description = "Enables a file transfer proxy service which clients behind NAT can use";
193 };
194
195 admin_telnet = mkOption {
196 type = types.bool;
197 default = false;
198 description = "Opens telnet console interface on localhost port 5582";
199 };
200
201 # HTTP modules
202 bosh = mkOption {
203 type = types.bool;
204 default = false;
205 description = "Enable BOSH clients, aka 'Jabber over HTTP'";
206 };
207
208 websocket = mkOption {
209 type = types.bool;
210 default = false;
211 description = "Enable WebSocket support";
212 };
213
214 # Other specific functionality
215 limits = mkOption {
216 type = types.bool;
217 default = false;
218 description = "Enable bandwidth limiting for XMPP connections";
219 };
220
221 groups = mkOption {
222 type = types.bool;
223 default = false;
224 description = "Shared roster support";
225 };
226
227 server_contact_info = mkOption {
228 type = types.bool;
229 default = false;
230 description = "Publish contact information for this service";
231 };
232
233 announce = mkOption {
234 type = types.bool;
235 default = false;
236 description = "Send announcement to all online users";
237 };
238
239 welcome = mkOption {
240 type = types.bool;
241 default = false;
242 description = "Welcome users who register accounts";
243 };
244
245 watchregistrations = mkOption {
246 type = types.bool;
247 default = false;
248 description = "Alert admins of registrations";
249 };
250
251 motd = mkOption {
252 type = types.bool;
253 default = false;
254 description = "Send a message to users when they log in";
255 };
256
257 legacyauth = mkOption {
258 type = types.bool;
259 default = false;
260 description = "Legacy authentication. Only used by some old clients and bots";
261 };
262 };
263
264 toLua =
265 x:
266 if builtins.isString x then
267 ''"${x}"''
268 else if builtins.isBool x then
269 boolToString x
270 else if builtins.isInt x then
271 toString x
272 else if builtins.isList x then
273 "{ ${lib.concatMapStringsSep ", " toLua x} }"
274 else
275 throw "Invalid Lua value";
276
277 settingsToLua =
278 prefix: settings:
279 generators.toKeyValue {
280 listsAsDuplicateKeys = false;
281 mkKeyValue =
282 k:
283 generators.mkKeyValueDefault {
284 mkValueString = toLua;
285 } " = " (prefix + k);
286 } (filterAttrs (k: v: v != null) settings);
287
288 createSSLOptsStr = o: ''
289 ssl = {
290 cafile = "/etc/ssl/certs/ca-bundle.crt";
291 key = "${o.key}";
292 certificate = "${o.cert}";
293 ${concatStringsSep "\n" (
294 mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions
295 )}
296 };
297 '';
298
299 mucOpts = _: {
300 options = {
301 domain = mkOption {
302 type = types.str;
303 description = "Domain name of the MUC";
304 };
305 name = mkOption {
306 type = types.str;
307 description = "The name to return in service discovery responses for the MUC service itself";
308 default = "Prosody Chatrooms";
309 };
310 restrictRoomCreation = mkOption {
311 type = types.enum [
312 true
313 false
314 "admin"
315 "local"
316 ];
317 default = false;
318 description = "Restrict room creation to server admins";
319 };
320 maxHistoryMessages = mkOption {
321 type = types.int;
322 default = 20;
323 description = "Specifies a limit on what each room can be configured to keep";
324 };
325 roomLocking = mkOption {
326 type = types.bool;
327 default = true;
328 description = ''
329 Enables room locking, which means that a room must be
330 configured before it can be used. Locked rooms are invisible
331 and cannot be entered by anyone but the creator
332 '';
333 };
334 roomLockTimeout = mkOption {
335 type = types.int;
336 default = 300;
337 description = ''
338 Timeout after which the room is destroyed or unlocked if not
339 configured, in seconds
340 '';
341 };
342 tombstones = mkOption {
343 type = types.bool;
344 default = true;
345 description = ''
346 When a room is destroyed, it leaves behind a tombstone which
347 prevents the room being entered or recreated. It also allows
348 anyone who was not in the room at the time it was destroyed
349 to learn about it, and to update their bookmarks. Tombstones
350 prevents the case where someone could recreate a previously
351 semi-anonymous room in order to learn the real JIDs of those
352 who often join there.
353 '';
354 };
355 tombstoneExpiry = mkOption {
356 type = types.int;
357 default = 2678400;
358 description = ''
359 This settings controls how long a tombstone is considered
360 valid. It defaults to 31 days. After this time, the room in
361 question can be created again.
362 '';
363 };
364 allowners_muc = mkOption {
365 type = types.bool;
366 default = false;
367 description = ''
368 Add module allowners, any user in chat is able to
369 kick other. Useful in jitsi-meet to kick ghosts.
370 '';
371 };
372 moderation = mkOption {
373 type = types.bool;
374 default = false;
375 description = "Allow rooms to be moderated";
376 };
377
378 # Extra parameters. Defaulting to prosody default values.
379 # Adding them explicitly to make them visible from the options
380 # documentation.
381 #
382 # See https://prosody.im/doc/modules/mod_muc for more details.
383 roomDefaultPublic = mkOption {
384 type = types.bool;
385 default = true;
386 description = "If set, the MUC rooms will be public by default.";
387 };
388 roomDefaultMembersOnly = mkOption {
389 type = types.bool;
390 default = false;
391 description = "If set, the MUC rooms will only be accessible to the members by default.";
392 };
393 roomDefaultModerated = mkOption {
394 type = types.bool;
395 default = false;
396 description = "If set, the MUC rooms will be moderated by default.";
397 };
398 roomDefaultPublicJids = mkOption {
399 type = types.bool;
400 default = false;
401 description = "If set, the MUC rooms will display the public JIDs by default.";
402 };
403 roomDefaultChangeSubject = mkOption {
404 type = types.bool;
405 default = false;
406 description = "If set, the rooms will display the public JIDs by default.";
407 };
408 roomDefaultHistoryLength = mkOption {
409 type = types.int;
410 default = 20;
411 description = "Number of history message sent to participants by default.";
412 };
413 roomDefaultLanguage = mkOption {
414 type = types.str;
415 default = "en";
416 description = "Default room language.";
417 };
418 extraConfig = mkOption {
419 type = types.lines;
420 default = "";
421 description = "Additional MUC specific configuration";
422 };
423 };
424 };
425
426 httpFileShareOpts =
427 { config, options, ... }:
428 {
429 freeformType =
430 with types;
431 let
432 atom = oneOf [
433 int
434 bool
435 str
436 (listOf atom)
437 ];
438 in
439 attrsOf (nullOr atom)
440 // {
441 description = "int, bool, string or list of them";
442 };
443 options = {
444 domain = mkOption {
445 type = with types; nullOr str;
446 description = "Domain name for a http_file_share service.";
447 };
448 http_host = mkOption {
449 type = types.nullOr types.str;
450 default = null;
451 description = ''
452 To avoid an additional DNS record and certificate, you may set this option to your primary domain (e.g. "example.com")
453 or use a reverse proxy to handle the HTTP for that domain.
454 '';
455 };
456 size_limit = mkOption {
457 type = types.int;
458 default = 10 * 1024 * 1024;
459 defaultText = "10 * 1024 * 1024";
460 description = "Maximum file size, in bytes.";
461 };
462 expires_after = mkOption {
463 type = types.str;
464 default = "1 week";
465 description = "Max age of a file before it gets deleted.";
466 };
467 daily_quota = mkOption {
468 type = types.nullOr types.int;
469 default = 10 * config.size_limit;
470 defaultText = lib.literalExpression "10 * ${options.size_limit}";
471 example = "100*1024*1024";
472 description = ''
473 Maximum size of daily uploaded files per user, in bytes.
474 '';
475 };
476 };
477 };
478
479 vHostOpts = _: {
480 options = {
481 # TODO: require attribute
482 domain = mkOption {
483 type = types.str;
484 description = "Domain name";
485 };
486
487 enabled = mkOption {
488 type = types.bool;
489 default = false;
490 description = "Whether to enable the virtual host";
491 };
492
493 ssl = mkOption {
494 type = types.nullOr (types.submodule sslOpts);
495 default = null;
496 description = "Paths to SSL files";
497 };
498
499 extraConfig = mkOption {
500 type = types.lines;
501 default = "";
502 description = "Additional virtual host specific configuration";
503 };
504 };
505 };
506
507 configFile =
508 let
509 httpDiscoItems = optional (cfg.httpFileShare != null) {
510 url = cfg.httpFileShare.domain;
511 description = "HTTP file share endpoint";
512 };
513 mucDiscoItems = builtins.foldl' (
514 acc: muc:
515 [
516 {
517 url = muc.domain;
518 description = "${muc.domain} MUC endpoint";
519 }
520 ]
521 ++ acc
522 ) [ ] cfg.muc;
523 discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems;
524 in
525 pkgs.writeText "prosody.cfg.lua" ''
526 pidfile = "/run/prosody/prosody.pid"
527
528 log = ${cfg.log}
529
530 data_path = "${cfg.dataDir}"
531 plugin_paths = {
532 ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths)}
533 }
534
535 ${optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl)}
536
537 admins = ${toLua cfg.admins}
538
539 modules_enabled = {
540 "admin_shell"; -- for prosodyctl
541 ${lib.concatStringsSep "\n " (
542 lib.mapAttrsToList (name: val: optionalString val "${toLua name};") cfg.modules
543 )}
544 ${lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)}
545 ${lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)}
546 };
547
548 disco_items = {
549 ${lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)}
550 };
551
552 allow_registration = ${toLua cfg.allowRegistration}
553
554 c2s_require_encryption = ${toLua cfg.c2sRequireEncryption}
555
556 s2s_require_encryption = ${toLua cfg.s2sRequireEncryption}
557 s2s_secure_auth = ${toLua cfg.s2sSecureAuth}
558 s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains}
559 s2s_secure_domains = ${toLua cfg.s2sSecureDomains}
560
561 authentication = ${toLua cfg.authentication}
562
563 http_interfaces = ${toLua cfg.httpInterfaces}
564 https_interfaces = ${toLua cfg.httpsInterfaces}
565
566 http_ports = ${toLua cfg.httpPorts}
567 https_ports = ${toLua cfg.httpsPorts}
568
569 mime_types_file = "${pkgs.mailcap}/etc/mime.types"
570
571 ${cfg.extraConfig}
572
573 ${lib.concatMapStrings (muc: ''
574 Component ${toLua muc.domain} "muc"
575 modules_enabled = {${optionalString cfg.modules.mam ''"muc_mam",''}${optionalString muc.allowners_muc ''"muc_allowners",''}${optionalString muc.moderation ''"muc_moderation",''} }
576 name = ${toLua muc.name}
577 restrict_room_creation = ${toLua muc.restrictRoomCreation}
578 max_history_messages = ${toLua muc.maxHistoryMessages}
579 muc_room_locking = ${toLua muc.roomLocking}
580 muc_room_lock_timeout = ${toLua muc.roomLockTimeout}
581 muc_tombstones = ${toLua muc.tombstones}
582 muc_tombstone_expiry = ${toLua muc.tombstoneExpiry}
583 muc_room_default_public = ${toLua muc.roomDefaultPublic}
584 muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly}
585 muc_room_default_moderated = ${toLua muc.roomDefaultModerated}
586 muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids}
587 muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject}
588 muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength}
589 muc_room_default_language = ${toLua muc.roomDefaultLanguage}
590 ${muc.extraConfig}
591 '') cfg.muc}
592
593 ${lib.optionalString (cfg.httpFileShare != null) ''
594 Component ${toLua cfg.httpFileShare.domain} "http_file_share"
595 modules_disabled = { "s2s" }
596 ${lib.optionalString (cfg.httpFileShare.http_host != null) ''
597 http_host = "${cfg.httpFileShare.http_host}"
598 ''}
599 ${settingsToLua " http_file_share_" (cfg.httpFileShare // { domain = null; })}
600 ''}
601
602 ${lib.concatStringsSep "\n" (
603 lib.mapAttrsToList (n: v: ''
604 VirtualHost "${v.domain}"
605 enabled = ${boolToString v.enabled};
606 ${optionalString (v.ssl != null) (createSSLOptsStr v.ssl)}
607 ${v.extraConfig}
608 '') cfg.virtualHosts
609 )}
610 '';
611in
612{
613 options = {
614 services.prosody = {
615 enable = mkOption {
616 type = types.bool;
617 default = false;
618 description = "Whether to enable the prosody server";
619 };
620
621 checkConfig = mkOption {
622 type = types.bool;
623 default = true;
624 example = false;
625 description = "Check the configuration file with `prosodyctl check config`";
626 };
627
628 xmppComplianceSuite = mkOption {
629 type = types.bool;
630 default = true;
631 description = ''
632 The XEP-0423 defines a set of recommended XEPs to implement
633 for a server. It's generally a good idea to implement this
634 set of extensions if you want to provide your users with a
635 good XMPP experience.
636
637 This NixOS module aims to provide a "advanced server"
638 experience as per defined in the XEP-0423[1] specification.
639
640 Setting this option to true will prevent you from building a
641 NixOS configuration which won't comply with this standard.
642 You can explicitly decide to ignore this standard if you
643 know what you are doing by setting this option to false.
644
645 [1] https://xmpp.org/extensions/xep-0423.html
646 '';
647 };
648
649 package = mkPackageOption pkgs "prosody" {
650 example = ''
651 pkgs.prosody.override {
652 withExtraLibs = [ pkgs.luaPackages.lpty ];
653 withCommunityModules = [ "auth_external" ];
654 };
655 '';
656 };
657
658 dataDir = mkOption {
659 type = types.path;
660 default = "/var/lib/prosody";
661 description = ''
662 The prosody home directory used to store all data. If left as the default value
663 this directory will automatically be created before the prosody server starts, otherwise
664 you are responsible for ensuring the directory exists with appropriate ownership
665 and permissions.
666 '';
667 };
668
669 disco_items = mkOption {
670 type = types.listOf (types.submodule discoOpts);
671 default = [ ];
672 description = "List of discoverable items you want to advertise.";
673 };
674
675 user = mkOption {
676 type = types.str;
677 default = "prosody";
678 description = ''
679 User account under which prosody runs.
680
681 ::: {.note}
682 If left as the default value this user will automatically be created
683 on system activation, otherwise you are responsible for
684 ensuring the user exists before the prosody service starts.
685 :::
686 '';
687 };
688
689 group = mkOption {
690 type = types.str;
691 default = "prosody";
692 description = ''
693 Group account under which prosody runs.
694
695 ::: {.note}
696 If left as the default value this group will automatically be created
697 on system activation, otherwise you are responsible for
698 ensuring the group exists before the prosody service starts.
699 :::
700 '';
701 };
702
703 allowRegistration = mkOption {
704 type = types.bool;
705 default = false;
706 description = "Allow account creation";
707 };
708
709 # HTTP server-related options
710 httpPorts = mkOption {
711 type = types.listOf types.port;
712 description = "Listening HTTP ports list for this service.";
713 default = [ 5280 ];
714 };
715
716 httpInterfaces = mkOption {
717 type = types.listOf types.str;
718 default = [
719 "*"
720 "::"
721 ];
722 description = "Interfaces on which the HTTP server will listen on.";
723 };
724
725 httpsPorts = mkOption {
726 type = types.listOf types.port;
727 description = "Listening HTTPS ports list for this service.";
728 default = [ 5281 ];
729 };
730
731 httpsInterfaces = mkOption {
732 type = types.listOf types.str;
733 default = [
734 "*"
735 "::"
736 ];
737 description = "Interfaces on which the HTTPS server will listen on.";
738 };
739
740 c2sRequireEncryption = mkOption {
741 type = types.bool;
742 default = true;
743 description = ''
744 Force clients to use encrypted connections? This option will
745 prevent clients from authenticating unless they are using encryption.
746 '';
747 };
748
749 s2sRequireEncryption = mkOption {
750 type = types.bool;
751 default = true;
752 description = ''
753 Force servers to use encrypted connections? This option will
754 prevent servers from authenticating unless they are using encryption.
755 Note that this is different from authentication.
756 '';
757 };
758
759 s2sSecureAuth = mkOption {
760 type = types.bool;
761 default = false;
762 description = ''
763 Force certificate authentication for server-to-server connections?
764 This provides ideal security, but requires servers you communicate
765 with to support encryption AND present valid, trusted certificates.
766 For more information see <https://prosody.im/doc/s2s#security>
767 '';
768 };
769
770 s2sInsecureDomains = mkOption {
771 type = types.listOf types.str;
772 default = [ ];
773 example = [ "insecure.example.com" ];
774 description = ''
775 Some servers have invalid or self-signed certificates. You can list
776 remote domains here that will not be required to authenticate using
777 certificates. They will be authenticated using DNS instead, even
778 when s2s_secure_auth is enabled.
779 '';
780 };
781
782 s2sSecureDomains = mkOption {
783 type = types.listOf types.str;
784 default = [ ];
785 example = [ "jabber.org" ];
786 description = ''
787 Even if you leave s2s_secure_auth disabled, you can still require valid
788 certificates for some domains by specifying a list here.
789 '';
790 };
791
792 modules = moduleOpts;
793
794 extraModules = mkOption {
795 type = types.listOf types.str;
796 default = [ ];
797 description = "Enable custom modules";
798 };
799
800 extraPluginPaths = mkOption {
801 type = types.listOf types.path;
802 default = [ ];
803 description = "Additional path in which to look find plugins/modules";
804 };
805
806 httpFileShare = mkOption {
807 description = ''
808 Configures the http_file_share module to handle user uploads.
809
810 See <https://prosody.im/doc/modules/mod_http_file_share> for a full list of options.
811 '';
812 type = types.nullOr (types.submodule httpFileShareOpts);
813 default = null;
814 example = {
815 domain = "uploads.my-xmpp-example-host.org";
816 };
817 };
818
819 muc = mkOption {
820 type = types.listOf (types.submodule mucOpts);
821 default = [ ];
822 example = [
823 {
824 domain = "conference.my-xmpp-example-host.org";
825 }
826 ];
827 description = "Multi User Chat (MUC) configuration";
828 };
829
830 virtualHosts = mkOption {
831
832 description = "Define the virtual hosts";
833
834 type = with types; attrsOf (submodule vHostOpts);
835
836 example = {
837 myhost = {
838 domain = "my-xmpp-example-host.org";
839 enabled = true;
840 };
841 };
842
843 default = {
844 localhost = {
845 domain = "localhost";
846 enabled = true;
847 };
848 };
849
850 };
851
852 ssl = mkOption {
853 type = types.nullOr (types.submodule sslOpts);
854 default = null;
855 description = "Paths to SSL files";
856 };
857
858 admins = mkOption {
859 type = types.listOf types.str;
860 default = [ ];
861 example = [
862 "admin1@example.com"
863 "admin2@example.com"
864 ];
865 description = "List of administrators of the current host";
866 };
867
868 authentication = mkOption {
869 type = types.enum [
870 "internal_plain"
871 "internal_hashed"
872 "cyrus"
873 "anonymous"
874 ];
875 default = "internal_hashed";
876 example = "internal_plain";
877 description = "Authentication mechanism used for logins.";
878 };
879
880 extraConfig = mkOption {
881 type = types.lines;
882 default = "";
883 description = ''
884 Additional prosody configuration
885
886 The generated file is processed by `envsubst` to allow secrets to be passed securely via environment variables.
887 '';
888 };
889
890 log = mkOption {
891 type = types.lines;
892 default = ''"*syslog"'';
893 description = "Logging configuration. See [](https://prosody.im/doc/logging) for more details";
894 example = ''
895 {
896 { min = "warn"; to = "*syslog"; };
897 }
898 '';
899 };
900 };
901 };
902
903 imports = [
904 (lib.mkRemovedOptionModule [ "services" "prosody" "uploadHttp" ]
905 "mod_http_upload has been obsoloted and been replaced by mod_http_file_share which can be configured with httpFileShare options."
906 )
907 ];
908
909 config = mkIf cfg.enable {
910 assertions =
911 let
912 genericErrMsg = ''
913
914 Having a server not XEP-0423-compliant might make your XMPP
915 experience terrible. See the NixOS manual for further
916 information.
917
918 If you know what you're doing, you can disable this warning by
919 setting config.services.prosody.xmppComplianceSuite to false.
920 '';
921 errors = [
922 {
923 assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite;
924 message = ''
925 You need to setup at least a MUC domain to comply with
926 XEP-0423.
927 ''
928 + genericErrMsg;
929 }
930 {
931 assertion = cfg.httpFileShare != null || !cfg.xmppComplianceSuite;
932 message = ''
933 You need to setup http_file_share modules through config.services.prosody.httpFileShare to comply with XEP-0423.
934 ''
935 + genericErrMsg;
936 }
937 ];
938 in
939 errors;
940
941 environment.systemPackages = [ cfg.package ];
942
943 # prevent error if not all certs are configured by the user
944 environment.etc."prosody/certs/.dummy".text = "";
945
946 environment.etc."prosody/prosody.cfg.lua".source =
947 if cfg.checkConfig then
948 pkgs.runCommandLocal "prosody.cfg.lua"
949 {
950 nativeBuildInputs = [ cfg.package ];
951 }
952 ''
953 cp ${configFile} prosody.cfg.lua
954 # Replace the hardcoded path to cacerts with one that is accessible in the build sandbox
955 sed 's|/etc/ssl/certs/ca-bundle.crt|${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt|' -i prosody.cfg.lua
956 # For some reason prosody hard fails to "find" certificates when this directory does not exist
957 mkdir certs
958 prosodyctl --config ./prosody.cfg.lua check config
959 cp prosody.cfg.lua $out
960 ''
961 else
962 configFile;
963
964 users.users.prosody = mkIf (cfg.user == "prosody") {
965 uid = config.ids.uids.prosody;
966 description = "Prosody user";
967 inherit (cfg) group;
968 home = cfg.dataDir;
969 };
970
971 users.groups.prosody = mkIf (cfg.group == "prosody") {
972 gid = config.ids.gids.prosody;
973 };
974
975 systemd.services.prosody = {
976 description = "Prosody XMPP server";
977 after = [ "network-online.target" ];
978 wants = [ "network-online.target" ];
979 wantedBy = [ "multi-user.target" ];
980 restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
981 preStart = ''
982 ${pkgs.envsubst}/bin/envsubst -i ${
983 config.environment.etc."prosody/prosody.cfg.lua".source
984 } -o /run/prosody/prosody.cfg.lua
985 '';
986 serviceConfig = mkMerge [
987 {
988 User = cfg.user;
989 Group = cfg.group;
990 Type = "simple";
991 RuntimeDirectory = "prosody";
992 PIDFile = "/run/prosody/prosody.pid";
993 Environment = "PROSODY_CONFIG=/run/prosody/prosody.cfg.lua";
994 ExecStart = "${lib.getExe cfg.package} -F";
995 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
996 Restart = "on-abnormal";
997
998 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
999 MemoryDenyWriteExecute = true;
1000 PrivateDevices = true;
1001 PrivateMounts = true;
1002 PrivateTmp = true;
1003 ProtectControlGroups = true;
1004 ProtectHome = true;
1005 ProtectHostname = true;
1006 ProtectKernelModules = true;
1007 ProtectKernelTunables = true;
1008 RestrictNamespaces = true;
1009 RestrictRealtime = true;
1010 RestrictSUIDSGID = true;
1011 }
1012 (mkIf (cfg.dataDir == "/var/lib/prosody") {
1013 StateDirectory = "prosody";
1014 })
1015 ];
1016 };
1017 };
1018
1019 meta.doc = ./prosody.md;
1020}