diff options
-rw-r--r-- | nixos/modules/services/networking/prosody.nix | 372 | ||||
-rw-r--r-- | nixos/tests/xmpp/prosody.nix | 85 | ||||
-rw-r--r-- | nixos/tests/xmpp/xmpp-sendmessage.nix | 105 | ||||
-rw-r--r-- | pkgs/servers/xmpp/prosody/default.nix | 15 |
4 files changed, 490 insertions, 87 deletions
diff --git a/nixos/modules/services/networking/prosody.nix b/nixos/modules/services/networking/prosody.nix index 7a503e71166..8058172cb08 100644 --- a/nixos/modules/services/networking/prosody.nix +++ b/nixos/modules/services/networking/prosody.nix @@ -1,9 +1,7 @@ { config, lib, pkgs, ... }: with lib; - let - cfg = config.services.prosody; sslOpts = { ... }: { @@ -30,8 +28,21 @@ let }; }; + 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"; + }; + }; + }; + moduleOpts = { - # Generally required + # Required for compliance with https://compliance.conversations.im/about/ roster = mkOption { type = types.bool; default = true; @@ -69,6 +80,18 @@ let 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; @@ -89,10 +112,22 @@ let vcard = mkOption { type = types.bool; - default = true; + 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; @@ -126,10 +161,16 @@ let mam = mkOption { type = types.bool; - default = false; + 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"; + }; + # Admin interfaces admin_adhoc = mkOption { type = types.bool; @@ -137,6 +178,18 @@ let description = "Allows administration via an XMPP client that supports ad-hoc commands"; }; + http_files = mkOption { + type = types.bool; + default = true; + description = "Serve static files from a directory over HTTP"; + }; + + proxy65 = 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; @@ -156,12 +209,6 @@ let description = "Enable WebSocket support"; }; - http_files = mkOption { - type = types.bool; - default = false; - description = "Serve static files from a directory over HTTP"; - }; - # Other specific functionality limits = mkOption { type = types.bool; @@ -210,13 +257,6 @@ let default = false; description = "Legacy authentication. Only used by some old clients and bots"; }; - - proxy65 = mkOption { - type = types.bool; - default = false; - description = "Enables a file transfer proxy service which clients behind NAT can use"; - }; - }; toLua = x: @@ -235,6 +275,153 @@ let }; ''; + 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."; + }; + }; + }; + + 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. By + default, uploaded files are put in a sub-directory of the + default Prosody storage path (usually /var/lib/prosody). + ''; + default = "/var/lib/prosody"; + }; + }; + }; + vHostOpts = { ... }: { options = { @@ -283,6 +470,27 @@ in 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"; @@ -302,6 +510,12 @@ in 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"; @@ -320,6 +534,31 @@ in 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; @@ -387,6 +626,26 @@ in description = "Addtional path in which to look find plugins/modules"; }; + uploadHttp = mkOption { + description = '' + Configures the Prosody builtin HTTP server to handle user uploads. + ''; + type = types.nullOr (types.submodule uploadHttpOpts); + default = null; + example = { + domain = "uploads.my-xmpp-example-host.org"; + }; + }; + + 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"; @@ -443,9 +702,44 @@ in config = mkIf cfg.enable { + assertions = let + genericErrMsg = '' + + Having a server not XEP-0423-compliant might make your XMPP + experience terrible. See the NixOS manual for further + informations. + + 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.xmppComplianceSuite; + message = '' + You need to setup the uploadHttp module through + config.services.prosody.uploadHttp to comply with + XEP-0423. + '' + genericErrMsg;} + ]; + in errors; + environment.systemPackages = [ cfg.package ]; - environment.etc."prosody/prosody.cfg.lua".text = '' + environment.etc."prosody/prosody.cfg.lua".text = + let + httpDiscoItems = if (cfg.uploadHttp != null) + then [{ url = cfg.uploadHttp.domain; description = "HTTP upload endpoint";}] + else []; + 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" @@ -472,6 +766,10 @@ in ${ 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} @@ -486,6 +784,42 @@ in authentication = ${toLua cfg.authentication} + http_interfaces = ${toLua cfg.httpInterfaces} + + https_interfaces = ${toLua cfg.httpsInterfaces} + + http_ports = ${toLua cfg.httpPorts} + + https_ports = ${toLua cfg.httpsPorts} + + ${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} + + '') cfg.muc} + + ${ 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} + ''} + ${ cfg.extraConfig } ${ lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: '' diff --git a/nixos/tests/xmpp/prosody.nix b/nixos/tests/xmpp/prosody.nix index 9d1374bff6b..e7755e24bab 100644 --- a/nixos/tests/xmpp/prosody.nix +++ b/nixos/tests/xmpp/prosody.nix @@ -1,27 +1,80 @@ -import ../make-test-python.nix { - name = "prosody"; +let + cert = pkgs: pkgs.runCommandNoCC "selfSignedCerts" { buildInputs = [ pkgs.openssl ]; } '' + openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -subj '/CN=example.com/CN=uploads.example.com/CN=conference.example.com' + mkdir -p $out + cp key.pem cert.pem $out + ''; + createUsers = pkgs: pkgs.writeScriptBin "create-prosody-users" '' + #!${pkgs.bash}/bin/bash + set -e + + # Creates and set password for the 2 xmpp test users. + # + # Doing that in a bash script instead of doing that in the test + # script allow us to easily provision the users when running that + # test interactively. + + prosodyctl register cthon98 example.com nothunter2 + prosodyctl register azurediamond example.com hunter2 + ''; + delUsers = pkgs: pkgs.writeScriptBin "delete-prosody-users" '' + #!${pkgs.bash}/bin/bash + set -e + + # Deletes the test users. + # + # Doing that in a bash script instead of doing that in the test + # script allow us to easily provision the users when running that + # test interactively. + prosodyctl deluser cthon98@example.com + prosodyctl deluser azurediamond@example.com + ''; +in import ../make-test-python.nix { + name = "prosody"; nodes = { - client = { nodes, pkgs, ... }: { + client = { nodes, pkgs, config, ... }: { + security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ]; + console.keyMap = "fr-bepo"; + networking.extraHosts = '' + ${nodes.server.config.networking.primaryIPAddress} example.com + ${nodes.server.config.networking.primaryIPAddress} conference.example.com + ${nodes.server.config.networking.primaryIPAddress} uploads.example.com + ''; environment.systemPackages = [ (pkgs.callPackage ./xmpp-sendmessage.nix { connectTo = nodes.server.config.networking.primaryIPAddress; }) ]; }; server = { config, pkgs, ... }: { + security.pki.certificateFiles = [ "${cert pkgs}/cert.pem" ]; + console.keyMap = "fr-bepo"; networking.extraHosts = '' ${config.networking.primaryIPAddress} example.com + ${config.networking.primaryIPAddress} conference.example.com + ${config.networking.primaryIPAddress} uploads.example.com ''; networking.firewall.enable = false; + environment.systemPackages = [ + (createUsers pkgs) + (delUsers pkgs) + ]; services.prosody = { enable = true; - # TODO: use a self-signed certificate - c2sRequireEncryption = false; - extraConfig = '' - storage = "sql" - ''; - virtualHosts.test = { + ssl.cert = "${cert pkgs}/cert.pem"; + ssl.key = "${cert pkgs}/key.pem"; + virtualHosts.example = { domain = "example.com"; enabled = true; + ssl.cert = "${cert pkgs}/cert.pem"; + ssl.key = "${cert pkgs}/key.pem"; + }; + muc = [ + { + domain = "conference.example.com"; + } + ]; + uploadHttp = { + domain = "uploads.example.com"; }; }; }; @@ -31,16 +84,8 @@ import ../make-test-python.nix { server.wait_for_unit("prosody.service") server.succeed('prosodyctl status | grep "Prosody is running"') - # set password to 'nothunter2' (it's asked twice) - server.succeed("yes nothunter2 | prosodyctl adduser cthon98@example.com") - # set password to 'y' - server.succeed("yes | prosodyctl adduser azurediamond@example.com") - # correct password to "hunter2" - server.succeed("yes hunter2 | prosodyctl passwd azurediamond@example.com") - - client.succeed("send-message") - - server.succeed("prosodyctl deluser cthon98@example.com") - server.succeed("prosodyctl deluser azurediamond@example.com") + server.succeed("create-prosody-users") + client.succeed('send-message 2>&1 | grep "XMPP SCRIPT TEST SUCCESS"') + server.succeed("delete-prosody-users") ''; } diff --git a/nixos/tests/xmpp/xmpp-sendmessage.nix b/nixos/tests/xmpp/xmpp-sendmessage.nix index 2a075a01813..349b9c6f38e 100644 --- a/nixos/tests/xmpp/xmpp-sendmessage.nix +++ b/nixos/tests/xmpp/xmpp-sendmessage.nix @@ -1,46 +1,61 @@ -{ writeScriptBin, python3, connectTo ? "localhost" }: -writeScriptBin "send-message" '' - #!${(python3.withPackages (ps: [ ps.sleekxmpp ])).interpreter} - # Based on the sleekxmpp send_client example, look there for more details: - # https://github.com/fritzy/SleekXMPP/blob/develop/examples/send_client.py - import sleekxmpp - - class SendMsgBot(sleekxmpp.ClientXMPP): - """ - A basic SleekXMPP bot that will log in, send a message, - and then log out. - """ - def __init__(self, jid, password, recipient, message): - sleekxmpp.ClientXMPP.__init__(self, jid, password) - - self.recipient = recipient - self.msg = message - - self.add_event_handler("session_start", self.start, threaded=True) - - def start(self, event): - self.send_presence() - self.get_roster() - - self.send_message(mto=self.recipient, - mbody=self.msg, - mtype='chat') - - self.disconnect(wait=True) - - - if __name__ == '__main__': - xmpp = SendMsgBot("cthon98@example.com", "nothunter2", "azurediamond@example.com", "hey, if you type in your pw, it will show as stars") - xmpp.register_plugin('xep_0030') # Service Discovery - xmpp.register_plugin('xep_0199') # XMPP Ping - - # TODO: verify certificate - # If you want to verify the SSL certificates offered by a server: - # xmpp.ca_certs = "path/to/ca/cert" - - if xmpp.connect(('${connectTo}', 5222)): - xmpp.process(block=True) - else: - print("Unable to connect.") - sys.exit(1) +{ writeScriptBin, writeText, python3, connectTo ? "localhost" }: +let + dummyFile = writeText "dummy-file" '' + Dear dog, + + Please find this *really* important attachment. + + Yours truly, + John + ''; +in writeScriptBin "send-message" '' +#!${(python3.withPackages (ps: [ ps.slixmpp ])).interpreter} +import logging +import sys +from types import MethodType + +from slixmpp import ClientXMPP +from slixmpp.exceptions import IqError, IqTimeout + + +class CthonTest(ClientXMPP): + + def __init__(self, jid, password): + ClientXMPP.__init__(self, jid, password) + self.add_event_handler("session_start", self.session_start) + + async def session_start(self, event): + log = logging.getLogger(__name__) + self.send_presence() + self.get_roster() + # Sending a test message + self.send_message(mto="azurediamond@example.com", mbody="Hello, this is dog.", mtype="chat") + log.info('Message sent') + + # Test http upload (XEP_0363) + def timeout_callback(arg): + log.error("ERROR: Cannot upload file. XEP_0363 seems broken") + sys.exit(1) + url = await self['xep_0363'].upload_file("${dummyFile}",timeout=10, timeout_callback=timeout_callback) + log.info('Upload success!') + # Test MUC + self.plugin['xep_0045'].join_muc('testMucRoom', 'cthon98', wait=True) + log.info('MUC join success!') + log.info('XMPP SCRIPT TEST SUCCESS') + self.disconnect(wait=True) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG, + format='%(levelname)-8s %(message)s') + + ct = CthonTest('cthon98@example.com', 'nothunter2') + ct.register_plugin('xep_0071') + ct.register_plugin('xep_0128') + # HTTP Upload + ct.register_plugin('xep_0363') + # MUC + ct.register_plugin('xep_0045') + ct.connect(("server", 5222)) + ct.process(forever=False) '' diff --git a/pkgs/servers/xmpp/prosody/default.nix b/pkgs/servers/xmpp/prosody/default.nix index 0d552e9e112..702da004b42 100644 --- a/pkgs/servers/xmpp/prosody/default.nix +++ b/pkgs/servers/xmpp/prosody/default.nix @@ -1,4 +1,4 @@ -{ stdenv, fetchurl, libidn, openssl, makeWrapper, fetchhg +{ stdenv, fetchurl, lib, libidn, openssl, makeWrapper, fetchhg , lua5, luasocket, luasec, luaexpat, luafilesystem, luabitop , withLibevent ? true, luaevent ? null , withDBI ? true, luadbi ? null @@ -16,7 +16,16 @@ with stdenv.lib; stdenv.mkDerivation rec { version = "0.11.5"; # also update communityModules pname = "prosody"; - + # The following community modules are necessary for the nixos module + # prosody module to comply with XEP-0423 and provide a working + # default setup. + nixosModuleDeps = [ + "bookmarks" + "cloud_notify" + "vcard_muc" + "smacks" + "http_upload" + ]; src = fetchurl { url = "https://prosody.im/downloads/source/${pname}-${version}.tar.gz"; sha256 = "12s0hn6hvjbi61cdw3165l6iw0878971dmlvfg663byjsmjvvy2m"; @@ -52,7 +61,7 @@ stdenv.mkDerivation rec { postInstall = '' ${concatMapStringsSep "\n" (module: '' cp -r $communityModules/mod_${module} $out/lib/prosody/modules/ - '') (withCommunityModules ++ withOnlyInstalledCommunityModules)} + '') (lib.lists.unique(nixosModuleDeps ++ withCommunityModules ++ withOnlyInstalledCommunityModules))} wrapProgram $out/bin/prosody \ --prefix LUA_PATH ';' "$LUA_PATH" \ --prefix LUA_CPATH ';' "$LUA_CPATH" |