grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / jitsi-meet.nix
blob39aa7379c0edf4fdd745c91950702889c0c8714f
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.jitsi-meet;
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     '');
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       jigasi = "jigasi.${cfg.hostName}";
39     };
40     bosh = "//${cfg.hostName}/http-bind";
41     websocket = "wss://${cfg.hostName}/xmpp-websocket";
43     fileRecordingsEnabled = true;
44     liveStreamingEnabled = true;
45     hiddenDomain = "recorder.${cfg.hostName}";
46   };
49   options.services.jitsi-meet = with types; {
50     enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
52     hostName = mkOption {
53       type = str;
54       example = "meet.example.org";
55       description = ''
56         FQDN of the Jitsi Meet instance.
57       '';
58     };
60     config = mkOption {
61       type = attrs;
62       default = { };
63       example = literalExpression ''
64         {
65           enableWelcomePage = false;
66           defaultLang = "fi";
67         }
68       '';
69       description = ''
70         Client-side web application settings that override the defaults in {file}`config.js`.
72         See <https://github.com/jitsi/jitsi-meet/blob/master/config.js> for default
73         configuration with comments.
74       '';
75     };
77     extraConfig = mkOption {
78       type = lines;
79       default = "";
80       description = ''
81         Text to append to {file}`config.js` web application config file.
83         Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
84       '';
85     };
87     interfaceConfig = mkOption {
88       type = attrs;
89       default = { };
90       example = literalExpression ''
91         {
92           SHOW_JITSI_WATERMARK = false;
93           SHOW_WATERMARK_FOR_GUESTS = false;
94         }
95       '';
96       description = ''
97         Client-side web-app interface settings that override the defaults in {file}`interface_config.js`.
99         See <https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js> for
100         default configuration with comments.
101       '';
102     };
104     videobridge = {
105       enable = mkOption {
106         type = bool;
107         default = true;
108         description = ''
109           Jitsi Videobridge instance and configure it to connect to Prosody.
111           Additional configuration is possible with {option}`services.jitsi-videobridge`
112         '';
113       };
115       passwordFile = mkOption {
116         type = nullOr str;
117         default = null;
118         example = "/run/keys/videobridge";
119         description = ''
120           File containing password to the Prosody account for videobridge.
122           If `null`, a file with password will be generated automatically. Setting
123           this option is useful if you plan to connect additional videobridges to the XMPP server.
124         '';
125       };
126     };
128     jicofo.enable = mkOption {
129       type = bool;
130       default = true;
131       description = ''
132         Whether to enable JiCoFo instance and configure it to connect to Prosody.
134         Additional configuration is possible with {option}`services.jicofo`.
135       '';
136     };
138     jibri.enable = mkOption {
139       type = bool;
140       default = false;
141       description = ''
142         Whether to enable a Jibri instance and configure it to connect to Prosody.
144         Additional configuration is possible with {option}`services.jibri`, and
145         {option}`services.jibri.finalizeScript` is especially useful.
146       '';
147     };
149     jigasi.enable = mkOption {
150       type = bool;
151       default = false;
152       description = ''
153         Whether to enable jigasi instance and configure it to connect to Prosody.
155         Additional configuration is possible with <option>services.jigasi</option>.
156       '';
157     };
159     nginx.enable = mkOption {
160       type = bool;
161       default = true;
162       description = ''
163         Whether to enable nginx virtual host that will serve the javascript application and act as
164         a proxy for the XMPP server. Further nginx configuration can be done by adapting
165         {option}`services.nginx.virtualHosts.<hostName>`.
166         When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
167         this, set the {option}`services.nginx.virtualHosts.<hostName>.enableACME` to
168         `false` and if appropriate do the same for
169         {option}`services.nginx.virtualHosts.<hostName>.forceSSL`.
170       '';
171     };
173     caddy.enable = mkEnableOption "caddy reverse proxy to expose jitsi-meet";
175     prosody.enable = mkOption {
176       type = bool;
177       default = true;
178       description = ''
179         Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
180         off if you want to configure it manually.
181       '';
182     };
184     excalidraw.enable = mkEnableOption "Excalidraw collaboration backend for Jitsi";
185     excalidraw.port = mkOption {
186       type = types.port;
187       default = 3002;
188       description = ''The port which the Excalidraw backend for Jitsi should listen to.'';
189     };
191     secureDomain = {
192       enable = mkEnableOption "Authenticated room creation";
193       authentication = mkOption {
194         type = types.str;
195         default = "internal_hashed";
196         description = ''The authentication type to be used by jitsi'';
197       };
198     };
199   };
201   config = mkIf cfg.enable {
202     services.prosody = mkIf cfg.prosody.enable {
203       enable = mkDefault true;
204       xmppComplianceSuite = mkDefault false;
205       modules = {
206         admin_adhoc = mkDefault false;
207         bosh = mkDefault true;
208         ping = mkDefault true;
209         roster = mkDefault true;
210         saslauth = mkDefault true;
211         smacks = mkDefault true;
212         tls = mkDefault true;
213         websocket = mkDefault true;
214       };
215       muc = [
216         {
217           domain = "conference.${cfg.hostName}";
218           name = "Jitsi Meet MUC";
219           roomLocking = false;
220           roomDefaultPublicJids = true;
221           extraConfig = ''
222             restrict_room_creation = true
223             storage = "memory"
224             admins = { "focus@auth.${cfg.hostName}" }
225           '';
226         }
227         {
228           domain = "breakout.${cfg.hostName}";
229           name = "Jitsi Meet Breakout MUC";
230           roomLocking = false;
231           roomDefaultPublicJids = true;
232           extraConfig = ''
233             restrict_room_creation = true
234             storage = "memory"
235             admins = { "focus@auth.${cfg.hostName}" }
236           '';
237         }
238         {
239           domain = "internal.auth.${cfg.hostName}";
240           name = "Jitsi Meet Videobridge MUC";
241           roomLocking = false;
242           roomDefaultPublicJids = true;
243           extraConfig = ''
244             storage = "memory"
245             admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}", "jigasi@auth.${cfg.hostName}" }
246           '';
247           #-- muc_room_cache_size = 1000
248         }
249         {
250           domain = "lobby.${cfg.hostName}";
251           name = "Jitsi Meet Lobby MUC";
252           roomLocking = false;
253           roomDefaultPublicJids = true;
254           extraConfig = ''
255             restrict_room_creation = true
256             storage = "memory"
257           '';
258         }
259       ];
260       extraModules = [
261         "pubsub"
262         "smacks"
263         "speakerstats"
264         "external_services"
265         "conference_duration"
266         "end_conference"
267         "muc_lobby_rooms"
268         "muc_breakout_rooms"
269         "av_moderation"
270         "muc_hide_all"
271         "muc_meeting_id"
272         "muc_domain_mapper"
273         "muc_rate_limit"
274         "limits_exception"
275         "persistent_lobby"
276         "room_metadata"
277       ];
278       extraPluginPaths = [ "${pkgs.jitsi-meet-prosody}/share/prosody-plugins" ];
279       extraConfig = lib.mkMerge [
280         (mkAfter ''
281           Component "focus.${cfg.hostName}" "client_proxy"
282             target_address = "focus@auth.${cfg.hostName}"
284           Component "jigasi.${cfg.hostName}" "client_proxy"
285             target_address = "jigasi@auth.${cfg.hostName}"
287           Component "speakerstats.${cfg.hostName}" "speakerstats_component"
288             muc_component = "conference.${cfg.hostName}"
290           Component "conferenceduration.${cfg.hostName}" "conference_duration_component"
291             muc_component = "conference.${cfg.hostName}"
293           Component "endconference.${cfg.hostName}" "end_conference"
294             muc_component = "conference.${cfg.hostName}"
296           Component "avmoderation.${cfg.hostName}" "av_moderation_component"
297             muc_component = "conference.${cfg.hostName}"
299           Component "metadata.${cfg.hostName}" "room_metadata_component"
300             muc_component = "conference.${cfg.hostName}"
301             breakout_rooms_component = "breakout.${cfg.hostName}"
302         '')
303         (mkBefore ''
304           muc_mapper_domain_base = "${cfg.hostName}"
306           cross_domain_websocket = true;
307           consider_websocket_secure = true;
309           unlimited_jids = {
310             "focus@auth.${cfg.hostName}",
311             "jvb@auth.${cfg.hostName}"
312           }
313         '')
314       ];
315       virtualHosts.${cfg.hostName} = {
316         enabled = true;
317         domain = cfg.hostName;
318         extraConfig = ''
319           authentication = ${if cfg.secureDomain.enable then "\"${cfg.secureDomain.authentication}\"" else "\"jitsi-anonymous\""}
320           c2s_require_encryption = false
321           admins = { "focus@auth.${cfg.hostName}" }
322           smacks_max_unacked_stanzas = 5
323           smacks_hibernation_time = 60
324           smacks_max_hibernated_sessions = 1
325           smacks_max_old_sessions = 1
327           av_moderation_component = "avmoderation.${cfg.hostName}"
328           speakerstats_component = "speakerstats.${cfg.hostName}"
329           conference_duration_component = "conferenceduration.${cfg.hostName}"
330           end_conference_component = "endconference.${cfg.hostName}"
332           c2s_require_encryption = false
333           lobby_muc = "lobby.${cfg.hostName}"
334           breakout_rooms_muc = "breakout.${cfg.hostName}"
335           room_metadata_component = "metadata.${cfg.hostName}"
336           main_muc = "conference.${cfg.hostName}"
337         '';
338         ssl = {
339           cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
340           key = "/var/lib/jitsi-meet/jitsi-meet.key";
341         };
342       };
343       virtualHosts."auth.${cfg.hostName}" = {
344         enabled = true;
345         domain = "auth.${cfg.hostName}";
346         extraConfig = ''
347           authentication = "internal_hashed"
348         '';
349         ssl = {
350           cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
351           key = "/var/lib/jitsi-meet/jitsi-meet.key";
352         };
353       };
354       virtualHosts."recorder.${cfg.hostName}" = {
355         enabled = true;
356         domain = "recorder.${cfg.hostName}";
357         extraConfig = ''
358           authentication = "internal_plain"
359           c2s_require_encryption = false
360         '';
361       };
362       virtualHosts."guest.${cfg.hostName}" = {
363         enabled = true;
364         domain = "guest.${cfg.hostName}";
365         extraConfig = ''
366           authentication = "anonymous"
367           c2s_require_encryption = false
368         '';
369       };
370     };
371     systemd.services.prosody = mkIf cfg.prosody.enable {
372       preStart = let
373         videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
374       in ''
375         ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
376         ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
377         ${config.services.prosody.package}/bin/prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
378         ${config.services.prosody.package}/bin/prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
379         ${config.services.prosody.package}/bin/prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
380       '' + optionalString cfg.jigasi.enable ''
381         ${config.services.prosody.package}/bin/prosodyctl register jigasi auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jigasi-user-secret)"
382       '';
384       serviceConfig = {
385         EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
386         SupplementaryGroups = [ "jitsi-meet" ];
387       };
388       reloadIfChanged = true;
389     };
391     users.groups.jitsi-meet = { };
392     systemd.tmpfiles.rules = [
393       "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
394     ];
396     systemd.services.jitsi-meet-init-secrets = {
397       wantedBy = [ "multi-user.target" ];
398       before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service") ++ (optional cfg.jigasi.enable "jigasi.service");
399       serviceConfig = {
400         Type = "oneshot";
401         UMask = "027";
402         User = "root";
403         Group = "jitsi-meet";
404         WorkingDirectory = "/var/lib/jitsi-meet";
405       };
407       script = let
408         secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optionals cfg.jigasi.enable [ "jigasi-user-secret" "jigasi-component-secret" ]) ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
409       in
410       ''
411         ${concatMapStringsSep "\n" (s: ''
412           if [ ! -f ${s} ]; then
413             tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
414           fi
415         '') secrets}
417         # for easy access in prosody
418         echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
419         echo "JIGASI_COMPONENT_SECRET=$(cat jigasi-component-secret)" >> secrets-env
420       ''
421       + optionalString cfg.prosody.enable ''
422         # generate self-signed certificates
423         if [ ! -f /var/lib/jitsi-meet/jitsi-meet.crt ]; then
424           ${getBin pkgs.openssl}/bin/openssl req \
425             -x509 \
426             -newkey rsa:4096 \
427             -keyout /var/lib/jitsi-meet/jitsi-meet.key \
428             -out /var/lib/jitsi-meet/jitsi-meet.crt \
429             -days 36500 \
430             -nodes \
431             -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
432           chmod 640 /var/lib/jitsi-meet/jitsi-meet.key
433         fi
434       '';
435     };
437     systemd.services.jitsi-excalidraw = mkIf cfg.excalidraw.enable {
438       description = "Excalidraw collaboration backend for Jitsi";
439       after = [ "network.target" ];
440       wantedBy = [ "multi-user.target" ];
441       environment.PORT = toString cfg.excalidraw.port;
443       serviceConfig = {
444         Type = "simple";
445         ExecStart = "${pkgs.jitsi-excalidraw}/bin/jitsi-excalidraw-backend";
446         Restart = "on-failure";
447         Group = "jitsi-meet";
448       };
449     };
451     services.nginx = mkIf cfg.nginx.enable {
452       enable = mkDefault true;
453       virtualHosts.${cfg.hostName} = {
454         enableACME = mkDefault true;
455         forceSSL = mkDefault true;
456         root = pkgs.jitsi-meet;
457         extraConfig = ''
458           ssi on;
459         '';
460         locations."@root_path".extraConfig = ''
461           rewrite ^/(.*)$ / break;
462         '';
463         locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
464         locations."^~ /xmpp-websocket" = {
465           priority = 100;
466           proxyPass = "http://localhost:5280/xmpp-websocket";
467           proxyWebsockets = true;
468         };
469         locations."=/http-bind" = {
470           proxyPass = "http://localhost:5280/http-bind";
471           extraConfig = ''
472             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
473             proxy_set_header Host $host;
474           '';
475         };
476         locations."=/external_api.js" = mkDefault {
477           alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
478         };
479         locations."=/_api/room-info" = {
480           proxyPass = "http://localhost:5280/room-info";
481           extraConfig = ''
482             proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
483             proxy_set_header Host $host;
484           '';
485         };
486         locations."=/config.js" = mkDefault {
487           alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
488         };
489         locations."=/interface_config.js" = mkDefault {
490           alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
491         };
492         locations."/socket.io/" = mkIf cfg.excalidraw.enable {
493           proxyPass = "http://127.0.0.1:${toString cfg.excalidraw.port}";
494           proxyWebsockets = true;
495         };
496       };
497     };
499     services.caddy = mkIf cfg.caddy.enable {
500       enable = mkDefault true;
501       virtualHosts.${cfg.hostName} = {
502         extraConfig =
503         let
504           templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" { } ''
505             cp -R --no-preserve=all ${pkgs.jitsi-meet}/* .
506             for file in *.html **/*.html ; do
507               ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
508             done
509             rm config.js
510             rm interface_config.js
511             cp -R . $out
512             cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
513             cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
514             cp ./libs/external_api.min.js $out/external_api.js
515           '';
516         in ''
517           handle /http-bind {
518             header Host ${cfg.hostName}
519             reverse_proxy 127.0.0.1:5280
520           }
521           handle /xmpp-websocket {
522             reverse_proxy 127.0.0.1:5280
523           }
524           handle {
525             templates
526             root * ${templatedJitsiMeet}
527             try_files {path} {path}
528             try_files {path} /index.html
529             file_server
530           }
531         '';
532       };
533     };
535     services.jitsi-meet.config = recursiveUpdate
536       (mkIf cfg.excalidraw.enable {
537         whiteboard = {
538           enabled = true;
539           collabServerBaseUrl = "https://${cfg.hostName}";
540         };
541       })
542       (mkIf cfg.secureDomain.enable {
543         hosts.anonymousdomain = "guest.${cfg.hostName}";
544       });
546     services.jitsi-videobridge = mkIf cfg.videobridge.enable {
547       enable = true;
548       xmppConfigs."localhost" = {
549         userName = "jvb";
550         domain = "auth.${cfg.hostName}";
551         passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
552         mucJids = "jvbbrewery@internal.auth.${cfg.hostName}";
553         disableCertificateVerification = true;
554       };
555     };
557     services.jicofo = mkIf cfg.jicofo.enable {
558       enable = true;
559       xmppHost = "localhost";
560       xmppDomain = cfg.hostName;
561       userDomain = "auth.${cfg.hostName}";
562       userName = "focus";
563       userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
564       componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
565       bridgeMuc = "jvbbrewery@internal.auth.${cfg.hostName}";
566       config = mkMerge [{
567         jicofo.xmpp.service.disable-certificate-verification = true;
568         jicofo.xmpp.client.disable-certificate-verification = true;
569       }
570         (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
571           jicofo.jibri = {
572             brewery-jid = "JibriBrewery@internal.auth.${cfg.hostName}";
573             pending-timeout = "90";
574           };
575         })
576         (lib.mkIf cfg.secureDomain.enable {
577           jicofo = {
578             authentication = {
579               enabled = "true";
580               type = "XMPP";
581               login-url = cfg.hostName;
582             };
583             xmpp.client.client-proxy = "focus.${cfg.hostName}";
584           };
585         })];
586     };
588     services.jibri = mkIf cfg.jibri.enable {
589       enable = true;
591       xmppEnvironments."jitsi-meet" = {
592         xmppServerHosts = [ "localhost" ];
593         xmppDomain = cfg.hostName;
595         control.muc = {
596           domain = "internal.auth.${cfg.hostName}";
597           roomName = "JibriBrewery";
598           nickname = "jibri";
599         };
601         control.login = {
602           domain = "auth.${cfg.hostName}";
603           username = "jibri";
604           passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
605         };
607         call.login = {
608           domain = "recorder.${cfg.hostName}";
609           username = "recorder";
610           passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
611         };
613         usageTimeout = "0";
614         disableCertificateVerification = true;
615         stripFromRoomDomain = "conference.";
616       };
617     };
619     services.jigasi = mkIf cfg.jigasi.enable {
620       enable = true;
621       xmppHost = "localhost";
622       xmppDomain = cfg.hostName;
623       userDomain = "auth.${cfg.hostName}";
624       userName = "jigasi";
625       userPasswordFile = "/var/lib/jitsi-meet/jigasi-user-secret";
626       componentPasswordFile = "/var/lib/jitsi-meet/jigasi-component-secret";
627       bridgeMuc = "jigasibrewery@internal.${cfg.hostName}";
628       config = {
629         "org.jitsi.jigasi.ALWAYS_TRUST_MODE_ENABLED" = "true";
630       };
631     };
632   };
634   meta.doc = ./jitsi-meet.md;
635   meta.maintainers = lib.teams.jitsi.members;