at 23.11-pre 16 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.jitsi-meet; 7 8 # The configuration files are JS of format "var <<string>> = <<JSON>>;". In order to 9 # override only some settings, we need to extract the JSON, use jq to merge it with 10 # the config provided by user, and then reconstruct the file. 11 overrideJs = 12 source: varName: userCfg: appendExtra: 13 let 14 extractor = pkgs.writeText "extractor.js" '' 15 var fs = require("fs"); 16 eval(fs.readFileSync(process.argv[2], 'utf8')); 17 process.stdout.write(JSON.stringify(eval(process.argv[3]))); 18 ''; 19 userJson = pkgs.writeText "user.json" (builtins.toJSON userCfg); 20 in (pkgs.runCommand "${varName}.js" { } '' 21 ${pkgs.nodejs}/bin/node ${extractor} ${source} ${varName} > default.json 22 ( 23 echo "var ${varName} = " 24 ${pkgs.jq}/bin/jq -s '.[0] * .[1]' default.json ${userJson} 25 echo ";" 26 echo ${escapeShellArg appendExtra} 27 ) > $out 28 ''); 29 30 # Essential config - it's probably not good to have these as option default because 31 # types.attrs doesn't do merging. Let's merge explicitly, can still be overridden if 32 # user desires. 33 defaultCfg = { 34 hosts = { 35 domain = cfg.hostName; 36 muc = "conference.${cfg.hostName}"; 37 focus = "focus.${cfg.hostName}"; 38 }; 39 bosh = "//${cfg.hostName}/http-bind"; 40 websocket = "wss://${cfg.hostName}/xmpp-websocket"; 41 42 fileRecordingsEnabled = true; 43 liveStreamingEnabled = true; 44 hiddenDomain = "recorder.${cfg.hostName}"; 45 }; 46in 47{ 48 options.services.jitsi-meet = with types; { 49 enable = mkEnableOption (lib.mdDoc "Jitsi Meet - Secure, Simple and Scalable Video Conferences"); 50 51 hostName = mkOption { 52 type = str; 53 example = "meet.example.org"; 54 description = lib.mdDoc '' 55 FQDN of the Jitsi Meet instance. 56 ''; 57 }; 58 59 config = mkOption { 60 type = attrs; 61 default = { }; 62 example = literalExpression '' 63 { 64 enableWelcomePage = false; 65 defaultLang = "fi"; 66 } 67 ''; 68 description = lib.mdDoc '' 69 Client-side web application settings that override the defaults in {file}`config.js`. 70 71 See <https://github.com/jitsi/jitsi-meet/blob/master/config.js> for default 72 configuration with comments. 73 ''; 74 }; 75 76 extraConfig = mkOption { 77 type = lines; 78 default = ""; 79 description = lib.mdDoc '' 80 Text to append to {file}`config.js` web application config file. 81 82 Can be used to insert JavaScript logic to determine user's region in cascading bridges setup. 83 ''; 84 }; 85 86 interfaceConfig = mkOption { 87 type = attrs; 88 default = { }; 89 example = literalExpression '' 90 { 91 SHOW_JITSI_WATERMARK = false; 92 SHOW_WATERMARK_FOR_GUESTS = false; 93 } 94 ''; 95 description = lib.mdDoc '' 96 Client-side web-app interface settings that override the defaults in {file}`interface_config.js`. 97 98 See <https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js> for 99 default configuration with comments. 100 ''; 101 }; 102 103 videobridge = { 104 enable = mkOption { 105 type = bool; 106 default = true; 107 description = lib.mdDoc '' 108 Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody. 109 110 Additional configuration is possible with {option}`services.jitsi-videobridge`. 111 ''; 112 }; 113 114 passwordFile = mkOption { 115 type = nullOr str; 116 default = null; 117 example = "/run/keys/videobridge"; 118 description = lib.mdDoc '' 119 File containing password to the Prosody account for videobridge. 120 121 If `null`, a file with password will be generated automatically. Setting 122 this option is useful if you plan to connect additional videobridges to the XMPP server. 123 ''; 124 }; 125 }; 126 127 jicofo.enable = mkOption { 128 type = bool; 129 default = true; 130 description = lib.mdDoc '' 131 Whether to enable JiCoFo instance and configure it to connect to Prosody. 132 133 Additional configuration is possible with {option}`services.jicofo`. 134 ''; 135 }; 136 137 jibri.enable = mkOption { 138 type = bool; 139 default = false; 140 description = lib.mdDoc '' 141 Whether to enable a Jibri instance and configure it to connect to Prosody. 142 143 Additional configuration is possible with {option}`services.jibri`, and 144 {option}`services.jibri.finalizeScript` is especially useful. 145 ''; 146 }; 147 148 nginx.enable = mkOption { 149 type = bool; 150 default = true; 151 description = lib.mdDoc '' 152 Whether to enable nginx virtual host that will serve the javascript application and act as 153 a proxy for the XMPP server. Further nginx configuration can be done by adapting 154 {option}`services.nginx.virtualHosts.<hostName>`. 155 When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable 156 this, set the {option}`services.nginx.virtualHosts.<hostName>.enableACME` to 157 `false` and if appropriate do the same for 158 {option}`services.nginx.virtualHosts.<hostName>.forceSSL`. 159 ''; 160 }; 161 162 caddy.enable = mkEnableOption (lib.mdDoc "Whether to enable caddy reverse proxy to expose jitsi-meet"); 163 164 prosody.enable = mkOption { 165 type = bool; 166 default = true; 167 description = lib.mdDoc '' 168 Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this 169 off if you want to configure it manually. 170 ''; 171 }; 172 }; 173 174 config = mkIf cfg.enable { 175 services.prosody = mkIf cfg.prosody.enable { 176 enable = mkDefault true; 177 xmppComplianceSuite = mkDefault false; 178 modules = { 179 admin_adhoc = mkDefault false; 180 bosh = mkDefault true; 181 ping = mkDefault true; 182 roster = mkDefault true; 183 saslauth = mkDefault true; 184 smacks = mkDefault true; 185 tls = mkDefault true; 186 websocket = mkDefault true; 187 }; 188 muc = [ 189 { 190 domain = "conference.${cfg.hostName}"; 191 name = "Jitsi Meet MUC"; 192 roomLocking = false; 193 roomDefaultPublicJids = true; 194 extraConfig = '' 195 storage = "memory" 196 ''; 197 } 198 { 199 domain = "internal.${cfg.hostName}"; 200 name = "Jitsi Meet Videobridge MUC"; 201 extraConfig = '' 202 storage = "memory" 203 admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" } 204 ''; 205 #-- muc_room_cache_size = 1000 206 } 207 ]; 208 extraModules = [ "pubsub" "smacks" ]; 209 extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ]; 210 extraConfig = lib.mkMerge [ (mkAfter '' 211 Component "focus.${cfg.hostName}" "client_proxy" 212 target_address = "focus@auth.${cfg.hostName}" 213 '') 214 (mkBefore '' 215 cross_domain_websocket = true; 216 consider_websocket_secure = true; 217 '') 218 ]; 219 virtualHosts.${cfg.hostName} = { 220 enabled = true; 221 domain = cfg.hostName; 222 extraConfig = '' 223 authentication = "anonymous" 224 c2s_require_encryption = false 225 admins = { "focus@auth.${cfg.hostName}" } 226 smacks_max_unacked_stanzas = 5 227 smacks_hibernation_time = 60 228 smacks_max_hibernated_sessions = 1 229 smacks_max_old_sessions = 1 230 ''; 231 ssl = { 232 cert = "/var/lib/jitsi-meet/jitsi-meet.crt"; 233 key = "/var/lib/jitsi-meet/jitsi-meet.key"; 234 }; 235 }; 236 virtualHosts."auth.${cfg.hostName}" = { 237 enabled = true; 238 domain = "auth.${cfg.hostName}"; 239 extraConfig = '' 240 authentication = "internal_plain" 241 ''; 242 ssl = { 243 cert = "/var/lib/jitsi-meet/jitsi-meet.crt"; 244 key = "/var/lib/jitsi-meet/jitsi-meet.key"; 245 }; 246 }; 247 virtualHosts."recorder.${cfg.hostName}" = { 248 enabled = true; 249 domain = "recorder.${cfg.hostName}"; 250 extraConfig = '' 251 authentication = "internal_plain" 252 c2s_require_encryption = false 253 ''; 254 }; 255 }; 256 systemd.services.prosody = mkIf cfg.prosody.enable { 257 preStart = let 258 videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret"; 259 in '' 260 ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)" 261 ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})" 262 ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName} 263 ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)" 264 ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)" 265 ''; 266 serviceConfig = { 267 EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ]; 268 SupplementaryGroups = [ "jitsi-meet" ]; 269 }; 270 reloadIfChanged = true; 271 }; 272 273 users.groups.jitsi-meet = {}; 274 systemd.tmpfiles.rules = [ 275 "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -" 276 ]; 277 278 systemd.services.jitsi-meet-init-secrets = { 279 wantedBy = [ "multi-user.target" ]; 280 before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service"); 281 serviceConfig = { 282 Type = "oneshot"; 283 }; 284 285 script = let 286 secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret"); 287 in 288 '' 289 cd /var/lib/jitsi-meet 290 ${concatMapStringsSep "\n" (s: '' 291 if [ ! -f ${s} ]; then 292 tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s} 293 chown root:jitsi-meet ${s} 294 chmod 640 ${s} 295 fi 296 '') secrets} 297 298 # for easy access in prosody 299 echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env 300 chown root:jitsi-meet secrets-env 301 chmod 640 secrets-env 302 '' 303 + optionalString cfg.prosody.enable '' 304 # generate self-signed certificates 305 if [ ! -f /var/lib/jitsi-meet.crt ]; then 306 ${getBin pkgs.openssl}/bin/openssl req \ 307 -x509 \ 308 -newkey rsa:4096 \ 309 -keyout /var/lib/jitsi-meet/jitsi-meet.key \ 310 -out /var/lib/jitsi-meet/jitsi-meet.crt \ 311 -days 36500 \ 312 -nodes \ 313 -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}' 314 chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key} 315 chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key} 316 fi 317 ''; 318 }; 319 320 services.nginx = mkIf cfg.nginx.enable { 321 enable = mkDefault true; 322 virtualHosts.${cfg.hostName} = { 323 enableACME = mkDefault true; 324 forceSSL = mkDefault true; 325 root = pkgs.jitsi-meet; 326 extraConfig = '' 327 ssi on; 328 ''; 329 locations."@root_path".extraConfig = '' 330 rewrite ^/(.*)$ / break; 331 ''; 332 locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path"; 333 locations."^~ /xmpp-websocket" = { 334 priority = 100; 335 proxyPass = "http://localhost:5280/xmpp-websocket"; 336 proxyWebsockets = true; 337 }; 338 locations."=/http-bind" = { 339 proxyPass = "http://localhost:5280/http-bind"; 340 extraConfig = '' 341 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 342 proxy_set_header Host $host; 343 ''; 344 }; 345 locations."=/external_api.js" = mkDefault { 346 alias = "${pkgs.jitsi-meet}/libs/external_api.min.js"; 347 }; 348 locations."=/config.js" = mkDefault { 349 alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig; 350 }; 351 locations."=/interface_config.js" = mkDefault { 352 alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""; 353 }; 354 }; 355 }; 356 357 services.caddy = mkIf cfg.caddy.enable { 358 enable = mkDefault true; 359 virtualHosts.${cfg.hostName} = { 360 extraConfig = 361 let 362 templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" {} '' 363 cp -R ${pkgs.jitsi-meet}/* . 364 for file in *.html **/*.html ; do 365 ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file 366 done 367 rm config.js 368 rm interface_config.js 369 cp -R . $out 370 cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js 371 cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js 372 cp ./libs/external_api.min.js $out/external_api.js 373 ''; 374 in '' 375 handle /http-bind { 376 header Host ${cfg.hostName} 377 reverse_proxy 127.0.0.1:5280 378 } 379 handle /xmpp-websocket { 380 reverse_proxy 127.0.0.1:5280 381 } 382 handle { 383 templates 384 root * ${templatedJitsiMeet} 385 try_files {path} {path} 386 try_files {path} /index.html 387 file_server 388 } 389 ''; 390 }; 391 }; 392 393 services.jitsi-videobridge = mkIf cfg.videobridge.enable { 394 enable = true; 395 xmppConfigs."localhost" = { 396 userName = "jvb"; 397 domain = "auth.${cfg.hostName}"; 398 passwordFile = "/var/lib/jitsi-meet/videobridge-secret"; 399 mucJids = "jvbbrewery@internal.${cfg.hostName}"; 400 disableCertificateVerification = true; 401 }; 402 }; 403 404 services.jicofo = mkIf cfg.jicofo.enable { 405 enable = true; 406 xmppHost = "localhost"; 407 xmppDomain = cfg.hostName; 408 userDomain = "auth.${cfg.hostName}"; 409 userName = "focus"; 410 userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret"; 411 componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret"; 412 bridgeMuc = "jvbbrewery@internal.${cfg.hostName}"; 413 config = mkMerge [{ 414 jicofo.xmpp.service.disable-certificate-verification = true; 415 jicofo.xmpp.client.disable-certificate-verification = true; 416 #} (lib.mkIf cfg.jibri.enable { 417 } (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) { 418 jicofo.jibri = { 419 brewery-jid = "JibriBrewery@internal.${cfg.hostName}"; 420 pending-timeout = "90"; 421 }; 422 })]; 423 }; 424 425 services.jibri = mkIf cfg.jibri.enable { 426 enable = true; 427 428 xmppEnvironments."jitsi-meet" = { 429 xmppServerHosts = [ "localhost" ]; 430 xmppDomain = cfg.hostName; 431 432 control.muc = { 433 domain = "internal.${cfg.hostName}"; 434 roomName = "JibriBrewery"; 435 nickname = "jibri"; 436 }; 437 438 control.login = { 439 domain = "auth.${cfg.hostName}"; 440 username = "jibri"; 441 passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret"; 442 }; 443 444 call.login = { 445 domain = "recorder.${cfg.hostName}"; 446 username = "recorder"; 447 passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret"; 448 }; 449 450 usageTimeout = "0"; 451 disableCertificateVerification = true; 452 stripFromRoomDomain = "conference."; 453 }; 454 }; 455 }; 456 457 meta.doc = ./jitsi-meet.md; 458 meta.maintainers = lib.teams.jitsi.members; 459}