X-Git-Url: https://git.sourcephile.fr/sourcephile-nix.git/blobdiff_plain/4c0efa07a56b4016c280a5f5ffdced8c704ce168..ac8c87d418ded83e2adafa5500122fa82f1692b7:/nixos/modules/services/networking/prosody.nix diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix index 184726b..0470cd8 100644 --- a/nixos/modules/services/networking/prosody.nix +++ b/nixos/modules/services/networking/prosody.nix @@ -1,921 +1,737 @@ -{ config, lib, pkgs, ... }: - -with lib; +{ options, config, pkgs, lib, ... }: let - cfg = config.services.prosody; + inherit (lib) isAttrs isBool isList isInt isString; + inherit (lib) mkChangedOptionModule mkDefault mkEnableOption mkForce mkIf mkMerge mkOption mkRenamedOptionModule; + inherit (lib) all any attrValues boolToString concatStringsSep elem filterAttrs genAttrs hasPrefix listToAttrs literalExpression mapAttrsToList mdDoc optionals optionalAttrs optionalString types unique; - sslOpts = { ... }: { + config' = config; + cfg = config.services.prosody; - options = { + luaType = with types; let + valueType = nullOr + (oneOf [ + bool + int + float + str + path + (attrsOf valueType) + (listOf valueType) + ]) // { + description = "Lua value"; + }; + in + valueType; + + sslOption = mkOption { + type = with types; nullOr (submodule { + freeformType = luaType; + options = { + + key = mkOption { + type = types.str; + description = mdDoc '' + Path to your private key file. + ''; + }; - key = mkOption { - type = types.path; - description = "Path to the key file."; - }; + certificate = mkOption { + type = types.str; + description = mdDoc '' + Path to your certificate file. + ''; + }; - # TODO: rename to certificate to match the prosody config - cert = mkOption { - type = types.path; - description = "Path to the certificate file."; - }; + cafile = mkOption { + type = types.str; + default = "/etc/ssl/certs/ca-bundle.crt"; + description = mdDoc '' + Path to a file containing root certificates that you wish Prosody to trust. + ''; + }; - extraOptions = mkOption { - type = types.attrs; - default = {}; - description = "Extra SSL configuration options."; }; + }); + default = null; + description = mdDoc '' + Advanced SSL/TLS configuration, as described by . - }; - }; - - discoOpts = { - options = { - url = mkOption { - type = types.str; - description = "URL of the endpoint you want to make discoverable"; - }; - description = mkOption { - type = types.str; - description = "A short description of the endpoint you want to advertise"; - }; - }; + ::: {.note} + Prosody passes the contents of the `ssl` option from the config file almost directly to LuaSec, the + library used for SSL/TLS support in Prosody. LuaSec accepts a range of options here, mostly things + that it passes directly to OpenSSL. It is recommended to leave Prosody's defaults in most cases, unless + you know what you are doing. + You could easily reduce security or introduce unnecessary compatibility issues with clients and other servers! + ::: + ''; }; - moduleOpts = { - # Required for compliance with https://compliance.conversations.im/about/ - roster = mkOption { - type = types.bool; - default = true; - description = "Allow users to have a roster"; - }; - - saslauth = mkOption { - type = types.bool; - default = true; - description = "Authentication for clients and servers. Recommended if you want to log in."; - }; - - tls = mkOption { - type = types.bool; - default = true; - description = "Add support for secure TLS on c2s/s2s connections"; - }; - - dialback = mkOption { - type = types.bool; - default = true; - description = "s2s dialback support"; - }; - - disco = mkOption { - type = types.bool; - default = true; - description = "Service discovery"; - }; - - # Not essential, but recommended - carbons = mkOption { - type = types.bool; - default = true; - description = "Keep multiple clients in sync"; - }; - - csi = mkOption { - type = types.bool; - default = true; - description = "Implements the CSI protocol that allows clients to report their active/inactive state to the server"; - }; - - cloud_notify = mkOption { - type = types.bool; - default = true; - description = "Push notifications to inform users of new messages or other pertinent information even when they have no XMPP clients online"; - }; - - pep = mkOption { - type = types.bool; - default = true; - description = "Enables users to publish their mood, activity, playing music and more"; - }; - - private = mkOption { - type = types.bool; - default = true; - description = "Private XML storage (for room bookmarks, etc.)"; - }; - - blocklist = mkOption { - type = types.bool; - default = true; - description = "Allow users to block communications with other users"; - }; - - vcard = mkOption { - type = types.bool; - default = false; - description = "Allow users to set vCards"; - }; - - vcard_legacy = mkOption { - type = types.bool; - default = true; - description = "Converts users profiles and Avatars between old and new formats"; - }; - - bookmarks = mkOption { - type = types.bool; - default = true; - description = "Allows interop between older clients that use XEP-0048: Bookmarks in its 1.0 version and recent clients which use it in PEP"; - }; - - # Nice to have - version = mkOption { - type = types.bool; - default = true; - description = "Replies to server version requests"; - }; - - uptime = mkOption { - type = types.bool; - default = true; - description = "Report how long server has been running"; - }; - - time = mkOption { - type = types.bool; - default = true; - description = "Let others know the time here on this server"; - }; + componentsOption = mkOption { + type = with types; attrsOf (submodule ({ name, config, ... }: { + options = { + module = mkOption { + type = with types; nullOr str; + default = null; + description = mdDoc '' + The name of the plugin you wish to use for the component if internal, or `null` if the + component is an external component. + ''; + }; - ping = mkOption { - type = types.bool; - default = true; - description = "Replies to XMPP pings with pongs"; - }; + settings = mkOption { + type = luaType; + default = { }; + description = mdDoc '' + Values specified here are applied to a specific component. Refer to + for additional details. + ''; + }; - register = mkOption { - type = types.bool; - default = true; - description = "Allow users to register on this server using a client and change passwords"; - }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = mdDoc '' + Additional component specific configuration. + ''; + }; - mam = mkOption { - type = types.bool; - default = true; - description = "Store messages in an archive and allow users to access it"; - }; + }; - smacks = mkOption { - type = types.bool; - default = true; - description = "Allow a client to resume a disconnected session, and prevent message loss"; - }; + config.settings = mkMerge [ + (mkIf (config.module == "http_upload") { + http_upload_path = mkIf (config.module == "http_upload_path") cfg.dataDir; + }) + (mkIf (config.module == "muc") { + modules_enabled = [ "muc_mam" ]; + }) + ]; + })); + default = { }; + description = mdDoc '' + Components are extra services on a server which are available to clients, usually on a subdomain + of the main server (such as mycomponent.example.com). Example components might be chatroom + servers, user directories, or gateways to other protocols. + + See for details. + ''; + }; - # Admin interfaces - admin_adhoc = mkOption { - type = types.bool; - default = true; - description = "Allows administration via an XMPP client that supports ad-hoc commands"; - }; + acmeHosts = unique (mapAttrsToList (domain: hostOpts: hostOpts.useACMEHost) (filterAttrs (_: v: v.useACMEHost != null) cfg.virtualHosts)); +in +{ + imports = [ + (mkRenamedOptionModule [ "services" "prosody" "allowRegistration" ] [ "services" "prosody" "settings" "allow_registration" ]) + (mkRenamedOptionModule [ "services" "prosody" "httpPorts" ] [ "services" "prosody" "settings" "http_ports" ]) + (mkRenamedOptionModule [ "services" "prosody" "httpInterfaces" ] [ "services" "prosody" "settings" "http_interfaces" ]) + (mkRenamedOptionModule [ "services" "prosody" "httpsPorts" ] [ "services" "prosody" "settings" "https_ports" ]) + (mkRenamedOptionModule [ "services" "prosody" "httpsInterfaces" ] [ "services" "prosody" "settings" "https_interfaces" ]) + (mkRenamedOptionModule [ "services" "prosody" "c2sRequireEncryption" ] [ "services" "prosody" "settings" "c2s_require_encryption" ]) + (mkRenamedOptionModule [ "services" "prosody" "s2sRequireEncryption" ] [ "services" "prosody" "settings" "s2s_require_encryption" ]) + (mkRenamedOptionModule [ "services" "prosody" "s2sSecureAuth" ] [ "services" "prosody" "settings" "s2s_secure_auth" ]) + (mkRenamedOptionModule [ "services" "prosody" "s2sInsecureDomains" ] [ "services" "prosody" "settings" "s2s_insecure_domains" ]) + (mkRenamedOptionModule [ "services" "prosody" "s2sSecureDomains" ] [ "services" "prosody" "settings" "s2s_secure_domains" ]) + (mkRenamedOptionModule [ "services" "prosody" "extraModules" ] [ "services" "prosody" "settings" "modules_enabled" ]) + (mkRenamedOptionModule [ "services" "prosody" "extraPluginPaths" ] [ "services" "prosody" "settings" "plugin_paths" ]) + (mkRenamedOptionModule [ "services" "prosody" "ssl" "key" ] [ "services" "prosody" "settings" "ssl" "key" ]) + (mkRenamedOptionModule [ "services" "prosody" "ssl" "cert" ] [ "services" "prosody" "settings" "ssl" "certificate" ]) + (mkRenamedOptionModule [ "services" "prosody" "ssl" "extraOptions" ] [ "services" "prosody" "settings" "ssl" ]) + (mkRenamedOptionModule [ "services" "prosody" "admins" ] [ "services" "prosody" "settings" "admins" ]) + (mkRenamedOptionModule [ "services" "prosody" "authentication" ] [ "services" "prosody" "settings" "authentication" ]) + (mkChangedOptionModule [ "services" "prosody" "disco_items" ] [ "services" "prosody" "settings" "disco_items" ] (config: + map ({ url, description }: [ url description ]) config.services.prosody.disco_items + )) + (mkChangedOptionModule [ "services" "prosody" "uploadHttp" ] [ "services" "prosody" "components" ] (config: + let + cfg = config.services.prosody; + in + { + ${cfg.uploadHttp.domain} = { + module = "http_upload"; + # these values are to preserve compatibility with this module pre nixos 23.11 + settings = { + http_upload_file_size_limit = { __compat = true; value = cfg.uploadHttp.uploadFileSizeLimit or "50 * 1024 * 1024"; }; + http_upload_expire_after = { __compat = true; value = cfg.uploadHttp.uploadExpireAfter or "60 * 60 * 24 * 7"; }; + http_upload_path = cfg.uploadHttp.httpUploadPath or "/var/lib/prosody"; + } // optionalAttrs (cfg.uploadHttp ? userQuota) { + http_upload_quota = cfg.uploadHttp.userQuota; + }; + }; + } + )) + (mkChangedOptionModule [ "services" "prosody" "muc" ] [ "services" "prosody" "components" ] (config: + listToAttrs (map + (muc: { + name = muc.domain; + value = { + module = "muc"; + extraConfig = muc.extraConfig or ""; + # these values are to preserve compatibility with this module pre nixos 23.11 + settings = { + modules_enabled = optionals (muc.vcard_muc or true) [ "vcard_muc" ]; + name = muc.name or "Prosody Chatrooms"; + restrict_room_creation = muc.restrictRoomCreation or "local"; + max_history_messages = muc.maxHistoryMessages or 20; + muc_room_locking = muc.roomLocking or true; + muc_room_lock_timeout = muc.roomLockTimeout or 300; + muc_tombstones = muc.tombstones or true; + muc_tombstone_expiry = muc.tombstoneExpiry or 2678400; + muc_room_default_public = muc.roomDefaultPublic or true; + muc_room_default_members_only = muc.roomDefaultMembersOnly or false; + muc_room_default_moderated = muc.roomDefaultModerated or false; + muc_room_default_public_jids = muc.roomDefaultPublicJids or false; + muc_room_default_change_subject = muc.roomDefaultChangeSubject or false; + muc_room_default_history_length = muc.roomDefaultHistoryLength or 20; + muc_room_default_language = muc.roomDefaultLanguage or "en"; + }; + }; + }) + config.services.prosody.muc) + )) + ]; - http_files = mkOption { - type = types.bool; - default = true; - description = "Serve static files from a directory over HTTP"; - }; + options.services.prosody = { + enable = mkEnableOption "Prosody, the modern XMPP communication server"; - proxy65 = mkOption { + xmppComplianceSuite = mkOption { type = types.bool; default = true; - description = "Enables a file transfer proxy service which clients behind NAT can use"; - }; - - admin_telnet = mkOption { - type = types.bool; - default = false; - description = "Opens telnet console interface on localhost port 5582"; + description = mdDoc '' + The XEP-0423 defines a set of recommended XEPs to implement + for a server. It's generally a good idea to implement this + set of extensions if you want to provide your users with a + good XMPP experience. + + This NixOS module aims to provide an "advanced server" + experience as per defined in the [XEP-0423](https://xmpp.org/extensions/xep-0423.html) specification. + + Setting this option to `true` will prevent you from building a + NixOS configuration which won't comply with this standard. + You can explicitly decide to ignore this standard if you + know what you are doing by setting this option to `false`. + ''; }; - # HTTP modules - bosh = mkOption { - type = types.bool; - default = false; - description = "Enable BOSH clients, aka 'Jabber over HTTP'"; + package = mkOption { + type = types.package; + description = mdDoc "Prosody package to use."; + default = pkgs.prosody; + defaultText = literalExpression "pkgs.prosody"; + example = literalExpression '' + pkgs.prosody.override { + withExtraLibs = [ pkgs.luaPackages.lpty ]; + withCommunityModules = [ "auth_external" ]; + }; + ''; }; - websocket = mkOption { - type = types.bool; - default = false; - description = "Enable WebSocket support"; - }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/prosody"; + description = mdDoc '' + The Prosody home directory used to store all data. - # Other specific functionality - limits = mkOption { - type = types.bool; - default = false; - description = "Enable bandwidth limiting for XMPP connections"; + ::: {.note} + If left as the default value this directory will automatically be created + before the Prosody server starts, otherwise you are responsible for + ensuring the directory exists with appropriate ownership and permissions. + ::: + ''; }; - groups = mkOption { - type = types.bool; - default = false; - description = "Shared roster support"; - }; + user = mkOption { + type = types.str; + default = "prosody"; + description = mdDoc '' + User account under which Prosody runs. - server_contact_info = mkOption { - type = types.bool; - default = false; - description = "Publish contact information for this service"; + ::: {.note} + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the Prosody service starts. + ::: + ''; }; - announce = mkOption { - type = types.bool; - default = false; - description = "Send announcement to all online users"; - }; + group = mkOption { + type = types.str; + default = "prosody"; + description = mdDoc '' + Group account under which Prosody runs. - welcome = mkOption { - type = types.bool; - default = false; - description = "Welcome users who register accounts"; + ::: {.note} + If left as the default value this group will automatically be created + on system activation, otherwise you are responsible for + ensuring the group exists before the Prosody service starts. + ::: + ''; }; - watchregistrations = mkOption { - type = types.bool; - default = false; - description = "Alert admins of registrations"; - }; + environmentFile = mkOption { + type = with types; nullOr path; + default = null; + description = mdDoc '' + File which may contain secrets in the format of an EnvironmentFile as described + by systemd.exec(5). Variables here can be used in the Prosody configuration + file by prefixing the environment variable name with `ENV_` in the configuration. - motd = mkOption { - type = types.bool; - default = false; - description = "Send a message to users when they log in"; + ::: {.note} + Environment variables in this file should *not* begin with an `ENV_` prefix, only + when referenced in the Prosody configuration file. + ::: + ''; }; - legacyauth = mkOption { + openFirewall = mkOption { type = types.bool; default = false; - description = "Legacy authentication. Only used by some old clients and bots"; - }; - }; + description = mdDoc '' + Whether to automatically open ports specified by the following options: - toLua = x: - if builtins.isString x then ''"${x}"'' - else if builtins.isBool x then boolToString x - else if builtins.isInt x then toString x - else if builtins.isList x then ''{ ${lib.concatStringsSep ", " (map (n: toLua n) x) } }'' - else throw "Invalid Lua value"; - - settingsToLua = prefix: settings: generators.toKeyValue { - listsAsDuplicateKeys = false; - mkKeyValue = k: - generators.mkKeyValueDefault { - mkValueString = toLua; - } " = " (prefix + k); - } - (filterAttrs (k: v: v != null) settings); - - createSSLOptsStr = o: '' - ssl = { - cafile = "/etc/ssl/certs/ca-bundle.crt"; - key = "${o.key}"; - certificate = "${o.cert}"; - ${concatStringsSep "\n" (mapAttrsToList (name: value: "${name} = ${toLua value};") o.extraOptions)} - }; - ''; - - mucOpts = { ... }: { - options = { - domain = mkOption { - type = types.str; - description = "Domain name of the MUC"; - }; - name = mkOption { - type = types.str; - description = "The name to return in service discovery responses for the MUC service itself"; - default = "Prosody Chatrooms"; - }; - restrictRoomCreation = mkOption { - type = types.enum [ true false "admin" "local" ]; - default = false; - description = "Restrict room creation to server admins"; - }; - maxHistoryMessages = mkOption { - type = types.int; - default = 20; - description = "Specifies a limit on what each room can be configured to keep"; - }; - roomLocking = mkOption { - type = types.bool; - default = true; - description = '' - Enables room locking, which means that a room must be - configured before it can be used. Locked rooms are invisible - and cannot be entered by anyone but the creator - ''; - }; - roomLockTimeout = mkOption { - type = types.int; - default = 300; - description = '' - Timout after which the room is destroyed or unlocked if not - configured, in seconds - ''; - }; - tombstones = mkOption { - type = types.bool; - default = true; - description = '' - When a room is destroyed, it leaves behind a tombstone which - prevents the room being entered or recreated. It also allows - anyone who was not in the room at the time it was destroyed - to learn about it, and to update their bookmarks. Tombstones - prevents the case where someone could recreate a previously - semi-anonymous room in order to learn the real JIDs of those - who often join there. - ''; - }; - tombstoneExpiry = mkOption { - type = types.int; - default = 2678400; - description = '' - This settings controls how long a tombstone is considered - valid. It defaults to 31 days. After this time, the room in - question can be created again. - ''; - }; - - vcard_muc = mkOption { - type = types.bool; - default = true; - description = "Adds the ability to set vCard for Multi User Chat rooms"; - }; - - # Extra parameters. Defaulting to prosody default values. - # Adding them explicitly to make them visible from the options - # documentation. - # - # See https://prosody.im/doc/modules/mod_muc for more details. - roomDefaultPublic = mkOption { - type = types.bool; - default = true; - description = "If set, the MUC rooms will be public by default."; - }; - roomDefaultMembersOnly = mkOption { - type = types.bool; - default = false; - description = "If set, the MUC rooms will only be accessible to the members by default."; - }; - roomDefaultModerated = mkOption { - type = types.bool; - default = false; - description = "If set, the MUC rooms will be moderated by default."; - }; - roomDefaultPublicJids = mkOption { - type = types.bool; - default = false; - description = "If set, the MUC rooms will display the public JIDs by default."; - }; - roomDefaultChangeSubject = mkOption { - type = types.bool; - default = false; - description = "If set, the rooms will display the public JIDs by default."; - }; - roomDefaultHistoryLength = mkOption { - type = types.int; - default = 20; - description = "Number of history message sent to participants by default."; - }; - roomDefaultLanguage = mkOption { - type = types.str; - default = "en"; - description = "Default room language."; - }; - extraConfig = mkOption { - type = types.lines; - default = ""; - description = "Additional MUC specific configuration"; - }; - }; - }; - - uploadHttpOpts = { ... }: { - options = { - domain = mkOption { - type = types.nullOr types.str; - description = "Domain name for the http-upload service"; - }; - uploadFileSizeLimit = mkOption { - type = types.str; - default = "50 * 1024 * 1024"; - description = "Maximum file size, in bytes. Defaults to 50MB."; - }; - uploadExpireAfter = mkOption { - type = types.str; - default = "60 * 60 * 24 * 7"; - description = "Max age of a file before it gets deleted, in seconds."; - }; - userQuota = mkOption { - type = types.nullOr types.int; - default = null; - example = 1234; - description = '' - Maximum size of all uploaded files per user, in bytes. There - will be no quota if this option is set to null. - ''; - }; - httpUploadPath = mkOption { - type = types.str; - description = '' - Directory where the uploaded files will be stored - when the http_upload module is used. - By default, uploaded files are put in a sub-directory of the - default Prosody storage path (usually /var/lib/prosody). - ''; - default = "/var/lib/prosody"; - }; - }; - }; - - httpFileShareOpts = { ... }: { - freeformType = with types; - let atom = oneOf [ int bool str (listOf atom) ]; in - attrsOf (nullOr atom); - options.domain = mkOption { - type = with types; nullOr str; - description = "Domain name for a http_file_share service."; + - {option}`services.prosody.settings.c2s_ports` + - {option}`services.prosody.settings.s2s_ports` + - {option}`services.prosody.settings.https_ports` + ''; }; - }; - - vHostOpts = { ... }: { - options = { + settings = mkOption { + type = types.submodule { + freeformType = luaType; + options = { - # TODO: require attribute - domain = mkOption { - type = types.str; - description = "Domain name"; - }; + modules_enabled = mkOption { + type = with types; listOf str; + apply = x: unique x; + default = [ ]; + description = mdDoc '' + List of modules to load for all virtual hosts. + ''; + }; - enabled = mkOption { - type = types.bool; - default = false; - description = "Whether to enable the virtual host"; - }; + modules_disabled = mkOption { + type = with types; listOf str; + apply = x: unique x; + default = [ ]; + description = mdDoc '' + Allows you to disable the loading of a list of modules for all virtual hosts + if those modules are set in the global settings. + ''; + }; - ssl = mkOption { - type = types.nullOr (types.submodule sslOpts); - default = null; - description = "Paths to SSL files"; - }; + ssl = sslOption; - extraConfig = mkOption { - type = types.lines; - default = ""; - description = "Additional virtual host specific configuration"; + }; }; + default = { }; + description = mdDoc '' + Values specified here are applied to the whole server, and are the default for all + virtual hosts. Refer to for additional details. + ::: {.note} + It's also possible to refer to environment variables (defined in + [services.prosody.environmentFile](#opt-services.prosody.environmentFile)) using the + syntax `"ENV_VARIABLE_NAME"` where the environment file contains a `VARIABLE_NAME` entry. + ::: + ''; + example = literalExpression '' + { + modules_enabled = [ "turn_external" ]; + turn_external_host = "turn.example.com"; + turn_external_port = 3478; + turn_external_secret = "ENV_TURN_EXTERNAL_SECRET"; + } + ''; }; - }; - -in - -{ - - ###### interface - - options = { - - services.prosody = { + components = componentsOption; - enable = mkOption { - type = types.bool; - default = false; - description = "Whether to enable the prosody server"; - }; - - xmppComplianceSuite = mkOption { - type = types.bool; - default = true; - description = '' - The XEP-0423 defines a set of recommended XEPs to implement - for a server. It's generally a good idea to implement this - set of extensions if you want to provide your users with a - good XMPP experience. - - This NixOS module aims to provide a "advanced server" - experience as per defined in the XEP-0423[1] specification. - - Setting this option to true will prevent you from building a - NixOS configuration which won't comply with this standard. - You can explicitely decide to ignore this standard if you - know what you are doing by setting this option to false. - - [1] https://xmpp.org/extensions/xep-0423.html - ''; - }; - - package = mkOption { - type = types.package; - description = "Prosody package to use"; - default = pkgs.prosody; - defaultText = literalExpression "pkgs.prosody"; - example = literalExpression '' - pkgs.prosody.override { - withExtraLibs = [ pkgs.luaPackages.lpty ]; - withCommunityModules = [ "auth_external" ]; + virtualHosts = let config' = config; in mkOption { + type = with types; attrsOf (submodule ({ name, config, ... }: { + options = { + domain = mkOption { + type = types.str; + default = name; + description = mdDoc "Domain name."; }; - ''; - }; - - dataDir = mkOption { - type = types.path; - description = "Directory where Prosody stores its data"; - default = "/var/lib/prosody"; - }; - - disco_items = mkOption { - type = types.listOf (types.submodule discoOpts); - default = []; - description = "List of discoverable items you want to advertise."; - }; - - user = mkOption { - type = types.str; - default = "prosody"; - description = "User account under which prosody runs."; - }; - - group = mkOption { - type = types.str; - default = "prosody"; - description = "Group account under which prosody runs."; - }; - - allowRegistration = mkOption { - type = types.bool; - default = false; - description = "Allow account creation"; - }; - # HTTP server-related options - httpPorts = mkOption { - type = types.listOf types.int; - description = "Listening HTTP ports list for this service."; - default = [ 5280 ]; - }; - - httpInterfaces = mkOption { - type = types.listOf types.str; - default = [ "*" "::" ]; - description = "Interfaces on which the HTTP server will listen on."; - }; - - httpsPorts = mkOption { - type = types.listOf types.int; - description = "Listening HTTPS ports list for this service."; - default = [ 5281 ]; - }; - - httpsInterfaces = mkOption { - type = types.listOf types.str; - default = [ "*" "::" ]; - description = "Interfaces on which the HTTPS server will listen on."; - }; - - c2sRequireEncryption = mkOption { - type = types.bool; - default = true; - description = '' - Force clients to use encrypted connections? This option will - prevent clients from authenticating unless they are using encryption. - ''; - }; - - s2sRequireEncryption = mkOption { - type = types.bool; - default = true; - description = '' - Force servers to use encrypted connections? This option will - prevent servers from authenticating unless they are using encryption. - Note that this is different from authentication. - ''; - }; - - s2sSecureAuth = mkOption { - type = types.bool; - default = false; - description = '' - Force certificate authentication for server-to-server connections? - This provides ideal security, but requires servers you communicate - with to support encryption AND present valid, trusted certificates. - For more information see https://prosody.im/doc/s2s#security - ''; - }; - - s2sInsecureDomains = mkOption { - type = types.listOf types.str; - default = []; - example = [ "insecure.example.com" ]; - description = '' - Some servers have invalid or self-signed certificates. You can list - remote domains here that will not be required to authenticate using - certificates. They will be authenticated using DNS instead, even - when s2s_secure_auth is enabled. - ''; - }; + useACMEHost = mkOption { + type = with types; nullOr str; + default = null; + description = mdDoc '' + A host of an existing Let's Encrypt certificate to use. + + ::: {.note} + Note that this option does not create any certificates, nor it does add subdomains to existing + ones – you will need to create them manually using [](#opt-security.acme.certs). + ::: + ''; + }; - s2sSecureDomains = mkOption { - type = types.listOf types.str; - default = []; - example = [ "jabber.org" ]; - description = '' - Even if you leave s2s_secure_auth disabled, you can still require valid - certificates for some domains by specifying a list here. - ''; - }; + components = componentsOption; + + settings = mkOption { + type = types.submodule { + freeformType = luaType; + options = { + + enabled = mkOption { + type = types.bool; + default = true; + description = mdDoc '' + Specifies whether this host is enabled or not. Disabled hosts are not loaded and + do not accept connections while Prosody is running. + ''; + }; + + modules_enabled = mkOption { + type = with types; listOf str; + apply = x: unique x; + default = [ ]; + description = mdDoc '' + List of modules to load for the virtual host. + ''; + }; + + modules_disabled = mkOption { + type = with types; listOf str; + apply = x: unique x; + default = [ ]; + description = mdDoc '' + Allows you to disable the loading of a list of modules for a particular host. + ''; + }; + + ssl = sslOption; + }; + }; + default = { }; + description = mdDoc '' + Values specified here are applied to a specific virtual host and will override values set + in the global [settings](#opt-services.prosody.settings) option. Refer to + for additional details. + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = mdDoc '' + Additional virtual host specific configuration. + ''; + }; - modules = moduleOpts; + # options to preserve compatibility with this module pre nixos 23.11 - extraModules = mkOption { - type = types.listOf types.str; - default = []; - description = "Enable custom modules"; - }; + enabled = mkOption { + type = with types; nullOr bool; + default = null; + description = mdDoc "Whether to enable the virtual host."; + }; - extraPluginPaths = mkOption { - type = types.listOf types.path; - default = []; - description = "Addtional path in which to look find plugins/modules"; - }; + ssl = mkOption { + type = types.nullOr (types.submodule { + options = { + key = mkOption { + type = types.path; + description = lib.mdDoc "Path to the key file."; + }; + + cert = mkOption { + type = types.path; + description = lib.mdDoc "Path to the certificate file."; + }; + + extraOptions = mkOption { + type = types.attrs; + default = { }; + description = lib.mdDoc "Extra SSL configuration options."; + }; + }; + }); + default = null; + description = mdDoc "Paths to SSL files."; + }; - uploadHttp = mkOption { - description = '' - Configures the old Prosody builtin HTTP server to handle user uploads. - ''; - type = types.nullOr (types.submodule uploadHttpOpts); - default = null; - example = { - domain = "uploads.my-xmpp-example-host.org"; }; - }; - httpFileShare = mkOption { - description = '' - Configures the http_file_share module to handle user uploads. - ''; - type = types.nullOr (types.submodule httpFileShareOpts); - default = null; - example = { - domain = "uploads.my-xmpp-example-host.org"; + config.settings = { + ssl = mkMerge [ + (mkIf (config.ssl != null) ({ + key = config.ssl.key; + certificate = config.ssl.cert; + } // config.ssl.extraOptions)) + (mkIf (config.useACMEHost != null) { + key = "${config'.security.acme.certs.${config.useACMEHost}.directory}/key.pem"; + certificate = "${config'.security.acme.certs.${config.useACMEHost}.directory}/fullchain.pem"; + }) + ]; + + disco_items = + mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") config.components) ++ + mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") config.components) + ; + + enabled = mkIf (config.enabled != null) config.enabled; }; + })); + default = { + localhost = { }; }; - - muc = mkOption { - type = types.listOf (types.submodule mucOpts); - default = [ ]; - example = [ { - domain = "conference.my-xmpp-example-host.org"; - } ]; - description = "Multi User Chat (MUC) configuration"; - }; - - virtualHosts = mkOption { - - description = "Define the virtual hosts"; - - type = with types; attrsOf (submodule vHostOpts); - - example = { - myhost = { - domain = "my-xmpp-example-host.org"; - enabled = true; - }; - }; - - default = { - localhost = { - domain = "localhost"; - enabled = true; + description = mdDoc '' + A host in Prosody is a domain on which user accounts can be created. For example if you want your users to have addresses + like `john.smith@example.com` then you need to add a host `example.com`. + ''; + example = literalExpression '' + { + "example.net" = { + useACMEHost = "example.net"; + settings = { + admins = [ "admin1@example.net" "admin2@example.net" ]; + c2s_require_encryption = true; + modules_enabled = [ + "announce" + ]; + }; }; - }; - - }; - - ssl = mkOption { - type = types.nullOr (types.submodule sslOpts); - default = null; - description = "Paths to SSL files"; - }; - - admins = mkOption { - type = types.listOf types.str; - default = []; - example = [ "admin1@example.com" "admin2@example.com" ]; - description = "List of administrators of the current host"; - }; + } + ''; + }; - authentication = mkOption { - type = types.enum [ "internal_plain" "internal_hashed" "cyrus" "anonymous" ]; - default = "internal_hashed"; - example = "internal_plain"; - description = "Authentication mechanism used for logins."; - }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = mdDoc '' + Additional prosody configuration. + ''; + }; - extraConfig = mkOption { - type = types.lines; - default = ""; - description = "Additional prosody configuration"; - }; + # options to preserve compatibility with this module pre nixos 23.11 + modules = mkOption { + type = types.attrsOf types.bool; + default = { }; + description = mdDoc '' + ''; }; }; - - ###### implementation - config = mkIf cfg.enable { - assertions = let - genericErrMsg = '' + assertions = + let + hasAnyModules = mods: b: any (e: elem e.module mods) (attrValues b.components); + virtualHosts = filterAttrs (_: v: v.settings.enabled) cfg.virtualHosts; - Having a server not XEP-0423-compliant might make your XMPP - experience terrible. See the NixOS manual for further - informations. + # ensure a given module (e.g. muc or http_upload) is applied or available to every virtual host + mkAssertion = modules: message: { + assertion = cfg.xmppComplianceSuite -> hasAnyModules modules cfg || all (hasAnyModules modules) (attrValues virtualHosts); + message = message + '' - If you know what you're doing, you can disable this warning by - setting config.services.prosody.xmppComplianceSuite to false. - ''; - errors = [ - { assertion = (builtins.length cfg.muc > 0) || !cfg.xmppComplianceSuite; - message = '' - You need to setup at least a MUC domain to comply with - XEP-0423. - '' + genericErrMsg;} - { assertion = cfg.uploadHttp != null || cfg.httpFileShare != null || !cfg.xmppComplianceSuite; - message = '' - You need to setup the http_upload or http_file_share modules through - config.services.prosody.uploadHttp - or config.services.prosody.httpFileShare - to comply with XEP-0423. - '' + genericErrMsg;} + Having a server not XEP-0423-compliant might make your XMPP + experience terrible. See the NixOS manual for further + information. + + If you know what you're doing, you can disable this warning by + setting config.services.prosody.xmppComplianceSuite to false. + ''; + }; + in + [ + (mkAssertion [ "muc" ] '' + You need to setup at least a MUC domain to comply with + XEP-0423 + '') + + (mkAssertion [ "http_upload" "http_file_share" ] '' + You need to setup the http_upload or http_file_share module through + config.services.prosody.components to comply with + XEP-0423. + '') ]; - in errors; - environment.systemPackages = [ cfg.package ]; + warnings = + 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'." ] ++ + 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) ++ + 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) + ; + + services.prosody.settings = { + log = mkDefault "*syslog"; + data_path = mkForce cfg.dataDir; + network_backend = "event"; + pidfile = "/run/prosody/prosody.pid"; + + authentication = mkDefault "internal_hashed"; + reload_modules = mkIf (acmeHosts != [ ]) [ "tls" ]; + + modules_enabled = [ + # required for compliance with https://compliance.conversations.im/about/ + "dialback" + "disco" + "roster" + "saslauth" + "tls" + + # not essential, but recommended + "blocklist" + "bookmarks" + "carbons" + "cloud_notify" + "csi" + "pep" + "private" + "vcard_legacy" + + # nice to have + "mam" + "ping" + "register" + "smacks" + "time" + "uptime" + "version" + + # admin interfaces + "admin_adhoc" + "http_files" + "proxy65" + ] ++ cfg.package.communityModules + ++ mapAttrsToList (k: _: k) (filterAttrs (_: v: v == true) cfg.modules); + + modules_disabled = mapAttrsToList (k: _: k) (filterAttrs (_: v: v == false) cfg.modules); + + disco_items = + mapAttrsToList (k: v: [ k "${k} HTTP upload endpoint" ]) (filterAttrs (k: v: v.module == "http_upload") cfg.components) ++ + mapAttrsToList (k: v: [ k "${k} MUC endpoint" ]) (filterAttrs (k: v: v.module == "muc") cfg.components) + ; + + # mod_tls configuration + c2s_require_encryption = mkDefault true; + s2s_require_encryption = mkDefault true; + + # upstream defaults - useful for `services.prosody.openFirewall` logic + c2s_ports = mkDefault [ 5222 ]; + s2s_ports = mkDefault [ 5269 ]; + https_ports = mkDefault [ 5281 ]; + # TODO: + # proxy65_ports = [ 5000 ]; + }; + environment.systemPackages = [ cfg.package ]; environment.etc."prosody/prosody.cfg.lua".text = let - httpDiscoItems = - optional (cfg.uploadHttp != null) - { url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";} ++ - optional (cfg.httpFileShare != null) - { url = cfg.httpFileShare.domain; description = "HTTP file share endpoint";}; - mucDiscoItems = builtins.foldl' - (acc: muc: [{ url = muc.domain; description = "${muc.domain} MUC endpoint";}] ++ acc) - [] - cfg.muc; - discoItems = cfg.disco_items ++ httpDiscoItems ++ mucDiscoItems; - in '' - - pidfile = "/run/prosody/prosody.pid" - - log = "*syslog" - - data_path = "${cfg.dataDir}" - plugin_paths = { - ${lib.concatStringsSep ", " (map (n: "\"${n}\"") cfg.extraPluginPaths) } - } - - ${ optionalString (cfg.ssl != null) (createSSLOptsStr cfg.ssl) } - - admins = ${toLua cfg.admins} - - -- we already build with libevent, so we can just enable it for a more performant server - use_libevent = true - - modules_enabled = { - - ${ lib.concatStringsSep "\n " (lib.mapAttrsToList - (name: val: optionalString val "${toLua name};") - cfg.modules) } - ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.package.communityModules)} - ${ lib.concatStringsSep "\n" (map (x: "${toLua x};") cfg.extraModules)} - }; - - disco_items = { - ${ lib.concatStringsSep "\n" (builtins.map (x: ''{ "${x.url}", "${x.description}"};'') discoItems)} - }; - - allow_registration = ${toLua cfg.allowRegistration} - - c2s_require_encryption = ${toLua cfg.c2sRequireEncryption} - - s2s_require_encryption = ${toLua cfg.s2sRequireEncryption} - - s2s_secure_auth = ${toLua cfg.s2sSecureAuth} - - s2s_insecure_domains = ${toLua cfg.s2sInsecureDomains} - - s2s_secure_domains = ${toLua cfg.s2sSecureDomains} - - authentication = ${toLua cfg.authentication} - - http_interfaces = ${toLua cfg.httpInterfaces} - - https_interfaces = ${toLua cfg.httpsInterfaces} - - http_ports = ${toLua cfg.httpPorts} - - https_ports = ${toLua cfg.httpsPorts} - - ${ cfg.extraConfig } + toFormat = attrs: concatStringsSep "\n" (mapAttrsToList (k: v: ''${k} = ${toStr v}'') (filterAttrs (_: v: v != null) attrs)); + + toStr = v: + if isString v then + # prosody will directly pass environment variables into its configuration file which have `ENV_` as a prefix + if hasPrefix "ENV_" v then v else ''"${toString v}"'' + else if isBool v then boolToString v + else if isInt v then toString v + else if isList v then ''{ ${concatStringsSep ", " (map (n: toStr n) v)} }'' + else if isAttrs v then + if v ? __compat then v.value else ''{ ${concatStringsSep ", " (mapAttrsToList (a: b: ''["${a}"] = ${toStr b}'') v)} }'' + else throw "Invalid Lua value"; + + componentsToStr = components: + concatStringsSep "\n" ( + mapAttrsToList + (domain: component: + '' + Component "${domain}" ${optionalString (component.module != null) "\"${component.module}\""} + ${toFormat component.settings} + ${component.extraConfig} + '' + ) + components + ); + + virtualHostsToStr = virtualHosts: + concatStringsSep "\n" ( + mapAttrsToList + (_: virtualHost: + '' + VirtualHost "${virtualHost.domain}" + ${toFormat virtualHost.settings} + ${virtualHost.extraConfig} + + ${componentsToStr virtualHost.components} + '' + ) + virtualHosts + ); + in + '' + ${toFormat cfg.settings} + ${cfg.extraConfig} + + ${componentsToStr cfg.components} + ${virtualHostsToStr cfg.virtualHosts} + ''; - ${lib.concatMapStrings (muc: '' - Component ${toLua muc.domain} "muc" - modules_enabled = { "muc_mam"; ${optionalString muc.vcard_muc ''"vcard_muc";'' } } - name = ${toLua muc.name} - restrict_room_creation = ${toLua muc.restrictRoomCreation} - max_history_messages = ${toLua muc.maxHistoryMessages} - muc_room_locking = ${toLua muc.roomLocking} - muc_room_lock_timeout = ${toLua muc.roomLockTimeout} - muc_tombstones = ${toLua muc.tombstones} - muc_tombstone_expiry = ${toLua muc.tombstoneExpiry} - muc_room_default_public = ${toLua muc.roomDefaultPublic} - muc_room_default_members_only = ${toLua muc.roomDefaultMembersOnly} - muc_room_default_moderated = ${toLua muc.roomDefaultModerated} - muc_room_default_public_jids = ${toLua muc.roomDefaultPublicJids} - muc_room_default_change_subject = ${toLua muc.roomDefaultChangeSubject} - muc_room_default_history_length = ${toLua muc.roomDefaultHistoryLength} - muc_room_default_language = ${toLua muc.roomDefaultLanguage} - ${ muc.extraConfig } - '') cfg.muc} + systemd.services.prosody = { + description = "Prosody XMPP server"; + wantedBy = [ "multi-user.target" ]; + before = map (domain: "acme-${domain}.service") acmeHosts; + after = [ "network-online.target" ] ++ map (domain: "acme-selfsigned-${domain}.service") acmeHosts; + wants = [ "network-online.target" ] ++ map (domain: "acme-finished-${domain}.target") acmeHosts; - ${ lib.optionalString (cfg.uploadHttp != null) '' - Component ${toLua cfg.uploadHttp.domain} "http_upload" - http_upload_file_size_limit = ${cfg.uploadHttp.uploadFileSizeLimit} - http_upload_expire_after = ${cfg.uploadHttp.uploadExpireAfter} - ${lib.optionalString (cfg.uploadHttp.userQuota != null) "http_upload_quota = ${toLua cfg.uploadHttp.userQuota}"} - http_upload_path = ${toLua cfg.uploadHttp.httpUploadPath} - ''} + restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ]; + serviceConfig = mkMerge [ + { + User = cfg.user; + Group = cfg.group; + Type = "forking"; + PIDFile = cfg.settings.pidfile; + ExecStart = "${cfg.package}/bin/prosodyctl start"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + } + (mkIf (cfg.dataDir == "/var/lib/prosody") { + StateDirectory = "prosody"; + StateDirectoryMode = "0750"; + }) + (mkIf (cfg.settings.pidfile == "/run/prosody/prosody.pid") { + RuntimeDirectory = [ "prosody" ]; + }) + ]; + }; - ${ lib.optionalString (cfg.httpFileShare != null) '' - Component ${toLua cfg.httpFileShare.domain} "http_file_share" - ${settingsToLua " http_file_share_" (cfg.httpFileShare // { domain = null; })} - ''} + security.acme.certs = genAttrs acmeHosts (_: { + reloadServices = [ "prosody.service" ]; + }); - ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: '' - VirtualHost "${v.domain}" - enabled = ${boolToString v.enabled}; - ${ optionalString (v.ssl != null) (createSSLOptsStr v.ssl) } - ${ v.extraConfig } - '') cfg.virtualHosts) } - ''; + networking.firewall = optionalAttrs cfg.openFirewall { + allowedTCPPorts = cfg.settings.c2s_ports + ++ cfg.settings.s2s_ports + ++ cfg.settings.https_ports + # TODO: cfg.settings.proxy65_ports + ; + }; users.users.prosody = mkIf (cfg.user == "prosody") { uid = config.ids.uids.prosody; description = "Prosody user"; - createHome = true; inherit (cfg) group; - home = "${cfg.dataDir}"; + home = cfg.dataDir; }; users.groups.prosody = mkIf (cfg.group == "prosody") { gid = config.ids.gids.prosody; }; - - systemd.services.prosody = { - description = "Prosody XMPP server"; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - restartTriggers = [ config.environment.etc."prosody/prosody.cfg.lua".source ]; - serviceConfig = { - User = cfg.user; - Group = cfg.group; - Type = "forking"; - RuntimeDirectory = [ "prosody" ]; - PIDFile = "/run/prosody/prosody.pid"; - ExecStart = "${cfg.package}/bin/prosodyctl start"; - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; - - MemoryDenyWriteExecute = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - }; - }; - }; - meta.doc = ./prosody.xml; + + meta.doc = ./prosody.md; }