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}