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 };
41in
42{
43 options.services.jitsi-meet = with types; {
44 enable = mkEnableOption "Jitsi Meet - Secure, Simple and Scalable Video Conferences";
45
46 hostName = mkOption {
47 type = str;
48 example = "meet.example.org";
49 description = ''
50 Hostname of the Jitsi Meet instance.
51 '';
52 };
53
54 config = mkOption {
55 type = attrs;
56 default = { };
57 example = literalExample ''
58 {
59 enableWelcomePage = false;
60 defaultLang = "fi";
61 }
62 '';
63 description = ''
64 Client-side web application settings that override the defaults in <filename>config.js</filename>.
65
66 See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/config.js" /> for default
67 configuration with comments.
68 '';
69 };
70
71 extraConfig = mkOption {
72 type = lines;
73 default = "";
74 description = ''
75 Text to append to <filename>config.js</filename> web application config file.
76
77 Can be used to insert JavaScript logic to determine user's region in cascading bridges setup.
78 '';
79 };
80
81 interfaceConfig = mkOption {
82 type = attrs;
83 default = { };
84 example = literalExample ''
85 {
86 SHOW_JITSI_WATERMARK = false;
87 SHOW_WATERMARK_FOR_GUESTS = false;
88 }
89 '';
90 description = ''
91 Client-side web-app interface settings that override the defaults in <filename>interface_config.js</filename>.
92
93 See <link xlink:href="https://github.com/jitsi/jitsi-meet/blob/master/interface_config.js" /> for
94 default configuration with comments.
95 '';
96 };
97
98 videobridge = {
99 enable = mkOption {
100 type = bool;
101 default = true;
102 description = ''
103 Whether to enable Jitsi Videobridge instance and configure it to connect to Prosody.
104
105 Additional configuration is possible with <option>services.jitsi-videobridge</option>.
106 '';
107 };
108
109 passwordFile = mkOption {
110 type = nullOr str;
111 default = null;
112 example = "/run/keys/videobridge";
113 description = ''
114 File containing password to the Prosody account for videobridge.
115
116 If <literal>null</literal>, a file with password will be generated automatically. Setting
117 this option is useful if you plan to connect additional videobridges to the XMPP server.
118 '';
119 };
120 };
121
122 jicofo.enable = mkOption {
123 type = bool;
124 default = true;
125 description = ''
126 Whether to enable JiCoFo instance and configure it to connect to Prosody.
127
128 Additional configuration is possible with <option>services.jicofo</option>.
129 '';
130 };
131
132 nginx.enable = mkOption {
133 type = bool;
134 default = true;
135 description = ''
136 Whether to enable nginx virtual host that will serve the javascript application and act as
137 a proxy for the XMPP server. Further nginx configuration can be done by adapting
138 <option>services.nginx.virtualHosts.<hostName></option>.
139 When this is enabled, ACME will be used to retrieve a TLS certificate by default. To disable
140 this, set the <option>services.nginx.virtualHosts.<hostName>.enableACME</option> to
141 <literal>false</literal> and if appropriate do the same for
142 <option>services.nginx.virtualHosts.<hostName>.forceSSL</option>.
143 '';
144 };
145
146 prosody.enable = mkOption {
147 type = bool;
148 default = true;
149 description = ''
150 Whether to configure Prosody to relay XMPP messages between Jitsi Meet components. Turn this
151 off if you want to configure it manually.
152 '';
153 };
154 };
155
156 config = mkIf cfg.enable {
157 services.prosody = mkIf cfg.prosody.enable {
158 enable = mkDefault true;
159 xmppComplianceSuite = mkDefault false;
160 modules = {
161 admin_adhoc = mkDefault false;
162 bosh = mkDefault true;
163 ping = mkDefault true;
164 roster = mkDefault true;
165 saslauth = mkDefault true;
166 tls = mkDefault true;
167 };
168 muc = [
169 {
170 domain = "conference.${cfg.hostName}";
171 name = "Jitsi Meet MUC";
172 roomLocking = false;
173 roomDefaultPublicJids = true;
174 extraConfig = ''
175 storage = "memory"
176 '';
177 }
178 {
179 domain = "internal.${cfg.hostName}";
180 name = "Jitsi Meet Videobridge MUC";
181 extraConfig = ''
182 storage = "memory"
183 admins = { "focus@auth.${cfg.hostName}", "jvb@auth.${cfg.hostName}" }
184 '';
185 #-- muc_room_cache_size = 1000
186 }
187 ];
188 extraModules = [ "pubsub" ];
189 extraConfig = mkAfter ''
190 Component "focus.${cfg.hostName}"
191 component_secret = os.getenv("JICOFO_COMPONENT_SECRET")
192 '';
193 virtualHosts.${cfg.hostName} = {
194 enabled = true;
195 domain = cfg.hostName;
196 extraConfig = ''
197 authentication = "anonymous"
198 c2s_require_encryption = false
199 admins = { "focus@auth.${cfg.hostName}" }
200 '';
201 ssl = {
202 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
203 key = "/var/lib/jitsi-meet/jitsi-meet.key";
204 };
205 };
206 virtualHosts."auth.${cfg.hostName}" = {
207 enabled = true;
208 domain = "auth.${cfg.hostName}";
209 extraConfig = ''
210 authentication = "internal_plain"
211 '';
212 ssl = {
213 cert = "/var/lib/jitsi-meet/jitsi-meet.crt";
214 key = "/var/lib/jitsi-meet/jitsi-meet.key";
215 };
216 };
217 };
218 systemd.services.prosody.serviceConfig = mkIf cfg.prosody.enable {
219 EnvironmentFile = [ "/var/lib/jitsi-meet/secrets-env" ];
220 SupplementaryGroups = [ "jitsi-meet" ];
221 };
222
223 users.groups.jitsi-meet = {};
224 systemd.tmpfiles.rules = [
225 "d '/var/lib/jitsi-meet' 0750 root jitsi-meet - -"
226 ];
227
228 systemd.services.jitsi-meet-init-secrets = {
229 wantedBy = [ "multi-user.target" ];
230 before = [ "jicofo.service" "jitsi-videobridge2.service" ] ++ (optional cfg.prosody.enable "prosody.service");
231 serviceConfig = {
232 Type = "oneshot";
233 };
234
235 script = let
236 secrets = [ "jicofo-component-secret" "jicofo-user-secret" ] ++ (optional (cfg.videobridge.passwordFile == null) "videobridge-secret");
237 videobridgeSecret = if cfg.videobridge.passwordFile != null then cfg.videobridge.passwordFile else "/var/lib/jitsi-meet/videobridge-secret";
238 in
239 ''
240 cd /var/lib/jitsi-meet
241 ${concatMapStringsSep "\n" (s: ''
242 if [ ! -f ${s} ]; then
243 tr -dc a-zA-Z0-9 </dev/urandom | head -c 64 > ${s}
244 chown root:jitsi-meet ${s}
245 chmod 640 ${s}
246 fi
247 '') secrets}
248
249 # for easy access in prosody
250 echo "JICOFO_COMPONENT_SECRET=$(cat jicofo-component-secret)" > secrets-env
251 chown root:jitsi-meet secrets-env
252 chmod 640 secrets-env
253 ''
254 + optionalString cfg.prosody.enable ''
255 ${config.services.prosody.package}/bin/prosodyctl register focus auth.${cfg.hostName} "$(cat /var/lib/jitsi-meet/jicofo-user-secret)"
256 ${config.services.prosody.package}/bin/prosodyctl register jvb auth.${cfg.hostName} "$(cat ${videobridgeSecret})"
257
258 # generate self-signed certificates
259 if [ ! -f /var/lib/jitsi-meet.crt ]; then
260 ${getBin pkgs.openssl}/bin/openssl req \
261 -x509 \
262 -newkey rsa:4096 \
263 -keyout /var/lib/jitsi-meet/jitsi-meet.key \
264 -out /var/lib/jitsi-meet/jitsi-meet.crt \
265 -days 36500 \
266 -nodes \
267 -subj '/CN=${cfg.hostName}/CN=auth.${cfg.hostName}'
268 chmod 640 /var/lib/jitsi-meet/jitsi-meet.{crt,key}
269 chown root:jitsi-meet /var/lib/jitsi-meet/jitsi-meet.{crt,key}
270 fi
271 '';
272 };
273
274 services.nginx = mkIf cfg.nginx.enable {
275 enable = mkDefault true;
276 virtualHosts.${cfg.hostName} = {
277 enableACME = mkDefault true;
278 forceSSL = mkDefault true;
279 root = pkgs.jitsi-meet;
280 extraConfig = ''
281 ssi on;
282 '';
283 locations."@root_path".extraConfig = ''
284 rewrite ^/(.*)$ / break;
285 '';
286 locations."~ ^/([^/\\?&:'\"]+)$".tryFiles = "$uri @root_path";
287 locations."=/http-bind" = {
288 proxyPass = "http://localhost:5280/http-bind";
289 extraConfig = ''
290 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
291 proxy_set_header Host $host;
292 '';
293 };
294 locations."=/external_api.js" = mkDefault {
295 alias = "${pkgs.jitsi-meet}/libs/external_api.min.js";
296 };
297 locations."=/config.js" = mkDefault {
298 alias = overrideJs "${pkgs.jitsi-meet}/config.js" "config" (recursiveUpdate defaultCfg cfg.config) cfg.extraConfig;
299 };
300 locations."=/interface_config.js" = mkDefault {
301 alias = overrideJs "${pkgs.jitsi-meet}/interface_config.js" "interfaceConfig" cfg.interfaceConfig "";
302 };
303 };
304 };
305
306 services.jitsi-videobridge = mkIf cfg.videobridge.enable {
307 enable = true;
308 xmppConfigs."localhost" = {
309 userName = "jvb";
310 domain = "auth.${cfg.hostName}";
311 passwordFile = "/var/lib/jitsi-meet/videobridge-secret";
312 mucJids = "jvbbrewery@internal.${cfg.hostName}";
313 disableCertificateVerification = true;
314 };
315 };
316
317 services.jicofo = mkIf cfg.jicofo.enable {
318 enable = true;
319 xmppHost = "localhost";
320 xmppDomain = cfg.hostName;
321 userDomain = "auth.${cfg.hostName}";
322 userName = "focus";
323 userPasswordFile = "/var/lib/jitsi-meet/jicofo-user-secret";
324 componentPasswordFile = "/var/lib/jitsi-meet/jicofo-component-secret";
325 bridgeMuc = "jvbbrewery@internal.${cfg.hostName}";
326 config = {
327 "org.jitsi.jicofo.ALWAYS_TRUST_MODE_ENABLED" = "true";
328 };
329 };
330 };
331
332 meta.doc = ./jitsi-meet.xml;
333 meta.maintainers = lib.teams.jitsi.members;
334}