{ config, lib, pkgs, ... }: with lib; let cfg = config.services.jitsi-meet; # The configuration files are JS of format "var <> = <>;". In order to # override only some settings, we need to extract the JSON, use jq to merge it with # the config provided by user, and then reconstruct the file. overrideJs = source: varName: userCfg: appendExtra: let extractor = pkgs.writeText "extractor.js" '' var fs = require("fs"); eval(fs.readFileSync(process.argv[2], 'utf8')); process.stdout.write(JSON.stringify(eval(process.argv[3]))); ''; userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg); in (pkgs.runCommand "${varName}.js" { } '' ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json ( echo "var ${varName} = " ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson} echo ";" echo ${escapeShellArg appendExtra} ) > $out ''); # Essential config - it's probably not good to have these as option default because # types.attrs doesn't do merging. Let's merge explicitly, can still be overridden if # user desires. defaultCfg = { hosts = { domain = cfg.hostName; muc = "conference.${cfg.hostName}"; focus = "focus.${cfg.hostName}"; }; bosh = "//${cfg.hostName}/http-bind"; websocket = "wss://${cfg.hostName}/xmpp-websocket"; fileRecordingsEnabled = true; liveStreamingEnabled = true; hiddenDomain = "recorder.${cfg.hostName}"; }; in { options.services.jitsi-meet = with types; { enable = mkEnableOption (lib.mdDoc "Jitsi Meet - Secure, Simple and Scalable Video Conferences"); hostName = mkOption { type = str; example = "meet.example.org"; description = lib.mdDoc '' FQDN of the Jitsi Meet instance. ''; }; config = mkOption { type = attrs; default = { }; example = literalExpression '' { enableWelcomePage = false; defaultLang = "fi"; } ''; description = lib.mdDoc '' Client-side web application settings that override the defaults in {file}`config.js`. See for default configuration with comments. ''; }; extraConfig = mkOption { type = lines; default = ""; description = lib.mdDoc '' Text to append to {file}`config.js` web application config file. Can be used to insert JavaScript logic to determine user's region in cascading bridges setup. ''; }; interfaceConfig = mkOption { type = attrs; default = { }; example = literalExpression '' { SHOW_JITSI_WATERMARK = false; SHOW_WATERMARK_FOR_GUESTS = false; } ''; description = lib.mdDoc '' Client-side web-app interface settings that override the defaults in {file}`interface_config.js`. See for default configuration with comments. ''; }; videobridge = { enable = mkOption { type = bool; default = true; description = lib.mdDoc '' Jitsi Videobridge instance and configure it to connect to Prosody. Additional configuration is possible with {option}`services.jitsi-videobridge` ''; }; passwordFile = mkOption { type = nullOr str; default = null; example = "/run/keys/videobridge"; description = lib.mdDoc '' File containing password to the Prosody account for videobridge. If `null`, a file with password will be generated automatically. Setting this option is useful if you plan to connect additional videobridges to the XMPP server. ''; }; }; jicofo.enable = mkOption { type = bool; default = true; description = lib.mdDoc '' Whether to enable JiCoFo instance and configure it to connect to Prosody. Additional configuration is possible with {option}`services.jicofo`. ''; }; jibri.enable = mkOption { type = bool; default = false; description = lib.mdDoc '' Whether to enable a Jibri instance and configure it to connect to Prosody. Additional configuration is possible with {option}`services.jibri`, and {option}`services.jibri.finalizeScript` is especially useful. ''; }; nginx.enable = mkOption { type = bool; default = true; description = lib.mdDoc '' Whether to enable nginx virtual host that will serve the javascript application and act as a proxy for the XMPP server. Further nginx configuration can be done by adapting {option}`services.nginx.virtualHosts.`. When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable this, set the {option}`services.nginx.virtualHosts..enableACME` to `false` and if appropriate do the same for {option}`services.nginx.virtualHosts..forceSSL`. ''; }; caddy.enable = mkEnableOption (lib.mdDoc "Whether to enable caddy reverse proxy to expose jitsi-meet"); prosody.enable = mkOption { type = bool; default = true; description = lib.mdDoc '' Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this off if you want to configure it manually. ''; }; excalidraw.enable = mkEnableOption (lib.mdDoc "Excalidraw collaboration backend for Jitsi"); excalidraw.port = mkOption { type = types.port; default = 3002; description = lib.mdDoc ''The port which the Excalidraw backend for Jitsi should listen to.''; }; secureDomain.enable = mkEnableOption (lib.mdDoc "Authenticated room creation"); }; config = mkIf cfg.enable { services.prosody = mkIf cfg.prosody.enable { enable = mkDefault true; xmppComplianceSuite = mkDefault false; modules = { admin_adhoc = mkDefault false; bosh = mkDefault true; ping = mkDefault true; roster = mkDefault true; saslauth = mkDefault true; smacks = mkDefault true; tls = mkDefault true; websocket = mkDefault true; }; muc = [ { domain = "conference.${cfg.hostName}"; name = "Jitsi Meet MUC"; roomLocking = false; roomDefaultPublicJids = true; extraConfig = '' restrict_room_creation = true storage = "memory" admins = { "focus@auth.${cfg.hostName}" } ''; } { domain = "breakout.${cfg.hostName}"; name = "Jitsi Meet Breakout MUC"; roomLocking = false; roomDefaultPublicJids = true; extraConfig = '' restrict_room_creation = true storage = "memory" admins = { "focus@auth.${cfg.hostName}" } ''; } { domain = "internal.auth.${cfg.hostName}"; name = "Jitsi Meet Videobridge MUC"; roomLocking = false; roomDefaultPublicJids = true; extraConfig = '' storage = "memory" admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" } ''; #-- muc_room_cache_size = 1000 } { domain = "lobby.${cfg.hostName}"; name = "Jitsi Meet Lobby MUC"; roomLocking = false; roomDefaultPublicJids = true; extraConfig = '' restrict_room_creation = true storage = "memory" ''; } ]; extraModules = [ "pubsub" "smacks" "speakerstats" "external_services" "conference_duration" "end_conference" "muc_lobby_rooms" "muc_breakout_rooms" "av_moderation" "muc_hide_all" "muc_meeting_id" "muc_domain_mapper" "muc_rate_limit" "limits_exception" "persistent_lobby" "room_metadata" ]; extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ]; extraConfig = lib.mkMerge [ (mkAfter '' Component "focus.${cfg.hostName}" "client_proxy" target_address = "focus@auth.${cfg.hostName}" Component "speakerstats.${cfg.hostName}" "speakerstats_component" muc_component = "conference.${cfg.hostName}" Component "conferenceduration.${cfg.hostName}" "conference_duration_component" muc_component = "conference.${cfg.hostName}" Component "endconference.${cfg.hostName}" "end_conference" muc_component = "conference.${cfg.hostName}" Component "avmoderation.${cfg.hostName}" "av_moderation_component" muc_component = "conference.${cfg.hostName}" Component "metadata.${cfg.hostName}" "room_metadata_component" muc_component = "conference.${cfg.hostName}" breakout_rooms_component = "breakout.${cfg.hostName}" '') (mkBefore '' muc_mapper_domain_base = "${cfg.hostName}" cross_domain_websocket = true; consider_websocket_secure = true; unlimited_jids = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" } '') ]; virtualHosts.${cfg.hostName} = { enabled = true; domain = cfg.hostName; extraConfig = '' authentication = ${if cfg.secureDomain.enable then "\"internal_hashed\"" else "\"jitsi-anonymous\""} c2s_require_encryption = false admins = { "focus@auth.${cfg.hostName}" } smacks_max_unacked_stanzas = 5 smacks_hibernation_time = 60 smacks_max_hibernated_sessions = 1 smacks_max_old_sessions = 1 av_moderation_component = "avmoderation.${cfg.hostName}" speakerstats_component = "speakerstats.${cfg.hostName}" conference_duration_component = "conferenceduration.${cfg.hostName}" end_conference_component = "endconference.${cfg.hostName}" c2s_require_encryption = false lobby_muc = "lobby.${cfg.hostName}" breakout_rooms_muc = "breakout.${cfg.hostName}" room_metadata_component = "metadata.${cfg.hostName}" main_muc = "conference.${cfg.hostName}" ''; ssl = { cert = "/var/lib/jitsi-meet/jitsi-meet.crt"; key = "/var/lib/jitsi-meet/jitsi-meet.key"; }; }; virtualHosts."auth.${cfg.hostName}" = { enabled = true; domain = "auth.${cfg.hostName}"; extraConfig = '' authentication = "internal_hashed" ''; ssl = { cert = "/var/lib/jitsi-meet/jitsi-meet.crt"; key = "/var/lib/jitsi-meet/jitsi-meet.key"; }; }; virtualHosts."recorder.${cfg.hostName}" = { enabled = true; domain = "recorder.${cfg.hostName}"; extraConfig = '' authentication = "internal_plain" c2s_require_encryption = false ''; }; virtualHosts."guest.${cfg.hostName}" = { enabled = true; domain = "guest.${cfg.hostName}"; extraConfig = '' authentication = "anonymous" c2s_require_encryption = false ''; }; }; systemd.services.prosody = mkIf cfg.prosody.enable { preStart = let videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret"; in '' ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)" ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})" ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName} ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)" ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)" ''; serviceConfig = { EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ]; SupplementaryGroups = [ "jitsi-meet" ]; }; reloadIfChanged = true; }; users.groups.jitsi-meet = { }; systemd.tmpfiles.rules = [ "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -" ]; systemd.services.jitsi-meet-init-secrets = { wantedBy = [ "multi-user.target" ]; before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service"); serviceConfig = { Type = "oneshot"; }; script = let secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret"); in '' cd /var/lib/jitsi-meet ${concatMapStringsSep "\n" (s: '' if [ ! -f ${s} ]; then tr -dc a-zA-Z0-9 ${s} chown root:jitsi-meet ${s} chmod 640 ${s} fi '') secrets} # for easy access in prosody echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env chown root:jitsi-meet secrets-env chmod 640 secrets-env '' + optionalString cfg.prosody.enable '' # generate self-signed certificates if [ ! -f /var/lib/jitsi-meet.crt ]; then ${getBin pkgs.openssl}/bin/openssl req \ -x509 \ -newkey rsa:4096 \ -keyout /var/lib/jitsi-meet/jitsi-meet.key \ -out /var/lib/jitsi-meet/jitsi-meet.crt \ -days 36500 \ -nodes \ -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}' chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key} chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key} fi ''; }; systemd.services.jitsi-excalidraw = mkIf cfg.excalidraw.enable { description = "Excalidraw collaboration backend for Jitsi"; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; environment.PORT = toString cfg.excalidraw.port; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.jitsi-excalidraw}/bin/jitsi-excalidraw-backend"; Restart = "on-failure"; Group = "jitsi-meet"; }; }; services.nginx = mkIf cfg.nginx.enable { enable = mkDefault true; virtualHosts.${cfg.hostName} = { enableACME = mkDefault true; forceSSL = mkDefault true; root = pkgs.jitsi-meet; extraConfig = '' ssi on; ''; locations."@root_path".extraConfig = '' rewrite ^/(.*)$ / break; ''; locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path"; locations."^~ /xmpp-websocket" = { priority = 100; proxyPass = "http://localhost:5280/xmpp-websocket"; proxyWebsockets = true; }; locations."=/http-bind" = { proxyPass = "http://localhost:5280/http-bind"; extraConfig = '' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; ''; }; locations."=/external_api.js" = mkDefault { alias = "${pkgs.jitsi-meet}/libs/external_api.min.js"; }; locations."=/_api/room-info" = { proxyPass = "http://localhost:5280/room-info"; extraConfig = '' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; ''; }; locations."=/config.js" = mkDefault { alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig; }; locations."=/interface_config.js" = mkDefault { alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""; }; locations."/socket.io/" = mkIf cfg.excalidraw.enable { proxyPass = "http://127.0.0.1:${toString cfg.excalidraw.port}"; proxyWebsockets = true; }; }; }; services.caddy = mkIf cfg.caddy.enable { enable = mkDefault true; virtualHosts.${cfg.hostName} = { extraConfig = let templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" { } '' cp -R ${pkgs.jitsi-meet}/* . for file in *.html **/*.html ; do ${pkgs.sd}/bin/sd '' '{{ include "$1" }}' $file done rm config.js rm interface_config.js cp -R . $out cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js cp ./libs/external_api.min.js $out/external_api.js ''; in '' handle /http-bind { header Host ${cfg.hostName} reverse_proxy 127.0.0.1:5280 } handle /xmpp-websocket { reverse_proxy 127.0.0.1:5280 } handle { templates root * ${templatedJitsiMeet} try_files {path} {path} try_files {path} /index.html file_server } ''; }; }; services.jitsi-meet.config = recursiveUpdate (mkIf cfg.excalidraw.enable { whiteboard = { enabled = true; collabServerBaseUrl = "https://${cfg.hostName}"; }; }) (mkIf cfg.secureDomain.enable { hosts.anonymousdomain = "guest.${cfg.hostName}"; }); services.jitsi-videobridge = mkIf cfg.videobridge.enable { enable = true; xmppConfigs."localhost" = { userName = "jvb"; domain = "auth.${cfg.hostName}"; passwordFile = "/var/lib/jitsi-meet/videobridge-secret"; mucJids = "jvbbrewery@internal.auth.${cfg.hostName}"; disableCertificateVerification = true; }; }; services.jicofo = mkIf cfg.jicofo.enable { enable = true; xmppHost = "localhost"; xmppDomain = cfg.hostName; userDomain = "auth.${cfg.hostName}"; userName = "focus"; userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret"; componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret"; bridgeMuc = "jvbbrewery@internal.auth.${cfg.hostName}"; config = mkMerge [{ jicofo.xmpp.service.disable-certificate-verification = true; jicofo.xmpp.client.disable-certificate-verification = true; } (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) { jicofo.jibri = { brewery-jid = "JibriBrewery@internal.auth.${cfg.hostName}"; pending-timeout = "90"; }; }) (lib.mkIf cfg.secureDomain.enable { jicofo = { authentication = { enabled = "true"; type = "XMPP"; login-url = cfg.hostName; }; xmpp.client.client-proxy = "focus.${cfg.hostName}"; }; })]; }; services.jibri = mkIf cfg.jibri.enable { enable = true; xmppEnvironments."jitsi-meet" = { xmppServerHosts = [ "localhost" ]; xmppDomain = cfg.hostName; control.muc = { domain = "internal.auth.${cfg.hostName}"; roomName = "JibriBrewery"; nickname = "jibri"; }; control.login = { domain = "auth.${cfg.hostName}"; username = "jibri"; passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret"; }; call.login = { domain = "recorder.${cfg.hostName}"; username = "recorder"; passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret"; }; usageTimeout = "0"; disableCertificateVerification = true; stripFromRoomDomain = "conference."; }; }; }; meta.doc = ./jitsi-meet.md; meta.maintainers = lib.teams.jitsi.members; }