1 { options, config, pkgs, lib, ... }:
 
   3   inherit (lib) isAttrs isBool isList isInt isString;
 
   4   inherit (lib) mkChangedOptionModule mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkRenamedOptionModule;
 
   5   inherit (lib) all any attrValues boolToString concatStringsSep elem filterAttrs genAttrs hasPrefix listToAttrs literalExpression mapAttrsToList mdDoc optionals optionalAttrs optionalString types unique;
 
   8   cfg = config.services.prosody;
 
  10   luaType = with types; let
 
  21       description = "Lua value";
 
  26   sslOption = mkOption {
 
  27     type = with types; nullOr (submodule {
 
  28       freeformType = luaType;
 
  33           description = mdDoc ''
 
  34             Path to your private key file.
 
  38         certificate = mkOption {
 
  40           description = mdDoc ''
 
  41             Path to your certificate file.
 
  47           default = "/etc/ssl/certs/ca-bundle.crt";
 
  48           description = mdDoc ''
 
  49             Path to a file containing root certificates that you wish Prosody to trust.
 
  56     description = mdDoc ''
 
  57       Advanced SSL/TLS configuration, as described by <https://prosody.im/doc/advanced_ssl_config>.
 
  60       Prosody passes the contents of the `ssl` option from the config file almost directly to LuaSec, the
 
  61       library used for SSL/TLS support in Prosody. LuaSec accepts a range of options here, mostly things
 
  62       that it passes directly to OpenSSL. It is recommended to leave Prosody's defaults in most cases, unless
 
  63       you know what you are doing.
 
  64       You could easily reduce security or introduce unnecessary compatibility issues with clients and other servers!
 
  69   componentsOption = mkOption {
 
  70     type = with types; attrsOf (submodule ({ name, config, ... }: {
 
  73           type = with types; nullOr str;
 
  75           description = mdDoc ''
 
  76             The name of the plugin you wish to use for the component if internal, or `null` if the
 
  77             component is an external component.
 
  84           description = mdDoc ''
 
  85             Values specified here are applied to a specific component. Refer to <https://prosody.im/doc/components>
 
  86             for additional details.
 
  90         extraConfig = mkOption {
 
  93           description = mdDoc ''
 
  94             Additional component specific configuration.
 
 100       config.settings = mkMerge [
 
 101         (mkIf (config.module == "http_upload") {
 
 102           http_upload_path = mkIf (config.module == "http_upload_path") cfg.dataDir;
 
 104         (mkIf (config.module == "muc") {
 
 105           modules_enabled = [ "muc_mam" ];
 
 110     description = mdDoc ''
 
 111       Components are extra services on a server which are available to clients, usually on a subdomain
 
 112       of the main server (such as mycomponent.example.com). Example components might be chatroom
 
 113       servers, user directories, or gateways to other protocols.
 
 115       See <https://prosody.im/doc/components> for details.
 
 119   acmeHosts = unique (mapAttrsToList (domain: hostOpts: hostOpts.useACMEHost) (filterAttrs (_: v: v.useACMEHost != null) cfg.virtualHosts));
 
 123     (mkRenamedOptionModule [ "services" "prosody" "allowRegistration" ] [ "services" "prosody" "settings" "allow_registration" ])
 
 124     (mkRenamedOptionModule [ "services" "prosody" "httpPorts" ] [ "services" "prosody" "settings" "http_ports" ])
 
 125     (mkRenamedOptionModule [ "services" "prosody" "httpInterfaces" ] [ "services" "prosody" "settings" "http_interfaces" ])
 
 126     (mkRenamedOptionModule [ "services" "prosody" "httpsPorts" ] [ "services" "prosody" "settings" "https_ports" ])
 
 127     (mkRenamedOptionModule [ "services" "prosody" "httpsInterfaces" ] [ "services" "prosody" "settings" "https_interfaces" ])
 
 128     (mkRenamedOptionModule [ "services" "prosody" "c2sRequireEncryption" ] [ "services" "prosody" "settings" "c2s_require_encryption" ])
 
 129     (mkRenamedOptionModule [ "services" "prosody" "s2sRequireEncryption" ] [ "services" "prosody" "settings" "s2s_require_encryption" ])
 
 130     (mkRenamedOptionModule [ "services" "prosody" "s2sSecureAuth" ] [ "services" "prosody" "settings" "s2s_secure_auth" ])
 
 131     (mkRenamedOptionModule [ "services" "prosody" "s2sInsecureDomains" ] [ "services" "prosody" "settings" "s2s_insecure_domains" ])
 
 132     (mkRenamedOptionModule [ "services" "prosody" "s2sSecureDomains" ] [ "services" "prosody" "settings" "s2s_secure_domains" ])
 
 133     (mkRenamedOptionModule [ "services" "prosody" "extraModules" ] [ "services" "prosody" "settings" "modules_enabled" ])
 
 134     (mkRenamedOptionModule [ "services" "prosody" "extraPluginPaths" ] [ "services" "prosody" "settings" "plugin_paths" ])
 
 135     (mkRenamedOptionModule [ "services" "prosody" "ssl" "key" ] [ "services" "prosody" "settings" "ssl" "key" ])
 
 136     (mkRenamedOptionModule [ "services" "prosody" "ssl" "cert" ] [ "services" "prosody" "settings" "ssl" "certificate" ])
 
 137     (mkRenamedOptionModule [ "services" "prosody" "ssl" "extraOptions" ] [ "services" "prosody" "settings" "ssl" ])
 
 138     (mkRenamedOptionModule [ "services" "prosody" "admins" ] [ "services" "prosody" "settings" "admins" ])
 
 139     (mkRenamedOptionModule [ "services" "prosody" "authentication" ] [ "services" "prosody" "settings" "authentication" ])
 
 140     (mkChangedOptionModule [ "services" "prosody" "disco_items" ] [ "services" "prosody" "settings" "disco_items" ] (config:
 
 141       map ({ url, description }: [ url description ]) config.services.prosody.disco_items
 
 143     (mkChangedOptionModule [ "services" "prosody" "uploadHttp" ] [ "services" "prosody" "components" ] (config:
 
 145         cfg = config.services.prosody;
 
 148         ${cfg.uploadHttp.domain} = {
 
 149           module = "http_upload";
 
 150           # these values are to preserve compatibility with this module pre nixos 23.11
 
 152             http_upload_file_size_limit = { __compat = true; value = cfg.uploadHttp.uploadFileSizeLimit or "50 * 1024 * 1024"; };
 
 153             http_upload_expire_after = { __compat = true; value = cfg.uploadHttp.uploadExpireAfter or "60 * 60 * 24 * 7"; };
 
 154             http_upload_path = cfg.uploadHttp.httpUploadPath or "/var/lib/prosody";
 
 155           } // optionalAttrs (cfg.uploadHttp ? userQuota) {
 
 156             http_upload_quota = cfg.uploadHttp.userQuota;
 
 161     (mkChangedOptionModule [ "services" "prosody" "muc" ] [ "services" "prosody" "components" ] (config:
 
 167             extraConfig = muc.extraConfig or "";
 
 168             # these values are to preserve compatibility with this module pre nixos 23.11
 
 170               modules_enabled = optionals (muc.vcard_muc or true) [ "vcard_muc" ];
 
 171               name = muc.name or "Prosody Chatrooms";
 
 172               restrict_room_creation = muc.restrictRoomCreation or "local";
 
 173               max_history_messages = muc.maxHistoryMessages or 20;
 
 174               muc_room_locking = muc.roomLocking or true;
 
 175               muc_room_lock_timeout = muc.roomLockTimeout or 300;
 
 176               muc_tombstones = muc.tombstones or true;
 
 177               muc_tombstone_expiry = muc.tombstoneExpiry or 2678400;
 
 178               muc_room_default_public = muc.roomDefaultPublic or true;
 
 179               muc_room_default_members_only = muc.roomDefaultMembersOnly or false;
 
 180               muc_room_default_moderated = muc.roomDefaultModerated or false;
 
 181               muc_room_default_public_jids = muc.roomDefaultPublicJids or false;
 
 182               muc_room_default_change_subject = muc.roomDefaultChangeSubject or false;
 
 183               muc_room_default_history_length = muc.roomDefaultHistoryLength or 20;
 
 184               muc_room_default_language = muc.roomDefaultLanguage or "en";
 
 188         config.services.prosody.muc)
 
 192   options.services.prosody = {
 
 193     enable = mkEnableOption "Prosody, the modern XMPP communication server";
 
 195     xmppComplianceSuite = mkOption {
 
 198       description = mdDoc ''
 
 199         The XEP-0423 defines a set of recommended XEPs to implement
 
 200         for a server. It's generally a good idea to implement this
 
 201         set of extensions if you want to provide your users with a
 
 202         good XMPP experience.
 
 204         This NixOS module aims to provide an "advanced server"
 
 205         experience as per defined in the [XEP-0423](https://xmpp.org/extensions/xep-0423.html) specification.
 
 207         Setting this option to `true` will prevent you from building a
 
 208         NixOS configuration which won't comply with this standard.
 
 209         You can explicitly decide to ignore this standard if you
 
 210         know what you are doing by setting this option to `false`.
 
 215       type = types.package;
 
 216       description = mdDoc "Prosody package to use.";
 
 217       default = pkgs.prosody;
 
 218       defaultText = literalExpression "pkgs.prosody";
 
 219       example = literalExpression ''
 
 220         pkgs.prosody.override {
 
 221           withExtraLibs = [ pkgs.luaPackages.lpty ];
 
 222           withCommunityModules = [ "auth_external" ];
 
 229       default = "/var/lib/prosody";
 
 230       description = mdDoc ''
 
 231         The Prosody home directory used to store all data.
 
 234         If left as the default value this directory will automatically be created
 
 235         before the Prosody server starts, otherwise you are responsible for
 
 236         ensuring the directory exists with appropriate ownership and permissions.
 
 244       description = mdDoc ''
 
 245         User account under which Prosody runs.
 
 248         If left as the default value this user will automatically be created
 
 249         on system activation, otherwise you are responsible for
 
 250         ensuring the user exists before the Prosody service starts.
 
 258       description = mdDoc ''
 
 259         Group account under which Prosody runs.
 
 262         If left as the default value this group will automatically be created
 
 263         on system activation, otherwise you are responsible for
 
 264         ensuring the group exists before the Prosody service starts.
 
 269     environmentFile = mkOption {
 
 270       type = with types; nullOr path;
 
 272       description = mdDoc ''
 
 273         File which may contain secrets in the format of an EnvironmentFile as described
 
 274         by systemd.exec(5). Variables here can be used in the Prosody configuration
 
 275         file by prefixing the environment variable name with `ENV_` in the configuration.
 
 278         Environment variables in this file should *not* begin with an `ENV_` prefix, only
 
 279         when referenced in the Prosody configuration file.
 
 284     openFirewall = mkOption {
 
 287       description = mdDoc ''
 
 288         Whether to automatically open ports specified by the following options:
 
 290         - {option}`services.prosody.settings.c2s_ports`
 
 291         - {option}`services.prosody.settings.s2s_ports`
 
 292         - {option}`services.prosody.settings.https_ports`
 
 296     settings = mkOption {
 
 297       type = types.submodule {
 
 298         freeformType = luaType;
 
 301           modules_enabled = mkOption {
 
 302             type = with types; listOf str;
 
 305             description = mdDoc ''
 
 306               List of modules to load for all virtual hosts.
 
 310           modules_disabled = mkOption {
 
 311             type = with types; listOf str;
 
 314             description = mdDoc ''
 
 315               Allows you to disable the loading of a list of modules for all virtual hosts
 
 316               if those modules are set in the global settings.
 
 325       description = mdDoc ''
 
 326         Values specified here are applied to the whole server, and are the default for all
 
 327         virtual hosts. Refer to <https://prosody.im/doc/configure> for additional details.
 
 330         It's also possible to refer to environment variables (defined in
 
 331         [services.prosody.environmentFile](#opt-services.prosody.environmentFile)) using the
 
 332         syntax `"ENV_VARIABLE_NAME"` where the environment file contains a `VARIABLE_NAME` entry.
 
 335       example = literalExpression ''
 
 337           modules_enabled = [ "turn_external" ];
 
 338           turn_external_host = "turn.example.com";
 
 339           turn_external_port = 3478;
 
 340           turn_external_secret = "ENV_TURN_EXTERNAL_SECRET";
 
 345     components = componentsOption;
 
 347     virtualHosts = let config' = config; in mkOption {
 
 348       type = with types; attrsOf (submodule ({ name, config, ... }: {
 
 353             description = mdDoc "Domain name.";
 
 356           useACMEHost = mkOption {
 
 357             type = with types; nullOr str;
 
 359             description = mdDoc ''
 
 360               A host of an existing Let's Encrypt certificate to use.
 
 363               Note that this option does not create any certificates, nor it does add subdomains to existing
 
 364               ones – you will need to create them manually using [](#opt-security.acme.certs).
 
 369           components = componentsOption;
 
 371           settings = mkOption {
 
 372             type = types.submodule {
 
 373               freeformType = luaType;
 
 379                   description = mdDoc ''
 
 380                     Specifies whether this host is enabled or not. Disabled hosts are not loaded and
 
 381                     do not accept connections while Prosody is running.
 
 385                 modules_enabled = mkOption {
 
 386                   type = with types; listOf str;
 
 389                   description = mdDoc ''
 
 390                     List of modules to load for the virtual host.
 
 394                 modules_disabled = mkOption {
 
 395                   type = with types; listOf str;
 
 398                   description = mdDoc ''
 
 399                     Allows you to disable the loading of a list of modules for a particular host.
 
 407             description = mdDoc ''
 
 408               Values specified here are applied to a specific virtual host and will override values set
 
 409               in the global [settings](#opt-services.prosody.settings) option. Refer to <https://prosody.im/doc/configure>
 
 410               for additional details.
 
 414           extraConfig = mkOption {
 
 417             description = mdDoc ''
 
 418               Additional virtual host specific configuration.
 
 422           # options to preserve compatibility with this module pre nixos 23.11
 
 425             type = with types; nullOr bool;
 
 427             description = mdDoc "Whether to enable the virtual host.";
 
 431             type = types.nullOr (types.submodule {
 
 435                   description = lib.mdDoc "Path to the key file.";
 
 440                   description = lib.mdDoc "Path to the certificate file.";
 
 443                 extraOptions = mkOption {
 
 446                   description = lib.mdDoc "Extra SSL configuration options.";
 
 451             description = mdDoc "Paths to SSL files.";
 
 458             (mkIf (config.ssl != null) ({
 
 459               key = config.ssl.key;
 
 460               certificate = config.ssl.cert;
 
 461             } // config.ssl.extraOptions))
 
 462             (mkIf (config.useACMEHost != null) {
 
 463               key = "${config'.security.acme.certs.${config.useACMEHost}.directory}/key.pem";
 
 464               certificate = "${config'.security.acme.certs.${config.useACMEHost}.directory}/fullchain.pem";
 
 469             mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") config.components) ++
 
 470             mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") config.components)
 
 473           enabled = mkIf (config.enabled != null) config.enabled;
 
 479       description = mdDoc ''
 
 480         A host in Prosody is a domain on which user accounts can be created. For example if you want your users to have addresses
 
 481         like `john.smith@example.com` then you need to add a host `example.com`.
 
 483       example = literalExpression ''
 
 486             useACMEHost = "example.net";
 
 488               admins = [ "admin1@example.net" "admin2@example.net" ];
 
 489               c2s_require_encryption = true;
 
 499     extraConfig = mkOption {
 
 502       description = mdDoc ''
 
 503         Additional prosody configuration.
 
 507     # options to preserve compatibility with this module pre nixos 23.11
 
 510       type = types.attrsOf types.bool;
 
 512       description = mdDoc ''
 
 517   config = mkIf cfg.enable {
 
 521         hasAnyModules = mods: b: any (e: elem e.module mods) (attrValues b.components);
 
 522         virtualHosts = filterAttrs (_: v: v.settings.enabled) cfg.virtualHosts;
 
 524         # ensure a given module (e.g. muc or http_upload) is applied or available to every virtual host
 
 525         mkAssertion = modules: message: {
 
 526           assertion = cfg.xmppComplianceSuite -> hasAnyModules modules cfg || all (hasAnyModules modules) (attrValues virtualHosts);
 
 527           message = message + ''
 
 529             Having a server not XEP-0423-compliant might make your XMPP
 
 530             experience terrible. See the NixOS manual for further
 
 533             If you know what you're doing, you can disable this warning by
 
 534             setting config.services.prosody.xmppComplianceSuite to false.
 
 539         (mkAssertion [ "muc" ] ''
 
 540           You need to setup at least a MUC domain to comply with
 
 544         (mkAssertion [ "http_upload" "http_file_share" ] ''
 
 545           You need to setup the http_upload or http_file_share module through
 
 546           config.services.prosody.components to comply with
 
 552       optionals (cfg.modules != { }) [ "The option `services.prosody.modules' has been and split into two separate options: `services.prosody.settings.modules_enabled' and `services.prosody.settings.modules_disabled'." ] ++
 
 553       mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.enabled' has been renamed to `services.prosody.virtualHosts.${k}.settings.enabled'.") (filterAttrs (_: v: v.enabled != null) cfg.virtualHosts) ++
 
 554       mapAttrsToList (k: v: "The option `services.prosody.virtualHosts.${k}.ssl' has been renamed to `services.prosody.virtualHosts.${k}.settings.ssl'.") (filterAttrs (_: v: v.ssl != null) cfg.virtualHosts)
 
 557     services.prosody.settings = {
 
 558       log = mkDefault "*syslog";
 
 559       data_path = mkForce cfg.dataDir;
 
 560       network_backend = "event";
 
 561       pidfile = "/run/prosody/prosody.pid";
 
 563       authentication = mkDefault "internal_hashed";
 
 564       reload_modules = mkIf (acmeHosts != [ ]) [ "tls" ];
 
 567         # required for compliance with https://compliance.conversations.im/about/
 
 574         # not essential, but recommended
 
 597       ] ++ cfg.package.communityModules
 
 598       ++ mapAttrsToList (k: _: k) (filterAttrs (_: v: v == true) cfg.modules);
 
 600       modules_disabled = mapAttrsToList (k: _: k) (filterAttrs (_: v: v == false) cfg.modules);
 
 603         mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") cfg.components) ++
 
 604         mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") cfg.components)
 
 607       # mod_tls configuration
 
 608       c2s_require_encryption = mkDefault true;
 
 609       s2s_require_encryption = mkDefault true;
 
 611       # upstream defaults - useful for `services.prosody.openFirewall` logic
 
 612       c2s_ports = mkDefault [ 5222 ];
 
 613       s2s_ports = mkDefault [ 5269 ];
 
 614       https_ports = mkDefault [ 5281 ];
 
 616       # proxy65_ports = [ 5000 ];
 
 619     environment.systemPackages = [ cfg.package ];
 
 620     environment.etc."prosody/prosody.cfg.lua".text =
 
 622         toFormat = attrs: concatStringsSep "\n" (mapAttrsToList (k: v: ''${k} = ${toStr v}'') (filterAttrs (_: v: v != null) attrs));
 
 626           # prosody will directly pass environment variables into its configuration file which have `ENV_` as a prefix
 
 627             if hasPrefix "ENV_" v then v else ''"${toString v}"''
 
 628           else if isBool v then boolToString v
 
 629           else if isInt v then toString v
 
 630           else if isList v then ''{ ${concatStringsSep ", " (map (n: toStr n) v)} }''
 
 631           else if isAttrs v then
 
 632             if v ? __compat then v.value else ''{ ${concatStringsSep ", " (mapAttrsToList (a: b: ''["${a}"] = ${toStr b}'') v)} }''
 
 633           else throw "Invalid Lua value";
 
 635         componentsToStr = components:
 
 636           concatStringsSep "\n" (
 
 640                   Component "${domain}" ${optionalString (component.module != null) "\"${component.module}\""}
 
 641                   ${toFormat component.settings}
 
 642                   ${component.extraConfig}
 
 648         virtualHostsToStr = virtualHosts:
 
 649           concatStringsSep "\n" (
 
 653                   VirtualHost "${virtualHost.domain}"
 
 654                   ${toFormat virtualHost.settings}
 
 655                   ${virtualHost.extraConfig}
 
 657                   ${componentsToStr virtualHost.components}
 
 664         ${toFormat cfg.settings}
 
 667         ${componentsToStr cfg.components}
 
 668         ${virtualHostsToStr cfg.virtualHosts}
 
 671     systemd.services.prosody = {
 
 672       description = "Prosody XMPP server";
 
 673       wantedBy = [ "multi-user.target" ];
 
 674       before = map (domain: "acme-${domain}.service") acmeHosts;
 
 675       after = [ "network-online.target" ] ++ map (domain: "acme-selfsigned-${domain}.service") acmeHosts;
 
 676       wants = [ "network-online.target" ] ++ map (domain: "acme-finished-${domain}.target") acmeHosts;
 
 678       restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ];
 
 679       serviceConfig = mkMerge [
 
 684           PIDFile = cfg.settings.pidfile;
 
 685           ExecStart = "${cfg.package}/bin/prosodyctl start";
 
 686           ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
 
 687           EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile;
 
 689           MemoryDenyWriteExecute = true;
 
 690           PrivateDevices = true;
 
 691           PrivateMounts = true;
 
 693           ProtectControlGroups = true;
 
 695           ProtectHostname = true;
 
 696           ProtectKernelModules = true;
 
 697           ProtectKernelTunables = true;
 
 698           RestrictNamespaces = true;
 
 699           RestrictRealtime = true;
 
 700           RestrictSUIDSGID = true;
 
 702         (mkIf (cfg.dataDir == "/var/lib/prosody") {
 
 703           StateDirectory = "prosody";
 
 704           StateDirectoryMode = "0750";
 
 706         (mkIf (cfg.settings.pidfile == "/run/prosody/prosody.pid") {
 
 707           RuntimeDirectory = [ "prosody" ];
 
 712     security.acme.certs = genAttrs acmeHosts (_: {
 
 713       reloadServices = [ "prosody.service" ];
 
 716     networking.firewall = optionalAttrs cfg.openFirewall {
 
 717       allowedTCPPorts = cfg.settings.c2s_ports
 
 718         ++ cfg.settings.s2s_ports
 
 719         ++ cfg.settings.https_ports
 
 720         # TODO: cfg.settings.proxy65_ports
 
 724     users.users.prosody = mkIf (cfg.user == "prosody") {
 
 725       uid = config.ids.uids.prosody;
 
 726       description = "Prosody user";
 
 731     users.groups.prosody = mkIf (cfg.group == "prosody") {
 
 732       gid = config.ids.gids.prosody;
 
 736   meta.doc = ./prosody.md;