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 overriden 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 "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
50
51 hostName = mkOption {
52 type = str;
53 example = "meet.example.org";
54 description = ''
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 = ''
69 Client-side web application settings that override the defaults in <filename>config.js</filename>.
70
71 See <link xlink:href="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 = ''
80 Text to append to <filename>config.js</filename> 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 = ''
96 Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
97
98 See <link xlink:href="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 = ''
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</option>.
111 '';
112 };
113
114 passwordFile = mkOption {
115 type = nullOr str;
116 default = null;
117 example = "/run/keys/videobridge";
118 description = ''
119 File containing password to the Prosody account for videobridge.
120
121 If <literal>null</literal>, 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 = ''
131 Whether to enable JiCoFo instance and configure it to connect to Prosody.
132
133 Additional configuration is possible with <option>services.jicofo</option>.
134 '';
135 };
136
137 jibri.enable = mkOption {
138 type = bool;
139 default = false;
140 description = ''
141 Whether to enable a Jibri instance and configure it to connect to Prosody.
142
143 Additional configuration is possible with <option>services.jibri</option>, and
144 <option>services.jibri.finalizeScript</option> is especially useful.
145 '';
146 };
147
148 nginx.enable = mkOption {
149 type = bool;
150 default = true;
151 description = ''
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></option>.
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</option> to
157 <literal>false</literal> and if appropriate do the same for
158 <option>services.nginx.virtualHosts.<hostName>.forceSSL</option>.
159 '';
160 };
161
162 caddy.enable = mkEnableOption "Whether to enablle caddy reverse proxy to expose jitsi-meet";
163
164 prosody.enable = mkOption {
165 type = bool;
166 default = true;
167 description = ''
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.serviceConfig = mkIf cfg.prosody.enable {
257 EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
258 SupplementaryGroups = [ "jitsi-meet" ];
259 };
260
261 users.groups.jitsi-meet = {};
262 systemd.tmpfiles.rules = [
263 "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
264 ];
265
266 systemd.services.jitsi-meet-init-secrets = {
267 wantedBy = [ "multi-user.target" ];
268 before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
269 path = [ config.services.prosody.package ];
270 serviceConfig = {
271 Type = "oneshot";
272 };
273
274 script = let
275 secrets = [ "jicofo-component-secret" "jicofo-user-secret" "jibri-auth-secret" "jibri-recorder-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
276 videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
277 in
278 ''
279 cd /var/lib/jitsi-meet
280 ${concatMapStringsSep "\n" (s: ''
281 if [ ! -f ${s} ]; then
282 tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
283 chown root:jitsi-meet ${s}
284 chmod 640 ${s}
285 fi
286 '') secrets}
287
288 # for easy access in prosody
289 echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
290 chown root:jitsi-meet secrets-env
291 chmod 640 secrets-env
292 ''
293 + optionalString cfg.prosody.enable ''
294 prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
295 prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
296 prosodyctl mod_roster_command subscribe focus.${cfg.hostName} focus@auth.${cfg.hostName}
297 prosodyctl register jibri auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-auth-secret)"
298 prosodyctl register recorder recorder.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jibri-recorder-secret)"
299
300 # generate self-signed certificates
301 if [ ! -f /var/lib/jitsi-meet.crt ]; then
302 ${getBin pkgs.openssl}/bin/openssl req \
303 -x509 \
304 -newkey rsa:4096 \
305 -keyout /var/lib/jitsi-meet/jitsi-meet.key \
306 -out /var/lib/jitsi-meet/jitsi-meet.crt \
307 -days 36500 \
308 -nodes \
309 -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
310 chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key}
311 chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key}
312 fi
313 '';
314 };
315
316 services.nginx = mkIf cfg.nginx.enable {
317 enable = mkDefault true;
318 virtualHosts.${cfg.hostName} = {
319 enableACME = mkDefault true;
320 forceSSL = mkDefault true;
321 root = pkgs.jitsi-meet;
322 extraConfig = ''
323 ssi on;
324 '';
325 locations."@root_path".extraConfig = ''
326 rewrite ^/(.*)$ / break;
327 '';
328 locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
329 locations."^~ /xmpp-websocket" = {
330 priority = 100;
331 proxyPass = "http://localhost:5280/xmpp-websocket";
332 proxyWebsockets = true;
333 };
334 locations."=/http-bind" = {
335 proxyPass = "http://localhost:5280/http-bind";
336 extraConfig = ''
337 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
338 proxy_set_header Host $host;
339 '';
340 };
341 locations."=/external_api.js" = mkDefault {
342 alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
343 };
344 locations."=/config.js" = mkDefault {
345 alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
346 };
347 locations."=/interface_config.js" = mkDefault {
348 alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
349 };
350 };
351 };
352
353 services.caddy = mkIf cfg.caddy.enable {
354 enable = mkDefault true;
355 virtualHosts.${cfg.hostName} = {
356 extraConfig =
357 let
358 templatedJitsiMeet = pkgs.runCommand "templated-jitsi-meet" {} ''
359 cp -R ${pkgs.jitsi-meet}/* .
360 for file in *.html **/*.html ; do
361 ${pkgs.sd}/bin/sd '<!--#include virtual="(.*)" -->' '{{ include "$1" }}' $file
362 done
363 rm config.js
364 rm interface_config.js
365 cp -R . $out
366 cp ${overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig} $out/config.js
367 cp ${overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig ""} $out/interface_config.js
368 cp ./libs/external_api.min.js $out/external_api.js
369 '';
370 in ''
371 handle /http-bind {
372 header Host ${cfg.hostName}
373 reverse_proxy 127.0.0.1:5280
374 }
375 handle /xmpp-websocket {
376 reverse_proxy 127.0.0.1:5280
377 }
378 handle {
379 templates
380 root * ${templatedJitsiMeet}
381 try_files {path} {path}
382 try_files {path} /index.html
383 file_server
384 }
385 '';
386 };
387 };
388
389 services.jitsi-videobridge = mkIf cfg.videobridge.enable {
390 enable = true;
391 xmppConfigs."localhost" = {
392 userName = "jvb";
393 domain = "auth.${cfg.hostName}";
394 passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
395 mucJids = "jvbbrewery@internal.${cfg.hostName}";
396 disableCertificateVerification = true;
397 };
398 };
399
400 services.jicofo = mkIf cfg.jicofo.enable {
401 enable = true;
402 xmppHost = "localhost";
403 xmppDomain = cfg.hostName;
404 userDomain = "auth.${cfg.hostName}";
405 userName = "focus";
406 userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
407 componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
408 bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
409 config = mkMerge [{
410 "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
411 #} (lib.mkIf cfg.jibri.enable {
412 } (lib.mkIf (config.services.jibri.enable || cfg.jibri.enable) {
413 "org.jitsi.jicofo.jibri.BREWERY" = "JibriBrewery@internal.${cfg.hostName}";
414 "org.jitsi.jicofo.jibri.PENDING_TIMEOUT" = "90";
415 })];
416 };
417
418 services.jibri = mkIf cfg.jibri.enable {
419 enable = true;
420
421 xmppEnvironments."jitsi-meet" = {
422 xmppServerHosts = [ "localhost" ];
423 xmppDomain = cfg.hostName;
424
425 control.muc = {
426 domain = "internal.${cfg.hostName}";
427 roomName = "JibriBrewery";
428 nickname = "jibri";
429 };
430
431 control.login = {
432 domain = "auth.${cfg.hostName}";
433 username = "jibri";
434 passwordFile = "/var/lib/jitsi-meet/jibri-auth-secret";
435 };
436
437 call.login = {
438 domain = "recorder.${cfg.hostName}";
439 username = "recorder";
440 passwordFile = "/var/lib/jitsi-meet/jibri-recorder-secret";
441 };
442
443 usageTimeout = "0";
444 disableCertificateVerification = true;
445 stripFromRoomDomain = "conference.";
446 };
447 };
448 };
449
450 meta.doc = ./jitsi-meet.xml;
451 meta.maintainers = lib.teams.jitsi.members;
452}