This commit is contained in:
177
resources/cloud-api.swagger
Normal file
177
resources/cloud-api.swagger
Normal file
@@ -0,0 +1,177 @@
|
||||
swagger: "2.0"
|
||||
info:
|
||||
description: "Documents the REST calls used by Jitsi Meet to integrate with other services"
|
||||
version: "1.0.0"
|
||||
title: "Swagger Video"
|
||||
termsOfService: "https://jitsi.org/CloudAPITOS/"
|
||||
contact:
|
||||
email: "team@jitsi.org"
|
||||
host: "jitsi-api.jitsi.org"
|
||||
basePath: "/"
|
||||
tags:
|
||||
- name: "conferenceMapper"
|
||||
description: "Conference to ID Mapper"
|
||||
externalDocs:
|
||||
description: "Conference API Details"
|
||||
url: "https://jitsi.org/CloudAPI"
|
||||
- name: "phoneNumberList"
|
||||
description: "List of dial-in numbers"
|
||||
schemes:
|
||||
- "https"
|
||||
paths:
|
||||
/conferenceMapper:
|
||||
get:
|
||||
tags:
|
||||
- "conferenceMapper"
|
||||
summary: "Create or retrieve conference ID mapping"
|
||||
description: "When called with a conference, creates a new ID and both stores and returns the result. When called with an ID, returns the mapping if previously created."
|
||||
operationId: "GETconferenceMapper"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "query"
|
||||
name: "conference"
|
||||
type: "string"
|
||||
format: "JID"
|
||||
description: "Full JID (room@conference.server.domain) for the conference to create or return existing conference mapping. Used preferentially over all other input parameters (search by conference)"
|
||||
- in: "query"
|
||||
name: "id"
|
||||
type: "number"
|
||||
description: "ID to search for existing conference mapping. Only used when provided alone (search by ID)"
|
||||
responses:
|
||||
200:
|
||||
description: "mapping search performed"
|
||||
schema:
|
||||
$ref: "#/definitions/ConferenceMapperDetails"
|
||||
405:
|
||||
description: "Invalid input"
|
||||
post:
|
||||
tags:
|
||||
- "conferenceMapper"
|
||||
summary: "Create or retrieve conference ID mapping"
|
||||
description: "When called with a conference, creates a new ID and both stores and returns the result. When called with an ID, returns the mapping if previously created."
|
||||
operationId: "POSTconferenceMapper"
|
||||
consumes:
|
||||
- "application/json"
|
||||
produces:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Conference Mapper Request"
|
||||
required: true
|
||||
schema:
|
||||
$ref: "#/definitions/ConferenceMapperRequest"
|
||||
responses:
|
||||
200:
|
||||
description: "mapping search performed"
|
||||
schema:
|
||||
$ref: "#/definitions/ConferenceMapperDetails"
|
||||
405:
|
||||
description: "Invalid input"
|
||||
|
||||
/phoneNumberList:
|
||||
get:
|
||||
tags:
|
||||
- "phoneNumberList"
|
||||
summary: "Returns a list phone numbers by country"
|
||||
description: "Used to populate the Share The Link section of jitsi-meet"
|
||||
operationId: "phoneNumberList"
|
||||
produces:
|
||||
- "application/json"
|
||||
responses:
|
||||
200:
|
||||
description: "successful operation"
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/definitions/PhoneNumberListAnswer"
|
||||
/waiting-queue/golive:
|
||||
post:
|
||||
tags:
|
||||
- waitingQueueGoLive
|
||||
summary: Post a go live request
|
||||
description: Mark the conference as live, notify all visitors waiting.
|
||||
operationId: goLive
|
||||
consumes:
|
||||
- "application/json"
|
||||
parameters:
|
||||
- in: "body"
|
||||
name: "body"
|
||||
description: "Go Live Request"
|
||||
required: true
|
||||
schema:
|
||||
"$ref": "#/definitions/GoLiveRequest"
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
'404':
|
||||
description: Missing conference
|
||||
'500':
|
||||
description: Failed operation
|
||||
securityDefinitions:
|
||||
token:
|
||||
type: "apiKey"
|
||||
name: "token"
|
||||
in: "query"
|
||||
Bearer:
|
||||
type: "apiKey"
|
||||
name: "Authorization"
|
||||
in: "header"
|
||||
definitions:
|
||||
|
||||
ConferenceMapperRequest:
|
||||
description: "Request to create or find a conference mapping"
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "number"
|
||||
description: "ID to search for existing conference mapping. Only used when provided alone (search by ID)"
|
||||
conference:
|
||||
type: "string"
|
||||
format: "JID"
|
||||
description: "Full JID (room@conference.server.domain) for the conference to create or return existing conference mapping. Used preferentially over all other input parameters (search by conference)"
|
||||
room:
|
||||
type: "string"
|
||||
description: "Room part of the conference. Required if 'conference' is not provided. Used to generate a 'conference' value (search by conference)"
|
||||
domain:
|
||||
type: "string"
|
||||
description: "Domain part of the conference. Used if 'conference' is not provided. Defaults to domain of the API endpoint. Used to generate a 'conference' value (search by conference)"
|
||||
|
||||
ConferenceMapperDetails:
|
||||
description: "Conference mapping between conference JID and numeric ID"
|
||||
type: "object"
|
||||
properties:
|
||||
id:
|
||||
type: "number"
|
||||
description: "Unique ID mapped to conference"
|
||||
conference:
|
||||
type: "string"
|
||||
format: "JID"
|
||||
description: "Full JID for the conference OR boolean false if no conference was found (search by ID)"
|
||||
|
||||
PhoneNumberListAnswer:
|
||||
description: "Answer with Phone number list and additional information"
|
||||
type: "object"
|
||||
properties:
|
||||
countryCode:
|
||||
type: string
|
||||
formattedNumber:
|
||||
type: string
|
||||
tollFree:
|
||||
type: boolean
|
||||
example:
|
||||
- {"countryCode":"US","tollFree":false,"formattedNumber":"+1 123-456-7890"}
|
||||
- {"countryCode":"UK","tollFree":true,"formattedNumber":"+44 123 456 7890"}
|
||||
|
||||
GoLiveRequest:
|
||||
type: object
|
||||
properties:
|
||||
conference:
|
||||
type: string
|
||||
externalDocs:
|
||||
description: "Find out more about the Jitsi Cloud API"
|
||||
url: "https://jitsi.org/CloudAPI"
|
||||
|
||||
47
resources/coturn-le-update.sh
Executable file
47
resources/coturn-le-update.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# This script is updating the Let's Encrypt certificates on renew or when installing
|
||||
# The only param it gets is the domain and expects the certificates to use are updated
|
||||
# in /etc/jitsi/meet folder.
|
||||
DOMAIN=$1
|
||||
|
||||
if [ -z "$DOMAIN" ] ; then
|
||||
echo "You need to pass the domain as parameter."
|
||||
exit 10;
|
||||
fi
|
||||
|
||||
COTURN_CERT_DIR="/etc/coturn/certs"
|
||||
TURN_CONFIG="/etc/turnserver.conf"
|
||||
|
||||
# Execute only if turnconfig exist and is one managed by jitsi-meet
|
||||
if [ -f $TURN_CONFIG ] && grep -q "jitsi-meet coturn config" "$TURN_CONFIG" ; then
|
||||
# create a directory to store certs if it does not exists
|
||||
if [ ! -d "$COTURN_CERT_DIR" ]; then
|
||||
mkdir -p $COTURN_CERT_DIR
|
||||
chown -R turnserver:turnserver /etc/coturn/
|
||||
chmod -R 700 /etc/coturn/
|
||||
fi
|
||||
|
||||
# Make sure the certificate and private key files are
|
||||
# never world readable, even just for an instant while
|
||||
# we're copying them into daemon_cert_root.
|
||||
umask 077
|
||||
|
||||
cp "/etc/jitsi/meet/${DOMAIN}.crt" "$COTURN_CERT_DIR/${DOMAIN}.fullchain.pem"
|
||||
cp "/etc/jitsi/meet/${DOMAIN}.key" "$COTURN_CERT_DIR/${DOMAIN}.privkey.pem"
|
||||
|
||||
# Apply the proper file ownership and permissions for
|
||||
# the daemon to read its certificate and key.
|
||||
chown turnserver "$COTURN_CERT_DIR/${DOMAIN}.fullchain.pem" \
|
||||
"$COTURN_CERT_DIR/${DOMAIN}.privkey.pem"
|
||||
chmod 400 "$COTURN_CERT_DIR/${DOMAIN}.fullchain.pem" \
|
||||
"$COTURN_CERT_DIR/${DOMAIN}.privkey.pem"
|
||||
|
||||
echo "Configuring turnserver"
|
||||
sed -i "/^cert/c\cert=\/etc\/coturn\/certs\/${DOMAIN}.fullchain.pem" $TURN_CONFIG
|
||||
sed -i "/^pkey/c\pkey=\/etc\/coturn\/certs\/${DOMAIN}.privkey.pem" $TURN_CONFIG
|
||||
|
||||
service coturn restart
|
||||
fi
|
||||
153
resources/custom-theme/custom-theme.json
Normal file
153
resources/custom-theme/custom-theme.json
Normal file
@@ -0,0 +1,153 @@
|
||||
{
|
||||
"spacing": [ 0, 4, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 ],
|
||||
"breakpoints": {
|
||||
"values": {
|
||||
"0": 0,
|
||||
"320": 320,
|
||||
"400": 400,
|
||||
"480": 480
|
||||
}
|
||||
},
|
||||
"palette": {
|
||||
"uiBackground": "#040404",
|
||||
"ui01": "#141414",
|
||||
"ui02": "#292929",
|
||||
"ui03": "#3D3D3D",
|
||||
"ui04": "#525252",
|
||||
"ui05": "#666",
|
||||
"action01": "#0056E0",
|
||||
"action01Hover": "#246FE5",
|
||||
"action01Active": "#0045B3",
|
||||
"disabled01": "#00225A",
|
||||
"action02": "#3D3D3D",
|
||||
"action02Hover": "#525252",
|
||||
"action02Active": "#292929",
|
||||
"action03": "transparent",
|
||||
"action03Hover": "#525252",
|
||||
"action03Active": "#292929",
|
||||
"actionDanger": "#CB2233",
|
||||
"actionDangerHover": "#E04757",
|
||||
"actionDangerActive": "#A21B29",
|
||||
"bottomSheet": "#111111",
|
||||
"text01": "#FFF",
|
||||
"text02": "#C2C2C2",
|
||||
"text03": "#858585",
|
||||
"text04": "#AAAAAA",
|
||||
"textError": "#E04757",
|
||||
"icon01": "#FFF",
|
||||
"icon02": "#C2C2C2",
|
||||
"icon03": "#858585",
|
||||
"iconError": "#E04757",
|
||||
"field01": "#040404",
|
||||
"link01": "#669AEC",
|
||||
"link01Hover": "#99BBF3",
|
||||
"link01Active": "#246FE5",
|
||||
"success01": "#1EC26A",
|
||||
"success02": "#1EC26A",
|
||||
"warning01": "#F8AE1A",
|
||||
"warning02": "#ED9E1B",
|
||||
"support01": "#FF9B42",
|
||||
"support02": "#F96E57",
|
||||
"support03": "#DF486F",
|
||||
"support04": "#B23683",
|
||||
"support05": "#73348C",
|
||||
"support06": "#6A50D3",
|
||||
"support07": "#4380E2",
|
||||
"support08": "#00A8B3",
|
||||
"support09": "#2AA076"
|
||||
},
|
||||
"typography": {
|
||||
"font": {
|
||||
"weightRegular": "400",
|
||||
"weightSemiBold": "600"
|
||||
},
|
||||
"labelRegular": {
|
||||
"fontSize": "0.75rem",
|
||||
"lineHeight": "1rem",
|
||||
"fontWeight": "400",
|
||||
"letterSpacing": 0.16
|
||||
},
|
||||
"labelBold": {
|
||||
"fontSize": "0.75rem",
|
||||
"lineHeight": "1rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0.16
|
||||
},
|
||||
"bodyShortRegular": {
|
||||
"fontSize": "0.875rem",
|
||||
"lineHeight": "1.125rem",
|
||||
"fontWeight": "400",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"bodyShortBold": {
|
||||
"fontSize": "0.875rem",
|
||||
"lineHeight": "1.125rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"bodyShortRegularLarge": {
|
||||
"fontSize": "1rem",
|
||||
"lineHeight": "1.5rem",
|
||||
"fontWeight": "400",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"bodyShortBoldLarge": {
|
||||
"fontSize": "1rem",
|
||||
"lineHeight": "1.5rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"bodyLongRegular": {
|
||||
"fontSize": "0.875rem",
|
||||
"lineHeight": "1.5rem",
|
||||
"fontWeight": "400",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"bodyLongBold": {
|
||||
"fontSize": "0.875rem",
|
||||
"lineHeight": "1.5rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading1": {
|
||||
"fontSize": "3.375rem",
|
||||
"lineHeight": "4rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading2": {
|
||||
"fontSize": "2.625rem",
|
||||
"lineHeight": "3.125rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading3": {
|
||||
"fontSize": "2rem",
|
||||
"lineHeight": "2.5rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading4": {
|
||||
"fontSize": "1.75rem",
|
||||
"lineHeight": "2.25rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading5": {
|
||||
"fontSize": "1.25rem",
|
||||
"lineHeight": "1.75rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
},
|
||||
"heading6": {
|
||||
"fontSize": "1rem",
|
||||
"lineHeight": "1.625rem",
|
||||
"fontWeight": "600",
|
||||
"letterSpacing": 0
|
||||
}
|
||||
},
|
||||
"shape": {
|
||||
"borderRadius": 6,
|
||||
"boxShadow": "inset 0px -1px 0px rgba(255, 255, 255, 0.15)"
|
||||
}
|
||||
}
|
||||
50
resources/encode-sound.sh
Executable file
50
resources/encode-sound.sh
Executable file
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--mp3] [--opus] [--stereo] <input file ...>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--stereo)
|
||||
STEREO=true
|
||||
shift
|
||||
;;
|
||||
--mp3)
|
||||
MP3=true
|
||||
shift
|
||||
;;
|
||||
--opus)
|
||||
OPUS=true
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ $# -lt 1 ] ;then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [ "$MP3" != "true" ] ;then
|
||||
if [ "$OPUS" != "true" ] ;then
|
||||
echo "At least one of --mp3 or --opus is required"
|
||||
usage
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "STEREO=$STEREO MP3=$MP3 OPUS=$OPUS"
|
||||
|
||||
AC1="-ac 1"
|
||||
if [ "$STEREO" = "true" ] ;then
|
||||
AC1=""
|
||||
fi
|
||||
|
||||
for i in "$@" ;do
|
||||
if [ "$MP3" = "true" ] ;then
|
||||
ffmpeg -i "$i" -codec:a libmp3lame -qscale:a 9 -map_metadata -1 $AC1 "${i%.*}.mp3"
|
||||
fi
|
||||
if [ "$OPUS" = "true" ] ;then
|
||||
ffmpeg -i "$i" -c:a libopus -b:a 30k -vbr on -compression_level 10 -map_metadata -1 $AC1 "${i%.*}.opus"
|
||||
fi
|
||||
done
|
||||
123
resources/extra-large-conference/README.md
Normal file
123
resources/extra-large-conference/README.md
Normal file
@@ -0,0 +1,123 @@
|
||||
WARNING: This is still a Work In Progress
|
||||
|
||||
The final implementation may diverge. Currently, only the participants after a
|
||||
configured threshold will be just viewers (visitors) and there is no promotion
|
||||
mechanism to become a main participant yet.
|
||||
|
||||
TODO:
|
||||
* Polls
|
||||
* Speaker stats
|
||||
* call duration
|
||||
|
||||
# Low-latency conference streaming to very large audiences
|
||||
|
||||
To have a low-latency conference with a very large audience, the media and
|
||||
signaling load must be spread beyond what can be handled by a typical Jitsi
|
||||
installation. A call with 10k participants requires around 50 bridges on decent
|
||||
vms (8+ cores). The main participants of a conference with a very large
|
||||
audience will share a main prosody, like with normal conferences, and
|
||||
additional prosody vms are needed to support signaling to the audience.
|
||||
|
||||
In the example configuration we use a 16 core machine. Eight of the cores are
|
||||
used for the main prosody and other services (nginx, jicofo, etc) and the other
|
||||
eight cores are used to run prosody services for visitors, i.e., "visitor
|
||||
prosodies".
|
||||
|
||||
We consider 2000 participants per visitor node a safe value. So eight visitor
|
||||
prosodies will be enough for one 10k participants meeting.
|
||||
|
||||
<img src="imgs/visitors-prosody.svg" alt="diagram of a central prosody connected to several visitor prosodies" width="500"/>
|
||||
|
||||
# Configuration
|
||||
If using older than Prosody 0.12.4 you need to apply the patch - s2sout_override1.patch and s2sout_override2.patch.
|
||||
Use the `pre-configure.sh` script to configure your system, passing it the
|
||||
number of visitor prosodies to set up.
|
||||
`./pre-configure.sh 8`
|
||||
|
||||
The script will add for each visitor prosody:
|
||||
- folders in `/etc/`
|
||||
- a systemd unit file in `/lib/systemd/system/`
|
||||
- a user for jicofo
|
||||
- a config entry in jicofo.conf
|
||||
|
||||
Setting up configuration for the main prosody is a manual process:
|
||||
- Add to the enabled modules list in the general part (e.g. [here](https://github.com/bjc/prosody/blob/76bf6d511f851c7cde8a81257afaaae0fb7a4160/prosody.cfg.lua.dist#L33)):
|
||||
```
|
||||
"s2s_bidi";
|
||||
"certs_s2soutinjection";
|
||||
"s2sout_override";
|
||||
"s2s_whitelist";
|
||||
```
|
||||
|
||||
- Add the following config also in the general part (matching the number of prosodies you generated config for):
|
||||
```
|
||||
-- targets must be IPs, not hostnames
|
||||
s2sout_override = {
|
||||
["conference.v1.meet.jitsi"] = "tcp://127.0.0.1:52691";
|
||||
["v1.meet.jitsi"] = "tcp://127.0.0.1:52691"; -- needed for v1.meet.jitsi->visitors.jitmeet.example.com
|
||||
["conference.v2.meet.jitsi"] = "tcp://127.0.0.1:52692";
|
||||
["v2.meet.jitsi"] = "tcp://127.0.0.1:52692";
|
||||
["conference.v3.meet.jitsi"] = "tcp://127.0.0.1:52693";
|
||||
["v3.meet.jitsi"] = "tcp://127.0.0.1:52693";
|
||||
["conference.v4.meet.jitsi"] = "tcp://127.0.0.1:52694";
|
||||
["v4.meet.jitsi"] = "tcp://127.0.0.1:52694";
|
||||
["conference.v5.meet.jitsi"] = "tcp://127.0.0.1:52695";
|
||||
["v5.meet.jitsi"] = "tcp://127.0.0.1:52695";
|
||||
["conference.v6.meet.jitsi"] = "tcp://127.0.0.1:52696";
|
||||
["v6.meet.jitsi"] = "tcp://127.0.0.1:52696";
|
||||
["conference.v7.meet.jitsi"] = "tcp://127.0.0.1:52697";
|
||||
["v7.meet.jitsi"] = "tcp://127.0.0.1:52697";
|
||||
["conference.v8.meet.jitsi"] = "tcp://127.0.0.1:52698";
|
||||
["v8.meet.jitsi"] = "tcp://127.0.0.1:52698";
|
||||
}
|
||||
-- allowed list of server-2-server connections
|
||||
s2s_whitelist = {
|
||||
"conference.v1.meet.jitsi", "conference.v2.meet.jitsi", "conference.v3.meet.jitsi", "conference.v4.meet.jitsi",
|
||||
"conference.v5.meet.jitsi", "conference.v6.meet.jitsi", "conference.v7.meet.jitsi", "conference.v8.meet.jitsi"
|
||||
};
|
||||
```
|
||||
|
||||
- Make sure s2s is not in modules_disabled
|
||||
- Enable `"visitors";` module under the main virtual host (e.g. [here](https://github.com/jitsi/jitsi-meet/blob/f42772ec5bcc87ff6de17423d36df9bcad6e770d/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example#L57))
|
||||
You can add under main virtual host the config: `visitors_ignore_list = { "recorder.jitmeet.example.com" }` to ignore jibri and transcribers from the visitor logic and use them only in the main prosody conference.
|
||||
- Create the visitors component in /etc/prosody/conf.d/jitmeet.example.com.cfg.lua:
|
||||
```
|
||||
Component "visitors.jitmeet.example.com" "visitors_component"
|
||||
auto_allow_visitor_promotion = true
|
||||
admins = { "focus@auth.jitmeet.example.com" }
|
||||
```
|
||||
- Make sure you add the correct upstreams to nginx config
|
||||
```
|
||||
upstream v1 {
|
||||
zone upstreams 64K;
|
||||
server 127.0.0.1:52801;
|
||||
keepalive 2;
|
||||
}
|
||||
upstream v2 {
|
||||
zone upstreams 64K;
|
||||
server 127.0.0.1:52802;
|
||||
keepalive 2;
|
||||
}
|
||||
```
|
||||
|
||||
After configuring you can set the maximum number of main participants, before
|
||||
redirecting to visitors.
|
||||
```
|
||||
hocon -f /etc/jitsi/jicofo/jicofo.conf set "jicofo.visitors.enabled" true
|
||||
hocon -f /etc/jitsi/jicofo/jicofo.conf set "jicofo.visitors.max-participants" 30
|
||||
```
|
||||
Now restart prosody and jicofo
|
||||
```
|
||||
service prosody restart
|
||||
service jicofo restart
|
||||
service nginx restart
|
||||
```
|
||||
|
||||
Now after the main 30 participants join, the rest will be visitors using the
|
||||
visitor nodes.
|
||||
|
||||
To enable promotion where visitors need to be approved by a moderator to join the meeting:
|
||||
- you need to switch `auto_allow_visitor_promotion=false`.
|
||||
- You need to enable http requests to jicofo by editing config.js and adding a nginx rule for it.
|
||||
- In /etc/jitsi/meet/jitmeet.example.com-config.js uncomment conferenceRequestUrl.
|
||||
- In jitsi-meet nginx config make sure you have the conference-request location rules.
|
||||
2109
resources/extra-large-conference/imgs/visitors-prosody.svg
Normal file
2109
resources/extra-large-conference/imgs/visitors-prosody.svg
Normal file
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 198 KiB |
61
resources/extra-large-conference/pre-configure.sh
Executable file
61
resources/extra-large-conference/pre-configure.sh
Executable file
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=`dirname "$0"`
|
||||
cd $SCRIPT_DIR
|
||||
|
||||
NUMBER_OF_INSTANCES=$1
|
||||
|
||||
if ! [[ $NUMBER_OF_INSTANCES =~ ^[0-9]+([.][0-9]+)?$ ]] ; then
|
||||
echo "error: Not a number param" >&2;
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Will configure $NUMBER_OF_INSTANCES number of visitor prosodies"
|
||||
set -e
|
||||
set -x
|
||||
|
||||
JICOFO_HOSTNAME=$(echo get jitsi-videobridge/jvb-hostname | sudo debconf-communicate jicofo | cut -d' ' -f2-)
|
||||
|
||||
# Configure prosody instances
|
||||
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
|
||||
do
|
||||
cp prosody-v.service.template /lib/systemd/system/prosody-v${i}.service
|
||||
sed -i "s/vX/v${i}/g" /lib/systemd/system/prosody-v${i}.service
|
||||
mkdir /etc/prosody-v${i}
|
||||
ln -s /etc/prosody/certs /etc/prosody-v${i}/certs
|
||||
cp prosody.cfg.lua.visitor.template /etc/prosody-v${i}/prosody.cfg.lua
|
||||
sed -i "s/vX/v${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
sed -i "s/jitmeet.example.com/$JICOFO_HOSTNAME/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
# fix the ports
|
||||
sed -i "s/52691/5269${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
sed -i "s/52221/5222${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
sed -i "s/52801/5280${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
sed -i "s/52811/5281${i}/g" /etc/prosody-v${i}/prosody.cfg.lua
|
||||
done
|
||||
|
||||
# Configure jicofo
|
||||
HOCON_CONFIG="/etc/jitsi/jicofo/jicofo.conf"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.bridge.selection-strategy" "VisitorSelectionStrategy"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.bridge.visitor-selection-strategy" "RegionBasedBridgeSelectionStrategy"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.bridge.participant-selection-strategy" "RegionBasedBridgeSelectionStrategy"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.bridge.topology-strategy" "VisitorTopologyStrategy"
|
||||
|
||||
PASS=$(hocon -f $HOCON_CONFIG get "jicofo.xmpp.client.password")
|
||||
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
|
||||
do
|
||||
prosodyctl --config /etc/prosody-v${i}/prosody.cfg.lua register focus auth.meet.jitsi $PASS
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.enabled" true
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.conference-service" "conference.v${i}.meet.jitsi"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.hostname" 127.0.0.1
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.port" 5222${i}
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.domain" "auth.meet.jitsi"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.xmpp-domain" "v${i}.meet.jitsi"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.password" "${PASS}"
|
||||
hocon -f $HOCON_CONFIG set "jicofo.xmpp.visitors.v${i}.disable-certificate-verification" true
|
||||
done
|
||||
|
||||
for (( i=1 ; i<=${NUMBER_OF_INSTANCES} ; i++ ));
|
||||
do
|
||||
service prosody-v${i} restart
|
||||
done
|
||||
service jicofo restart
|
||||
46
resources/extra-large-conference/prosody-v.service.template
Normal file
46
resources/extra-large-conference/prosody-v.service.template
Normal file
@@ -0,0 +1,46 @@
|
||||
[Unit]
|
||||
### see man systemd.unit
|
||||
Description=Prosody vX (visitor vX) JVB XMPP Server
|
||||
Documentation=https://prosody.im/doc
|
||||
|
||||
Requires=network-online.target
|
||||
After=network-online.target network.target mariadb.service mysql.service postgresql.service
|
||||
Before=biboumi.service
|
||||
|
||||
[Service]
|
||||
### see man systemd.service
|
||||
Type=simple
|
||||
|
||||
# Start by executing the main executable
|
||||
# Note: -F option requires Prosody 0.11.5 or later
|
||||
ExecStart=/usr/bin/prosody --config /etc/prosody-vX/prosody.cfg.lua -F
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-abnormal
|
||||
|
||||
### see man systemd.exec
|
||||
User=prosody
|
||||
Group=prosody
|
||||
UMask=0027
|
||||
|
||||
RuntimeDirectory=prosody-vX
|
||||
ConfigurationDirectory=prosody-vX
|
||||
StateDirectory=prosody-vX
|
||||
StateDirectoryMode=0750
|
||||
LogsDirectory=prosody-vX
|
||||
WorkingDirectory=~
|
||||
|
||||
# Set stdin to /dev/null since Prosody does not need it
|
||||
StandardInput=null
|
||||
|
||||
# Direct stdout/-err to journald for use with log = "*stdout"
|
||||
StandardOutput=journal
|
||||
StandardError=inherit
|
||||
|
||||
# Allow binding low ports
|
||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
||||
|
||||
[Install]
|
||||
### see man systemd.unit
|
||||
WantedBy=multi-user.target
|
||||
|
||||
# vim: filetype=systemd
|
||||
@@ -0,0 +1,140 @@
|
||||
---------- Server-wide settings ----------
|
||||
s2s_ports = { 52691 };
|
||||
c2s_ports = { 52221 }
|
||||
http_ports = { 52801 }
|
||||
https_ports = { 52811 }
|
||||
|
||||
daemonize = true;
|
||||
|
||||
-- we use a common jid for jicofo
|
||||
admins = {
|
||||
'focus@auth.meet.jitsi'
|
||||
}
|
||||
|
||||
-- Enable use of native prosody 0.11 support for epoll over select
|
||||
network_backend = 'epoll';
|
||||
network_settings = {
|
||||
tcp_backlog = 511;
|
||||
}
|
||||
|
||||
modules_enabled = {
|
||||
'saslauth';
|
||||
'tls';
|
||||
'disco';
|
||||
'posix';
|
||||
|
||||
'secure_interfaces';
|
||||
|
||||
-- jitsi
|
||||
'websocket';
|
||||
'bosh';
|
||||
's2s_bidi';
|
||||
's2s_whitelist';
|
||||
's2sout_override';
|
||||
'certs_s2soutinjection';
|
||||
};
|
||||
|
||||
s2s_whitelist = {
|
||||
'conference.jitmeet.example.com', -- needed for visitors to send messages to main room
|
||||
'visitors.jitmeet.example.com'; -- needed for sending promotion request to visitors.jitmeet.example.com component
|
||||
'jitmeet.example.com'; -- unavailable presences back to main room
|
||||
};
|
||||
|
||||
s2sout_override = {
|
||||
["conference.jitmeet.example.com"] = "tcp://127.0.0.1:5269"; -- needed for visitors to send messages to main room
|
||||
["jitmeet.example.com"] = "tcp://127.0.0.1:5269"; -- needed for the main room when connecting in to send main participants
|
||||
["visitors.jitmeet.example.com"] = "tcp://127.0.0.1:5269"; -- needed for sending promotion request to visitors.jitmeet.example.com component
|
||||
}
|
||||
|
||||
external_service_secret = '__turnSecret__';
|
||||
|
||||
external_services = {
|
||||
{ type = 'stun', host = 'jitmeet.example.com', port = 3478 },
|
||||
{ type = 'turn', host = 'jitmeet.example.com', port = 3478, transport = 'udp', secret = true, ttl = 86400, algorithm = 'turn' },
|
||||
{ type = 'turns', host = 'jitmeet.example.com', port = 5349, transport = 'tcp', secret = true, ttl = 86400, algorithm = 'turn' }
|
||||
};
|
||||
|
||||
muc_mapper_domain_base = 'vX.meet.jitsi';
|
||||
main_domain = 'jitmeet.example.com';
|
||||
|
||||
-- https://prosody.im/doc/modules/mod_smacks
|
||||
smacks_max_unacked_stanzas = 5;
|
||||
smacks_hibernation_time = 60;
|
||||
-- this is dropped in 0.12
|
||||
smacks_max_hibernated_sessions = 1;
|
||||
smacks_max_old_sessions = 1;
|
||||
|
||||
unlimited_jids = { 'focus@auth.meet.jitsi' }
|
||||
limits = {
|
||||
c2s = {
|
||||
rate = '512kb/s';
|
||||
};
|
||||
}
|
||||
|
||||
modules_disabled = {
|
||||
'offline';
|
||||
'pubsub';
|
||||
'register';
|
||||
};
|
||||
|
||||
allow_registration = false;
|
||||
authentication = 'internal_hashed'
|
||||
storage = 'internal'
|
||||
log = {
|
||||
-- Log files (change 'info' to 'debug' for debug logs):
|
||||
info = '/var/log/prosody-vX/prosody.log';
|
||||
error = '/var/log/prosody-vX/prosody.err';
|
||||
}
|
||||
|
||||
consider_websocket_secure = true;
|
||||
consider_bosh_secure = true;
|
||||
bosh_max_inactivity = 60;
|
||||
|
||||
plugin_paths = { '/usr/share/jitsi-meet/prosody-plugins/' }
|
||||
|
||||
----------- Virtual hosts -----------
|
||||
VirtualHost 'vX.meet.jitsi'
|
||||
authentication = 'jitsi-anonymous'
|
||||
ssl = {
|
||||
key = '/etc/prosody/certs/jitmeet.example.com.key';
|
||||
certificate = '/etc/prosody/certs/jitmeet.example.com.crt';
|
||||
}
|
||||
modules_enabled = {
|
||||
'bosh';
|
||||
'ping';
|
||||
'external_services';
|
||||
'smacks';
|
||||
'jiconop';
|
||||
'conference_duration';
|
||||
}
|
||||
main_muc = 'conference.vX.meet.jitsi';
|
||||
|
||||
VirtualHost 'auth.meet.jitsi'
|
||||
modules_enabled = {
|
||||
'limits_exception';
|
||||
'ping';
|
||||
'smacks';
|
||||
}
|
||||
authentication = 'internal_hashed'
|
||||
smacks_hibernation_time = 15;
|
||||
|
||||
Component 'conference.vX.meet.jitsi' 'muc'
|
||||
storage = 'memory'
|
||||
muc_room_cache_size = 10000
|
||||
restrict_room_creation = true
|
||||
modules_enabled = {
|
||||
'muc_hide_all';
|
||||
'muc_domain_mapper';
|
||||
'muc_meeting_id';
|
||||
'fmuc';
|
||||
's2s_bidi';
|
||||
's2s_whitelist';
|
||||
's2sout_override';
|
||||
}
|
||||
muc_room_default_presence_broadcast = {
|
||||
visitor = false;
|
||||
participant = true;
|
||||
moderator = true;
|
||||
};
|
||||
muc_room_locking = false
|
||||
muc_room_default_public_jids = true
|
||||
292
resources/file-sharing.yaml
Normal file
292
resources/file-sharing.yaml
Normal file
@@ -0,0 +1,292 @@
|
||||
openapi: 3.0.1
|
||||
info:
|
||||
title: File Sharing API
|
||||
description: Management of the file sharing feature
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: https://your-server-here
|
||||
description: Generated server url
|
||||
security:
|
||||
- HttpBearerKey: []
|
||||
tags:
|
||||
- name: Content sharing history
|
||||
description: crud operation for shared documents
|
||||
paths:
|
||||
/v1/documents/sessions/{sessionId}/files:
|
||||
get:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Return a list of metadata with pre-sign urls for download
|
||||
description: Used to get the list of files in past meetings
|
||||
operationId: retrieveContentSharingHistory_1
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: offset
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
default: 0
|
||||
- name: page-size
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
default: 20
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CssFileMetadataResponse'
|
||||
post:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Save an document and metadata
|
||||
description: Add a document to a meeting - allowed only to those with file-sharing feature if any
|
||||
operationId: addDocumentInMeeting
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
description: The session ID of the meeting.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
required:
|
||||
- file
|
||||
- metadata
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: string
|
||||
description: The metadata of the document in JSON format. Must conform to FileMetadata schema.
|
||||
example: '{"conferenceFullName":"myroomname@conference.tenant.jitsi-meet.example.com","timestamp":1741017572040,"fileSize":1042157,"fileId":"e393a7e5-e790-4f43-836e-d27238201904"}'
|
||||
file:
|
||||
type: string
|
||||
description: The file to be uploaded.
|
||||
format: binary
|
||||
responses:
|
||||
'200':
|
||||
description: Document added successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/AddDocumentResponse'
|
||||
example:
|
||||
fileId: e393a7e5-e790-4f43-836e-d27238201904
|
||||
delete:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Deletes documents for a given session, user and customer
|
||||
operationId: deleteDocumentsForSessionCustomerAndUser
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: user-id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: customer-id
|
||||
in: query
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/v1/documents/sessions/{sessionId}/files/{fileId}:
|
||||
get:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Get file pre-signed URL plus document info
|
||||
description: Used by UI to get the presign url for the file before serving it to the user who needs it
|
||||
operationId: getDocumentInfoDuringMeeting
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: fileId
|
||||
in: path
|
||||
description: The file ID to be deleted.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
content:
|
||||
'*/*':
|
||||
schema:
|
||||
$ref: '#/components/schemas/DocumentMetadataResponse'
|
||||
delete:
|
||||
tags:
|
||||
- Document sharing history
|
||||
summary: Delete a file by sessionId and fileId
|
||||
description: Delete a file by sessionId and fileId allowed by all moderators
|
||||
operationId: deleteFile
|
||||
parameters:
|
||||
- name: sessionId
|
||||
in: path
|
||||
description: The session ID of the meeting.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: 86bf35e2-62a5-497e-9cae-efd35139f81f
|
||||
- name: fileId
|
||||
in: path
|
||||
description: The file ID to be deleted.
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
components:
|
||||
schemas:
|
||||
FileMetadata:
|
||||
type: object
|
||||
required:
|
||||
- fileId
|
||||
- conferenceFullName
|
||||
- timestamp
|
||||
- fileSize
|
||||
properties:
|
||||
fileId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Client-generated unique identifier for the file
|
||||
example: e393a7e5-e790-4f43-836e-d27238201904
|
||||
conferenceFullName:
|
||||
type: string
|
||||
description: Full name of the conference/meeting room
|
||||
example: myroomname@conference.tenant.jitsi-meet.example.com
|
||||
timestamp:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Upload timestamp in milliseconds since epoch
|
||||
example: 1741017572040
|
||||
fileSize:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Size of the file in bytes
|
||||
example: 1042157
|
||||
description: Metadata structure that must be sent as JSON string in the metadata field
|
||||
Payload:
|
||||
type: object
|
||||
properties:
|
||||
isBreakout:
|
||||
type: boolean
|
||||
conference:
|
||||
type: string
|
||||
AddDocumentResponse:
|
||||
type: object
|
||||
properties:
|
||||
fileId:
|
||||
type: string
|
||||
description: File ID of the added document
|
||||
example: e393a7e5-e790-4f43-836e-d27238201904
|
||||
description: Response body containing the fileId of the added document
|
||||
CssFileMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
objectId:
|
||||
type: string
|
||||
description: Object id - can be file id
|
||||
example: e393a7e5-e790-4f43-836e-d27238201904
|
||||
sessionId:
|
||||
type: string
|
||||
description: Session id
|
||||
example: 85a32e37-ddd5-45de-89a6-e94ccffe547a
|
||||
timestamp:
|
||||
type: integer
|
||||
description: Added timestamp
|
||||
format: int64
|
||||
example: 124
|
||||
contentType:
|
||||
type: string
|
||||
description: Content type
|
||||
example: application/pdf
|
||||
objectName:
|
||||
type: string
|
||||
description: Object name
|
||||
example: e393a7e5-e790-4f43-836e-d27238201904
|
||||
initiatorId:
|
||||
type: string
|
||||
description: User id for the author
|
||||
example: f56g5y4
|
||||
preSignedUrl:
|
||||
type: string
|
||||
description: Presign url - expires after 24h for JaaS
|
||||
example: https://oracle.com/presigned-url
|
||||
description: Response body containing the file metadata
|
||||
PaginatedResponseCssFileMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/CssFileMetadataResponse'
|
||||
nextStartWith:
|
||||
type: string
|
||||
DocumentMetadataResponse:
|
||||
type: object
|
||||
properties:
|
||||
fileId:
|
||||
type: string
|
||||
description: File ID of the document
|
||||
example: e393a7e5-e790-4f43-836e-d27238201904
|
||||
sessionId:
|
||||
type: string
|
||||
description: Session ID of the document
|
||||
example: 85a32e37-ddd5-45de-89a6-e94ccffe547a
|
||||
fileName:
|
||||
type: string
|
||||
description: Filename
|
||||
example: sample.pdf
|
||||
customerId:
|
||||
type: string
|
||||
description: Customer id
|
||||
example: vthtryv56yb65
|
||||
userId:
|
||||
type: string
|
||||
description: User id
|
||||
example: dvdsvfhjv
|
||||
presignedUrl:
|
||||
type: string
|
||||
description: Presign url - points to document sharing since for CSS the link expires in 3h
|
||||
example: https://content-sharing-url.com
|
||||
createdAt:
|
||||
type: integer
|
||||
description: Created at
|
||||
format: int64
|
||||
example: 1745436546
|
||||
fileSize:
|
||||
type: integer
|
||||
description: File Size
|
||||
format: int64
|
||||
example: 124
|
||||
description: Response body containing the document metadata
|
||||
securitySchemes:
|
||||
HttpBearerKey:
|
||||
type: http
|
||||
description: Http request Authorization header
|
||||
scheme: bearer
|
||||
BIN
resources/img/appstore-badge.png
Normal file
BIN
resources/img/appstore-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
BIN
resources/img/f-droid-badge.png
Normal file
BIN
resources/img/f-droid-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
resources/img/google-play-badge.png
Normal file
BIN
resources/img/google-play-badge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
74
resources/install-letsencrypt-cert.sh
Executable file
74
resources/install-letsencrypt-cert.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "-------------------------------------------------------------------------"
|
||||
echo "This script will:"
|
||||
echo "- Need a working DNS record pointing to this machine(for hostname ${DOMAIN})"
|
||||
echo "- Install additional dependencies in order to request Let’s Encrypt certificate (acme.sh)"
|
||||
echo "- Configure and reload nginx or apache2, whichever is used"
|
||||
echo "- Configure the coturn server to use Let's Encrypt certificate and add required deploy hooks"
|
||||
echo "- Configure renew of certificate"
|
||||
echo ""
|
||||
|
||||
EMAIL=$1
|
||||
|
||||
if [ -z "$EMAIL" ]; then
|
||||
echo "You need to agree to the ACME server's Subscriber Agreement (https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf) "
|
||||
echo "by providing an email address for important account notifications"
|
||||
|
||||
echo -n "Enter your email and press [ENTER]: "
|
||||
read EMAIL
|
||||
fi
|
||||
|
||||
DOMAIN=$2
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
DEB_CONF_RESULT=$(debconf-show jitsi-meet-web-config | grep jitsi-meet/jvb-hostname)
|
||||
DOMAIN="${DEB_CONF_RESULT##*:}"
|
||||
fi
|
||||
# remove whitespace
|
||||
DOMAIN="$(echo -e "${DOMAIN}" | tr -d '[:space:]')"
|
||||
|
||||
|
||||
export HOME=/opt/acmesh
|
||||
curl https://get.acme.sh | sh -s email=$EMAIL
|
||||
|
||||
# Checks whether nginx or apache is installed
|
||||
NGINX_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx' 2>/dev/null | awk '{print $3}' || true)"
|
||||
NGINX_FULL_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-full' 2>/dev/null | awk '{print $3}' || true)"
|
||||
NGINX_EXTRAS_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'nginx-extras' 2>/dev/null | awk '{print $3}' || true)"
|
||||
OPENRESTY_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'openresty' 2>/dev/null | awk '{print $3}' || true)"
|
||||
APACHE_INSTALL_CHECK="$(dpkg-query -f '${Status}' -W 'apache2' 2>/dev/null | awk '{print $3}' || true)"
|
||||
|
||||
RELOAD_CMD=""
|
||||
if [ "$NGINX_INSTALL_CHECK" = "installed" ] || [ "$NGINX_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| [ "$NGINX_FULL_INSTALL_CHECK" = "installed" ] || [ "$NGINX_FULL_INSTALL_CHECK" = "unpacked" ] \
|
||||
|| [ "$NGINX_EXTRAS_INSTALL_CHECK" = "installed" ] || [ "$NGINX_EXTRAS_INSTALL_CHECK" = "unpacked" ]; then
|
||||
RELOAD_CMD="systemctl force-reload nginx.service"
|
||||
elif [ "$OPENRESTY_INSTALL_CHECK" = "installed" ] || [ "$OPENRESTY_INSTALL_CHECK" = "unpacked" ] ; then
|
||||
RELOAD_CMD="systemctl force-reload openresty.service"
|
||||
elif [ "$APACHE_INSTALL_CHECK" = "installed" ] || [ "$APACHE_INSTALL_CHECK" = "unpacked" ] ; then
|
||||
RELOAD_CMD="systemctl force-reload apache2.service"
|
||||
else
|
||||
RELOAD_CMD="echo 'No webserver found'"
|
||||
fi
|
||||
|
||||
RELOAD_CMD+=" && /usr/share/jitsi-meet/scripts/coturn-le-update.sh ${DOMAIN}"
|
||||
|
||||
ISSUE_FAILED_CODE=0
|
||||
ISSUE_CERT_CMD="/opt/acmesh/.acme.sh/acme.sh -f --issue -d ${DOMAIN} -w /usr/share/jitsi-meet --server letsencrypt"
|
||||
eval "${ISSUE_CERT_CMD}" || ISSUE_FAILED_CODE=$?
|
||||
|
||||
INSTALL_CERT_CMD="/opt/acmesh/.acme.sh/acme.sh -f --install-cert -d ${DOMAIN} --key-file /etc/jitsi/meet/${DOMAIN}.key --fullchain-file /etc/jitsi/meet/${DOMAIN}.crt --reloadcmd \"${RELOAD_CMD}\""
|
||||
if [ ${ISSUE_FAILED_CODE} -ne 0 ] ; then
|
||||
# it maybe this certificate already exists (code 2 - skip, no need to renew)
|
||||
if [ ${ISSUE_FAILED_CODE} -eq 2 ]; then
|
||||
eval "$INSTALL_CERT_CMD"
|
||||
else
|
||||
echo "Issuing the certificate from Let's Encrypt failed, continuing ..."
|
||||
echo "You can retry later by executing:"
|
||||
echo "/usr/share/jitsi-meet/scripts/install-letsencrypt-cert.sh $EMAIL"
|
||||
fi
|
||||
else
|
||||
eval "$INSTALL_CERT_CMD"
|
||||
fi
|
||||
8
resources/lang-sort.sh
Executable file
8
resources/lang-sort.sh
Executable file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
for file in ./lang/*.json; do
|
||||
echo "Sorting and standardizing ${file}"
|
||||
t=$(mktemp)
|
||||
jq --indent 4 -S "." "${file}" > "${t}"
|
||||
mv "${t}" "${file}"
|
||||
done
|
||||
92
resources/prosody-plugins/README.md
Normal file
92
resources/prosody-plugins/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
#### room._data fields
|
||||
- allModerators - If is set to true, all authenticated participants are moderators. You need a custom module to set participants as moderators based on the field.
|
||||
- av_can_unmute - Default value is missing/true. If set to false, when the first moderator joining the room we enable AV moderation and disable the ability for participants to unmute themselves.
|
||||
- av_first_moderator_joined - When av_can_unmute is set to false, this flag is used to indicate that the first moderator has joined the room and AV moderation is enabled.
|
||||
- breakout_rooms - A table containing breakout rooms created in the main room. The keys are the JIDs of the breakout rooms, and the values are their subjects.
|
||||
- breakout_rooms_active - Whether there was a breakout room created in the main room.
|
||||
- breakout_rooms_counter - A counter for breakout rooms created in the main room.
|
||||
- flip_participant_nick - Used in mod_muc_flip, when flipping a participant we store the nick of the second device/participant. Same processing as kicked_participant_nick.
|
||||
- hideDisplayNameForAll - TODO: drop.
|
||||
- hideDisplayNameForGuests - When set to true, the display name of participants is hidden for guests.
|
||||
- jicofo_lock - A boolean value, when set to true the room is locked waiting for Jicofo to join. All attempts to join will be queued until Jicofo joins.
|
||||
- kicked_participant_nick - Used in mod_muc_flip, when flipping a participant we store the nick of the initial device/participant so we can kick it after the second on joins, everything happens in the same pass when the second join presence is processed, so concurrency and overwriting are not a concern.
|
||||
- lobby_extra_reason - Used in mod_muc_lobby_rooms to store the reason for creating a lobby room. This is set by the global event 'create-lobby-room' and can be used to provide additional context or information about the lobby room creation. Used by mod_muc_wait_for_host with value 'waiting-for-host'. Value is sent as the error reply 'registration-required'.
|
||||
- lobby_skip_display_name_check - Used in mod_muc_lobby_rooms to skip the display name check for the lobby room. This is used when the lobby room is created with a specific reason that does not require a display name check via the global event 'create-lobby-room'.
|
||||
- lobbyroom - Holds the JID of the lobby room if it exists. It is set by mod_muc_lobby_rooms.lua when a lobby room is created. Value is available in the room config form for all clients to use it.
|
||||
- max_occupants - The maximum number of occupants allowed in the room used by mod_muc_max_occupants.
|
||||
- meetingId - A unique identifier for the meeting, generated by mod_muc_meeting_id. Set in the room config form for all clients to use it. It is also sent to all visitor nodes if any.
|
||||
- moderator_id - The id (from the token) for the moderator of the room. The value can be userId or a groupId where all participants from the same group will be moderators. You need a custom module to set participants as moderators based on the field.
|
||||
- moderators - A list of moderator ids (from the token) that are allowed to join the room. The list is sent to jicofo to take decisions about forwarding to main room or visitor node in case of large meetings. Those participants will be moderators. You need a custom module to set participants as moderators based on the field.
|
||||
- participants - A list of participants ids (from the token) that are allowed to join the room. The list is sent to jicofo to take decisions about forwarding to main room or visitor node in case of large meetings. Those participants will not be moderators.
|
||||
- participants_details - Used in mod_muc_flip to store details about authenticated participants in the room. A table where the keys are participant id from the token and the values are their room jid.
|
||||
- persist_lobby - A boolean value, when set to true the lobby room will be persistent and not destroyed after the main room is destroyed. Used by mod_persistent_lobby.
|
||||
- subject - The subject of the room. Set by subject.lib.lua in prosody.
|
||||
|
||||
# room fields added by jitsi
|
||||
- _connected_vnodes - A cache object that holds connected vnodes for the room. It is used by mod_visitors_component.lua to manage connected vnodes.
|
||||
- _jid_nick - Prosody's internal table that maps connection JIDs to room jids in the room. It is used by mod_muc_max_occupants.lua.
|
||||
- _jitsi_go_live_sent - A boolean value indicating whether the go live request has been sent for the room. It is used by mod_visitors_component.lua to manage the go live state of the room.
|
||||
- _main_room_lobby_enabled - A boolean value indicating whether the main room lobby is enabled. It is set by mod_fmuc.lua when the main room lobby is enabled or disabled.
|
||||
- _muc_messages_count - A counter for the number of messages sent in the room. It is used by mod_measure_message_count.lua to track message counts for analytics.
|
||||
- _muc_messages_limit - A boolean value indicating whether the message limit is enabled for the room. It is used by mod_muc_limit_messages.lua to control the message limit in the room.
|
||||
- _muc_messages_limit_count - A counter for the number of messages sent in the room when the message limit is enabled. It is used by mod_muc_limit_messages.lua to track message counts for the limit.
|
||||
- _muc_polls_count - A counter for the number of polls created in the room. It is used by mod_measure_message_count.lua to track poll counts for analytics.
|
||||
- _occupants - Prosody's internal table that holds room occupants.
|
||||
- _transcription_languages - A table containing transcription languages for each occupant in the room. The keys are occupant JIDs and the values are language codes. Used by mod_fmuc.lua to manage transcription languages for occupants.
|
||||
- av_moderation - A table containing media types as keys ('audio', 'video', 'desktop') and arrays of occupant nicknames that are allowed to unmute themselves. Used by mod_av_moderation_component.lua.
|
||||
- av_moderation_actors - A table containing media types as keys ('audio', 'video', 'desktop') and occupant nicknames of the moderators that performed the last AV moderation action. Used by mod_av_moderation_component.lua.
|
||||
- av_moderation_startMuted_restore - Keeps the startMuted metadata state of the room before AV moderation was applied. Used by mod_av_moderation_component.lua.
|
||||
- broadcast_timer - A timer used to broadcast the list of breakout rooms in the main room. It is set by mod_muc_breakout_rooms.lua to periodically update the list of breakout rooms.
|
||||
- close_timer - A timer used to close the main room when all occupants have left. It is set by mod_muc_breakout_rooms.lua to clean up the main room after all participants left from breakout room.
|
||||
- created_timestamp - A timestamp in milliseconds when the room was created. It is set by mod_conference_duration_component.lua and used by mod_conference_duration.lua to calculate the conference duration. It is also used by mod_measure_message_count.lua to calculate the room duration for analytics.
|
||||
- has_host - Whether the host (an authenticated user) has arrived in the room.
|
||||
- is_vpaas - A boolean value indicating whether the room is a VPAAS room, optimization for the is_vpaas function in util.internal.lib.lua.
|
||||
- jibri_throttle - A throttle object used to limit the number of Jibri requests per room. It is created in mod_filter_iq_jibri.lua to prevent abuse of Jibri resources.
|
||||
- jitsi_meet_tenant_mismatch - A boolean value indicating whether there is a tenant mismatch comparing room tenant and jwt tenant value.
|
||||
- jitsi_shared_files - A table containing shared files in the room. The keys are file IDs and the values are file objects with properties like fileId, name, size, type, and url. Used by mod_filesharing_component.lua to manage file sharing in the room.
|
||||
- jitsiMetadata - The metadata for the room. Example values:
|
||||
```json
|
||||
{
|
||||
"allownersEnabled": true,
|
||||
"asyncTranscription": true,
|
||||
"conferencePresetsServiceEnabled": true,
|
||||
"participantsSoftLimit": 200,
|
||||
"permissions": {
|
||||
"groupChatRestricted": true,
|
||||
"pollCreationRestricted": true
|
||||
},
|
||||
"recording": {
|
||||
"autoAudioRecording": true,
|
||||
"autoVideoRecording": true,
|
||||
"autoTranscriptions": true,
|
||||
"isTranscribingEnabled": true
|
||||
},
|
||||
"startMuted": {
|
||||
"audio": true,
|
||||
"video": true
|
||||
},
|
||||
"transcriberType": "GOOGLE | ORACLE_CLOUD_AI_SPEECH | EGHT_WHISPER",
|
||||
"visitors": {
|
||||
"autoPromote": true,
|
||||
"live": true
|
||||
},
|
||||
"visitorsEnabled": true
|
||||
}
|
||||
```
|
||||
- join_rate_presence_queue - A queue used to hold join presence requests when the join rate throttle is exceeded. It is set by mod_muc_rate_limit.lua to manage join requests in the room.
|
||||
- join_rate_queue_timer - A timer used to process the join rate presence queue. It is set by mod_muc_rate_limit.lua to periodically check and process join requests in the queue.
|
||||
- join_rate_throttle - A throttle object used to limit the rate of join requests in the room. It is set by mod_muc_rate_limit.lua to prevent high join rate in case of large conferences.
|
||||
- leave_rate_presence_queue - A queue used to hold leave presence requests when the leave rate throttle is exceeded. It is set by mod_muc_rate_limit.lua to manage leave requests in the room.
|
||||
- leave_rate_queue_timer - A timer used to process the leave rate presence queue. It is set by mod_muc_rate_limit.lua to periodically check and process leave requests in the queue.
|
||||
- leave_rate_throttle - A throttle object used to limit the rate of leave requests in the room. It is set by mod_muc_rate_limit.lua to prevent high leave rate in case of large conferences.
|
||||
- main_room - For lobby or breakout rooms it is the main room object.
|
||||
- moderators_list - A set containing the JIDs of moderators in the room. It is used by mod_fmuc.lua to manage moderators in the room.
|
||||
- polls - A table containing polls created in the room. The keys are poll IDs and the values are poll objects with properties like id, question, options, votes, and creator. Used by mod_polls.lua to manage polls in the room.
|
||||
- pre_join_queue - A queue used to hold join requests when the room is locked waiting for Jicofo to join. It is set by mod_muc_meeting_id.lua when the room is locked. The queue is processed once Jicofo joins the room.
|
||||
- send_default_permissions_to - A table containing the default permissions to be sent to occupants in the room. The keys are bare JIDs and the values are boolean values indicating whether the permissions should be sent. Used by mod_jitsi_permissions.lua to manage permissions in the room.
|
||||
- sent_initial_metadata - A table containing the initial metadata sent to occupants in the room. The keys are bare JIDs and the values are boolean values indicating whether the metadata has been sent. Used by mod_room_metadata_component.lua to manage metadata in the room.
|
||||
- speakerStats - A table containing speaker statistics for occupants in the room. The keys are occupant JIDs and the values are objects with properties like dominantSpeakerId, faceLandmarks, and sessionId. Used by mod_speakerstats_component.lua to manage speaker statistics in the room.
|
||||
- visitors_destroy_timer - A timer used to destroy the room when there are no main occupants or visitors left. It is set by mod_fmuc.lua to clean up the room after a certain period of inactivity.
|
||||
|
||||
#### Notes:
|
||||
When modules need to store data they should do it in the room object in _data or directly. The data needs to be a simple as strings or table of strings, they should not add objects like room, sessions or occupants that cannot be serialized. Attaching data to the room object makes reloading modules safe and guarantees data will be wiped once the room is destroyed.
|
||||
263
resources/prosody-plugins/luajwtjitsi.lib.lua
Normal file
263
resources/prosody-plugins/luajwtjitsi.lib.lua
Normal file
@@ -0,0 +1,263 @@
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
local basexx = require 'basexx'
|
||||
local digest = require 'openssl.digest'
|
||||
local hmac = require 'openssl.hmac'
|
||||
local pkey = require 'openssl.pkey'
|
||||
|
||||
-- Generates an RSA signature of the data.
|
||||
-- @param data The data to be signed.
|
||||
-- @param key The private signing key in PEM format.
|
||||
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
|
||||
-- @return The signature or nil and an error message.
|
||||
local function signRS (data, key, algo)
|
||||
local privkey = pkey.new(key)
|
||||
if privkey == nil then
|
||||
return nil, 'Not a private PEM key'
|
||||
else
|
||||
local datadigest = digest.new(algo):update(data)
|
||||
return privkey:sign(datadigest)
|
||||
end
|
||||
end
|
||||
|
||||
-- Verifies an RSA signature on the data.
|
||||
-- @param data The signed data.
|
||||
-- @param signature The signature to be verified.
|
||||
-- @param key The public key of the signer.
|
||||
-- @param algo The digest algorithm to user when generating the signature: sha256, sha384, or sha512.
|
||||
-- @return True if the signature is valid, false otherwise. Also returns false if the key is invalid.
|
||||
local function verifyRS (data, signature, key, algo)
|
||||
local pubkey = pkey.new(key)
|
||||
if pubkey == nil then
|
||||
return false
|
||||
end
|
||||
|
||||
local datadigest = digest.new(algo):update(data)
|
||||
return pubkey:verify(signature, datadigest)
|
||||
end
|
||||
|
||||
local alg_sign = {
|
||||
['HS256'] = function(data, key) return hmac.new(key, 'sha256'):final(data) end,
|
||||
['HS384'] = function(data, key) return hmac.new(key, 'sha384'):final(data) end,
|
||||
['HS512'] = function(data, key) return hmac.new(key, 'sha512'):final(data) end,
|
||||
['RS256'] = function(data, key) return signRS(data, key, 'sha256') end,
|
||||
['RS384'] = function(data, key) return signRS(data, key, 'sha384') end,
|
||||
['RS512'] = function(data, key) return signRS(data, key, 'sha512') end
|
||||
}
|
||||
|
||||
local alg_verify = {
|
||||
['HS256'] = function(data, signature, key) return signature == alg_sign['HS256'](data, key) end,
|
||||
['HS384'] = function(data, signature, key) return signature == alg_sign['HS384'](data, key) end,
|
||||
['HS512'] = function(data, signature, key) return signature == alg_sign['HS512'](data, key) end,
|
||||
['RS256'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha256') end,
|
||||
['RS384'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha384') end,
|
||||
['RS512'] = function(data, signature, key) return verifyRS(data, signature, key, 'sha512') end
|
||||
}
|
||||
|
||||
-- Splits a token into segments, separated by '.'.
|
||||
-- @param token The full token to be split.
|
||||
-- @return A table of segments.
|
||||
local function split_token(token)
|
||||
local segments={}
|
||||
for str in string.gmatch(token, "([^\\.]+)") do
|
||||
table.insert(segments, str)
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
-- Parses a JWT token into it's header, body, and signature.
|
||||
-- @param token The JWT token to be parsed.
|
||||
-- @return A JSON header and body represented as a table, and a signature.
|
||||
local function parse_token(token)
|
||||
local segments=split_token(token)
|
||||
if #segments ~= 3 then
|
||||
return nil, nil, nil, "Invalid token"
|
||||
end
|
||||
|
||||
local header, err = cjson_safe.decode(basexx.from_url64(segments[1]))
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid header"
|
||||
end
|
||||
|
||||
local body, err = cjson_safe.decode(basexx.from_url64(segments[2]))
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid body"
|
||||
end
|
||||
|
||||
local sig, err = basexx.from_url64(segments[3])
|
||||
if err then
|
||||
return nil, nil, nil, "Invalid signature"
|
||||
end
|
||||
|
||||
return header, body, sig
|
||||
end
|
||||
|
||||
-- Removes the signature from a JWT token.
|
||||
-- @param token A JWT token.
|
||||
-- @return The token without its signature.
|
||||
local function strip_signature(token)
|
||||
local segments=split_token(token)
|
||||
if #segments ~= 3 then
|
||||
return nil, nil, nil, "Invalid token"
|
||||
end
|
||||
|
||||
table.remove(segments)
|
||||
return table.concat(segments, ".")
|
||||
end
|
||||
|
||||
-- Verifies that a claim is in a list of allowed claims. Allowed claims can be exact values, or the
|
||||
-- catch all wildcard '*'.
|
||||
-- @param claim The claim to be verified.
|
||||
-- @param acceptedClaims A table of accepted claims.
|
||||
-- @return True if the claim was allowed, false otherwise.
|
||||
local function verify_claim(claim, acceptedClaims)
|
||||
for i, accepted in ipairs(acceptedClaims) do
|
||||
if accepted == '*' then
|
||||
return true;
|
||||
end
|
||||
if claim == accepted then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Encodes the data into a signed JWT token.
|
||||
-- @param data The data the put in the body of the JWT token.
|
||||
-- @param key The key to use for signing the JWT token.
|
||||
-- @param alg The signature algorithm to use: HS256, HS384, HS512, RS256, RS384, or RS512.
|
||||
-- @param header Additional values to put in the JWT header.
|
||||
-- @param The resulting JWT token, or nil and an error message.
|
||||
function M.encode(data, key, alg, header)
|
||||
if type(data) ~= 'table' then return nil, "Argument #1 must be table" end
|
||||
if type(key) ~= 'string' then return nil, "Argument #2 must be string" end
|
||||
|
||||
alg = alg or "HS256"
|
||||
|
||||
if not alg_sign[alg] then
|
||||
return nil, "Algorithm not supported"
|
||||
end
|
||||
|
||||
header = header or {}
|
||||
|
||||
header['typ'] = 'JWT'
|
||||
header['alg'] = alg
|
||||
|
||||
local headerEncoded, err = cjson_safe.encode(header)
|
||||
if headerEncoded == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local dataEncoded, err = cjson_safe.encode(data)
|
||||
if dataEncoded == nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
local segments = {
|
||||
basexx.to_url64(headerEncoded),
|
||||
basexx.to_url64(dataEncoded)
|
||||
}
|
||||
|
||||
local signing_input = table.concat(segments, ".")
|
||||
local signature, error = alg_sign[alg](signing_input, key)
|
||||
if signature == nil then
|
||||
return nil, error
|
||||
end
|
||||
|
||||
segments[#segments+1] = basexx.to_url64(signature)
|
||||
|
||||
return table.concat(segments, ".")
|
||||
end
|
||||
|
||||
-- Verify that the token is valid, and if it is return the decoded JSON payload data.
|
||||
-- @param token The token to verify.
|
||||
-- @param expectedAlgo The signature algorithm the caller expects the token to be signed with:
|
||||
-- HS256, HS384, HS512, RS256, RS384, or RS512.
|
||||
-- @param key The verification key used for the signature.
|
||||
-- @param acceptedIssuers Optional table of accepted issuers. If not nil, the 'iss' claim will be
|
||||
-- checked against this list.
|
||||
-- @param acceptedAudiences Optional table of accepted audiences. If not nil, the 'aud' claim will
|
||||
-- be checked against this list.
|
||||
-- @return A table representing the JSON body of the token, or nil and an error message.
|
||||
function M.verify(token, expectedAlgo, key, acceptedIssuers, acceptedAudiences)
|
||||
if type(token) ~= 'string' then return nil, "token argument must be string" end
|
||||
if type(expectedAlgo) ~= 'string' then return nil, "algorithm argument must be string" end
|
||||
if type(key) ~= 'string' then return nil, "key argument must be string" end
|
||||
if acceptedIssuers ~= nil and type(acceptedIssuers) ~= 'table' then
|
||||
return nil, "acceptedIssuers argument must be table"
|
||||
end
|
||||
if acceptedAudiences ~= nil and type(acceptedAudiences) ~= 'table' then
|
||||
return nil, "acceptedAudiences argument must be table"
|
||||
end
|
||||
|
||||
if not alg_verify[expectedAlgo] then
|
||||
return nil, "Algorithm not supported"
|
||||
end
|
||||
|
||||
local header, body, sig, err = parse_token(token)
|
||||
if err ~= nil then
|
||||
return nil, err
|
||||
end
|
||||
|
||||
-- Validate header
|
||||
if not header.typ or header.typ ~= "JWT" then
|
||||
return nil, "Invalid typ"
|
||||
end
|
||||
|
||||
if not header.alg or header.alg ~= expectedAlgo then
|
||||
return nil, "Invalid or incorrect alg"
|
||||
end
|
||||
|
||||
-- Validate signature
|
||||
if not alg_verify[expectedAlgo](strip_signature(token), sig, key) then
|
||||
return nil, 'Invalid signature'
|
||||
end
|
||||
|
||||
-- Validate body
|
||||
if body.exp and type(body.exp) ~= "number" then
|
||||
return nil, "exp must be number"
|
||||
end
|
||||
|
||||
if body.nbf and type(body.nbf) ~= "number" then
|
||||
return nil, "nbf must be number"
|
||||
end
|
||||
|
||||
|
||||
if body.exp and os.time() >= body.exp then
|
||||
local extra_msg = '';
|
||||
if body.iat then
|
||||
extra_msg = ", valid for:"..tostring(body.exp-body.iat).." sec";
|
||||
end
|
||||
return nil, "Not acceptable by exp ("..tostring(os.time()-body.exp).." sec since expired"..extra_msg..")"
|
||||
end
|
||||
|
||||
if body.nbf and os.time() < body.nbf then
|
||||
return nil, "Not acceptable by nbf"
|
||||
end
|
||||
|
||||
if acceptedIssuers ~= nil then
|
||||
local issClaim = body.iss;
|
||||
if issClaim == nil then
|
||||
return nil, "'iss' claim is missing";
|
||||
end
|
||||
if not verify_claim(issClaim, acceptedIssuers) then
|
||||
return nil, "invalid 'iss' claim";
|
||||
end
|
||||
end
|
||||
|
||||
if acceptedAudiences ~= nil then
|
||||
local audClaim = body.aud;
|
||||
if audClaim == nil then
|
||||
return nil, "'aud' claim is missing";
|
||||
end
|
||||
if not verify_claim(audClaim, acceptedAudiences) then
|
||||
return nil, "invalid 'aud' claim";
|
||||
end
|
||||
end
|
||||
|
||||
return body
|
||||
end
|
||||
|
||||
return M
|
||||
78
resources/prosody-plugins/mod_auth_jitsi-anonymous.lua
Normal file
78
resources/prosody-plugins/mod_auth_jitsi-anonymous.lua
Normal file
@@ -0,0 +1,78 @@
|
||||
-- Anonymous authentication with extras:
|
||||
-- * session resumption
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local generate_uuid = require "util.uuid".generate;
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local sasl = require "util.sasl";
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return nil, "Password based auth not supported";
|
||||
end
|
||||
|
||||
function provider.get_password(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Set password not supported";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler(session)
|
||||
-- Custom session matching so we can resume session even with randomly
|
||||
-- generated user IDs.
|
||||
local function get_username(self, message)
|
||||
if (session.previd ~= nil) then
|
||||
for _, session1 in pairs(sessions) do
|
||||
if (session1.resumption_token == session.previd) then
|
||||
self.username = session1.username;
|
||||
break;
|
||||
end
|
||||
end
|
||||
else
|
||||
self.username = message;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
return new_sasl(module.host, { anonymous = get_username });
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
||||
local function anonymous(self, message)
|
||||
-- Same as the vanilla anonymous auth plugin
|
||||
local username = generate_uuid();
|
||||
|
||||
-- This calls the handler created in 'provider.get_sasl_handler(session)'
|
||||
local result, err, msg = self.profile.anonymous(self, username, self.realm);
|
||||
|
||||
if result == true then
|
||||
if (self.username == nil) then
|
||||
-- Session was not resumed
|
||||
self.username = username;
|
||||
end
|
||||
return "success";
|
||||
else
|
||||
return "failure", err, msg;
|
||||
end
|
||||
end
|
||||
|
||||
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
|
||||
65
resources/prosody-plugins/mod_auth_jitsi-shared-secret.lua
Normal file
65
resources/prosody-plugins/mod_auth_jitsi-shared-secret.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
-- Authentication with shared secret where the username is ignored
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local saslprep = require "util.encodings".stringprep.saslprep;
|
||||
local secure_equals = require "util.hashes".equals;
|
||||
|
||||
local shared_secret = module:get_option_string('shared_secret');
|
||||
local shared_secret_prev = module:get_option_string('shared_secret_prev');
|
||||
if shared_secret == nil then
|
||||
module:log('error', 'No shared_secret specified. No secret to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
function provider.test_password(username, password)
|
||||
password = saslprep(password);
|
||||
if not password then
|
||||
return nil, "Password fails SASLprep.";
|
||||
end
|
||||
|
||||
if secure_equals(password, saslprep(shared_secret)) then
|
||||
return true;
|
||||
elseif (shared_secret_prev ~= nil and secure_equals(password, saslprep(shared_secret_prev))) then
|
||||
module:log("info", "Accepting login using previous shared secret, username=%s", username);
|
||||
return true;
|
||||
else
|
||||
return nil, "Auth failed. Invalid username or password.";
|
||||
end
|
||||
end
|
||||
|
||||
function provider.get_password(username)
|
||||
return shared_secret;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Set password not supported";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return true; -- all usernames exist
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler(session)
|
||||
local getpass_authentication_profile = {
|
||||
plain = function(_, username, realm)
|
||||
return shared_secret, true;
|
||||
end
|
||||
};
|
||||
return new_sasl(module.host, getpass_authentication_profile);
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
169
resources/prosody-plugins/mod_auth_token.lua
Normal file
169
resources/prosody-plugins/mod_auth_token.lua
Normal file
@@ -0,0 +1,169 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local generate_uuid = require "util.uuid".generate;
|
||||
local new_sasl = require "util.sasl".new;
|
||||
local sasl = require "util.sasl";
|
||||
local token_util = module:require "token/util".new(module);
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
-- no token configuration
|
||||
if token_util == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local measure_pre_fetch_fail = module:measure('pre_fetch_fail', 'counter');
|
||||
local measure_verify_fail = module:measure('verify_fail', 'counter');
|
||||
local measure_success = module:measure('success', 'counter');
|
||||
local measure_ban = module:measure('ban', 'counter');
|
||||
local measure_post_auth_fail = module:measure('post_auth_fail', 'counter');
|
||||
|
||||
-- define auth provider
|
||||
local provider = {};
|
||||
|
||||
local host = module.host;
|
||||
|
||||
-- Extract 'token' param from URL when session is created
|
||||
function init_session(event)
|
||||
local session, request = event.session, event.request;
|
||||
local query = request.url.query;
|
||||
|
||||
local token = nil;
|
||||
|
||||
-- extract token from Authorization header
|
||||
if request.headers["authorization"] then
|
||||
-- assumes the header value starts with "Bearer "
|
||||
token = request.headers["authorization"]:sub(8,#request.headers["authorization"])
|
||||
end
|
||||
|
||||
-- allow override of token via query parameter
|
||||
if query ~= nil then
|
||||
local params = formdecode(query);
|
||||
|
||||
-- The following fields are filled in the session, by extracting them
|
||||
-- from the query and no validation is being done.
|
||||
-- After validating auth_token will be cleaned in case of error and few
|
||||
-- other fields will be extracted from the token and set in the session
|
||||
|
||||
if params and params.token then
|
||||
token = params.token;
|
||||
end
|
||||
end
|
||||
|
||||
-- in either case set auth_token in the session
|
||||
session.auth_token = token;
|
||||
session.user_agent_header = request.headers['user_agent'];
|
||||
end
|
||||
|
||||
module:hook_global("bosh-session", init_session);
|
||||
module:hook_global("websocket-session", init_session);
|
||||
|
||||
function provider.test_password(username, password)
|
||||
return nil, "Password based auth not supported";
|
||||
end
|
||||
|
||||
function provider.get_password(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.set_password(username, password)
|
||||
return nil, "Set password not supported";
|
||||
end
|
||||
|
||||
function provider.user_exists(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.create_user(username, password)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.delete_user(username)
|
||||
return nil;
|
||||
end
|
||||
|
||||
function provider.get_sasl_handler(session)
|
||||
|
||||
local function get_username_from_token(self, message)
|
||||
|
||||
-- retrieve custom public key from server and save it on the session
|
||||
local pre_event_result = prosody.events.fire_event("pre-jitsi-authentication-fetch-key", session);
|
||||
if pre_event_result ~= nil and pre_event_result.res == false then
|
||||
module:log("warn",
|
||||
"Error verifying token on pre authentication stage:%s, reason:%s", pre_event_result.error, pre_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
measure_pre_fetch_fail(1);
|
||||
return pre_event_result.res, pre_event_result.error, pre_event_result.reason;
|
||||
end
|
||||
|
||||
local res, error, reason = token_util:process_and_verify_token(session);
|
||||
if res == false then
|
||||
module:log("warn",
|
||||
"Error verifying token err:%s, reason:%s tenant:%s room:%s user_agent:%s",
|
||||
error, reason, session.jitsi_web_query_prefix, session.jitsi_web_query_room,
|
||||
session.user_agent_header);
|
||||
session.auth_token = nil;
|
||||
measure_verify_fail(1);
|
||||
return res, error, reason;
|
||||
end
|
||||
|
||||
local shouldAllow = prosody.events.fire_event("jitsi-access-ban-check", session);
|
||||
if shouldAllow == false then
|
||||
module:log("warn", "user is banned")
|
||||
measure_ban(1);
|
||||
return false, "not-allowed", "user is banned";
|
||||
end
|
||||
|
||||
local customUsername = prosody.events.fire_event("pre-jitsi-authentication", session);
|
||||
if customUsername then
|
||||
self.username = customUsername;
|
||||
elseif session.previd ~= nil then
|
||||
for _, session1 in pairs(sessions) do
|
||||
if (session1.resumption_token == session.previd) then
|
||||
self.username = session1.username;
|
||||
break;
|
||||
end
|
||||
end
|
||||
else
|
||||
self.username = message;
|
||||
end
|
||||
|
||||
local post_event_result = prosody.events.fire_event("post-jitsi-authentication", session);
|
||||
if post_event_result ~= nil and post_event_result.res == false then
|
||||
module:log("warn",
|
||||
"Error verifying token on post authentication stage :%s, reason:%s", post_event_result.error, post_event_result.reason);
|
||||
session.auth_token = nil;
|
||||
measure_post_auth_fail(1);
|
||||
return post_event_result.res, post_event_result.error, post_event_result.reason;
|
||||
end
|
||||
|
||||
measure_success(1);
|
||||
return res;
|
||||
end
|
||||
|
||||
return new_sasl(host, { anonymous = get_username_from_token });
|
||||
end
|
||||
|
||||
module:provides("auth", provider);
|
||||
|
||||
local function anonymous(self, message)
|
||||
|
||||
local username = generate_uuid();
|
||||
|
||||
-- This calls the handler created in 'provider.get_sasl_handler(session)'
|
||||
local result, err, msg = self.profile.anonymous(self, username, self.realm);
|
||||
|
||||
if result == true then
|
||||
if (self.username == nil) then
|
||||
self.username = username;
|
||||
end
|
||||
return "success";
|
||||
else
|
||||
return "failure", err, msg;
|
||||
end
|
||||
end
|
||||
|
||||
sasl.registerMechanism("ANONYMOUS", {"anonymous"}, anonymous);
|
||||
6
resources/prosody-plugins/mod_av_moderation.lua
Normal file
6
resources/prosody-plugins/mod_av_moderation.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
-- TODO: Remove this file after several stable releases when people update their configs
|
||||
module:log('warn', 'mod_av_moderation is deprecated and will be removed in a future release. '
|
||||
.. 'Please update your config by removing this module from the list of loaded modules.');
|
||||
|
||||
module:depends('jitsi_session');
|
||||
module:depends('features_identity');
|
||||
431
resources/prosody-plugins/mod_av_moderation_component.lua
Normal file
431
resources/prosody-plugins/mod_av_moderation_component.lua
Normal file
@@ -0,0 +1,431 @@
|
||||
local util = module:require 'util';
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local is_admin = util.is_admin;
|
||||
local array = require "util.array";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
local main_virtual_host = module:get_option_string('muc_mapper_domain_base');
|
||||
if not main_virtual_host then
|
||||
module:log('warn', 'No "muc_mapper_domain_base" option set, disabling AV moderation.');
|
||||
return ;
|
||||
end
|
||||
|
||||
module:log('info', 'Starting av_moderation for %s', muc_component_host);
|
||||
|
||||
-- Returns the index of the given element in the table
|
||||
-- @param table in which to look
|
||||
-- @param elem the element for which to find the index
|
||||
function get_index_in_table(table, elem)
|
||||
for index, value in pairs(table) do
|
||||
if value == elem then
|
||||
return index
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
function send_json_message(to_jid, json_message)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
-- Notifies that av moderation has been enabled or disabled
|
||||
-- @param jid the jid to notify, if missing will notify all occupants
|
||||
-- @param enable whether it is enabled or disabled
|
||||
-- @param room the room
|
||||
-- @param actorJid the jid that is performing the enable/disable operation (the muc jid)
|
||||
-- @param mediaType the media type for the moderation
|
||||
function notify_occupants_enable(jid, enable, room, actorJid, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.enabled = enable;
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.actor = internal_room_jid_match_rewrite(actorJid);
|
||||
body_json.mediaType = mediaType;
|
||||
local body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not body_json_str then
|
||||
module:log('error', 'error encoding json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
if jid then
|
||||
send_json_message(jid, body_json_str)
|
||||
else
|
||||
for _, occupant in room:each_occupant() do
|
||||
send_json_message(occupant.jid, body_json_str)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Notifies about a change to the whitelist. Notifies all moderators and admin and the jid itself
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param moderators whether to notify all moderators in the room
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType used only when a participant is approved (not sent to moderators)
|
||||
-- @param removed whether the jid is removed or added
|
||||
function notify_whitelist_change(jid, moderators, room, mediaType, removed)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
-- we will be modifying it, so we need a copy
|
||||
body_json.whitelists = table_shallow_copy(room.av_moderation);
|
||||
if removed then
|
||||
body_json.removed = true;
|
||||
end
|
||||
body_json.mediaType = mediaType;
|
||||
|
||||
-- sanitize, make sure we don't have an empty array as it will encode it as {} not as []
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if body_json.whitelists[mediaType] and #body_json.whitelists[mediaType] == 0 then
|
||||
body_json.whitelists[mediaType] = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local moderators_body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not moderators_body_json_str then
|
||||
module:log('error', 'error encoding moderator json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
body_json.whitelists = nil;
|
||||
if not removed then
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
end
|
||||
local participant_body_json_str, error = json.encode(body_json);
|
||||
|
||||
if not participant_body_json_str then
|
||||
module:log('error', 'error encoding participant json room:%s error:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
if moderators and occupant.role == 'moderator' then
|
||||
send_json_message(occupant.jid, moderators_body_json_str);
|
||||
elseif occupant.jid == jid then
|
||||
-- if the occupant is not moderator we send him that it is approved
|
||||
-- if it is moderator we update him with the list, this is moderator joining or grant moderation was executed
|
||||
if occupant.role == 'moderator' then
|
||||
send_json_message(occupant.jid, moderators_body_json_str);
|
||||
else
|
||||
send_json_message(occupant.jid, participant_body_json_str);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Notifies jid that is approved. This is a moderator to jid message to ask to unmute,
|
||||
-- @param jid the jid to notify about the change
|
||||
-- @param from the jid that triggered this
|
||||
-- @param room the room where to send it
|
||||
-- @param mediaType the mediaType it was approved for
|
||||
function notify_jid_approved(jid, from, room, mediaType)
|
||||
local body_json = {};
|
||||
body_json.type = 'av_moderation';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.approved = true; -- we want to send to participants only that they were approved to unmute
|
||||
body_json.mediaType = mediaType;
|
||||
body_json.from = from;
|
||||
|
||||
local json_message, error = json.encode(body_json);
|
||||
if not json_message then
|
||||
module:log('error', 'skip sending json message to:%s error:%s', jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
send_json_message(jid, json_message);
|
||||
end
|
||||
|
||||
function start_av_moderation(room, mediaType, occupant)
|
||||
if not room.av_moderation then
|
||||
room.av_moderation = {};
|
||||
room.av_moderation_actors = {};
|
||||
end
|
||||
room.av_moderation[mediaType] = array();
|
||||
|
||||
-- add all current moderators to the new whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.role == 'moderator' and not ends_with(room_occupant.nick, '/focus') then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
|
||||
-- We want to set startMuted policy in metadata, in case of new participants are joining to respect
|
||||
-- it, that will be enforced by jicofo
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
-- We want to keep the previous value of startMuted for this mediaType if av moderation is disabled
|
||||
-- to be able to restore
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
av_moderation_startMuted_restore[mediaType] = startMutedMetadata[mediaType];
|
||||
room.av_moderation_startMuted_restore = av_moderation_startMuted_restore;
|
||||
|
||||
startMutedMetadata[mediaType] = true;
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
room.av_moderation_actors[mediaType] = occupant.nick;
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending A/V moderation enable/disable commands or adding
|
||||
-- jids to the whitelist
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local moderation_command = event.stanza:get_child('av_moderation');
|
||||
|
||||
if moderation_command then
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local mediaType = moderation_command.attr.mediaType;
|
||||
if mediaType then
|
||||
if mediaType ~= 'audio' and mediaType ~= 'video' and mediaType ~= 'desktop' then
|
||||
module:log('warn', 'Wrong mediaType %s for %s', mediaType, room.jid);
|
||||
return false;
|
||||
end
|
||||
else
|
||||
module:log('warn', 'Missing mediaType for %s', room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if moderation_command.attr.enable ~= nil then
|
||||
local enabled;
|
||||
if moderation_command.attr.enable == 'true' then
|
||||
enabled = true;
|
||||
if room.av_moderation and room.av_moderation[mediaType] then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
end
|
||||
else
|
||||
enabled = false;
|
||||
if not room.av_moderation then
|
||||
module:log('warn', 'Concurrent moderator enable/disable request or something is out of sync');
|
||||
return true;
|
||||
else
|
||||
room.av_moderation[mediaType] = nil;
|
||||
room.av_moderation_actors[mediaType] = nil;
|
||||
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
local av_moderation_startMuted_restore = room.av_moderation_startMuted_restore or {};
|
||||
startMutedMetadata[mediaType] = av_moderation_startMuted_restore[mediaType];
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
local is_empty = true;
|
||||
for key,_ in pairs(room.av_moderation) do
|
||||
if room.av_moderation[key] then
|
||||
is_empty = false;
|
||||
end
|
||||
end
|
||||
if is_empty then
|
||||
room.av_moderation = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- send message to all occupants
|
||||
notify_occupants_enable(nil, enabled, room, occupant.nick, mediaType);
|
||||
|
||||
if enabled then
|
||||
-- inform all moderators for the newly created whitelist
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
|
||||
return true;
|
||||
elseif moderation_command.attr.jidToWhitelist then
|
||||
local occupant_jid = moderation_command.attr.jidToWhitelist;
|
||||
-- check if jid is in the room, if so add it to whitelist
|
||||
-- inform all moderators and admins and the jid
|
||||
local occupant_to_add = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
|
||||
if not occupant_to_add then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if not whitelist then
|
||||
whitelist = array{};
|
||||
room.av_moderation[mediaType] = whitelist;
|
||||
end
|
||||
whitelist:push(occupant_jid);
|
||||
|
||||
notify_whitelist_change(occupant_to_add.jid, true, room, mediaType, false);
|
||||
|
||||
return true;
|
||||
else
|
||||
-- this is a moderator asking the jid to unmute without enabling av moderation
|
||||
-- let's just send the event
|
||||
notify_jid_approved(occupant_to_add.jid, occupant.nick, room, mediaType);
|
||||
end
|
||||
elseif moderation_command.attr.jidToBlacklist then
|
||||
local occupant_jid = moderation_command.attr.jidToBlacklist;
|
||||
-- check if jid is in the room, if so remove it from the whitelist
|
||||
-- inform all moderators and admins
|
||||
local occupant_to_remove = room:get_occupant_by_nick(room_jid_match_rewrite(occupant_jid));
|
||||
if not occupant_to_remove then
|
||||
module:log('warn', 'No occupant %s found for %s', occupant_jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
local whitelist = room.av_moderation[mediaType];
|
||||
if whitelist then
|
||||
local index = get_index_in_table(whitelist, occupant_jid)
|
||||
if(index) then
|
||||
whitelist:pop(index);
|
||||
notify_whitelist_change(occupant_to_remove.jid, true, room, mediaType, true);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- return error
|
||||
return false
|
||||
end
|
||||
|
||||
-- handles new occupants to inform them about the state enabled/disabled, new moderators also get and the whitelist
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- when first moderator joins if av_can_unmute from password preset is set to false, we enable av moderation for both
|
||||
-- audio and video, and set the first moderator as the actor that enabled it
|
||||
if room._data.av_can_unmute ~= nil
|
||||
and not room._data.av_first_moderator_joined
|
||||
|
||||
-- occupant.role is not reflecting the actual role after set_affiliation is used in same occupant_joined event
|
||||
and room:get_role(occupant.nick) == 'moderator' then
|
||||
|
||||
if not room._data.av_can_unmute then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
start_av_moderation(room, mediaType, occupant);
|
||||
|
||||
notify_occupants_enable(nil, true, room, occupant.nick, mediaType);
|
||||
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
|
||||
room._data.av_first_moderator_joined = true;
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
if room.av_moderation then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_occupants_enable(
|
||||
occupant.jid, true, room, room.av_moderation_actors[mediaType], mediaType);
|
||||
end
|
||||
end
|
||||
|
||||
-- NOTE for some reason event.occupant.role is not reflecting the actual occupant role (when changed
|
||||
-- from allowners module) but iterating over room occupants returns the correct role
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- if it is a moderator, send the whitelist to every moderator
|
||||
if room_occupant.nick == occupant.nick and room_occupant.role == 'moderator' then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- when a occupant was granted moderator we need to update him with the whitelist
|
||||
function occupant_affiliation_changed(event)
|
||||
local room = event.room;
|
||||
if not room.av_moderation or is_healthcheck_room(room.jid) or is_admin(event.jid)
|
||||
or event.affiliation ~= 'owner' then
|
||||
return;
|
||||
end
|
||||
|
||||
-- in any enabled media type add the new moderator to the whitelist
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
if room_occupant.bare_jid == event.jid then
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
room.av_moderation[mediaType]:push(internal_room_jid_match_rewrite(room_occupant.nick));
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
if event.actor and event.affiliation == 'owner' then
|
||||
-- notify all moderators for the new grant moderator and the change in whitelists
|
||||
for _,mediaType in pairs({'audio', 'video', 'desktop'}) do
|
||||
if room.av_moderation[mediaType] then
|
||||
notify_whitelist_change(nil, true, room, mediaType);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log('info','Hook to muc events on %s', host);
|
||||
host_module:hook('muc-occupant-joined', occupant_joined, -2); -- make sure it runs after allowners or similar
|
||||
host_module:hook('muc-set-affiliation', occupant_affiliation_changed, -1);
|
||||
end);
|
||||
|
||||
process_host_module(main_virtual_host, function(host_module)
|
||||
module:context(host_module.host):fire_event('jitsi-add-identity', {
|
||||
name = 'av_moderation'; host = module.host;
|
||||
});
|
||||
end);
|
||||
23
resources/prosody-plugins/mod_certs_s2soutinjection.lua
Normal file
23
resources/prosody-plugins/mod_certs_s2soutinjection.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
-- global module
|
||||
-- validates certificates for all hosts used for s2soutinjection or s2sout_override
|
||||
module:set_global();
|
||||
|
||||
local s2s_overrides = module:get_option("s2s_connect_overrides");
|
||||
|
||||
if not s2s_overrides then
|
||||
s2s_overrides = module:get_option("s2sout_override");
|
||||
end
|
||||
|
||||
function attach(event)
|
||||
local session = event.session;
|
||||
|
||||
if s2s_overrides and s2s_overrides[event.host] then
|
||||
session.cert_chain_status = 'valid';
|
||||
session.cert_identity_status = 'valid';
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
module:wrap_event('s2s-check-certificate', function (handlers, event_name, event_data)
|
||||
return attach(event_data);
|
||||
end);
|
||||
202
resources/prosody-plugins/mod_client_proxy.lua
Normal file
202
resources/prosody-plugins/mod_client_proxy.lua
Normal file
@@ -0,0 +1,202 @@
|
||||
if module:get_host_type() ~= "component" then
|
||||
error("proxy_component should be loaded as component", 0);
|
||||
end
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local st = require "util.stanza";
|
||||
local array = require "util.array";
|
||||
|
||||
local target_address = module:get_option_string("target_address");
|
||||
|
||||
sessions = array{};
|
||||
local sessions = sessions;
|
||||
|
||||
local function handle_target_presence(stanza)
|
||||
local type = stanza.attr.type;
|
||||
module:log("debug", "received presence from destination: %s", type)
|
||||
local _, _, resource = jid_split(stanza.attr.from);
|
||||
if type == "error" then
|
||||
-- drop all known sessions
|
||||
for k in pairs(sessions) do
|
||||
sessions[k] = nil
|
||||
end
|
||||
module:log(
|
||||
"debug",
|
||||
"received error presence, dropping all target sessions",
|
||||
resource
|
||||
)
|
||||
elseif type == "unavailable" then
|
||||
for k in pairs(sessions) do
|
||||
if sessions[k] == resource then
|
||||
sessions[k] = nil
|
||||
module:log(
|
||||
"debug",
|
||||
"dropped target session: %s",
|
||||
resource
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
elseif not type then
|
||||
-- available
|
||||
local found = false;
|
||||
for k in pairs(sessions) do
|
||||
if sessions[k] == resource then
|
||||
found = true;
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
module:log(
|
||||
"debug",
|
||||
"registered new target session: %s",
|
||||
resource
|
||||
)
|
||||
sessions:push(resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_from_target(stanza)
|
||||
local type = stanza.attr.type
|
||||
module:log(
|
||||
"debug",
|
||||
"non-presence stanza from target: name = %s, type = %s",
|
||||
stanza.name,
|
||||
type
|
||||
)
|
||||
if stanza.name == "iq" then
|
||||
if type == "error" or type == "result" then
|
||||
-- de-NAT message
|
||||
local _, _, denatted_to_unprepped = jid_split(stanza.attr.to);
|
||||
local denatted_to = jid_prep(denatted_to_unprepped);
|
||||
if not denatted_to then
|
||||
module:log(
|
||||
"debug",
|
||||
"cannot de-NAT stanza, invalid to: %s",
|
||||
denatted_to_unprepped
|
||||
)
|
||||
return
|
||||
end
|
||||
local denatted_from = module:get_host();
|
||||
|
||||
module:log(
|
||||
"debug",
|
||||
"de-NAT-ed stanza: from: %s -> %s, to: %s -> %s",
|
||||
stanza.attr.from,
|
||||
denatted_from,
|
||||
stanza.attr.to,
|
||||
denatted_to
|
||||
)
|
||||
|
||||
stanza.attr.from = denatted_from
|
||||
stanza.attr.to = denatted_to
|
||||
|
||||
module:send(stanza)
|
||||
else
|
||||
-- FIXME: we don’t support NATing outbund requests atm.
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
elseif stanza.name == "message" then
|
||||
-- not implemented yet, we need a way to ensure that routing doesn’t
|
||||
-- break
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_to_target(stanza)
|
||||
local type = stanza.attr.type;
|
||||
module:log(
|
||||
"debug",
|
||||
"stanza to target: name = %s, type = %s",
|
||||
stanza.name, type
|
||||
)
|
||||
if stanza.name == "presence" then
|
||||
if type ~= "error" then
|
||||
module:send(st.error_reply(stanza, "cancel", "bad-request"))
|
||||
return
|
||||
end
|
||||
elseif stanza.name == "iq" then
|
||||
if type == "get" or type == "set" then
|
||||
if #sessions == 0 then
|
||||
-- no sessions available to send to
|
||||
module:log("debug", "no sessions to send to!")
|
||||
module:send(st.error_reply(stanza, "cancel", "service-unavailable"))
|
||||
return
|
||||
end
|
||||
|
||||
-- find a target session
|
||||
local target_session = sessions:random()
|
||||
local target = target_address .. "/" .. target_session
|
||||
|
||||
-- encode sender JID in resource
|
||||
local natted_from = module:get_host() .. "/" .. stanza.attr.from;
|
||||
|
||||
module:log(
|
||||
"debug",
|
||||
"NAT-ed stanza: from: %s -> %s, to: %s -> %s",
|
||||
stanza.attr.from,
|
||||
natted_from,
|
||||
stanza.attr.to,
|
||||
target
|
||||
)
|
||||
|
||||
stanza.attr.from = natted_from
|
||||
stanza.attr.to = target
|
||||
|
||||
module:send(stanza)
|
||||
end
|
||||
-- FIXME: handle and forward result/error correctly
|
||||
elseif stanza.name == "message" then
|
||||
-- not implemented yet, we need a way to ensure that routing doesn’t
|
||||
-- break
|
||||
module:send(st.error_reply(stanza, "cancel", "feature-not-implemented"))
|
||||
end
|
||||
end
|
||||
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza
|
||||
module:log("debug", "received stanza from %s session", origin.type)
|
||||
|
||||
local bare_from = jid_bare(stanza.attr.from);
|
||||
local _, _, to = jid_split(stanza.attr.to);
|
||||
if bare_from == target_address then
|
||||
-- from our target, to whom?
|
||||
if not to then
|
||||
-- directly to component
|
||||
if stanza.name == "presence" then
|
||||
handle_target_presence(stanza)
|
||||
else
|
||||
module:send(st.error_reply(stanza, "cancel", "bad-request"))
|
||||
return true
|
||||
end
|
||||
else
|
||||
-- to someone else
|
||||
handle_from_target(stanza)
|
||||
end
|
||||
else
|
||||
handle_to_target(stanza)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
module:hook("iq/bare", stanza_handler, -1);
|
||||
module:hook("message/bare", stanza_handler, -1);
|
||||
module:hook("presence/bare", stanza_handler, -1);
|
||||
module:hook("iq/full", stanza_handler, -1);
|
||||
module:hook("message/full", stanza_handler, -1);
|
||||
module:hook("presence/full", stanza_handler, -1);
|
||||
module:hook("iq/host", stanza_handler, -1);
|
||||
module:hook("message/host", stanza_handler, -1);
|
||||
module:hook("presence/host", stanza_handler, -1);
|
||||
|
||||
module:log("debug", "loaded proxy on %s", module:get_host())
|
||||
|
||||
subscription_request = st.presence({
|
||||
type = "subscribe",
|
||||
to = target_address,
|
||||
from = module:get_host()}
|
||||
)
|
||||
module:send(subscription_request)
|
||||
48
resources/prosody-plugins/mod_conference_duration.lua
Normal file
48
resources/prosody-plugins/mod_conference_duration.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
local it = require "util.iterators";
|
||||
local process_host_module = module:require "util".process_host_module;
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
-- Returns the meeting created timestamp form data.
|
||||
function getMeetingCreatedTSConfig(room)
|
||||
return {
|
||||
name = "muc#roominfo_created_timestamp";
|
||||
type = "text-single";
|
||||
label = "The meeting created_timestamp.";
|
||||
value = room.created_timestamp or "";
|
||||
};
|
||||
end
|
||||
|
||||
function occupant_joined(event)
|
||||
local room = event.room;
|
||||
local occupant = event.occupant;
|
||||
|
||||
local participant_count = it.count(room:each_occupant());
|
||||
|
||||
if participant_count > 1 then
|
||||
if room.created_timestamp == nil then
|
||||
room.created_timestamp = string.format('%i', os.time() * 1000); -- Lua provides UTC time in seconds, so convert to milliseconds
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- add meeting Id to the disco info requests to the room
|
||||
host_module:hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, getMeetingCreatedTSConfig(event.room));
|
||||
end);
|
||||
|
||||
-- Marks the created timestamp in the room object
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
end);
|
||||
|
||||
-- DEPRECATED and will be removed, giving time for mobile clients to update
|
||||
local conference_duration_component
|
||||
= module:get_option_string("conference_duration_component", "conferenceduration."..module.host);
|
||||
if conference_duration_component then
|
||||
module:add_identity("component", "conference_duration", conference_duration_component);
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
-- DEPRECATED and will be removed, giving time for mobile clients to update
|
||||
local st = require "util.stanza";
|
||||
local socket = require "socket";
|
||||
local json = require 'cjson.safe';
|
||||
local it = require "util.iterators";
|
||||
local process_host_module = module:require "util".process_host_module;
|
||||
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("warn", "conference duration will not work with Prosody version 0.10 or less.");
|
||||
return;
|
||||
end
|
||||
|
||||
local muc_component_host = module:get_option_string("muc_component");
|
||||
if muc_component_host == nil then
|
||||
module:log("error", "No muc_component specified. No muc to operate on!");
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("info", "Starting conference duration timer for %s", muc_component_host);
|
||||
|
||||
function occupant_joined(event)
|
||||
local room = event.room;
|
||||
local occupant = event.occupant;
|
||||
|
||||
local participant_count = it.count(room:each_occupant());
|
||||
|
||||
if participant_count > 1 then
|
||||
local body_json = {};
|
||||
body_json.type = 'conference_duration';
|
||||
body_json.created_timestamp = room.created_timestamp;
|
||||
|
||||
local stanza = st.message({
|
||||
from = module.host;
|
||||
to = occupant.jid;
|
||||
})
|
||||
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
|
||||
:text(json.encode(body_json)):up();
|
||||
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
end);
|
||||
54
resources/prosody-plugins/mod_debug_traceback.lua
Normal file
54
resources/prosody-plugins/mod_debug_traceback.lua
Normal file
@@ -0,0 +1,54 @@
|
||||
module:set_global();
|
||||
|
||||
local traceback = require "util.debug".traceback;
|
||||
local pposix = require "util.pposix";
|
||||
local os_date = os.date;
|
||||
local render_filename = require "util.interpolation".new("%b{}", function (s) return s; end, {
|
||||
yyyymmdd = function (t)
|
||||
return os_date("%Y%m%d", t);
|
||||
end;
|
||||
hhmmss = function (t)
|
||||
return os_date("%H%M%S", t);
|
||||
end;
|
||||
});
|
||||
|
||||
local count = 0;
|
||||
|
||||
local function get_filename(filename_template)
|
||||
filename_template = filename_template;
|
||||
return render_filename(filename_template, {
|
||||
paths = prosody.paths;
|
||||
pid = pposix.getpid();
|
||||
count = count;
|
||||
time = os.time();
|
||||
});
|
||||
end
|
||||
|
||||
local default_filename_template = "{paths.data}/traceback-{pid}-{count}.log";
|
||||
local filename_template = module:get_option_string("debug_traceback_filename", default_filename_template);
|
||||
local signal_name = module:get_option_string("debug_traceback_signal", "SIGUSR1");
|
||||
|
||||
function dump_traceback()
|
||||
module:log("info", "Received %s, writing traceback", signal_name);
|
||||
|
||||
local tb = traceback();
|
||||
module:fire_event("debug_traceback/triggered", { traceback = tb });
|
||||
|
||||
local f, err = io.open(get_filename(filename_template), "a+");
|
||||
if not f then
|
||||
module:log("error", "Unable to write traceback: %s", err);
|
||||
return;
|
||||
end
|
||||
f:write("-- Traceback generated at ", os.date("%b %d %H:%M:%S"), " --\n");
|
||||
f:write(tb, "\n");
|
||||
f:write("-- End of traceback --\n");
|
||||
f:close();
|
||||
count = count + 1;
|
||||
end
|
||||
|
||||
local mod_posix = module:depends("posix");
|
||||
if rawget(mod_posix, "features") and mod_posix.features.signal_events then
|
||||
module:hook("signal/"..signal_name, dump_traceback);
|
||||
else
|
||||
require"util.signal".signal(signal_name, dump_traceback);
|
||||
end
|
||||
95
resources/prosody-plugins/mod_end_conference.lua
Normal file
95
resources/prosody-plugins/mod_end_conference.lua
Normal file
@@ -0,0 +1,95 @@
|
||||
--
|
||||
-- Component "endconference.jitmeet.example.com" "end_conference"
|
||||
-- muc_component = muc.jitmeet.example.com
|
||||
--
|
||||
local util = module:require 'util';
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local END_CONFERENCE_REASON = 'The meeting has been terminated';
|
||||
|
||||
-- Since this file serves as both the host module and the component, we rely on the assumption that
|
||||
-- end_conference_component var would only be define for the host and not in the end_conference component
|
||||
-- TODO: Remove this if block after several stable releases when people update their configs
|
||||
local end_conference_component = module:get_option_string('end_conference_component');
|
||||
if end_conference_component then
|
||||
module:log('warn', 'Please update your config by removing muc_end_conference module from '
|
||||
.. 'the list of loaded modules in the main virtual host.');
|
||||
module:depends("features_identity");
|
||||
return; -- nothing left to do if called as host module
|
||||
end
|
||||
|
||||
-- What follows is logic for the end_conference component
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
local main_virtual_host = module:get_option_string('muc_mapper_domain_base');
|
||||
if not main_virtual_host then
|
||||
module:log('warn', 'No "muc_mapper_domain_base" option set, disabling end conference component.');
|
||||
return ;
|
||||
end
|
||||
|
||||
module:log('info', 'Starting end_conference for %s', muc_component_host);
|
||||
|
||||
-- receives messages from clients to the component to end a conference
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local moderation_command = event.stanza:get_child('end_conference');
|
||||
|
||||
if moderation_command then
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- destroy the room
|
||||
room:destroy(nil, END_CONFERENCE_REASON);
|
||||
module:log('info', 'Room %s destroyed by occupant %s', room.jid, from);
|
||||
return true;
|
||||
end
|
||||
|
||||
-- return error
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
||||
|
||||
process_host_module(main_virtual_host, function(host_module)
|
||||
module:context(host_module.host):fire_event('jitsi-add-identity', {
|
||||
name = 'end_conference'; host = module.host;
|
||||
});
|
||||
end);
|
||||
8
resources/prosody-plugins/mod_features_identity.lua
Normal file
8
resources/prosody-plugins/mod_features_identity.lua
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Other components can use the event 'jitsi-add-identity' to attach identity which
|
||||
-- will be advertised by the main virtual host and discovered by clients.
|
||||
-- With this we avoid having an almost empty module to just add identity with an extra config
|
||||
|
||||
module:hook('jitsi-add-identity', function(event)
|
||||
module:log('info', 'Adding identity %s for host %s', event.name, event.host);
|
||||
module:add_identity('component', event.name, event.host);
|
||||
end);
|
||||
245
resources/prosody-plugins/mod_filesharing_component.lua
Normal file
245
resources/prosody-plugins/mod_filesharing_component.lua
Normal file
@@ -0,0 +1,245 @@
|
||||
local json = require 'cjson.safe';
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local is_admin = util.is_admin;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local FILE_SHARING_IDENTITY_TYPE = 'file-sharing';
|
||||
local JSON_TYPE_ADD_FILE = 'add';
|
||||
local JSON_TYPE_REMOVE_FILE = 'remove';
|
||||
local JSON_TYPE_LIST_FILES = 'list';
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
|
||||
-- this is the main virtual host of the main prosody that this vnode serves
|
||||
local main_domain = module:get_option_string('main_domain');
|
||||
-- only the visitor prosody has main_domain setting
|
||||
local is_visitor_prosody = main_domain ~= nil;
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling file sharing component.");
|
||||
return ;
|
||||
end
|
||||
|
||||
-- receives messages from clients to the component sending file sharing commands for adding or removing files
|
||||
function on_message(event)
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local message = stanza:get_child(FILE_SHARING_IDENTITY_TYPE, 'http://jitsi.org/jitmeet');
|
||||
|
||||
if not message then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found for %s/%s', session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant sending the message is an occupant in the room
|
||||
local from = stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if not is_feature_allowed(
|
||||
'file-upload',
|
||||
session.jitsi_meet_context_features,
|
||||
room:get_affiliation(stanza.attr.from) == 'owner') then
|
||||
session.send(st.error_reply(stanza, 'auth', 'forbidden'));
|
||||
return true;
|
||||
end
|
||||
|
||||
if message.attr.type == JSON_TYPE_ADD_FILE then
|
||||
local msg_obj, error = json.decode(message:get_text());
|
||||
|
||||
if error then
|
||||
module:log('error','Error decoding data error:%s %s', error, stanza);
|
||||
return false;
|
||||
end
|
||||
|
||||
if not msg_obj.fileId then
|
||||
module:log('error', 'Error missing required field: %s', stanza);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- make sure we overwrite data for sender so we avoid spoofing
|
||||
msg_obj.authorParticipantId = jid.resource(occupant.nick);
|
||||
msg_obj.authorParticipantJid = from;
|
||||
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
msg_obj.authorParticipantName = nick_element:get_text();
|
||||
else
|
||||
msg_obj.authorParticipantName = 'anonymous';
|
||||
end
|
||||
msg_obj.conferenceFullName = internal_room_jid_match_rewrite(room.jid);
|
||||
|
||||
module:context(muc_domain_base):fire_event('jitsi-filesharing-add', {
|
||||
room = room; file = msg_obj; actor = occupant.nick;
|
||||
});
|
||||
|
||||
module:context(muc_domain_base):fire_event('jitsi-filesharing-updated', {
|
||||
room = room;
|
||||
});
|
||||
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_REMOVE_FILE then
|
||||
if not message.attr.fileId then
|
||||
module:log('error', 'Error missing required field: %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
module:context(muc_domain_base):fire_event('jitsi-filesharing-remove', {
|
||||
room = room; id = message.attr.fileId; actor = occupant.nick;
|
||||
});
|
||||
|
||||
module:context(muc_domain_base):fire_event('jitsi-filesharing-updated', {
|
||||
room = room;
|
||||
});
|
||||
|
||||
return true;
|
||||
else
|
||||
-- return error.
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
-- handles new occupants to inform them about any file shared by other participants
|
||||
function occupant_joined(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
-- healthcheck rooms does not have shared files
|
||||
if not room.jitsi_shared_files
|
||||
or is_admin(occupant.bare_jid)
|
||||
or not room.jitsi_shared_files
|
||||
or next(room.jitsi_shared_files) == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- send file list to the new occupant
|
||||
local json_msg, error = json.encode({
|
||||
type = FILE_SHARING_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_LIST_FILES,
|
||||
files = room.jitsi_shared_files
|
||||
});
|
||||
|
||||
local stanza = st.message({ from = module.host; to = occupant.jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
|
||||
:text(json_msg):up();
|
||||
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log('info','Hook to muc events on %s', host);
|
||||
host_module:hook('muc-occupant-joined', occupant_joined, -10); -- make sure it runs after allowners or similar
|
||||
end);
|
||||
|
||||
-- we will receive messages from the clients
|
||||
module:hook('message/host', on_message);
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
module:context(muc_domain_base):fire_event('jitsi-add-identity', {
|
||||
name = FILE_SHARING_IDENTITY_TYPE; host = module.host;
|
||||
});
|
||||
module:context(muc_domain_base):hook('jitsi-filesharing-add', function(event)
|
||||
local actor, file, room = event.actor, event.file, event.room;
|
||||
|
||||
if not room.jitsi_shared_files then
|
||||
room.jitsi_shared_files = {};
|
||||
end
|
||||
|
||||
room.jitsi_shared_files[file.fileId] = file;
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = FILE_SHARING_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_ADD_FILE,
|
||||
file = file
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'skip sending add request room:%s error:%s', room.jid, error);
|
||||
return false
|
||||
end
|
||||
|
||||
local stanza = st.message({ from = module.host; }):tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
|
||||
:text(json_msg):up();
|
||||
|
||||
-- send add file to all occupants except jicofo and sender
|
||||
-- if this is visitor prosody send it only to visitors
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
local send_event = not is_admin(room_occupant.bare_jid) and room_occupant.nick ~= actor;
|
||||
if is_visitor_prosody then
|
||||
send_event = room_occupant.role == 'visitor';
|
||||
end
|
||||
if send_event then
|
||||
local to_send = st.clone(stanza);
|
||||
to_send.attr.to = room_occupant.jid;
|
||||
module:send(to_send);
|
||||
end
|
||||
end
|
||||
end);
|
||||
module:context(muc_domain_base):hook('jitsi-filesharing-remove', function(event)
|
||||
local actor, id, room = event.actor, event.id, event.room;
|
||||
|
||||
if not room.jitsi_shared_files then
|
||||
return;
|
||||
end
|
||||
|
||||
room.jitsi_shared_files[id] = nil;
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = FILE_SHARING_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_REMOVE_FILE,
|
||||
fileId = id
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'skip sending remove request room:%s error:%s', room.jid, error);
|
||||
return false
|
||||
end
|
||||
|
||||
local stanza = st.message({ from = module.host; }):tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
|
||||
:text(json_msg):up();
|
||||
|
||||
-- send remove file to all occupants except jicofo and sender
|
||||
-- if this is visitor prosody send it only to visitors
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
local send_event = not is_admin(room_occupant.bare_jid) and room_occupant.nick ~= actor;
|
||||
if is_visitor_prosody then
|
||||
send_event = room_occupant.role == 'visitor';
|
||||
end
|
||||
if send_event then
|
||||
local to_send = st.clone(stanza);
|
||||
to_send.attr.to = room_occupant.jid;
|
||||
module:send(to_send);
|
||||
end
|
||||
end
|
||||
end);
|
||||
end);
|
||||
68
resources/prosody-plugins/mod_filter_iq_jibri.lua
Normal file
68
resources/prosody-plugins/mod_filter_iq_jibri.lua
Normal file
@@ -0,0 +1,68 @@
|
||||
-- This module is enabled under the main virtual host
|
||||
local cache = require 'util.cache';
|
||||
local new_throttle = require 'util.throttle'.create;
|
||||
local st = require "util.stanza";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local util = module:require 'util';
|
||||
local is_feature_allowed = util.is_feature_allowed;
|
||||
local get_ip = util.get_ip;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
|
||||
local limit_jibri_reach_ip_attempts;
|
||||
local limit_jibri_reach_room_attempts;
|
||||
local rates_per_ip;
|
||||
local function load_config()
|
||||
limit_jibri_reach_ip_attempts = module:get_option_number("max_number_ip_attempts_per_minute", 9);
|
||||
limit_jibri_reach_room_attempts = module:get_option_number("max_number_room_attempts_per_minute", 3);
|
||||
-- The size of the cache that saves state for IP addresses
|
||||
cache_size = module:get_option_number("jibri_rate_limit_cache_size", 10000);
|
||||
|
||||
-- Maps an IP address to a util.throttle which keeps the rate of attempts to reach jibri events from that IP.
|
||||
rates_per_ip = cache.new(cache_size);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- filters jibri iq in case of requested from jwt authenticated session that
|
||||
-- has features in the user context, but without feature for recording
|
||||
module:hook("pre-iq/full", function(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.name == "iq" then
|
||||
local jibri = stanza:get_child('jibri', 'http://jitsi.org/protocol/jibri');
|
||||
if jibri then
|
||||
local session = event.origin;
|
||||
local token = session.auth_token;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(jid_bare(stanza.attr.to)));
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.from);
|
||||
local feature = jibri.attr.recording_mode == 'file' and 'recording' or 'livestreaming';
|
||||
local is_allowed = is_feature_allowed(
|
||||
feature,
|
||||
session.jitsi_meet_context_features,
|
||||
occupant.role == 'moderator');
|
||||
|
||||
if jibri.attr.action == 'start' or jibri.attr.action == 'stop' then
|
||||
if not is_allowed then
|
||||
module:log('info', 'Filtering jibri start recording, stanza:%s', tostring(stanza));
|
||||
session.send(st.error_reply(stanza, 'auth', 'forbidden'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local ip = get_ip(session);
|
||||
if not rates_per_ip:get(ip) then
|
||||
rates_per_ip:set(ip, new_throttle(limit_jibri_reach_ip_attempts, 60));
|
||||
end
|
||||
|
||||
if not room.jibri_throttle then
|
||||
room.jibri_throttle = new_throttle(limit_jibri_reach_room_attempts, 60);
|
||||
end
|
||||
|
||||
if not rates_per_ip:get(ip):poll(1) or not room.jibri_throttle:poll(1) then
|
||||
module:log('warn', 'Filtering jibri start recording, ip:%s, room:%s stanza:%s',
|
||||
ip, room.jid, tostring(stanza));
|
||||
session.send(st.error_reply(stanza, 'wait', 'policy-violation'));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
278
resources/prosody-plugins/mod_filter_iq_rayo.lua
Normal file
278
resources/prosody-plugins/mod_filter_iq_rayo.lua
Normal file
@@ -0,0 +1,278 @@
|
||||
-- This module is enabled under the main virtual host
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local st = require "util.stanza";
|
||||
local jid = require "util.jid";
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local is_feature_allowed = util.is_feature_allowed;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local process_host_module = util.process_host_module;
|
||||
local jid_bare = require "util.jid".bare;
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
local measure_drop = module:measure('drop', 'counter');
|
||||
|
||||
local main_muc_component_host = module:get_option_string('main_muc');
|
||||
if main_muc_component_host == nil then
|
||||
module:log('error', 'main_muc not configured. Cannot proceed.');
|
||||
return;
|
||||
end
|
||||
local main_muc_service;
|
||||
|
||||
|
||||
-- this is the main virtual host of the main prosody that this vnode serves
|
||||
local main_domain = module:get_option_string('main_domain');
|
||||
-- only the visitor prosody has main_domain setting
|
||||
local is_visitor_prosody = main_domain ~= nil;
|
||||
|
||||
-- this is the main virtual host of this vnode
|
||||
local local_domain = module:get_option_string('muc_mapper_domain_base');
|
||||
|
||||
local parentCtx = module:context(local_domain);
|
||||
if parentCtx == nil then
|
||||
log("error",
|
||||
"Failed to start - unable to get parent context for host: %s",
|
||||
tostring(local_domain));
|
||||
return;
|
||||
end
|
||||
local token_util = module:require "token/util".new(parentCtx);
|
||||
|
||||
-- no token configuration but required
|
||||
if token_util == nil then
|
||||
module:log("error", "no token configuration but it is required");
|
||||
return;
|
||||
end
|
||||
|
||||
-- The maximum number of simultaneous calls,
|
||||
-- and also the maximum number of new calls per minute that a session is allowed to create.
|
||||
local limit_outgoing_calls;
|
||||
local function load_config()
|
||||
limit_outgoing_calls = module:get_option_number("max_number_outgoing_calls", -1);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- Header names to use to push extra data extracted from token, if any
|
||||
local OUT_INITIATOR_USER_ATTR_NAME = "X-outbound-call-initiator-user";
|
||||
local OUT_INITIATOR_GROUP_ATTR_NAME = "X-outbound-call-initiator-group";
|
||||
local OUT_ROOM_NAME_ATTR_NAME = "JvbRoomName";
|
||||
|
||||
local OUTGOING_CALLS_THROTTLE_INTERVAL = 60; -- if max_number_outgoing_calls is enabled it will be
|
||||
-- the max number of outgoing calls a user can try for a minute
|
||||
|
||||
-- filters rayo iq in case of requested from not jwt authenticated sessions
|
||||
-- or if the session has features in user context and it doesn't mention
|
||||
-- feature "outbound-call" to be enabled
|
||||
module:hook("pre-iq/full", function(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.name == "iq" then
|
||||
local dial = stanza:get_child('dial', 'urn:xmpp:rayo:1');
|
||||
if dial then
|
||||
local session = event.origin;
|
||||
local token = session.auth_token;
|
||||
|
||||
-- find header with attr name 'JvbRoomName' and extract its value
|
||||
local roomName;
|
||||
-- Remove any 'header' element if it already exists, so it cannot be spoofed by a client
|
||||
dial:maptags(function(tag)
|
||||
if tag.name == "header"
|
||||
and (tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME
|
||||
or tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME) then
|
||||
return nil
|
||||
elseif tag.name == "header" and tag.attr.name == OUT_ROOM_NAME_ATTR_NAME then
|
||||
roomName = tag.attr.value;
|
||||
-- we will remove it as we will add it later, modified
|
||||
if is_visitor_prosody then
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end);
|
||||
|
||||
local room_jid = jid.bare(stanza.attr.to);
|
||||
local room_real_jid = room_jid_match_rewrite(room_jid);
|
||||
local room = main_muc_service.get_room_from_jid(room_real_jid);
|
||||
local is_sender_in_room = room:get_occupant_jid(stanza.attr.from) ~= nil;
|
||||
|
||||
if not room or not is_sender_in_room then
|
||||
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
local feature = dial.attr.to == 'jitsi_meet_transcribe' and 'transcription' or 'outbound-call';
|
||||
local is_session_allowed = is_feature_allowed(
|
||||
feature,
|
||||
session.jitsi_meet_context_features,
|
||||
room:get_affiliation(stanza.attr.from) == 'owner');
|
||||
|
||||
if roomName == nil
|
||||
or roomName ~= room_jid
|
||||
or (token ~= nil and not token_util:verify_room(session, room_real_jid))
|
||||
or not is_session_allowed
|
||||
then
|
||||
module:log("warn", "Filtering stanza dial, stanza:%s", tostring(stanza));
|
||||
session.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- we get current user_id or group, or the one from the granted one
|
||||
-- so guests and the user that granted rights are sharing same limit, as guest can be without token
|
||||
local user_id, group_id = nil, session.jitsi_meet_context_group;
|
||||
if session.jitsi_meet_context_user then
|
||||
user_id = session.jitsi_meet_context_user["id"];
|
||||
else
|
||||
user_id = session.granted_jitsi_meet_context_user_id;
|
||||
group_id = session.granted_jitsi_meet_context_group_id;
|
||||
end
|
||||
|
||||
-- now lets check any limits for outgoing calls if configured
|
||||
if feature == 'outbound-call' and limit_outgoing_calls > 0 then
|
||||
if not session.dial_out_throttle then
|
||||
-- module:log("debug", "Enabling dial-out throttle session=%s.", session);
|
||||
session.dial_out_throttle = new_throttle(limit_outgoing_calls, OUTGOING_CALLS_THROTTLE_INTERVAL);
|
||||
end
|
||||
|
||||
if not session.dial_out_throttle:poll(1) -- we first check the throttle so we can mark one incoming dial for the balance
|
||||
or get_concurrent_outgoing_count(user_id, group_id) >= limit_outgoing_calls
|
||||
then
|
||||
module:log("warn",
|
||||
"Filtering stanza dial, stanza:%s, outgoing calls limit reached", tostring(stanza));
|
||||
measure_drop(1);
|
||||
session.send(st.error_reply(stanza, "cancel", "resource-constraint"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- now lets insert token information if any
|
||||
if session and user_id then
|
||||
-- adds initiator user id from token
|
||||
dial:tag("header", {
|
||||
xmlns = "urn:xmpp:rayo:1",
|
||||
name = OUT_INITIATOR_USER_ATTR_NAME,
|
||||
value = tostring(user_id)});
|
||||
dial:up();
|
||||
|
||||
-- Add the initiator group information if it is present
|
||||
if session.jitsi_meet_context_group then
|
||||
dial:tag("header", {
|
||||
xmlns = "urn:xmpp:rayo:1",
|
||||
name = OUT_INITIATOR_GROUP_ATTR_NAME,
|
||||
value = tostring(session.jitsi_meet_context_group) });
|
||||
dial:up();
|
||||
end
|
||||
end
|
||||
|
||||
-- we want to instruct jigasi to enter the main room, so send the correct main room jid
|
||||
if is_visitor_prosody then
|
||||
dial:tag("header", {
|
||||
xmlns = "urn:xmpp:rayo:1",
|
||||
name = OUT_ROOM_NAME_ATTR_NAME,
|
||||
value = string.gsub(roomName, local_domain, main_domain) });
|
||||
dial:up();
|
||||
end
|
||||
end
|
||||
end
|
||||
end, 1); -- make sure we run before domain mapper
|
||||
|
||||
--- Finds and returns the number of concurrent outgoing calls for a user
|
||||
-- @param context_user the user id extracted from the token
|
||||
-- @param context_group the group id extracted from the token
|
||||
-- @return returns the count of concurrent calls
|
||||
function get_concurrent_outgoing_count(context_user, context_group)
|
||||
local count = 0;
|
||||
local rooms = main_muc_service.live_rooms();
|
||||
|
||||
-- now lets iterate over rooms and occupants and search for
|
||||
-- call initiated by the user
|
||||
for room in rooms do
|
||||
for _, occupant in room:each_occupant() do
|
||||
for _, presence in occupant:each_session() do
|
||||
|
||||
local initiator = is_sip_jigasi(presence);
|
||||
|
||||
local found_user = false;
|
||||
local found_group = false;
|
||||
|
||||
if initiator then
|
||||
initiator:maptags(function (tag)
|
||||
if tag.name == "header"
|
||||
and tag.attr.name == OUT_INITIATOR_USER_ATTR_NAME then
|
||||
found_user = tag.attr.value == context_user;
|
||||
elseif tag.name == "header"
|
||||
and tag.attr.name == OUT_INITIATOR_GROUP_ATTR_NAME then
|
||||
found_group = tag.attr.value == context_group;
|
||||
end
|
||||
|
||||
return tag;
|
||||
end );
|
||||
-- if found a jigasi participant initiated by the concurrent
|
||||
-- participant, count it
|
||||
if found_user
|
||||
and (context_group == nil or found_group) then
|
||||
count = count + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return count;
|
||||
end
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
|
||||
main_muc_service = main_muc;
|
||||
end
|
||||
|
||||
process_host_module(main_muc_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- when recording participants may enable and backend transcriptions
|
||||
-- it is possible that participant is not moderator, but has the features enabled for
|
||||
-- transcribing, we need to allow that operation
|
||||
module:hook('jitsi-metadata-allow-moderation', function (event)
|
||||
local data, key, occupant, session = event.data, event.key, event.actor, event.session;
|
||||
|
||||
if key == 'recording' and data and data.isTranscribingEnabled ~= nil then
|
||||
-- if it is recording we want to allow setting in metadata if not moderator but features
|
||||
-- are present
|
||||
if session.jitsi_meet_context_features
|
||||
and occupant.role ~= 'moderator'
|
||||
and is_feature_allowed('transcription', session.jitsi_meet_context_features)
|
||||
and is_feature_allowed('recording', session.jitsi_meet_context_features) then
|
||||
local res = {};
|
||||
res.isTranscribingEnabled = data.isTranscribingEnabled;
|
||||
return res;
|
||||
elseif not session.jitsi_meet_context_features and occupant.role == 'moderator' then
|
||||
return data;
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
if occupant.role == 'moderator' then
|
||||
return data;
|
||||
end
|
||||
|
||||
return nil;
|
||||
end);
|
||||
|
||||
42
resources/prosody-plugins/mod_filter_messages.lua
Normal file
42
resources/prosody-plugins/mod_filter_messages.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
-- enable under the main muc module
|
||||
-- a module that will filter group messages based on features (jitsi_meet_context_features)
|
||||
-- when requested via metadata (permissions.groupChatRestricted)
|
||||
local util = module:require 'util';
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
local session = event.origin;
|
||||
|
||||
if not body or not session then
|
||||
-- we ignore messages without body - lobby, polls ...
|
||||
return;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
-- this should already been through domain mapper and this should be the real room jid [tenant]name format
|
||||
local room = get_room_from_jid(stanza.attr.to);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s', stanza.attr.to);
|
||||
return;
|
||||
end
|
||||
|
||||
if room.jitsiMetadata and room.jitsiMetadata.permissions
|
||||
and room.jitsiMetadata.permissions.groupChatRestricted
|
||||
and not is_feature_allowed('send-groupchat', session.jitsi_meet_context_features) then
|
||||
|
||||
local reply = st.error_reply(stanza, 'cancel', 'not-allowed', 'Sending group messages not allowed');
|
||||
if session.type == 's2sin' or session.type == 's2sout' then
|
||||
reply.skipMapping = true;
|
||||
end
|
||||
module:send(reply);
|
||||
|
||||
-- let's filter this message
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
module:hook('jitsi-visitor-groupchat-pre-route', on_message); -- visitors messages
|
||||
280
resources/prosody-plugins/mod_firewall/actions.lib.lua
Normal file
280
resources/prosody-plugins/mod_firewall/actions.lib.lua
Normal file
@@ -0,0 +1,280 @@
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
local interpolation = require "util.interpolation";
|
||||
local template = interpolation.new("%b$$", function (s) return ("%q"):format(s) end);
|
||||
|
||||
--luacheck: globals meta idsafe
|
||||
local action_handlers = {};
|
||||
|
||||
|
||||
-- Takes an XML string and returns a code string that builds that stanza
|
||||
-- using st.stanza()
|
||||
local function compile_xml(data)
|
||||
local code = {};
|
||||
local first, short_close = true, nil;
|
||||
for tagline, text in data:gmatch("<([^>]+)>([^<]*)") do
|
||||
if tagline:sub(-1,-1) == "/" then
|
||||
tagline = tagline:sub(1, -2);
|
||||
short_close = true;
|
||||
end
|
||||
if tagline:sub(1,1) == "/" then
|
||||
code[#code+1] = (":up()");
|
||||
else
|
||||
local name, attr = tagline:match("^(%S*)%s*(.*)$");
|
||||
local attr_str = {};
|
||||
for k, _, v in attr:gmatch("(%S+)=([\"'])([^%2]-)%2") do
|
||||
if #attr_str == 0 then
|
||||
table.insert(attr_str, ", { ");
|
||||
else
|
||||
table.insert(attr_str, ", ");
|
||||
end
|
||||
if k:find("^%a%w*$") then
|
||||
table.insert(attr_str, string.format("%s = %q", k, v));
|
||||
else
|
||||
table.insert(attr_str, string.format("[%q] = %q", k, v));
|
||||
end
|
||||
end
|
||||
if #attr_str > 0 then
|
||||
table.insert(attr_str, " }");
|
||||
end
|
||||
if first then
|
||||
code[#code+1] = (string.format("st.stanza(%q %s)", name, #attr_str>0 and table.concat(attr_str) or ", nil"));
|
||||
first = nil;
|
||||
else
|
||||
code[#code+1] = (string.format(":tag(%q%s)", name, table.concat(attr_str)));
|
||||
end
|
||||
end
|
||||
if text and text:find("%S") then
|
||||
code[#code+1] = (string.format(":text(%q)", text));
|
||||
elseif short_close then
|
||||
short_close = nil;
|
||||
code[#code+1] = (":up()");
|
||||
end
|
||||
end
|
||||
return table.concat(code, "");
|
||||
end
|
||||
|
||||
function action_handlers.PASS()
|
||||
return "do return pass_return end"
|
||||
end
|
||||
|
||||
function action_handlers.DROP()
|
||||
return "do return true end";
|
||||
end
|
||||
|
||||
function action_handlers.DEFAULT()
|
||||
return "do return false end";
|
||||
end
|
||||
|
||||
function action_handlers.RETURN()
|
||||
return "do return end"
|
||||
end
|
||||
|
||||
function action_handlers.STRIP(tag_desc)
|
||||
local code = {};
|
||||
local name, xmlns = tag_desc:match("^(%S+) (.+)$");
|
||||
if not name then
|
||||
name, xmlns = tag_desc, nil;
|
||||
end
|
||||
if name == "*" then
|
||||
name = nil;
|
||||
end
|
||||
code[#code+1] = ("local stanza_xmlns = stanza.attr.xmlns; ");
|
||||
code[#code+1] = "stanza:maptags(function (tag) if ";
|
||||
if name then
|
||||
code[#code+1] = ("tag.name == %q and "):format(name);
|
||||
end
|
||||
if xmlns then
|
||||
code[#code+1] = ("(tag.attr.xmlns or stanza_xmlns) == %q "):format(xmlns);
|
||||
else
|
||||
code[#code+1] = ("tag.attr.xmlns == stanza_xmlns ");
|
||||
end
|
||||
code[#code+1] = "then return nil; end return tag; end );";
|
||||
return table.concat(code);
|
||||
end
|
||||
|
||||
function action_handlers.INJECT(tag)
|
||||
return "stanza:add_child("..compile_xml(tag)..")", { "st" };
|
||||
end
|
||||
|
||||
local error_types = {
|
||||
["bad-request"] = "modify";
|
||||
["conflict"] = "cancel";
|
||||
["feature-not-implemented"] = "cancel";
|
||||
["forbidden"] = "auth";
|
||||
["gone"] = "cancel";
|
||||
["internal-server-error"] = "cancel";
|
||||
["item-not-found"] = "cancel";
|
||||
["jid-malformed"] = "modify";
|
||||
["not-acceptable"] = "modify";
|
||||
["not-allowed"] = "cancel";
|
||||
["not-authorized"] = "auth";
|
||||
["payment-required"] = "auth";
|
||||
["policy-violation"] = "modify";
|
||||
["recipient-unavailable"] = "wait";
|
||||
["redirect"] = "modify";
|
||||
["registration-required"] = "auth";
|
||||
["remote-server-not-found"] = "cancel";
|
||||
["remote-server-timeout"] = "wait";
|
||||
["resource-constraint"] = "wait";
|
||||
["service-unavailable"] = "cancel";
|
||||
["subscription-required"] = "auth";
|
||||
["undefined-condition"] = "cancel";
|
||||
["unexpected-request"] = "wait";
|
||||
};
|
||||
|
||||
|
||||
local function route_modify(make_new, to, drop)
|
||||
local reroute, deps = "session.send(newstanza)", { "st" };
|
||||
if to then
|
||||
reroute = ("newstanza.attr.to = %q; core_post_stanza(session, newstanza)"):format(to);
|
||||
deps[#deps+1] = "core_post_stanza";
|
||||
end
|
||||
return ([[do local newstanza = st.%s; %s;%s end]])
|
||||
:format(make_new, reroute, drop and " return true" or ""), deps;
|
||||
end
|
||||
|
||||
function action_handlers.BOUNCE(with)
|
||||
local error = with and with:match("^%S+") or "service-unavailable";
|
||||
local error_type = error:match(":(%S+)");
|
||||
if not error_type then
|
||||
error_type = error_types[error] or "cancel";
|
||||
else
|
||||
error = error:match("^[^:]+");
|
||||
end
|
||||
error, error_type = string.format("%q", error), string.format("%q", error_type);
|
||||
local text = with and with:match(" %((.+)%)$");
|
||||
if text then
|
||||
text = string.format("%q", text);
|
||||
else
|
||||
text = "nil";
|
||||
end
|
||||
local route_modify_code, deps = route_modify(("error_reply(stanza, %s, %s, %s)"):format(error_type, error, text), nil, true);
|
||||
deps[#deps+1] = "type";
|
||||
deps[#deps+1] = "name";
|
||||
return [[if type == "error" or (name == "iq" and type == "result") then return true; end -- Don't reply to 'error' stanzas, or iq results
|
||||
]]..route_modify_code, deps;
|
||||
end
|
||||
|
||||
function action_handlers.REDIRECT(where)
|
||||
return route_modify("clone(stanza)", where, true);
|
||||
end
|
||||
|
||||
function action_handlers.COPY(where)
|
||||
return route_modify("clone(stanza)", where, false);
|
||||
end
|
||||
|
||||
function action_handlers.REPLY(with)
|
||||
return route_modify(("reply(stanza):body(%q)"):format(with));
|
||||
end
|
||||
|
||||
function action_handlers.FORWARD(where)
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza);
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where), { "core_post_stanza", "current_host" };
|
||||
end
|
||||
|
||||
function action_handlers.LOG(string)
|
||||
local level = string:match("^%[(%a+)%]") or "info";
|
||||
string = string:gsub("^%[%a+%] ?", "");
|
||||
local meta_deps = {};
|
||||
local code = meta(("(session.log or log)(%q, '%%s', %q);"):format(level, string), meta_deps);
|
||||
return code, meta_deps;
|
||||
end
|
||||
|
||||
function action_handlers.RULEDEP(dep)
|
||||
return "", { dep };
|
||||
end
|
||||
|
||||
function action_handlers.EVENT(name)
|
||||
return ("fire_event(%q, event)"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_EVENT(name)
|
||||
return ("do return fire_event(%q, event); end"):format(name);
|
||||
end
|
||||
|
||||
function action_handlers.JUMP_CHAIN(name)
|
||||
return template([[do
|
||||
local ret = fire_event($chain_event$, event);
|
||||
if ret ~= nil then
|
||||
if ret == false then
|
||||
log("debug", "Chain %q accepted stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return pass_return;
|
||||
end
|
||||
log("debug", "Chain %q rejected stanza (ret %s)", $chain_name$, tostring(ret));
|
||||
return ret;
|
||||
end
|
||||
end]], { chain_event = "firewall/chains/"..name, chain_name = name });
|
||||
end
|
||||
|
||||
function action_handlers.MARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = current_timestamp;]], { "timestamp" };
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_ORIGIN(name)
|
||||
return [[session.firewall_marked_]]..idsafe(name)..[[ = nil;]]
|
||||
end
|
||||
|
||||
function action_handlers.MARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
timestamp = current_timestamp;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to MARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name)), {
|
||||
"current_host";
|
||||
"timestamp";
|
||||
};
|
||||
end
|
||||
|
||||
function action_handlers.UNMARK_USER(name)
|
||||
return ([[if session.username and session.host == current_host then
|
||||
fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = %q;
|
||||
});
|
||||
else
|
||||
log("warn", "Attempt to UNMARK a remote user - only local users may be marked");
|
||||
end]]):format(assert(idsafe(name), "Invalid characters in mark name: "..name));
|
||||
end
|
||||
|
||||
function action_handlers.ADD_TO(spec)
|
||||
local list_name, value = spec:match("(%S+) (.+)");
|
||||
local meta_deps = {};
|
||||
value = meta(("%q"):format(value), meta_deps);
|
||||
return ("list_%s:add(%s);"):format(list_name, value), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function action_handlers.UNSUBSCRIBE_SENDER()
|
||||
return "rostermanager.unsubscribed(to_node, to_host, bare_from);\
|
||||
rostermanager.roster_push(to_node, to_host, bare_from);\
|
||||
core_post_stanza(session, st.presence({ from = bare_to, to = bare_from, type = \"unsubscribed\" }));",
|
||||
{ "rostermanager", "core_post_stanza", "st", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function action_handlers.REPORT_TO(spec)
|
||||
local where, reason, text = spec:match("^%s*(%S+) *(%S*) *(.*)$");
|
||||
if reason == "spam" then
|
||||
reason = "urn:xmpp:reporting:spam";
|
||||
elseif reason == "abuse" or not reason then
|
||||
reason = "urn:xmpp:reporting:abuse";
|
||||
end
|
||||
local code = [[
|
||||
local newstanza = st.stanza("message", { to = %q, from = current_host }):tag("forwarded", { xmlns = "urn:xmpp:forward:0" });
|
||||
local tmp_stanza = st.clone(stanza); tmp_stanza.attr.xmlns = "jabber:client"; newstanza:add_child(tmp_stanza):up();
|
||||
newstanza:tag("report", { xmlns = "urn:xmpp:reporting:1", reason = %q })
|
||||
do local text = %q; if text ~= "" then newstanza:text_tag("text", text); end end
|
||||
newstanza:up();
|
||||
core_post_stanza(session, newstanza);
|
||||
]];
|
||||
return code:format(where, reason, text), { "core_post_stanza", "current_host", "st" };
|
||||
end
|
||||
|
||||
return action_handlers;
|
||||
384
resources/prosody-plugins/mod_firewall/conditions.lib.lua
Normal file
384
resources/prosody-plugins/mod_firewall/conditions.lib.lua
Normal file
@@ -0,0 +1,384 @@
|
||||
--luacheck: globals meta idsafe
|
||||
local condition_handlers = {};
|
||||
|
||||
local jid = require "util.jid";
|
||||
local unpack = table.unpack or unpack;
|
||||
|
||||
-- Helper to convert user-input strings (yes/true//no/false) to a bool
|
||||
local function string_to_boolean(s)
|
||||
s = s:lower();
|
||||
return s == "yes" or s == "true";
|
||||
end
|
||||
|
||||
-- Return a code string for a condition that checks whether the contents
|
||||
-- of variable with the name 'name' matches any of the values in the
|
||||
-- comma/space/pipe delimited list 'values'.
|
||||
local function compile_comparison_list(name, values)
|
||||
local conditions = {};
|
||||
for value in values:gmatch("[^%s,|]+") do
|
||||
table.insert(conditions, ("%s == %q"):format(name, value));
|
||||
end
|
||||
return table.concat(conditions, " or ");
|
||||
end
|
||||
|
||||
function condition_handlers.KIND(kind)
|
||||
assert(kind, "Expected stanza kind to match against");
|
||||
return compile_comparison_list("name", kind), { "name" };
|
||||
end
|
||||
|
||||
local wildcard_equivs = { ["*"] = ".*", ["?"] = "." };
|
||||
|
||||
local function compile_jid_match_part(part, match)
|
||||
if not match then
|
||||
return part.." == nil";
|
||||
end
|
||||
local pattern = match:match("^<(.*)>$");
|
||||
if pattern then
|
||||
if pattern == "*" then
|
||||
return part;
|
||||
end
|
||||
if pattern:find("^<.*>$") then
|
||||
pattern = pattern:match("^<(.*)>$");
|
||||
else
|
||||
pattern = pattern:gsub("%p", "%%%0"):gsub("%%(%p)", wildcard_equivs);
|
||||
end
|
||||
return ("(%s and %s:find(%q))"):format(part, part, "^"..pattern.."$");
|
||||
else
|
||||
return ("%s == %q"):format(part, match);
|
||||
end
|
||||
end
|
||||
|
||||
local function compile_jid_match(which, match_jid)
|
||||
local match_node, match_host, match_resource = jid.split(match_jid);
|
||||
local conditions = {};
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_node", match_node);
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_host", match_host);
|
||||
if match_resource then
|
||||
conditions[#conditions+1] = compile_jid_match_part(which.."_resource", match_resource);
|
||||
end
|
||||
return table.concat(conditions, " and ");
|
||||
end
|
||||
|
||||
function condition_handlers.TO(to)
|
||||
return compile_jid_match("to", to), { "split_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM(from)
|
||||
return compile_jid_match("from", from), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_FULL_JID()
|
||||
return "not "..compile_jid_match_part("from_resource", nil), { "split_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_EXACTLY(from)
|
||||
local metadeps = {};
|
||||
return ("from == %s"):format(metaq(from, metadeps)), { "from", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_EXACTLY(to)
|
||||
local metadeps = {};
|
||||
return ("to == %s"):format(metaq(to, metadeps)), { "to", unpack(metadeps) };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_SELF()
|
||||
-- Intentionally not using 'to' here, as that defaults to bare JID when nil
|
||||
return ("stanza.attr.to == nil");
|
||||
end
|
||||
|
||||
function condition_handlers.TYPE(type)
|
||||
assert(type, "Expected 'type' value to match against");
|
||||
return compile_comparison_list("(type or (name == 'message' and 'normal') or (name == 'presence' and 'available'))", type), { "type", "name" };
|
||||
end
|
||||
|
||||
local function zone_check(zone, which)
|
||||
local zone_var = zone;
|
||||
if zone == "$local" then zone_var = "_local" end
|
||||
local which_not = which == "from" and "to" or "from";
|
||||
return ("(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s]) "
|
||||
.."and not(zone_%s[%s_host] or zone_%s[%s] or zone_%s[bare_%s])"
|
||||
)
|
||||
:format(zone_var, which, zone_var, which, zone_var, which,
|
||||
zone_var, which_not, zone_var, which_not, zone_var, which_not), {
|
||||
"split_to", "split_from", "bare_to", "bare_from", "zone:"..zone
|
||||
};
|
||||
end
|
||||
|
||||
function condition_handlers.ENTERING(zone)
|
||||
return zone_check(zone, "to");
|
||||
end
|
||||
|
||||
function condition_handlers.LEAVING(zone)
|
||||
return zone_check(zone, "from");
|
||||
end
|
||||
|
||||
-- IN ROSTER? (parameter is deprecated)
|
||||
function condition_handlers.IN_ROSTER(yes_no)
|
||||
local in_roster_requirement = string_to_boolean(yes_no or "yes"); -- COMPAT w/ older scripts
|
||||
return "not "..(in_roster_requirement and "not" or "").." roster_entry", { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.IN_ROSTER_GROUP(group)
|
||||
return ("not not (roster_entry and roster_entry.groups[%q])"):format(group), { "roster_entry" };
|
||||
end
|
||||
|
||||
function condition_handlers.SUBSCRIBED()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_subscribed(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PENDING_SUBSCRIPTION_FROM_SENDER()
|
||||
return "(bare_to == bare_from or to_node and rostermanager.is_contact_pending_in(to_node, to_host, bare_from))",
|
||||
{ "rostermanager", "split_to", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.PAYLOAD(payload_ns)
|
||||
return ("stanza:get_child(nil, %q)"):format(payload_ns);
|
||||
end
|
||||
|
||||
function condition_handlers.INSPECT(path)
|
||||
if path:find("=") then
|
||||
local query, match_type, value = path:match("(.-)([~/$]*)=(.*)");
|
||||
if not(query:match("#$") or query:match("@[^/]+")) then
|
||||
error("Stanza path does not return a string (append # for text content or @name for value of named attribute)", 0);
|
||||
end
|
||||
local meta_deps = {};
|
||||
local quoted_value = ("%q"):format(value);
|
||||
if match_type:find("$", 1, true) then
|
||||
match_type = match_type:gsub("%$", "");
|
||||
quoted_value = meta(quoted_value, meta_deps);
|
||||
end
|
||||
if match_type == "~" then -- Lua pattern match
|
||||
return ("(stanza:find(%q) or ''):match(%s)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "/" then -- find literal substring
|
||||
return ("(stanza:find(%q) or ''):find(%s, 1, true)"):format(query, quoted_value), meta_deps;
|
||||
elseif match_type == "" then -- exact match
|
||||
return ("stanza:find(%q) == %s"):format(query, quoted_value), meta_deps;
|
||||
else
|
||||
error("Unrecognised comparison '"..match_type.."='", 0);
|
||||
end
|
||||
end
|
||||
return ("stanza:find(%q)"):format(path);
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_from)"):format(group_name), { "group_contains", "bare_from" };
|
||||
end
|
||||
|
||||
function condition_handlers.TO_GROUP(group_name)
|
||||
return ("group_contains(%q, bare_to)"):format(group_name), { "group_contains", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.CROSSING_GROUPS(group_names)
|
||||
local code = {};
|
||||
for group_name in group_names:gmatch("([^, ][^,]+)") do
|
||||
group_name = group_name:match("^%s*(.-)%s*$"); -- Trim leading/trailing whitespace
|
||||
-- Just check that's it is crossing from outside group to inside group
|
||||
table.insert(code, ("(group_contains(%q, bare_to) and group_contains(%q, bare_from))"):format(group_name, group_name))
|
||||
end
|
||||
return "not "..table.concat(code, " or "), { "group_contains", "bare_to", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN_OF(host)
|
||||
return ("is_admin(bare_from, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_from" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN_OF(host)
|
||||
return ("is_admin(bare_to, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_to" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.FROM_ADMIN()
|
||||
return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
|
||||
end
|
||||
|
||||
-- COMPAT w/0.12: Deprecated
|
||||
function condition_handlers.TO_ADMIN()
|
||||
return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
|
||||
end
|
||||
|
||||
-- MAY: permission_to_check
|
||||
function condition_handlers.MAY(permission_to_check)
|
||||
return ("module:may(%q, event)"):format(permission_to_check);
|
||||
end
|
||||
|
||||
function condition_handlers.TO_ROLE(role_name)
|
||||
return ("get_jid_role(bare_to, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_to" };
|
||||
end
|
||||
|
||||
function condition_handlers.FROM_ROLE(role_name)
|
||||
return ("get_jid_role(bare_from, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_from" };
|
||||
end
|
||||
|
||||
local day_numbers = { sun = 0, mon = 2, tue = 3, wed = 4, thu = 5, fri = 6, sat = 7 };
|
||||
|
||||
local function current_time_check(op, hour, minute)
|
||||
hour, minute = tonumber(hour), tonumber(minute);
|
||||
local adj_op = op == "<" and "<" or ">="; -- Start time inclusive, end time exclusive
|
||||
if minute == 0 then
|
||||
return "(current_hour"..adj_op..hour..")";
|
||||
else
|
||||
return "((current_hour"..op..hour..") or (current_hour == "..hour.." and current_minute"..adj_op..minute.."))";
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_day_number(day_name)
|
||||
return assert(day_numbers[day_name:sub(1,3):lower()], "Unknown day name: "..day_name);
|
||||
end
|
||||
|
||||
function condition_handlers.DAY(days)
|
||||
local conditions = {};
|
||||
for day_range in days:gmatch("[^,]+") do
|
||||
local day_start, day_end = day_range:match("(%a+)%s*%-%s*(%a+)");
|
||||
if day_start and day_end then
|
||||
local day_start_num, day_end_num = resolve_day_number(day_start), resolve_day_number(day_end);
|
||||
local op = "and";
|
||||
if day_end_num < day_start_num then
|
||||
op = "or";
|
||||
end
|
||||
table.insert(conditions, ("current_day >= %d %s current_day <= %d"):format(day_start_num, op, day_end_num));
|
||||
elseif day_range:find("%a") then
|
||||
local day = resolve_day_number(day_range:match("%a+"));
|
||||
table.insert(conditions, "current_day == "..day);
|
||||
else
|
||||
error("Unable to parse day/day range: "..day_range);
|
||||
end
|
||||
end
|
||||
assert(#conditions>0, "Expected a list of days or day ranges");
|
||||
return "("..table.concat(conditions, ") or (")..")", { "time:day" };
|
||||
end
|
||||
|
||||
function condition_handlers.TIME(ranges)
|
||||
local conditions = {};
|
||||
for range in ranges:gmatch("([^,]+)") do
|
||||
local clause = {};
|
||||
range = range:lower()
|
||||
:gsub("(%d+):?(%d*) *am", function (h, m) return tostring(tonumber(h)%12)..":"..(tonumber(m) or "00"); end)
|
||||
:gsub("(%d+):?(%d*) *pm", function (h, m) return tostring(tonumber(h)%12+12)..":"..(tonumber(m) or "00"); end);
|
||||
local start_hour, start_minute = range:match("(%d+):(%d+) *%-");
|
||||
local end_hour, end_minute = range:match("%- *(%d+):(%d+)");
|
||||
local op = tonumber(start_hour) > tonumber(end_hour) and " or " or " and ";
|
||||
if start_hour and end_hour then
|
||||
table.insert(clause, current_time_check(">", start_hour, start_minute));
|
||||
table.insert(clause, current_time_check("<", end_hour, end_minute));
|
||||
end
|
||||
if #clause == 0 then
|
||||
error("Unable to parse time range: "..range);
|
||||
end
|
||||
table.insert(conditions, "("..table.concat(clause, " "..op.." ")..")");
|
||||
end
|
||||
return table.concat(conditions, " or "), { "time:hour,min" };
|
||||
end
|
||||
|
||||
function condition_handlers.LIMIT(spec)
|
||||
local name, param = spec:match("^(%w+) on (.+)$");
|
||||
local meta_deps = {};
|
||||
|
||||
if not name then
|
||||
name = spec:match("^%w+$");
|
||||
if not name then
|
||||
error("Unable to parse LIMIT specification");
|
||||
end
|
||||
else
|
||||
param = meta(("%q"):format(param), meta_deps);
|
||||
end
|
||||
|
||||
if not param then
|
||||
return ("not global_throttle_%s:poll(1)"):format(name), { "globalthrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
return ("not multi_throttle_%s:poll_on(%s, 1)"):format(name, param), { "multithrottle:"..name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
function condition_handlers.ORIGIN_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ("(current_timestamp - (session.firewall_marked_%s or 0)) < %d"):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not session.firewall_marked_"..idsafe(name));
|
||||
end
|
||||
|
||||
function condition_handlers.USER_MARKED(name_and_time)
|
||||
local name, time = name_and_time:match("^%s*([%w_]+)%s+%(([^)]+)s%)%s*$");
|
||||
if not name then
|
||||
name = name_and_time:match("^%s*([%w_]+)%s*$");
|
||||
end
|
||||
if not name then
|
||||
error("Error parsing mark name, see documentation for usage examples");
|
||||
end
|
||||
if time then
|
||||
return ([[(
|
||||
current_timestamp - (session.firewall_marks and session.firewall_marks.%s or 0)
|
||||
) < %d]]):format(idsafe(name), tonumber(time)), { "timestamp" };
|
||||
end
|
||||
return ("not not (session.firewall_marks and session.firewall_marks."..idsafe(name)..")");
|
||||
end
|
||||
|
||||
function condition_handlers.SENT_DIRECTED_PRESENCE_TO_SENDER()
|
||||
return "not not (session.directed and session.directed[from])", { "from" };
|
||||
end
|
||||
|
||||
-- TO FULL JID?
|
||||
function condition_handlers.TO_FULL_JID()
|
||||
return "not not full_sessions[to]", { "to", "full_sessions" };
|
||||
end
|
||||
|
||||
-- CHECK LIST: spammers contains $<@from>
|
||||
function condition_handlers.CHECK_LIST(list_condition)
|
||||
local list_name, expr = list_condition:match("(%S+) contains (.+)$");
|
||||
if not (list_name and expr) then
|
||||
error("Error parsing list check, syntax: LISTNAME contains EXPRESSION");
|
||||
end
|
||||
local meta_deps = {};
|
||||
expr = meta(("%q"):format(expr), meta_deps);
|
||||
return ("list_%s:contains(%s) == true"):format(list_name, expr), { "list:"..list_name, unpack(meta_deps) };
|
||||
end
|
||||
|
||||
-- SCAN: body for word in badwords
|
||||
function condition_handlers.SCAN(scan_expression)
|
||||
local search_name, pattern_name, list_name = scan_expression:match("(%S+) for (%S+) in (%S+)$");
|
||||
if not (search_name) then
|
||||
error("Error parsing SCAN expression, syntax: SEARCH for PATTERN in LIST");
|
||||
end
|
||||
return ("scan_list(list_%s, %s)"):format(
|
||||
list_name,
|
||||
"tokens_"..search_name.."_"..pattern_name
|
||||
), {
|
||||
"scan_list",
|
||||
"tokens:"..search_name.."-"..pattern_name, "list:"..list_name
|
||||
};
|
||||
end
|
||||
|
||||
-- COUNT: lines in body < 10
|
||||
local valid_comp_ops = { [">"] = ">", ["<"] = "<", ["="] = "==", ["=="] = "==", ["<="] = "<=", [">="] = ">=" };
|
||||
function condition_handlers.COUNT(count_expression)
|
||||
local pattern_name, search_name, comparator_expression = count_expression:match("(%S+) in (%S+) (.+)$");
|
||||
if not (pattern_name) then
|
||||
error("Error parsing COUNT expression, syntax: PATTERN in SEARCH COMPARATOR");
|
||||
end
|
||||
local value;
|
||||
comparator_expression = comparator_expression:gsub("%d+", function (value_string)
|
||||
value = tonumber(value_string);
|
||||
return "";
|
||||
end);
|
||||
if not value then
|
||||
error("Error parsing COUNT expression, expected value");
|
||||
end
|
||||
local comp_op = comparator_expression:gsub("%s+", "");
|
||||
assert(valid_comp_ops[comp_op], "Error parsing COUNT expression, unknown comparison operator: "..comp_op);
|
||||
return ("it_count(search_%s:gmatch(pattern_%s)) %s %d"):format(
|
||||
search_name, pattern_name, comp_op, value
|
||||
), {
|
||||
"it_count",
|
||||
"search:"..search_name, "pattern:"..pattern_name
|
||||
};
|
||||
end
|
||||
|
||||
return condition_handlers;
|
||||
335
resources/prosody-plugins/mod_firewall/definitions.lib.lua
Normal file
335
resources/prosody-plugins/mod_firewall/definitions.lib.lua
Normal file
@@ -0,0 +1,335 @@
|
||||
|
||||
-- Name arguments are unused here
|
||||
-- luacheck: ignore 212
|
||||
|
||||
local definition_handlers = {};
|
||||
|
||||
local http = require "net.http";
|
||||
local timer = require "util.timer";
|
||||
local set = require"util.set";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local hashes = require "util.hashes";
|
||||
local jid = require "util.jid";
|
||||
local lfs = require "lfs";
|
||||
|
||||
local multirate_cache_size = module:get_option_number("firewall_multirate_cache_limit", 1000);
|
||||
|
||||
function definition_handlers.ZONE(zone_name, zone_members)
|
||||
local zone_member_list = {};
|
||||
for member in zone_members:gmatch("[^, ]+") do
|
||||
zone_member_list[#zone_member_list+1] = member;
|
||||
end
|
||||
return set.new(zone_member_list)._items;
|
||||
end
|
||||
|
||||
-- Helper function used by RATE handler
|
||||
local function evict_only_unthrottled(name, throttle)
|
||||
throttle:update();
|
||||
-- Check whether the throttle is at max balance (i.e. totally safe to forget about it)
|
||||
if throttle.balance < throttle.max then
|
||||
-- Not safe to forget
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
function definition_handlers.RATE(name, line)
|
||||
local rate = assert(tonumber(line:match("([%d.]+)")), "Unable to parse rate");
|
||||
local burst = tonumber(line:match("%(%s*burst%s+([%d.]+)%s*%)")) or 1;
|
||||
local max_throttles = tonumber(line:match("%(%s*entries%s+([%d]+)%s*%)")) or multirate_cache_size;
|
||||
local deny_when_full = not line:match("%(allow overflow%)");
|
||||
return {
|
||||
single = function ()
|
||||
return new_throttle(rate*burst, burst);
|
||||
end;
|
||||
|
||||
multi = function ()
|
||||
local cache = require "util.cache".new(max_throttles, deny_when_full and evict_only_unthrottled or nil);
|
||||
return {
|
||||
poll_on = function (_, key, amount)
|
||||
assert(key, "no key");
|
||||
local throttle = cache:get(key);
|
||||
if not throttle then
|
||||
throttle = new_throttle(rate*burst, burst);
|
||||
if not cache:set(key, throttle) then
|
||||
module:log("warn", "Multirate '%s' has hit its maximum number of active throttles (%d), denying new events", name, max_throttles);
|
||||
return false;
|
||||
end
|
||||
end
|
||||
return throttle:poll(amount);
|
||||
end;
|
||||
}
|
||||
end;
|
||||
};
|
||||
end
|
||||
|
||||
local list_backends = {
|
||||
-- %LIST name: memory (limit: number)
|
||||
memory = {
|
||||
init = function (self, type, opts)
|
||||
if opts.limit then
|
||||
local have_cache_lib, cache_lib = pcall(require, "util.cache");
|
||||
if not have_cache_lib then
|
||||
error("In-memory lists with a size limit require Prosody 0.10");
|
||||
end
|
||||
self.cache = cache_lib.new((assert(tonumber(opts.limit), "Invalid list limit")));
|
||||
if not self.cache.table then
|
||||
error("In-memory lists with a size limit require a newer version of Prosody 0.10");
|
||||
end
|
||||
self.items = self.cache:table();
|
||||
else
|
||||
self.items = {};
|
||||
end
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST name: http://example.com/ (ttl: number, pattern: pat, hash: sha1)
|
||||
http = {
|
||||
init = function (self, url, opts)
|
||||
local poll_interval = assert(tonumber(opts.ttl or "3600"), "invalid ttl for <"..url.."> (expected number of seconds)");
|
||||
local pattern = opts.pattern or "([^\r\n]+)\r?\n";
|
||||
assert(pcall(string.match, "", pattern), "invalid pattern for <"..url..">");
|
||||
if opts.hash then
|
||||
assert(opts.hash:match("^%w+$") and type(hashes[opts.hash]) == "function", "invalid hash function: "..opts.hash);
|
||||
self.hash_function = hashes[opts.hash];
|
||||
end
|
||||
local etag;
|
||||
local failure_count = 0;
|
||||
local retry_intervals = { 60, 120, 300 };
|
||||
-- By default only check the certificate if net.http supports SNI
|
||||
local sni_supported = http.feature and http.features.sni;
|
||||
local insecure = false;
|
||||
if opts.checkcert == "never" then
|
||||
insecure = true;
|
||||
elseif (opts.checkcert == nil or opts.checkcert == "when-sni") and not sni_supported then
|
||||
insecure = false;
|
||||
end
|
||||
local function update_list()
|
||||
http.request(url, {
|
||||
insecure = insecure;
|
||||
headers = {
|
||||
["If-None-Match"] = etag;
|
||||
};
|
||||
}, function (body, code, response)
|
||||
local next_poll = poll_interval;
|
||||
if code == 200 and body then
|
||||
etag = response.headers.etag;
|
||||
local items = {};
|
||||
for entry in body:gmatch(pattern) do
|
||||
items[entry] = true;
|
||||
end
|
||||
self.items = items;
|
||||
module:log("debug", "Fetched updated list from <%s>", url);
|
||||
elseif code == 304 then
|
||||
module:log("debug", "List at <%s> is unchanged", url);
|
||||
elseif code == 0 or (code >= 400 and code <=599) then
|
||||
module:log("warn", "Failed to fetch list from <%s>: %d %s", url, code, tostring(body));
|
||||
failure_count = failure_count + 1;
|
||||
next_poll = retry_intervals[failure_count] or retry_intervals[#retry_intervals];
|
||||
end
|
||||
if next_poll > 0 then
|
||||
timer.add_task(next_poll+math.random(0, 60), update_list);
|
||||
end
|
||||
end);
|
||||
end
|
||||
update_list();
|
||||
end;
|
||||
add = function ()
|
||||
end;
|
||||
remove = function ()
|
||||
end;
|
||||
contains = function (self, item)
|
||||
if self.hash_function then
|
||||
item = self.hash_function(item);
|
||||
end
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: file:/path/to/file
|
||||
file = {
|
||||
init = function (self, file_spec, opts)
|
||||
local n, items = 0, {};
|
||||
self.items = items;
|
||||
local filename = file_spec:gsub("^file:", "");
|
||||
if opts.missing == "ignore" and not lfs.attributes(filename, "mode") then
|
||||
module:log("debug", "Ignoring missing list file: %s", filename);
|
||||
return;
|
||||
end
|
||||
local file, err = io.open(filename);
|
||||
if not file then
|
||||
module:log("warn", "Failed to open list from %s: %s", filename, err);
|
||||
return;
|
||||
else
|
||||
for line in file:lines() do
|
||||
if not items[line] then
|
||||
n = n + 1;
|
||||
items[line] = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Loaded %d items from %s", n, filename);
|
||||
end;
|
||||
add = function (self, item)
|
||||
self.items[item] = true;
|
||||
end;
|
||||
remove = function (self, item)
|
||||
self.items[item] = nil;
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
|
||||
-- %LIST: pubsub:pubsub.example.com/node
|
||||
-- TODO or the actual URI scheme? Bit overkill maybe?
|
||||
-- TODO Publish items back to the service?
|
||||
-- Step 1: Receiving pubsub events and storing them in the list
|
||||
-- We'll start by using only the item id.
|
||||
-- TODO Invent some custom schema for this? Needed for just a set of strings?
|
||||
pubsubitemid = {
|
||||
init = function(self, pubsub_spec, opts)
|
||||
local service_addr, node = pubsub_spec:match("^pubsubitemid:([^/]*)/(.*)");
|
||||
if not service_addr then
|
||||
module:log("warn", "Invalid list specification (expected 'pubsubitemid:<service>/<node>', got: '%s')", pubsub_spec);
|
||||
return;
|
||||
end
|
||||
module:depends("pubsub_subscription");
|
||||
module:add_item("pubsub-subscription", {
|
||||
service = service_addr;
|
||||
node = node;
|
||||
on_subscribed = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_item = function (event)
|
||||
self:add(event.item.attr.id);
|
||||
end;
|
||||
on_retract = function (event)
|
||||
self:remove(event.item.attr.id);
|
||||
end;
|
||||
on_purge = function ()
|
||||
self.items = {};
|
||||
end;
|
||||
on_unsubscribed = function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
on_delete= function ()
|
||||
self.items = nil;
|
||||
end;
|
||||
});
|
||||
-- TODO Initial fetch? Or should mod_pubsub_subscription do this?
|
||||
end;
|
||||
add = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = true;
|
||||
end
|
||||
end;
|
||||
remove = function (self, item)
|
||||
if self.items then
|
||||
self.items[item] = nil;
|
||||
end
|
||||
end;
|
||||
contains = function (self, item)
|
||||
return self.items and self.items[item] == true;
|
||||
end;
|
||||
};
|
||||
};
|
||||
list_backends.https = list_backends.http;
|
||||
|
||||
local normalize_functions = {
|
||||
upper = string.upper, lower = string.lower;
|
||||
md5 = hashes.md5, sha1 = hashes.sha1, sha256 = hashes.sha256;
|
||||
prep = jid.prep, bare = jid.bare;
|
||||
};
|
||||
|
||||
local function wrap_list_method(list_method, filter)
|
||||
return function (self, item)
|
||||
return list_method(self, filter(item));
|
||||
end
|
||||
end
|
||||
|
||||
local function create_list(list_backend, list_def, opts)
|
||||
if not list_backends[list_backend] then
|
||||
error("Unknown list type '"..list_backend.."'", 0);
|
||||
end
|
||||
local list = setmetatable({}, { __index = list_backends[list_backend] });
|
||||
if list.init then
|
||||
list:init(list_def, opts);
|
||||
end
|
||||
if opts.filter then
|
||||
local filters = {};
|
||||
for func_name in opts.filter:gmatch("[%w_]+") do
|
||||
if func_name == "log" then
|
||||
table.insert(filters, function (s)
|
||||
--print("&&&&&", s);
|
||||
module:log("debug", "Checking list <%s> for: %s", list_def, s);
|
||||
return s;
|
||||
end);
|
||||
else
|
||||
assert(normalize_functions[func_name], "Unknown list filter: "..func_name);
|
||||
table.insert(filters, normalize_functions[func_name]);
|
||||
end
|
||||
end
|
||||
|
||||
local filter;
|
||||
local n = #filters;
|
||||
if n == 1 then
|
||||
filter = filters[1];
|
||||
else
|
||||
function filter(s)
|
||||
for i = 1, n do
|
||||
s = filters[i](s or "");
|
||||
end
|
||||
return s;
|
||||
end
|
||||
end
|
||||
|
||||
list.add = wrap_list_method(list.add, filter);
|
||||
list.remove = wrap_list_method(list.remove, filter);
|
||||
list.contains = wrap_list_method(list.contains, filter);
|
||||
end
|
||||
return list;
|
||||
end
|
||||
|
||||
--[[
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
%LIST spammers: memory (source: /etc/spammers.txt)
|
||||
|
||||
|
||||
%LIST spammers: http://example.com/blacklist.txt
|
||||
]]
|
||||
|
||||
function definition_handlers.LIST(list_name, list_definition)
|
||||
local list_backend = list_definition:match("^%w+");
|
||||
local opts = {};
|
||||
local opt_string = list_definition:match("^%S+%s+%((.+)%)");
|
||||
if opt_string then
|
||||
for opt_k, opt_v in opt_string:gmatch("(%w+): ?([^,]+)") do
|
||||
opts[opt_k] = opt_v;
|
||||
end
|
||||
end
|
||||
return create_list(list_backend, list_definition:match("^%S+"), opts);
|
||||
end
|
||||
|
||||
function definition_handlers.PATTERN(name, pattern)
|
||||
local ok, err = pcall(string.match, "", pattern);
|
||||
if not ok then
|
||||
error("Invalid pattern '"..name.."': "..err);
|
||||
end
|
||||
return pattern;
|
||||
end
|
||||
|
||||
function definition_handlers.SEARCH(name, pattern)
|
||||
return pattern;
|
||||
end
|
||||
|
||||
return definition_handlers;
|
||||
35
resources/prosody-plugins/mod_firewall/marks.lib.lua
Normal file
35
resources/prosody-plugins/mod_firewall/marks.lib.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
local mark_storage = module:open_store("firewall_marks");
|
||||
local mark_map_storage = module:open_store("firewall_marks", "map");
|
||||
|
||||
local user_sessions = prosody.hosts[module.host].sessions;
|
||||
|
||||
module:hook("firewall/marked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if user and not marks then
|
||||
-- Load marks from storage to cache on the user object
|
||||
marks = mark_storage:get(event.username) or {};
|
||||
user.firewall_marks = marks; --luacheck: ignore 122
|
||||
end
|
||||
if marks then
|
||||
marks[event.mark] = event.timestamp;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, event.timestamp);
|
||||
if not ok then
|
||||
module:log("error", "Failed to mark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
||||
|
||||
module:hook("firewall/unmarked/user", function (event)
|
||||
local user = user_sessions[event.username];
|
||||
local marks = user and user.firewall_marks;
|
||||
if marks then
|
||||
marks[event.mark] = nil;
|
||||
end
|
||||
local ok, err = mark_map_storage:set(event.username, event.mark, nil);
|
||||
if not ok then
|
||||
module:log("error", "Failed to unmark user %q with %q: %s", event.username, event.mark, err);
|
||||
end
|
||||
return true;
|
||||
end, -1);
|
||||
784
resources/prosody-plugins/mod_firewall/mod_firewall.lua
Normal file
784
resources/prosody-plugins/mod_firewall/mod_firewall.lua
Normal file
@@ -0,0 +1,784 @@
|
||||
|
||||
local lfs = require "lfs";
|
||||
local resolve_relative_path = require "core.configmanager".resolve_relative_path;
|
||||
local envload = require "util.envload".envload;
|
||||
local logger = require "util.logger".init;
|
||||
local it = require "util.iterators";
|
||||
local set = require "util.set";
|
||||
|
||||
local have_features, features = pcall(require, "core.features");
|
||||
features = have_features and features.available or set.new();
|
||||
|
||||
-- [definition_type] = definition_factory(param)
|
||||
local definitions = module:shared("definitions");
|
||||
|
||||
-- When a definition instance has been instantiated, it lives here
|
||||
-- [definition_type][definition_name] = definition_object
|
||||
local active_definitions = {
|
||||
ZONE = {
|
||||
-- Default zone that includes all local hosts
|
||||
["$local"] = setmetatable({}, { __index = prosody.hosts });
|
||||
};
|
||||
};
|
||||
|
||||
local default_chains = {
|
||||
preroute = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"pre-message/bare", "pre-message/full", "pre-message/host";
|
||||
"pre-presence/bare", "pre-presence/full", "pre-presence/host";
|
||||
"pre-iq/bare", "pre-iq/full", "pre-iq/host";
|
||||
};
|
||||
deliver = {
|
||||
type = "event";
|
||||
priority = 0.1;
|
||||
"message/bare", "message/full", "message/host";
|
||||
"presence/bare", "presence/full", "presence/host";
|
||||
"iq/bare", "iq/full", "iq/host";
|
||||
};
|
||||
deliver_remote = {
|
||||
type = "event"; "route/remote";
|
||||
priority = 0.1;
|
||||
};
|
||||
};
|
||||
|
||||
local extra_chains = module:get_option("firewall_extra_chains", {});
|
||||
|
||||
local chains = {};
|
||||
for k,v in pairs(default_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
for k,v in pairs(extra_chains) do
|
||||
chains[k] = v;
|
||||
end
|
||||
|
||||
-- Returns the input if it is safe to be used as a variable name, otherwise nil
|
||||
function idsafe(name)
|
||||
return name:match("^%a[%w_]*$");
|
||||
end
|
||||
|
||||
local meta_funcs = {
|
||||
bare = function (code)
|
||||
return "jid_bare("..code..")", {"jid_bare"};
|
||||
end;
|
||||
node = function (code)
|
||||
return "(jid_split("..code.."))", {"jid_split"};
|
||||
end;
|
||||
host = function (code)
|
||||
return "(select(2, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
resource = function (code)
|
||||
return "(select(3, jid_split("..code..")))", {"jid_split"};
|
||||
end;
|
||||
};
|
||||
|
||||
-- Run quoted (%q) strings through this to allow them to contain code. e.g.: LOG=Received: $(stanza:top_tag())
|
||||
function meta(s, deps, extra)
|
||||
return (s:gsub("$(%b())", function (expr)
|
||||
expr = expr:gsub("\\(.)", "%1");
|
||||
return [["..tostring(]]..expr..[[).."]];
|
||||
end)
|
||||
:gsub("$(%b<>)", function (expr)
|
||||
expr = expr:sub(2,-2);
|
||||
local default = "<undefined>";
|
||||
expr = expr:gsub("||(%b\"\")$", function (default_string)
|
||||
default = stripslashes(default_string:sub(2,-2));
|
||||
return "";
|
||||
end);
|
||||
local func_chain = expr:match("|[%w|]+$");
|
||||
if func_chain then
|
||||
expr = expr:sub(1, -1-#func_chain);
|
||||
end
|
||||
local code;
|
||||
if expr:match("^@") then
|
||||
-- Skip stanza:find() for simple attribute lookup
|
||||
local attr_name = expr:sub(2);
|
||||
if deps and (attr_name == "to" or attr_name == "from" or attr_name == "type") then
|
||||
-- These attributes may be cached in locals
|
||||
code = attr_name;
|
||||
table.insert(deps, attr_name);
|
||||
else
|
||||
code = "stanza.attr["..("%q"):format(attr_name).."]";
|
||||
end
|
||||
elseif expr:match("^%w+#$") then
|
||||
code = ("stanza:get_child_text(%q)"):format(expr:sub(1, -2));
|
||||
else
|
||||
code = ("stanza:find(%q)"):format(expr);
|
||||
end
|
||||
if func_chain then
|
||||
for func_name in func_chain:gmatch("|(%w+)") do
|
||||
-- to/from are already available in local variables, use those if possible
|
||||
if (code == "to" or code == "from") and func_name == "bare" then
|
||||
code = "bare_"..code;
|
||||
table.insert(deps, code);
|
||||
elseif (code == "to" or code == "from") and (func_name == "node" or func_name == "host" or func_name == "resource") then
|
||||
table.insert(deps, "split_"..code);
|
||||
code = code.."_"..func_name;
|
||||
else
|
||||
assert(meta_funcs[func_name], "unknown function: "..func_name);
|
||||
local new_code, new_deps = meta_funcs[func_name](code);
|
||||
code = new_code;
|
||||
if new_deps and #new_deps > 0 then
|
||||
assert(deps, "function not supported here: "..func_name);
|
||||
for _, dep in ipairs(new_deps) do
|
||||
table.insert(deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return "\"..tostring("..code.." or "..("%q"):format(default)..")..\"";
|
||||
end)
|
||||
:gsub("$$(%a+)", extra or {})
|
||||
:gsub([[^""%.%.]], "")
|
||||
:gsub([[%.%.""$]], ""));
|
||||
end
|
||||
|
||||
function metaq(s, ...)
|
||||
return meta(("%q"):format(s), ...);
|
||||
end
|
||||
|
||||
local escape_chars = {
|
||||
a = "\a", b = "\b", f = "\f", n = "\n", r = "\r", t = "\t",
|
||||
v = "\v", ["\\"] = "\\", ["\""] = "\"", ["\'"] = "\'"
|
||||
};
|
||||
function stripslashes(s)
|
||||
return (s:gsub("\\(.)", escape_chars));
|
||||
end
|
||||
|
||||
-- Dependency locations:
|
||||
-- <type lib>
|
||||
-- <type global>
|
||||
-- function handler()
|
||||
-- <local deps>
|
||||
-- if <conditions> then
|
||||
-- <actions>
|
||||
-- end
|
||||
-- end
|
||||
|
||||
local available_deps = {
|
||||
st = { global_code = [[local st = require "util.stanza";]]};
|
||||
it = { global_code = [[local it = require "util.iterators";]]};
|
||||
it_count = { global_code = [[local it_count = it.count;]], depends = { "it" } };
|
||||
current_host = { global_code = [[local current_host = module.host;]] };
|
||||
jid_split = {
|
||||
global_code = [[local jid_split = require "util.jid".split;]];
|
||||
};
|
||||
jid_bare = {
|
||||
global_code = [[local jid_bare = require "util.jid".bare;]];
|
||||
};
|
||||
to = { local_code = [[local to = stanza.attr.to or jid_bare(session.full_jid);]]; depends = { "jid_bare" } };
|
||||
from = { local_code = [[local from = stanza.attr.from;]] };
|
||||
type = { local_code = [[local type = stanza.attr.type;]] };
|
||||
name = { local_code = [[local name = stanza.name;]] };
|
||||
split_to = { -- The stanza's split to address
|
||||
depends = { "jid_split", "to" };
|
||||
local_code = [[local to_node, to_host, to_resource = jid_split(to);]];
|
||||
};
|
||||
split_from = { -- The stanza's split from address
|
||||
depends = { "jid_split", "from" };
|
||||
local_code = [[local from_node, from_host, from_resource = jid_split(from);]];
|
||||
};
|
||||
bare_to = { depends = { "jid_bare", "to" }, local_code = "local bare_to = jid_bare(to)"};
|
||||
bare_from = { depends = { "jid_bare", "from" }, local_code = "local bare_from = jid_bare(from)"};
|
||||
group_contains = {
|
||||
global_code = [[local group_contains = module:depends("groups").group_contains]];
|
||||
};
|
||||
is_admin = require"core.usermanager".is_admin and { global_code = [[local is_admin = require "core.usermanager".is_admin;]]} or nil;
|
||||
get_jid_role = require "core.usermanager".get_jid_role and { global_code = [[local get_jid_role = require "core.usermanager".get_jid_role;]] } or nil;
|
||||
core_post_stanza = { global_code = [[local core_post_stanza = prosody.core_post_stanza;]] };
|
||||
zone = { global_code = function (zone)
|
||||
local var = zone;
|
||||
if var == "$local" then
|
||||
var = "_local"; -- See #1090
|
||||
else
|
||||
assert(idsafe(var), "Invalid zone name: "..zone);
|
||||
end
|
||||
return ("local zone_%s = zones[%q] or {};"):format(var, zone);
|
||||
end };
|
||||
date_time = { global_code = [[local os_date = os.date]]; local_code = [[local current_date_time = os_date("*t");]] };
|
||||
time = { local_code = function (what)
|
||||
local defs = {};
|
||||
for field in what:gmatch("%a+") do
|
||||
table.insert(defs, ("local current_%s = current_date_time.%s;"):format(field, field));
|
||||
end
|
||||
return table.concat(defs, " ");
|
||||
end, depends = { "date_time" }; };
|
||||
timestamp = { global_code = [[local get_time = require "socket".gettime;]]; local_code = [[local current_timestamp = get_time();]]; };
|
||||
globalthrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local global_throttle_%s = rates.%s:single();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
multithrottle = {
|
||||
global_code = function (throttle)
|
||||
assert(pcall(require, "util.cache"), "Using LIMIT with 'on' requires Prosody 0.10 or higher");
|
||||
assert(idsafe(throttle), "Invalid rate limit name: "..throttle);
|
||||
assert(active_definitions.RATE[throttle], "Unknown rate limit: "..throttle);
|
||||
return ("local multi_throttle_%s = rates.%s:multi();"):format(throttle, throttle);
|
||||
end;
|
||||
};
|
||||
full_sessions = {
|
||||
global_code = [[local full_sessions = prosody.full_sessions;]];
|
||||
};
|
||||
rostermanager = {
|
||||
global_code = [[local rostermanager = require "core.rostermanager";]];
|
||||
};
|
||||
roster_entry = {
|
||||
local_code = [[local roster_entry = (to_node and rostermanager.load_roster(to_node, to_host) or {})[bare_from];]];
|
||||
depends = { "rostermanager", "split_to", "bare_from" };
|
||||
};
|
||||
list = { global_code = function (list)
|
||||
assert(idsafe(list), "Invalid list name: "..list);
|
||||
assert(active_definitions.LIST[list], "Unknown list: "..list);
|
||||
return ("local list_%s = lists[%q];"):format(list, list);
|
||||
end
|
||||
};
|
||||
search = {
|
||||
local_code = function (search_name)
|
||||
local search_path = assert(active_definitions.SEARCH[search_name], "Undefined search path: "..search_name);
|
||||
return ("local search_%s = tostring(stanza:find(%q) or \"\")"):format(search_name, search_path);
|
||||
end;
|
||||
};
|
||||
pattern = {
|
||||
local_code = function (pattern_name)
|
||||
local pattern = assert(active_definitions.PATTERN[pattern_name], "Undefined pattern: "..pattern_name);
|
||||
return ("local pattern_%s = %q"):format(pattern_name, pattern);
|
||||
end;
|
||||
};
|
||||
tokens = {
|
||||
local_code = function (search_and_pattern)
|
||||
local search_name, pattern_name = search_and_pattern:match("^([^%-]+)-(.+)$");
|
||||
local code = ([[local tokens_%s_%s = {};
|
||||
if search_%s then
|
||||
for s in search_%s:gmatch(pattern_%s) do
|
||||
tokens_%s_%s[s] = true;
|
||||
end
|
||||
end
|
||||
]]):format(search_name, pattern_name, search_name, search_name, pattern_name, search_name, pattern_name);
|
||||
return code, { "search:"..search_name, "pattern:"..pattern_name };
|
||||
end;
|
||||
};
|
||||
scan_list = {
|
||||
global_code = [[local function scan_list(list, items) for item in pairs(items) do if list:contains(item) then return true; end end end]];
|
||||
}
|
||||
};
|
||||
|
||||
local function include_dep(dependency, code)
|
||||
local dep, dep_param = dependency:match("^([^:]+):?(.*)$");
|
||||
local dep_info = available_deps[dep];
|
||||
if not dep_info then
|
||||
module:log("error", "Dependency not found: %s", dep);
|
||||
return;
|
||||
end
|
||||
if code.included_deps[dependency] ~= nil then
|
||||
if code.included_deps[dependency] ~= true then
|
||||
module:log("error", "Circular dependency on %s", dep);
|
||||
end
|
||||
return;
|
||||
end
|
||||
code.included_deps[dependency] = false; -- Pending flag (used to detect circular references)
|
||||
for _, dep_dep in ipairs(dep_info.depends or {}) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
if dep_info.global_code then
|
||||
if dep_param ~= "" then
|
||||
local global_code, deps = dep_info.global_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code.global_header, global_code);
|
||||
else
|
||||
table.insert(code.global_header, dep_info.global_code);
|
||||
end
|
||||
end
|
||||
if dep_info.local_code then
|
||||
if dep_param ~= "" then
|
||||
local local_code, deps = dep_info.local_code(dep_param);
|
||||
if deps then
|
||||
for _, dep_dep in ipairs(deps) do
|
||||
include_dep(dep_dep, code);
|
||||
end
|
||||
end
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..local_code.."\n");
|
||||
else
|
||||
table.insert(code, "\n\t\t-- "..dep.."\n\t\t"..dep_info.local_code.."\n");
|
||||
end
|
||||
end
|
||||
code.included_deps[dependency] = true;
|
||||
end
|
||||
|
||||
local definition_handlers = module:require("definitions");
|
||||
local condition_handlers = module:require("conditions");
|
||||
local action_handlers = module:require("actions");
|
||||
|
||||
if module:get_option_boolean("firewall_experimental_user_marks", true) then
|
||||
module:require"marks";
|
||||
end
|
||||
|
||||
local function new_rule(ruleset, chain)
|
||||
assert(chain, "no chain specified");
|
||||
local rule = { conditions = {}, actions = {}, deps = {} };
|
||||
table.insert(ruleset[chain], rule);
|
||||
return rule;
|
||||
end
|
||||
|
||||
local function parse_firewall_rules(filename)
|
||||
local line_no = 0;
|
||||
|
||||
local function errmsg(err)
|
||||
return "Error compiling "..filename.." on line "..line_no..": "..err;
|
||||
end
|
||||
|
||||
local ruleset = {
|
||||
deliver = {};
|
||||
};
|
||||
|
||||
local chain = "deliver"; -- Default chain
|
||||
local rule;
|
||||
|
||||
local file, err = io.open(filename);
|
||||
if not file then return nil, err; end
|
||||
|
||||
local state; -- nil -> "rules" -> "actions" -> nil -> ...
|
||||
|
||||
local line_hold;
|
||||
for line in file:lines() do
|
||||
line = line:match("^%s*(.-)%s*$");
|
||||
if line_hold and line:sub(-1,-1) ~= "\\" then
|
||||
line = line_hold..line;
|
||||
line_hold = nil;
|
||||
elseif line:sub(-1,-1) == "\\" then
|
||||
line_hold = (line_hold or "")..line:sub(1,-2);
|
||||
end
|
||||
line_no = line_no + 1;
|
||||
|
||||
if line_hold or line:find("^[#;]") then -- luacheck: ignore 542
|
||||
-- No action; comment or partial line
|
||||
elseif line == "" then
|
||||
if state == "rules" then
|
||||
return nil, ("Expected an action on line %d for preceding criteria")
|
||||
:format(line_no);
|
||||
end
|
||||
state = nil;
|
||||
elseif not(state) and line:sub(1, 2) == "::" then
|
||||
chain = line:gsub("^::%s*", "");
|
||||
local chain_info = chains[chain];
|
||||
if not chain_info then
|
||||
if chain:match("^user/") then
|
||||
chains[chain] = { type = "event", priority = 1, pass_return = false };
|
||||
else
|
||||
return nil, errmsg("Unknown chain: "..chain);
|
||||
end
|
||||
elseif chain_info.type ~= "event" then
|
||||
return nil, errmsg("Only event chains supported at the moment");
|
||||
end
|
||||
ruleset[chain] = ruleset[chain] or {};
|
||||
elseif not(state) and line:sub(1,1) == "%" then -- Definition (zone, limit, etc.)
|
||||
local what, name = line:match("^%%%s*([%w_]+) +([^ :]+)");
|
||||
if not definition_handlers[what] then
|
||||
return nil, errmsg("Definition of unknown object: "..what);
|
||||
elseif not name or not idsafe(name) then
|
||||
return nil, errmsg("Invalid "..what.." name");
|
||||
end
|
||||
|
||||
local val = line:match(": ?(.*)$");
|
||||
if not val and line:find(":<") then -- Read from file
|
||||
local fn = line:match(":< ?(.-)%s*$");
|
||||
if not fn then
|
||||
return nil, errmsg("Unable to parse filename");
|
||||
end
|
||||
local f, err = io.open(fn);
|
||||
if not f then return nil, errmsg(err); end
|
||||
val = f:read("*a"):gsub("\r?\n", " "):gsub("%s+$", "");
|
||||
end
|
||||
if not val then
|
||||
return nil, errmsg("No value given for definition");
|
||||
end
|
||||
val = stripslashes(val);
|
||||
local ok, ret = pcall(definition_handlers[what], name, val);
|
||||
if not ok then
|
||||
return nil, errmsg(ret);
|
||||
end
|
||||
|
||||
if not active_definitions[what] then
|
||||
active_definitions[what] = {};
|
||||
end
|
||||
active_definitions[what][name] = ret;
|
||||
elseif line:find("^[%w_ ]+[%.=]") then
|
||||
-- Action
|
||||
if state == nil then
|
||||
-- This is a standalone action with no conditions
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
state = "actions";
|
||||
-- Action handlers?
|
||||
local action = line:match("^[%w_ ]+"):upper():gsub(" ", "_");
|
||||
if not action_handlers[action] then
|
||||
return nil, ("Unknown action on line %d: %s"):format(line_no, action or "<unknown>");
|
||||
end
|
||||
table.insert(rule.actions, "-- "..line)
|
||||
local ok, action_string, action_deps = pcall(action_handlers[action], line:match("=(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(action_string);
|
||||
end
|
||||
table.insert(rule.actions, action_string);
|
||||
for _, dep in ipairs(action_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
elseif state == "actions" then -- state is actions but action pattern did not match
|
||||
state = nil; -- Awaiting next rule, etc.
|
||||
table.insert(ruleset[chain], rule);
|
||||
rule = nil;
|
||||
else
|
||||
if not state then
|
||||
state = "rules";
|
||||
rule = new_rule(ruleset, chain);
|
||||
end
|
||||
-- Check standard modifiers for the condition (e.g. NOT)
|
||||
local negated;
|
||||
local condition = line:match("^[^:=%.?]*");
|
||||
if condition:find("%f[%w]NOT%f[^%w]") then
|
||||
local s, e = condition:match("%f[%w]()NOT()%f[^%w]");
|
||||
condition = (condition:sub(1,s-1)..condition:sub(e+1, -1)):match("^%s*(.-)%s*$");
|
||||
negated = true;
|
||||
end
|
||||
condition = condition:gsub(" ", "_");
|
||||
if not condition_handlers[condition] then
|
||||
return nil, ("Unknown condition on line %d: %s"):format(line_no, (condition:gsub("_", " ")));
|
||||
end
|
||||
-- Get the code for this condition
|
||||
local ok, condition_code, condition_deps = pcall(condition_handlers[condition], line:match(":%s?(.+)$"));
|
||||
if not ok then
|
||||
return nil, errmsg(condition_code);
|
||||
end
|
||||
if negated then condition_code = "not("..condition_code..")"; end
|
||||
table.insert(rule.conditions, condition_code);
|
||||
for _, dep in ipairs(condition_deps or {}) do
|
||||
table.insert(rule.deps, dep);
|
||||
end
|
||||
end
|
||||
end
|
||||
return ruleset;
|
||||
end
|
||||
|
||||
local function process_firewall_rules(ruleset)
|
||||
-- Compile ruleset and return complete code
|
||||
|
||||
local chain_handlers = {};
|
||||
|
||||
-- Loop through the chains in the parsed ruleset (e.g. incoming, outgoing)
|
||||
for chain_name, rules in pairs(ruleset) do
|
||||
local code = { included_deps = {}, global_header = {} };
|
||||
local condition_uses = {};
|
||||
-- This inner loop assumes chain is an event-based, not a filter-based
|
||||
-- chain (filter-based will be added later)
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, condition in ipairs(rule.conditions) do
|
||||
if condition:find("^not%(.+%)$") then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
condition_uses[condition] = (condition_uses[condition] or 0) + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local condition_cache, n_conditions = {}, 0;
|
||||
for _, rule in ipairs(rules) do
|
||||
for _, dep in ipairs(rule.deps) do
|
||||
include_dep(dep, code);
|
||||
end
|
||||
table.insert(code, "\n\t\t");
|
||||
local rule_code;
|
||||
if #rule.conditions > 0 then
|
||||
for i, condition in ipairs(rule.conditions) do
|
||||
local negated = condition:match("^not%(.+%)$");
|
||||
if negated then
|
||||
condition = condition:match("^not%((.+)%)$");
|
||||
end
|
||||
if condition_uses[condition] > 1 then
|
||||
local name = condition_cache[condition];
|
||||
if not name then
|
||||
n_conditions = n_conditions + 1;
|
||||
name = "condition"..n_conditions;
|
||||
condition_cache[condition] = name;
|
||||
table.insert(code, "local "..name.." = "..condition..";\n\t\t");
|
||||
end
|
||||
rule.conditions[i] = (negated and "not(" or "")..name..(negated and ")" or "");
|
||||
else
|
||||
rule.conditions[i] = (negated and "not(" or "(")..condition..")";
|
||||
end
|
||||
end
|
||||
|
||||
rule_code = "if "..table.concat(rule.conditions, " and ").." then\n\t\t\t"
|
||||
..table.concat(rule.actions, "\n\t\t\t")
|
||||
.."\n\t\tend\n";
|
||||
else
|
||||
rule_code = table.concat(rule.actions, "\n\t\t");
|
||||
end
|
||||
table.insert(code, rule_code);
|
||||
end
|
||||
|
||||
for name in pairs(definition_handlers) do
|
||||
table.insert(code.global_header, 1, "local "..name:lower().."s = definitions."..name..";");
|
||||
end
|
||||
|
||||
local code_string = "return function (definitions, fire_event, log, module, pass_return)\n\t"
|
||||
..table.concat(code.global_header, "\n\t")
|
||||
.."\n\tlocal db = require 'util.debug';\n\n\t"
|
||||
.."return function (event)\n\t\t"
|
||||
.."local stanza, session = event.stanza, event.origin;\n"
|
||||
..table.concat(code, "")
|
||||
.."\n\tend;\nend";
|
||||
|
||||
chain_handlers[chain_name] = code_string;
|
||||
end
|
||||
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
local function compile_firewall_rules(filename)
|
||||
local ruleset, err = parse_firewall_rules(filename);
|
||||
if not ruleset then return nil, err; end
|
||||
local chain_handlers = process_firewall_rules(ruleset);
|
||||
return chain_handlers;
|
||||
end
|
||||
|
||||
-- Compile handler code into a factory that produces a valid event handler. Factory accepts
|
||||
-- a value to be returned on PASS
|
||||
local function compile_handler(code_string, filename)
|
||||
-- Prepare event handler function
|
||||
local chunk, err = envload(code_string, "="..filename, _G);
|
||||
if not chunk then
|
||||
return nil, "Error compiling (probably a compiler bug, please report): "..err;
|
||||
end
|
||||
local function fire_event(name, data)
|
||||
return module:fire_event(name, data);
|
||||
end
|
||||
local init_ok, initialized_chunk = pcall(chunk);
|
||||
if not init_ok then
|
||||
return nil, "Error initializing compiled rules: "..initialized_chunk;
|
||||
end
|
||||
return function (pass_return)
|
||||
return initialized_chunk(active_definitions, fire_event, logger(filename), module, pass_return); -- Returns event handler with upvalues
|
||||
end
|
||||
end
|
||||
|
||||
local function resolve_script_path(script_path)
|
||||
local relative_to = prosody.paths.config;
|
||||
if script_path:match("^module:") then
|
||||
relative_to = module.path:sub(1, -#("/mod_"..module.name..".lua"));
|
||||
script_path = script_path:match("^module:(.+)$");
|
||||
end
|
||||
return resolve_relative_path(relative_to, script_path);
|
||||
end
|
||||
|
||||
-- [filename] = { last_modified = ..., events_hooked = { [name] = handler } }
|
||||
local loaded_scripts = {};
|
||||
|
||||
function load_script(script)
|
||||
script = resolve_script_path(script);
|
||||
local last_modified = (lfs.attributes(script) or {}).modification or os.time();
|
||||
if loaded_scripts[script] then
|
||||
if loaded_scripts[script].last_modified == last_modified then
|
||||
return; -- Already loaded, and source file hasn't changed
|
||||
end
|
||||
module:log("debug", "Reloading %s", script);
|
||||
-- Already loaded, but the source file has changed
|
||||
-- unload it now, and we'll load the new version below
|
||||
unload_script(script, true);
|
||||
end
|
||||
local chain_functions, err = compile_firewall_rules(script);
|
||||
|
||||
if not chain_functions then
|
||||
module:log("error", "Error compiling %s: %s", script, err or "unknown error");
|
||||
return;
|
||||
end
|
||||
|
||||
-- Loop through the chains in the script, and for each chain attach the compiled code to the
|
||||
-- relevant events, keeping track in events_hooked so we can cleanly unload later
|
||||
local events_hooked = {};
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
local new_handler, err = compile_handler(handler_code, "mod_firewall::"..chain);
|
||||
if not new_handler then
|
||||
module:log("error", "Compilation error for %s: %s", script, err);
|
||||
else
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
local handler = new_handler(chain_definition.pass_return);
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler, chain_definition.priority);
|
||||
end
|
||||
elseif not chain:sub(1, 5) == "user/" then
|
||||
module:log("warn", "Unknown chain %q", chain);
|
||||
end
|
||||
local event_name, handler = "firewall/chains/"..chain, new_handler(false);
|
||||
events_hooked[event_name] = handler;
|
||||
module:hook(event_name, handler);
|
||||
end
|
||||
end
|
||||
loaded_scripts[script] = { last_modified = last_modified, events_hooked = events_hooked };
|
||||
module:log("debug", "Loaded %s", script);
|
||||
end
|
||||
|
||||
--COMPAT w/0.9 (no module:unhook()!)
|
||||
local function module_unhook(event, handler)
|
||||
return module:unhook_object_event((hosts[module.host] or prosody).events, event, handler);
|
||||
end
|
||||
|
||||
function unload_script(script, is_reload)
|
||||
script = resolve_script_path(script);
|
||||
local script_info = loaded_scripts[script];
|
||||
if not script_info then
|
||||
return; -- Script not loaded
|
||||
end
|
||||
local events_hooked = script_info.events_hooked;
|
||||
for event_name, event_handler in pairs(events_hooked) do
|
||||
module_unhook(event_name, event_handler);
|
||||
events_hooked[event_name] = nil;
|
||||
end
|
||||
loaded_scripts[script] = nil;
|
||||
if not is_reload then
|
||||
module:log("debug", "Unloaded %s", script);
|
||||
end
|
||||
end
|
||||
|
||||
-- Given a set of scripts (e.g. from config) figure out which ones need to
|
||||
-- be loaded, which are already loaded but need unloading, and which to reload
|
||||
function load_unload_scripts(script_list)
|
||||
local wanted_scripts = script_list / resolve_script_path;
|
||||
local currently_loaded = set.new(it.to_array(it.keys(loaded_scripts)));
|
||||
local scripts_to_unload = currently_loaded - wanted_scripts;
|
||||
for script in wanted_scripts do
|
||||
-- If the script is already loaded, this is fine - it will
|
||||
-- reload the script for us if the file has changed
|
||||
load_script(script);
|
||||
end
|
||||
for script in scripts_to_unload do
|
||||
unload_script(script);
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
if not prosody.arg then return end -- Don't run in prosodyctl
|
||||
local firewall_scripts = module:get_option_set("firewall_scripts", {});
|
||||
load_unload_scripts(firewall_scripts);
|
||||
-- Replace contents of definitions table (shared) with active definitions
|
||||
for k in it.keys(definitions) do definitions[k] = nil; end
|
||||
for k,v in pairs(active_definitions) do definitions[k] = v; end
|
||||
end
|
||||
|
||||
function module.save()
|
||||
return { active_definitions = active_definitions, loaded_scripts = loaded_scripts };
|
||||
end
|
||||
|
||||
function module.restore(state)
|
||||
active_definitions = state.active_definitions;
|
||||
loaded_scripts = state.loaded_scripts;
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", function ()
|
||||
load_unload_scripts(module:get_option_set("firewall_scripts", {}));
|
||||
end);
|
||||
|
||||
function module.command(arg)
|
||||
if not arg[1] or arg[1] == "--help" then
|
||||
require"util.prosodyctl".show_usage([[mod_firewall <firewall.pfw>]], [[Compile files with firewall rules to Lua code]]);
|
||||
return 1;
|
||||
end
|
||||
local verbose = arg[1] == "-v";
|
||||
if verbose then table.remove(arg, 1); end
|
||||
|
||||
if arg[1] == "test" then
|
||||
table.remove(arg, 1);
|
||||
return module:require("test")(arg);
|
||||
end
|
||||
|
||||
local serialize = require "util.serialization".serialize;
|
||||
if verbose then
|
||||
print("local logger = require \"util.logger\".init;");
|
||||
print();
|
||||
print("local function fire_event(name, data)\n\tmodule:fire_event(name, data)\nend");
|
||||
print();
|
||||
end
|
||||
|
||||
for _, filename in ipairs(arg) do
|
||||
filename = resolve_script_path(filename);
|
||||
print("do -- File "..filename);
|
||||
local chain_functions = assert(compile_firewall_rules(filename));
|
||||
if verbose then
|
||||
print();
|
||||
print("local active_definitions = "..serialize(active_definitions)..";");
|
||||
print();
|
||||
end
|
||||
local c = 0;
|
||||
for chain, handler_code in pairs(chain_functions) do
|
||||
c = c + 1;
|
||||
print("---- Chain "..chain:gsub("_", " "));
|
||||
local chain_func_name = "chain_"..tostring(c).."_"..chain:gsub("%p", "_");
|
||||
if not verbose then
|
||||
print(("%s = %s;"):format(chain_func_name, handler_code:sub(8)));
|
||||
else
|
||||
|
||||
print(("local %s = (%s)(active_definitions, fire_event, logger(%q));"):format(chain_func_name, handler_code:sub(8), filename));
|
||||
print();
|
||||
|
||||
local chain_definition = chains[chain];
|
||||
if chain_definition and chain_definition.type == "event" then
|
||||
for _, event_name in ipairs(chain_definition) do
|
||||
print(("module:hook(%q, %s, %d);"):format(event_name, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
end
|
||||
print(("module:hook(%q, %s, %d);"):format("firewall/chains/"..chain, chain_func_name, chain_definition.priority or 0));
|
||||
end
|
||||
|
||||
print("---- End of chain "..chain);
|
||||
print();
|
||||
end
|
||||
print("end -- End of file "..filename);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Console
|
||||
|
||||
local console_env = module:shared("/*/admin_shell/env");
|
||||
|
||||
console_env.firewall = {};
|
||||
|
||||
function console_env.firewall:mark(user_jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/marked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
timestamp = os.time();
|
||||
}) then
|
||||
return nil, "Mark not set - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User marked";
|
||||
end
|
||||
|
||||
function console_env.firewall:unmark(jid, mark_name)
|
||||
local username, host = jid.split(user_jid);
|
||||
if not username or not hosts[host] then
|
||||
return nil, "Invalid JID supplied";
|
||||
elseif not idsafe(mark_name) then
|
||||
return nil, "Invalid characters in mark name";
|
||||
end
|
||||
if not module:context(host):fire_event("firewall/unmarked/user", {
|
||||
username = session.username;
|
||||
mark = mark_name;
|
||||
}) then
|
||||
return nil, "Mark not removed - is mod_firewall loaded on that host?";
|
||||
end
|
||||
return true, "User unmarked";
|
||||
end
|
||||
75
resources/prosody-plugins/mod_firewall/test.lib.lua
Normal file
75
resources/prosody-plugins/mod_firewall/test.lib.lua
Normal file
@@ -0,0 +1,75 @@
|
||||
-- luacheck: globals load_unload_scripts
|
||||
local set = require "util.set";
|
||||
local ltn12 = require "ltn12";
|
||||
|
||||
local xmppstream = require "util.xmppstream";
|
||||
|
||||
local function stderr(...)
|
||||
io.stderr:write("** ", table.concat({...}, "\t", 1, select("#", ...)), "\n");
|
||||
end
|
||||
|
||||
return function (arg)
|
||||
require "net.http".request = function (url, ex, cb)
|
||||
stderr("Making HTTP request to "..url);
|
||||
local body_table = {};
|
||||
local ok, response_status, response_headers = require "ssl.https".request({
|
||||
url = url;
|
||||
headers = ex.headers;
|
||||
method = ex.body and "POST" or "GET";
|
||||
sink = ltn12.sink.table(body_table);
|
||||
source = ex.body and ltn12.source.string(ex.body) or nil;
|
||||
});
|
||||
stderr("HTTP response "..response_status);
|
||||
cb(table.concat(body_table), response_status, { headers = response_headers });
|
||||
return true;
|
||||
end;
|
||||
|
||||
local stats_dropped, stats_passed = 0, 0;
|
||||
|
||||
load_unload_scripts(set.new(arg));
|
||||
local stream_callbacks = { default_ns = "jabber:client" };
|
||||
|
||||
function stream_callbacks.streamopened(session)
|
||||
session.notopen = nil;
|
||||
end
|
||||
function stream_callbacks.streamclosed()
|
||||
end
|
||||
function stream_callbacks.error(session, error_name, error_message) -- luacheck: ignore 212/session
|
||||
stderr("Fatal error parsing XML stream: "..error_name..": "..tostring(error_message))
|
||||
assert(false);
|
||||
end
|
||||
function stream_callbacks.handlestanza(session, stanza)
|
||||
if not module:fire_event("firewall/chains/deliver", { origin = session, stanza = stanza }) then
|
||||
stats_passed = stats_passed + 1;
|
||||
print(stanza);
|
||||
print("");
|
||||
else
|
||||
stats_dropped = stats_dropped + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local session = { notopen = true };
|
||||
function session.send(stanza)
|
||||
stderr("Reply:", "\n"..tostring(stanza).."\n");
|
||||
end
|
||||
local stream = xmppstream.new(session, stream_callbacks);
|
||||
stream:feed("<stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client'>");
|
||||
local line_count = 0;
|
||||
for line in io.lines() do
|
||||
line_count = line_count + 1;
|
||||
local ok, err = stream:feed(line.."\n");
|
||||
if not ok then
|
||||
stderr("Fatal XML parse error on line "..line_count..": "..err);
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
|
||||
stderr("Summary");
|
||||
stderr("-------");
|
||||
stderr("");
|
||||
stderr(stats_dropped + stats_passed, "processed");
|
||||
stderr(stats_passed, "passed");
|
||||
stderr(stats_dropped, "dropped");
|
||||
stderr(line_count, "input lines");
|
||||
stderr("");
|
||||
end
|
||||
811
resources/prosody-plugins/mod_fmuc.lua
Normal file
811
resources/prosody-plugins/mod_fmuc.lua
Normal file
@@ -0,0 +1,811 @@
|
||||
--- activate under main muc component
|
||||
--- Add the following config under the main muc component
|
||||
--- muc_room_default_presence_broadcast = {
|
||||
--- visitor = false;
|
||||
--- participant = true;
|
||||
--- moderator = true;
|
||||
--- };
|
||||
--- Enable in global modules: 's2s_bidi'
|
||||
--- Make sure 's2s' is not in modules_disabled
|
||||
--- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com), as there
|
||||
--- are certain configs for whitelisted domains and connections that are domain based
|
||||
--- TODO: filter presence from main occupants back to main prosody
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local filters = require 'util.filters';
|
||||
local array = require 'util.array';
|
||||
local set = require 'util.set';
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local ends_with = util.ends_with;
|
||||
local is_vpaas = util.is_vpaas;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local get_focus_occupant = util.get_focus_occupant;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local respond_iq_result = util.respond_iq_result;
|
||||
local table_compare = util.table_compare;
|
||||
|
||||
local PARTICIPANT_PROP_RAISE_HAND = 'jitsi_participant_raisedHand';
|
||||
local PARTICIPANT_PROP_REQUEST_TRANSCRIPTION = 'jitsi_participant_requestingTranscription';
|
||||
local PARTICIPANT_PROP_TRANSLATION_LANG = 'jitsi_participant_translation_language';
|
||||
local TRANSCRIPT_DEFAULT_LANG = module:get_option_string('transcriptions_default_language', 'en');
|
||||
|
||||
-- this is the main virtual host of this vnode
|
||||
local local_domain = module:get_option_string('muc_mapper_domain_base');
|
||||
if not local_domain then
|
||||
module:log('warn', "No 'muc_mapper_domain_base' option set, disabling fmuc plugin");
|
||||
return;
|
||||
end
|
||||
|
||||
-- this is the main virtual host of the main prosody that this vnode serves
|
||||
local main_domain = module:get_option_string('main_domain');
|
||||
if not main_domain then
|
||||
module:log('warn', "No 'main_domain' option set, disabling fmuc plugin");
|
||||
return;
|
||||
end
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
local local_muc_domain = muc_domain_prefix..'.'..local_domain;
|
||||
|
||||
local NICK_NS = 'http://jabber.org/protocol/nick';
|
||||
|
||||
-- in certain cases we consider participants with token as moderators, this is the default behavior which can be turned off
|
||||
local auto_promoted_with_token = module:get_option_boolean('visitors_auto_promoted_with_token', true);
|
||||
|
||||
-- we send stats for the total number of rooms, total number of participants and total number of visitors
|
||||
local measure_rooms = module:measure('vnode-rooms', 'amount');
|
||||
local measure_participants = module:measure('vnode-participants', 'amount');
|
||||
local measure_visitors = module:measure('vnode-visitors', 'amount');
|
||||
|
||||
local sent_iq_cache = require 'util.cache'.new(200);
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
|
||||
local function send_transcriptions_update(room)
|
||||
-- let's notify main prosody
|
||||
local lang_array = array();
|
||||
local count = 0;
|
||||
|
||||
for k, v in pairs(room._transcription_languages) do
|
||||
lang_array:push(v);
|
||||
count = count + 1;
|
||||
end
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
module:send(st.iq({
|
||||
type = 'set',
|
||||
to = 'visitors.'..main_domain,
|
||||
from = local_domain,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
|
||||
:tag('transcription-languages', {
|
||||
xmlns = 'jitsi:visitors',
|
||||
langs = lang_array:unique():sort():concat(','),
|
||||
count = tostring(count)
|
||||
}):up());
|
||||
end
|
||||
|
||||
local function remove_transcription(room, occupant)
|
||||
local send_update = false;
|
||||
if room._transcription_languages then
|
||||
if room._transcription_languages[occupant.jid] then
|
||||
send_update = true;
|
||||
end
|
||||
room._transcription_languages[occupant.jid] = nil;
|
||||
end
|
||||
|
||||
if send_update then
|
||||
send_transcriptions_update(room);
|
||||
end
|
||||
end
|
||||
|
||||
-- if lang is nil we will remove it from the list
|
||||
local function add_transcription(room, occupant, lang)
|
||||
if not room._transcription_languages then
|
||||
room._transcription_languages = {};
|
||||
end
|
||||
|
||||
local old = room._transcription_languages[occupant.jid];
|
||||
room._transcription_languages[occupant.jid] = lang or TRANSCRIPT_DEFAULT_LANG;
|
||||
|
||||
if old ~= room._transcription_languages[occupant.jid] then
|
||||
send_transcriptions_update(room);
|
||||
end
|
||||
end
|
||||
|
||||
-- mark all occupants as visitors
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local occupant, room, origin, stanza = event.occupant, event.room, event.origin, event.stanza;
|
||||
local node, host = jid.split(occupant.bare_jid);
|
||||
local resource = jid.resource(occupant.nick);
|
||||
|
||||
if is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if prosody.hosts[host] then
|
||||
-- local participants which host is defined in this prosody
|
||||
if room._main_room_lobby_enabled then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitors not allowed while lobby is on!')
|
||||
:tag('no-visitors-lobby', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
else
|
||||
occupant.role = 'visitor';
|
||||
end
|
||||
elseif room.moderators_list and room.moderators_list:contains(resource) then
|
||||
-- remote participants, host is the main prosody
|
||||
occupant.role = 'moderator';
|
||||
end
|
||||
end, 3);
|
||||
|
||||
-- if a visitor leaves we want to lower its hand if it was still raised before leaving
|
||||
-- this is to clear indication for promotion on moderators visitors list
|
||||
module:hook('muc-occupant-pre-leave', function (event)
|
||||
local occupant = event.occupant;
|
||||
|
||||
---- we are interested only of visitors presence
|
||||
if occupant.role ~= 'visitor' then
|
||||
return;
|
||||
end
|
||||
|
||||
local room = event.room;
|
||||
|
||||
-- let's check if the visitor has a raised hand send a lower hand
|
||||
-- to main prosody
|
||||
local pr = occupant:get_presence();
|
||||
|
||||
local raiseHand = pr:get_child_text(PARTICIPANT_PROP_RAISE_HAND);
|
||||
|
||||
-- a promotion detected let's send it to main prosody
|
||||
if raiseHand and #raiseHand > 0 then
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local promotion_request = st.iq({
|
||||
type = 'set',
|
||||
to = 'visitors.'..main_domain,
|
||||
from = local_domain,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
|
||||
:tag('promotion-request', {
|
||||
xmlns = 'jitsi:visitors',
|
||||
jid = occupant.jid,
|
||||
time = nil;
|
||||
}):up();
|
||||
|
||||
module:send(promotion_request);
|
||||
end
|
||||
|
||||
remove_transcription(room, occupant);
|
||||
end, 1); -- rate limit is 0
|
||||
|
||||
-- Returns the main participants count and the visitors count
|
||||
local function get_occupant_counts(room)
|
||||
local main_count = 0;
|
||||
local visitors_count = 0;
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if o.role == 'visitor' then
|
||||
visitors_count = visitors_count + 1;
|
||||
elseif not is_admin(o.bare_jid) then
|
||||
main_count = main_count + 1;
|
||||
end
|
||||
end
|
||||
|
||||
return main_count, visitors_count;
|
||||
end
|
||||
|
||||
local function cancel_destroy_timer(room)
|
||||
if room.visitors_destroy_timer then
|
||||
room.visitors_destroy_timer:stop();
|
||||
room.visitors_destroy_timer = nil;
|
||||
end
|
||||
end
|
||||
|
||||
local function destroy_with_conference_ended(room)
|
||||
-- if the room is being destroyed, ignore
|
||||
if room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
cancel_destroy_timer(room);
|
||||
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
|
||||
room:destroy(nil, 'Conference ended.');
|
||||
return true;
|
||||
end
|
||||
|
||||
-- schedules a new destroy timer which will destroy the room if there are no visitors after the timeout
|
||||
local function schedule_destroy_timer(room)
|
||||
cancel_destroy_timer(room);
|
||||
|
||||
room.visitors_destroy_timer = module:add_timer(15, function()
|
||||
-- if the room is being destroyed, ignore
|
||||
if room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
|
||||
if visitors_count == 0 then
|
||||
module:log('info', 'Will destroy:%s main_occupants:%s visitors:%s', room.jid, main_count, visitors_count);
|
||||
room:destroy(nil, 'No visitors.');
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- when occupant is leaving forward presences to jicofo for visitors
|
||||
-- do not check occupant.role as it maybe already reset
|
||||
-- if there are no main occupants or no visitors, destroy the room (give 15 seconds of grace period for reconnections)
|
||||
module:hook('muc-occupant-left', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
local occupant_domain = jid.host(occupant.bare_jid);
|
||||
|
||||
if prosody.hosts[occupant_domain] and not is_admin(occupant.bare_jid) then
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
if not focus_occupant then
|
||||
if not room.destroying then
|
||||
module:log('warn', 'No focus found for %s', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
-- Let's forward unavailable presence to the special jicofo
|
||||
room:route_stanza(st.presence({
|
||||
to = focus_occupant.jid,
|
||||
from = internal_room_jid_match_rewrite(occupant.nick),
|
||||
type = 'unavailable' })
|
||||
:tag('x', { xmlns = 'http://jabber.org/protocol/muc#user' })
|
||||
:tag('item', {
|
||||
affiliation = room:get_affiliation(occupant.bare_jid) or 'none';
|
||||
role = 'none';
|
||||
nick = event.nick;
|
||||
jid = occupant.bare_jid }):up():up());
|
||||
end
|
||||
|
||||
-- if the room is being destroyed, ignore
|
||||
if room.destroying then
|
||||
return;
|
||||
end
|
||||
|
||||
-- if there are no main participants, the main room will be destroyed and
|
||||
-- we can destroy and the visitor one as when jicofo leaves all visitors will reload
|
||||
-- if there are no visitors give them 15 secs to reconnect, if not destroy it
|
||||
local main_count, visitors_count = get_occupant_counts(room);
|
||||
|
||||
if visitors_count == 0 then
|
||||
schedule_destroy_timer(room);
|
||||
end
|
||||
|
||||
if main_count == 0 then
|
||||
destroy_with_conference_ended(room);
|
||||
end
|
||||
end);
|
||||
|
||||
-- forward visitor presences to jicofo
|
||||
-- detects raise hand in visitors presence, this is request for promotion
|
||||
-- detects the requested transcription and its language to send updates for it
|
||||
module:hook('muc-broadcast-presence', function (event)
|
||||
local occupant = event.occupant;
|
||||
|
||||
---- we are interested only of visitors presence to send it to jicofo
|
||||
if occupant.role ~= 'visitor' then
|
||||
return;
|
||||
end
|
||||
|
||||
local room = event.room;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
|
||||
if not focus_occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
local actor, base_presence, nick, reason, x = event.actor, event.stanza, event.nick, event.reason, event.x;
|
||||
local actor_nick;
|
||||
if actor then
|
||||
actor_nick = jid.resource(room:get_occupant_jid(actor));
|
||||
end
|
||||
|
||||
-- create a presence to send it to jicofo, as jicofo is special :)
|
||||
local full_x = st.clone(x.full or x);
|
||||
|
||||
room:build_item_list(occupant, full_x, false, nick, actor_nick, actor, reason);
|
||||
local full_p = st.clone(base_presence):add_child(full_x);
|
||||
full_p.attr.to = focus_occupant.jid;
|
||||
room:route_to_occupant(focus_occupant, full_p);
|
||||
|
||||
local raiseHand = full_p:get_child_text(PARTICIPANT_PROP_RAISE_HAND);
|
||||
-- a promotion detected let's send it to main prosody
|
||||
if raiseHand then
|
||||
local user_id;
|
||||
local group_id;
|
||||
local is_moderator;
|
||||
local session = sessions[occupant.jid];
|
||||
local identity = session and session.jitsi_meet_context_user;
|
||||
|
||||
if is_vpaas(room) and identity then
|
||||
-- in case of moderator in vpaas meeting we want to do auto-promotion
|
||||
local is_vpaas_moderator = identity.moderator;
|
||||
if is_vpaas_moderator == 'true' or is_vpaas_moderator == true then
|
||||
is_moderator = true;
|
||||
end
|
||||
else
|
||||
-- The case with single moderator in the room, we want to report our id
|
||||
-- so we can be auto promoted
|
||||
if identity and identity.id then
|
||||
user_id = session.jitsi_meet_context_user.id;
|
||||
group_id = session.jitsi_meet_context_group;
|
||||
|
||||
if session.auth_token and auto_promoted_with_token then
|
||||
if not session.jitsi_meet_tenant_mismatch or session.jitsi_web_query_prefix == '' then
|
||||
-- non-vpaas and having a token is considered a moderator, and if it is not in '/' tenant
|
||||
-- the tenant from url and token should match
|
||||
is_moderator = true;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local promotion_request = st.iq({
|
||||
type = 'set',
|
||||
to = 'visitors.'..main_domain,
|
||||
from = local_domain,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain) })
|
||||
:tag('promotion-request', {
|
||||
xmlns = 'jitsi:visitors',
|
||||
jid = occupant.jid,
|
||||
time = raiseHand,
|
||||
userId = user_id,
|
||||
groupId = group_id,
|
||||
forcePromote = is_moderator and 'true' or 'false';
|
||||
}):up();
|
||||
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
promotion_request:add_child(nick_element);
|
||||
end
|
||||
|
||||
module:send(promotion_request);
|
||||
end
|
||||
|
||||
local requestTranscriptionValue = full_p:get_child_text(PARTICIPANT_PROP_REQUEST_TRANSCRIPTION);
|
||||
local hasTranscriptionEnabled = room._transcription_languages and room._transcription_languages[occupant.jid];
|
||||
|
||||
-- detect transcription
|
||||
if requestTranscriptionValue == 'true' then
|
||||
local lang = full_p:get_child_text(PARTICIPANT_PROP_TRANSLATION_LANG);
|
||||
|
||||
add_transcription(room, occupant, lang);
|
||||
elseif hasTranscriptionEnabled then
|
||||
remove_transcription(room, occupant, nil);
|
||||
end
|
||||
|
||||
return;
|
||||
end);
|
||||
|
||||
-- listens for responses to the iq sent for requesting promotion and forward it to the visitor
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' then
|
||||
return;
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.from ~= 'visitors.'..main_domain then
|
||||
module:log('warn', 'not from visitors component, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
return;
|
||||
end
|
||||
|
||||
local request_promotion = visitors_iq:get_child('promotion-response');
|
||||
if not request_promotion then
|
||||
return;
|
||||
end
|
||||
|
||||
-- respond with successful receiving the iq
|
||||
respond_iq_result(origin, stanza);
|
||||
|
||||
local req_jid = request_promotion.attr.jid;
|
||||
-- now let's find the occupant and forward the response
|
||||
local occupant = room:get_occupant_by_real_jid(req_jid);
|
||||
|
||||
if occupant then
|
||||
stanza.attr.to = occupant.jid;
|
||||
stanza.attr.from = room.jid;
|
||||
room:route_to_occupant(occupant, stanza);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
--process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
if host == name then
|
||||
callback(module:context(host), host);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[name] == nil then
|
||||
module:log('debug', 'No host/component found, will wait for it: %s', name)
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler('host-activated', process_host);
|
||||
else
|
||||
process_host(name);
|
||||
end
|
||||
end
|
||||
-- if the message received ends with the main domain, these are system messages
|
||||
-- for visitors, let's correct the room name there
|
||||
local function message_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if ends_with(stanza.attr.from, main_domain) then
|
||||
stanza.attr.from = stanza.attr.from:sub(1, -(main_domain:len() + 1))..local_domain;
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(local_domain, function(host_module, host)
|
||||
host_module:hook('iq/host', stanza_handler, 10);
|
||||
host_module:hook('message/full', message_handler);
|
||||
end);
|
||||
|
||||
-- only live chat is supported for visitors
|
||||
module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local from = stanza.attr.from;
|
||||
local occupant_host;
|
||||
|
||||
-- if there is no occupant this is a message from main, probably coming from other vnode
|
||||
if occupant then
|
||||
occupant_host = jid.host(occupant.bare_jid);
|
||||
|
||||
-- we manage nick only for visitors
|
||||
if occupant_host ~= main_domain then
|
||||
-- add to message stanza display name for the visitor
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS })
|
||||
:text('anonymous'):up();
|
||||
end
|
||||
end
|
||||
|
||||
stanza.attr.from = occupant.nick;
|
||||
else
|
||||
stanza.attr.from = jid.join(jid.node(from), module.host);
|
||||
end
|
||||
|
||||
-- let's send it to main chat and rest of visitors here
|
||||
for _, o in room:each_occupant() do
|
||||
-- filter remote occupants
|
||||
if jid.host(o.bare_jid) == local_domain then
|
||||
room:route_to_occupant(o, stanza)
|
||||
end
|
||||
end
|
||||
|
||||
-- send to main participants only messages from local occupants (skip from remote vnodes)
|
||||
if occupant and occupant_host == local_domain then
|
||||
local main_message = st.clone(stanza);
|
||||
main_message.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain);
|
||||
-- make sure we fix the from to be the real jid
|
||||
main_message.attr.from = room_jid_match_rewrite(stanza.attr.from);
|
||||
module:send(main_message);
|
||||
end
|
||||
stanza.attr.from = from; -- something prosody does internally
|
||||
|
||||
return true;
|
||||
end, 55); -- prosody check for visitor's chat is prio 50, we want to override it
|
||||
|
||||
-- Private messaging support for visitors
|
||||
module:hook('muc-private-message', function(event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
local from = stanza.attr.from;
|
||||
local to = stanza.attr.to;
|
||||
local recipient_occupant = room:get_occupant_by_nick(to);
|
||||
local recipient_domain = recipient_occupant and jid.host(recipient_occupant.bare_jid) or nil;
|
||||
local sender_occupant = room:get_occupant_by_nick(from);
|
||||
local sender_domain = sender_occupant and jid.host(sender_occupant.bare_jid) or nil;
|
||||
|
||||
if sender_domain == nil or recipient_domain == nil then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- If both sender and recipient are local (on this vnode)
|
||||
if sender_domain == local_domain and recipient_domain == local_domain then
|
||||
event.origin.send(st.error_reply(event.stanza, 'auth', 'forbidden',
|
||||
'Private messaging between visitors is disabled on visitor nodes'));
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end
|
||||
|
||||
-- If sender is local (visitor node) and recipient is from main prosody, forward to main prosody
|
||||
if sender_domain == local_domain and recipient_domain == main_domain then
|
||||
local original_to = stanza.attr.to;
|
||||
local original_from = stanza.attr.from;
|
||||
|
||||
-- Add nick element for visitor identification
|
||||
-- remove existing nick to avoid forgery
|
||||
stanza:remove_children('nick', NICK_NS);
|
||||
local nick_element = sender_occupant:get_presence():get_child('nick', NICK_NS);
|
||||
if nick_element then
|
||||
stanza:add_child(nick_element);
|
||||
else
|
||||
stanza:tag('nick', { xmlns = NICK_NS }):text('anonymous'):up();
|
||||
end
|
||||
|
||||
-- Forward to main prosody, preserving the resource and original from
|
||||
stanza.attr.to = jid.join(jid.node(room.jid), muc_domain_prefix..'.'..main_domain, jid.resource(to));
|
||||
module:send(stanza);
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end
|
||||
|
||||
-- For main->visitor messages, let the default MUC handler process it
|
||||
-- We don't need to do anything special
|
||||
if sender_domain == main_domain and recipient_domain == local_domain then
|
||||
return; -- Return nothing, let other handlers continue. The default MUC handler will process it.
|
||||
end
|
||||
|
||||
return false; -- Prevent sending the original message and stop further processing
|
||||
end, 100); -- Lower priority to run after other handlers
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
module:hook_global('stats-update', function ()
|
||||
local participants_count, rooms_count, visitors_count = 0, 0, 0;
|
||||
|
||||
-- iterate over all rooms
|
||||
for room in prosody.hosts[module.host].modules.muc.each_room() do
|
||||
rooms_count = rooms_count + 1;
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local _, host = jid.split(o.bare_jid);
|
||||
if prosody.hosts[host] then -- local hosts are visitors (including jigasi)
|
||||
visitors_count = visitors_count + 1;
|
||||
else
|
||||
participants_count = participants_count + 1;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
measure_rooms(rooms_count);
|
||||
measure_visitors(visitors_count);
|
||||
measure_participants(participants_count);
|
||||
end);
|
||||
|
||||
-- we skip it till the main participants are added from the main prosody
|
||||
module:hook('jicofo-unlock-room', function(e)
|
||||
-- we do not block events we fired
|
||||
if e.fmuc_fired then
|
||||
return;
|
||||
end
|
||||
|
||||
return true;
|
||||
end);
|
||||
|
||||
-- handles incoming iq visitors stanzas
|
||||
-- connect - sent after sending all main participant's presences
|
||||
-- disconnect - sent when main room is destroyed or when we receive a 'disconnect-vnode' iq from jicofo
|
||||
-- update - sent on:
|
||||
-- * room secret is changed
|
||||
-- * lobby enabled or disabled
|
||||
-- * initially before connect to report currently joined moderators
|
||||
-- * moderator participant joins main room
|
||||
-- * a participant has been granted moderator rights
|
||||
local function iq_from_main_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' then
|
||||
return;
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.attr.from ~= main_domain then
|
||||
module:log('warn', 'not from main prosody, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found %s in iq_from_main_handler for:%s', room_jid, visitors_iq);
|
||||
return;
|
||||
end
|
||||
|
||||
local node = visitors_iq:get_child('connect');
|
||||
local fire_jicofo_unlock = true;
|
||||
local process_disconnect = false;
|
||||
|
||||
if not node then
|
||||
node = visitors_iq:get_child('update');
|
||||
fire_jicofo_unlock = false;
|
||||
end
|
||||
|
||||
if not node then
|
||||
node = visitors_iq:get_child('disconnect');
|
||||
process_disconnect = true;
|
||||
end
|
||||
|
||||
if not node then
|
||||
return;
|
||||
end
|
||||
|
||||
-- respond with successful receiving the iq
|
||||
respond_iq_result(origin, stanza);
|
||||
|
||||
if process_disconnect then
|
||||
return destroy_with_conference_ended(room);
|
||||
end
|
||||
|
||||
-- if there is password supplied use it
|
||||
-- if this is update it will either set or remove the password
|
||||
room:set_password(node.attr.password);
|
||||
room._data.meetingId = node.attr.meetingId;
|
||||
local createdTimestamp = node.attr.createdTimestamp;
|
||||
room.created_timestamp = createdTimestamp and tonumber(createdTimestamp) or nil;
|
||||
|
||||
if node.attr.lobby == 'true' then
|
||||
room._main_room_lobby_enabled = true;
|
||||
elseif node.attr.lobby == 'false' then
|
||||
room._main_room_lobby_enabled = false;
|
||||
end
|
||||
|
||||
-- read the moderators list
|
||||
room.moderators_list = room.moderators_list or set.new();
|
||||
local moderators = node:get_child('moderators');
|
||||
|
||||
if moderators then
|
||||
for _, child in ipairs(moderators.tags) do
|
||||
if child.name == 'item' then
|
||||
room.moderators_list:add(child.attr.epId);
|
||||
end
|
||||
end
|
||||
|
||||
-- let's check current occupants roles and promote them if needed
|
||||
-- we change only main participants which are not moderators, but participant
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid)
|
||||
and o.role == 'participant'
|
||||
and room.moderators_list:contains(jid.resource(o.nick)) then
|
||||
room:set_affiliation(true, o.bare_jid, 'owner');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local files = node:get_child('files');
|
||||
if files then
|
||||
local received_files = {};
|
||||
for _, child in ipairs(files.tags) do
|
||||
if child.name == 'file' then
|
||||
received_files[child.attr.id] = json.decode(child:get_text());
|
||||
end
|
||||
end
|
||||
|
||||
-- fire events so file sharing component will add/remove files and will notify clients
|
||||
local removed, added = table_compare(room.jitsi_shared_files or {}, received_files)
|
||||
for _, id in ipairs(removed) do
|
||||
module:context(local_domain):fire_event('jitsi-filesharing-remove', {
|
||||
room = room; id = id;
|
||||
});
|
||||
end
|
||||
for _, id in ipairs(added) do
|
||||
module:context(local_domain):fire_event('jitsi-filesharing-add', {
|
||||
room = room; file = received_files[id];
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
if fire_jicofo_unlock then
|
||||
-- everything is connected allow participants to join
|
||||
module:fire_event('jicofo-unlock-room', { room = room; fmuc_fired = true; });
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
module:hook('iq/host', iq_from_main_handler, 10);
|
||||
|
||||
-- Filters presences (if detected) that are with destination the main prosody
|
||||
function filter_stanza(stanza, session)
|
||||
if (stanza.name == 'presence' or stanza.name == 'message') and session.type ~= 'c2s' then
|
||||
-- we clone it so we do not affect broadcast using same stanza, sending it to clients
|
||||
local f_st = st.clone(stanza);
|
||||
f_st.skipMapping = true;
|
||||
return f_st;
|
||||
elseif stanza.name == 'presence' and session.type == 'c2s' and jid.node(stanza.attr.to) == 'focus' then
|
||||
local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
|
||||
if presence_check_status(x, '110') then
|
||||
return stanza; -- no filter
|
||||
end
|
||||
|
||||
-- we want to filter presences to jicofo for the main participants, skipping visitors
|
||||
-- no point of having them, but if it is the one of the first to be sent
|
||||
-- when first visitor is joining can produce the 'No hosts[from_host]' error as we
|
||||
-- rewrite the from, but we need to not do it to be able to filter it later for the s2s
|
||||
if jid.host(room_jid_match_rewrite(stanza.attr.from)) ~= local_muc_domain then
|
||||
return nil; -- returning nil filters the stanza
|
||||
end
|
||||
end
|
||||
return stanza; -- no filter
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it before that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, 2);
|
||||
end
|
||||
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
function route_s2s_stanza(event)
|
||||
local from_host, to_host, stanza = event.from_host, event.to_host, event.stanza;
|
||||
|
||||
if to_host ~= main_domain then
|
||||
return; -- continue with hook listeners
|
||||
end
|
||||
|
||||
if stanza.name == 'message' then
|
||||
if jid.resource(stanza.attr.to) then
|
||||
-- there is no point of delivering messages to main participants individually
|
||||
return true; -- drop it
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if stanza.name == 'presence' then
|
||||
-- we want to leave only unavailable presences to go to main node
|
||||
-- all other presences from jicofo or the main participants there is no point to go to the main node
|
||||
-- they are anyway not handled
|
||||
if stanza.attr.type ~= 'unavailable' then
|
||||
return true; -- drop it
|
||||
end
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- routing to sessions in mod_s2s is -1 and -10, we want to hook before that to make sure to is correct
|
||||
-- or if we want to filter that stanza
|
||||
module:hook("route/remote", route_s2s_stanza, 10);
|
||||
71
resources/prosody-plugins/mod_jibri_session.lua
Normal file
71
resources/prosody-plugins/mod_jibri_session.lua
Normal file
@@ -0,0 +1,71 @@
|
||||
local json = require 'cjson';
|
||||
|
||||
local util = module:require 'util';
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
|
||||
-- This needs to be attached to the main virtual host and the virtual host where jicofo is connected and authenticated.
|
||||
-- The first pass is the iq coming from the client where we get the creator and attach it to the app_data.
|
||||
-- The second pass is jicofo approving that and inviting jibri where we attach the session_id information to app_data
|
||||
local function attachJibriSessionId(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.name == "iq" then
|
||||
local jibri = stanza:get_child('jibri', 'http://jitsi.org/protocol/jibri');
|
||||
if jibri then
|
||||
if jibri.attr.action == 'start' then
|
||||
|
||||
local update_app_data = false;
|
||||
local app_data = jibri.attr.app_data;
|
||||
if app_data then
|
||||
app_data = json.decode(app_data);
|
||||
else
|
||||
app_data = {};
|
||||
end
|
||||
if app_data.file_recording_metadata == nil then
|
||||
app_data.file_recording_metadata = {};
|
||||
end
|
||||
|
||||
if jibri.attr.room then
|
||||
local jibri_room = jibri.attr.room;
|
||||
jibri_room = room_jid_match_rewrite(jibri_room)
|
||||
local room = get_room_from_jid(jibri_room);
|
||||
if room then
|
||||
local conference_details = {};
|
||||
conference_details["session_id"] = room._data.meetingId;
|
||||
app_data.file_recording_metadata.conference_details = conference_details;
|
||||
update_app_data = true;
|
||||
end
|
||||
else
|
||||
-- no room is because the iq received by the initiator in the room
|
||||
local session = event.origin;
|
||||
-- if a token is provided, add data to app_data
|
||||
if session ~= nil then
|
||||
local initiator = {};
|
||||
|
||||
if session.jitsi_meet_context_user ~= nil then
|
||||
initiator.id = session.jitsi_meet_context_user.id;
|
||||
else
|
||||
initiator.id = session.granted_jitsi_meet_context_user_id;
|
||||
end
|
||||
|
||||
initiator.group
|
||||
= session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id;
|
||||
|
||||
app_data.file_recording_metadata.initiator = initiator
|
||||
update_app_data = true;
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if update_app_data then
|
||||
app_data = json.encode(app_data);
|
||||
jibri.attr.app_data = app_data;
|
||||
jibri:up()
|
||||
stanza:up()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('pre-iq/full', attachJibriSessionId);
|
||||
77
resources/prosody-plugins/mod_jiconop.lua
Normal file
77
resources/prosody-plugins/mod_jiconop.lua
Normal file
@@ -0,0 +1,77 @@
|
||||
local st = require "util.stanza";
|
||||
local ext_services = module:depends("external_services");
|
||||
local get_services = ext_services.get_services;
|
||||
local services_xml = ext_services.services_xml;
|
||||
|
||||
-- Jitsi Connection Optimization
|
||||
-- gathers needed information and pushes it with a message to clients
|
||||
-- this way we skip 4 request responses during every client setup
|
||||
|
||||
local main_virtual_host = module:get_option_string('muc_mapper_domain_base');
|
||||
if not main_virtual_host then
|
||||
module:log('warn', 'No muc_mapper_domain_base option set.');
|
||||
return;
|
||||
end
|
||||
|
||||
local shard_name_config = module:get_option_string('shard_name');
|
||||
if shard_name_config then
|
||||
module:add_identity("server", "shard", shard_name_config);
|
||||
end
|
||||
|
||||
local region_name_config = module:get_option_string('region_name');
|
||||
if region_name_config then
|
||||
module:add_identity("server", "region", region_name_config);
|
||||
end
|
||||
|
||||
local release_number_config = module:get_option_string('release_number');
|
||||
if release_number_config then
|
||||
module:add_identity("server", "release", release_number_config);
|
||||
end
|
||||
|
||||
-- we cache the query as server identities will not change dynamically, amd use its clone every time
|
||||
local query_cache;
|
||||
|
||||
-- this is after xmpp-bind, the moment a client has resource and can be contacted
|
||||
module:hook("resource-bind", function (event)
|
||||
local session = event.session;
|
||||
|
||||
if query_cache == nil then
|
||||
-- disco info data / all identity and features
|
||||
local query = st.stanza("query", { xmlns = "http://jabber.org/protocol/disco#info" });
|
||||
local done = {};
|
||||
-- to lod this module in different virtual hosts than the main, make sure we query here for main
|
||||
for _,identity in ipairs(module:context(main_virtual_host):get_host_items("identity")) do
|
||||
local identity_s = identity.category.."\0"..identity.type;
|
||||
if not done[identity_s] then
|
||||
query:tag("identity", identity):up();
|
||||
done[identity_s] = true;
|
||||
end
|
||||
end
|
||||
|
||||
query_cache = query;
|
||||
end
|
||||
|
||||
local query = st.clone(query_cache);
|
||||
|
||||
-- check whether room has lobby enabled and display name is required for those trying to join
|
||||
local lobby_muc_component_config = module:get_option_string('lobby_muc');
|
||||
module:context(lobby_muc_component_config):fire_event('host-disco-info-node',
|
||||
{origin = session; reply = query; node = 'lobbyrooms';});
|
||||
|
||||
-- will add a rename feature for breakout rooms.
|
||||
local breakout_rooms_muc_component_config = module:get_option_string('breakout_rooms_muc');
|
||||
if breakout_rooms_muc_component_config then
|
||||
module:context(breakout_rooms_muc_component_config):fire_event('host-disco-info-node',
|
||||
{origin = session; reply = query; node = 'breakout_rooms';});
|
||||
end
|
||||
|
||||
local stanza = st.message({
|
||||
from = module.host;
|
||||
to = session.full_jid; });
|
||||
stanza:add_child(query):up();
|
||||
|
||||
--- get turnservers and credentials
|
||||
stanza:add_child(services_xml(get_services()));
|
||||
|
||||
session.send(stanza);
|
||||
end);
|
||||
196
resources/prosody-plugins/mod_jitsi_permissions.lua
Normal file
196
resources/prosody-plugins/mod_jitsi_permissions.lua
Normal file
@@ -0,0 +1,196 @@
|
||||
-- this is auto loaded by meeting_id
|
||||
local filters = require 'util.filters';
|
||||
local jid = require 'util.jid';
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local ends_with = util.ends_with;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
if not muc_domain_base then
|
||||
module:log('warn', 'No "muc_domain_base" option set, disabling module.');
|
||||
return ;
|
||||
end
|
||||
|
||||
-- only the visitor prosody has main_domain setting
|
||||
local is_visitor_prosody = module:get_option_string('main_domain') ~= nil;
|
||||
|
||||
-- load it only on the main muc component as it is loaded by muc_meeting_id which is loaded and for the breakout room muc
|
||||
if muc_domain_prefix..'.'..muc_domain_base ~= module.host or is_visitor_prosody then
|
||||
return;
|
||||
end
|
||||
|
||||
local sessions = prosody.full_sessions;
|
||||
local default_permissions;
|
||||
|
||||
local function load_config()
|
||||
default_permissions = module:get_option('jitsi_default_permissions', {
|
||||
livestreaming = true;
|
||||
recording = true;
|
||||
transcription = true;
|
||||
['outbound-call'] = true;
|
||||
['create-polls'] = true;
|
||||
['send-groupchat'] = true;
|
||||
flip = true;
|
||||
});
|
||||
end
|
||||
load_config();
|
||||
|
||||
function process_set_affiliation(event)
|
||||
local actor, affiliation, jid, previous_affiliation, room
|
||||
= event.actor, event.affiliation, event.jid, event.previous_affiliation, event.room;
|
||||
local actor_session = sessions[actor];
|
||||
|
||||
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not previous_affiliation
|
||||
or not actor_session or not actor_session.jitsi_meet_context_features then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant;
|
||||
for _, o in room:each_occupant() do
|
||||
if o.bare_jid == jid then
|
||||
occupant = o;
|
||||
end
|
||||
end
|
||||
|
||||
if not occupant then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant_session = sessions[occupant.jid];
|
||||
if not occupant_session then
|
||||
return;
|
||||
end
|
||||
|
||||
if (previous_affiliation == 'none' or previous_affiliation == 'member') and affiliation == 'owner' then
|
||||
occupant_session.jitsi_meet_context_features = actor_session.jitsi_meet_context_features;
|
||||
if actor_session.jitsi_meet_context_user then
|
||||
occupant_session.granted_jitsi_meet_context_user_id = actor_session.jitsi_meet_context_user['id']
|
||||
or actor_session.granted_jitsi_meet_context_user_id;
|
||||
end
|
||||
occupant_session.granted_jitsi_meet_context_group_id = actor_session.jitsi_meet_context_group
|
||||
or actor_session.granted_jitsi_meet_context_group_id;
|
||||
-- even if token and features are set we may want to re-send permissions
|
||||
occupant_session.force_permissions_update = true;
|
||||
elseif previous_affiliation == 'owner' and ( affiliation == 'member' or affiliation == 'none' ) then
|
||||
occupant_session.granted_jitsi_meet_context_user_id = nil;
|
||||
occupant_session.granted_jitsi_meet_context_group_id = nil;
|
||||
|
||||
-- on revoke
|
||||
if not session.auth_token then
|
||||
occupant_session.jitsi_meet_context_features = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Detects when sending self-presence because of role change
|
||||
-- we can end up here because of the following cases:
|
||||
-- 1. user joins the room and is granted moderator by another moderator or jicofo
|
||||
-- 2. Some module changes the role of the user by using set_affiliation method
|
||||
-- In cases where authentication is 'anonymous', 'jitsi-anonymous', 'internal_hashed', 'internal_plain', 'cyrus' we
|
||||
-- want to send default permissions all to indicate UI that everything is allowed (to not relay on the UI to check
|
||||
-- is participant moderator or not), to allow finer control over the permissions.
|
||||
-- In case the authentication is 'token' based we want to send permissions only if the token of the user does not include
|
||||
-- features in the user.context.
|
||||
-- In case of allowners we want to send the permissions, no matter of the authentication method.
|
||||
-- In case permissions were granted we want to send the granted permissions in all cases except when the user is
|
||||
-- using token that has features pre-defined (authentication is 'token').
|
||||
function filter_stanza(stanza, session)
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local bare_to = jid.bare(stanza.attr.to);
|
||||
if is_admin(bare_to) then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local muc_x = stanza:get_child('x', MUC_NS..'#user');
|
||||
if not muc_x or not presence_check_status(muc_x, '110') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from)));
|
||||
|
||||
if not room or is_healthcheck_room(room.jid) then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if not room.send_default_permissions_to then
|
||||
room.send_default_permissions_to = {};
|
||||
end
|
||||
|
||||
if not session.force_permissions_update then
|
||||
if session.auth_token and session.jitsi_meet_context_features then -- token and features are set so skip
|
||||
room.send_default_permissions_to[bare_to] = nil;
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- we are sending permissions only when becoming a member
|
||||
local is_moderator = false;
|
||||
for item in muc_x:childtags('item') do
|
||||
if item.attr.role == 'moderator' then
|
||||
is_moderator = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if not is_moderator then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if not room.send_default_permissions_to[bare_to] then
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
|
||||
session.force_permissions_update = false;
|
||||
|
||||
if not session.jitsi_meet_context_features then
|
||||
session.jitsi_meet_context_features = default_permissions;
|
||||
end
|
||||
|
||||
room.send_default_permissions_to[bare_to] = nil;
|
||||
|
||||
stanza:tag('permissions', { xmlns='http://jitsi.org/jitmeet' });
|
||||
for k, v in pairs(session.jitsi_meet_context_features) do
|
||||
local val = tostring(v);
|
||||
stanza:tag('p', { name = k, val = val }):up();
|
||||
end
|
||||
stanza:up();
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- we need to indicate that we will send permissions if we need to
|
||||
-- we need to handle granted features and stuff in the pre-set hook so they are unavailable
|
||||
-- when the self presence is set, so we can update the client, the checks
|
||||
-- whether the actor is allowed to set the affiliation are done before pre-set hook is fired
|
||||
module:hook('muc-pre-set-affiliation', function(event)
|
||||
local jid, room = event.jid, event.room;
|
||||
|
||||
if not room.send_default_permissions_to then
|
||||
room.send_default_permissions_to = {};
|
||||
end
|
||||
room.send_default_permissions_to[jid] = true;
|
||||
|
||||
process_set_affiliation(event);
|
||||
end);
|
||||
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0
|
||||
-- allowners is -1 and we need it after that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -2);
|
||||
end
|
||||
|
||||
-- enable filtering presences
|
||||
filters.add_filter_hook(filter_session);
|
||||
33
resources/prosody-plugins/mod_jitsi_session.lua
Normal file
33
resources/prosody-plugins/mod_jitsi_session.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Jitsi session information
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
module:set_global();
|
||||
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local region_header_name = module:get_option_string('region_header_name', 'x_proxy_region');
|
||||
|
||||
-- Extract the following parameters from the URL and set them in the session:
|
||||
-- * previd: for session resumption
|
||||
function init_session(event)
|
||||
local session, request = event.session, event.request;
|
||||
local query = request.url.query;
|
||||
|
||||
if query ~= nil then
|
||||
local params = formdecode(query);
|
||||
|
||||
-- previd is used together with https://modules.prosody.im/mod_smacks.html
|
||||
-- the param is used to find resumed session and re-use anonymous(random) user id
|
||||
session.previd = query and params.previd or nil;
|
||||
|
||||
-- customusername can be used with combination with "pre-jitsi-authentication" event to pre-set a known jid to a session
|
||||
session.customusername = query and params.customusername or nil;
|
||||
|
||||
-- The room name and optional prefix from the web query
|
||||
session.jitsi_web_query_room = params.room;
|
||||
session.jitsi_web_query_prefix = params.prefix or "";
|
||||
end
|
||||
|
||||
session.user_region = request.headers[region_header_name];
|
||||
end
|
||||
|
||||
module:hook_global("bosh-session", init_session, 1);
|
||||
module:hook_global("websocket-session", init_session, 1);
|
||||
32
resources/prosody-plugins/mod_limits_exception.lua
Normal file
32
resources/prosody-plugins/mod_limits_exception.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, 'util.async');
|
||||
|
||||
if not have_async then
|
||||
return;
|
||||
end
|
||||
|
||||
local unlimited_jids = module:get_option_inherited_set("unlimited_jids", {});
|
||||
|
||||
-- rises the limit of the stanza size for the unlimited jids, default is 10MB
|
||||
local unlimited_stanza_size_limit = module:get_option_number("unlimited_size", 10*1024*1024);
|
||||
|
||||
if unlimited_jids:empty() then
|
||||
return;
|
||||
end
|
||||
|
||||
module:hook("authentication-success", function (event)
|
||||
local session = event.session;
|
||||
local jid = session.username .. "@" .. session.host;
|
||||
if unlimited_jids:contains(jid) then
|
||||
if session.conn and session.conn.setlimit then
|
||||
session.conn:setlimit(0);
|
||||
elseif session.throttle then
|
||||
session.throttle = nil;
|
||||
end
|
||||
|
||||
if unlimited_stanza_size_limit and session.stream.set_stanza_size_limit then
|
||||
module:log('info', 'Setting stanza size limits for %s to %s', jid, unlimited_stanza_size_limit)
|
||||
session.stream:set_stanza_size_limit(unlimited_stanza_size_limit);
|
||||
end
|
||||
end
|
||||
end);
|
||||
120
resources/prosody-plugins/mod_log_ringbuffer.lua
Normal file
120
resources/prosody-plugins/mod_log_ringbuffer.lua
Normal file
@@ -0,0 +1,120 @@
|
||||
module:set_global();
|
||||
|
||||
local loggingmanager = require "core.loggingmanager";
|
||||
local format = require "util.format".format;
|
||||
local pposix = require "util.pposix";
|
||||
local rb = require "util.ringbuffer";
|
||||
local queue = require "util.queue";
|
||||
|
||||
local default_timestamp = "%b %d %H:%M:%S ";
|
||||
local max_chunk_size = module:get_option_number("log_ringbuffer_chunk_size", 16384);
|
||||
|
||||
local os_date = os.date;
|
||||
|
||||
local default_filename_template = "{paths.data}/ringbuffer-logs-{pid}-{count}.log";
|
||||
local render_filename = require "util.interpolation".new("%b{}", function (s) return s; end, {
|
||||
yyyymmdd = function (t)
|
||||
return os_date("%Y%m%d", t);
|
||||
end;
|
||||
hhmmss = function (t)
|
||||
return os_date("%H%M%S", t);
|
||||
end;
|
||||
});
|
||||
|
||||
local dump_count = 0;
|
||||
|
||||
local function dump_buffer(dump, filename)
|
||||
dump_count = dump_count + 1;
|
||||
local f, err = io.open(filename, "a+");
|
||||
if not f then
|
||||
module:log("error", "Unable to open output file: %s", err);
|
||||
return;
|
||||
end
|
||||
f:write(("-- Dumping log buffer at %s --\n"):format(os_date(default_timestamp)));
|
||||
dump(f);
|
||||
f:write("-- End of dump --\n\n");
|
||||
f:close();
|
||||
end
|
||||
|
||||
local function get_filename(filename_template)
|
||||
filename_template = filename_template or default_filename_template;
|
||||
return render_filename(filename_template, {
|
||||
paths = prosody.paths;
|
||||
pid = pposix.getpid();
|
||||
count = dump_count;
|
||||
time = os.time();
|
||||
});
|
||||
end
|
||||
|
||||
local function new_buffer(config)
|
||||
local write, dump;
|
||||
|
||||
if config.lines then
|
||||
local buffer = queue.new(config.lines, true);
|
||||
function write(line)
|
||||
buffer:push(line);
|
||||
end
|
||||
function dump(f)
|
||||
-- COMPAT w/0.11 - update to use :consume()
|
||||
for line in buffer.pop, buffer do
|
||||
f:write(line);
|
||||
end
|
||||
end
|
||||
else
|
||||
local buffer_size = config.size or 100*1024;
|
||||
local buffer = rb.new(buffer_size);
|
||||
function write(line)
|
||||
if not buffer:write(line) then
|
||||
if #line > buffer_size then
|
||||
buffer:discard(buffer_size);
|
||||
buffer:write(line:sub(-buffer_size));
|
||||
else
|
||||
buffer:discard(#line);
|
||||
buffer:write(line);
|
||||
end
|
||||
end
|
||||
end
|
||||
function dump(f)
|
||||
local bytes_remaining = buffer:length();
|
||||
while bytes_remaining > 0 do
|
||||
local chunk_size = math.min(bytes_remaining, max_chunk_size);
|
||||
local chunk = buffer:read(chunk_size);
|
||||
if not chunk then
|
||||
return;
|
||||
end
|
||||
f:write(chunk);
|
||||
bytes_remaining = bytes_remaining - chunk_size;
|
||||
end
|
||||
end
|
||||
end
|
||||
return write, dump;
|
||||
end
|
||||
|
||||
local function ringbuffer_log_sink_maker(sink_config)
|
||||
local write, dump = new_buffer(sink_config);
|
||||
|
||||
local timestamps = sink_config.timestamps;
|
||||
|
||||
if timestamps == true or timestamps == nil then
|
||||
timestamps = default_timestamp; -- Default format
|
||||
elseif timestamps then
|
||||
timestamps = timestamps .. " ";
|
||||
end
|
||||
|
||||
local function handler()
|
||||
dump_buffer(dump, sink_config.filename or get_filename(sink_config.filename_template));
|
||||
end
|
||||
|
||||
if sink_config.signal then
|
||||
require "util.signal".signal(sink_config.signal, handler);
|
||||
elseif sink_config.event then
|
||||
module:hook_global(sink_config.event, handler);
|
||||
end
|
||||
|
||||
return function (name, level, message, ...)
|
||||
local line = format("%s%s\t%s\t%s\n", timestamps and os_date(timestamps) or "", name, level, format(message, ...));
|
||||
write(line);
|
||||
end;
|
||||
end
|
||||
|
||||
loggingmanager.register_sink_type("ringbuffer", ringbuffer_log_sink_maker);
|
||||
166
resources/prosody-plugins/mod_measure_message_count.lua
Normal file
166
resources/prosody-plugins/mod_measure_message_count.lua
Normal file
@@ -0,0 +1,166 @@
|
||||
-- Measure the number of messages used in a meeting. Sends amplitude event.
|
||||
-- Needs to be activated under the muc component where the limit needs to be applied (main muc and breakout muc)
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local jid = require 'util.jid';
|
||||
local http = require 'net.http';
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
|
||||
local amplitude_endpoint = module:get_option_string('amplitude_endpoint', 'https://api2.amplitude.com/2/httpapi');
|
||||
local amplitude_api_key = module:get_option_string('amplitude_api_key');
|
||||
|
||||
if not amplitude_api_key then
|
||||
module:log("warn", "No 'amplitude_api_key' option set, disabling amplitude reporting");
|
||||
return
|
||||
end
|
||||
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
local isBreakoutRoom = module.host == 'breakout.' .. muc_domain_base;
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local extract_subdomain = util.extract_subdomain;
|
||||
|
||||
module:log('info', 'Loading measure message count');
|
||||
|
||||
local shard_name = module:context(muc_domain_base):get_option_string('shard_name');
|
||||
local region_name = module:context(muc_domain_base):get_option_string('region_name');
|
||||
local release_number = module:context(muc_domain_base):get_option_string('release_number');
|
||||
local http_headers = {
|
||||
['User-Agent'] = 'Prosody ('..prosody.version..'; '..prosody.platform..')',
|
||||
['Content-Type'] = 'application/json'
|
||||
};
|
||||
|
||||
local inspect = require "inspect"
|
||||
|
||||
function table.clone(t)
|
||||
return {table.unpack(t)}
|
||||
end
|
||||
|
||||
local function event_cb(content_, code_, response_, request_)
|
||||
if code_ == 200 or code_ == 204 then
|
||||
module:log('debug', 'URL Callback: Code %s, Content %s, Request (host %s, path %s, body %s), Response: %s',
|
||||
code_, content_, request_.host, request_.path, inspect(request_.body), inspect(response_));
|
||||
else
|
||||
module:log('warn', 'URL Callback non successful: Code %s, Content %s, Request (%s), Response: %s',
|
||||
code_, content_, inspect(request_), inspect(response_));
|
||||
end
|
||||
end
|
||||
|
||||
function send_event(room)
|
||||
local user_properties = {
|
||||
shard_name = shard_name;
|
||||
region_name = region_name;
|
||||
release_number = release_number;
|
||||
};
|
||||
|
||||
local node = jid.split(room.jid);
|
||||
local subdomain, room_name = extract_subdomain(node);
|
||||
user_properties.tenant = subdomain or '/';
|
||||
user_properties.conference_name = room_name or node;
|
||||
|
||||
local event_properties = {
|
||||
messages_count = room._muc_messages_count or 0;
|
||||
polls_count = room._muc_polls_count or 0;
|
||||
tenant_mismatch = room.jitsi_meet_tenant_mismatch or false;
|
||||
};
|
||||
|
||||
if room.created_timestamp then
|
||||
event_properties.duration = (os.time() * 1000 - room.created_timestamp) / 1000;
|
||||
end
|
||||
|
||||
local event = {
|
||||
api_key = amplitude_api_key;
|
||||
events = {
|
||||
{
|
||||
user_id = room._data.meetingId;
|
||||
device_id = room._data.meetingId;
|
||||
event_type = 'conference_ended';
|
||||
event_properties = event_properties;
|
||||
user_properties = user_properties;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
local request = http.request(amplitude_endpoint, {
|
||||
headers = http_headers,
|
||||
method = "POST",
|
||||
body = cjson_safe.encode(event)
|
||||
}, event_cb);
|
||||
end
|
||||
|
||||
function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
|
||||
if not body then
|
||||
-- we ignore messages without body - lobby, polls ...
|
||||
return;
|
||||
end
|
||||
|
||||
local session = event.origin;
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return;
|
||||
end
|
||||
|
||||
if not room._muc_messages_count then
|
||||
room._muc_messages_count = 0;
|
||||
end
|
||||
|
||||
room._muc_messages_count = room._muc_messages_count + 1;
|
||||
end
|
||||
|
||||
-- Conference ended, send stats
|
||||
function room_destroyed(event)
|
||||
local room, session = event.room, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if isBreakoutRoom then
|
||||
return;
|
||||
end
|
||||
send_event(room);
|
||||
end
|
||||
|
||||
function poll_created(event)
|
||||
local session = event.event.origin;
|
||||
|
||||
-- get room name with tenant and find room.
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
if not room._muc_polls_count then
|
||||
room._muc_polls_count = 0;
|
||||
end
|
||||
|
||||
room._muc_polls_count = room._muc_polls_count + 1;
|
||||
end
|
||||
|
||||
module:hook('message/full', on_message); -- private messages
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
|
||||
module:hook('muc-room-destroyed', room_destroyed, -1);
|
||||
module:hook("muc-occupant-left", function(event)
|
||||
local occupant, room = event.occupant, event.room;
|
||||
local session = event.origin;
|
||||
|
||||
if session and session.jitsi_meet_tenant_mismatch then
|
||||
room.jitsi_meet_tenant_mismatch = true;
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook('poll-created', poll_created);
|
||||
32
resources/prosody-plugins/mod_measure_stanza_counts.lua
Normal file
32
resources/prosody-plugins/mod_measure_stanza_counts.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
module:set_global()
|
||||
|
||||
local filters = require"util.filters";
|
||||
|
||||
local stanzas_in = module:metric(
|
||||
"counter", "received", "",
|
||||
"Stanzas received by Prosody",
|
||||
{ "session_type", "stanza_kind" }
|
||||
)
|
||||
local stanzas_out = module:metric(
|
||||
"counter", "sent", "",
|
||||
"Stanzas sent by prosody",
|
||||
{ "session_type", "stanza_kind" }
|
||||
)
|
||||
|
||||
local stanza_kinds = { message = true, presence = true, iq = true };
|
||||
|
||||
local function rate(metric_family)
|
||||
return function (stanza, session)
|
||||
if stanza.attr and not stanza.attr.xmlns and stanza_kinds[stanza.name] then
|
||||
metric_family:with_labels(session.type, stanza.name):add(1);
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
|
||||
local function measure_stanza_counts(session)
|
||||
filters.add_filter(session, "stanzas/in", rate(stanzas_in));
|
||||
filters.add_filter(session, "stanzas/out", rate(stanzas_out));
|
||||
end
|
||||
|
||||
filters.add_filter_hook(measure_stanza_counts);
|
||||
160
resources/prosody-plugins/mod_muc_allowners.lua
Normal file
160
resources/prosody-plugins/mod_muc_allowners.lua
Normal file
@@ -0,0 +1,160 @@
|
||||
--- activate under the main muc component
|
||||
local filters = require 'util.filters';
|
||||
local jid = require "util.jid";
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid_host = require "util.jid".host;
|
||||
local st = require "util.stanza";
|
||||
local util = module:require "util";
|
||||
local is_admin = util.is_admin;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_moderated = util.is_moderated;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local disable_revoke_owners;
|
||||
|
||||
local function load_config()
|
||||
disable_revoke_owners = module:get_option_boolean("allowners_disable_revoke_owners", false);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- List of the bare_jids of all occupants that are currently joining (went through pre-join) and will be promoted
|
||||
-- as moderators. As pre-join (where added) and joined event (where removed) happen one after another this list should
|
||||
-- have length of 1
|
||||
local joining_moderator_participants = module:shared('moderators/joining_moderator_participants');
|
||||
|
||||
module:hook("muc-room-created", function(event)
|
||||
local room = event.room;
|
||||
|
||||
if room.jitsiMetadata then
|
||||
room.jitsiMetadata.allownersEnabled = true;
|
||||
end
|
||||
end, -2); -- room_metadata should run before this module on -1
|
||||
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local moderated, room_name, subdomain = is_moderated(room.jid);
|
||||
if moderated then
|
||||
local session = event.origin;
|
||||
local token = session.auth_token;
|
||||
|
||||
if not token then
|
||||
module:log('debug', 'skip allowners for non-auth user subdomain:%s room_name:%s', subdomain, room_name);
|
||||
return;
|
||||
end
|
||||
|
||||
if not (room_name == session.jitsi_meet_room or session.jitsi_meet_room == '*') then
|
||||
module:log('debug', 'skip allowners for auth user and non matching room name: %s, jwt room name: %s',
|
||||
room_name, session.jitsi_meet_room);
|
||||
return;
|
||||
end
|
||||
|
||||
if session.jitsi_meet_domain ~= '*' and subdomain ~= session.jitsi_meet_domain then
|
||||
module:log('debug', 'skip allowners for auth user and non matching room subdomain: %s, jwt subdomain: %s',
|
||||
subdomain, session.jitsi_meet_domain);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
-- mark this participant that it will be promoted and is currently joining
|
||||
joining_moderator_participants[occupant.bare_jid] = true;
|
||||
end, 2);
|
||||
|
||||
module:hook("muc-occupant-joined", function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
local promote_to_moderator = joining_moderator_participants[occupant.bare_jid];
|
||||
-- clear it
|
||||
joining_moderator_participants[occupant.bare_jid] = nil;
|
||||
|
||||
if promote_to_moderator ~= nil then
|
||||
room:set_affiliation(true, occupant.bare_jid, "owner");
|
||||
end
|
||||
end, 2);
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
|
||||
-- Filters self-presences to a jid that exist in joining_participants array
|
||||
-- We want to filter those presences where we send first `participant` and just after it `moderator`
|
||||
function filter_stanza(stanza)
|
||||
-- when joining_moderator_participants is empty there is nothing to filter
|
||||
if next(joining_moderator_participants) == nil
|
||||
or not stanza.attr
|
||||
or not stanza.attr.to
|
||||
or stanza.name ~= "presence" then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- we want to filter presences only on this host for allowners and skip anything like lobby etc.
|
||||
local host_from = jid_host(room_jid_match_rewrite(stanza.attr.from));
|
||||
if host_from ~= module.host then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local bare_to = jid_bare(stanza.attr.to);
|
||||
if stanza:get_error() and joining_moderator_participants[bare_to] then
|
||||
-- pre-join succeeded but joined did not so we need to clear cache
|
||||
joining_moderator_participants[bare_to] = nil;
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local muc_x = stanza:get_child('x', MUC_NS..'#user');
|
||||
if not muc_x then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if joining_moderator_participants[bare_to] and presence_check_status(muc_x, '110') then
|
||||
-- skip the local presence for participant
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- skip sending the 'participant' presences to all other people in the room
|
||||
for item in muc_x:childtags('item') do
|
||||
if joining_moderator_participants[jid_bare(item.attr.jid)] then
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it after that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
|
||||
end
|
||||
|
||||
-- enable filtering presences
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
-- filters any attempt to revoke owner rights on non moderated rooms
|
||||
function filter_admin_set_query(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid_bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
local item = stanza.tags[1].tags[1];
|
||||
local _aff = item.attr.affiliation;
|
||||
|
||||
-- if it is a moderated room we skip it
|
||||
if room and is_moderated(room.jid) then
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- any revoking is disabled, everyone should be owners
|
||||
if _aff == 'none' or _aff == 'outcast' or _aff == 'member' then
|
||||
origin.send(st.error_reply(stanza, "auth", "forbidden"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
if not disable_revoke_owners then
|
||||
-- default prosody priority for handling these is -2
|
||||
module:hook("iq-set/bare/http://jabber.org/protocol/muc#admin:query", filter_admin_set_query, 5);
|
||||
module:hook("iq-set/host/http://jabber.org/protocol/muc#admin:query", filter_admin_set_query, 5);
|
||||
end
|
||||
88
resources/prosody-plugins/mod_muc_auth_ban.lua
Normal file
88
resources/prosody-plugins/mod_muc_auth_ban.lua
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Can be used to ban users based on external http service
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ACCESS_MANAGER_URL = module:get_option_string("muc_prosody_jitsi_access_manager_url");
|
||||
if not ACCESS_MANAGER_URL then
|
||||
module:log("warn", "No 'muc_prosody_jitsi_access_manager_url' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
local json = require "cjson.safe";
|
||||
local http = require "net.http";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local ban_check_count = module:measure("muc_auth_ban_check", "rate")
|
||||
local ban_check_users_banned_count = module:measure("muc_auth_ban_users_banned", "rate")
|
||||
|
||||
-- we will cache banned tokens to avoid extra requests
|
||||
-- on destroying session, websocket retries 2 more times before giving up
|
||||
local cache = require "util.cache".new(100);
|
||||
|
||||
local CACHE_DURATION = 5*60; -- 5 mins
|
||||
|
||||
local cache_timer = module:add_timer(CACHE_DURATION, function()
|
||||
for k, v in cache:items() do
|
||||
if socket.gettime() > v + CACHE_DURATION then
|
||||
cache:set(k, nil);
|
||||
end
|
||||
end
|
||||
|
||||
if cache:count() > 0 then
|
||||
-- rescheduling the timer
|
||||
return CACHE_DURATION;
|
||||
end
|
||||
|
||||
-- skipping return value stops the timer
|
||||
end);
|
||||
|
||||
local function shouldAllow(session)
|
||||
local token = session.auth_token;
|
||||
|
||||
if token ~= nil then
|
||||
-- module:log("debug", "Checking whether user should be banned ")
|
||||
|
||||
-- cached tokens are banned
|
||||
if cache:get(token) then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- TODO: do this only for enabled customers
|
||||
ban_check_count();
|
||||
local function cb(content, code, response, request)
|
||||
if code == 200 then
|
||||
|
||||
local r = json.decode(content)
|
||||
if r['access'] ~= nil and r['access'] == false then
|
||||
module:log("info", "User is banned room:%s tenant:%s user_id:%s group:%s",
|
||||
session.jitsi_meet_room, session.jitsi_web_query_prefix,
|
||||
inspect(session.jitsi_meet_context_user), session.jitsi_meet_context_group);
|
||||
|
||||
ban_check_users_banned_count();
|
||||
|
||||
session:close();
|
||||
|
||||
-- if the cache is empty and the timer is not running reschedule it
|
||||
if cache:count() == 0 then
|
||||
cache_timer:reschedule(CACHE_DURATION);
|
||||
end
|
||||
|
||||
cache:set(token, socket.gettime());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local request_headers = {}
|
||||
request_headers['Authorization'] = 'Bearer ' .. token;
|
||||
|
||||
http.request(ACCESS_MANAGER_URL, {
|
||||
headers = request_headers,
|
||||
method = "GET",
|
||||
}, cb);
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
prosody.events.add_handler("jitsi-access-ban-check", function(session)
|
||||
return shouldAllow(session)
|
||||
end)
|
||||
670
resources/prosody-plugins/mod_muc_breakout_rooms.lua
Normal file
670
resources/prosody-plugins/mod_muc_breakout_rooms.lua
Normal file
@@ -0,0 +1,670 @@
|
||||
-- This module is added under the main virtual host domain
|
||||
-- It needs a breakout rooms muc component
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "muc_breakout_rooms"
|
||||
-- }
|
||||
-- breakout_rooms_muc = "breakout.jitmeet.example.com"
|
||||
-- main_muc = "muc.jitmeet.example.com"
|
||||
--
|
||||
-- Component "breakout.jitmeet.example.com" "muc"
|
||||
-- restrict_room_creation = true
|
||||
-- storage = "memory"
|
||||
-- admins = { "focusUser@auth.jitmeet.example.com" }
|
||||
-- muc_room_locking = false
|
||||
-- muc_room_default_public_jids = true
|
||||
--
|
||||
|
||||
module:depends('room_destroy');
|
||||
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, 'util.async');
|
||||
|
||||
if not have_async then
|
||||
module:log('warn', 'Breakout rooms will not work with Prosody version 0.10 or less.');
|
||||
return;
|
||||
end
|
||||
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local jid_host = require 'util.jid'.host;
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local json = require 'cjson.safe';
|
||||
local st = require 'util.stanza';
|
||||
local uuid_gen = require 'util.uuid'.generate;
|
||||
|
||||
local util = module:require 'util';
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local BREAKOUT_ROOMS_IDENTITY_TYPE = 'breakout_rooms';
|
||||
-- Available breakout room functionality
|
||||
local RENAME_FEATURE = 'http://jitsi.org/protocol/breakout_rooms#rename';
|
||||
-- only send at most this often updates on breakout rooms to avoid flooding.
|
||||
local BROADCAST_ROOMS_INTERVAL = .3;
|
||||
-- close conference after this amount of seconds if all leave.
|
||||
local ROOMS_TTL_IF_ALL_LEFT = 5;
|
||||
local JSON_TYPE_ADD_BREAKOUT_ROOM = 'features/breakout-rooms/add';
|
||||
local JSON_TYPE_MOVE_TO_ROOM_REQUEST = 'features/breakout-rooms/move-to-room';
|
||||
local JSON_TYPE_REMOVE_BREAKOUT_ROOM = 'features/breakout-rooms/remove';
|
||||
local JSON_TYPE_RENAME_BREAKOUT_ROOM = 'features/breakout-rooms/rename';
|
||||
local JSON_TYPE_UPDATE_BREAKOUT_ROOMS = 'features/breakout-rooms/update';
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'breakout rooms not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
local breakout_rooms_muc_component_config = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
|
||||
|
||||
module:depends('jitsi_session');
|
||||
|
||||
local breakout_rooms_muc_service;
|
||||
local main_muc_service;
|
||||
|
||||
-- Maps a breakout room jid to the main room jid
|
||||
local main_rooms_map = {};
|
||||
|
||||
-- Utility functions
|
||||
|
||||
function get_main_room_jid(room_jid)
|
||||
local _, host = jid_split(room_jid);
|
||||
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and room_jid
|
||||
or main_rooms_map[room_jid];
|
||||
end
|
||||
|
||||
function get_main_room(room_jid)
|
||||
local main_room_jid = get_main_room_jid(room_jid);
|
||||
|
||||
return main_muc_service.get_room_from_jid(main_room_jid), main_room_jid;
|
||||
end
|
||||
|
||||
function get_room_from_jid(room_jid)
|
||||
local host = jid_host(room_jid);
|
||||
|
||||
return
|
||||
host == main_muc_component_config
|
||||
and main_muc_service.get_room_from_jid(room_jid)
|
||||
or breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
end
|
||||
|
||||
function send_json_msg(to_jid, json_msg)
|
||||
local stanza = st.message({ from = breakout_rooms_muc_component_config; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_msg):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
function get_participants(room)
|
||||
local participants = {};
|
||||
|
||||
if room then
|
||||
for room_nick, occupant in room:each_occupant() do
|
||||
-- Filter focus as we keep it as a hidden participant
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
local display_name = occupant:get_presence():get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
local real_nick = internal_room_jid_match_rewrite(room_nick);
|
||||
participants[real_nick] = {
|
||||
jid = occupant.jid,
|
||||
role = occupant.role,
|
||||
displayName = display_name
|
||||
};
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return participants;
|
||||
end
|
||||
|
||||
function broadcast_breakout_rooms(room_jid)
|
||||
local main_room = get_main_room(room_jid);
|
||||
|
||||
if not main_room or main_room.broadcast_timer then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Only send each BROADCAST_ROOMS_INTERVAL seconds to prevent flooding of messages.
|
||||
main_room.broadcast_timer = module:add_timer(BROADCAST_ROOMS_INTERVAL, function()
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if not main_room then
|
||||
return;
|
||||
end
|
||||
|
||||
main_room.broadcast_timer = nil;
|
||||
|
||||
local real_jid = internal_room_jid_match_rewrite(main_room_jid);
|
||||
local real_node = jid_node(real_jid);
|
||||
local rooms = {
|
||||
[real_node] = {
|
||||
isMainRoom = true,
|
||||
id = real_node,
|
||||
jid = real_jid,
|
||||
name = main_room._data.subject,
|
||||
participants = get_participants(main_room)
|
||||
};
|
||||
}
|
||||
|
||||
for breakout_room_jid, subject in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
local breakout_room_node = jid_node(breakout_room_jid)
|
||||
|
||||
rooms[breakout_room_node] = {
|
||||
id = breakout_room_node,
|
||||
jid = breakout_room_jid,
|
||||
name = subject,
|
||||
participants = {}
|
||||
}
|
||||
|
||||
-- The room may not physically exist yet.
|
||||
if breakout_room then
|
||||
rooms[breakout_room_node].participants = get_participants(breakout_room);
|
||||
end
|
||||
end
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_UPDATE_BREAKOUT_ROOMS,
|
||||
roomCounter = main_room._data.breakout_rooms_counter,
|
||||
rooms = rooms
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'not broadcasting breakout room information room:%s error:%s', main_room_jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
for _, occupant in main_room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
send_json_msg(occupant.jid, json_msg)
|
||||
end
|
||||
end
|
||||
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if room then
|
||||
for _, occupant in room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
send_json_msg(occupant.jid, json_msg)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
|
||||
-- Managing breakout rooms
|
||||
|
||||
function create_breakout_room(orig_room, subject)
|
||||
local main_room, main_room_jid = get_main_room(orig_room.jid);
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid create breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_room_jid = uuid_gen() .. '@' .. breakout_rooms_muc_component_config;
|
||||
|
||||
if not main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms = {};
|
||||
main_room._data.breakout_rooms_counter = 0;
|
||||
end
|
||||
main_room._data.breakout_rooms_counter = main_room._data.breakout_rooms_counter + 1;
|
||||
main_room._data.breakout_rooms[breakout_room_jid] = subject;
|
||||
main_room._data.breakout_rooms_active = true;
|
||||
-- Make room persistent - not to be destroyed - if all participants join breakout rooms.
|
||||
main_room:set_persistent(true);
|
||||
main_room:save(true);
|
||||
|
||||
main_rooms_map[breakout_room_jid] = main_room_jid;
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
function destroy_breakout_room(orig_room, room_jid, message)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid destroy breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
|
||||
if breakout_room then
|
||||
message = message or 'Breakout room removed.';
|
||||
breakout_room:destroy(main_room and main_room_jid or nil, message);
|
||||
end
|
||||
if main_room then
|
||||
if main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms[room_jid] = nil;
|
||||
end
|
||||
main_room:save(true);
|
||||
|
||||
main_rooms_map[room_jid] = nil;
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function rename_breakout_room(orig_room, room_jid, name)
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if room_jid == main_room_jid then
|
||||
return;
|
||||
end
|
||||
|
||||
if orig_room ~= main_room then
|
||||
module:log('warn', 'Invalid rename breakout room request for %s', orig_room.jid);
|
||||
return;
|
||||
end
|
||||
|
||||
if main_room then
|
||||
if main_room._data.breakout_rooms then
|
||||
main_room._data.breakout_rooms[room_jid] = name;
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(room_jid);
|
||||
|
||||
if breakout_room then
|
||||
breakout_room:set_subject(breakout_room.jid, name);
|
||||
end
|
||||
|
||||
end
|
||||
main_room:save(true);
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
-- Handling events
|
||||
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local message = event.stanza:get_child(BREAKOUT_ROOMS_IDENTITY_TYPE);
|
||||
|
||||
if not message then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
|
||||
if not occupant then
|
||||
-- Check if the participant is in any breakout room.
|
||||
for breakout_room_jid in pairs(room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room then
|
||||
occupant = breakout_room:get_occupant_by_real_jid(from);
|
||||
if occupant then
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if message.attr.type == JSON_TYPE_ADD_BREAKOUT_ROOM then
|
||||
create_breakout_room(room, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_REMOVE_BREAKOUT_ROOM then
|
||||
destroy_breakout_room(room, message.attr.breakoutRoomJid);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_RENAME_BREAKOUT_ROOM then
|
||||
rename_breakout_room(room, message.attr.breakoutRoomJid, message.attr.subject);
|
||||
return true;
|
||||
elseif message.attr.type == JSON_TYPE_MOVE_TO_ROOM_REQUEST then
|
||||
local participant_jid = message.attr.participantJid;
|
||||
local target_room_jid = message.attr.roomJid;
|
||||
|
||||
if not room._data.breakout_rooms or not (
|
||||
room._data.breakout_rooms[target_room_jid] or target_room_jid == internal_room_jid_match_rewrite(room.jid))
|
||||
then
|
||||
module:log('warn', 'Invalid breakout room %s for %s', target_room_jid, room.jid);
|
||||
return false
|
||||
end
|
||||
|
||||
local json_msg, error = json.encode({
|
||||
type = BREAKOUT_ROOMS_IDENTITY_TYPE,
|
||||
event = JSON_TYPE_MOVE_TO_ROOM_REQUEST,
|
||||
roomJid = target_room_jid
|
||||
});
|
||||
|
||||
if not json_msg then
|
||||
module:log('error', 'skip sending request room:%s error:%s', room.jid, error);
|
||||
return false
|
||||
end
|
||||
|
||||
send_json_msg(participant_jid, json_msg)
|
||||
return true;
|
||||
end
|
||||
|
||||
-- return error.
|
||||
return false;
|
||||
end
|
||||
|
||||
function on_breakout_room_pre_create(event)
|
||||
local breakout_room = event.room;
|
||||
local main_room, main_room_jid = get_main_room(breakout_room.jid);
|
||||
|
||||
-- Only allow existent breakout rooms to be started.
|
||||
-- Authorisation of breakout rooms is done by their random uuid name
|
||||
if main_room and main_room._data.breakout_rooms and main_room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room:set_subject(breakout_room.jid, main_room._data.breakout_rooms[breakout_room.jid]);
|
||||
else
|
||||
module:log('debug', 'Invalid breakout room %s will not be created.', breakout_room.jid);
|
||||
breakout_room:destroy(main_room_jid, 'Breakout room is invalid.');
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
function on_occupant_joined(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_room, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
if main_room and main_room._data.breakout_rooms_active then
|
||||
if jid_node(event.occupant.jid) ~= 'focus' then
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
-- Prevent closing all rooms if a participant has joined (see on_occupant_left).
|
||||
if main_room.close_timer then
|
||||
main_room.close_timer:stop();
|
||||
main_room.close_timer = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function exist_occupants_in_room(room)
|
||||
if not room then
|
||||
return false;
|
||||
end
|
||||
for _, occupant in room:each_occupant() do
|
||||
if jid_node(occupant.jid) ~= 'focus' then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function exist_occupants_in_rooms(main_room)
|
||||
if exist_occupants_in_room(main_room) then
|
||||
return true;
|
||||
end
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if exist_occupants_in_room(room) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function on_occupant_pre_leave(event)
|
||||
local room, occupant, session, stanza = event.room, event.occupant, event.origin, event.stanza;
|
||||
|
||||
local main_room = get_main_room(room.jid);
|
||||
|
||||
prosody.events.fire_event('jitsi-breakout-occupant-leaving', {
|
||||
room = room; main_room = main_room; occupant = occupant; stanza = stanza; session = session;
|
||||
});
|
||||
end
|
||||
|
||||
function on_occupant_left(event)
|
||||
local room_jid = event.room.jid;
|
||||
|
||||
if is_healthcheck_room(room_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
|
||||
if not main_room then
|
||||
return;
|
||||
end
|
||||
|
||||
if main_room._data.breakout_rooms_active and jid_node(event.occupant.jid) ~= 'focus' then
|
||||
broadcast_breakout_rooms(main_room_jid);
|
||||
end
|
||||
|
||||
-- Close the conference if all left for good.
|
||||
if main_room._data.breakout_rooms_active and not main_room.close_timer and not exist_occupants_in_rooms(main_room) then
|
||||
main_room.close_timer = module:add_timer(ROOMS_TTL_IF_ALL_LEFT, function()
|
||||
-- we need to look up again the room as till the timer is fired, the room maybe already destroyed/recreated
|
||||
-- and we will have the old instance
|
||||
local main_room, main_room_jid = get_main_room(room_jid);
|
||||
if main_room and main_room.close_timer then
|
||||
prosody.events.fire_event("maybe-destroy-room", {
|
||||
room = main_room;
|
||||
reason = 'All occupants left.';
|
||||
caller = module:get_name();
|
||||
});
|
||||
end
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
-- Stop other modules from destroying room if breakout rooms not empty
|
||||
function handle_maybe_destroy_main_room(event)
|
||||
local main_room = event.room;
|
||||
local caller = event.caller;
|
||||
|
||||
if caller == module:get_name() then
|
||||
-- we were the one that requested the deletion. Do not override.
|
||||
return nil; -- stop room destruction
|
||||
end
|
||||
|
||||
-- deletion was requested by another module. Check for break room occupants.
|
||||
for breakout_room_jid, _ in pairs(main_room._data.breakout_rooms or {}) do
|
||||
local breakout_room = breakout_rooms_muc_service.get_room_from_jid(breakout_room_jid);
|
||||
if breakout_room and breakout_room:has_occupant() then
|
||||
module:log('info', 'Suppressing room destroy. Breakout room still occupied %s', breakout_room_jid);
|
||||
return true; -- stop room destruction
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_maybe_destroy_main_room)
|
||||
|
||||
|
||||
function on_main_room_destroyed(event)
|
||||
local main_room = event.room;
|
||||
|
||||
if is_healthcheck_room(main_room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
for breakout_room_jid in pairs(main_room._data.breakout_rooms or {}) do
|
||||
destroy_breakout_room(main_room, breakout_room_jid, event.reason)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Module operations
|
||||
|
||||
-- operates on already loaded breakout rooms muc module
|
||||
function process_breakout_rooms_muc_loaded(breakout_rooms_muc, host_module)
|
||||
module:log('debug', 'Breakout rooms muc loaded');
|
||||
|
||||
-- Advertise the breakout rooms component so clients can pick up the address and use it
|
||||
module:add_identity('component', BREAKOUT_ROOMS_IDENTITY_TYPE, breakout_rooms_muc_component_config);
|
||||
|
||||
-- Tag the disco#info response with available features of breakout rooms.
|
||||
host_module:hook('host-disco-info-node', function (event)
|
||||
local session, reply, node = event.origin, event.reply, event.node;
|
||||
if node == BREAKOUT_ROOMS_IDENTITY_TYPE and session.jitsi_web_query_room then
|
||||
reply:tag('feature', { var = RENAME_FEATURE }):up();
|
||||
end
|
||||
event.exists = true;
|
||||
end);
|
||||
|
||||
breakout_rooms_muc_service = breakout_rooms_muc;
|
||||
module:log("info", "Hook to muc events on %s", breakout_rooms_muc_component_config);
|
||||
host_module:hook('message/host', on_message);
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-pre-create', on_breakout_room_pre_create);
|
||||
host_module:hook('muc-occupant-pre-leave', on_occupant_pre_leave);
|
||||
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
local main_room, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
-- Breakout room metadata.
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_isbreakout';
|
||||
label = 'Is this a breakout room?';
|
||||
type = "boolean";
|
||||
});
|
||||
event.formdata['muc#roominfo_isbreakout'] = true;
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_breakout_main_room';
|
||||
label = 'The main room associated with this breakout room';
|
||||
});
|
||||
event.formdata['muc#roominfo_breakout_main_room'] = internal_room_jid_match_rewrite(main_room_jid);
|
||||
|
||||
-- If the main room has a lobby, make it so this breakout room also uses it.
|
||||
if (main_room and main_room._data.lobbyroom and main_room:get_members_only()) then
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_lobbyroom';
|
||||
label = 'Lobby room jid';
|
||||
});
|
||||
event.formdata['muc#roominfo_lobbyroom'] = main_room._data.lobbyroom;
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook("muc-config-form", function(event)
|
||||
local room = event.room;
|
||||
local _, main_room_jid = get_main_room(room.jid);
|
||||
|
||||
-- Breakout room metadata.
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_isbreakout';
|
||||
label = 'Is this a breakout room?';
|
||||
type = "boolean";
|
||||
value = true;
|
||||
});
|
||||
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_breakout_main_room';
|
||||
label = 'The main room associated with this breakout room';
|
||||
value = internal_room_jid_match_rewrite(main_room_jid);
|
||||
});
|
||||
end);
|
||||
|
||||
local room_mt = breakout_rooms_muc_service.room_mt;
|
||||
|
||||
room_mt.get_members_only = function(room)
|
||||
local main_room = get_main_room(room.jid);
|
||||
|
||||
if not main_room then
|
||||
module:log('error', 'No main room (%s)!', room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
return main_room.get_members_only(main_room)
|
||||
end
|
||||
|
||||
-- we base affiliations (roles) in breakout rooms muc component to be based on the roles in the main muc
|
||||
room_mt.get_affiliation = function(room, jid)
|
||||
local main_room, _ = get_main_room(room.jid);
|
||||
|
||||
if not main_room then
|
||||
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
|
||||
return 'none';
|
||||
end
|
||||
|
||||
-- moderators in main room are moderators here
|
||||
local role = main_room.get_affiliation(main_room, jid);
|
||||
if role then
|
||||
return role;
|
||||
end
|
||||
|
||||
return 'none';
|
||||
end
|
||||
end
|
||||
|
||||
-- process or waits to process the breakout rooms muc component
|
||||
process_host_module(breakout_rooms_muc_component_config, function(host_module, host)
|
||||
module:log('info', 'Breakout rooms component created %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_breakout_rooms_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_breakout_rooms_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
module:log('debug', 'Main muc loaded');
|
||||
|
||||
main_muc_service = main_muc;
|
||||
module:log("info", "Hook to muc events on %s", main_muc_component_config);
|
||||
host_module:hook('muc-occupant-joined', on_occupant_joined);
|
||||
host_module:hook('muc-occupant-left', on_occupant_left);
|
||||
host_module:hook('muc-room-destroyed', on_main_room_destroyed);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
125
resources/prosody-plugins/mod_muc_call.lua
Normal file
125
resources/prosody-plugins/mod_muc_call.lua
Normal file
@@ -0,0 +1,125 @@
|
||||
local jid = require "util.jid"
|
||||
local extract_subdomain = module:require "util".extract_subdomain;
|
||||
|
||||
-- Options and configuration
|
||||
local poltergeist_component = module:get_option_string(
|
||||
"poltergeist_component",
|
||||
module.host
|
||||
);
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log(
|
||||
"warn",
|
||||
"No 'muc_domain_base' option set, unable to send call events."
|
||||
);
|
||||
return
|
||||
end
|
||||
|
||||
-- Status strings that trigger call events.
|
||||
local calling_status = "calling"
|
||||
local busy_status = "busy"
|
||||
local rejected_status = "rejected"
|
||||
local connected_status = "connected"
|
||||
local expired_status = "expired"
|
||||
|
||||
-- url_from_room_jid will determine the url for a conference
|
||||
-- provided a room jid. It is required that muc domain mapping
|
||||
-- is enabled and configured. There are two url formats that are supported.
|
||||
-- The following urls are examples of the supported formats.
|
||||
-- https://meet.jit.si/jitsi/ProductiveMeeting
|
||||
-- https://meet.jit.si/MoreProductiveMeeting
|
||||
-- The urls are derived from portions of the room jid.
|
||||
local function url_from_room_jid(room_jid)
|
||||
local node, _, _ = jid.split(room_jid)
|
||||
if not node then return nil end
|
||||
|
||||
local target_subdomain, target_node = extract_subdomain(node);
|
||||
|
||||
if not(target_node or target_subdomain) then
|
||||
return "https://"..muc_domain_base.."/"..node
|
||||
else
|
||||
return "https://"..muc_domain_base.."/"..target_subdomain.."/"..target_node
|
||||
end
|
||||
end
|
||||
|
||||
-- Listening for all muc presences stanza events. If a presence stanza is from
|
||||
-- a poltergeist then it will be further processed to determine if a call
|
||||
-- event should be triggered. Call events are triggered by status strings
|
||||
-- the status strings supported are:
|
||||
-- -------------------------
|
||||
-- Status | Event Type
|
||||
-- _________________________
|
||||
-- "calling" | INVITE
|
||||
-- "busy" | CANCEL
|
||||
-- "rejected" | CANCEL
|
||||
-- "connected" | CANCEL
|
||||
module:hook(
|
||||
"muc-broadcast-presence",
|
||||
function (event)
|
||||
-- Detect if the presence is for a poltergeist or not.
|
||||
-- FIX ME: luacheck warning 581
|
||||
-- not (x == y)' can be replaced by 'x ~= y' (if neither side is a table or NaN)
|
||||
if not (jid.bare(event.occupant.jid) == poltergeist_component) then
|
||||
return
|
||||
end
|
||||
|
||||
-- A presence stanza is needed in order to trigger any calls.
|
||||
if not event.stanza then
|
||||
return
|
||||
end
|
||||
|
||||
local call_id = event.stanza:get_child_text("call_id")
|
||||
if not call_id then
|
||||
module:log("info", "A call id was not provided in the status.")
|
||||
return
|
||||
end
|
||||
|
||||
local invite = function()
|
||||
local url = assert(url_from_room_jid(event.stanza.attr.from))
|
||||
module:fire_event('jitsi-call-invite', { stanza = event.stanza; url = url; call_id = call_id; });
|
||||
end
|
||||
|
||||
local cancel = function()
|
||||
local url = assert(url_from_room_jid(event.stanza.attr.from))
|
||||
local status = event.stanza:get_child_text("status")
|
||||
module:fire_event('jitsi-call-cancel', {
|
||||
stanza = event.stanza;
|
||||
url = url;
|
||||
reason = string.lower(status);
|
||||
call_id = call_id;
|
||||
});
|
||||
end
|
||||
|
||||
-- If for any reason call_cancel is set to true then a cancel
|
||||
-- is sent regardless of the rest of the presence info.
|
||||
local should_cancel = event.stanza:get_child_text("call_cancel")
|
||||
if should_cancel == "true" then
|
||||
cancel()
|
||||
return
|
||||
end
|
||||
|
||||
local missed = function()
|
||||
cancel()
|
||||
module:fire_event('jitsi-call-missed', { stanza = event.stanza; call_id = call_id; });
|
||||
end
|
||||
|
||||
-- All other call flow actions will require a status.
|
||||
if event.stanza:get_child_text("status") == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local switch = function(status)
|
||||
case = {
|
||||
[calling_status] = function() invite() end,
|
||||
[busy_status] = function() cancel() end,
|
||||
[rejected_status] = function() missed() end,
|
||||
[expired_status] = function() missed() end,
|
||||
[connected_status] = function() cancel() end
|
||||
}
|
||||
if case[status] then case[status]() end
|
||||
end
|
||||
|
||||
switch(event.stanza:get_child_text("status"))
|
||||
end,
|
||||
-101
|
||||
);
|
||||
106
resources/prosody-plugins/mod_muc_census.lua
Normal file
106
resources/prosody-plugins/mod_muc_census.lua
Normal file
@@ -0,0 +1,106 @@
|
||||
-- provides an http endpoint at /room-census that reports list of rooms with the
|
||||
-- number of members and created date in this JSON format:
|
||||
--
|
||||
-- {
|
||||
-- "room_census": [
|
||||
-- {
|
||||
-- "room_name": "<muc name>",
|
||||
-- "participants": <# participants>,
|
||||
-- "created_time": <unix timestamp>,
|
||||
-- },
|
||||
-- ...
|
||||
-- ]
|
||||
-- }
|
||||
--
|
||||
-- to activate, add "muc_census" to the modules_enabled table in prosody.cfg.lua
|
||||
--
|
||||
-- warning: this module is unprotected and intended for server admin use only.
|
||||
-- when enabled, make sure to secure the endpoint at the web server or via
|
||||
-- network filters
|
||||
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
local iterators = require "util.iterators";
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return;
|
||||
end
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
|
||||
local tostring = tostring;
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to "conference"
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local leaked_rooms = 0;
|
||||
|
||||
--- handles request to get number of participants in all rooms
|
||||
-- @return GET response
|
||||
function handle_get_room_census(event)
|
||||
local host_session = prosody.hosts[muc_domain_prefix .. "." .. tostring(module.host)]
|
||||
if not host_session or not host_session.modules.muc then
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
room_data = {}
|
||||
leaked_rooms = 0;
|
||||
for room in host_session.modules.muc.each_room() do
|
||||
if not is_healthcheck_room(room.jid) then
|
||||
local occupants = room._occupants;
|
||||
local participant_count = 0;
|
||||
local missing_connections_count = 0;
|
||||
|
||||
if occupants then
|
||||
for _, o in room:each_occupant() do
|
||||
participant_count = participant_count + 1;
|
||||
|
||||
-- let's check whether that occupant has connection in the full_sessions of prosody
|
||||
-- attempt to detect leaked occupants/rooms.
|
||||
if prosody.full_sessions[o.jid] == nil then
|
||||
missing_connections_count = missing_connections_count + 1;
|
||||
end
|
||||
end
|
||||
participant_count = participant_count - 1; -- subtract focus
|
||||
end
|
||||
|
||||
local leaked = false;
|
||||
if participant_count > 0 and missing_connections_count == participant_count then
|
||||
leaked = true;
|
||||
leaked_rooms = leaked_rooms + 1;
|
||||
end
|
||||
|
||||
table.insert(room_data, {
|
||||
room_name = room.jid;
|
||||
participants = participant_count;
|
||||
created_time = room.created_timestamp;
|
||||
leaked = leaked;
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
census_resp = json.encode({
|
||||
room_census = room_data;
|
||||
});
|
||||
return { status_code = 200; body = census_resp }
|
||||
end
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET room-census"] = function (event) return async_handler_wrapper(event,handle_get_room_census) end;
|
||||
};
|
||||
});
|
||||
end
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
local measure_leaked_rooms = module:measure('leaked_rooms', 'amount');
|
||||
module:hook_global('stats-update', function ()
|
||||
measure_leaked_rooms(leaked_rooms);
|
||||
end);
|
||||
56
resources/prosody-plugins/mod_muc_displayname.lua
Normal file
56
resources/prosody-plugins/mod_muc_displayname.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
--- This module removes identity information from presence stanzas when the
|
||||
--- hideDisplayNameForAll or hideDisplayNameForGuests options are enabled
|
||||
--- for a room.
|
||||
|
||||
--- To be enabled under the main muc component
|
||||
local filters = require 'util.filters';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_admin = util.is_admin;
|
||||
local ends_with = util.ends_with;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
|
||||
-- we need to get the shared resource for joining moderators, as participants are marked as moderators
|
||||
-- after joining which is after the filter for stanza/out, but we need to know will this participant be a moderator
|
||||
local joining_moderator_participants = module:shared('moderators/joining_moderator_participants');
|
||||
|
||||
--- Filter presence sent to non-moderator members of a room when the hideDisplayNameForGuests option is set.
|
||||
function filter_stanza_out(stanza, session)
|
||||
if stanza.name ~= 'presence' or stanza.attr.type == 'error'
|
||||
or stanza.attr.type == 'unavailable' or ends_with(stanza.attr.from, '/focus') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
local shouldFilter = false;
|
||||
|
||||
if room and (room._data.hideDisplayNameForGuests == true or room._data.hideDisplayNameForAll == true) then
|
||||
local occupant = room:get_occupant_by_real_jid(stanza.attr.to);
|
||||
-- don't touch self-presence
|
||||
if occupant and stanza.attr.from ~= internal_room_jid_match_rewrite(occupant.nick) then
|
||||
local isModerator = (occupant.role == 'moderator' or joining_moderator_participants[occupant.bare_jid]);
|
||||
shouldFilter = room._data.hideDisplayNameForAll or not isModerator;
|
||||
end
|
||||
end
|
||||
|
||||
if shouldFilter then
|
||||
return filter_identity_from_presence(stanza);
|
||||
else
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
|
||||
function filter_session(session)
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza_out, -100);
|
||||
end
|
||||
|
||||
function module.load()
|
||||
filters.add_filter_hook(filter_session);
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
filters.remove_filter_hook(filter_session);
|
||||
end
|
||||
107
resources/prosody-plugins/mod_muc_domain_mapper.lua
Normal file
107
resources/prosody-plugins/mod_muc_domain_mapper.lua
Normal file
@@ -0,0 +1,107 @@
|
||||
-- Maps MUC JIDs like room1@muc.foo.example.com to JIDs like [foo]room1@muc.example.com
|
||||
-- Must be loaded on the client host in Prosody
|
||||
|
||||
-- It is recommended to set muc_mapper_domain_base to the main domain being served (example.com)
|
||||
|
||||
local filters = require "util.filters";
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_mapper_domain_base' option set, disabling muc_mapper plugin inactive");
|
||||
return
|
||||
end
|
||||
|
||||
local log_not_allowed_errors = module:get_option_boolean('muc_mapper_log_not_allowed_errors', false);
|
||||
|
||||
local util = module:require "util";
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
|
||||
-- We must filter stanzas in order to hook in to all incoming and outgoing messaging which skips the stanza routers
|
||||
function filter_stanza(stanza, session)
|
||||
if stanza.skipMapping then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if stanza.name == "message" or stanza.name == "iq" or stanza.name == "presence" then
|
||||
-- module:log("debug", "Filtering stanza type %s to %s from %s",stanza.name,stanza.attr.to,stanza.attr.from);
|
||||
if stanza.name == "iq" then
|
||||
local conf = stanza:get_child('conference')
|
||||
if conf then
|
||||
-- module:log("debug", "Filtering stanza conference %s to %s from %s",conf.attr.room,stanza.attr.to,stanza.attr.from);
|
||||
conf.attr.room = room_jid_match_rewrite(conf.attr.room, stanza)
|
||||
end
|
||||
end
|
||||
if stanza.attr.to then
|
||||
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
|
||||
end
|
||||
if stanza.attr.from then
|
||||
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
|
||||
end
|
||||
|
||||
if log_not_allowed_errors and stanza.name == 'presence' and stanza.attr.type == 'error' then
|
||||
local error = stanza:get_child('error');
|
||||
if error and error.attr.type == 'cancel'
|
||||
and error:get_child('not-allowed', 'urn:ietf:params:xml:ns:xmpp-stanzas')
|
||||
and not session.jitsi_not_allowed_logged then
|
||||
session.jitsi_not_allowed_logged = true;
|
||||
session.log('error', 'Not allowed presence %s', stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
return stanza;
|
||||
end
|
||||
|
||||
function filter_session(session)
|
||||
-- module:log("warn", "Session filters applied");
|
||||
filters.add_filter(session, "stanzas/out", filter_stanza);
|
||||
end
|
||||
|
||||
function module.load()
|
||||
if module.reloading then
|
||||
module:log("debug", "Reloading MUC mapper!");
|
||||
else
|
||||
module:log("debug", "First load of MUC mapper!");
|
||||
end
|
||||
filters.add_filter_hook(filter_session);
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
filters.remove_filter_hook(filter_session);
|
||||
end
|
||||
|
||||
|
||||
local function outgoing_stanza_rewriter(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.to then
|
||||
stanza.attr.to = room_jid_match_rewrite(stanza.attr.to, stanza)
|
||||
end
|
||||
end
|
||||
|
||||
local function incoming_stanza_rewriter(event)
|
||||
local stanza = event.stanza;
|
||||
if stanza.attr.from then
|
||||
stanza.attr.from = internal_room_jid_match_rewrite(stanza.attr.from, stanza)
|
||||
end
|
||||
end
|
||||
|
||||
-- The stanza rewriters helper functions are attached for all stanza router hooks
|
||||
local function hook_all_stanzas(handler, host_module, event_prefix)
|
||||
for _, stanza_type in ipairs({ "message", "presence", "iq" }) do
|
||||
for _, jid_type in ipairs({ "host", "bare", "full" }) do
|
||||
host_module:hook((event_prefix or "")..stanza_type.."/"..jid_type, handler);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function add_host(host)
|
||||
module:log("info", "Loading mod_muc_domain_mapper for host %s!", host);
|
||||
local host_module = module:context(host);
|
||||
hook_all_stanzas(incoming_stanza_rewriter, host_module);
|
||||
hook_all_stanzas(outgoing_stanza_rewriter, host_module, "pre-");
|
||||
end
|
||||
|
||||
prosody.events.add_handler("host-activated", add_host);
|
||||
for host in pairs(prosody.hosts) do
|
||||
add_host(host);
|
||||
end
|
||||
113
resources/prosody-plugins/mod_muc_end_meeting.lua
Normal file
113
resources/prosody-plugins/mod_muc_end_meeting.lua
Normal file
@@ -0,0 +1,113 @@
|
||||
-- A global module which can be used as http endpoint to end meetings. The provided token
|
||||
--- in the request is verified whether it has the right to do so.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
module:set_global();
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local starts_with = util.starts_with;
|
||||
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
|
||||
local event_count = module:measure("muc_end_meeting_rate", "rate")
|
||||
local event_count_success = module:measure("muc_end_meeting_success", "rate")
|
||||
|
||||
function verify_token(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function handle_terminate_meeting (event)
|
||||
module:log("info", "Request for terminate meeting received: reqid %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
if not event.request.url.query then
|
||||
return { status_code = 400 };
|
||||
end
|
||||
local params = parse(event.request.url.query);
|
||||
local conference = params["conference"];
|
||||
local room_jid;
|
||||
|
||||
if conference then
|
||||
room_jid = room_jid_match_rewrite(conference)
|
||||
else
|
||||
module:log('warn', "conference param was not provided")
|
||||
return { status_code = 400 };
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
if not verify_token(token, room_jid) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid);
|
||||
if not room then
|
||||
module:log("warn", "Room not found")
|
||||
return { status_code = 404 };
|
||||
else
|
||||
module:log("info", "Destroy room jid %s", room.jid)
|
||||
room:destroy(nil, "The meeting has been terminated")
|
||||
end
|
||||
event_count_success()
|
||||
return { status_code = 200 };
|
||||
end
|
||||
|
||||
|
||||
-- module API called on virtual host added, passing the host module
|
||||
function module.add_host(host_module)
|
||||
if host_module.host == muc_domain_base then
|
||||
-- the main virtual host
|
||||
module:log("info", "Initialize token_util using %s", host_module.host)
|
||||
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
|
||||
if asapKeyServer then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(asapKeyServer)
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /end-meeting on %s", host_module.host);
|
||||
host_module:depends("http");
|
||||
host_module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST end-meeting"] = function(event)
|
||||
return async_handler_wrapper(event, handle_terminate_meeting)
|
||||
end;
|
||||
};
|
||||
});
|
||||
end
|
||||
end
|
||||
27
resources/prosody-plugins/mod_muc_filter_access.lua
Normal file
27
resources/prosody-plugins/mod_muc_filter_access.lua
Normal file
@@ -0,0 +1,27 @@
|
||||
-- Restricts access to a muc component to certain domains
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
-- a list of (authenticated)domains that can access rooms(send presence)
|
||||
local whitelist = module:get_option_set("muc_filter_whitelist");
|
||||
|
||||
if not whitelist then
|
||||
module:log("warn", "No 'muc_filter_whitelist' option set, disabling muc_filter_access, plugin inactive");
|
||||
return
|
||||
end
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
|
||||
local function incoming_presence_filter(event)
|
||||
local stanza = event.stanza;
|
||||
local _, domain, _ = jid_split(stanza.attr.from);
|
||||
|
||||
if not stanza.attr.from or not whitelist:contains(domain) then
|
||||
-- Filter presence
|
||||
module:log("error", "Filtering unauthorized presence: %s", stanza:top_tag());
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
for _, jid_type in ipairs({ "host", "bare", "full" }) do
|
||||
module:hook("presence/"..jid_type, incoming_presence_filter, 2000);
|
||||
end
|
||||
207
resources/prosody-plugins/mod_muc_flip.lua
Normal file
207
resources/prosody-plugins/mod_muc_flip.lua
Normal file
@@ -0,0 +1,207 @@
|
||||
-- Allows flipping device. When a presence contains flip_device tag
|
||||
-- and the used jwt matches the id(session.jitsi_meet_context_user.id) of another user this is indication that the user
|
||||
-- is moving from one device to another. The flip feature should be present and enabled in the token features.
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local oss_util = module:require "util";
|
||||
local is_admin = oss_util.is_admin;
|
||||
local is_healthcheck_room = oss_util.is_healthcheck_room;
|
||||
local process_host_module = oss_util.process_host_module;
|
||||
local inspect = require('inspect');
|
||||
local jid_bare = require "util.jid".bare;
|
||||
local jid = require "util.jid";
|
||||
local MUC_NS = "http://jabber.org/protocol/muc";
|
||||
|
||||
local lobby_host;
|
||||
local lobby_muc_service;
|
||||
|
||||
local lobby_muc_component_config = 'lobby.' .. module:get_option_string("muc_mapper_domain_base");
|
||||
if lobby_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
local function remove_flip_tag(stanza)
|
||||
stanza:maptags(function(tag)
|
||||
if tag and tag.name == "flip_device" then
|
||||
-- module:log("debug", "Removing %s tag from presence stanza!", tag.name);
|
||||
return nil;
|
||||
else
|
||||
return tag;
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
-- Make user that switch devices bypass lobby or password.
|
||||
-- A user is considered to join from another device if the
|
||||
-- id from jwt is the same as another occupant and the presence
|
||||
-- stanza has flip_device tag
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
local session = event.origin;
|
||||
local stanza = event.stanza;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
local flip_device_tag = stanza:get_child("flip_device");
|
||||
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local participants = room._data.participants_details or {};
|
||||
local id = session.jitsi_meet_context_user.id;
|
||||
local first_device_occ_nick = participants[id];
|
||||
if flip_device_tag then
|
||||
if first_device_occ_nick and session.jitsi_meet_context_features.flip and (session.jitsi_meet_context_features.flip == true or session.jitsi_meet_context_features.flip == "true") then
|
||||
room._data.kicked_participant_nick = first_device_occ_nick;
|
||||
room._data.flip_participant_nick = occupant.nick;
|
||||
-- allow participant from flip device to bypass Lobby
|
||||
local occupant_jid = stanza.attr.from;
|
||||
local affiliation = room:get_affiliation(occupant_jid);
|
||||
if not affiliation or affiliation == 'none' or affiliation == 'member' then
|
||||
-- module:log("debug", "Bypass lobby invitee %s", occupant_jid)
|
||||
occupant.role = "participant";
|
||||
room:set_affiliation(true, jid_bare(occupant_jid), "member")
|
||||
room:save_occupant(occupant);
|
||||
end
|
||||
|
||||
if room:get_password() then
|
||||
-- bypass password on the flip device
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
if not join then
|
||||
join = stanza:tag("x", { xmlns = MUC_NS });
|
||||
end
|
||||
local password = join:get_child("password", MUC_NS);
|
||||
if password then
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "password" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end);
|
||||
end
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
elseif not session.jitsi_meet_context_features.flip or session.jitsi_meet_context_features.flip == false or session.jitsi_meet_context_features.flip == "false" then
|
||||
module:log("warn", "Flip device tag present without jwt permission")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
else
|
||||
module:log("warn", "Flip device tag present without user from different device")
|
||||
--remove flip_device tag if somebody wants to abuse this feature
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
-- update authenticated participant list
|
||||
participants[id] = occupant.nick;
|
||||
room._data.participants_details = participants
|
||||
-- module:log("debug", "current details list %s", inspect(participants))
|
||||
else
|
||||
if flip_device_tag then
|
||||
module:log("warn", "Flip device tag present for a guest user")
|
||||
-- remove flip_device tag because a guest want to do a sneaky join
|
||||
remove_flip_tag(stanza)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Kick participant from the the first device from the main room and lobby if applies
|
||||
-- and transfer role from the previous participant, this will take care of the grant
|
||||
-- moderation case
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if room._data.flip_participant_nick and occupant.nick == room._data.flip_participant_nick then
|
||||
-- make joining participant from flip device have the same role and affiliation as for the previous device
|
||||
local kicked_occupant = room:get_occupant_by_nick(room._data.kicked_participant_nick);
|
||||
|
||||
if not kicked_occupant then
|
||||
module:log("info", "Kick participant not found, nick %s from main room jid %s",
|
||||
room._data.kicked_participant_nick, room.jid)
|
||||
return;
|
||||
end
|
||||
|
||||
local initial_affiliation = room:get_affiliation(kicked_occupant.jid) or "member";
|
||||
-- module:log("debug", "Transfer affiliation %s to occupant jid %s", initial_affiliation, occupant.jid)
|
||||
room:set_affiliation(true, occupant.bare_jid, initial_affiliation)
|
||||
if initial_affiliation == "owner" then
|
||||
event.occupant.role = "moderator";
|
||||
elseif initial_affiliation == "member" then
|
||||
event.occupant.role = "participant";
|
||||
end
|
||||
-- Kick participant from the first device from the main room
|
||||
local kicked_participant_node_jid = jid.split(kicked_occupant.jid);
|
||||
module:log("info", "Kick participant jid %s nick %s from main room jid %s", kicked_occupant.jid, room._data.kicked_participant_nick, room.jid)
|
||||
room:set_role(true, room._data.kicked_participant_nick, 'none')
|
||||
room:save_occupant(occupant);
|
||||
-- Kick participant from the first device from the lobby room
|
||||
if room._data.lobbyroom then
|
||||
local lobby_room_jid = room._data.lobbyroom;
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid)
|
||||
for _, occupant in lobby_room:each_occupant() do
|
||||
local node = jid.split(occupant.jid);
|
||||
if kicked_participant_node_jid == node then
|
||||
module:log("info", "Kick participant from lobby %s", occupant.jid)
|
||||
lobby_room:set_role(true, occupant.nick, 'none')
|
||||
end
|
||||
end
|
||||
end
|
||||
event.room._data.flip_participant_nick = nil
|
||||
event.room._data.kicked_participant_nick = nil;
|
||||
end
|
||||
end,-2)
|
||||
|
||||
-- Update the local table after a participant leaves
|
||||
module:hook("muc-occupant-left", function(event)
|
||||
local occupant = event.occupant;
|
||||
local session = event.origin;
|
||||
if is_healthcheck_room(event.room.jid) or is_admin(occupant.bare_jid) then
|
||||
return ;
|
||||
end
|
||||
if session and session.jitsi_meet_context_user and session.jitsi_meet_context_user.id then
|
||||
local id = session.jitsi_meet_context_user.id
|
||||
local participants = event.room._data.participants_details or {};
|
||||
local occupant_left_nick = participants[id]
|
||||
if occupant_left_nick == occupant.nick then
|
||||
participants[id] = nil
|
||||
event.room._data.participants_details = participants
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Add a flip_device tag on the unavailable presence from the kicked participant in order to silent the notifications
|
||||
module:hook('muc-broadcast-presence', function(event)
|
||||
local kicked_participant_nick = event.room._data.kicked_participant_nick
|
||||
local stanza = event.stanza;
|
||||
if kicked_participant_nick and stanza.attr.from == kicked_participant_nick and stanza.attr.type == 'unavailable' then
|
||||
-- module:log("debug", "Add flip_device tag for presence unavailable from occupant nick %s", kicked_participant_nick)
|
||||
stanza:tag("flip_device"):up();
|
||||
end
|
||||
end)
|
||||
|
||||
function process_lobby_muc_loaded(lobby_muc, host_module)
|
||||
module:log('info', 'Lobby muc loaded');
|
||||
lobby_muc_service = lobby_muc;
|
||||
lobby_host = module:context(host_module);
|
||||
end
|
||||
|
||||
-- process or waits to process the lobby muc component
|
||||
process_host_module(lobby_muc_component_config, function(host_module, host)
|
||||
-- lobby muc component created
|
||||
module:log('info', 'Lobby component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_lobby_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
30
resources/prosody-plugins/mod_muc_hide_all.lua
Normal file
30
resources/prosody-plugins/mod_muc_hide_all.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
-- This module makes all MUCs in Prosody unavailable on disco#items query
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local util = module:require 'util';
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
|
||||
module:hook('muc-room-pre-create', function(event)
|
||||
event.room:set_hidden(true);
|
||||
end, -1);
|
||||
|
||||
for _, event_name in pairs {
|
||||
'iq-get/bare/http://jabber.org/protocol/disco#info:query';
|
||||
'iq-get/host/http://jabber.org/protocol/disco#info:query';
|
||||
} do
|
||||
module:hook(event_name, function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid.bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if room then
|
||||
if not room:get_occupant_by_real_jid(stanza.attr.from) then
|
||||
origin.send(st.error_reply(stanza, 'auth', 'forbidden'));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
-- prosody will send item-not-found
|
||||
end, 1) -- make sure we handle it before prosody that uses priority -2 for this
|
||||
end
|
||||
191
resources/prosody-plugins/mod_muc_jigasi_invite.lua
Normal file
191
resources/prosody-plugins/mod_muc_jigasi_invite.lua
Normal file
@@ -0,0 +1,191 @@
|
||||
-- A http endpoint to invite jigasi to a meeting via http endpoint
|
||||
-- jwt is used to validate access
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local jid_split = require "util.jid".split;
|
||||
local hashes = require "util.hashes";
|
||||
local random = require "util.random";
|
||||
local st = require("util.stanza");
|
||||
local json = require 'cjson.safe';
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
-- This module chooses jigasi from the brewery room, so it needs information for the configured brewery
|
||||
local muc_domain = module:get_option_string("muc_internal_domain_base", 'internal.auth.' .. muc_domain_base);
|
||||
|
||||
local jigasi_brewery_room_jid = module:get_option_string("muc_jigasi_brewery_jid", 'jigasibrewery@' .. muc_domain);
|
||||
|
||||
local jigasi_bare_jid = module:get_option_string("muc_jigasi_jid", "jigasi@auth." .. muc_domain_base);
|
||||
local focus_jid = module:get_option_string("muc_jicofo_brewery_jid", jigasi_brewery_room_jid .. "/focus");
|
||||
|
||||
local main_muc_service;
|
||||
local JSON_CONTENT_TYPE = "application/json";
|
||||
|
||||
local event_count = module:measure("muc_invite_jigasi_rate", "rate")
|
||||
local event_count_success = module:measure("muc_invite_jigasi_success", "rate")
|
||||
local ASAP_KEY_SERVER = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
local token_util = module:require "token/util".new(module);
|
||||
if ASAP_KEY_SERVER then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(ASAP_KEY_SERVER)
|
||||
end
|
||||
|
||||
local function invite_jigasi(conference, phone_no)
|
||||
local jigasi_brewery_room = main_muc_service.get_room_from_jid(jigasi_brewery_room_jid);
|
||||
if not jigasi_brewery_room then
|
||||
module:log("error", "Jigasi brewery room not found")
|
||||
return 404, 'Brewery room was not found'
|
||||
end
|
||||
module:log("info", "Invite jigasi from %s to join conference %s and outbound phone_no %s", jigasi_brewery_room.jid, conference, phone_no)
|
||||
|
||||
--select least stressed Jigasi
|
||||
local least_stressed_value = math.huge;
|
||||
local least_stressed_jigasi_jid;
|
||||
for occupant_jid, occupant in jigasi_brewery_room:each_occupant() do
|
||||
local _, _, resource = jid_split(occupant_jid);
|
||||
if resource ~= 'focus' then
|
||||
local occ = occupant:get_presence();
|
||||
local stats_child = occ:get_child("stats", "http://jitsi.org/protocol/colibri")
|
||||
|
||||
local is_sip_jigasi = true;
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'supports_sip' and stats_tag.attr.value == 'false' then
|
||||
is_sip_jigasi = false;
|
||||
end
|
||||
end
|
||||
|
||||
if is_sip_jigasi then
|
||||
for stats_tag in stats_child:children() do
|
||||
if stats_tag.attr.name == 'stress_level' then
|
||||
local stress_level = tonumber(stats_tag.attr.value);
|
||||
module:log("debug", "Stressed level %s %s ", stress_level, occupant_jid)
|
||||
if stress_level < least_stressed_value then
|
||||
least_stressed_jigasi_jid = occupant_jid
|
||||
least_stressed_value = stress_level
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
module:log("debug", "Least stressed jigasi selected jid %s value %s", least_stressed_jigasi_jid, least_stressed_value)
|
||||
if not least_stressed_jigasi_jid then
|
||||
module:log("error", "Cannot invite jigasi from room %s", jigasi_brewery_room.jid)
|
||||
return 404, 'Jigasi not found'
|
||||
end
|
||||
|
||||
-- invite Jigasi to join the conference
|
||||
local _, _, jigasi_res = jid_split(least_stressed_jigasi_jid)
|
||||
local jigasi_full_jid = jigasi_bare_jid .. "/" .. jigasi_res;
|
||||
local stanza_id = hashes.sha256(random.bytes(8), true);
|
||||
|
||||
local invite_jigasi_stanza = st.iq({ xmlns = "jabber:client", type = "set", to = jigasi_full_jid, from = focus_jid, id = stanza_id })
|
||||
:tag("dial", { xmlns = "urn:xmpp:rayo:1", from = "fromnumber", to = phone_no })
|
||||
:tag("header", { xmlns = "urn:xmpp:rayo:1", name = "JvbRoomName", value = conference })
|
||||
|
||||
module:log("debug", "Invite jigasi stanza %s", invite_jigasi_stanza)
|
||||
jigasi_brewery_room:route_stanza(invite_jigasi_stanza);
|
||||
return 200
|
||||
end
|
||||
|
||||
local function is_token_valid(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local function handle_jigasi_invite(event)
|
||||
module:log("debug", "Request for invite jigasi received: reqId %s", event.request.headers["request_id"])
|
||||
event_count()
|
||||
local request = event.request;
|
||||
-- verify access
|
||||
local token = event.request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", conference)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if util.starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if not is_token_valid(token) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
-- verify payload
|
||||
if request.headers.content_type ~= JSON_CONTENT_TYPE
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s or missing payload", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
local payload, error = json.decode(request.body);
|
||||
|
||||
if not payload then
|
||||
module:log('error', 'Cannot decode json error:%s', error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local conference = payload["conference"];
|
||||
local phone_no = payload["phoneNo"];
|
||||
if not conference then
|
||||
module:log("warn", "Missing conference param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
if not phone_no then
|
||||
module:log("warn", "Missing phone no param")
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
--invite jigasi
|
||||
local status_code, error_msg = invite_jigasi(conference, phone_no)
|
||||
|
||||
if not error_msg then
|
||||
event_count_success()
|
||||
return { status_code = 200 }
|
||||
else
|
||||
return { status_code = status_code, body = json.encode({ error = error_msg }) }
|
||||
end
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /invite-jigasi on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST invite-jigasi"] = function(event)
|
||||
return async_handler_wrapper(event, handle_jigasi_invite)
|
||||
end;
|
||||
};
|
||||
});
|
||||
|
||||
process_host_module(muc_domain, function(_, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
main_muc_service = muc_module;
|
||||
module:log('info', 'Found main_muc_service: %s', main_muc_service);
|
||||
else
|
||||
module:log('info', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
main_muc_service = prosody.hosts[host].modules.muc;
|
||||
module:log('info', 'Found(on loaded) main_muc_service: %s', main_muc_service);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
171
resources/prosody-plugins/mod_muc_kick_participant.lua
Normal file
171
resources/prosody-plugins/mod_muc_kick_participant.lua
Normal file
@@ -0,0 +1,171 @@
|
||||
-- http endpoint to kick participants, access is based on provided jwt token
|
||||
-- the correct jigasi we fined based on the display name and the number provided
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local starts_with = util.starts_with;
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling kick check endpoint.");
|
||||
return ;
|
||||
end
|
||||
|
||||
local json_content_type = "application/json";
|
||||
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
local asapKeyServer = module:get_option_string('prosody_password_public_key_repo_url', '');
|
||||
if asapKeyServer == '' then
|
||||
module:log('warn', 'No "prosody_password_public_key_repo_url" option set, disabling kick endpoint.');
|
||||
return ;
|
||||
end
|
||||
|
||||
token_util:set_asap_key_server(asapKeyServer);
|
||||
|
||||
--- Verifies the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided for %s", room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
|
||||
-- Validates the request by checking for required url param room and
|
||||
-- validates the token provided with the request
|
||||
-- @param request - The request to validate.
|
||||
-- @return [error_code, room]
|
||||
local function validate_and_get_room(request)
|
||||
if not request.url.query then
|
||||
module:log("warn", "No query");
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local params = formdecode(request.url.query);
|
||||
local room_name = urlencode(params.room) or "";
|
||||
local subdomain = urlencode(params.prefix) or "";
|
||||
|
||||
if not room_name then
|
||||
module:log("warn", "Missing room param for %s", room_name);
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
|
||||
if token and starts_with(token,'Bearer ') then
|
||||
token = token:sub(8,#token)
|
||||
end
|
||||
|
||||
if not verify_token(token, room_address) then
|
||||
return 403, nil;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found for %s", room_address);
|
||||
return 404, nil;
|
||||
else
|
||||
return 200, room;
|
||||
end
|
||||
end
|
||||
|
||||
function handle_kick_participant (event)
|
||||
local request = event.request;
|
||||
if request.headers.content_type ~= json_content_type
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local params, error = json.decode(request.body);
|
||||
if not params then
|
||||
module:log("warn", "Missing params error:%s", error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local number = params["number"];
|
||||
local participantId = params["participantId"];
|
||||
|
||||
if (not number and not participantId) or (number and participantId) then
|
||||
module:log("warn", "Invalid parameters: exactly one of 'number' or 'participantId' must be provided.");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local error_code, room = validate_and_get_room(request);
|
||||
|
||||
if error_code and error_code ~= 200 then
|
||||
module:log("error", "Error validating %s", error_code);
|
||||
return { error_code = 400; }
|
||||
end
|
||||
|
||||
if not room then
|
||||
return { status_code = 404; }
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
local pr = occupant:get_presence();
|
||||
|
||||
if is_participant_match(pr, number, participantId) then
|
||||
room:set_role(true, occupant.nick, nil);
|
||||
module:log('info', 'Occupant kicked %s from %s', occupant.nick, room.jid);
|
||||
return { status_code = 200; }
|
||||
end
|
||||
end
|
||||
|
||||
-- not found participant to kick
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
function is_participant_match(pr, number, participantId)
|
||||
if number then
|
||||
local displayName = pr:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
return is_sip_jigasi(pr) and displayName and starts_with(displayName, number);
|
||||
elseif participantId then
|
||||
local from = pr.attr.from;
|
||||
local _, _, from_resource = jid.split(from);
|
||||
if from_resource then
|
||||
return from_resource == participantId;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
module:log("info","Adding http handler for /kick-participant on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["PUT kick-participant"] = function (event) return async_handler_wrapper(event, handle_kick_participant) end;
|
||||
};
|
||||
});
|
||||
103
resources/prosody-plugins/mod_muc_limit_messages.lua
Normal file
103
resources/prosody-plugins/mod_muc_limit_messages.lua
Normal file
@@ -0,0 +1,103 @@
|
||||
-- A module to limit the number of messages in a meeting
|
||||
-- Needs to be activated under the muc component where the limit needs to be applied
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local id = require 'util.id';
|
||||
local st = require 'util.stanza';
|
||||
|
||||
local get_room_by_name_and_subdomain = module:require 'util'.get_room_by_name_and_subdomain;
|
||||
|
||||
local count;
|
||||
local check_token;
|
||||
|
||||
local function load_config()
|
||||
count = module:get_option_number('muc_limit_messages_count');
|
||||
check_token = module:get_option_boolean('muc_limit_messages_check_token', false);
|
||||
end
|
||||
load_config();
|
||||
|
||||
if not count then
|
||||
module:log('warn', "No 'muc_limit_messages_count' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
module:log('info', 'Loaded muc limits for %s, limit:%s, will check for authenticated users:%s',
|
||||
module.host, count, check_token);
|
||||
|
||||
local error_text = 'The message limit for the room has been reached. Messaging is now disabled.';
|
||||
|
||||
function on_message(event)
|
||||
local stanza = event.stanza;
|
||||
local body = stanza:get_child('body');
|
||||
-- we ignore any non groupchat message without a body
|
||||
if not body then
|
||||
if stanza.attr.type ~= 'groupchat' then -- lobby messages
|
||||
return;
|
||||
else
|
||||
-- we want to pass through only polls answers
|
||||
local json_data = stanza:get_child_text('json-message', 'http://jitsi.org/jitmeet');
|
||||
if json_data and string.find(json_data, 'answer-poll', 1, true) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local session = event.origin;
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
-- if this is a message from visitor, pass it through. Limits are applied in the visitor node.
|
||||
if event.origin.type == 's2sin' then
|
||||
return;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get room name with tenant and find room
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
if check_token and session.auth_token then
|
||||
-- there is an authenticated participant drop all limits
|
||||
room._muc_messages_limit = false;
|
||||
end
|
||||
|
||||
if room._muc_messages_limit == false then
|
||||
-- no limits for this room, just skip
|
||||
return;
|
||||
end
|
||||
|
||||
if not room._muc_messages_limit_count then
|
||||
room._muc_messages_limit_count = 0;
|
||||
end
|
||||
|
||||
room._muc_messages_limit_count = room._muc_messages_limit_count + 1;
|
||||
|
||||
-- on the first message above the limit we set the limit and we send an announcement to the room
|
||||
if room._muc_messages_limit_count == count + 1 then
|
||||
module:log('warn', 'Room message limit reached: %s', room.jid);
|
||||
|
||||
-- send a message to the room
|
||||
local announcement = st.message({ from = room.jid, type = 'groupchat', id = id.medium(), })
|
||||
:tag('body'):text(error_text);
|
||||
room:broadcast_message(announcement);
|
||||
|
||||
room._muc_messages_limit = true;
|
||||
end
|
||||
|
||||
if room._muc_messages_limit == true then
|
||||
-- return error to the sender of this message
|
||||
event.origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', error_text));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- handle messages sent in the component
|
||||
-- 'message/host' is used for breakout rooms
|
||||
module:hook('message/full', on_message); -- private messages
|
||||
module:hook('message/bare', on_message); -- room messages
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
664
resources/prosody-plugins/mod_muc_lobby_rooms.lua
Normal file
664
resources/prosody-plugins/mod_muc_lobby_rooms.lua
Normal file
@@ -0,0 +1,664 @@
|
||||
-- This module added under the main virtual host domain
|
||||
-- It needs a lobby muc component
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "muc_lobby_rooms"
|
||||
-- }
|
||||
-- lobby_muc = "lobby.jitmeet.example.com"
|
||||
-- main_muc = "conference.jitmeet.example.com"
|
||||
--
|
||||
-- Component "lobby.jitmeet.example.com" "muc"
|
||||
-- storage = "memory"
|
||||
-- muc_room_cache_size = 1000
|
||||
-- restrict_room_creation = true
|
||||
-- muc_room_locking = false
|
||||
-- muc_room_default_public_jids = true
|
||||
--
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, 'util.async');
|
||||
|
||||
if not have_async then
|
||||
module:log('warn', 'Lobby rooms will not work with Prosody version 0.10 or less.');
|
||||
return;
|
||||
end
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
local jid_prep = require "util.jid".prep;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local resourceprep = require "util.encodings".stringprep.resourceprep;
|
||||
local json = require 'cjson.safe';
|
||||
local filters = require 'util.filters';
|
||||
local st = require 'util.stanza';
|
||||
local muc_util = module:require "muc/util";
|
||||
local valid_affiliations = muc_util.valid_affiliations;
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
local MUC_USER_NS = 'http://jabber.org/protocol/muc#user';
|
||||
local DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info';
|
||||
local DISPLAY_NAME_REQUIRED_FEATURE = 'http://jitsi.org/protocol/lobbyrooms#displayname_required';
|
||||
local LOBBY_IDENTITY_TYPE = 'lobbyrooms';
|
||||
local NOTIFY_JSON_MESSAGE_TYPE = 'lobby-notify';
|
||||
local NOTIFY_LOBBY_ENABLED = 'LOBBY-ENABLED';
|
||||
local NOTIFY_LOBBY_ACCESS_GRANTED = 'LOBBY-ACCESS-GRANTED';
|
||||
local NOTIFY_LOBBY_ACCESS_DENIED = 'LOBBY-ACCESS-DENIED';
|
||||
|
||||
local util = module:require "util";
|
||||
local ends_with = util.ends_with;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
local lobby_muc_component_config = module:get_option_string('lobby_muc');
|
||||
if lobby_muc_component_config == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
local whitelist;
|
||||
local check_display_name_required;
|
||||
local function load_config()
|
||||
whitelist = module:get_option_set('muc_lobby_whitelist', {});
|
||||
check_display_name_required
|
||||
= module:get_option_boolean('muc_lobby_check_display_name_required', true);
|
||||
end
|
||||
load_config();
|
||||
|
||||
local lobby_muc_service;
|
||||
local main_muc_service;
|
||||
|
||||
function broadcast_json_msg(room, from, json_msg)
|
||||
json_msg.type = NOTIFY_JSON_MESSAGE_TYPE;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if occupant then
|
||||
local json_msg_str, error = json.encode(json_msg);
|
||||
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Error broadcasting message room:%s', room.jid, error);
|
||||
return;
|
||||
end
|
||||
|
||||
room:broadcast_message(
|
||||
st.message({ type = 'groupchat', from = occupant.nick })
|
||||
:tag('json-message', {xmlns='http://jitsi.org/jitmeet'})
|
||||
:text(json_msg_str):up());
|
||||
end
|
||||
end
|
||||
|
||||
-- Sends a json message notifying for lobby enabled/disable
|
||||
-- the message from is the actor that did the operation
|
||||
function notify_lobby_enabled(room, actor, value)
|
||||
broadcast_json_msg(room, actor, {
|
||||
event = NOTIFY_LOBBY_ENABLED,
|
||||
value = value
|
||||
});
|
||||
end
|
||||
|
||||
-- Sends a json message notifying that the jid was granted/denied access in lobby
|
||||
-- the message from is the actor that did the operation
|
||||
function notify_lobby_access(room, actor, jid, display_name, granted)
|
||||
local notify_json = {
|
||||
value = jid,
|
||||
name = display_name
|
||||
};
|
||||
if granted then
|
||||
notify_json.event = NOTIFY_LOBBY_ACCESS_GRANTED;
|
||||
else
|
||||
notify_json.event = NOTIFY_LOBBY_ACCESS_DENIED;
|
||||
end
|
||||
|
||||
broadcast_json_msg(room, actor, notify_json);
|
||||
end
|
||||
|
||||
function filter_stanza(stanza)
|
||||
if not stanza.attr or not stanza.attr.from or not main_muc_service or not lobby_muc_service then
|
||||
return stanza;
|
||||
end
|
||||
-- Allow self-presence (code=110)
|
||||
local node, from_domain = jid_split(stanza.attr.from);
|
||||
|
||||
if from_domain == lobby_muc_component_config then
|
||||
if stanza.name == 'presence' then
|
||||
local muc_x = stanza:get_child('x', MUC_NS..'#user');
|
||||
if not muc_x or presence_check_status(muc_x, '110') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local lobby_room_jid = jid_bare(stanza.attr.from);
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if not lobby_room then
|
||||
module:log('warn', 'No lobby room found %s', lobby_room_jid);
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- check is an owner, only owners can receive the presence
|
||||
-- do not forward presence of owners (other than unavailable)
|
||||
local room = main_muc_service.get_room_from_jid(jid_bare(node .. '@' .. main_muc_component_config));
|
||||
local item = muc_x:get_child('item');
|
||||
if not room
|
||||
or stanza.attr.type == 'unavailable'
|
||||
or (room.get_affiliation(room, stanza.attr.to) == 'owner'
|
||||
and room.get_affiliation(room, item.attr.jid) ~= 'owner') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
|
||||
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
|
||||
if not from_occupant then
|
||||
if is_to_moderator then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
module:log('warn', 'No lobby occupant found %s', stanza.attr.from);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local from_real_jid;
|
||||
for real_jid in from_occupant:each_session() do
|
||||
from_real_jid = real_jid;
|
||||
end
|
||||
|
||||
if is_to_moderator and lobby_room:get_affiliation(from_real_jid) ~= 'owner' then
|
||||
return stanza;
|
||||
end
|
||||
elseif stanza.name == 'iq' and stanza:get_child('query', DISCO_INFO_NS) then
|
||||
-- allow disco info from the lobby component
|
||||
return stanza;
|
||||
elseif stanza.name == 'message' then
|
||||
-- allow messages to or from moderator
|
||||
local lobby_room_jid = jid_bare(stanza.attr.from);
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
|
||||
if not lobby_room then
|
||||
module:log('warn', 'No lobby room found %s', stanza.attr.from);
|
||||
return nil;
|
||||
end
|
||||
|
||||
local is_to_moderator = lobby_room:get_affiliation(stanza.attr.to) == 'owner';
|
||||
local from_occupant = lobby_room:get_occupant_by_nick(stanza.attr.from);
|
||||
|
||||
local from_real_jid;
|
||||
if from_occupant then
|
||||
for real_jid in from_occupant:each_session() do
|
||||
from_real_jid = real_jid;
|
||||
end
|
||||
end
|
||||
|
||||
if not from_real_jid then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local is_from_moderator = lobby_room:get_affiliation(from_real_jid) == 'owner';
|
||||
|
||||
if is_to_moderator or is_from_moderator then
|
||||
return stanza;
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
return nil;
|
||||
else
|
||||
return stanza;
|
||||
end
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0, and we need it after that
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -1);
|
||||
end
|
||||
|
||||
-- actor can be null if called from backend (another module using hook create-lobby-room)
|
||||
function attach_lobby_room(room, actor)
|
||||
local node = jid_split(room.jid);
|
||||
local lobby_room_jid = node .. '@' .. lobby_muc_component_config;
|
||||
if not lobby_muc_service.get_room_from_jid(lobby_room_jid) then
|
||||
local new_room = lobby_muc_service.create_room(lobby_room_jid);
|
||||
-- set persistent the lobby room to avoid it to be destroyed
|
||||
-- there are cases like when selecting new moderator after the current one leaves
|
||||
-- which can leave the room with no occupants and it will be destroyed and we want to
|
||||
-- avoid lobby destroy while it is enabled
|
||||
new_room:set_persistent(true);
|
||||
module:log("info","Lobby room jid = %s created from:%s", lobby_room_jid, actor);
|
||||
new_room.main_room = room;
|
||||
room._data.lobbyroom = new_room.jid;
|
||||
room:save(true);
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- destroys lobby room for the supplied main room
|
||||
function destroy_lobby_room(room, newjid, message)
|
||||
if not message then
|
||||
message = 'Lobby room closed.';
|
||||
end
|
||||
if lobby_muc_service and room and room._data.lobbyroom then
|
||||
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
|
||||
if lobby_room_obj then
|
||||
lobby_room_obj:set_persistent(false);
|
||||
lobby_room_obj:destroy(newjid, message);
|
||||
end
|
||||
room._data.lobbyroom = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- This is a copy of the function(handle_admin_query_set_command) from prosody 12 (d7857ef7843a)
|
||||
function handle_admin_query_set_command_item(self, origin, stanza, item)
|
||||
if not item then
|
||||
origin.send(st.error_reply(stanza, "cancel", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
if item.attr.jid then -- Validate provided JID
|
||||
item.attr.jid = jid_prep(item.attr.jid);
|
||||
if not item.attr.jid then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed"));
|
||||
return true;
|
||||
elseif jid_resource(item.attr.jid) then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed", "Bare JID expected, got full JID"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
if item.attr.nick then -- Validate provided nick
|
||||
item.attr.nick = resourceprep(item.attr.nick);
|
||||
if not item.attr.nick then
|
||||
origin.send(st.error_reply(stanza, "modify", "jid-malformed", "invalid nickname"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
if not item.attr.jid and item.attr.nick then
|
||||
-- COMPAT Workaround for Miranda sending 'nick' instead of 'jid' when changing affiliation
|
||||
local occupant = self:get_occupant_by_nick(self.jid.."/"..item.attr.nick);
|
||||
if occupant then item.attr.jid = occupant.bare_jid; end
|
||||
elseif item.attr.role and not item.attr.nick and item.attr.jid then
|
||||
-- Role changes should use nick, but we have a JID so pull the nick from that
|
||||
local nick = self:get_occupant_jid(item.attr.jid);
|
||||
if nick then item.attr.nick = jid_resource(nick); end
|
||||
end
|
||||
local actor = stanza.attr.from;
|
||||
local reason = item:get_child_text("reason");
|
||||
local success, errtype, err
|
||||
if item.attr.affiliation and item.attr.jid and not item.attr.role then
|
||||
local registration_data;
|
||||
if item.attr.nick then
|
||||
local room_nick = self.jid.."/"..item.attr.nick;
|
||||
local existing_occupant = self:get_occupant_by_nick(room_nick);
|
||||
if existing_occupant and existing_occupant.bare_jid ~= item.attr.jid then
|
||||
module:log("debug", "Existing occupant for %s: %s does not match %s", room_nick, existing_occupant.bare_jid, item.attr.jid);
|
||||
self:set_role(true, room_nick, nil, "This nickname is reserved");
|
||||
end
|
||||
module:log("debug", "Reserving %s for %s (%s)", item.attr.nick, item.attr.jid, item.attr.affiliation);
|
||||
registration_data = { reserved_nickname = item.attr.nick };
|
||||
end
|
||||
success, errtype, err = self:set_affiliation(actor, item.attr.jid, item.attr.affiliation, reason, registration_data);
|
||||
elseif item.attr.role and item.attr.nick and not item.attr.affiliation then
|
||||
success, errtype, err = self:set_role(actor, self.jid.."/"..item.attr.nick, item.attr.role, reason);
|
||||
else
|
||||
success, errtype, err = nil, "cancel", "bad-request";
|
||||
end
|
||||
self:save(true);
|
||||
if not success then
|
||||
origin.send(st.error_reply(stanza, errtype, err));
|
||||
else
|
||||
origin.send(st.reply(stanza));
|
||||
end
|
||||
end
|
||||
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
function handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
local invitee = jid_prep(payload.attr.to);
|
||||
if not invitee then
|
||||
origin.send(st.error_reply(stanza, "cancel", "jid-malformed"));
|
||||
return true;
|
||||
elseif host_module:fire_event("muc-pre-invite", {room = room, origin = origin, stanza = stanza}) then
|
||||
return true;
|
||||
end
|
||||
local invite = muc_util.filter_muc_x(st.clone(stanza));
|
||||
invite.attr.from = room.jid;
|
||||
invite.attr.to = invitee;
|
||||
invite:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('invite', {from = stanza.attr.from;})
|
||||
:tag('reason'):text(payload:get_child_text("reason")):up()
|
||||
:up()
|
||||
:up();
|
||||
if not host_module:fire_event("muc-invite", {room = room, stanza = invite, origin = origin, incoming = stanza}) then
|
||||
local join = invite:get_child('x', MUC_USER_NS);
|
||||
-- make sure we filter password added by any module
|
||||
if join then
|
||||
local password = join:get_child('password');
|
||||
if password then
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == 'name' and v == 'password' then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end
|
||||
);
|
||||
end
|
||||
end
|
||||
room:route_stanza(invite);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
local prosody_overrides = {
|
||||
-- handle multiple items at once
|
||||
handle_admin_query_set_command = function(self, origin, stanza)
|
||||
for i=1,#stanza.tags[1] do
|
||||
if handle_admin_query_set_command_item(self, origin, stanza, stanza.tags[1].tags[i]) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end,
|
||||
-- this is extracted from prosody to handle multiple invites
|
||||
handle_message_to_room = function(room, origin, stanza, host_module)
|
||||
local type = stanza.attr.type;
|
||||
if type == nil or type == "normal" then
|
||||
local x = stanza:get_child("x", MUC_USER_NS);
|
||||
if x then
|
||||
local handled = false;
|
||||
for _, payload in pairs(x.tags) do
|
||||
if payload ~= nil and payload.name == "invite" and payload.attr.to then
|
||||
handled = true;
|
||||
handle_mediated_invite(room, origin, stanza, payload, host_module)
|
||||
end
|
||||
end
|
||||
return handled;
|
||||
end
|
||||
end
|
||||
end
|
||||
};
|
||||
|
||||
-- operates on already loaded lobby muc module
|
||||
function process_lobby_muc_loaded(lobby_muc, host_module)
|
||||
module:log('debug', 'Lobby muc loaded');
|
||||
lobby_muc_service = lobby_muc;
|
||||
|
||||
-- enable filtering presences in the lobby muc rooms
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
-- Advertise lobbyrooms support on main domain so client can pick up the address and use it
|
||||
module:add_identity('component', LOBBY_IDENTITY_TYPE, lobby_muc_component_config);
|
||||
|
||||
-- Tag the disco#info response with a feature that display name is required
|
||||
-- when the conference name from the web request has a lobby enabled.
|
||||
host_module:hook('host-disco-info-node', function (event)
|
||||
local session, reply, node = event.origin, event.reply, event.node;
|
||||
if node == LOBBY_IDENTITY_TYPE
|
||||
and session.jitsi_web_query_room
|
||||
and check_display_name_required then
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if room and room._data.lobbyroom then
|
||||
reply:tag('feature', { var = DISPLAY_NAME_REQUIRED_FEATURE }):up();
|
||||
end
|
||||
end
|
||||
event.exists = true;
|
||||
end);
|
||||
|
||||
local room_mt = lobby_muc_service.room_mt;
|
||||
-- we base affiliations (roles) in lobby muc component to be based on the roles in the main muc
|
||||
room_mt.get_affiliation = function(room, jid)
|
||||
if not room.main_room then
|
||||
module:log('error', 'No main room(%s) for %s!', room.jid, jid);
|
||||
return 'none';
|
||||
end
|
||||
|
||||
-- moderators in main room are moderators here
|
||||
local role = room.main_room.get_affiliation(room.main_room, jid);
|
||||
if role then
|
||||
return role;
|
||||
end
|
||||
|
||||
return 'none';
|
||||
end
|
||||
|
||||
-- listens for kicks in lobby room, 307 is the status for kick according to xep-0045
|
||||
host_module:hook('muc-broadcast-presence', function (event)
|
||||
local actor, occupant, room, x = event.actor, event.occupant, event.room, event.x;
|
||||
if presence_check_status(x, '307') then
|
||||
local display_name = occupant:get_presence():get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
-- we need to notify in the main room
|
||||
notify_lobby_access(room.main_room, actor, occupant.nick, display_name, false);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- process or waits to process the lobby muc component
|
||||
process_host_module(lobby_muc_component_config, function(host_module, host)
|
||||
-- lobby muc component created
|
||||
module:log('info', 'Lobby component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_lobby_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_lobby_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
main_muc_service = prosody.hosts[host].modules.muc;
|
||||
|
||||
-- hooks when lobby is enabled to create its room, only done here or by admin
|
||||
host_module:hook('muc-config-submitted', function(event)
|
||||
local actor, room = event.actor, event.room;
|
||||
local actor_node = jid_split(actor);
|
||||
if actor_node == 'focus' then
|
||||
return;
|
||||
end
|
||||
local members_only = event.fields['muc#roomconfig_membersonly'] and true or nil;
|
||||
if members_only then
|
||||
local lobby_created = attach_lobby_room(room, actor);
|
||||
if lobby_created then
|
||||
module:fire_event('jitsi-lobby-enabled', { room = room; });
|
||||
event.status_codes['104'] = true;
|
||||
notify_lobby_enabled(room, actor, true);
|
||||
end
|
||||
elseif room._data.lobbyroom then
|
||||
destroy_lobby_room(room, room.jid);
|
||||
module:fire_event('jitsi-lobby-disabled', { room = room; });
|
||||
notify_lobby_enabled(room, actor, false);
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-room-destroyed',function(event)
|
||||
local room = event.room;
|
||||
if room._data.lobbyroom then
|
||||
destroy_lobby_room(room, nil);
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-disco#info', function (event)
|
||||
local room = event.room;
|
||||
if (room._data.lobbyroom and room:get_members_only()) then
|
||||
table.insert(event.form, {
|
||||
name = 'muc#roominfo_lobbyroom';
|
||||
label = 'Lobby room jid';
|
||||
value = '';
|
||||
});
|
||||
event.formdata['muc#roominfo_lobbyroom'] = room._data.lobbyroom;
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-pre-join', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
|
||||
if is_healthcheck_room(room.jid) or not room:get_members_only() or ends_with(occupant.nick, '/focus') then
|
||||
return;
|
||||
end
|
||||
|
||||
local join = stanza:get_child('x', MUC_NS);
|
||||
if not join then
|
||||
return;
|
||||
end
|
||||
|
||||
local invitee = event.stanza.attr.from;
|
||||
local invitee_bare_jid = jid_bare(invitee);
|
||||
local _, invitee_domain = jid_split(invitee);
|
||||
local whitelistJoin = false;
|
||||
|
||||
-- whitelist participants
|
||||
if whitelist:contains(invitee_domain) or whitelist:contains(invitee_bare_jid) then
|
||||
whitelistJoin = true;
|
||||
end
|
||||
|
||||
local password = join:get_child_text('password', MUC_NS);
|
||||
if password and room:get_password() and password == room:get_password() then
|
||||
whitelistJoin = true;
|
||||
end
|
||||
|
||||
if whitelistJoin then
|
||||
local affiliation = room:get_affiliation(invitee);
|
||||
-- if it was already set to be whitelisted member
|
||||
if not affiliation or affiliation == 'none' or affiliation == 'member' then
|
||||
occupant.role = 'participant';
|
||||
room:set_affiliation(true, invitee_bare_jid, 'member');
|
||||
room:save_occupant(occupant);
|
||||
|
||||
return;
|
||||
end
|
||||
elseif room:get_password() then
|
||||
local affiliation = room:get_affiliation(invitee);
|
||||
-- if pre-approved and password is set for the room, add the password to allow joining
|
||||
if affiliation == 'member' and not password then
|
||||
join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
end
|
||||
|
||||
-- Check for display name if missing return an error
|
||||
local displayName = stanza:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
if (not displayName or #displayName == 0) and not room._data.lobby_skip_display_name_check then
|
||||
local reply = st.error_reply(stanza, 'modify', 'not-acceptable');
|
||||
reply.tags[1].attr.code = '406';
|
||||
reply:tag('displayname-required', { xmlns = 'http://jitsi.org/jitmeet', lobby = 'true' }):up():up();
|
||||
|
||||
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- we want to add the custom lobbyroom field to fill in the lobby room jid
|
||||
local invitee = event.stanza.attr.from;
|
||||
local affiliation = room:get_affiliation(invitee);
|
||||
if not affiliation or affiliation == 'none' then
|
||||
local reply = st.error_reply(stanza, 'auth', 'registration-required');
|
||||
reply.tags[1].attr.code = '407';
|
||||
if room._data.lobby_extra_reason then
|
||||
reply:tag(room._data.lobby_extra_reason, { xmlns = 'http://jitsi.org/jitmeet' }):up();
|
||||
end
|
||||
reply:tag('lobbyroom', { xmlns = 'http://jitsi.org/jitmeet' }):text(room._data.lobbyroom):up():up();
|
||||
|
||||
-- TODO: Drop this tag at some point (when all mobile clients and jigasi are updated), as this violates the rfc
|
||||
reply:tag('lobbyroom'):text(room._data.lobbyroom):up();
|
||||
|
||||
event.origin.send(reply:tag('x', {xmlns = MUC_NS}));
|
||||
return true;
|
||||
end
|
||||
end, -4); -- the default hook on members_only module is on -5
|
||||
|
||||
-- listens for invites for participants to join the main room
|
||||
host_module:hook('muc-invite', function(event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
local invitee = stanza.attr.to;
|
||||
local from = stanza:get_child('x', MUC_USER_NS)
|
||||
:get_child('invite').attr.from;
|
||||
|
||||
if lobby_muc_service and room._data.lobbyroom then
|
||||
local lobby_room_obj = lobby_muc_service.get_room_from_jid(room._data.lobbyroom);
|
||||
if lobby_room_obj then
|
||||
local occupant = lobby_room_obj:get_occupant_by_real_jid(invitee);
|
||||
if occupant then
|
||||
local display_name = occupant:get_presence():get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
|
||||
notify_lobby_access(room, from, occupant.nick, display_name, true);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
-- listen for admin set
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/bare"] = "handle_message_to_room" ;
|
||||
-- Host room
|
||||
["iq-set/host/http://jabber.org/protocol/muc#admin:query"] = "handle_admin_query_set_command" ;
|
||||
["message/host"] = "handle_message_to_room" ;
|
||||
} do
|
||||
host_module:hook(event_name, function (event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
local room_jid = jid_bare(stanza.attr.to);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if room then
|
||||
return prosody_overrides[method](room, origin, stanza, host_module);
|
||||
end
|
||||
end, 1) -- make sure we handle it before prosody that uses priority -2 for this
|
||||
end
|
||||
end);
|
||||
|
||||
function handle_create_lobby(event)
|
||||
local room = event.room;
|
||||
|
||||
-- since this is called by backend rather than triggered by UI, we need to handle a few additional things:
|
||||
-- 1. Make sure existing participants are already members or they will get kicked out when set_members_only(true)
|
||||
-- 2. Trigger a 104 (config change) status message so UI state is properly updated for existing users
|
||||
|
||||
-- make sure all existing occupants are members
|
||||
for _, occupant in room:each_occupant() do
|
||||
local affiliation = room:get_affiliation(occupant.bare_jid);
|
||||
if valid_affiliations[affiliation or "none"] < valid_affiliations.member then
|
||||
room:set_affiliation(true, occupant.bare_jid, 'member');
|
||||
end
|
||||
end
|
||||
-- Now it is safe to set the room to members only
|
||||
room:set_members_only(true);
|
||||
room._data.lobby_extra_reason = event.reason;
|
||||
room._data.lobby_skip_display_name_check = event.skip_display_name_check;
|
||||
|
||||
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
|
||||
room:broadcast_message(
|
||||
st.message({ type='groupchat', from=room.jid })
|
||||
:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('status', { code='104' })
|
||||
);
|
||||
|
||||
-- Attach the lobby room.
|
||||
attach_lobby_room(room);
|
||||
end
|
||||
|
||||
function handle_destroy_lobby(event)
|
||||
local room = event.room;
|
||||
|
||||
-- since this is called by backend rather than triggered by UI, we need to
|
||||
-- trigger a 104 (config change) status message so UI state is properly updated for existing users (and jicofo)
|
||||
destroy_lobby_room(room, event.newjid, event.message);
|
||||
|
||||
-- Trigger a presence with 104 so existing participants retrieves new muc#roomconfig
|
||||
room:broadcast_message(
|
||||
st.message({ type='groupchat', from=room.jid })
|
||||
:tag('x', { xmlns = MUC_USER_NS })
|
||||
:tag('status', { code='104' })
|
||||
);
|
||||
end
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
module:hook_global('create-lobby-room', handle_create_lobby);
|
||||
module:hook_global('destroy-lobby-room', handle_destroy_lobby);
|
||||
72
resources/prosody-plugins/mod_muc_max_occupants.lua
Normal file
72
resources/prosody-plugins/mod_muc_max_occupants.lua
Normal file
@@ -0,0 +1,72 @@
|
||||
-- MUC Max Occupants
|
||||
-- Configuring muc_max_occupants will set a limit of the maximum number
|
||||
-- of participants that will be able to join in a room.
|
||||
-- Participants in muc_access_whitelist will not be counted for the
|
||||
-- max occupants value (values are jids like recorder@jitsi.meeet.example.com).
|
||||
-- This module is configured under the muc component that is used for jitsi-meet
|
||||
local split_jid = require "util.jid".split;
|
||||
local st = require "util.stanza";
|
||||
local it = require "util.iterators";
|
||||
local is_healthcheck_room = module:require "util".is_healthcheck_room;
|
||||
|
||||
local whitelist = module:get_option_set("muc_access_whitelist");
|
||||
local MAX_OCCUPANTS = module:get_option_number("muc_max_occupants", -1);
|
||||
|
||||
local function count_keys(t)
|
||||
return it.count(it.keys(t));
|
||||
end
|
||||
|
||||
local function check_for_max_occupants(event)
|
||||
local room, origin, stanza = event.room, event.origin, event.stanza;
|
||||
local user, domain, res = split_jid(stanza.attr.from);
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
--no user object means no way to check for max occupants
|
||||
if user == nil then
|
||||
return
|
||||
end
|
||||
-- If we're a whitelisted user joining the room, don't bother checking the max
|
||||
-- occupants.
|
||||
if whitelist and (whitelist:contains(domain) or whitelist:contains(user..'@'..domain)) then
|
||||
return;
|
||||
end
|
||||
|
||||
if room and not room._jid_nick[stanza.attr.from] then
|
||||
local max_occupants_by_room = event.room._data.max_occupants;
|
||||
local count = count_keys(room._occupants);
|
||||
-- if no of occupants limit is set per room basis use
|
||||
-- that settings otherwise use the global one
|
||||
local slots = max_occupants_by_room or MAX_OCCUPANTS;
|
||||
|
||||
-- If there is no whitelist, just check the count.
|
||||
if not whitelist and count >= slots then
|
||||
module:log("info", "Attempt to enter a maxed out MUC");
|
||||
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- TODO: Are Prosody hooks atomic, or is this a race condition?
|
||||
-- For each person in the room that's not on the whitelist, subtract one
|
||||
-- from the count.
|
||||
for _, occupant in room:each_occupant() do
|
||||
user, domain, res = split_jid(occupant.bare_jid);
|
||||
if not whitelist or (not whitelist:contains(domain) and not whitelist:contains(user..'@'..domain)) then
|
||||
slots = slots - 1
|
||||
end
|
||||
end
|
||||
|
||||
-- If the room is full (<0 slots left), error out.
|
||||
if slots <= 0 then
|
||||
module:log("info", "Attempt to enter a maxed out MUC");
|
||||
origin.send(st.error_reply(stanza, "cancel", "service-unavailable"));
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if MAX_OCCUPANTS > 0 then
|
||||
module:hook("muc-occupant-pre-join", check_for_max_occupants, 10);
|
||||
end
|
||||
243
resources/prosody-plugins/mod_muc_meeting_id.lua
Normal file
243
resources/prosody-plugins/mod_muc_meeting_id.lua
Normal file
@@ -0,0 +1,243 @@
|
||||
local jid = require 'util.jid';
|
||||
local json = require 'cjson.safe';
|
||||
local queue = require "util.queue";
|
||||
local uuid_gen = require "util.uuid".generate;
|
||||
local main_util = module:require "util";
|
||||
local is_admin = main_util.is_admin;
|
||||
local ends_with = main_util.ends_with;
|
||||
local get_room_from_jid = main_util.get_room_from_jid;
|
||||
local is_healthcheck_room = main_util.is_healthcheck_room;
|
||||
local internal_room_jid_match_rewrite = main_util.internal_room_jid_match_rewrite;
|
||||
local presence_check_status = main_util.presence_check_status;
|
||||
local extract_subdomain = main_util.extract_subdomain;
|
||||
|
||||
local QUEUE_MAX_SIZE = 500;
|
||||
|
||||
module:depends("jitsi_permissions");
|
||||
|
||||
-- Common module for all logic that can be loaded under the conference muc component.
|
||||
--
|
||||
-- This module:
|
||||
-- a) Generates a unique meetingId, attaches it to the room and adds it to all disco info form data
|
||||
-- (when room is queried or in the initial room owner config).
|
||||
-- b) Updates user region (obtain it from the incoming http headers) in the occupant's presence on pre-join.
|
||||
-- c) Avoids any participant joining the room in the interval between creating the room and jicofo entering the room.
|
||||
-- d) Removes any nick that maybe set to messages being sent to the room.
|
||||
-- e) Fires event for received endpoint messages (optimization to decode them once).
|
||||
|
||||
-- Hook to assign meetingId for new rooms
|
||||
module:hook("muc-room-created", function(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
room._data.meetingId = uuid_gen();
|
||||
|
||||
module:log("debug", "Created meetingId:%s for %s",
|
||||
room._data.meetingId, room.jid);
|
||||
end);
|
||||
|
||||
-- Returns the meeting config Id form data.
|
||||
function getMeetingIdConfig(room)
|
||||
return {
|
||||
name = "muc#roominfo_meetingId";
|
||||
type = "text-single";
|
||||
label = "The meeting unique id.";
|
||||
value = room._data.meetingId or "";
|
||||
};
|
||||
end
|
||||
|
||||
-- add meeting Id to the disco info requests to the room
|
||||
module:hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, getMeetingIdConfig(event.room));
|
||||
end);
|
||||
|
||||
-- add the meeting Id in the default config we return to jicofo
|
||||
module:hook("muc-config-form", function(event)
|
||||
table.insert(event.form, getMeetingIdConfig(event.room));
|
||||
end, 90-3);
|
||||
|
||||
-- disabled few options for room config, to not mess with visitor logic
|
||||
module:hook("muc-config-submitted/muc#roomconfig_moderatedroom", function()
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook("muc-config-submitted/muc#roomconfig_presencebroadcast", function()
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook("muc-config-submitted/muc#roominfo_meetingId", function(event)
|
||||
-- we allow jicofo to overwrite the meetingId
|
||||
if is_admin(event.actor) then
|
||||
event.room._data.meetingId = event.value;
|
||||
return;
|
||||
end
|
||||
|
||||
return true;
|
||||
end, 99);
|
||||
module:hook('muc-broadcast-presence', function (event)
|
||||
local actor, occupant, room, x = event.actor, event.occupant, event.room, event.x;
|
||||
if presence_check_status(x, '307') then
|
||||
-- make sure we update and affiliation for kicked users
|
||||
room:set_affiliation(actor, occupant.bare_jid, 'none');
|
||||
end
|
||||
end);
|
||||
|
||||
local function process_region(session, stanza)
|
||||
if not session.user_region then
|
||||
return;
|
||||
end
|
||||
|
||||
local region = stanza:get_child_text('jitsi_participant_region');
|
||||
if region then
|
||||
return;
|
||||
end
|
||||
|
||||
stanza:tag('jitsi_participant_region'):text(session.user_region):up();
|
||||
end
|
||||
|
||||
--- Avoids any participant joining the room in the interval between creating the room
|
||||
--- and jicofo entering the room
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
|
||||
local is_health_room = is_healthcheck_room(room.jid);
|
||||
-- check for region
|
||||
if not is_admin(occupant.bare_jid) and not is_health_room then
|
||||
process_region(event.origin, stanza);
|
||||
end
|
||||
|
||||
-- we skip processing only if jicofo_lock is set to false
|
||||
if room._data.jicofo_lock == false or is_health_room then
|
||||
return;
|
||||
end
|
||||
|
||||
if ends_with(occupant.nick, '/focus') then
|
||||
module:fire_event('jicofo-unlock-room', { room = room; });
|
||||
else
|
||||
room._data.jicofo_lock = true;
|
||||
if not room.pre_join_queue then
|
||||
room.pre_join_queue = queue.new(QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
if not room.pre_join_queue:push(event) then
|
||||
module:log('error', 'Error enqueuing occupant event for: %s', occupant.nick);
|
||||
return true;
|
||||
end
|
||||
module:log('debug', 'Occupant pushed to prejoin queue %s', occupant.nick);
|
||||
|
||||
-- stop processing
|
||||
return true;
|
||||
end
|
||||
end, 8); -- just after the rate limit
|
||||
|
||||
function handle_jicofo_unlock(event)
|
||||
local room = event.room;
|
||||
|
||||
room._data.jicofo_lock = false;
|
||||
if not room.pre_join_queue then
|
||||
return;
|
||||
end
|
||||
|
||||
-- and now let's handle all pre_join_queue events
|
||||
for _, ev in room.pre_join_queue:items() do
|
||||
-- if the connection was closed while waiting in the queue, ignore
|
||||
if ev.origin.conn then
|
||||
module:log('debug', 'Occupant processed from queue %s', ev.occupant.nick);
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
end
|
||||
end
|
||||
room.pre_join_queue = nil;
|
||||
end
|
||||
|
||||
module:hook('jicofo-unlock-room', handle_jicofo_unlock);
|
||||
|
||||
-- make sure we remove nick if someone is sending it with a message to protect
|
||||
-- forgery of display name
|
||||
module:hook("muc-occupant-groupchat", function(event)
|
||||
event.stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
end, 45); -- prosody check is prio 50, we want to run after it
|
||||
|
||||
module:hook('message/bare', function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
if stanza.attr.type ~= 'groupchat' then
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- we are interested in all messages without a body
|
||||
local body = stanza:get_child('body')
|
||||
if body then
|
||||
return;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(stanza.attr.to);
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s', stanza.attr.to);
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant_jid = stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(occupant_jid);
|
||||
if not occupant then
|
||||
module:log("error", "Occupant sending msg %s was not found in room %s", occupant_jid, room.jid)
|
||||
return;
|
||||
end
|
||||
|
||||
local json_message = stanza:get_child_text('json-message', 'http://jitsi.org/jitmeet')
|
||||
or stanza:get_child_text('json-message');
|
||||
if not json_message then
|
||||
return;
|
||||
end
|
||||
|
||||
-- TODO: add optimization by moving type and certain fields like is_interim as attribute on 'json-message'
|
||||
-- using string find is roughly 70x faster than json decode for checking the value
|
||||
if string.find(json_message, '"is_interim":true', 1, true) then
|
||||
return;
|
||||
end
|
||||
|
||||
local msg_obj, error = json.decode(json_message);
|
||||
|
||||
if error then
|
||||
module:log('error', 'Error decoding data error:%s Sender: %s to:%s', error, stanza.attr.from, stanza.attr.to);
|
||||
return true;
|
||||
end
|
||||
|
||||
if msg_obj.transcript ~= nil then
|
||||
local transcription = msg_obj;
|
||||
|
||||
-- in case of the string matching optimization above failed
|
||||
if transcription.is_interim then
|
||||
return;
|
||||
end
|
||||
|
||||
-- TODO what if we have multiple alternative transcriptions not just 1
|
||||
local text_message = transcription.transcript[1].text;
|
||||
--do not send empty messages
|
||||
if text_message == '' then
|
||||
return;
|
||||
end
|
||||
|
||||
local user_id = transcription.participant.id;
|
||||
local who = room:get_occupant_by_nick(jid.bare(room.jid)..'/'..user_id);
|
||||
|
||||
transcription.jid = who and who.jid;
|
||||
transcription.session_id = room._data.meetingId;
|
||||
|
||||
local tenant, conference_name, id = extract_subdomain(jid.node(room.jid));
|
||||
if tenant then
|
||||
transcription.fqn = tenant..'/'..conference_name;
|
||||
else
|
||||
transcription.fqn = conference_name;
|
||||
end
|
||||
transcription.customer_id = id;
|
||||
|
||||
return module:fire_event('jitsi-transcript-received', {
|
||||
room = room, occupant = occupant, transcription = transcription, stanza = stanza });
|
||||
end
|
||||
|
||||
return module:fire_event('jitsi-endpoint-message-received', {
|
||||
room = room, occupant = occupant, message = msg_obj,
|
||||
origin = event.origin,
|
||||
stanza = stanza, raw_message = json_message });
|
||||
end);
|
||||
186
resources/prosody-plugins/mod_muc_password_check.lua
Normal file
186
resources/prosody-plugins/mod_muc_password_check.lua
Normal file
@@ -0,0 +1,186 @@
|
||||
local inspect = require "inspect";
|
||||
local formdecode = require "util.http".formdecode;
|
||||
local urlencode = require "util.http".urlencode;
|
||||
local jid = require "util.jid";
|
||||
local json = require 'cjson.safe';
|
||||
local util = module:require "util";
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local starts_with = util.starts_with;
|
||||
local process_host_module = util.process_host_module;
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
-- option to enable/disable room API token verifications
|
||||
local enableTokenVerification
|
||||
= module:get_option_boolean("enable_password_token_verification", true);
|
||||
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, disabling password check endpoint.");
|
||||
return ;
|
||||
end
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
local json_content_type = "application/json";
|
||||
|
||||
--- Verifies the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
if not enableTokenVerification then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- if enableTokenVerification is enabled and we do not have token
|
||||
-- stop here, cause the main virtual host can have guest access enabled
|
||||
-- (allowEmptyToken = true) and we will allow access to rooms info without
|
||||
-- a token
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided for %s", room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s for %s", tostring(reason), tostring(msg), room_address);
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Validates the request by checking for required url param room and
|
||||
-- validates the token provided with the request
|
||||
-- @param request - The request to validate.
|
||||
-- @return [error_code, room]
|
||||
local function validate_and_get_room(request)
|
||||
if not request.url.query then
|
||||
module:log("warn", "No query");
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local params = formdecode(request.url.query);
|
||||
local room_name = urlencode(params.room) or "";
|
||||
local subdomain = urlencode(params.prefix) or "";
|
||||
|
||||
if not room_name then
|
||||
module:log("warn", "Missing room param for %s", room_name);
|
||||
return 400, nil;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
|
||||
if token and starts_with(token,'Bearer ') then
|
||||
token = token:sub(8,#token)
|
||||
end
|
||||
|
||||
if not verify_token(token, room_address) then
|
||||
return 403, nil;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found for %s", room_address);
|
||||
return 404, nil;
|
||||
else
|
||||
return 200, room;
|
||||
end
|
||||
end
|
||||
|
||||
function handle_validate_room_password (event)
|
||||
local request = event.request;
|
||||
|
||||
if request.headers.content_type ~= json_content_type
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("warn", "Wrong content type: %s", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local params, error = json.decode(request.body);
|
||||
if not params then
|
||||
module:log("warn", "Missing params error:%s", error);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local passcode = params["passcode"];
|
||||
|
||||
if not passcode then
|
||||
module:log("warn", "Missing passcode param");
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local error_code, room = validate_and_get_room(request);
|
||||
|
||||
if not room then
|
||||
return { status_code = error_code; }
|
||||
end
|
||||
|
||||
local json_msg_str, error_encode = json.encode({ valid = (room:get_password() == passcode) });
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Cannot encode json room:%s error:%s', room.jid, error_encode);
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local PUT_response = {
|
||||
headers = { content_type = "application/json"; };
|
||||
body = json_msg_str;
|
||||
};
|
||||
|
||||
-- module:log("debug","Sending response for room password validate: %s", inspect(PUT_response));
|
||||
|
||||
return PUT_response;
|
||||
end
|
||||
|
||||
--- Handles request for retrieving the room participants details
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants details
|
||||
function handle_get_room_password (event)
|
||||
local error_code, room = validate_and_get_room(event.request);
|
||||
|
||||
if not room then
|
||||
return { status_code = error_code; }
|
||||
end
|
||||
|
||||
room_details = {};
|
||||
room_details["conference"] = room.jid;
|
||||
room_details["passcodeProtected"] = room:get_password() ~= nil;
|
||||
room_details["lobbyEnabled"] = room._data ~= nil and room._data.lobbyroom ~= nil;
|
||||
|
||||
local json_msg_str, error = json.encode(room_details);
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Cannot encode json room:%s error:%s', room.jid, error);
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local GET_response = {
|
||||
headers = {
|
||||
content_type = "application/json";
|
||||
};
|
||||
body = json_msg_str;
|
||||
};
|
||||
-- module:log("debug","Sending response for room password: %s", inspect(GET_response));
|
||||
|
||||
return GET_response;
|
||||
end
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
module:log("info","Adding http handler for /room-info on %s", host_module.host);
|
||||
host_module:depends("http");
|
||||
host_module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET room-info"] = function (event) return async_handler_wrapper(event, handle_get_room_password) end;
|
||||
["PUT room-info"] = function (event) return async_handler_wrapper(event, handle_validate_room_password) end;
|
||||
};
|
||||
});
|
||||
end);
|
||||
56
resources/prosody-plugins/mod_muc_password_whitelist.lua
Normal file
56
resources/prosody-plugins/mod_muc_password_whitelist.lua
Normal file
@@ -0,0 +1,56 @@
|
||||
--- AUTHOR: https://gist.github.com/legastero Lance Stout
|
||||
local jid_split = require "util.jid".split;
|
||||
local whitelist = module:get_option_set("muc_password_whitelist");
|
||||
|
||||
local MUC_NS = "http://jabber.org/protocol/muc";
|
||||
|
||||
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
local user, domain, res = jid_split(event.stanza.attr.from);
|
||||
|
||||
--no user object means no way to check whitelist
|
||||
if user == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if not whitelist then
|
||||
return;
|
||||
end
|
||||
if not whitelist:contains(domain) and not whitelist:contains(user..'@'..domain) then
|
||||
return;
|
||||
end
|
||||
|
||||
local join = stanza:get_child("x", MUC_NS);
|
||||
if not join then
|
||||
join = stanza:tag("x", { xmlns = MUC_NS });
|
||||
end
|
||||
|
||||
local password = join:get_child("password", MUC_NS);
|
||||
if password then
|
||||
-- removes <password... node,
|
||||
-- seems like password:text( appends text, not replacing it
|
||||
join:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "password" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end
|
||||
);
|
||||
|
||||
end
|
||||
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room:get_password());
|
||||
|
||||
-- module:log("debug", "Applied password access whitelist for %s in room %s", event.stanza.attr.from, room.jid);
|
||||
end, -7); --- Run before the password check (priority -20), runs after lobby(priority -4) and members-only (priority -5).
|
||||
|
||||
|
||||
module:hook_global("config-reloaded", function (event)
|
||||
module:log("debug", "Reloading MUC password access whitelist");
|
||||
whitelist = module:get_option_set("muc_password_whitelist");
|
||||
end)
|
||||
319
resources/prosody-plugins/mod_muc_poltergeist.lua
Normal file
319
resources/prosody-plugins/mod_muc_poltergeist.lua
Normal file
@@ -0,0 +1,319 @@
|
||||
local bare = require "util.jid".bare;
|
||||
local get_room_by_name_and_subdomain = module:require "util".get_room_by_name_and_subdomain;
|
||||
local jid = require "util.jid";
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
local poltergeist = module:require "poltergeist";
|
||||
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return;
|
||||
end
|
||||
|
||||
module:depends("jitsi_session");
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
|
||||
-- Options
|
||||
local poltergeist_component
|
||||
= module:get_option_string("poltergeist_component", module.host);
|
||||
|
||||
-- this basically strips the domain from the conference.domain address
|
||||
local parentHostName = string.gmatch(tostring(module.host), "%w+.(%w.+)")();
|
||||
if parentHostName == nil then
|
||||
log("error", "Failed to start - unable to get parent hostname");
|
||||
return;
|
||||
end
|
||||
|
||||
local parentCtx = module:context(parentHostName);
|
||||
if parentCtx == nil then
|
||||
log("error",
|
||||
"Failed to start - unable to get parent context for host: %s",
|
||||
tostring(parentHostName));
|
||||
return;
|
||||
end
|
||||
local token_util = module:require "token/util".new(parentCtx);
|
||||
|
||||
-- option to enable/disable token verifications
|
||||
local disableTokenVerification
|
||||
= module:get_option_boolean("disable_polergeist_token_verification", false);
|
||||
|
||||
-- poltergaist management functions
|
||||
|
||||
--- Verifies room name, domain name with the values in the token
|
||||
-- @param token the token we received
|
||||
-- @param room_name the room name
|
||||
-- @param group name of the group (optional)
|
||||
-- @param session the session to use for storing token specific fields
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_name, group, session)
|
||||
if disableTokenVerification then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- if not disableTokenVerification and we do not have token
|
||||
-- stop here, cause the main virtual host can have guest access enabled
|
||||
-- (allowEmptyToken = true) and we will allow access to rooms info without
|
||||
-- a token
|
||||
if token == nil then
|
||||
log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
session.auth_token = token;
|
||||
local verified, reason = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
log("warn", "not a valid token %s", tostring(reason));
|
||||
return false;
|
||||
end
|
||||
|
||||
local room_address = jid.join(room_name, module:get_host());
|
||||
-- if there is a group we are in multidomain mode and that group is not
|
||||
-- our parent host
|
||||
if group and group ~= "" and group ~= parentHostName then
|
||||
room_address = "["..group.."]"..room_address;
|
||||
end
|
||||
|
||||
if not token_util:verify_room(session, room_address) then
|
||||
log("warn", "Token %s not allowed to join: %s",
|
||||
tostring(token), tostring(room_address));
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Event handlers
|
||||
|
||||
-- if we found that a session for a user with id has a poltergiest already
|
||||
-- created, retrieve its jid and return it to the authentication
|
||||
-- so we can reuse it and we that real user will replace the poltergiest
|
||||
prosody.events.add_handler("pre-jitsi-authentication", function(session)
|
||||
|
||||
if (session.jitsi_meet_context_user) then
|
||||
local room = get_room_by_name_and_subdomain(
|
||||
session.jitsi_web_query_room,
|
||||
session.jitsi_web_query_prefix);
|
||||
|
||||
if (not room) then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local username = poltergeist.get_username(
|
||||
room,
|
||||
session.jitsi_meet_context_user["id"]
|
||||
);
|
||||
|
||||
if (not username) then
|
||||
return nil;
|
||||
end
|
||||
|
||||
log("debug", "Found predefined username %s", username);
|
||||
|
||||
-- let's find the room and if the poltergeist occupant is there
|
||||
-- lets remove him before the real participant joins
|
||||
-- when we see the unavailable presence to go out the server
|
||||
-- we will mark it with ignore tag
|
||||
local nick = poltergeist.create_nick(username);
|
||||
if (poltergeist.occupies(room, nick)) then
|
||||
module:log("info", "swapping poltergeist for user: %s/%s", room, nick)
|
||||
-- notify that user connected using the poltergeist
|
||||
poltergeist.update(room, nick, "connected");
|
||||
poltergeist.remove(room, nick, true);
|
||||
end
|
||||
|
||||
return username;
|
||||
end
|
||||
|
||||
return nil;
|
||||
end);
|
||||
|
||||
--- Note: mod_muc and some of its sub-modules add event handlers between 0 and -100,
|
||||
--- e.g. to check for banned users, etc.. Hence adding these handlers at priority -100.
|
||||
module:hook("muc-decline", function (event)
|
||||
poltergeist.remove(event.room, bare(event.stanza.attr.from), false);
|
||||
end, -100);
|
||||
-- before sending the presence for a poltergeist leaving add ignore tag
|
||||
-- as poltergeist is leaving just before the real user joins and in the client
|
||||
-- we ignore this presence to avoid leaving/joining experience and the real
|
||||
-- user will reuse all currently created UI components for the same nick
|
||||
module:hook("muc-broadcast-presence", function (event)
|
||||
if (bare(event.occupant.jid) == poltergeist_component) then
|
||||
if(event.stanza.attr.type == "unavailable"
|
||||
and poltergeist.should_ignore(event.occupant.nick)) then
|
||||
event.stanza:tag(
|
||||
"ignore", { xmlns = "http://jitsi.org/jitmeet/" }):up();
|
||||
poltergeist.reset_ignored(event.occupant.nick);
|
||||
end
|
||||
end
|
||||
end, -100);
|
||||
|
||||
-- cleanup room table after room is destroyed
|
||||
module:hook(
|
||||
"muc-room-destroyed",
|
||||
function(event)
|
||||
poltergeist.remove_room(event.room);
|
||||
end
|
||||
);
|
||||
|
||||
--- Handles request for creating/managing poltergeists
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with response details
|
||||
function handle_create_poltergeist (event)
|
||||
if (not event.request.url.query) then
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local params = parse(event.request.url.query);
|
||||
local user_id = params["user"];
|
||||
local room_name = params["room"];
|
||||
local group = params["group"];
|
||||
local name = params["name"];
|
||||
local avatar = params["avatar"];
|
||||
local status = params["status"];
|
||||
local conversation = params["conversation"];
|
||||
local session = {};
|
||||
|
||||
if not verify_token(params["token"], room_name, group, session) then
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
-- If the provided room conference doesn't exist then we
|
||||
-- can't add a poltergeist to it.
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
-- If the poltergiest is already in the conference then it will
|
||||
-- be in our username store and another can't be added.
|
||||
local username = poltergeist.get_username(room, user_id);
|
||||
if (username ~=nil and
|
||||
poltergeist.occupies(room, poltergeist.create_nick(username))) then
|
||||
log("warn",
|
||||
"poltergeist for username:%s already in the room:%s",
|
||||
username,
|
||||
room_name
|
||||
);
|
||||
return { status_code = 202; };
|
||||
end
|
||||
|
||||
local context = {
|
||||
user = {
|
||||
id = user_id;
|
||||
};
|
||||
group = group;
|
||||
creator_user = session.jitsi_meet_context_user;
|
||||
creator_group = session.jitsi_meet_context_group;
|
||||
};
|
||||
if avatar ~= nil then
|
||||
context.user.avatar = avatar
|
||||
end
|
||||
local resources = {};
|
||||
if conversation ~= nil then
|
||||
resources["conversation"] = conversation
|
||||
end
|
||||
|
||||
poltergeist.add_to_muc(room, user_id, name, avatar, context, status, resources)
|
||||
return { status_code = 200; };
|
||||
end
|
||||
|
||||
--- Handles request for updating poltergeists status
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with response details
|
||||
function handle_update_poltergeist (event)
|
||||
if (not event.request.url.query) then
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local params = parse(event.request.url.query);
|
||||
local user_id = params["user"];
|
||||
local room_name = params["room"];
|
||||
local group = params["group"];
|
||||
local status = params["status"];
|
||||
local call_id = params["callid"];
|
||||
|
||||
local call_cancel = false
|
||||
if params["callcancel"] == "true" then
|
||||
call_cancel = true;
|
||||
end
|
||||
|
||||
if not verify_token(params["token"], room_name, group, {}) then
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
local username = poltergeist.get_username(room, user_id);
|
||||
if (not username) then
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
local call_details = {
|
||||
["cancel"] = call_cancel;
|
||||
["id"] = call_id;
|
||||
};
|
||||
|
||||
local nick = poltergeist.create_nick(username);
|
||||
if (not poltergeist.occupies(room, nick)) then
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
poltergeist.update(room, nick, status, call_details);
|
||||
return { status_code = 200; };
|
||||
end
|
||||
|
||||
--- Handles remove poltergeists
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with response details
|
||||
function handle_remove_poltergeist (event)
|
||||
if (not event.request.url.query) then
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local params = parse(event.request.url.query);
|
||||
local user_id = params["user"];
|
||||
local room_name = params["room"];
|
||||
local group = params["group"];
|
||||
|
||||
if not verify_token(params["token"], room_name, group, {}) then
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(room_name, group);
|
||||
if (not room) then
|
||||
log("error", "no room found %s", room_name);
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
local username = poltergeist.get_username(room, user_id);
|
||||
if (not username) then
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
local nick = poltergeist.create_nick(username);
|
||||
if (not poltergeist.occupies(room, nick)) then
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
poltergeist.remove(room, nick, false);
|
||||
return { status_code = 200; };
|
||||
end
|
||||
|
||||
log("info", "Loading poltergeist service");
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
name = "poltergeist";
|
||||
route = {
|
||||
["GET /poltergeist/create"] = function (event) return async_handler_wrapper(event,handle_create_poltergeist) end;
|
||||
["GET /poltergeist/update"] = function (event) return async_handler_wrapper(event,handle_update_poltergeist) end;
|
||||
["GET /poltergeist/remove"] = function (event) return async_handler_wrapper(event,handle_remove_poltergeist) end;
|
||||
};
|
||||
});
|
||||
231
resources/prosody-plugins/mod_muc_rate_limit.lua
Normal file
231
resources/prosody-plugins/mod_muc_rate_limit.lua
Normal file
@@ -0,0 +1,231 @@
|
||||
-- enable under the main muc component
|
||||
|
||||
local queue = require "util.queue";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local st = require "util.stanza";
|
||||
|
||||
-- we max to 500 participants per meeting so this should be enough, we are not suppose to handle all
|
||||
-- participants in one meeting
|
||||
local PRESENCE_QUEUE_MAX_SIZE = 1000;
|
||||
|
||||
-- default to 3 participants per second
|
||||
local join_rate_per_conference = module:get_option_number("muc_rate_joins", 3);
|
||||
local leave_rate_per_conference = module:get_option_number("muc_rate_leaves", 5);
|
||||
|
||||
-- Measure/monitor the room rate limiting queue
|
||||
local measure = require "core.statsmanager".measure;
|
||||
local measure_longest_queue = measure("distribution",
|
||||
"/mod_" .. module.name .. "/longest_queue");
|
||||
local measure_rooms_with_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/rooms_with_queue");
|
||||
|
||||
-- throws a stat that the queue was full, counts the total number of times we hit it
|
||||
local measure_full_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/full_queue");
|
||||
|
||||
-- keeps track of the total times we had an error processing the queue
|
||||
local measure_errors_processing_queue = measure("rate",
|
||||
"/mod_" .. module.name .. "/errors_processing_queue");
|
||||
|
||||
-- we keep track here what was the longest queue we have seen
|
||||
local stat_longest_queue = 0;
|
||||
|
||||
-- Adds item to the queue
|
||||
-- @returns false if queue is full and item was not added, true otherwise
|
||||
local function add_item_to_queue(queue, item, room, from, send_stats)
|
||||
if not queue:push(item) then
|
||||
module:log('error',
|
||||
'Error pushing item in %s queue for %s in %s', send_stats and 'join' or 'leave', from, room.jid);
|
||||
|
||||
if send_stats then
|
||||
measure_full_queue();
|
||||
end
|
||||
|
||||
return false;
|
||||
else
|
||||
-- check is this the longest queue and if so throws a stat
|
||||
if send_stats and queue:count() > stat_longest_queue then
|
||||
stat_longest_queue = queue:count();
|
||||
measure_longest_queue(stat_longest_queue);
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
-- process join_rate_presence_queue in the room and pops element passing them to handle_normal_presence
|
||||
-- returns 1 if we want to reschedule it after 1 second
|
||||
local function timer_process_queue_elements (rate, queue, process, queue_empty_cb)
|
||||
if not queue or queue:count() == 0 or queue.empty then
|
||||
return;
|
||||
end
|
||||
|
||||
for _ = 1, rate do
|
||||
local ev = queue:pop();
|
||||
if ev then
|
||||
process(ev);
|
||||
end
|
||||
end
|
||||
|
||||
-- if there are elements left, schedule an execution in a second
|
||||
if queue:count() > 0 then
|
||||
return 1;
|
||||
else
|
||||
queue_empty_cb();
|
||||
end
|
||||
end
|
||||
|
||||
-- we check join rate before occupant joins. If rate is exceeded we queue the events and start a timer
|
||||
-- that will run every second processing the events passing them to the room handling function handle_normal_presence
|
||||
-- from where those arrived, this way we keep a maximum rate of joining
|
||||
module:hook("muc-occupant-pre-join", function (event)
|
||||
local room, stanza = event.room, event.stanza;
|
||||
|
||||
-- skipping events we had produced and clear our flag
|
||||
if stanza.delayed_join_skip == true then
|
||||
event.stanza.delayed_join_skip = nil;
|
||||
return nil;
|
||||
end
|
||||
|
||||
local throttle = room.join_rate_throttle;
|
||||
if not room.join_rate_throttle then
|
||||
throttle = new_throttle(join_rate_per_conference, 1); -- rate per one second
|
||||
room.join_rate_throttle = throttle;
|
||||
end
|
||||
|
||||
if not throttle:poll(1) then
|
||||
if not room.join_rate_presence_queue then
|
||||
-- if this is the first item for a room we increment the stat for rooms with queues
|
||||
measure_rooms_with_queue();
|
||||
room.join_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
if not add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
if not room.join_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
join_rate_per_conference,
|
||||
room.join_rate_presence_queue,
|
||||
function(ev)
|
||||
-- we mark what we pass here so we can skip it on the next muc-occupant-pre-join event
|
||||
ev.stanza.delayed_join_skip = true;
|
||||
room:handle_normal_presence(ev.origin, ev.stanza);
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.join_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing join queue: %s', result);
|
||||
|
||||
measure_errors_processing_queue();
|
||||
|
||||
-- let's re-schedule timer so we do not lose the queue
|
||||
return 1;
|
||||
end
|
||||
|
||||
return result;
|
||||
end);
|
||||
room.join_rate_queue_timer = true;
|
||||
end
|
||||
|
||||
return true; -- we stop execution, so we do not process this join at the moment
|
||||
end
|
||||
|
||||
if room.join_rate_queue_timer then
|
||||
-- there is timer so we need to order the presences, put it in the queue
|
||||
|
||||
-- if add fails as queue is full we return false and the event will continue processing, we risk re-order
|
||||
-- but not losing it
|
||||
return add_item_to_queue(room.join_rate_presence_queue, event, room, stanza.attr.from, true);
|
||||
end
|
||||
|
||||
end, 9); -- as we will rate limit joins we need to be the first to execute
|
||||
-- we ran it after muc_max_occupants which is with priority 10, there is nothing to rate limit
|
||||
-- if max number of occupants is reached
|
||||
|
||||
-- clear queue on room destroy so timer will skip next run if any
|
||||
module:hook('muc-room-destroyed',function(event)
|
||||
if event.room.join_rate_presence_queue then
|
||||
event.room.join_rate_presence_queue.empty = true;
|
||||
end
|
||||
if event.room.leave_rate_presence_queue then
|
||||
event.room.leave_rate_presence_queue.empty = true;
|
||||
end
|
||||
end);
|
||||
|
||||
module:hook('muc-occupant-pre-leave', function (event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
local throttle = room.leave_rate_throttle;
|
||||
|
||||
if not throttle then
|
||||
throttle = new_throttle(leave_rate_per_conference, 1); -- rate per one second
|
||||
room.leave_rate_throttle = throttle;
|
||||
end
|
||||
|
||||
if not throttle:poll(1) then
|
||||
if not room.leave_rate_presence_queue then
|
||||
room.leave_rate_presence_queue = queue.new(PRESENCE_QUEUE_MAX_SIZE);
|
||||
end
|
||||
|
||||
-- we need it later when processing the event
|
||||
event.orig_role = occupant.role;
|
||||
|
||||
if not add_item_to_queue(room.leave_rate_presence_queue, event, room, stanza.attr.from, false) then
|
||||
-- let's not stop processing the event
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- set role to nil so the occupant will be removed from room occupants when we save it
|
||||
-- we remove occupant from the list early on batches so we can spare sending few presences
|
||||
occupant.role = nil;
|
||||
room:save_occupant(occupant);
|
||||
|
||||
if not room.leave_rate_queue_timer then
|
||||
timer.add_task(1, function ()
|
||||
local status, result = pcall(timer_process_queue_elements,
|
||||
leave_rate_per_conference,
|
||||
room.leave_rate_presence_queue,
|
||||
function(ev)
|
||||
local occupant, orig_role, origin, room, stanza
|
||||
= ev.occupant, ev.orig_role, ev.origin, ev.room, ev.stanza;
|
||||
|
||||
room:publicise_occupant_status(
|
||||
occupant,
|
||||
st.stanza("x", {xmlns = "http://jabber.org/protocol/muc#user";}),
|
||||
nil, nil, nil, orig_role);
|
||||
|
||||
module:fire_event("muc-occupant-left", {
|
||||
room = room;
|
||||
nick = occupant.nick;
|
||||
occupant = occupant;
|
||||
origin = origin;
|
||||
stanza = stanza;
|
||||
});
|
||||
end,
|
||||
function() -- empty callback
|
||||
room.leave_rate_queue_timer = false;
|
||||
end
|
||||
);
|
||||
if not status then
|
||||
-- there was an error in the timer function
|
||||
module:log('error', 'Error processing leave queue: %s', result);
|
||||
|
||||
-- let's re-schedule timer so we do not lose the queue
|
||||
return 1;
|
||||
end
|
||||
|
||||
return result;
|
||||
end);
|
||||
room.leave_rate_queue_timer = true;
|
||||
end
|
||||
|
||||
return true; -- we stop execution, so we do not process this leave at the moment
|
||||
end
|
||||
end);
|
||||
197
resources/prosody-plugins/mod_muc_size.lua
Normal file
197
resources/prosody-plugins/mod_muc_size.lua
Normal file
@@ -0,0 +1,197 @@
|
||||
-- Prosody IM
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
--
|
||||
|
||||
local jid = require "util.jid";
|
||||
local it = require "util.iterators";
|
||||
local json = require 'cjson.safe';
|
||||
local iterators = require "util.iterators";
|
||||
local array = require"util.array";
|
||||
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return;
|
||||
end
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
|
||||
local tostring = tostring;
|
||||
local neturl = require "net.url";
|
||||
local parse = neturl.parseQuery;
|
||||
|
||||
-- option to enable/disable room API token verifications
|
||||
local enableTokenVerification
|
||||
= module:get_option_boolean("enable_roomsize_token_verification", false);
|
||||
|
||||
local token_util = module:require "token/util".new(module);
|
||||
local get_room_from_jid = module:require "util".get_room_from_jid;
|
||||
|
||||
-- no token configuration but required
|
||||
if token_util == nil and enableTokenVerification then
|
||||
log("error", "no token configuration but it is required");
|
||||
return;
|
||||
end
|
||||
|
||||
-- required parameter for custom muc component prefix,
|
||||
-- defaults to "conference"
|
||||
local muc_domain_prefix
|
||||
= module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
--- Verifies room name, domain name with the values in the token
|
||||
-- @param token the token we received
|
||||
-- @param room_address the full room address jid
|
||||
-- @return true if values are ok or false otherwise
|
||||
function verify_token(token, room_address)
|
||||
if not enableTokenVerification then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- if enableTokenVerification is enabled and we do not have token
|
||||
-- stop here, cause the main virtual host can have guest access enabled
|
||||
-- (allowEmptyToken = true) and we will allow access to rooms info without
|
||||
-- a token
|
||||
if token == nil then
|
||||
log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
log("warn", "not a valid token %s", tostring(reason));
|
||||
return false;
|
||||
end
|
||||
|
||||
if not token_util:verify_room(session, room_address) then
|
||||
log("warn", "Token %s not allowed to join: %s",
|
||||
tostring(token), tostring(room_address));
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
--- Handles request for retrieving the room size
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants count,
|
||||
-- the value is without counting the focus.
|
||||
function handle_get_room_size(event)
|
||||
if (not event.request.url.query) then
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local params = parse(event.request.url.query);
|
||||
local room_name = params["room"];
|
||||
local domain_name = params["domain"];
|
||||
local subdomain = params["subdomain"];
|
||||
|
||||
local room_address
|
||||
= jid.join(room_name, muc_domain_prefix.."."..domain_name);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
if not verify_token(params["token"], room_address) then
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
local participant_count = 0;
|
||||
|
||||
log("debug", "Querying room %s", tostring(room_address));
|
||||
|
||||
if room then
|
||||
local occupants = room._occupants;
|
||||
if occupants then
|
||||
participant_count = iterators.count(room:each_occupant());
|
||||
end
|
||||
log("debug",
|
||||
"there are %s occupants in room", tostring(participant_count));
|
||||
else
|
||||
log("debug", "no such room exists");
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
if participant_count > 1 then
|
||||
participant_count = participant_count - 1;
|
||||
end
|
||||
|
||||
return { status_code = 200; body = [[{"participants":]]..participant_count..[[}]] };
|
||||
end
|
||||
|
||||
--- Handles request for retrieving the room participants details
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants details
|
||||
function handle_get_room (event)
|
||||
if (not event.request.url.query) then
|
||||
return { status_code = 400; };
|
||||
end
|
||||
|
||||
local params = parse(event.request.url.query);
|
||||
local room_name = params["room"];
|
||||
local domain_name = params["domain"];
|
||||
local subdomain = params["subdomain"];
|
||||
local room_address
|
||||
= jid.join(room_name, muc_domain_prefix.."."..domain_name);
|
||||
|
||||
if subdomain and subdomain ~= "" then
|
||||
room_address = "["..subdomain.."]"..room_address;
|
||||
end
|
||||
|
||||
if not verify_token(params["token"], room_address) then
|
||||
return { status_code = 403; };
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_address);
|
||||
local participant_count = 0;
|
||||
local occupants_json = array();
|
||||
|
||||
log("debug", "Querying room %s", tostring(room_address));
|
||||
|
||||
if room then
|
||||
local occupants = room._occupants;
|
||||
if occupants then
|
||||
participant_count = iterators.count(room:each_occupant());
|
||||
for _, occupant in room:each_occupant() do
|
||||
-- filter focus as we keep it as hidden participant
|
||||
if string.sub(occupant.nick,-string.len("/focus"))~="/focus" then
|
||||
for _, pr in occupant:each_session() do
|
||||
local nick = pr:get_child_text("nick", "http://jabber.org/protocol/nick") or "";
|
||||
local email = pr:get_child_text("email") or "";
|
||||
occupants_json:push({
|
||||
jid = tostring(occupant.nick),
|
||||
email = tostring(email),
|
||||
display_name = tostring(nick)});
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
log("debug",
|
||||
"there are %s occupants in room", tostring(participant_count));
|
||||
else
|
||||
log("debug", "no such room exists");
|
||||
return { status_code = 404; };
|
||||
end
|
||||
|
||||
if participant_count > 1 then
|
||||
participant_count = participant_count - 1;
|
||||
end
|
||||
|
||||
return { status_code = 200; body = json.encode(occupants_json); };
|
||||
end;
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET room-size"] = function (event) return async_handler_wrapper(event,handle_get_room_size) end;
|
||||
["GET sessions"] = function () return tostring(it.count(it.keys(prosody.full_sessions))); end;
|
||||
["GET room"] = function (event) return async_handler_wrapper(event,handle_get_room) end;
|
||||
};
|
||||
});
|
||||
end
|
||||
|
||||
98
resources/prosody-plugins/mod_muc_wait_for_host.lua
Normal file
98
resources/prosody-plugins/mod_muc_wait_for_host.lua
Normal file
@@ -0,0 +1,98 @@
|
||||
-- This module is activated under the main muc component
|
||||
-- This will prevent anyone joining the call till jicofo and one moderator join the room
|
||||
-- for the rest of the participants lobby will be turned on and they will be waiting there till
|
||||
-- the main participant joins and lobby will be turned off at that time and rest of the participants will
|
||||
-- join the room. It expects main virtual host to be set to require jwt tokens and guests to use
|
||||
-- the guest domain which is anonymous.
|
||||
-- The module has the option to set participants to moderators when connected via token/when they are authenticated
|
||||
-- This module depends on mod_persistent_lobby.
|
||||
local jid = require 'util.jid';
|
||||
local util = module:require "util";
|
||||
local is_admin = util.is_admin;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_moderated = util.is_moderated;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local disable_auto_owners = module:get_option_boolean('wait_for_host_disable_auto_owners', false);
|
||||
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
if not muc_domain_base then
|
||||
module:log('warn', "No 'muc_mapper_domain_base' option set, disabling module");
|
||||
return
|
||||
end
|
||||
|
||||
-- to activate this you need the following config in general config file in log = { }
|
||||
-- { to = 'file', filename = '/var/log/prosody/prosody.audit.log', levels = { 'audit' } }
|
||||
local logger = require 'util.logger';
|
||||
local audit_logger = logger.make_logger('mod_'..module.name, 'audit');
|
||||
|
||||
local lobby_muc_component_config = 'lobby.' .. muc_domain_base;
|
||||
local lobby_host;
|
||||
|
||||
if not disable_auto_owners then
|
||||
module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant, session = event.room, event.occupant, event.origin;
|
||||
local is_moderated_room = is_moderated(room.jid);
|
||||
|
||||
-- for jwt authenticated and username and password authenticated
|
||||
-- only if it is not a moderated room
|
||||
if not is_moderated_room and
|
||||
(session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base)) then
|
||||
room:set_affiliation(true, occupant.bare_jid, 'owner');
|
||||
end
|
||||
end, 2);
|
||||
end
|
||||
|
||||
-- if not authenticated user is trying to join the room we enable lobby in it
|
||||
-- and wait for the moderator to join
|
||||
module:hook('muc-occupant-pre-join', function (event)
|
||||
local room, occupant, session = event.room, event.occupant, event.origin;
|
||||
|
||||
-- we ignore jicofo as we want it to join the room or if the room has already seen its
|
||||
-- authenticated host
|
||||
if is_admin(occupant.bare_jid) or is_healthcheck_room(room.jid) or room.has_host then
|
||||
return;
|
||||
end
|
||||
|
||||
local has_host = false;
|
||||
for _, o in room:each_occupant() do
|
||||
if jid.host(o.bare_jid) == muc_domain_base then
|
||||
room.has_host = true;
|
||||
end
|
||||
end
|
||||
|
||||
if not room.has_host then
|
||||
if session.auth_token or (session.username and jid.host(occupant.bare_jid) == muc_domain_base) then
|
||||
-- the host is here, let's drop the lobby
|
||||
room:set_members_only(false);
|
||||
|
||||
-- let's set the default role of 'participant' for the newly created occupant as it was nil when created
|
||||
-- when the room was still members_only, later if not disabled this participant will become a moderator
|
||||
occupant.role = room:get_default_role(room:get_affiliation(occupant.bare_jid)) or 'participant';
|
||||
|
||||
module:log('info', 'Host %s arrived in %s.', occupant.bare_jid, room.jid);
|
||||
audit_logger('room_jid:%s created_by:%s', room.jid,
|
||||
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or 'nil');
|
||||
module:fire_event('room_host_arrived', room.jid, session);
|
||||
lobby_host:fire_event('destroy-lobby-room', {
|
||||
room = room,
|
||||
newjid = room.jid,
|
||||
message = 'Host arrived.',
|
||||
});
|
||||
elseif not room:get_members_only() then
|
||||
-- let's enable lobby
|
||||
module:log('info', 'Will wait for host in %s.', room.jid);
|
||||
prosody.events.fire_event('create-persistent-lobby-room', {
|
||||
room = room;
|
||||
reason = 'waiting-for-host',
|
||||
skip_display_name_check = true;
|
||||
});
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
process_host_module(lobby_muc_component_config, function(host_module, host)
|
||||
-- lobby muc component created
|
||||
module:log('info', 'Lobby component loaded %s', host);
|
||||
lobby_host = module:context(host_module);
|
||||
end);
|
||||
199
resources/prosody-plugins/mod_persistent_lobby.lua
Normal file
199
resources/prosody-plugins/mod_persistent_lobby.lua
Normal file
@@ -0,0 +1,199 @@
|
||||
-- This module allows lobby room to be created even when the main room is empty.
|
||||
-- Without this module, the empty main room will get deleted after grace period
|
||||
-- which triggers lobby room deletion even if there are still people in the lobby.
|
||||
--
|
||||
-- This module should be added to the main virtual host domain.
|
||||
-- It assumes you have properly configured the muc_lobby_rooms module and lobby muc component.
|
||||
--
|
||||
-- To trigger creation of lobby room:
|
||||
-- prosody.events.fire_event("create-persistent-lobby-room", { room = room; });
|
||||
--
|
||||
module:depends('room_destroy');
|
||||
|
||||
local util = module:require "util";
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local main_muc_component_host = module:get_option_string('main_muc');
|
||||
local lobby_muc_component_host = module:get_option_string('lobby_muc');
|
||||
|
||||
|
||||
if main_muc_component_host == nil then
|
||||
module:log('error', 'main_muc not configured. Cannot proceed.');
|
||||
return;
|
||||
end
|
||||
|
||||
if lobby_muc_component_host == nil then
|
||||
module:log('error', 'lobby not enabled missing lobby_muc config');
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- Helper function to wait till a component is loaded before running the given callback
|
||||
local function run_when_component_loaded(component_host_name, callback)
|
||||
local function trigger_callback()
|
||||
module:log('info', 'Component loaded %s', component_host_name);
|
||||
callback(module:context(component_host_name), component_host_name);
|
||||
end
|
||||
|
||||
if prosody.hosts[component_host_name] == nil then
|
||||
module:log('debug', 'Host %s not yet loaded. Will trigger when it is loaded.', component_host_name);
|
||||
prosody.events.add_handler('host-activated', function (host)
|
||||
if host == component_host_name then
|
||||
trigger_callback();
|
||||
end
|
||||
end);
|
||||
else
|
||||
trigger_callback();
|
||||
end
|
||||
end
|
||||
|
||||
-- Helper function to wait till a component's muc module is loaded before running the given callback
|
||||
local function run_when_muc_module_loaded(component_host_module, component_host_name, callback)
|
||||
local function trigger_callback()
|
||||
module:log('info', 'MUC module loaded for %s', component_host_name);
|
||||
callback(prosody.hosts[component_host_name].modules.muc, component_host_module);
|
||||
end
|
||||
|
||||
if prosody.hosts[component_host_name].modules.muc == nil then
|
||||
module:log('debug', 'MUC module for %s not yet loaded. Will trigger when it is loaded.', component_host_name);
|
||||
prosody.hosts[component_host_name].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
trigger_callback();
|
||||
end
|
||||
end);
|
||||
else
|
||||
trigger_callback()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local lobby_muc_service;
|
||||
local main_muc_service;
|
||||
local main_muc_module;
|
||||
|
||||
|
||||
-- Helper methods to track rooms that have persistent lobby
|
||||
local function set_persistent_lobby(room)
|
||||
room._data.persist_lobby = true;
|
||||
end
|
||||
|
||||
local function has_persistent_lobby(room)
|
||||
if room._data.persist_lobby == true then
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Helper method to trigger main room destroy
|
||||
local function trigger_room_destroy(room)
|
||||
prosody.events.fire_event("maybe-destroy-room", {
|
||||
room = room;
|
||||
reason = 'main room and lobby now empty';
|
||||
caller = module:get_name();
|
||||
});
|
||||
end
|
||||
|
||||
|
||||
-- For rooms with persistent lobby, we need to trigger deletion ourselves when both the main room
|
||||
-- and the lobby room are empty. This will be checked each time an occupant leaves the main room
|
||||
-- of if someone drops off the lobby.
|
||||
|
||||
|
||||
-- Handle events on main muc module
|
||||
run_when_component_loaded(main_muc_component_host, function(host_module, host_name)
|
||||
run_when_muc_module_loaded(host_module, host_name, function (main_muc, main_module)
|
||||
main_muc_service = main_muc; -- so it can be accessed from lobby muc event handlers
|
||||
main_muc_module = main_module;
|
||||
|
||||
main_module:hook("muc-occupant-left", function(event)
|
||||
-- Check if room should be destroyed when someone leaves the main room
|
||||
|
||||
local main_room = event.room;
|
||||
if is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
|
||||
return;
|
||||
end
|
||||
|
||||
local lobby_room_jid = main_room._data.lobbyroom;
|
||||
|
||||
-- If occupant leaving results in main room being empty, we trigger room destroy if
|
||||
-- a) lobby exists and is not empty
|
||||
-- b) lobby does not exist (possible for lobby to be disabled manually by moderator in meeting)
|
||||
--
|
||||
-- (main room destroy also triggers lobby room destroy in muc_lobby_rooms)
|
||||
if not main_room:has_occupant() then
|
||||
if lobby_room_jid == nil then -- lobby disabled
|
||||
trigger_room_destroy(main_room);
|
||||
else -- lobby exists
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if lobby_room and not lobby_room:has_occupant() then
|
||||
trigger_room_destroy(main_room);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
-- Handle events on lobby muc module
|
||||
run_when_component_loaded(lobby_muc_component_host, function(host_module, host_name)
|
||||
run_when_muc_module_loaded(host_module, host_name, function (lobby_muc, lobby_module)
|
||||
lobby_muc_service = lobby_muc; -- so it can be accessed from main muc event handlers
|
||||
|
||||
lobby_module:hook("muc-occupant-left", function(event)
|
||||
-- Check if room should be destroyed when someone leaves the lobby
|
||||
|
||||
local lobby_room = event.room;
|
||||
local main_room = lobby_room.main_room;
|
||||
|
||||
if not main_room or is_healthcheck_room(main_room.jid) or not has_persistent_lobby(main_room) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- If both lobby room and main room are empty, we destroy main room.
|
||||
-- (main room destroy also triggers lobby room destroy in muc_lobby_rooms)
|
||||
if not lobby_room:has_occupant() and main_room and not main_room:has_occupant() then
|
||||
trigger_room_destroy(main_room);
|
||||
end
|
||||
|
||||
end);
|
||||
end);
|
||||
end);
|
||||
|
||||
|
||||
function handle_create_persistent_lobby(event)
|
||||
local room = event.room;
|
||||
prosody.events.fire_event("create-lobby-room", event);
|
||||
|
||||
set_persistent_lobby(room);
|
||||
room:set_persistent(true);
|
||||
end
|
||||
|
||||
|
||||
module:hook_global('create-persistent-lobby-room', handle_create_persistent_lobby);
|
||||
|
||||
|
||||
-- Stop other modules from destroying room if persistent lobby not empty
|
||||
function handle_maybe_destroy_main_room(event)
|
||||
local main_room = event.room;
|
||||
local caller = event.caller;
|
||||
|
||||
if caller == module:get_name() then
|
||||
-- we were the one that requested the deletion. Do not override.
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- deletion was requested by another module. Check for lobby occupants.
|
||||
if has_persistent_lobby(main_room) and main_room._data.lobbyroom then
|
||||
local lobby_room_jid = main_room._data.lobbyroom;
|
||||
local lobby_room = lobby_muc_service.get_room_from_jid(lobby_room_jid);
|
||||
if lobby_room and lobby_room:has_occupant() then
|
||||
module:log('info', 'Suppressing room destroy. Persistent lobby still occupied %s', lobby_room_jid);
|
||||
return true; -- stop room destruction
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_maybe_destroy_main_room);
|
||||
207
resources/prosody-plugins/mod_polls.lua
Normal file
207
resources/prosody-plugins/mod_polls.lua
Normal file
@@ -0,0 +1,207 @@
|
||||
-- This module provides persistence for the "polls" feature,
|
||||
-- by keeping track of the state of polls in each room, and sending
|
||||
-- that state to new participants when they join.
|
||||
|
||||
local json = require 'cjson.safe';
|
||||
local st = require("util.stanza");
|
||||
local jid = require "util.jid";
|
||||
local util = module:require("util");
|
||||
local muc = module:depends("muc");
|
||||
|
||||
local NS_NICK = 'http://jabber.org/protocol/nick';
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
|
||||
local POLLS_LIMIT = 128;
|
||||
local POLL_PAYLOAD_LIMIT = 1024;
|
||||
|
||||
-- Logs a warning and returns true if a room does not
|
||||
-- have poll data associated with it.
|
||||
local function check_polls(room)
|
||||
if room.polls == nil then
|
||||
module:log("warn", "no polls data in room");
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
--- Returns a table having occupant id and occupant name.
|
||||
--- If the id cannot be extracted from nick a nil value is returned
|
||||
--- if the occupant name cannot be extracted from presence the Fellow Jitster
|
||||
--- name is used
|
||||
local function get_occupant_details(occupant)
|
||||
if not occupant then
|
||||
return nil
|
||||
end
|
||||
local presence = occupant:get_presence();
|
||||
local occupant_name;
|
||||
if presence then
|
||||
occupant_name = presence:get_child("nick", NS_NICK) and presence:get_child("nick", NS_NICK):get_text() or 'Fellow Jitster';
|
||||
else
|
||||
occupant_name = 'Fellow Jitster'
|
||||
end
|
||||
local _, _, occupant_id = jid.split(occupant.nick)
|
||||
if not occupant_id then
|
||||
return nil
|
||||
end
|
||||
return { ["occupant_id"] = occupant_id, ["occupant_name"] = occupant_name }
|
||||
end
|
||||
|
||||
-- Sets up poll data in new rooms.
|
||||
module:hook("muc-room-created", function(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then return end
|
||||
module:log("debug", "setting up polls in room %s", room.jid);
|
||||
room.polls = {
|
||||
by_id = {};
|
||||
order = {};
|
||||
count = 0;
|
||||
};
|
||||
end);
|
||||
|
||||
-- Keeps track of the current state of the polls in each room,
|
||||
-- by listening to "new-poll" and "answer-poll" messages,
|
||||
-- and updating the room poll data accordingly.
|
||||
-- This mirrors the client-side poll update logic.
|
||||
module:hook('jitsi-endpoint-message-received', function(event)
|
||||
local data, error, occupant, room, origin, stanza
|
||||
= event.message, event.error, event.occupant, event.room, event.origin, event.stanza;
|
||||
|
||||
if not data or (data.type ~= "new-poll" and data.type ~= "answer-poll") then
|
||||
return;
|
||||
end
|
||||
|
||||
if string.len(event.raw_message) >= POLL_PAYLOAD_LIMIT then
|
||||
module:log('error', 'Poll payload too large, discarding. Sender: %s to:%s', stanza.attr.from, stanza.attr.to);
|
||||
return true;
|
||||
end
|
||||
|
||||
if data.type == "new-poll" then
|
||||
if check_polls(room) then return end
|
||||
|
||||
local poll_creator = get_occupant_details(occupant)
|
||||
if not poll_creator then
|
||||
module:log("error", "Cannot retrieve poll creator id and name for %s from %s", occupant.jid, room.jid)
|
||||
return
|
||||
end
|
||||
|
||||
if room.polls.count >= POLLS_LIMIT then
|
||||
module:log("error", "Too many polls created in %s", room.jid)
|
||||
return true;
|
||||
end
|
||||
|
||||
if room.polls.by_id[data.pollId] ~= nil then
|
||||
module:log("error", "Poll already exists: %s", data.pollId);
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Poll already exists'));
|
||||
return true;
|
||||
end
|
||||
|
||||
if room.jitsiMetadata and room.jitsiMetadata.permissions
|
||||
and room.jitsiMetadata.permissions.pollCreationRestricted
|
||||
and not is_feature_allowed('create-polls', origin.jitsi_meet_context_features) then
|
||||
origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Creation of polls not allowed for user'));
|
||||
return true;
|
||||
end
|
||||
|
||||
local answers = {}
|
||||
local compact_answers = {}
|
||||
for i, name in ipairs(data.answers) do
|
||||
table.insert(answers, { name = name, voters = {} });
|
||||
table.insert(compact_answers, { key = i, name = name});
|
||||
end
|
||||
|
||||
local poll = {
|
||||
id = data.pollId,
|
||||
sender_id = poll_creator.occupant_id,
|
||||
sender_name = poll_creator.occupant_name,
|
||||
question = data.question,
|
||||
answers = answers
|
||||
};
|
||||
|
||||
room.polls.by_id[data.pollId] = poll
|
||||
table.insert(room.polls.order, poll)
|
||||
room.polls.count = room.polls.count + 1;
|
||||
|
||||
local pollData = {
|
||||
event = event,
|
||||
room = room,
|
||||
poll = {
|
||||
pollId = data.pollId,
|
||||
senderId = poll_creator.occupant_id,
|
||||
senderName = poll_creator.occupant_name,
|
||||
question = data.question,
|
||||
answers = compact_answers
|
||||
}
|
||||
}
|
||||
module:fire_event("poll-created", pollData);
|
||||
elseif data.type == "answer-poll" then
|
||||
if check_polls(room) then return end
|
||||
|
||||
local poll = room.polls.by_id[data.pollId];
|
||||
if poll == nil then
|
||||
module:log("warn", "answering inexistent poll");
|
||||
return;
|
||||
end
|
||||
|
||||
local voter = get_occupant_details(occupant)
|
||||
if not voter then
|
||||
module:log("error", "Cannot retrieve voter id and name for %s from %s", occupant.jid, room.jid)
|
||||
return
|
||||
end
|
||||
|
||||
local answers = {};
|
||||
for vote_option_idx, vote_flag in ipairs(data.answers) do
|
||||
table.insert(answers, {
|
||||
key = vote_option_idx,
|
||||
value = vote_flag,
|
||||
name = poll.answers[vote_option_idx].name,
|
||||
});
|
||||
poll.answers[vote_option_idx].voters[voter.occupant_id] = vote_flag and voter.occupant_name or nil;
|
||||
end
|
||||
local answerData = {
|
||||
event = event,
|
||||
room = room,
|
||||
pollId = poll.id,
|
||||
voterName = voter.occupant_name,
|
||||
voterId = voter.occupant_id,
|
||||
answers = answers
|
||||
}
|
||||
module:fire_event("answer-poll", answerData);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Sends the current poll state to new occupants after joining a room.
|
||||
module:hook("muc-occupant-joined", function(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then return end
|
||||
if room.polls == nil or #room.polls.order == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local data = {
|
||||
type = "old-polls",
|
||||
polls = {},
|
||||
};
|
||||
for i, poll in ipairs(room.polls.order) do
|
||||
data.polls[i] = {
|
||||
id = poll.id,
|
||||
senderId = poll.sender_id,
|
||||
senderName = poll.sender_name,
|
||||
question = poll.question,
|
||||
answers = poll.answers
|
||||
};
|
||||
end
|
||||
|
||||
local json_msg_str, error = json.encode(data);
|
||||
if not json_msg_str then
|
||||
module:log('error', 'Error encoding data room:%s error:%s', room.jid, error);
|
||||
end
|
||||
|
||||
local stanza = st.message({
|
||||
from = room.jid,
|
||||
to = event.occupant.jid
|
||||
})
|
||||
:tag("json-message", { xmlns = "http://jitsi.org/jitmeet" })
|
||||
:text(json_msg_str)
|
||||
:up();
|
||||
room:route_stanza(stanza);
|
||||
end);
|
||||
21
resources/prosody-plugins/mod_poltergeist_component.lua
Normal file
21
resources/prosody-plugins/mod_poltergeist_component.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
local st = require "util.stanza";
|
||||
|
||||
-- A component which we use to receive all stanzas for the created poltergeists
|
||||
-- replays with error if an iq is sent
|
||||
function no_action()
|
||||
return true;
|
||||
end
|
||||
|
||||
function error_reply(event)
|
||||
module:send(st.error_reply(event.stanza, "cancel", "service-unavailable"));
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("presence/host", no_action);
|
||||
module:hook("message/host", no_action);
|
||||
module:hook("presence/full", no_action);
|
||||
module:hook("message/full", no_action);
|
||||
|
||||
module:hook("iq/host", error_reply);
|
||||
module:hook("iq/full", error_reply);
|
||||
module:hook("iq/bare", error_reply);
|
||||
19
resources/prosody-plugins/mod_presence_identity.lua
Normal file
19
resources/prosody-plugins/mod_presence_identity.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
local stanza = require "util.stanza";
|
||||
local update_presence_identity = module:require "util".update_presence_identity;
|
||||
|
||||
-- For all received presence messages, if the jitsi_meet_context_(user|group)
|
||||
-- values are set in the session, then insert them into the presence messages
|
||||
-- for that session.
|
||||
function on_message(event)
|
||||
local stanza, session = event.stanza, event.origin;
|
||||
if stanza and session then
|
||||
update_presence_identity(
|
||||
stanza,
|
||||
session.jitsi_meet_context_user,
|
||||
session.jitsi_meet_context_group
|
||||
);
|
||||
end
|
||||
end
|
||||
|
||||
module:hook("pre-presence/bare", on_message);
|
||||
module:hook("pre-presence/full", on_message);
|
||||
234
resources/prosody-plugins/mod_rate_limit.lua
Normal file
234
resources/prosody-plugins/mod_rate_limit.lua
Normal file
@@ -0,0 +1,234 @@
|
||||
-- Rate limits connection based on their ip address.
|
||||
-- Rate limits creating sessions (new connections),
|
||||
-- rate limits sent stanzas from same ip address (presence, iq, messages)
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local cache = require"util.cache";
|
||||
local ceil = math.ceil;
|
||||
local http_server = require "net.http.server";
|
||||
local gettime = require "util.time".now
|
||||
local filters = require "util.filters";
|
||||
local new_throttle = require "util.throttle".create;
|
||||
local timer = require "util.timer";
|
||||
local ip_util = require "util.ip";
|
||||
local new_ip = ip_util.new_ip;
|
||||
local match_ip = ip_util.match;
|
||||
local parse_cidr = ip_util.parse_cidr;
|
||||
local get_ip = module:require "util".get_ip;
|
||||
|
||||
local config = {};
|
||||
local limits_resolution = 1;
|
||||
|
||||
local function load_config()
|
||||
-- Max allowed login rate in events per second.
|
||||
config.login_rate = module:get_option_number("rate_limit_login_rate", 3);
|
||||
-- The rate to which sessions from IPs exceeding the join rate will be limited, in bytes per second.
|
||||
config.ip_rate = module:get_option_number("rate_limit_ip_rate", 2000);
|
||||
-- The rate to which sessions exceeding the stanza(iq, presence, message) rate will be limited, in bytes per second.
|
||||
config.session_rate = module:get_option_number("rate_limit_session_rate", 1000);
|
||||
-- The time in seconds, after which the limit for an IP address is lifted.
|
||||
config.timeout = module:get_option_number("rate_limit_timeout", 60);
|
||||
-- List of regular expressions for IP addresses that are not limited by this module.
|
||||
config.whitelist = module:get_option_set("rate_limit_whitelist", { "127.0.0.1", "::1" })._items;
|
||||
-- The size of the cache that saves state for IP addresses
|
||||
config.cache_size = module:get_option_number("rate_limit_cache_size", 10000);
|
||||
|
||||
-- Max allowed presence rate in events per second.
|
||||
config.presence_rate = module:get_option_number("rate_limit_presence_rate", 4);
|
||||
-- Max allowed iq rate in events per second.
|
||||
config.iq_rate = module:get_option_number("rate_limit_iq_rate", 15);
|
||||
-- Max allowed message rate in events per second.
|
||||
config.message_rate = module:get_option_number("rate_limit_message_rate", 3);
|
||||
|
||||
-- A list of hosts for which sessions we ignore rate limiting
|
||||
config.whitelist_hosts = module:get_option_set("rate_limit_whitelist_hosts", {});
|
||||
|
||||
local wl = "";
|
||||
for ip in config.whitelist do wl = wl .. ip .. "," end
|
||||
local wl_hosts = "";
|
||||
for j in config.whitelist_hosts do wl_hosts = wl_hosts .. j .. "," end
|
||||
module:log("info", "Loaded configuration: ");
|
||||
module:log("info", "- ip_rate=%s bytes/sec, session_rate=%s bytes/sec, timeout=%s sec, cache size=%s, whitelist=%s, whitelist_hosts=%s",
|
||||
config.ip_rate, config.session_rate, config.timeout, config.cache_size, wl, wl_hosts);
|
||||
module:log("info", "- login_rate=%s/sec, presence_rate=%s/sec, iq_rate=%s/sec, message_rate=%s/sec",
|
||||
config.login_rate, config.presence_rate, config.iq_rate, config.message_rate);
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- Maps an IP address to a util.throttle which keeps the rate of login/join events from that IP.
|
||||
local login_rates = cache.new(config.cache_size);
|
||||
|
||||
-- Keeps the IP addresses that have exceeded the allowed login/join rate (i.e. the IP addresses whose sessions need
|
||||
-- to be limited). Mapped to the last instant at which the rate was exceeded.
|
||||
local limited_ips = cache.new(config.cache_size);
|
||||
|
||||
local function is_whitelisted(ip)
|
||||
local parsed_ip = new_ip(ip)
|
||||
for entry in config.whitelist do
|
||||
if match_ip(parsed_ip, parse_cidr(entry)) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
local function is_whitelisted_host(h)
|
||||
return config.whitelist_hosts:contains(h);
|
||||
end
|
||||
|
||||
-- Add an IP to the set of limied IPs
|
||||
local function limit_ip(ip)
|
||||
module:log("info", "Limiting %s due to login/join rate exceeded.", ip);
|
||||
limited_ips:set(ip, gettime());
|
||||
end
|
||||
|
||||
-- Installable as a session filter to limit the reading rate for a session. Based on mod_limits.
|
||||
local function limit_bytes_in(bytes, session)
|
||||
local sess_throttle = session.jitsi_throttle;
|
||||
if sess_throttle then
|
||||
-- if the limit timeout has elapsed let's stop the throttle
|
||||
if not sess_throttle.start or gettime() - sess_throttle.start > config.timeout then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.jitsi_throttle = nil;
|
||||
return bytes;
|
||||
end
|
||||
local ok, _, outstanding = sess_throttle:poll(#bytes, true);
|
||||
if not ok then
|
||||
session.log("debug",
|
||||
"Session over rate limit (%d) with %d (by %d), pausing", sess_throttle.max, #bytes, outstanding);
|
||||
outstanding = ceil(outstanding);
|
||||
session.conn:pause(); -- Read no more data from the connection until there is no outstanding data
|
||||
local outstanding_data = bytes:sub(-outstanding);
|
||||
bytes = bytes:sub(1, #bytes-outstanding);
|
||||
timer.add_task(limits_resolution, function ()
|
||||
if not session.conn then return; end
|
||||
if sess_throttle:peek(#outstanding_data) then
|
||||
session.log("debug", "Resuming paused session");
|
||||
session.conn:resume();
|
||||
end
|
||||
-- Handle what we can of the outstanding data
|
||||
session.data(outstanding_data);
|
||||
end);
|
||||
end
|
||||
end
|
||||
return bytes;
|
||||
end
|
||||
|
||||
-- Throttles reading from the connection of a specific session.
|
||||
local function throttle_session(session, rate, timeout)
|
||||
if not session.jitsi_throttle then
|
||||
if (session.conn and session.conn.setlimit) then
|
||||
session.jitsi_throttle_counter = session.jitsi_throttle_counter + 1;
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via setlimit, session=%s, ip=%s, counter=%s.",
|
||||
rate, session.id, session.ip, session.jitsi_throttle_counter);
|
||||
session.conn:setlimit(rate);
|
||||
if timeout then
|
||||
if session.jitsi_throttle_timer then
|
||||
-- if there was a timer stop it as we will schedule a new one
|
||||
session.jitsi_throttle_timer:stop();
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end
|
||||
session.jitsi_throttle_timer = module:add_timer(timeout, function()
|
||||
if session.conn then
|
||||
module:log("info", "Stop throttling session=%s, ip=%s.", session.id, session.ip);
|
||||
session.conn:setlimit(0);
|
||||
end
|
||||
session.jitsi_throttle_timer = nil;
|
||||
end);
|
||||
end
|
||||
else
|
||||
module:log("info", "Enabling throttle (%s bytes/s) via filter, session=%s, ip=%s.", rate, session.id, session.ip);
|
||||
session.jitsi_throttle = new_throttle(rate, 2);
|
||||
filters.add_filter(session, "bytes/in", limit_bytes_in, 1000);
|
||||
-- throttle.start used for stop throttling after the timeout
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
else
|
||||
-- update the throttling start
|
||||
session.jitsi_throttle.start = gettime();
|
||||
end
|
||||
end
|
||||
|
||||
-- checks different stanzas for rate limiting (per session)
|
||||
function filter_stanza(stanza, session)
|
||||
local rate = session[stanza.name.."_rate"];
|
||||
if rate then
|
||||
local ok, _, _ = rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "%s rate exceeded for %s, limiting.", stanza.name, session.full_jid);
|
||||
throttle_session(session, config.session_rate, config.timeout);
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local function on_login(session, ip)
|
||||
local login_rate = login_rates:get(ip);
|
||||
if not login_rate then
|
||||
module:log("debug", "Create new join rate for %s", ip);
|
||||
login_rate = new_throttle(config.login_rate, 2);
|
||||
login_rates:set(ip, login_rate);
|
||||
end
|
||||
|
||||
local ok, _, _ = login_rate:poll(1, true);
|
||||
if not ok then
|
||||
module:log("info", "Join rate exceeded for %s, limiting.", ip);
|
||||
limit_ip(ip);
|
||||
end
|
||||
end
|
||||
|
||||
local function filter_hook(session)
|
||||
-- ignore outgoing sessions (s2s)
|
||||
if session.outgoing then
|
||||
return;
|
||||
end
|
||||
|
||||
local ip = get_ip(session);
|
||||
module:log("debug", "New session from %s", ip);
|
||||
if is_whitelisted(ip) or is_whitelisted_host(session.host) then
|
||||
return;
|
||||
end
|
||||
|
||||
on_login(session, ip);
|
||||
|
||||
-- creates the stanzas rates
|
||||
session.jitsi_throttle_counter = 0;
|
||||
session.presence_rate = new_throttle(config.presence_rate, 2);
|
||||
session.iq_rate = new_throttle(config.iq_rate, 2);
|
||||
session.message_rate = new_throttle(config.message_rate, 2);
|
||||
filters.add_filter(session, "stanzas/in", filter_stanza);
|
||||
|
||||
local oldt = limited_ips:get(ip);
|
||||
if oldt then
|
||||
local newt = gettime();
|
||||
local elapsed = newt - oldt;
|
||||
if elapsed < config.timeout then
|
||||
if elapsed < 5 then
|
||||
module:log("info", "IP address %s was limited %s seconds ago, refreshing.", ip, elapsed);
|
||||
limited_ips:set(ip, newt);
|
||||
end
|
||||
throttle_session(session, config.ip_rate);
|
||||
else
|
||||
module:log("info", "Removing the limit for %s", ip);
|
||||
limited_ips:set(ip, nil);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function module.load()
|
||||
filters.add_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
function module.unload()
|
||||
filters.remove_filter_hook(filter_hook);
|
||||
end
|
||||
|
||||
module:hook_global("config-reloaded", load_config);
|
||||
|
||||
-- we calculate the stats on the configured interval (60 seconds by default)
|
||||
local measure_limited_ips = module:measure('limited-ips', 'amount'); -- we send stats for the total number limited ips
|
||||
module:hook_global('stats-update', function ()
|
||||
measure_limited_ips(limited_ips:count());
|
||||
end);
|
||||
695
resources/prosody-plugins/mod_reservations.lua
Normal file
695
resources/prosody-plugins/mod_reservations.lua
Normal file
@@ -0,0 +1,695 @@
|
||||
--- This is a port of Jicofo's Reservation System as a prosody module
|
||||
-- ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
||||
--
|
||||
-- We try to retain the same behaviour and interfaces where possible, but there
|
||||
-- is some difference:
|
||||
-- * In the event that the DELETE call fails, Jicofo's reservation
|
||||
-- system retains reservation data and allows re-creation of room if requested by
|
||||
-- the same creator without making further call to the API; this module does not
|
||||
-- offer this behaviour. Re-creation of a closed room will behave like a new meeting
|
||||
-- and trigger a new API call to validate the reservation.
|
||||
-- * Jicofo's reservation system expect int-based conflict_id. We take any sensible string.
|
||||
--
|
||||
-- In broad strokes, this module works by intercepting Conference IQs sent to focus component
|
||||
-- and buffers it until reservation is confirmed (by calling the provided API endpoint).
|
||||
-- The IQ events are routed on to focus component if reservation is valid, or error
|
||||
-- response is sent back to the origin if reservation is denied. Events are routed as usual
|
||||
-- if the room already exists.
|
||||
--
|
||||
--
|
||||
-- Installation:
|
||||
-- =============
|
||||
--
|
||||
-- Under domain config,
|
||||
-- 1. add "reservations" to modules_enabled.
|
||||
-- 2. Specify URL base for your API endpoint using "reservations_api_prefix" (required)
|
||||
-- 3. Optional config:
|
||||
-- * set "reservations_api_timeout" to change API call timeouts (defaults to 20 seconds)
|
||||
-- * set "reservations_api_headers" to specify custom HTTP headers included in
|
||||
-- all API calls e.g. to provide auth tokens.
|
||||
-- * set "reservations_api_retry_count" to the number of times API call failures are retried (defaults to 3)
|
||||
-- * set "reservations_api_retry_delay" seconds to wait between retries (defaults to 3s)
|
||||
-- * set "reservations_api_should_retry_for_code" to a function that takes an HTTP response code and
|
||||
-- returns true if API call should be retried. By default, retries are done for 5XX
|
||||
-- responses. Timeouts are never retried, and HTTP call failures are always retried.
|
||||
-- * set "reservations_enable_max_occupants" to true to enable integration with
|
||||
-- mod_muc_max_occupants. Setting thia will allow optional "max_occupants" (integer)
|
||||
-- payload from API to influence max occupants allowed for a given room.
|
||||
-- * set "reservations_enable_lobby_support" to true to enable integration
|
||||
-- with "muc_lobby_rooms". Setting this will allow optional "lobby" (boolean)
|
||||
-- fields in API payload. If set to true, Lobby will be enabled for the room.
|
||||
-- "persistent_lobby" module must also be enabled for this to work.
|
||||
-- * set "reservations_enable_password_support" to allow optional "password" (string)
|
||||
-- field in API payload. If set and not empty, then room password will be set
|
||||
-- to the given string.
|
||||
-- * By default, reservation checks are skipped for breakout rooms. You can subject
|
||||
-- breakout rooms to the same checks by setting "reservations_skip_breakout_rooms" to false.
|
||||
--
|
||||
--
|
||||
-- Example config:
|
||||
--
|
||||
-- VirtualHost "jitmeet.example.com"
|
||||
-- modules_enabled = {
|
||||
-- "reservations";
|
||||
-- }
|
||||
-- reservations_api_prefix = "http://reservation.example.com"
|
||||
--
|
||||
-- --- The following are all optional
|
||||
-- reservations_api_headers = {
|
||||
-- ["Authorization"] = "Bearer TOKEN-237958623045";
|
||||
-- }
|
||||
-- reservations_api_timeout = 10 -- timeout if API does not respond within 10s
|
||||
-- reservations_api_retry_count = 5 -- retry up to 5 times
|
||||
-- reservations_api_retry_delay = 1 -- wait 1s between retries
|
||||
-- reservations_api_should_retry_for_code = function (code)
|
||||
-- return code >= 500 or code == 408
|
||||
-- end
|
||||
--
|
||||
-- reservations_enable_max_occupants = true -- support "max_occupants" field
|
||||
-- reservations_enable_lobby_support = true -- support "lobby" field
|
||||
-- reservations_enable_password_support = true -- support "password" field
|
||||
--
|
||||
|
||||
local jid = require 'util.jid';
|
||||
local http = require "net.http";
|
||||
local json = require 'cjson.safe';
|
||||
local st = require "util.stanza";
|
||||
local timer = require 'util.timer';
|
||||
local datetime = require 'util.datetime';
|
||||
|
||||
local util = module:require "util";
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
|
||||
local api_prefix = module:get_option("reservations_api_prefix");
|
||||
local api_headers = module:get_option("reservations_api_headers");
|
||||
local api_timeout = module:get_option("reservations_api_timeout", 20);
|
||||
local api_retry_count = tonumber(module:get_option("reservations_api_retry_count", 3));
|
||||
local api_retry_delay = tonumber(module:get_option("reservations_api_retry_delay", 3));
|
||||
local max_occupants_enabled = module:get_option("reservations_enable_max_occupants", false);
|
||||
local lobby_support_enabled = module:get_option("reservations_enable_lobby_support", false);
|
||||
local password_support_enabled = module:get_option("reservations_enable_password_support", false);
|
||||
local skip_breakout_room = module:get_option("reservations_skip_breakout_rooms", true);
|
||||
|
||||
|
||||
-- Option for user to control HTTP response codes that will result in a retry.
|
||||
-- Defaults to returning true on any 5XX code or 0
|
||||
local api_should_retry_for_code = module:get_option("reservations_api_should_retry_for_code", function (code)
|
||||
return code >= 500;
|
||||
end)
|
||||
|
||||
|
||||
local muc_component_host = module:get_option_string("main_muc");
|
||||
local breakout_muc_component_host = module:get_option_string('breakout_rooms_muc', 'breakout.'..module.host);
|
||||
|
||||
|
||||
-- How often to check and evict expired reservation data
|
||||
local expiry_check_period = 60;
|
||||
|
||||
|
||||
-- Cannot proceed if "reservations_api_prefix" not configured
|
||||
if not api_prefix then
|
||||
module:log("error", "reservations_api_prefix not specified. Disabling %s", module:get_name());
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- get/infer focus component hostname so we can intercept IQ bound for it
|
||||
local focus_component_host = module:get_option_string("focus_component");
|
||||
if not focus_component_host then
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
if not muc_domain_base then
|
||||
module:log("error", "Could not infer focus domain. Disabling %s", module:get_name());
|
||||
return;
|
||||
end
|
||||
focus_component_host = 'focus.'..muc_domain_base;
|
||||
end
|
||||
|
||||
-- common HTTP headers added to all API calls
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")";
|
||||
};
|
||||
if api_headers then -- extra headers from config
|
||||
for key, value in pairs(api_headers) do
|
||||
http_headers[key] = value;
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Utils
|
||||
|
||||
--- Converts int timestamp to datetime string compatible with Java SimpleDateFormat
|
||||
-- @param t timestamps in seconds. Supports int (as returned by os.time()) or higher
|
||||
-- precision (as returned by socket.gettime())
|
||||
-- @return formatted datetime string (yyyy-MM-dd'T'HH:mm:ss.SSSX)
|
||||
local function to_java_date_string(t)
|
||||
local t_secs, mantissa = math.modf(t);
|
||||
local ms_str = (mantissa == 0) and '.000' or tostring(mantissa):sub(2,5);
|
||||
local date_str = os.date("!%Y-%m-%dT%H:%M:%S", t_secs);
|
||||
return date_str..ms_str..'Z';
|
||||
end
|
||||
|
||||
|
||||
--- Start non-blocking HTTP call
|
||||
-- @param url URL to call
|
||||
-- @param options options table as expected by net.http where we provide optional headers, body or method.
|
||||
-- @param callback if provided, called with callback(response_body, response_code) when call complete.
|
||||
-- @param timeout_callback if provided, called without args when request times out.
|
||||
-- @param retries how many times to retry on failure; 0 means no retries.
|
||||
local function async_http_request(url, options, callback, timeout_callback, retries)
|
||||
local completed = false;
|
||||
local timed_out = false;
|
||||
local retries = retries or api_retry_count;
|
||||
|
||||
local function cb_(response_body, response_code)
|
||||
if not timed_out then -- request completed before timeout
|
||||
completed = true;
|
||||
if (response_code == 0 or api_should_retry_for_code(response_code)) and retries > 0 then
|
||||
module:log("warn", "API Response code %d. Will retry after %ds", response_code, api_retry_delay);
|
||||
timer.add_task(api_retry_delay, function()
|
||||
async_http_request(url, options, callback, timeout_callback, retries - 1)
|
||||
end)
|
||||
return;
|
||||
end
|
||||
|
||||
if callback then
|
||||
callback(response_body, response_code)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local request = http.request(url, options, cb_);
|
||||
|
||||
timer.add_task(api_timeout, function ()
|
||||
timed_out = true;
|
||||
|
||||
if not completed then
|
||||
http.destroy_request(request);
|
||||
if timeout_callback then
|
||||
timeout_callback()
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
end
|
||||
|
||||
--- Returns current timestamp
|
||||
local function now()
|
||||
-- Don't really need higher precision of socket.gettime(). Besides, we loose
|
||||
-- milliseconds precision when converting back to timestamp from date string
|
||||
-- when we use datetime.parse(t), so let's be consistent.
|
||||
return os.time();
|
||||
end
|
||||
|
||||
--- Start RoomReservation implementation
|
||||
|
||||
-- Status enums used in RoomReservation:meta.status
|
||||
local STATUS = {
|
||||
PENDING = 0;
|
||||
SUCCESS = 1;
|
||||
FAILED = -1;
|
||||
}
|
||||
|
||||
local RoomReservation = {};
|
||||
RoomReservation.__index = RoomReservation;
|
||||
|
||||
function newRoomReservation(room_jid, creator_jid)
|
||||
return setmetatable({
|
||||
room_jid = room_jid;
|
||||
|
||||
-- Reservation metadata. store as table so we can set and read atomically.
|
||||
-- N.B. This should always be updated using self.set_status_*
|
||||
meta = {
|
||||
status = STATUS.PENDING;
|
||||
mail_owner = jid.bare(creator_jid);
|
||||
conflict_id = nil;
|
||||
start_time = now(); -- timestamp, in seconds
|
||||
expires_at = nil; -- timestamp, in seconds
|
||||
error_text = nil;
|
||||
error_code = nil;
|
||||
};
|
||||
|
||||
-- Array of pending events that we need to route once API call is complete
|
||||
pending_events = {};
|
||||
|
||||
-- Set true when API call trigger has been triggered (by enqueue of first event)
|
||||
api_call_triggered = false;
|
||||
}, RoomReservation);
|
||||
end
|
||||
|
||||
|
||||
--- Extracts room name from room jid
|
||||
function RoomReservation:get_room_name()
|
||||
return jid.node(self.room_jid);
|
||||
end
|
||||
|
||||
--- Checks if reservation data is expires and should be evicted from store
|
||||
function RoomReservation:is_expired()
|
||||
return self.meta.expires_at ~= nil and now() > self.meta.expires_at;
|
||||
end
|
||||
|
||||
--- Main entry point for handing and routing events.
|
||||
function RoomReservation:enqueue_or_route_event(event)
|
||||
if self.meta.status == STATUS.PENDING then
|
||||
table.insert(self.pending_events, event)
|
||||
if self.api_call_triggered ~= true then
|
||||
self:call_api_create_conference();
|
||||
end
|
||||
else
|
||||
-- API call already complete. Immediately route without enqueueing.
|
||||
-- This could happen if request comes in between the time reservation approved
|
||||
-- and when Jicofo actually creates the room.
|
||||
module:log("debug", "Reservation details already stored. Skipping queue for %s", self.room_jid);
|
||||
self:route_event(event);
|
||||
end
|
||||
end
|
||||
|
||||
--- Updates status and initiates event routing. Called internally when API call complete.
|
||||
function RoomReservation:set_status_success(start_time, duration, mail_owner, conflict_id, data)
|
||||
module:log("info", "Reservation created successfully for %s", self.room_jid);
|
||||
self.meta = {
|
||||
status = STATUS.SUCCESS;
|
||||
mail_owner = mail_owner or self.meta.mail_owner;
|
||||
conflict_id = conflict_id;
|
||||
start_time = start_time;
|
||||
expires_at = start_time + duration;
|
||||
error_text = nil;
|
||||
error_code = nil;
|
||||
}
|
||||
if max_occupants_enabled and data.max_occupants then
|
||||
self.meta.max_occupants = data.max_occupants
|
||||
end
|
||||
if lobby_support_enabled and data.lobby then
|
||||
self.meta.lobby = data.lobby
|
||||
end
|
||||
if password_support_enabled and data.password then
|
||||
self.meta.password = data.password
|
||||
end
|
||||
self:route_pending_events()
|
||||
end
|
||||
|
||||
--- Updates status and initiates error response to pending events. Called internally when API call complete.
|
||||
function RoomReservation:set_status_failed(error_code, error_text)
|
||||
module:log("info", "Reservation creation failed for %s - (%s) %s", self.room_jid, error_code, error_text);
|
||||
self.meta = {
|
||||
status = STATUS.FAILED;
|
||||
mail_owner = self.meta.mail_owner;
|
||||
conflict_id = nil;
|
||||
start_time = self.meta.start_time;
|
||||
-- Retain reservation rejection for a short while so we have time to report failure to
|
||||
-- existing clients and not trigger a re-query too soon.
|
||||
-- N.B. Expiry could take longer since eviction happens periodically.
|
||||
expires_at = now() + 30;
|
||||
error_text = error_text;
|
||||
error_code = error_code;
|
||||
}
|
||||
self:route_pending_events()
|
||||
end
|
||||
|
||||
--- Triggers routing of all enqueued events
|
||||
function RoomReservation:route_pending_events()
|
||||
if self.meta.status == STATUS.PENDING then -- should never be called while PENDING. check just in case.
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("debug", "Routing all pending events for %s", self.room_jid);
|
||||
local event;
|
||||
|
||||
while #self.pending_events ~= 0 do
|
||||
event = table.remove(self.pending_events);
|
||||
self:route_event(event)
|
||||
end
|
||||
end
|
||||
|
||||
--- Event routing implementation
|
||||
function RoomReservation:route_event(event)
|
||||
-- this should only be called after API call complete and status no longer PENDING
|
||||
assert(self.meta.status ~= STATUS.PENDING, "Attempting to route event while API call still PENDING")
|
||||
|
||||
local meta = self.meta;
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if meta.status == STATUS.FAILED then
|
||||
module:log("debug", "Route: Sending reservation error to %s", stanza.attr.from);
|
||||
self:reply_with_error(event, meta.error_code, meta.error_text);
|
||||
else
|
||||
if meta.status == STATUS.SUCCESS then
|
||||
if self:is_expired() then
|
||||
module:log("debug", "Route: Sending reservation expiry to %s", stanza.attr.from);
|
||||
self:reply_with_error(event, 419, "Reservation expired");
|
||||
else
|
||||
module:log("debug", "Route: Forwarding on event from %s", stanza.attr.from);
|
||||
prosody.core_post_stanza(origin, stanza, false); -- route iq to intended target (focus)
|
||||
end
|
||||
else
|
||||
-- this should never happen unless dev made a mistake. Block by default just in case.
|
||||
module:log("error", "Reservation for %s has invalid state %s. Rejecting request.", self.room_jid, meta.status);
|
||||
self:reply_with_error(event, 500, "Failed to determine reservation state");
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Generates reservation-error stanza and sends to event origin.
|
||||
function RoomReservation:reply_with_error(event, error_code, error_text)
|
||||
local stanza = event.stanza;
|
||||
local id = stanza.attr.id;
|
||||
local to = stanza.attr.from;
|
||||
local from = stanza.attr.to;
|
||||
|
||||
event.origin.send(
|
||||
st.iq({ type="error", to=to, from=from, id=id })
|
||||
:tag("error", { type="cancel" })
|
||||
:tag("service-unavailable", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):up()
|
||||
:tag("text", { xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" }):text(error_text):up()
|
||||
:tag("reservation-error", { xmlns="http://jitsi.org/protocol/focus", ["error-code"]=tostring(error_code) })
|
||||
);
|
||||
end
|
||||
|
||||
--- Initiates non-blocking API call to validate reservation
|
||||
function RoomReservation:call_api_create_conference()
|
||||
self.api_call_triggered = true;
|
||||
|
||||
local url = api_prefix..'/conference';
|
||||
local request_data = {
|
||||
name = self:get_room_name();
|
||||
start_time = to_java_date_string(self.meta.start_time);
|
||||
mail_owner = self.meta.mail_owner;
|
||||
}
|
||||
|
||||
local http_options = {
|
||||
body = http.formencode(request_data); -- because Jicofo reservation encodes as form data instead JSON
|
||||
method = 'POST';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
module:log("debug", "Sending POST /conference for %s", self.room_jid);
|
||||
async_http_request(url, http_options, function (response_body, response_code)
|
||||
self:on_api_create_conference_complete(response_body, response_code);
|
||||
end, function ()
|
||||
self:on_api_call_timeout();
|
||||
end);
|
||||
end
|
||||
|
||||
--- Parses and validates HTTP response body for conference payload
|
||||
-- Ref: https://github.com/jitsi/jicofo/blob/master/doc/reservation.md
|
||||
-- @return nil if invalid, or table with payload parsed from JSON response
|
||||
function RoomReservation:parse_conference_response(response_body)
|
||||
local data, error = json.decode(response_body);
|
||||
|
||||
if data == nil then -- invalid JSON payload
|
||||
module:log("error", "Invalid JSON response from API - %s error:%s", response_body, error);
|
||||
return;
|
||||
end
|
||||
|
||||
if data.name == nil or data.name:lower() ~= self:get_room_name() then
|
||||
module:log("error", "Missing or mismatching room name - %s", data.name);
|
||||
return;
|
||||
end
|
||||
|
||||
if data.id == nil then
|
||||
module:log("error", "Missing id");
|
||||
return;
|
||||
end
|
||||
|
||||
if data.mail_owner == nil then
|
||||
module:log("error", "Missing mail_owner");
|
||||
return;
|
||||
end
|
||||
|
||||
local duration = tonumber(data.duration);
|
||||
if duration == nil then
|
||||
module:log("error", "Missing or invalid duration - %s", data.duration);
|
||||
return;
|
||||
end
|
||||
data.duration = duration;
|
||||
|
||||
-- if optional "max_occupants" field set, cast to number
|
||||
if data.max_occupants ~= nil then
|
||||
local max_occupants = tonumber(data.max_occupants)
|
||||
if max_occupants == nil or max_occupants < 1 then
|
||||
-- N.B. invalid max_occupants rejected even if max_occupants_enabled=false
|
||||
module:log("error", "Invalid value for max_occupants - %s", data.max_occupants);
|
||||
return;
|
||||
end
|
||||
data.max_occupants = max_occupants
|
||||
end
|
||||
|
||||
-- if optional "lobby" field set, accept boolean true or "true"
|
||||
if data.lobby ~= nil then
|
||||
if (type(data.lobby) == "boolean" and data.lobby) or data.lobby == "true" then
|
||||
data.lobby = true
|
||||
else
|
||||
data.lobby = false
|
||||
end
|
||||
end
|
||||
|
||||
-- if optional "password" field set, it has to be string
|
||||
if data.password ~= nil then
|
||||
if type(data.password) ~= "string" then
|
||||
-- N.B. invalid "password" rejected even if reservations_enable_password_support=false
|
||||
module:log("error", "Invalid type for password - string expected");
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local start_time = datetime.parse(data.start_time); -- N.B. we lose milliseconds portion of the date
|
||||
if start_time == nil then
|
||||
module:log("error", "Missing or invalid start_time - %s", data.start_time);
|
||||
return;
|
||||
end
|
||||
data.start_time = start_time;
|
||||
|
||||
return data;
|
||||
end
|
||||
|
||||
--- Parses and validates HTTP error response body for API call.
|
||||
-- Expect JSON with a "message" field.
|
||||
-- @return message string, or generic error message if invalid payload.
|
||||
function RoomReservation:parse_error_message_from_response(response_body)
|
||||
local data = json.decode(response_body);
|
||||
if data ~= nil and data.message ~= nil then
|
||||
module:log("debug", "Invalid error response body. Will use generic error message.");
|
||||
return data.message;
|
||||
else
|
||||
return "Rejected by reservation server";
|
||||
end
|
||||
end
|
||||
|
||||
--- callback on API timeout
|
||||
function RoomReservation:on_api_call_timeout()
|
||||
self:set_status_failed(500, 'Reservation lookup timed out');
|
||||
end
|
||||
|
||||
--- callback on API response
|
||||
function RoomReservation:on_api_create_conference_complete(response_body, response_code)
|
||||
if response_code == 200 or response_code == 201 then
|
||||
self:handler_conference_data_returned_from_api(response_body);
|
||||
elseif response_code == 409 then
|
||||
self:handle_conference_already_exist(response_body);
|
||||
elseif response_code == nil then -- warrants a retry, but this should be done automatically by the http call method.
|
||||
self:set_status_failed(500, 'Could not contact reservation server');
|
||||
else
|
||||
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
||||
end
|
||||
end
|
||||
|
||||
function RoomReservation:handler_conference_data_returned_from_api(response_body)
|
||||
local data = self:parse_conference_response(response_body);
|
||||
if not data then -- invalid response from API
|
||||
module:log("error", "API returned success code but invalid payload");
|
||||
self:set_status_failed(500, 'Invalid response from reservation server');
|
||||
else
|
||||
self:set_status_success(data.start_time, data.duration, data.mail_owner, data.id, data)
|
||||
end
|
||||
end
|
||||
|
||||
function RoomReservation:handle_conference_already_exist(response_body)
|
||||
local data = json.decode(response_body);
|
||||
if data == nil or data.conflict_id == nil then
|
||||
-- yes, in the case of 409, API expected to return "id" as "conflict_id".
|
||||
self:set_status_failed(409, 'Invalid response from reservation server');
|
||||
else
|
||||
local url = api_prefix..'/conference/'..data.conflict_id;
|
||||
local http_options = {
|
||||
method = 'GET';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
async_http_request(url, http_options, function(response_body, response_code)
|
||||
if response_code == 200 then
|
||||
self:handler_conference_data_returned_from_api(response_body);
|
||||
else
|
||||
self:set_status_failed(response_code, self:parse_error_message_from_response(response_body));
|
||||
end
|
||||
end, function ()
|
||||
self:on_api_call_timeout();
|
||||
end);
|
||||
end
|
||||
end
|
||||
|
||||
--- End RoomReservation
|
||||
|
||||
--- Store reservations lookups that are still pending or with room still active
|
||||
local reservations = {}
|
||||
|
||||
local function get_or_create_reservations(room_jid, creator_jid)
|
||||
if reservations[room_jid] == nil then
|
||||
module:log("debug", "Creating new reservation data for %s", room_jid);
|
||||
reservations[room_jid] = newRoomReservation(room_jid, creator_jid);
|
||||
end
|
||||
|
||||
return reservations[room_jid];
|
||||
end
|
||||
|
||||
local function evict_expired_reservations()
|
||||
local expired = {}
|
||||
|
||||
-- first, gather jids of expired rooms. So we don't remove from table while iterating.
|
||||
for room_jid, res in pairs(reservations) do
|
||||
if res:is_expired() then
|
||||
table.insert(expired, room_jid);
|
||||
end
|
||||
end
|
||||
|
||||
local room;
|
||||
for _, room_jid in ipairs(expired) do
|
||||
room = get_room_from_jid(room_jid);
|
||||
if room then
|
||||
-- Close room if still active (reservation duration exceeded)
|
||||
module:log("info", "Room exceeded reservation duration. Terminating %s", room_jid);
|
||||
room:destroy(nil, "Scheduled conference duration exceeded.");
|
||||
-- Rely on room_destroyed to calls DELETE /conference and drops reservation[room_jid]
|
||||
else
|
||||
module:log("error", "Reservation references expired room that is no longer active. Dropping %s", room_jid);
|
||||
-- This should not happen unless evict_expired_reservations somehow gets triggered
|
||||
-- between the time room is destroyed and room_destroyed callback is called. (Possible?)
|
||||
-- But just in case, we drop the reservation to avoid repeating this path on every pass.
|
||||
reservations[room_jid] = nil;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
timer.add_task(expiry_check_period, function()
|
||||
evict_expired_reservations();
|
||||
return expiry_check_period;
|
||||
end)
|
||||
|
||||
|
||||
--- Intercept conference IQ to Jicofo handle reservation checks before allowing normal event flow
|
||||
module:log("info", "Hook to global pre-iq/host");
|
||||
module:hook("pre-iq/host", function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
if stanza.name ~= "iq" or stanza.attr.to ~= focus_component_host or stanza.attr.type ~= 'set' then
|
||||
return; -- not IQ for jicofo. Ignore this event.
|
||||
end
|
||||
|
||||
local conference = stanza:get_child('conference', 'http://jitsi.org/protocol/focus');
|
||||
if conference == nil then
|
||||
return; -- not Conference IQ. Ignore.
|
||||
end
|
||||
|
||||
local room_jid = room_jid_match_rewrite(conference.attr.room);
|
||||
|
||||
if get_room_from_jid(room_jid) ~= nil then
|
||||
module:log("debug", "Skip reservation check for existing room %s", room_jid);
|
||||
return; -- room already exists. Continue with normal flow
|
||||
end
|
||||
|
||||
if skip_breakout_room then
|
||||
local _, host = jid.split(room_jid);
|
||||
if host == breakout_muc_component_host then
|
||||
module:log("debug", "Skip reservation check for breakout room %s", room_jid);
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local res = get_or_create_reservations(room_jid, stanza.attr.from);
|
||||
res:enqueue_or_route_event(event); -- hand over to reservation obj to route event
|
||||
return true;
|
||||
|
||||
end);
|
||||
|
||||
|
||||
--- Forget reservation details once room destroyed so query is repeated if room re-created
|
||||
local function room_destroyed(event)
|
||||
local res;
|
||||
local room = event.room
|
||||
|
||||
if not is_healthcheck_room(room.jid) then
|
||||
res = reservations[room.jid]
|
||||
|
||||
-- drop reservation data for this room
|
||||
reservations[room.jid] = nil
|
||||
|
||||
if res then -- just in case event triggered more than once?
|
||||
module:log("info", "Dropped reservation data for destroyed room %s", room.jid);
|
||||
|
||||
local conflict_id = res.meta.conflict_id
|
||||
if conflict_id then
|
||||
local url = api_prefix..'/conference/'..conflict_id;
|
||||
local http_options = {
|
||||
method = 'DELETE';
|
||||
headers = http_headers;
|
||||
}
|
||||
|
||||
module:log("debug", "Sending DELETE /conference/%s", conflict_id);
|
||||
async_http_request(url, http_options);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function room_created(event)
|
||||
local room = event.room
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local res = reservations[room.jid]
|
||||
|
||||
if res and max_occupants_enabled and res.meta.max_occupants ~= nil then
|
||||
module:log("info", "Setting max_occupants %d for room %s", res.meta.max_occupants, room.jid);
|
||||
room._data.max_occupants = res.meta.max_occupants
|
||||
end
|
||||
|
||||
if res and password_support_enabled and res.meta.password ~= nil then
|
||||
module:log("info", "Setting password for room %s", room.jid);
|
||||
room:set_password(res.meta.password);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function room_pre_create(event)
|
||||
local room = event.room
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
local res = reservations[room.jid]
|
||||
|
||||
if res and lobby_support_enabled and res.meta.lobby then
|
||||
module:log("info", "Enabling lobby for room %s", room.jid);
|
||||
prosody.events.fire_event("create-persistent-lobby-room", { room = room; });
|
||||
end
|
||||
end
|
||||
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log("info", "Hook to muc-room-destroyed on %s", host);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
|
||||
if max_occupants_enabled or password_support_enabled then
|
||||
module:log("info", "Hook to muc-room-created on %s (max_occupants or password integration enabled)", host);
|
||||
host_module:hook("muc-room-created", room_created);
|
||||
end
|
||||
|
||||
if lobby_support_enabled then
|
||||
module:log("info", "Hook to muc-room-pre-create on %s (lobby integration enabled)", host);
|
||||
host_module:hook("muc-room-pre-create", room_pre_create);
|
||||
end
|
||||
end);
|
||||
15
resources/prosody-plugins/mod_room_destroy.lua
Normal file
15
resources/prosody-plugins/mod_room_destroy.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Handle room destroy requests it such a way that it can be suppressed by other
|
||||
-- modules that handle room lifecycle and wish to keep the room alive.
|
||||
|
||||
function handle_room_destroy(event)
|
||||
local room = event.room;
|
||||
local reason = event.reason;
|
||||
local caller = event.caller;
|
||||
|
||||
module:log('info', 'Destroying room %s (requested by %s)', room.jid, caller);
|
||||
room:set_persistent(false);
|
||||
room:destroy(nil, reason);
|
||||
end
|
||||
|
||||
module:hook_global("maybe-destroy-room", handle_room_destroy, -1);
|
||||
module:log('info', 'loaded');
|
||||
6
resources/prosody-plugins/mod_room_metadata.lua
Normal file
6
resources/prosody-plugins/mod_room_metadata.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
-- TODO: Remove this file after several stable releases when people update their configs
|
||||
module:log('warn', 'mod_room_metadata is deprecated and will be removed in a future release. '
|
||||
.. 'Please update your config by removing this module from the list of loaded modules.');
|
||||
|
||||
module:depends("jitsi_session");
|
||||
module:depends("features_identity");
|
||||
376
resources/prosody-plugins/mod_room_metadata_component.lua
Normal file
376
resources/prosody-plugins/mod_room_metadata_component.lua
Normal file
@@ -0,0 +1,376 @@
|
||||
-- This module implements a generic metadata storage system for rooms.
|
||||
--
|
||||
-- Component "metadata.jitmeet.example.com" "room_metadata_component"
|
||||
-- muc_component = "conference.jitmeet.example.com"
|
||||
-- breakout_rooms_component = "breakout.jitmeet.example.com"
|
||||
local array = require 'util.array';
|
||||
local filters = require 'util.filters';
|
||||
local jid_node = require 'util.jid'.node;
|
||||
local json = require 'util.json';
|
||||
local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_shallow_copy = util.table_shallow_copy;
|
||||
local table_add = util.table_add;
|
||||
local table_equals = util.table_equals;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
local COMPONENT_IDENTITY_TYPE = 'room_metadata';
|
||||
local FORM_KEY = 'muc#roominfo_jitsimetadata';
|
||||
|
||||
local muc_component_host = module:get_option_string('muc_component');
|
||||
|
||||
if muc_component_host == nil then
|
||||
module:log('error', 'No muc_component specified. No muc to operate on!');
|
||||
return;
|
||||
end
|
||||
|
||||
local main_virtual_host = module:get_option_string('muc_mapper_domain_base');
|
||||
if not main_virtual_host then
|
||||
module:log('warn', 'No muc_mapper_domain_base option set.');
|
||||
return;
|
||||
end
|
||||
|
||||
local breakout_rooms_component_host = module:get_option_string('breakout_rooms_component');
|
||||
|
||||
module:log("info", "Starting room metadata for %s", muc_component_host);
|
||||
|
||||
local main_muc_module;
|
||||
|
||||
-- Utility functions
|
||||
|
||||
-- Returns json string with the metadata for the room.
|
||||
-- @param room The room object.
|
||||
-- @param metadata Optional metadata to use instead of the room's jitsiMetadata.
|
||||
function getMetadataJSON(room, metadata)
|
||||
local res, error = json.encode({
|
||||
type = COMPONENT_IDENTITY_TYPE,
|
||||
metadata = metadata or room.jitsiMetadata or {}
|
||||
});
|
||||
|
||||
if not res then
|
||||
module:log('error', 'Error encoding data room:%s', room.jid, error);
|
||||
end
|
||||
|
||||
return res;
|
||||
end
|
||||
|
||||
function broadcastMetadata(room, json_msg)
|
||||
if not json_msg then
|
||||
return;
|
||||
end
|
||||
|
||||
for _, occupant in room:each_occupant() do
|
||||
send_metadata(occupant, room, json_msg)
|
||||
end
|
||||
end
|
||||
|
||||
function send_metadata(occupant, room, json_msg)
|
||||
if not json_msg or is_admin(occupant.bare_jid) then
|
||||
local metadata_to_send = room.jitsiMetadata or {};
|
||||
|
||||
-- we want to send the main meeting participants only to jicofo
|
||||
if is_admin(occupant.bare_jid) then
|
||||
local participants;
|
||||
local moderators = array();
|
||||
|
||||
if room._data.participants then
|
||||
participants = array();
|
||||
participants:append(room._data.participants);
|
||||
end
|
||||
|
||||
if room._data.moderator_id then
|
||||
moderators:push(room._data.moderator_id);
|
||||
end
|
||||
|
||||
if room._data.moderators then
|
||||
moderators:append(room._data.moderators);
|
||||
end
|
||||
|
||||
metadata_to_send = table_shallow_copy(metadata_to_send);
|
||||
metadata_to_send.participants = participants;
|
||||
metadata_to_send.moderators = moderators;
|
||||
|
||||
module:log('info', 'Sending metadata to jicofo room=%s,meeting_id=%s', room.jid, room._data.meeting_id);
|
||||
end
|
||||
|
||||
json_msg = getMetadataJSON(room, metadata_to_send);
|
||||
end
|
||||
|
||||
local stanza = st.message({ from = module.host; to = occupant.jid; })
|
||||
:tag('json-message', {
|
||||
xmlns = 'http://jitsi.org/jitmeet',
|
||||
room = internal_room_jid_match_rewrite(room.jid)
|
||||
}):text(json_msg):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
-- Handling events
|
||||
|
||||
function room_created(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
|
||||
room.sent_initial_metadata = {};
|
||||
end
|
||||
|
||||
function on_message(event)
|
||||
local session = event.origin;
|
||||
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == 'error' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
if not session or not session.jitsi_web_query_room then
|
||||
return false;
|
||||
end
|
||||
|
||||
local message = event.stanza:get_child(COMPONENT_IDENTITY_TYPE, 'http://jitsi.org/jitmeet');
|
||||
local messageText = message:get_text();
|
||||
|
||||
if not message or not messageText then
|
||||
return false;
|
||||
end
|
||||
|
||||
local roomJid = message.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(roomJid));
|
||||
|
||||
if not room then
|
||||
module:log('warn', 'No room found found for %s/%s',
|
||||
session.jitsi_web_query_prefix, session.jitsi_web_query_room);
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check that the participant requesting is a moderator and is an occupant in the room
|
||||
local from = event.stanza.attr.from;
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
local jsonData, error = json.decode(messageText);
|
||||
if jsonData == nil then -- invalid JSON
|
||||
module:log("error", "Invalid JSON message: %s error:%s", messageText, error);
|
||||
return false;
|
||||
end
|
||||
|
||||
if jsonData.key == nil or jsonData.data == nil then
|
||||
module:log("error", "Invalid JSON payload, key or data are missing: %s", messageText);
|
||||
return false;
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' then
|
||||
-- will return a non nil filtered data to use, if it is nil, it is not allowed
|
||||
local res = module:context(main_virtual_host):fire_event('jitsi-metadata-allow-moderation',
|
||||
{ room = room; actor = occupant; key = jsonData.key ; data = jsonData.data; session = session; });
|
||||
|
||||
if not res then
|
||||
module:log('warn', 'Occupant %s is not moderator and not allowed this operation for %s', from, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
jsonData.data = res;
|
||||
end
|
||||
|
||||
local old_value = room.jitsiMetadata[jsonData.key];
|
||||
if not table_equals(old_value, jsonData.data) then
|
||||
room.jitsiMetadata[jsonData.key] = jsonData.data;
|
||||
|
||||
module:log('info', 'Мetadata key "%s" updated by %s in room:%s,meeting_id:%s', jsonData.key, from, room.jid, room._data.meeting_id);
|
||||
broadcastMetadata(room, getMetadataJSON(room));
|
||||
|
||||
-- fire and event for the change
|
||||
main_muc_module:fire_event('jitsi-metadata-updated', { room = room; actor = occupant; key = jsonData.key; });
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Module operations
|
||||
|
||||
-- handle messages to this component
|
||||
module:hook("message/host", on_message);
|
||||
|
||||
-- operates on already loaded main muc module
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
main_muc_module = host_module;
|
||||
|
||||
module:log('debug', 'Main muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", muc_component_host);
|
||||
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
|
||||
-- The room metadata was updated internally (from another module).
|
||||
host_module:hook("room-metadata-changed", function(event)
|
||||
local room = event.room;
|
||||
local json_msg = getMetadataJSON(room);
|
||||
|
||||
module:log('info', 'Metadata changed internally in room:%s,meeting_id:%s - broadcasting data:%s', room.jid, room._data.meeting_id, json_msg);
|
||||
broadcastMetadata(room, json_msg);
|
||||
end);
|
||||
|
||||
-- TODO: Once clients update to read/write metadata for startMuted policy we can drop this
|
||||
-- this is to convert presence settings from old clients to metadata
|
||||
host_module:hook('muc-broadcast-presence', function (event)
|
||||
local actor, occupant, room, stanza, x = event.actor, event.occupant, event.room, event.stanza, event.x;
|
||||
|
||||
if is_healthcheck_room(room.jid) or occupant.role ~= 'moderator' then
|
||||
return;
|
||||
end
|
||||
|
||||
local startMuted = stanza:get_child('startmuted', 'http://jitsi.org/jitmeet/start-muted');
|
||||
|
||||
if not startMuted then
|
||||
return;
|
||||
end
|
||||
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
|
||||
local startMutedMetadata = room.jitsiMetadata.startMuted or {};
|
||||
|
||||
local audioNewValue = startMuted.attr.audio == 'true';
|
||||
local videoNewValue = startMuted.attr.video == 'true';
|
||||
local send_update = false;
|
||||
|
||||
if startMutedMetadata.audio ~= audioNewValue then
|
||||
startMutedMetadata.audio = audioNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
if startMutedMetadata.video ~= videoNewValue then
|
||||
startMutedMetadata.video = videoNewValue;
|
||||
send_update = true;
|
||||
end
|
||||
|
||||
if send_update then
|
||||
room.jitsiMetadata.startMuted = startMutedMetadata;
|
||||
|
||||
host_module:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end);
|
||||
|
||||
-- The the connection jid for authenticated users (like jicofo) stays the same,
|
||||
-- so leaving and re-joining will result not sending metatadata again.
|
||||
-- Make sure we clear the sent_initial_metadata entry for the occupant on leave.
|
||||
host_module:hook("muc-occupant-left", function(event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if room.sent_initial_metadata then
|
||||
room.sent_initial_metadata[jid.bare(event.occupant.jid)] = nil;
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- process or waits to process the main muc component
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_main_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- breakout rooms support
|
||||
function process_breakout_muc_loaded(breakout_muc, host_module)
|
||||
module:log('debug', 'Breakout rooms muc loaded');
|
||||
module:log("info", "Hook to muc events on %s", breakout_rooms_component_host);
|
||||
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
end
|
||||
|
||||
if breakout_rooms_component_host then
|
||||
process_host_module(breakout_rooms_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
process_breakout_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
end
|
||||
|
||||
-- Send a message update for metadata before sending the first self presence
|
||||
function filter_stanza(stanza, session)
|
||||
if not stanza.attr or not stanza.attr.to or stanza.name ~= 'presence' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local bare_to = jid.bare(stanza.attr.to);
|
||||
local muc_x = stanza:get_child('x', MUC_NS..'#user');
|
||||
if not muc_x or not presence_check_status(muc_x, '110') then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(jid.bare(stanza.attr.from)));
|
||||
|
||||
if not room or not room.sent_initial_metadata or is_healthcheck_room(room.jid) then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
if room.sent_initial_metadata[bare_to] then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
local occupant;
|
||||
for _, o in room:each_occupant() do
|
||||
if o.bare_jid == bare_to then
|
||||
occupant = o;
|
||||
end
|
||||
end
|
||||
|
||||
if not occupant then
|
||||
module:log('warn', 'No occupant %s found for %s', bare_to, room.jid);
|
||||
return stanza;
|
||||
end
|
||||
|
||||
room.sent_initial_metadata[bare_to] = true;
|
||||
|
||||
send_metadata(occupant, room);
|
||||
|
||||
return stanza;
|
||||
end
|
||||
function filter_session(session)
|
||||
-- domain mapper is filtering on default priority 0
|
||||
-- allowners is -1 and we need it after that, permissions is -2
|
||||
filters.add_filter(session, 'stanzas/out', filter_stanza, -3);
|
||||
end
|
||||
|
||||
-- enable filtering presences
|
||||
filters.add_filter_hook(filter_session);
|
||||
|
||||
process_host_module(main_virtual_host, function(host_module)
|
||||
module:context(host_module.host):fire_event('jitsi-add-identity', {
|
||||
name = 'room_metadata'; host = module.host;
|
||||
});
|
||||
end);
|
||||
164
resources/prosody-plugins/mod_roster_command.lua
Normal file
164
resources/prosody-plugins/mod_roster_command.lua
Normal file
@@ -0,0 +1,164 @@
|
||||
-----------------------------------------------------------
|
||||
-- mod_roster_command: Manage rosters through prosodyctl
|
||||
-- version 0.02
|
||||
-----------------------------------------------------------
|
||||
-- Copyright (C) 2011 Matthew Wild
|
||||
-- Copyright (C) 2011 Adam Nielsen
|
||||
--
|
||||
-- This project is MIT/X11 licensed. Please see the
|
||||
-- COPYING file in the source package for more information.
|
||||
-----------------------------------------------------------
|
||||
|
||||
if module.host ~= "*" then
|
||||
module:log("error", "Do not load this module in Prosody, for correct usage see: https://modules.prosody.im/mod_roster_command.html");
|
||||
return;
|
||||
end
|
||||
|
||||
|
||||
-- Workaround for lack of util.startup...
|
||||
local prosody = _G.prosody;
|
||||
local hosts = prosody.hosts;
|
||||
prosody.bare_sessions = prosody.bare_sessions or {};
|
||||
_G.bare_sessions = _G.bare_sessions or prosody.bare_sessions;
|
||||
|
||||
local usermanager = require "core.usermanager";
|
||||
local rostermanager = require "core.rostermanager";
|
||||
local storagemanager = require "core.storagemanager";
|
||||
local jid = require "util.jid";
|
||||
local warn = require"util.prosodyctl".show_warning;
|
||||
|
||||
-- Make a *one-way* subscription. User will see when contact is online,
|
||||
-- contact will not see when user is online.
|
||||
function subscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
end
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription request is pending...
|
||||
rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, contact_host, user_jid);
|
||||
-- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters.
|
||||
if user_username ~= nil then
|
||||
rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Make a mutual subscription between jid1 and jid2. Each JID will see
|
||||
-- when the other one is online.
|
||||
function subscribe_both(jid1, jid2)
|
||||
subscribe(jid1, jid2);
|
||||
subscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Unsubscribes user from contact (not contact from user, if subscribed).
|
||||
function unsubscribe(user_jid, contact_jid)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local contact_username, contact_host = jid.split(contact_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
-- Update user's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribe(user_username, user_host, contact_jid);
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
usermanager.initialize_host(contact_host);
|
||||
end
|
||||
-- Update contact's roster to say subscription is cancelled...
|
||||
rostermanager.unsubscribed(contact_username, contact_host, user_jid);
|
||||
end
|
||||
end
|
||||
|
||||
-- Cancel any subscription in either direction.
|
||||
function unsubscribe_both(jid1, jid2)
|
||||
unsubscribe(jid1, jid2);
|
||||
unsubscribe(jid2, jid1);
|
||||
end
|
||||
|
||||
-- Set the name shown and group used in the contact list
|
||||
function rename(user_jid, contact_jid, contact_nick, contact_group)
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
if not hosts[user_host] then
|
||||
warn("The host '%s' is not configured for this server.", user_host);
|
||||
return;
|
||||
end
|
||||
if hosts[user_host].users.name == "null" then
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
|
||||
-- Load user's roster and find the contact
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
local item = roster[contact_jid];
|
||||
if item then
|
||||
if contact_nick then
|
||||
item.name = contact_nick;
|
||||
end
|
||||
if contact_group then
|
||||
item.groups = {}; -- Remove from all current groups
|
||||
item.groups[contact_group] = true;
|
||||
end
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
end
|
||||
|
||||
function remove(user_jid, contact_jid)
|
||||
unsubscribe_both(user_jid, contact_jid);
|
||||
local user_username, user_host = jid.split(user_jid);
|
||||
local roster = rostermanager.load_roster(user_username, user_host);
|
||||
roster[contact_jid] = nil;
|
||||
rostermanager.save_roster(user_username, user_host, roster);
|
||||
end
|
||||
|
||||
function module.command(arg)
|
||||
local command = arg[1];
|
||||
if not command then
|
||||
warn("Valid subcommands: (un)subscribe(_both) | rename");
|
||||
return 0;
|
||||
end
|
||||
table.remove(arg, 1);
|
||||
if command == "subscribe" then
|
||||
subscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "subscribe_both" then
|
||||
subscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe" then
|
||||
unsubscribe(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "unsubscribe_both" then
|
||||
unsubscribe_both(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "remove" then
|
||||
remove(arg[1], arg[2]);
|
||||
return 0;
|
||||
elseif command == "rename" then
|
||||
rename(arg[1], arg[2], arg[3], arg[4]);
|
||||
return 0;
|
||||
else
|
||||
warn("Unknown command: %s", command);
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
47
resources/prosody-plugins/mod_roster_command.patch
Normal file
47
resources/prosody-plugins/mod_roster_command.patch
Normal file
@@ -0,0 +1,47 @@
|
||||
# HG changeset patch
|
||||
# User Boris Grozev <boris@jitsi.org>
|
||||
# Date 1609874100 21600
|
||||
# Tue Jan 05 13:15:00 2021 -0600
|
||||
# Node ID f646babfc401494ff33f2126ef6c4df541ebf846
|
||||
# Parent 456b9f608fcf9667cfba1bd7bf9eba2151af50d0
|
||||
mod_roster_command: Fix subscription when the "user JID" is a bare domain.
|
||||
|
||||
Do not attempt to update the roster when the user is bare domain (e.g. a
|
||||
component), since they don't have rosters and the attempt results in an error:
|
||||
|
||||
$ prosodyctl mod_roster_command subscribe proxy.example.com contact@example.com
|
||||
xxxxxxxxxxFailed to execute command: Error: /usr/lib/prosody/core/rostermanager.lua:104: attempt to concatenate local 'username' (a nil value)
|
||||
stack traceback:
|
||||
/usr/lib/prosody/core/rostermanager.lua:104: in function 'load_roster'
|
||||
/usr/lib/prosody/core/rostermanager.lua:305: in function 'set_contact_pending_out'
|
||||
mod_roster_command.lua:44: in function 'subscribe'
|
||||
|
||||
diff -r 456b9f608fcf -r f646babfc401 mod_roster_command/mod_roster_command.lua
|
||||
--- a/mod_roster_command/mod_roster_command.lua Tue Jan 05 13:49:50 2021 +0000
|
||||
+++ b/mod_roster_command/mod_roster_command.lua Tue Jan 05 13:15:00 2021 -0600
|
||||
@@ -40,8 +40,10 @@
|
||||
storagemanager.initialize_host(user_host);
|
||||
usermanager.initialize_host(user_host);
|
||||
end
|
||||
- -- Update user's roster to say subscription request is pending...
|
||||
- rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
+ -- Update user's roster to say subscription request is pending. Bare hosts (e.g. components) don't have rosters.
|
||||
+ if user_username ~= nil then
|
||||
+ rostermanager.set_contact_pending_out(user_username, user_host, contact_jid);
|
||||
+ end
|
||||
if hosts[contact_host] then
|
||||
if contact_host ~= user_host and hosts[contact_host].users.name == "null" then
|
||||
storagemanager.initialize_host(contact_host);
|
||||
@@ -51,8 +53,10 @@
|
||||
rostermanager.set_contact_pending_in(contact_username, contact_host, user_jid);
|
||||
-- Update contact's roster to say subscription request approved...
|
||||
rostermanager.subscribed(contact_username, contact_host, user_jid);
|
||||
- -- Update user's roster to say subscription request approved...
|
||||
- rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
+ -- Update user's roster to say subscription request approved. Bare hosts (e.g. components) don't have rosters.
|
||||
+ if user_username ~= nil then
|
||||
+ rostermanager.process_inbound_subscription_approval(user_username, user_host, contact_jid);
|
||||
+ end
|
||||
end
|
||||
end
|
||||
|
||||
26
resources/prosody-plugins/mod_s2s_whitelist.lua
Normal file
26
resources/prosody-plugins/mod_s2s_whitelist.lua
Normal file
@@ -0,0 +1,26 @@
|
||||
-- Using as a base version https://hg.prosody.im/prosody-modules/file/c1a8ce147885/mod_s2s_whitelist/mod_s2s_whitelist.lua
|
||||
local st = require "util.stanza";
|
||||
|
||||
local whitelist = module:get_option_inherited_set("s2s_whitelist", {});
|
||||
|
||||
module:hook("route/remote", function (event)
|
||||
if not whitelist:contains(event.to_host) then
|
||||
-- make sure we do not send error replies for errors
|
||||
if event.stanza.attr.type == 'error' then
|
||||
module:log('debug', 'Not whitelisted destination domain for an error: %s', event.stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
module:send(st.error_reply(event.stanza, "cancel", "not-allowed", "Communication with this domain is restricted"));
|
||||
return true;
|
||||
end
|
||||
end, 100);
|
||||
|
||||
module:hook("s2s-stream-features", function (event)
|
||||
if not whitelist:contains(event.origin.from_host) then
|
||||
event.origin:close({
|
||||
condition = "policy-violation";
|
||||
text = "Communication with this domain is restricted";
|
||||
});
|
||||
end
|
||||
end, 1000);
|
||||
20
resources/prosody-plugins/mod_s2sout_override.lua
Normal file
20
resources/prosody-plugins/mod_s2sout_override.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Using as a base version https://hg.prosody.im/prosody-modules/file/6cf2f32dbf40/mod_s2sout_override/mod_s2sout_override.lua
|
||||
--% requires: s2sout-pre-connect-event
|
||||
|
||||
local url = require"socket.url";
|
||||
local basic_resolver = require "net.resolvers.basic";
|
||||
|
||||
local override_for = module:get_option(module.name, {}); -- map of host to "tcp://example.com:5269"
|
||||
|
||||
module:hook("s2sout-pre-connect", function(event)
|
||||
local override = override_for[event.session.to_host];
|
||||
if type(override) == "string" then
|
||||
override = url.parse(override);
|
||||
end
|
||||
if type(override) == "table" and override.scheme == "tcp" and type(override.host) == "string" then
|
||||
event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5269, override.scheme, {});
|
||||
elseif type(override) == "table" and override.scheme == "tls" and type(override.host) == "string" then
|
||||
event.resolver = basic_resolver.new(override.host, tonumber(override.port) or 5270, "tcp",
|
||||
{ servername = event.session.to_host; sslctx = event.session.ssl_ctx });
|
||||
end
|
||||
end);
|
||||
20
resources/prosody-plugins/mod_secure_interfaces.lua
Normal file
20
resources/prosody-plugins/mod_secure_interfaces.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
-- Using version https://hg.prosody.im/prosody-modules/file/6c806a99f802/mod_secure_interfaces/mod_secure_interfaces.lua
|
||||
local secure_interfaces = module:get_option_set("secure_interfaces", { "127.0.0.1", "::1" });
|
||||
|
||||
module:hook("stream-features", function (event)
|
||||
local session = event.origin;
|
||||
if session.type ~= "c2s_unauthed" then return; end
|
||||
local socket = session.conn:socket();
|
||||
if not socket.getsockname then
|
||||
module:log("debug", "Unable to determine local address of incoming connection");
|
||||
return;
|
||||
end
|
||||
local localip = socket:getsockname();
|
||||
if secure_interfaces:contains(localip) then
|
||||
-- module:log("debug", "Marking session from %s to %s as secure", session.ip or "[?]", localip);
|
||||
session.secure = true;
|
||||
session.conn.starttls = false;
|
||||
-- else
|
||||
-- module:log("debug", "Not marking session from %s to %s as secure", session.ip or "[?]", localip);
|
||||
end
|
||||
end, 2500);
|
||||
138
resources/prosody-plugins/mod_short_lived_token.lua
Normal file
138
resources/prosody-plugins/mod_short_lived_token.lua
Normal file
@@ -0,0 +1,138 @@
|
||||
-- to be enabled under the main virtual host with all required settings
|
||||
-- short_lived_token = {
|
||||
-- issuer = 'myissuer';
|
||||
-- accepted_audiences = { 'file-sharing' };
|
||||
-- key_path = '/etc/prosody/short_lived_token.key';
|
||||
-- key_id = 'my_kid';
|
||||
-- ttl_seconds = 30;
|
||||
-- };
|
||||
-- The key in key_path can be generated via: openssl genrsa -out $PRIVATE_KEY_PATH 2048
|
||||
-- And you can get the public key from it, which can be used ot verify those tokens via:
|
||||
-- openssl rsa -in $PRIVATE_KEY_PATH -pubout -out $PUBLIC_KEY_PATH
|
||||
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local jwt = module:require 'luajwtjitsi';
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_vpaas = util.is_vpaas;
|
||||
local process_host_module = util.process_host_module;
|
||||
local table_find = util.table_find;
|
||||
local create_throttle = require 'prosody.util.throttle'.create;
|
||||
|
||||
local SERVICE_TYPE = 'short-lived-token';
|
||||
local options = module:get_option('short_lived_token');
|
||||
|
||||
if not (options.issuer and options.accepted_audiences
|
||||
and options.key_path and options.key_id and options.ttl_seconds) then
|
||||
module:log('error', 'Missing required options for short_lived_token');
|
||||
return;
|
||||
end
|
||||
|
||||
local f = io.open(options.key_path, 'r');
|
||||
if f then
|
||||
options.key = f:read('*all');
|
||||
f:close();
|
||||
end
|
||||
|
||||
local accepted_requests = {};
|
||||
for _, host in pairs(options.accepted_audiences) do
|
||||
accepted_requests[string.format('%s:%s:0', SERVICE_TYPE, host)] = host;
|
||||
end
|
||||
|
||||
local server_region_name = module:get_option_string('region_name');
|
||||
|
||||
local main_muc_component_host = module:get_option_string('main_muc');
|
||||
if main_muc_component_host == nil then
|
||||
module:log('error', 'main_muc not configured. Cannot proceed.');
|
||||
return;
|
||||
end
|
||||
local main_muc_service;
|
||||
|
||||
function generateToken(session, audience, room, occupant)
|
||||
local t = os.time();
|
||||
local exp = t + options.ttl_seconds;
|
||||
local presence = occupant:get_presence(session.full_jid);
|
||||
local _, _, id = extract_subdomain(jid.node(room.jid));
|
||||
|
||||
local payload = {
|
||||
iss = options.issuer,
|
||||
aud = audience,
|
||||
nbf = t,
|
||||
exp = exp,
|
||||
sub = session.jitsi_web_query_prefix or module.host,
|
||||
context = {
|
||||
group = session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id,
|
||||
user = session.jitsi_meet_context_user or {
|
||||
id = session.full_jid,
|
||||
name = presence:get_child_text('nick', 'http://jabber.org/protocol/nick'),
|
||||
email = presence:get_child_text("email") or nil,
|
||||
nick = jid.resource(occupant.nick)
|
||||
},
|
||||
features = session.jitsi_meet_context_features
|
||||
},
|
||||
room = session.jitsi_web_query_room,
|
||||
meeting_id = room._data.meetingId,
|
||||
granted_from = session.granted_jitsi_meet_context_user_id,
|
||||
customer_id = id or session.jitsi_meet_context_group or session.granted_jitsi_meet_context_group_id,
|
||||
backend_region = server_region_name,
|
||||
user_region = session.user_region
|
||||
};
|
||||
|
||||
local alg = 'RS256';
|
||||
local token, err = jwt.encode(payload, options.key, alg, { kid = options.key_id });
|
||||
if not err then
|
||||
return token
|
||||
else
|
||||
module:log('error', 'Error generating token: %s', err);
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('external_service/credentials', function (event)
|
||||
local requested_credentials, services, session, stanza
|
||||
= event.requested_credentials, event.services, event.origin, event.stanza;
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
|
||||
if not room then
|
||||
session.send(st.error_reply(stanza, 'cancel', 'not-allowed'));
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(session.full_jid);
|
||||
if not occupant then
|
||||
session.send(st.error_reply(stanza, 'cancel', 'not-allowed'));
|
||||
return;
|
||||
end
|
||||
|
||||
for request in requested_credentials do
|
||||
local host = accepted_requests[request];
|
||||
if host then
|
||||
services:push({
|
||||
type = SERVICE_TYPE;
|
||||
host = host;
|
||||
username = 'token';
|
||||
password = generateToken(session, host, room, occupant);
|
||||
expires = os.time() + options.ttl_seconds;
|
||||
restricted = true;
|
||||
transport = 'https';
|
||||
port = 443;
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
end);
|
||||
|
||||
process_host_module(main_muc_component_host, function(host_module, host)
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
|
||||
if muc_module then
|
||||
main_muc_service = muc_module;
|
||||
else
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
main_muc_service = prosody.hosts[host].modules.muc;
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
6
resources/prosody-plugins/mod_speakerstats.lua
Normal file
6
resources/prosody-plugins/mod_speakerstats.lua
Normal file
@@ -0,0 +1,6 @@
|
||||
-- TODO: Remove this file after several stable releases when people update their configs
|
||||
module:log('warn', 'mod_speakerstats is deprecated and will be removed in a future release. '
|
||||
.. 'Please update your config by removing this module from the list of loaded modules.');
|
||||
|
||||
module:depends('jitsi_session');
|
||||
module:depends('features_identity');
|
||||
384
resources/prosody-plugins/mod_speakerstats_component.lua
Normal file
384
resources/prosody-plugins/mod_speakerstats_component.lua
Normal file
@@ -0,0 +1,384 @@
|
||||
local util = module:require "util";
|
||||
local is_admin = util.is_admin;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local is_jibri = util.is_jibri;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local jid_resource = require "util.jid".resource;
|
||||
local st = require "util.stanza";
|
||||
local socket = require "socket";
|
||||
local json = require 'cjson.safe';
|
||||
local jid_split = require 'util.jid'.split;
|
||||
|
||||
-- we use async to detect Prosody 0.10 and earlier
|
||||
local have_async = pcall(require, "util.async");
|
||||
if not have_async then
|
||||
module:log("warn", "speaker stats will not work with Prosody version 0.10 or less.");
|
||||
return;
|
||||
end
|
||||
|
||||
local muc_component_host = module:get_option_string("muc_component");
|
||||
local main_virtual_host = module:get_option_string("muc_mapper_domain_base");
|
||||
|
||||
if muc_component_host == nil or main_virtual_host == nil then
|
||||
module:log("error", "No muc_component specified. No muc to operate on!");
|
||||
return;
|
||||
end
|
||||
local breakout_room_component_host = "breakout." .. main_virtual_host;
|
||||
|
||||
module:log("info", "Starting speakerstats for %s", muc_component_host);
|
||||
|
||||
local main_muc_service;
|
||||
|
||||
-- Searches all rooms in the main muc component that holds a breakout room
|
||||
-- caches it if found so we don't search it again
|
||||
-- we should not cache objects in _data as this is being serialized when calling room:save()
|
||||
local function get_main_room(breakout_room)
|
||||
if breakout_room.main_room then
|
||||
return breakout_room.main_room;
|
||||
end
|
||||
|
||||
-- let's search all rooms to find the main room
|
||||
for room in main_muc_service.each_room() do
|
||||
if room._data and room._data.breakout_rooms_active and room._data.breakout_rooms[breakout_room.jid] then
|
||||
breakout_room.main_room = room;
|
||||
return room;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- receives messages from client currently connected to the room
|
||||
-- clients indicates their own dominant speaker events
|
||||
function on_message(event)
|
||||
-- Check the type of the incoming stanza to avoid loops:
|
||||
if event.stanza.attr.type == "error" then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
local speakerStats
|
||||
= event.stanza:get_child('speakerstats', 'http://jitsi.org/jitmeet');
|
||||
if speakerStats then
|
||||
local roomAddress = speakerStats.attr.room;
|
||||
local silence = speakerStats.attr.silence == 'true';
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
if not room.speakerStats then
|
||||
module:log("warn", "No speakerStats found for %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
local roomSpeakerStats = room.speakerStats;
|
||||
local from = event.stanza.attr.from;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant then
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
|
||||
local newDominantSpeaker = roomSpeakerStats[occupant.jid];
|
||||
local oldDominantSpeakerId = roomSpeakerStats['dominantSpeakerId'];
|
||||
|
||||
if oldDominantSpeakerId and occupant.jid ~= oldDominantSpeakerId then
|
||||
local oldDominantSpeaker = roomSpeakerStats[oldDominantSpeakerId];
|
||||
if oldDominantSpeaker then
|
||||
oldDominantSpeaker:setDominantSpeaker(false, false);
|
||||
end
|
||||
end
|
||||
|
||||
if newDominantSpeaker then
|
||||
newDominantSpeaker:setDominantSpeaker(true, silence);
|
||||
end
|
||||
|
||||
room.speakerStats['dominantSpeakerId'] = occupant.jid;
|
||||
end
|
||||
|
||||
local newFaceLandmarks = event.stanza:get_child('faceLandmarks', 'http://jitsi.org/jitmeet');
|
||||
|
||||
if newFaceLandmarks then
|
||||
local roomAddress = newFaceLandmarks.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(roomAddress));
|
||||
|
||||
if not room then
|
||||
module:log("warn", "No room found %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
if not room.speakerStats then
|
||||
module:log("warn", "No speakerStats found for %s", roomAddress);
|
||||
return false;
|
||||
end
|
||||
local from = event.stanza.attr.from;
|
||||
|
||||
local occupant = room:get_occupant_by_real_jid(from);
|
||||
if not occupant or not room.speakerStats[occupant.jid] then
|
||||
module:log("warn", "No occupant %s found for %s", from, roomAddress);
|
||||
return false;
|
||||
end
|
||||
local faceLandmarks = room.speakerStats[occupant.jid].faceLandmarks;
|
||||
table.insert(faceLandmarks,
|
||||
{
|
||||
faceExpression = newFaceLandmarks.attr.faceExpression,
|
||||
timestamp = tonumber(newFaceLandmarks.attr.timestamp),
|
||||
duration = tonumber(newFaceLandmarks.attr.duration),
|
||||
})
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
--- Start SpeakerStats implementation
|
||||
local SpeakerStats = {};
|
||||
SpeakerStats.__index = SpeakerStats;
|
||||
|
||||
function new_SpeakerStats(nick, context_user)
|
||||
return setmetatable({
|
||||
totalDominantSpeakerTime = 0;
|
||||
_dominantSpeakerStart = 0;
|
||||
_isSilent = false;
|
||||
_isDominantSpeaker = false;
|
||||
nick = nick;
|
||||
context_user = context_user;
|
||||
displayName = nil;
|
||||
faceLandmarks = {};
|
||||
}, SpeakerStats);
|
||||
end
|
||||
|
||||
-- Changes the dominantSpeaker data for current occupant
|
||||
-- saves start time if it is new dominat speaker
|
||||
-- or calculates and accumulates time of speaking
|
||||
function SpeakerStats:setDominantSpeaker(isNowDominantSpeaker, silence)
|
||||
-- module:log("debug", "set isDominant %s for %s", tostring(isNowDominantSpeaker), self.nick);
|
||||
|
||||
local now = socket.gettime()*1000;
|
||||
|
||||
if not self:isDominantSpeaker() and isNowDominantSpeaker and not silence then
|
||||
self._dominantSpeakerStart = now;
|
||||
elseif self:isDominantSpeaker() then
|
||||
if not isNowDominantSpeaker then
|
||||
if not self._isSilent then
|
||||
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
||||
|
||||
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
||||
self._dominantSpeakerStart = 0;
|
||||
end
|
||||
elseif self._isSilent and not silence then
|
||||
self._dominantSpeakerStart = now;
|
||||
elseif not self._isSilent and silence then
|
||||
local timeElapsed = math.floor(now - self._dominantSpeakerStart);
|
||||
|
||||
self.totalDominantSpeakerTime = self.totalDominantSpeakerTime + timeElapsed;
|
||||
self._dominantSpeakerStart = 0;
|
||||
end
|
||||
end
|
||||
|
||||
self._isDominantSpeaker = isNowDominantSpeaker;
|
||||
self._isSilent = silence;
|
||||
end
|
||||
|
||||
-- Returns true if the tracked user is currently a dominant speaker.
|
||||
function SpeakerStats:isDominantSpeaker()
|
||||
return self._isDominantSpeaker;
|
||||
end
|
||||
|
||||
-- Returns true if the tracked user is currently silent.
|
||||
function SpeakerStats:isSilent()
|
||||
return self._isSilent;
|
||||
end
|
||||
--- End SpeakerStats
|
||||
|
||||
-- create speakerStats for the room
|
||||
function room_created(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return ;
|
||||
end
|
||||
room.speakerStats = {};
|
||||
room.speakerStats.sessionId = room._data.meetingId;
|
||||
end
|
||||
|
||||
-- create speakerStats for the breakout
|
||||
function breakout_room_created(event)
|
||||
local room = event.room;
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return ;
|
||||
end
|
||||
local main_room = get_main_room(room);
|
||||
room.speakerStats = {};
|
||||
room.speakerStats.isBreakout = true
|
||||
room.speakerStats.breakoutRoomId = jid_split(room.jid)
|
||||
room.speakerStats.sessionId = main_room._data.meetingId;
|
||||
end
|
||||
|
||||
-- Create SpeakerStats object for the joined user
|
||||
function occupant_joined(event)
|
||||
local occupant, room, stanza = event.occupant, event.room, event.stanza;
|
||||
|
||||
if is_healthcheck_room(room.jid)
|
||||
or is_admin(occupant.bare_jid)
|
||||
or is_transcriber_jigasi(stanza)
|
||||
or is_jibri(occupant) then
|
||||
return;
|
||||
end
|
||||
|
||||
local nick = jid_resource(occupant.nick);
|
||||
|
||||
if room.speakerStats then
|
||||
-- lets send the current speaker stats to that user, so he can update
|
||||
-- its local stats
|
||||
if next(room.speakerStats) ~= nil then
|
||||
local users_json = {};
|
||||
for jid, values in pairs(room.speakerStats) do
|
||||
-- skip reporting those without a nick('dominantSpeakerId')
|
||||
-- and skip focus if sneaked into the table
|
||||
if values and type(values) == 'table' and values.nick ~= nil and values.nick ~= 'focus' then
|
||||
local totalDominantSpeakerTime = values.totalDominantSpeakerTime;
|
||||
local faceLandmarks = values.faceLandmarks;
|
||||
if totalDominantSpeakerTime > 0 or room:get_occupant_jid(jid) == nil or values:isDominantSpeaker()
|
||||
or next(faceLandmarks) ~= nil then
|
||||
-- before sending we need to calculate current dominant speaker state
|
||||
if values:isDominantSpeaker() and not values:isSilent() then
|
||||
local timeElapsed = math.floor(socket.gettime()*1000 - values._dominantSpeakerStart);
|
||||
totalDominantSpeakerTime = totalDominantSpeakerTime + timeElapsed;
|
||||
end
|
||||
|
||||
users_json[values.nick] = {
|
||||
displayName = values.displayName,
|
||||
totalDominantSpeakerTime = totalDominantSpeakerTime,
|
||||
faceLandmarks = faceLandmarks
|
||||
};
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if next(users_json) ~= nil then
|
||||
local body_json = {};
|
||||
body_json.type = 'speakerstats';
|
||||
body_json.users = users_json;
|
||||
|
||||
local json_msg_str, error = json.encode(body_json);
|
||||
|
||||
if json_msg_str then
|
||||
local stanza = st.message({
|
||||
from = module.host;
|
||||
to = occupant.jid; })
|
||||
:tag("json-message", {xmlns='http://jitsi.org/jitmeet'})
|
||||
:text(json_msg_str):up();
|
||||
|
||||
room:route_stanza(stanza);
|
||||
else
|
||||
module:log('error', 'Error encoding room:%s error:%s', room.jid, error);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local context_user = event.origin and event.origin.jitsi_meet_context_user or nil;
|
||||
room.speakerStats[occupant.jid] = new_SpeakerStats(nick, context_user);
|
||||
end
|
||||
end
|
||||
|
||||
-- Occupant left set its dominant speaker to false and update the store the
|
||||
-- display name
|
||||
function occupant_leaving(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
if not room.speakerStats then
|
||||
return;
|
||||
end
|
||||
|
||||
local occupant = event.occupant;
|
||||
|
||||
local speakerStatsForOccupant = room.speakerStats[occupant.jid];
|
||||
if speakerStatsForOccupant then
|
||||
speakerStatsForOccupant:setDominantSpeaker(false, false);
|
||||
|
||||
-- set display name
|
||||
local displayName = occupant:get_presence():get_child_text(
|
||||
'nick', 'http://jabber.org/protocol/nick');
|
||||
speakerStatsForOccupant.displayName = displayName;
|
||||
end
|
||||
end
|
||||
|
||||
-- Conference ended, send speaker stats
|
||||
function room_destroyed(event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
module:fire_event("send-speaker-stats", { room = room; roomSpeakerStats = room.speakerStats; });
|
||||
end
|
||||
|
||||
module:hook("message/host", on_message);
|
||||
|
||||
function process_main_muc_loaded(main_muc, host_module)
|
||||
-- the conference muc component
|
||||
module:log("info", "Hook to muc events on %s", host_module.host);
|
||||
main_muc_service = main_muc;
|
||||
module:log("info", "Main muc service %s", main_muc_service)
|
||||
host_module:hook("muc-room-created", room_created, -1);
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
end
|
||||
|
||||
function process_breakout_muc_loaded(breakout_muc, host_module)
|
||||
-- the Breakout muc component
|
||||
module:log("info", "Hook to muc events on %s", host_module.host);
|
||||
host_module:hook("muc-room-created", breakout_room_created, -1);
|
||||
host_module:hook("muc-occupant-joined", occupant_joined, -1);
|
||||
host_module:hook("muc-occupant-pre-leave", occupant_leaving, -1);
|
||||
host_module:hook("muc-room-destroyed", room_destroyed, -1);
|
||||
end
|
||||
|
||||
-- process or waits to process the conference muc component
|
||||
process_host_module(muc_component_host, function(host_module, host)
|
||||
module:log('info', 'Conference component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_main_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_main_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
-- process or waits to process the breakout rooms muc component
|
||||
process_host_module(breakout_room_component_host, function(host_module, host)
|
||||
module:log('info', 'Breakout component loaded %s', host);
|
||||
|
||||
local muc_module = prosody.hosts[host].modules.muc;
|
||||
if muc_module then
|
||||
process_breakout_muc_loaded(muc_module, host_module);
|
||||
else
|
||||
module:log('debug', 'Will wait for muc to be available');
|
||||
prosody.hosts[host].events.add_handler('module-loaded', function(event)
|
||||
if (event.module == 'muc') then
|
||||
process_breakout_muc_loaded(prosody.hosts[host].modules.muc, host_module);
|
||||
end
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
process_host_module(main_virtual_host, function(host_module)
|
||||
module:context(host_module.host):fire_event('jitsi-add-identity', {
|
||||
name = 'speakerstats'; host = module.host;
|
||||
});
|
||||
end);
|
||||
125
resources/prosody-plugins/mod_system_chat_message.lua
Normal file
125
resources/prosody-plugins/mod_system_chat_message.lua
Normal file
@@ -0,0 +1,125 @@
|
||||
-- Module which can be used as an http endpoint to send system private chat messages to meeting participants. The provided token
|
||||
--- in the request is verified whether it has the right to do so. This module should be loaded under the virtual host.
|
||||
-- Copyright (C) 2024-present 8x8, Inc.
|
||||
|
||||
-- curl https://{host}/send-system-chat-message -d '{"message": "testmessage", "connectionJIDs": ["{connection_jid}"], "room": "{room_jid}"}' -H "content-type: application/json" -H "authorization: Bearer {token}"
|
||||
|
||||
local util = module:require "util";
|
||||
local token_util = module:require "token/util".new(module);
|
||||
|
||||
local async_handler_wrapper = util.async_handler_wrapper;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local starts_with = util.starts_with;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
|
||||
local st = require "util.stanza";
|
||||
local json = require "cjson.safe";
|
||||
|
||||
local asapKeyServer = module:get_option_string("prosody_password_public_key_repo_url", "");
|
||||
|
||||
if asapKeyServer then
|
||||
-- init token util with our asap keyserver
|
||||
token_util:set_asap_key_server(asapKeyServer)
|
||||
end
|
||||
|
||||
function verify_token(token)
|
||||
if token == nil then
|
||||
module:log("warn", "no token provided");
|
||||
return false;
|
||||
end
|
||||
|
||||
local session = {};
|
||||
session.auth_token = token;
|
||||
local verified, reason, msg = token_util:process_and_verify_token(session);
|
||||
if not verified then
|
||||
module:log("warn", "not a valid token %s %s", tostring(reason), tostring(msg));
|
||||
return false;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function handle_send_system_message (event)
|
||||
local request = event.request;
|
||||
|
||||
module:log("debug", "Request for sending a system message received: reqid %s", request.headers["request_id"])
|
||||
|
||||
-- verify payload
|
||||
if request.headers.content_type ~= "application/json"
|
||||
or (not request.body or #request.body == 0) then
|
||||
module:log("error", "Wrong content type: %s or missing payload", request.headers.content_type);
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local payload = json.decode(request.body);
|
||||
|
||||
if not payload then
|
||||
module:log("error", "Request body is missing");
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local displayName = payload["displayName"];
|
||||
local message = payload["message"];
|
||||
local connectionJIDs = payload["connectionJIDs"];
|
||||
local payload_room = payload["room"];
|
||||
|
||||
if not message or not connectionJIDs or not payload_room then
|
||||
module:log("error", "One of [message, connectionJIDs, room] was not provided");
|
||||
return { status_code = 400; }
|
||||
end
|
||||
|
||||
local room_jid = room_jid_match_rewrite(payload_room);
|
||||
local room = get_room_from_jid(room_jid);
|
||||
|
||||
if not room then
|
||||
module:log("error", "Room %s not found", room_jid);
|
||||
return { status_code = 404; }
|
||||
end
|
||||
|
||||
-- verify access
|
||||
local token = request.headers["authorization"]
|
||||
if not token then
|
||||
module:log("error", "Authorization header was not provided for conference %s", room_jid)
|
||||
return { status_code = 401 };
|
||||
end
|
||||
if starts_with(token, 'Bearer ') then
|
||||
token = token:sub(8, #token)
|
||||
else
|
||||
module:log("error", "Authorization header is invalid")
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
if not verify_token(token, room_jid) then
|
||||
return { status_code = 401 };
|
||||
end
|
||||
|
||||
local data = {
|
||||
displayName = displayName,
|
||||
type = "system_chat_message",
|
||||
message = message,
|
||||
};
|
||||
|
||||
for _, to in ipairs(connectionJIDs) do
|
||||
local stanza = st.message({
|
||||
from = room.jid,
|
||||
to = to
|
||||
})
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' })
|
||||
:text(json.encode(data))
|
||||
:up();
|
||||
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
|
||||
return { status_code = 200 };
|
||||
end
|
||||
|
||||
module:log("info", "Adding http handler for /send-system-chat-message on %s", module.host);
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["POST send-system-chat-message"] = function(event)
|
||||
return async_handler_wrapper(event, handle_send_system_message)
|
||||
end;
|
||||
};
|
||||
});
|
||||
137
resources/prosody-plugins/mod_token_verification.lua
Normal file
137
resources/prosody-plugins/mod_token_verification.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local log = module._log;
|
||||
local host = module.host;
|
||||
local st = require "util.stanza";
|
||||
local jid_split = require 'util.jid'.split;
|
||||
local jid_bare = require 'util.jid'.bare;
|
||||
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
|
||||
local DEBUG = false;
|
||||
|
||||
local measure_success = module:measure('success', 'counter');
|
||||
local measure_fail = module:measure('fail', 'counter');
|
||||
|
||||
local parentHostName = string.gmatch(tostring(host), "%w+.(%w.+)")();
|
||||
if parentHostName == nil then
|
||||
module:log("error", "Failed to start - unable to get parent hostname");
|
||||
return;
|
||||
end
|
||||
|
||||
local parentCtx = module:context(parentHostName);
|
||||
if parentCtx == nil then
|
||||
module:log("error",
|
||||
"Failed to start - unable to get parent context for host: %s",
|
||||
tostring(parentHostName));
|
||||
return;
|
||||
end
|
||||
|
||||
local token_util = module:require "token/util".new(parentCtx);
|
||||
|
||||
-- no token configuration
|
||||
if token_util == nil then
|
||||
return;
|
||||
end
|
||||
|
||||
module:log("debug",
|
||||
"%s - starting MUC token verifier app_id: %s app_secret: %s allow empty: %s",
|
||||
tostring(host), tostring(token_util.appId), tostring(token_util.appSecret),
|
||||
tostring(token_util.allowEmptyToken));
|
||||
|
||||
-- option to disable room modification (sending muc config form) for guest that do not provide token
|
||||
local require_token_for_moderation;
|
||||
-- option to allow domains to skip token verification
|
||||
local allowlist;
|
||||
local function load_config()
|
||||
require_token_for_moderation = module:get_option_boolean("token_verification_require_token_for_moderation");
|
||||
allowlist = module:get_option_set('token_verification_allowlist', {});
|
||||
end
|
||||
load_config();
|
||||
|
||||
-- verify user and whether he is allowed to join a room based on the token information
|
||||
local function verify_user(session, stanza)
|
||||
if DEBUG then
|
||||
module:log("debug", "Session token: %s, session room: %s",
|
||||
tostring(session.auth_token), tostring(session.jitsi_meet_room));
|
||||
end
|
||||
|
||||
-- token not required for admin users
|
||||
local user_jid = stanza.attr.from;
|
||||
if is_admin(user_jid) then
|
||||
if DEBUG then module:log("debug", "Token not required from admin user: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
-- token not required for users matching allow list
|
||||
local user_bare_jid = jid_bare(user_jid);
|
||||
local _, user_domain = jid_split(user_jid);
|
||||
|
||||
-- allowlist for participants
|
||||
if allowlist:contains(user_domain) or allowlist:contains(user_bare_jid) then
|
||||
if DEBUG then module:log("debug", "Token not required from user in allow list: %s", user_jid); end
|
||||
return true;
|
||||
end
|
||||
|
||||
if DEBUG then module:log("debug", "Will verify token for user: %s, room: %s ", user_jid, stanza.attr.to); end
|
||||
if not token_util:verify_room(session, stanza.attr.to) then
|
||||
module:log("error", "Token %s not allowed to join: %s",
|
||||
tostring(session.auth_token), tostring(stanza.attr.to));
|
||||
session.send(
|
||||
st.error_reply(
|
||||
stanza, "cancel", "not-allowed", "Room and token mismatched"));
|
||||
return false; -- we need to just return non nil
|
||||
end
|
||||
if DEBUG then module:log("debug", "allowed: %s to enter/create room: %s", user_jid, stanza.attr.to); end
|
||||
return true;
|
||||
end
|
||||
|
||||
module:hook("muc-room-pre-create", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if DEBUG then module:log("debug", "pre create: %s %s", tostring(origin), tostring(stanza)); end
|
||||
if not verify_user(origin, stanza) then
|
||||
measure_fail(1);
|
||||
return true; -- Returning any value other than nil will halt processing of the event
|
||||
end
|
||||
measure_success(1);
|
||||
end, 99);
|
||||
|
||||
module:hook("muc-occupant-pre-join", function(event)
|
||||
local origin, room, stanza = event.origin, event.room, event.stanza;
|
||||
if DEBUG then module:log("debug", "pre join: %s %s", tostring(room), tostring(stanza)); end
|
||||
if not verify_user(origin, stanza) then
|
||||
measure_fail(1);
|
||||
return true; -- Returning any value other than nil will halt processing of the event
|
||||
end
|
||||
measure_success(1);
|
||||
end, 99);
|
||||
|
||||
for event_name, method in pairs {
|
||||
-- Normal room interactions
|
||||
["iq-set/bare/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
|
||||
-- Host room
|
||||
["iq-set/host/http://jabber.org/protocol/muc#owner:query"] = "handle_owner_query_set_to_room" ;
|
||||
} do
|
||||
module:hook(event_name, function (event)
|
||||
local session, stanza = event.origin, event.stanza;
|
||||
|
||||
-- if we do not require token we pass it through(default behaviour)
|
||||
-- or the request is coming from admin (focus)
|
||||
if not require_token_for_moderation or is_admin(stanza.attr.from) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- jitsi_meet_room is set after the token had been verified
|
||||
if not session.auth_token or not session.jitsi_meet_room then
|
||||
session.send(
|
||||
st.error_reply(
|
||||
stanza, "cancel", "not-allowed", "Room modification disabled for guests"));
|
||||
return true;
|
||||
end
|
||||
|
||||
end, -1); -- the default prosody hook is on -2
|
||||
end
|
||||
|
||||
module:hook_global('config-reloaded', load_config);
|
||||
80
resources/prosody-plugins/mod_turncredentials.lua
Normal file
80
resources/prosody-plugins/mod_turncredentials.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
-- XEP-0215 implementation for time-limited turn credentials
|
||||
-- Copyright (C) 2012-2014 Philipp Hancke
|
||||
-- This file is MIT/X11 licensed.
|
||||
|
||||
--turncredentials_secret = "keepthissecret";
|
||||
--turncredentials = {
|
||||
-- { type = "stun", host = "8.8.8.8" },
|
||||
-- { type = "turn", host = "8.8.8.8", port = "3478" },
|
||||
-- { type = "turn", host = "8.8.8.8", port = "80", transport = "tcp" }
|
||||
--}
|
||||
-- for stun servers, host is required, port defaults to 3478
|
||||
-- for turn servers, host is required, port defaults to tcp,
|
||||
-- transport defaults to udp
|
||||
-- hosts can be a list of server names / ips for random
|
||||
-- choice loadbalancing
|
||||
|
||||
local st = require "util.stanza";
|
||||
local hmac_sha1 = require "util.hashes".hmac_sha1;
|
||||
local base64 = require "util.encodings".base64;
|
||||
local os_time = os.time;
|
||||
local secret = module:get_option_string("turncredentials_secret");
|
||||
local ttl = module:get_option_number("turncredentials_ttl", 86400);
|
||||
local hosts = module:get_option("turncredentials") or {};
|
||||
if not (secret) then
|
||||
module:log("error", "turncredentials not configured");
|
||||
return;
|
||||
end
|
||||
|
||||
module:add_feature("urn:xmpp:extdisco:1");
|
||||
|
||||
function random(arr)
|
||||
local index = math.random(1, #arr);
|
||||
return arr[index];
|
||||
end
|
||||
|
||||
|
||||
module:hook_global("config-reloaded", function()
|
||||
module:log("debug", "config-reloaded")
|
||||
secret = module:get_option_string("turncredentials_secret");
|
||||
ttl = module:get_option_number("turncredentials_ttl", 86400);
|
||||
hosts = module:get_option("turncredentials") or {};
|
||||
end);
|
||||
|
||||
module:hook("iq-get/host/urn:xmpp:extdisco:1:services", function(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
if origin.type ~= "c2s" then
|
||||
return;
|
||||
end
|
||||
local now = os_time() + ttl;
|
||||
local userpart = tostring(now);
|
||||
local nonce = base64.encode(hmac_sha1(secret, tostring(userpart), false));
|
||||
local reply = st.reply(stanza):tag("services", {xmlns = "urn:xmpp:extdisco:1"})
|
||||
for idx, item in pairs(hosts) do
|
||||
if item.type == "stun" or item.type == "stuns" then
|
||||
-- stun items need host and port (defaults to 3478)
|
||||
reply:tag("service",
|
||||
{ type = item.type, host = item.host, port = tostring(item.port) or "3478" }
|
||||
):up();
|
||||
elseif item.type == "turn" or item.type == "turns" then
|
||||
local turn = {}
|
||||
-- turn items need host, port (defaults to 3478),
|
||||
-- transport (defaults to udp)
|
||||
-- username, password, ttl
|
||||
turn.type = item.type;
|
||||
turn.port = tostring(item.port);
|
||||
turn.transport = item.transport;
|
||||
turn.username = userpart;
|
||||
turn.password = nonce;
|
||||
turn.ttl = tostring(ttl);
|
||||
if item.hosts then
|
||||
turn.host = random(item.hosts)
|
||||
else
|
||||
turn.host = item.host
|
||||
end
|
||||
reply:tag("service", turn):up();
|
||||
end
|
||||
end
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
31
resources/prosody-plugins/mod_turncredentials_http.lua
Normal file
31
resources/prosody-plugins/mod_turncredentials_http.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
-- http endpoint to expose turn credentials for other services
|
||||
-- Copyright (C) 2023-present 8x8, Inc.
|
||||
|
||||
local ext_services = module:depends("external_services");
|
||||
local get_services = ext_services.get_services;
|
||||
|
||||
local async_handler_wrapper = module:require "util".async_handler_wrapper;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
--- Handles request for retrieving turn credentials
|
||||
-- @param event the http event, holds the request query
|
||||
-- @return GET response, containing a json with participants details
|
||||
function handle_get_turn_credentials (event)
|
||||
local GET_response = {
|
||||
headers = {
|
||||
content_type = "application/json";
|
||||
};
|
||||
body = json.encode(get_services());
|
||||
};
|
||||
return GET_response;
|
||||
end;
|
||||
|
||||
function module.load()
|
||||
module:depends("http");
|
||||
module:provides("http", {
|
||||
default_path = "/";
|
||||
route = {
|
||||
["GET turn-credentials"] = function (event) return async_handler_wrapper(event,handle_get_turn_credentials) end;
|
||||
};
|
||||
});
|
||||
end
|
||||
490
resources/prosody-plugins/mod_visitors.lua
Normal file
490
resources/prosody-plugins/mod_visitors.lua
Normal file
@@ -0,0 +1,490 @@
|
||||
--- activate under main vhost
|
||||
--- In /etc/hosts add:
|
||||
--- vm1-ip-address visitors1.domain.com
|
||||
--- vm1-ip-address conference.visitors1.domain.com
|
||||
--- vm2-ip-address visitors2.domain.com
|
||||
--- vm2-ip-address conference.visitors2.domain.com
|
||||
--- Enable in global modules: 's2s_bidi' and 'certs_all'
|
||||
--- Make sure 's2s' is not in modules_disabled
|
||||
--- Open port 5269 on the provider side and on the firewall on the machine (iptables -I INPUT 4 -p tcp -m tcp --dport 5269 -j ACCEPT)
|
||||
--- NOTE: Make sure all communication between prosodies is using the real jids ([foo]room1@muc.example.com)
|
||||
local st = require 'util.stanza';
|
||||
local jid = require 'util.jid';
|
||||
local new_id = require 'util.id'.medium;
|
||||
local util = module:require 'util';
|
||||
local filter_identity_from_presence = util.filter_identity_from_presence;
|
||||
local is_admin = util.is_admin;
|
||||
local presence_check_status = util.presence_check_status;
|
||||
local process_host_module = util.process_host_module;
|
||||
local is_transcriber_jigasi = util.is_transcriber_jigasi;
|
||||
local json = require 'cjson.safe';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
-- required parameter for custom muc component prefix, defaults to 'conference'
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
|
||||
local main_muc_component_config = module:get_option_string('main_muc');
|
||||
if main_muc_component_config == nil then
|
||||
module:log('error', 'visitors rooms not enabled missing main_muc config');
|
||||
return ;
|
||||
end
|
||||
|
||||
-- A list of domains which to be ignored for visitors. For occupants using those domain we do not propagate them
|
||||
-- to visitor nodes and we do not update them with presence changes
|
||||
local ignore_list = module:get_option_set('visitors_ignore_list', {});
|
||||
|
||||
-- Advertise the component for discovery via disco#items
|
||||
module:add_identity('component', 'visitors', 'visitors.'..module.host);
|
||||
|
||||
local sent_iq_cache = require 'util.cache'.new(200);
|
||||
|
||||
-- visitors_nodes = {
|
||||
-- roomjid1 = {
|
||||
-- nodes = {
|
||||
-- ['conference.visitors1.jid'] = 2, // number of main participants, on 0 we clean it
|
||||
-- ['conference.visitors2.jid'] = 3
|
||||
-- }
|
||||
-- },
|
||||
-- roomjid2 = {}
|
||||
--}
|
||||
local visitors_nodes = {};
|
||||
|
||||
-- sends connect or update iq
|
||||
-- @parameter type - Type of iq to send 'connect' or 'update'
|
||||
local function send_visitors_iq(conference_service, room, type)
|
||||
-- send iq informing the vnode that the connect is done and it will allow visitors to join
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
local visitors_iq = st.iq({
|
||||
type = 'set',
|
||||
to = conference_service,
|
||||
from = module.host,
|
||||
id = iq_id })
|
||||
:tag('visitors', { xmlns = 'jitsi:visitors',
|
||||
room = jid.join(jid.node(room.jid), conference_service) })
|
||||
:tag(type, { xmlns = 'jitsi:visitors',
|
||||
password = type ~= 'disconnect' and room:get_password() or '',
|
||||
lobby = room._data.lobbyroom and 'true' or 'false',
|
||||
meetingId = room._data.meetingId,
|
||||
createdTimestamp = room.created_timestamp and tostring(room.created_timestamp) or nil
|
||||
});
|
||||
|
||||
if type == 'update' then
|
||||
visitors_iq:tag('moderators', { xmlns = 'jitsi:visitors' });
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) and o.role == 'moderator' then
|
||||
visitors_iq:tag('item', { epId = jid.resource(o.nick) }):up();
|
||||
end
|
||||
end
|
||||
|
||||
visitors_iq:up();
|
||||
|
||||
-- files that are shared in the room
|
||||
if room.jitsi_shared_files then
|
||||
visitors_iq:tag('files', { xmlns = 'jitsi:visitors' });
|
||||
for k, v in pairs(room.jitsi_shared_files) do
|
||||
visitors_iq:tag('file', {
|
||||
id = k
|
||||
}):text(json.encode(v)):up();
|
||||
end
|
||||
visitors_iq:up();
|
||||
end
|
||||
end
|
||||
|
||||
visitors_iq:up();
|
||||
|
||||
module:send(visitors_iq);
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza,
|
||||
-- if the hideDisplayNameForGuests option for the room is set.
|
||||
-- This is applied to presence of main room participants before it is sent out to vnodes.
|
||||
local function filter_stanza_nick_if_needed(stanza, room)
|
||||
if not stanza or stanza.name ~= 'presence' or stanza.attr.type == 'error' or stanza.attr.type == 'unavailable' then
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- if hideDisplayNameForGuests we want to drop any display name from the presence stanza
|
||||
if room and (room._data.hideDisplayNameForGuests or room._data.hideDisplayNameForAll) then
|
||||
return filter_identity_from_presence(stanza);
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function connect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
local conference_service = muc_domain_prefix..'.'..vnode..'.meet.jitsi';
|
||||
|
||||
if visitors_nodes[room.jid] and
|
||||
visitors_nodes[room.jid].nodes and
|
||||
visitors_nodes[room.jid].nodes[conference_service] then
|
||||
-- nothing to do
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_nodes[room.jid] == nil then
|
||||
visitors_nodes[room.jid] = {};
|
||||
end
|
||||
if visitors_nodes[room.jid].nodes == nil then
|
||||
visitors_nodes[room.jid].nodes = {};
|
||||
end
|
||||
|
||||
local sent_main_participants = 0;
|
||||
|
||||
-- send update initially so we can report the moderators that will join
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
|
||||
for _, o in room:each_occupant() do
|
||||
if not is_admin(o.bare_jid) then
|
||||
local fmuc_pr = filter_stanza_nick_if_needed(st.clone(o:get_presence()), room);
|
||||
local user, _, res = jid.split(o.nick);
|
||||
fmuc_pr.attr.to = jid.join(user, conference_service , res);
|
||||
fmuc_pr.attr.from = o.jid;
|
||||
-- add <x>
|
||||
fmuc_pr:tag('x', { xmlns = MUC_NS });
|
||||
|
||||
-- if there is a password on the main room let's add the password for the vnode join
|
||||
-- as we will set the password to the vnode room and we will need it
|
||||
local pass = room:get_password();
|
||||
if pass and pass ~= '' then
|
||||
fmuc_pr:tag('password'):text(pass);
|
||||
end
|
||||
fmuc_pr:up();
|
||||
|
||||
module:send(fmuc_pr);
|
||||
|
||||
sent_main_participants = sent_main_participants + 1;
|
||||
end
|
||||
end
|
||||
visitors_nodes[room.jid].nodes[conference_service] = sent_main_participants;
|
||||
|
||||
send_visitors_iq(conference_service, room, 'connect');
|
||||
end
|
||||
module:hook('jitsi-connect-vnode', connect_vnode);
|
||||
|
||||
-- listens for responses to the iq sent for connecting vnode
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
-- we receive error from vnode for our disconnect message as the room was already destroyed (all visitors left)
|
||||
if (stanza.attr.type == 'result' or stanza.attr.type == 'error') and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
-- an event received from visitors component, which receives iqs from jicofo
|
||||
local function disconnect_vnode(event)
|
||||
local room, vnode = event.room, event.vnode;
|
||||
|
||||
if visitors_nodes[event.room.jid] == nil then
|
||||
-- maybe the room was already destroyed and vnodes cleared
|
||||
return;
|
||||
end
|
||||
|
||||
local conference_service = muc_domain_prefix..'.'..vnode..'.meet.jitsi';
|
||||
|
||||
visitors_nodes[room.jid].nodes[conference_service] = nil;
|
||||
|
||||
send_visitors_iq(conference_service, room, 'disconnect');
|
||||
end
|
||||
module:hook('jitsi-disconnect-vnode', disconnect_vnode);
|
||||
|
||||
-- takes care when the visitor nodes destroys the room to count the leaving participants from there, and if its really destroyed
|
||||
-- we clean up, so if we establish again the connection to the same visitor node to send the main participants
|
||||
module:hook('presence/full', function(event)
|
||||
local stanza = event.stanza;
|
||||
local room_name, from_host = jid.split(stanza.attr.from);
|
||||
if stanza.attr.type == 'unavailable' and from_host ~= main_muc_component_config then
|
||||
local room_jid = jid.join(room_name, main_muc_component_config); -- converts from visitor to main room jid
|
||||
|
||||
local x = stanza:get_child('x', 'http://jabber.org/protocol/muc#user');
|
||||
if not presence_check_status(x, '110') then
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_nodes[room_jid] and visitors_nodes[room_jid].nodes
|
||||
and visitors_nodes[room_jid].nodes[from_host] then
|
||||
visitors_nodes[room_jid].nodes[from_host] = visitors_nodes[room_jid].nodes[from_host] - 1;
|
||||
-- we clean only on disconnect coming from jicofo
|
||||
end
|
||||
end
|
||||
end, 900);
|
||||
|
||||
process_host_module(main_muc_component_config, function(host_module, host)
|
||||
-- detects presence change in a main participant and propagate it to the used visitor nodes
|
||||
host_module:hook('muc-occupant-pre-change', function (event)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.dest_occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus and configured domains (used for jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
-- a change in the presence of a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end);
|
||||
|
||||
-- when a main participant leaves inform the visitor nodes
|
||||
host_module:hook('muc-occupant-left', function (event)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- ignore configured domains (jibri and transcribers)
|
||||
if is_admin(occupant.bare_jid) or visitors_nodes[room.jid] == nil or visitors_nodes[room.jid].nodes == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
return;
|
||||
end
|
||||
|
||||
--this is probably participant kick scenario, create an unavailable presence and send to vnodes.
|
||||
if not stanza then
|
||||
stanza = st.presence {from = occupant.nick; type = "unavailable";};
|
||||
end
|
||||
|
||||
-- we want to update visitor node that a main participant left or kicked.
|
||||
if stanza then
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
-- cleanup cache
|
||||
host_module:hook('muc-room-destroyed',function(event)
|
||||
local room = event.room;
|
||||
|
||||
-- room is destroyed let's disconnect all vnodes
|
||||
if visitors_nodes[room.jid] then
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'disconnect');
|
||||
end
|
||||
|
||||
visitors_nodes[room.jid] = nil;
|
||||
end
|
||||
end);
|
||||
|
||||
-- detects new participants joining main room and sending them to the visitor nodes
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter focus, ignore configured domains (jibri and transcribers)
|
||||
if is_admin(stanza.attr.from) or visitors_nodes[room.jid] == nil
|
||||
or (ignore_list:contains(jid.host(occupant.bare_jid)) and not is_transcriber_jigasi(stanza)) then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user, _, res = jid.split(occupant.nick);
|
||||
-- a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
if occupant.role == 'moderator' then
|
||||
-- first send that the participant is a moderator
|
||||
send_visitors_iq(k, room, 'update');
|
||||
end
|
||||
local fmuc_pr = st.clone(stanza);
|
||||
fmuc_pr.attr.to = jid.join(user, k, res);
|
||||
fmuc_pr.attr.from = occupant.jid;
|
||||
module:send(fmuc_pr);
|
||||
end
|
||||
end);
|
||||
-- forwards messages from main participants to vnodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local room, stanzaEv, occupant = event.room, event.stanza, event.occupant;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
|
||||
-- filter sending messages from transcribers/jibris to visitors
|
||||
if not visitors_nodes[room.jid] then
|
||||
return;
|
||||
end
|
||||
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
local user = jid.node(occupant.nick);
|
||||
-- a main participant we need to update all active visitor nodes
|
||||
for k in pairs(vnodes) do
|
||||
local fmuc_msg = st.clone(stanza);
|
||||
fmuc_msg.attr.to = jid.join(user, k);
|
||||
fmuc_msg.attr.from = occupant.jid;
|
||||
module:send(fmuc_msg);
|
||||
end
|
||||
end);
|
||||
-- receiving messages from visitor nodes and forward them to local main participants
|
||||
-- and forward them to the rest of visitor nodes
|
||||
host_module:hook('muc-occupant-groupchat', function(event)
|
||||
local occupant, room, stanzaEv = event.occupant, event.room, event.stanza;
|
||||
local stanza = filter_stanza_nick_if_needed(stanzaEv, room);
|
||||
local to = stanza.attr.to;
|
||||
local from = stanza.attr.from;
|
||||
local from_vnode = jid.host(from);
|
||||
|
||||
if occupant or not (visitors_nodes[to]
|
||||
and visitors_nodes[to].nodes
|
||||
and visitors_nodes[to].nodes[from_vnode]) then
|
||||
return;
|
||||
end
|
||||
|
||||
if host_module:fire_event('jitsi-visitor-groupchat-pre-route', event) then
|
||||
-- message filtered
|
||||
return;
|
||||
end
|
||||
|
||||
-- a message from visitor occupant of known visitor node
|
||||
stanza.attr.from = to;
|
||||
for _, o in room:each_occupant() do
|
||||
-- send it to the nick to be able to route it to the room (ljm multiple rooms) from unknown occupant
|
||||
room:route_to_occupant(o, stanza);
|
||||
end
|
||||
-- let's add the message to the history of the room
|
||||
host_module:fire_event("muc-add-history", { room = room; stanza = stanza; from = from; visitor = true; });
|
||||
|
||||
-- now we need to send to rest of visitor nodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for k in pairs(vnodes) do
|
||||
if k ~= from_vnode then
|
||||
local st_copy = st.clone(stanza);
|
||||
st_copy.attr.to = jid.join(jid.node(room.jid), k);
|
||||
module:send(st_copy);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end, 55); -- prosody check for unknown participant chat is prio 50, we want to override it
|
||||
|
||||
-- Handle private messages from visitor nodes to main participants
|
||||
-- This routes forwarded private messages through the proper MUC system
|
||||
host_module:hook('message/full', function(event)
|
||||
local stanza = event.stanza;
|
||||
|
||||
-- Only handle chat messages (private messages)
|
||||
if stanza.attr.type ~= 'chat' then
|
||||
return; -- Let other handlers process non-chat messages
|
||||
end
|
||||
|
||||
local to = stanza.attr.to;
|
||||
|
||||
-- Early return if this is not targeted at our MUC component
|
||||
if jid.host(to) ~= main_muc_component_config then
|
||||
return; -- Not for our MUC component, let other handlers process
|
||||
end
|
||||
|
||||
local from = stanza.attr.from;
|
||||
local from_host = jid.host(from);
|
||||
local to_node = jid.node(to);
|
||||
local to_resource = jid.resource(to);
|
||||
|
||||
-- Check if this is a private message from a known visitor node
|
||||
local target_room_jid = jid.bare(to);
|
||||
|
||||
-- Early return if we don't have any visitor nodes for this room
|
||||
if not (visitors_nodes[target_room_jid] and visitors_nodes[target_room_jid].nodes) then
|
||||
return; -- No visitor nodes for this room, let default MUC handle it
|
||||
end
|
||||
|
||||
-- Early return if the from_host is not a known visitor node
|
||||
if not visitors_nodes[target_room_jid].nodes[from_host] then
|
||||
-- This could be a main->visitor message, let it go through s2s
|
||||
return; -- Not from a known visitor node, let default MUC handle it
|
||||
end
|
||||
|
||||
-- At this point we know it's a visitor message, handle it
|
||||
local room = prosody.hosts[main_muc_component_config].modules.muc.get_room_from_jid(target_room_jid);
|
||||
if room then
|
||||
-- Find the occupant
|
||||
local occupant = room:get_occupant_by_nick(to);
|
||||
if occupant then
|
||||
-- Add addresses element (XEP-0033) to store original visitor JID for reply functionality
|
||||
stanza:tag('addresses', { xmlns = 'http://jabber.org/protocol/address' })
|
||||
:tag('address', { type = 'ofrom', jid = stanza.attr.from }):up()
|
||||
:up();
|
||||
|
||||
-- Change from to be the main domain equivalent for proper client recognition
|
||||
-- Use bare JID without resource
|
||||
stanza.attr.from = jid.join(to_node, main_muc_component_config);
|
||||
|
||||
room:route_to_occupant(occupant, stanza);
|
||||
|
||||
return true;
|
||||
else
|
||||
module:log('warn', 'VISITOR PRIVATE MESSAGE: Occupant not found for %s', to);
|
||||
end
|
||||
else
|
||||
module:log('warn', 'VISITOR PRIVATE MESSAGE: Room not found for %s', to);
|
||||
end
|
||||
|
||||
return false;
|
||||
end, 10); -- Normal priority since we're in the right place now
|
||||
|
||||
-- Main->visitor private messages work via s2s routing automatically
|
||||
-- No special handling needed!
|
||||
|
||||
host_module:hook('muc-config-submitted/muc#roomconfig_roomsecret', function(event)
|
||||
if event.status_codes['104'] then
|
||||
local room = event.room;
|
||||
|
||||
if visitors_nodes[room.jid] then
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end
|
||||
end, -100); -- we want to run last in order to check is the status code 104
|
||||
|
||||
host_module:hook('muc-set-affiliation', function (event)
|
||||
if event.actor and not is_admin(event.actor) and event.affiliation == 'owner' then
|
||||
local room = event.room;
|
||||
|
||||
if not visitors_nodes[room.jid] then
|
||||
return;
|
||||
end
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end, -2);
|
||||
end);
|
||||
|
||||
local function update_vnodes_for_room(event)
|
||||
local room = event.room;
|
||||
if visitors_nodes[room.jid] then
|
||||
-- we need to update all vnodes
|
||||
local vnodes = visitors_nodes[room.jid].nodes;
|
||||
for conference_service in pairs(vnodes) do
|
||||
send_visitors_iq(conference_service, room, 'update');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module:hook('jitsi-lobby-enabled', update_vnodes_for_room);
|
||||
module:hook('jitsi-lobby-disabled', update_vnodes_for_room);
|
||||
module:hook('jitsi-filesharing-updated', update_vnodes_for_room);
|
||||
753
resources/prosody-plugins/mod_visitors_component.lua
Normal file
753
resources/prosody-plugins/mod_visitors_component.lua
Normal file
@@ -0,0 +1,753 @@
|
||||
module:log('info', 'Starting visitors_component at %s', module.host);
|
||||
|
||||
local array = require "util.array";
|
||||
local http = require 'net.http';
|
||||
local jid = require 'util.jid';
|
||||
local st = require 'util.stanza';
|
||||
local util = module:require 'util';
|
||||
local is_admin = util.is_admin;
|
||||
local is_healthcheck_room = util.is_healthcheck_room;
|
||||
local is_sip_jigasi = util.is_sip_jigasi;
|
||||
local room_jid_match_rewrite = util.room_jid_match_rewrite;
|
||||
local get_room_from_jid = util.get_room_from_jid;
|
||||
local get_focus_occupant = util.get_focus_occupant;
|
||||
local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
|
||||
local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
|
||||
local table_find = util.table_find;
|
||||
local is_vpaas = util.is_vpaas;
|
||||
local is_sip_jibri_join = util.is_sip_jibri_join;
|
||||
local process_host_module = util.process_host_module;
|
||||
local respond_iq_result = util.respond_iq_result;
|
||||
local split_string = util.split_string;
|
||||
local new_id = require 'util.id'.medium;
|
||||
local json = require 'cjson.safe';
|
||||
local inspect = require 'inspect';
|
||||
|
||||
-- Debug flag
|
||||
local DEBUG = false;
|
||||
|
||||
-- will be initialized once the main virtual host module is initialized
|
||||
local token_util;
|
||||
|
||||
local MUC_NS = 'http://jabber.org/protocol/muc';
|
||||
|
||||
local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
|
||||
local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
|
||||
if not muc_domain_base then
|
||||
module:log('warn', 'No muc_domain_base option set.');
|
||||
return;
|
||||
end
|
||||
|
||||
-- A list of domains which to be ignored for visitors. The config is set under the main virtual host
|
||||
local ignore_list = module:context(muc_domain_base):get_option_set('visitors_ignore_list', {});
|
||||
|
||||
local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promotion', false);
|
||||
|
||||
-- whether to always advertise that visitors feature is enabled for rooms
|
||||
-- can be set to off and being controlled by another module, turning it on and off for rooms
|
||||
local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
|
||||
|
||||
local visitors_queue_service = module:get_option_string('visitors_queue_service');
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody (" .. prosody.version .. "; " .. prosody.platform .. ")",
|
||||
["Content-Type"] = "application/json",
|
||||
["Accept"] = "application/json"
|
||||
};
|
||||
|
||||
-- This is a map to keep data for room and the jids that were allowed to join after visitor mode is enabled
|
||||
-- automatically allowed or allowed by a moderator
|
||||
local visitors_promotion_map = {};
|
||||
|
||||
-- A map with key room jid. The content is a map with key jid from which the request is received
|
||||
-- and the value is a table that has the json message that needs to be sent to any future moderator that joins
|
||||
-- and the vnode from which the request is received and where the response will be sent
|
||||
local visitors_promotion_requests = {};
|
||||
|
||||
local cache = require 'util.cache';
|
||||
local sent_iq_cache = cache.new(200);
|
||||
|
||||
-- Function to get visitors room metadata
|
||||
local function get_visitors_room_metadata(room)
|
||||
if not room.jitsiMetadata then
|
||||
room.jitsiMetadata = {};
|
||||
end
|
||||
if not room.jitsiMetadata.visitors then
|
||||
room.jitsiMetadata.visitors = {};
|
||||
end
|
||||
return room.jitsiMetadata.visitors;
|
||||
end
|
||||
|
||||
-- Sends a json-message to the destination jid
|
||||
-- @param to_jid the destination jid
|
||||
-- @param json_message the message content to send
|
||||
function send_json_message(to_jid, json_message)
|
||||
local stanza = st.message({ from = module.host; to = to_jid; })
|
||||
:tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
|
||||
module:send(stanza);
|
||||
end
|
||||
|
||||
local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, group_id, force_promote_requested)
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received promotion request from %s for room %s, nick: %s, time: %s, user_id: %s, group_id: %s, force_promote_requested: %s',
|
||||
from_jid, room.jid, nick, time, user_id, group_id, force_promote_requested);
|
||||
end
|
||||
|
||||
-- if visitors is enabled for the room
|
||||
if visitors_promotion_map[room.jid] then
|
||||
local force_promote = auto_allow_promotion or get_visitors_room_metadata(room).autoPromote;
|
||||
if not force_promote and force_promote_requested == 'true' then
|
||||
-- Let's do the force_promote checks if requested
|
||||
-- if it is vpaas meeting we trust the moderator computation from visitor node (value of force_promote_requested)
|
||||
-- if it is not vpaas we need to check further settings only if they exist
|
||||
if is_vpaas(room) or (not room._data.moderator_id and not room._data.moderators)
|
||||
-- _data.moderator_id can be used from external modules to set single moderator for a meeting
|
||||
-- or a whole group of moderators
|
||||
or (room._data.moderator_id
|
||||
and room._data.moderator_id == user_id or room._data.moderator_id == group_id)
|
||||
|
||||
-- all moderators are allowed to auto promote, the fact that user_id and force_promote_requested are set
|
||||
-- means that the user has token and is moderator on visitor node side
|
||||
or room._data.allModerators
|
||||
|
||||
-- can be used by external modules to set multiple moderator ids (table of values)
|
||||
or table_find(room._data.moderators, user_id)
|
||||
then
|
||||
force_promote = true;
|
||||
end
|
||||
end
|
||||
|
||||
-- only for raise hand, ignore lowering the hand
|
||||
if time and time > 0 and force_promote then
|
||||
-- we are in auto-allow mode, let's reply with accept
|
||||
-- we store where the request is coming from so we can send back the response
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = from_vnode;
|
||||
jid = from_jid;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username ,
|
||||
allow = 'true' }):up());
|
||||
return true;
|
||||
else
|
||||
-- send promotion request to all moderators
|
||||
local body_json = {};
|
||||
body_json.type = 'visitors';
|
||||
body_json.room = internal_room_jid_match_rewrite(room.jid);
|
||||
body_json.action = 'promotion-request';
|
||||
body_json.nick = nick;
|
||||
body_json.from = from_jid;
|
||||
|
||||
if time and time > 0 then
|
||||
-- raise hand
|
||||
body_json.on = true;
|
||||
else
|
||||
-- lower hand, we want to inform interested parties that
|
||||
-- the visitor is no longer interested in joining the main call
|
||||
body_json.on = false;
|
||||
end
|
||||
|
||||
local msg_to_send, error = json.encode(body_json);
|
||||
|
||||
if not msg_to_send then
|
||||
module:log('error', 'Error encoding msg room:%s error:%s', room.jid, error)
|
||||
return true;
|
||||
end
|
||||
|
||||
if visitors_promotion_requests[room.jid] then
|
||||
visitors_promotion_requests[room.jid][from_jid] = {
|
||||
msg = msg_to_send;
|
||||
from = from_vnode;
|
||||
};
|
||||
else
|
||||
module:log('warn', 'Received promotion request for room %s with visitors not enabled. %s',
|
||||
room.jid, msg_to_send);
|
||||
end
|
||||
|
||||
-- let's send a notification to every moderator
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.role == 'moderator' and not is_admin(occupant.bare_jid) then
|
||||
send_json_message(occupant.jid, msg_to_send);
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
module:log('warn', 'Received promotion request from %s for room %s without active visitors', from, room.jid);
|
||||
end
|
||||
|
||||
local function connect_vnode_received(room, vnode)
|
||||
module:context(muc_domain_base):fire_event('jitsi-connect-vnode', { room = room; vnode = vnode; });
|
||||
|
||||
if not visitors_promotion_map[room.jid] then
|
||||
-- visitors is enabled
|
||||
visitors_promotion_map[room.jid] = {};
|
||||
visitors_promotion_requests[room.jid] = {};
|
||||
room._connected_vnodes = cache.new(16); -- we up to 16 vnodes for this prosody
|
||||
end
|
||||
|
||||
room._connected_vnodes:set(vnode..'.meet.jitsi', {});
|
||||
end
|
||||
|
||||
local function disconnect_vnode_received(room, vnode)
|
||||
module:context(muc_domain_base):fire_event('jitsi-disconnect-vnode', { room = room; vnode = vnode; });
|
||||
|
||||
room._connected_vnodes:set(vnode..'.meet.jitsi', nil);
|
||||
|
||||
if room._connected_vnodes:count() == 0 then
|
||||
visitors_promotion_map[room.jid] = nil;
|
||||
visitors_promotion_requests[room.jid] = nil;
|
||||
room._connected_vnodes = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- returns the accumulated data for visitors nodes, count all visitors requesting transcriptions
|
||||
-- and accumulated languages requested
|
||||
-- @returns count, languages
|
||||
function get_visitors_languages(room)
|
||||
if not room._connected_vnodes then
|
||||
return;
|
||||
end
|
||||
|
||||
local count = 0;
|
||||
local languages = array();
|
||||
|
||||
-- iterate over visitor nodes we are connected to and accumulate data if we have it
|
||||
for k, v in room._connected_vnodes:items() do
|
||||
if v.count then
|
||||
count = count + v.count;
|
||||
end
|
||||
if v.langs then
|
||||
for k in pairs(v.langs) do
|
||||
local val = v.langs[k]
|
||||
if not languages[val] then
|
||||
languages:push(val);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return count, languages:sort():concat(',');
|
||||
end
|
||||
|
||||
-- listens for iq request for promotion and forward it to moderators in the meeting for approval
|
||||
-- or auto-allow it if such the config is set enabling it
|
||||
local function stanza_handler(event)
|
||||
local origin, stanza = event.origin, event.stanza;
|
||||
|
||||
if stanza.name ~= 'iq' then
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Received stanza %s from %s', stanza, origin.full_jid);
|
||||
end
|
||||
|
||||
if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
|
||||
sent_iq_cache:set(stanza.attr.id, nil);
|
||||
return true;
|
||||
end
|
||||
|
||||
if stanza.attr.type ~= 'set' and stanza.attr.type ~= 'get' then
|
||||
return; -- We do not want to reply to these, so leave.
|
||||
end
|
||||
|
||||
local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
|
||||
if not visitors_iq then
|
||||
return;
|
||||
end
|
||||
|
||||
-- set stanzas are coming from s2s connection
|
||||
if stanza.attr.type == 'set' and origin.type ~= 's2sin' then
|
||||
module:log('warn', 'not from s2s session, ignore! %s', stanza);
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_jid = visitors_iq.attr.room;
|
||||
local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
|
||||
|
||||
if not room then
|
||||
-- this maybe as we receive the iq from jicofo after the room is already destroyed
|
||||
module:log('debug', 'No room found %s', room_jid);
|
||||
return true;
|
||||
end
|
||||
|
||||
local from_vnode;
|
||||
if room._connected_vnodes then
|
||||
from_vnode = room._connected_vnodes:get(stanza.attr.from);
|
||||
end
|
||||
|
||||
local processed;
|
||||
-- promotion request is coming from visitors and is a set and is over the s2s connection
|
||||
local request_promotion = visitors_iq:get_child('promotion-request');
|
||||
if request_promotion then
|
||||
if not from_vnode then
|
||||
module:log('warn', 'Received forged request_promotion message: %s %s',stanza, inspect(room._connected_vnodes));
|
||||
return true; -- stop processing
|
||||
end
|
||||
|
||||
local display_name = visitors_iq:get_child_text('nick', 'http://jabber.org/protocol/nick');
|
||||
processed = request_promotion_received(
|
||||
room,
|
||||
request_promotion.attr.jid,
|
||||
stanza.attr.from,
|
||||
display_name,
|
||||
tonumber(request_promotion.attr.time),
|
||||
request_promotion.attr.userId,
|
||||
request_promotion.attr.groupId,
|
||||
request_promotion.attr.forcePromote
|
||||
);
|
||||
end
|
||||
|
||||
-- connect and disconnect are only received from jicofo
|
||||
if is_admin(jid.bare(stanza.attr.from)) then
|
||||
for item in visitors_iq:childtags('connect-vnode') do
|
||||
connect_vnode_received(room, item.attr.vnode);
|
||||
processed = true;
|
||||
end
|
||||
|
||||
for item in visitors_iq:childtags('disconnect-vnode') do
|
||||
disconnect_vnode_received(room, item.attr.vnode);
|
||||
processed = true;
|
||||
end
|
||||
end
|
||||
|
||||
-- request to update metadata service for jigasi languages
|
||||
local transcription_languages = visitors_iq:get_child('transcription-languages');
|
||||
|
||||
if transcription_languages
|
||||
and (transcription_languages.attr.langs or transcription_languages.attr.count) then
|
||||
if not from_vnode then
|
||||
module:log('warn', 'Received forged transcription_languages message: %s %s',stanza, inspect(room._connected_vnodes));
|
||||
return true; -- stop processing
|
||||
end
|
||||
|
||||
local metadata = get_visitors_room_metadata(room);
|
||||
|
||||
-- we keep the split by languages array to optimize accumulating languages
|
||||
from_vnode.langs = split_string(transcription_languages.attr.langs, ',');
|
||||
from_vnode.count = transcription_languages.attr.count;
|
||||
|
||||
local count, languages = get_visitors_languages(room);
|
||||
|
||||
if metadata.transcribingLanguages ~= languages then
|
||||
metadata.transcribingLanguages = languages;
|
||||
processed = true;
|
||||
end
|
||||
|
||||
if metadata.transcribingCount ~= count then
|
||||
metadata.transcribingCount = count;
|
||||
processed = true;
|
||||
end
|
||||
|
||||
if processed then
|
||||
module:context(muc_domain_prefix..'.'..muc_domain_base)
|
||||
:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
end
|
||||
|
||||
if not processed then
|
||||
module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza);
|
||||
end
|
||||
|
||||
respond_iq_result(origin, stanza);
|
||||
return processed;
|
||||
end
|
||||
|
||||
local function process_promotion_response(room, id, approved)
|
||||
if not approved then
|
||||
module:log('debug', 'promotion not approved %s, %s', room.jid, id);
|
||||
return;
|
||||
end
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Processing promotion response for room %s, id %s, approved %s',
|
||||
room.jid, id, approved);
|
||||
end
|
||||
|
||||
-- lets reply to participant that requested promotion
|
||||
local username = new_id():lower();
|
||||
visitors_promotion_map[room.jid][username] = {
|
||||
from = visitors_promotion_requests[room.jid][id].from;
|
||||
jid = id;
|
||||
};
|
||||
|
||||
local req_from = visitors_promotion_map[room.jid][username].from;
|
||||
local req_jid = visitors_promotion_map[room.jid][username].jid;
|
||||
local focus_occupant = get_focus_occupant(room);
|
||||
local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
|
||||
|
||||
local iq_id = new_id();
|
||||
sent_iq_cache:set(iq_id, socket.gettime());
|
||||
|
||||
local node = jid.node(room.jid);
|
||||
|
||||
module:send(st.iq({
|
||||
type='set', to = req_from, from = module.host, id = iq_id })
|
||||
:tag('visitors', {
|
||||
xmlns='jitsi:visitors',
|
||||
room = jid.join(node, muc_domain_prefix..'.'..req_from),
|
||||
focusjid = focus_jid })
|
||||
:tag('promotion-response', {
|
||||
xmlns='jitsi:visitors',
|
||||
jid = req_jid,
|
||||
username = username,
|
||||
allow = approved }):up());
|
||||
end
|
||||
|
||||
-- if room metadata does not have visitors.live set to `true` and there are no occupants in the meeting
|
||||
-- it will skip calling goLive endpoint
|
||||
local function go_live(room)
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Checking if room %s is live', room.jid);
|
||||
end
|
||||
|
||||
if room._jitsi_go_live_sent then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s already sent go live request, skipping', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
-- if missing we assume room is live, only skip if it is marked explicitly as false
|
||||
if room.jitsiMetadata and room.jitsiMetadata.visitors
|
||||
and room.jitsiMetadata.visitors.live ~= nil and room.jitsiMetadata.visitors.live == false then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s is not live, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
local has_occupant = false;
|
||||
for _, occupant in room:each_occupant() do
|
||||
if not is_admin(occupant.bare_jid) then
|
||||
has_occupant = true;
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
-- when there is an occupant then go live
|
||||
if not has_occupant then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Room %s has no occupants, skipping go live request', room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
-- let's inform the queue service
|
||||
local function cb(content_, code_, response_, request_)
|
||||
local room = room;
|
||||
if code_ ~= 200 then
|
||||
module:log('warn', 'External call to visitors_queue_service/golive failed. Code %s, Content %s',
|
||||
code_, content_)
|
||||
end
|
||||
end
|
||||
|
||||
local headers = http_headers or {};
|
||||
headers['Authorization'] = token_util:generateAsapToken();
|
||||
|
||||
local ev = {
|
||||
conference = internal_room_jid_match_rewrite(room.jid)
|
||||
};
|
||||
|
||||
room._jitsi_go_live_sent = true;
|
||||
|
||||
http.request(visitors_queue_service..'/golive', {
|
||||
headers = headers,
|
||||
method = 'POST',
|
||||
body = json.encode(ev);
|
||||
}, cb);
|
||||
end
|
||||
|
||||
module:hook('iq/host', stanza_handler, 10);
|
||||
|
||||
process_host_module(muc_domain_base, function(host_module, host)
|
||||
token_util = module:require "token/util".new(host_module);
|
||||
end);
|
||||
|
||||
process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
|
||||
-- if visitor mode is started, then you are not allowed to join without request/response exchange of iqs -> deny access
|
||||
-- check list of allowed jids for the room
|
||||
host_module:hook('muc-occupant-pre-join', function (event)
|
||||
local room, stanza, occupant, session = event.room, event.stanza, event.occupant, event.origin;
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s',
|
||||
room.jid, occupant.bare_jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
-- visitors were already in the room one way or another they have access
|
||||
-- skip password challenge
|
||||
local join = stanza:get_child('x', MUC_NS);
|
||||
if join and room:get_password() and
|
||||
visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] then
|
||||
join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
|
||||
end
|
||||
|
||||
local is_live = get_visitors_room_metadata(room).live;
|
||||
|
||||
-- we skip any checks when auto-allow is enabled and room is live
|
||||
if (auto_allow_promotion or get_visitors_room_metadata(room).autoPromote and (is_live or is_live == nil))
|
||||
or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
|
||||
or is_sip_jigasi(stanza)
|
||||
or is_sip_jibri_join(stanza)
|
||||
or table_find(room._data.moderators, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id)
|
||||
or (room._data.moderator_id and room._data.moderator_id == (session.jitsi_meet_context_user and session.jitsi_meet_context_user.id))
|
||||
or table_find(room._data.participants, session.jitsi_meet_context_user and session.jitsi_meet_context_user.id) then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Auto-allowing visitor %s in room %s', stanza.attr.from, room.jid);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] then
|
||||
local in_ignore_list = ignore_list:contains(jid.host(stanza.attr.from));
|
||||
|
||||
-- now let's check for jid
|
||||
if visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] -- promotion was approved
|
||||
or in_ignore_list then -- jibri or other domains to ignore
|
||||
-- allow join
|
||||
if not in_ignore_list then
|
||||
-- let's update metadata
|
||||
local metadata = get_visitors_room_metadata(room);
|
||||
if not metadata.promoted then
|
||||
metadata.promoted = {};
|
||||
end
|
||||
metadata.promoted[jid.resource(occupant.nick)] = true;
|
||||
module:context(muc_domain_prefix..'.'..muc_domain_base)
|
||||
:fire_event('room-metadata-changed', { room = room; });
|
||||
end
|
||||
|
||||
return;
|
||||
end
|
||||
module:log('error', 'Visitor needs to be allowed by a moderator %s', stanza.attr.from);
|
||||
session.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitor needs to be allowed by a moderator')
|
||||
:tag('promotion-not-allowed', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
elseif is_vpaas(room) then
|
||||
-- special case for vpaas where if someone with a visitor token tries to join a room, where
|
||||
-- there are no visitors yet, we deny access
|
||||
if session.jitsi_meet_context_user and session.jitsi_meet_context_user.role == 'visitor' then
|
||||
session.log('warn', 'Deny user join as visitor in the main meeting, not approved');
|
||||
session.send(st.error_reply(
|
||||
stanza, 'cancel', 'not-allowed', 'Visitor tried to join the main room without approval')
|
||||
:tag('no-main-participants', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
elseif room._data.participants then
|
||||
-- This is non jaas room which has a list of participants allowed to participate in the main room
|
||||
-- but this occupant is not one of them and the room is either not live or has no participants joined
|
||||
session.log('warn',
|
||||
'Deny user join in the main not live meeting, not in the list of main participants');
|
||||
session.send(st.error_reply(
|
||||
stanza, 'cancel', 'not-allowed',
|
||||
'Tried to join the main (not live or without main participants) room')
|
||||
:tag('not-live-room', { xmlns = 'jitsi:visitors' }));
|
||||
return true;
|
||||
end
|
||||
|
||||
end, 7); -- after muc_meeting_id, the logic for not joining before jicofo
|
||||
host_module:hook('muc-room-destroyed', function (event)
|
||||
visitors_promotion_map[event.room.jid] = nil;
|
||||
visitors_promotion_requests[event.room.jid] = nil;
|
||||
end);
|
||||
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room, occupant = event.room, event.occupant;
|
||||
|
||||
if DEBUG then
|
||||
module:log('debug', 'Occupant %s joined room %s', occupant.jid, room.jid);
|
||||
end
|
||||
|
||||
if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) or occupant.role ~= 'moderator' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
if DEBUG then
|
||||
module:log('debug', 'Skipping visitor checks for healthcheck room %s or admin %s or not moderator %s',
|
||||
room.jid, occupant.bare_jid, occupant.role);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
|
||||
send_json_message(occupant.jid, value.msg);
|
||||
end
|
||||
end);
|
||||
host_module:hook('muc-set-affiliation', function (event)
|
||||
-- the actor can be nil if is coming from allowners or similar module we want to skip it here
|
||||
-- as we will handle it in occupant_joined
|
||||
local actor, affiliation, jid, room = event.actor, event.affiliation, event.jid, event.room;
|
||||
|
||||
if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not affiliation == 'owner' -- luacheck: ignore
|
||||
or not visitors_promotion_requests[event.room.jid] then
|
||||
return;
|
||||
end
|
||||
|
||||
-- event.jid is the bare jid of participant
|
||||
for _, occupant in room:each_occupant() do
|
||||
if occupant.bare_jid == event.jid then
|
||||
for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
|
||||
send_json_message(occupant.jid, value.msg);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
host_module:hook('jitsi-endpoint-message-received', function(event)
|
||||
local data, error, occupant, room, stanza
|
||||
= event.message, event.error, event.occupant, event.room, event.stanza;
|
||||
|
||||
if not data or data.type ~= 'visitors'
|
||||
or (data.action ~= "promotion-response" and data.action ~= "demote-request") then
|
||||
if error then
|
||||
module:log('error', 'Error decoding error:%s', error);
|
||||
end
|
||||
return;
|
||||
end
|
||||
|
||||
if occupant.role ~= 'moderator' then
|
||||
module:log('error', 'Occupant %s sending response message but not moderator in room %s',
|
||||
occupant.jid, room.jid);
|
||||
return false;
|
||||
end
|
||||
|
||||
if data.action == "demote-request" then
|
||||
if occupant.nick ~= room.jid..'/'..data.actor then
|
||||
module:log('error', 'Bad actor in demote request %s', stanza);
|
||||
event.origin.send(st.error_reply(stanza, "cancel", "bad-request"));
|
||||
return true;
|
||||
end
|
||||
|
||||
-- when demoting we want to send message to the demoted participant and to moderators
|
||||
local target_jid = room.jid..'/'..data.id;
|
||||
stanza.attr.type = 'chat'; -- it is safe as we are not using this stanza instance anymore
|
||||
stanza.attr.from = module.host;
|
||||
|
||||
for _, room_occupant in room:each_occupant() do
|
||||
-- do not send it to jicofo or back to the sender
|
||||
if room_occupant.jid ~= occupant.jid and not is_admin(room_occupant.bare_jid) then
|
||||
if room_occupant.role == 'moderator'
|
||||
or room_occupant.nick == target_jid then
|
||||
stanza.attr.to = room_occupant.jid;
|
||||
room:route_stanza(stanza);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
if data.id then
|
||||
process_promotion_response(room, data.id, data.approved and 'true' or 'false');
|
||||
else
|
||||
-- we are in the case with admit all, we need to read data.ids
|
||||
for _,value in pairs(data.ids) do
|
||||
process_promotion_response(room, value, data.approved and 'true' or 'false');
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true; -- halt processing, but return true that we handled it
|
||||
end);
|
||||
|
||||
if visitors_queue_service then
|
||||
host_module:hook('muc-room-created', function (event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end, -2); -- metadata hook on -1
|
||||
host_module:hook('jitsi-metadata-updated', function (event)
|
||||
if event.key == 'visitors' then
|
||||
go_live(event.room);
|
||||
end
|
||||
end);
|
||||
-- when metadata changed internally from another module
|
||||
host_module:hook('room-metadata-changed', function (event)
|
||||
go_live(event.room);
|
||||
end);
|
||||
host_module:hook('muc-occupant-joined', function (event)
|
||||
local room = event.room;
|
||||
|
||||
if is_healthcheck_room(room.jid) then
|
||||
return;
|
||||
end
|
||||
|
||||
go_live(room);
|
||||
end);
|
||||
end
|
||||
|
||||
if always_visitors_enabled then
|
||||
local visitorsEnabledField = {
|
||||
name = "muc#roominfo_visitorsEnabled";
|
||||
type = "boolean";
|
||||
label = "Whether visitors are enabled.";
|
||||
value = 1;
|
||||
};
|
||||
-- Append "visitors enabled" to the MUC config form.
|
||||
host_module:context(host):hook("muc-disco#info", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
host_module:context(host):hook("muc-config-form", function(event)
|
||||
table.insert(event.form, visitorsEnabledField);
|
||||
end);
|
||||
end
|
||||
end);
|
||||
|
||||
prosody.events.add_handler('pre-jitsi-authentication', function(session)
|
||||
if not session.customusername or not session.jitsi_web_query_room then
|
||||
return nil;
|
||||
end
|
||||
|
||||
local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
|
||||
if not room then
|
||||
return nil;
|
||||
end
|
||||
|
||||
if visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][session.customusername] then
|
||||
-- user was previously allowed to join, let him use the requested jid
|
||||
return session.customusername;
|
||||
end
|
||||
end);
|
||||
|
||||
-- when occupant is leaving breakout to join the main room and visitors are enabled
|
||||
-- make sure we will allow that participant to join as it is already part of the main room
|
||||
function handle_occupant_leaving_breakout(event)
|
||||
local main_room, occupant, stanza = event.main_room, event.occupant, event.stanza;
|
||||
local presence_status = stanza:get_child_text('status');
|
||||
|
||||
if presence_status ~= 'switch_room' or not visitors_promotion_map[main_room.jid] then
|
||||
return;
|
||||
end
|
||||
|
||||
local node = jid.node(occupant.bare_jid);
|
||||
|
||||
visitors_promotion_map[main_room.jid][node] = {
|
||||
from = 'none';
|
||||
jid = occupant.bare_jid;
|
||||
};
|
||||
end
|
||||
module:hook_global('jitsi-breakout-occupant-leaving', handle_occupant_leaving_breakout);
|
||||
22
resources/prosody-plugins/muc_owner_allow_kick-0.12.patch
Normal file
22
resources/prosody-plugins/muc_owner_allow_kick-0.12.patch
Normal file
@@ -0,0 +1,22 @@
|
||||
--- muc.lib.lua 2016-10-26 18:26:53.432377291 +0000
|
||||
+++ muc.lib.lua 2016-10-26 18:41:40.754426072 +0000
|
||||
@@ -1582,16 +1582,16 @@
|
||||
if event.allowed ~= nil then
|
||||
return event.allowed, event.error, event.condition;
|
||||
end
|
||||
+ local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
|
||||
|
||||
-- Can't do anything to other owners or admins
|
||||
- local occupant_affiliation = self:get_affiliation(occupant.bare_jid);
|
||||
- if occupant_affiliation == "owner" or occupant_affiliation == "admin" then
|
||||
+ local actor_affiliation = self:get_affiliation(actor);
|
||||
+ if (occupant_affiliation == "owner" and actor_affiliation ~= "owner") or (occupant_affiliation == "admin" and actor_affiliation ~= "admin" and actor_affiliation ~= "owner") then
|
||||
return nil, "cancel", "not-allowed";
|
||||
end
|
||||
|
||||
-- If you are trying to give or take moderator role you need to be an owner or admin
|
||||
if occupant.role == "moderator" or role == "moderator" then
|
||||
- local actor_affiliation = self:get_affiliation(actor);
|
||||
if actor_affiliation ~= "owner" and actor_affiliation ~= "admin" then
|
||||
return nil, "cancel", "not-allowed";
|
||||
end
|
||||
397
resources/prosody-plugins/poltergeist.lib.lua
Normal file
397
resources/prosody-plugins/poltergeist.lib.lua
Normal file
@@ -0,0 +1,397 @@
|
||||
local inspect = require("inspect")
|
||||
local jid = require("util.jid")
|
||||
local stanza = require("util.stanza")
|
||||
local timer = require("util.timer")
|
||||
local update_presence_identity = module:require("util").update_presence_identity
|
||||
local uuid = require("util.uuid")
|
||||
|
||||
local component = module:get_option_string(
|
||||
"poltergeist_component",
|
||||
module.host
|
||||
)
|
||||
|
||||
local expiration_timeout = module:get_option_string(
|
||||
"poltergeist_leave_timeout",
|
||||
30 -- defaults to 30 seconds
|
||||
)
|
||||
|
||||
local MUC_NS = "http://jabber.org/protocol/muc"
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Utility functions for commonly used poltergeist codes.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Creates a nick for a poltergeist.
|
||||
-- @param username is the unique username of the poltergeist
|
||||
-- @return a nick to use for xmpp
|
||||
local function create_nick(username)
|
||||
return string.sub(username, 0,8)
|
||||
end
|
||||
|
||||
-- Returns the last presence of the occupant.
|
||||
-- @param room the room instance where to check for occupant
|
||||
-- @param nick the nick of the occupant
|
||||
-- @return presence stanza of the occupant
|
||||
function get_presence(room, nick)
|
||||
local occupant_jid = room:get_occupant_jid(component.."/"..nick)
|
||||
if occupant_jid then
|
||||
return room:get_occupant_by_nick(occupant_jid):get_presence();
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Checks for existence of a poltergeist occupant in a room.
|
||||
-- @param room the room instance where to check for the occupant
|
||||
-- @param nick the nick of the occupant
|
||||
-- @return true if occupant is found, false otherwise
|
||||
function occupies(room, nick)
|
||||
-- Find out if we have a poltergeist occupant in the room for this JID
|
||||
return not not room:get_occupant_jid(component.."/"..nick);
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Username storage for poltergeist.
|
||||
--
|
||||
-- Every poltergeist will have a username stored in a table underneath
|
||||
-- the room name that they are currently active in. The username can
|
||||
-- be retrieved given a room and a user_id. The username is removed from
|
||||
-- a room by providing the room and the nick.
|
||||
--
|
||||
-- A table with a single entry looks like:
|
||||
-- {
|
||||
-- ["[hug]hostilewerewolvesthinkslightly"] = {
|
||||
-- ["655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"] = "ed7757d6-d88d-4e6a-8e24-aca2adc31348",
|
||||
-- ed7757d6 = "655363:52148a3e-b5fb-4cfc-8fbd-f55e793cf657"
|
||||
-- }
|
||||
-- }
|
||||
--------------------------------------------------------------------------------
|
||||
-- state is the table where poltergeist usernames and call resources are stored
|
||||
-- for a given xmpp muc.
|
||||
local state = module:shared("state")
|
||||
|
||||
-- Adds a poltergeist to the store.
|
||||
-- @param room is the room the poltergeist is being added to
|
||||
-- @param user_id is the user_id of the user the poltergeist represents
|
||||
-- @param username is the unique id of the poltergeist itself
|
||||
local function store_username(room, user_id, username)
|
||||
local room_name = jid.node(room.jid)
|
||||
|
||||
if not state[room_name] then
|
||||
state[room_name] = {}
|
||||
end
|
||||
|
||||
state[room_name][user_id] = username
|
||||
state[room_name][create_nick(username)] = user_id
|
||||
end
|
||||
|
||||
-- Retrieves a poltergeist username from the store if one exists.
|
||||
-- @param room is the room to check for the poltergeist in the store
|
||||
-- @param user_id is the user id of the user the poltergeist represents
|
||||
local function get_username(room, user_id)
|
||||
local room_name = jid.node(room.jid)
|
||||
|
||||
if not state[room_name] then
|
||||
return nil
|
||||
end
|
||||
|
||||
return state[room_name][user_id]
|
||||
end
|
||||
|
||||
local function get_username_from_nick(room_name, nick)
|
||||
if not state[room_name] then
|
||||
return nil
|
||||
end
|
||||
|
||||
local user_id = state[room_name][nick]
|
||||
return state[room_name][user_id]
|
||||
end
|
||||
|
||||
-- Removes the username from the store.
|
||||
-- @param room is the room the poltergeist is being removed from
|
||||
-- @param nick is the nick of the muc occupant
|
||||
local function remove_username(room, nick)
|
||||
local room_name = jid.node(room.jid)
|
||||
if not state[room_name] then
|
||||
return
|
||||
end
|
||||
|
||||
local user_id = state[room_name][nick]
|
||||
state[room_name][user_id] = nil
|
||||
state[room_name][nick] = nil
|
||||
end
|
||||
|
||||
-- Removes all poltergeists in the store for the provided room.
|
||||
-- @param room is the room all poltergiest will be removed from
|
||||
local function remove_room(room)
|
||||
local room_name = jid.node(room.jid)
|
||||
if state[room_name] then
|
||||
state[room_name] = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Adds a resource that is associated with a a call in a room. There
|
||||
-- is only one resource for each type.
|
||||
-- @param room is the room the call and poltergeist is in.
|
||||
-- @param call_id is the unique id for the call.
|
||||
-- @param resource_type is type of resource being added.
|
||||
-- @param resource_id is the id of the resource being added.
|
||||
local function add_call_resource(room, call_id, resource_type, resource_id)
|
||||
local room_name = jid.node(room.jid)
|
||||
if not state[room_name] then
|
||||
state[room_name] = {}
|
||||
end
|
||||
|
||||
if not state[room_name][call_id] then
|
||||
state[room_name][call_id] = {}
|
||||
end
|
||||
|
||||
state[room_name][call_id][resource_type] = resource_id
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- State for toggling the tagging of presence stanzas with ignored tag.
|
||||
--
|
||||
-- A poltergeist with it's full room/nick set to ignore will have a jitsi ignore
|
||||
-- tag applied to all presence stanza's broadcasted. The following functions
|
||||
-- assist in managing this state.
|
||||
--------------------------------------------------------------------------------
|
||||
local presence_ignored = {}
|
||||
|
||||
-- Sets the nick to ignored state.
|
||||
-- @param room_nick full room/nick jid
|
||||
local function set_ignored(room_nick)
|
||||
presence_ignored[room_nick] = true
|
||||
end
|
||||
|
||||
-- Resets the nick out of ignored state.
|
||||
-- @param room_nick full room/nick jid
|
||||
local function reset_ignored(room_nick)
|
||||
presence_ignored[room_nick] = nil
|
||||
end
|
||||
|
||||
-- Determines whether or not the leave presence should be tagged with ignored.
|
||||
-- @param room_nick full room/nick jid
|
||||
local function should_ignore(room_nick)
|
||||
if presence_ignored[room_nick] == nil then
|
||||
return false
|
||||
end
|
||||
return presence_ignored[room_nick]
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Poltergeist control functions for adding, updating and removing poltergeist.
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Updates the status tags and call flow tags of an existing poltergeist
|
||||
-- presence.
|
||||
-- @param presence_stanza is the actual presence stanza for a poltergeist.
|
||||
-- @param status is the new status to be updated in the stanza.
|
||||
-- @param call_details is a table of call flow signal information.
|
||||
function update_presence_tags(presence_stanza, status, call_details)
|
||||
local call_cancel = false
|
||||
local call_id = nil
|
||||
|
||||
-- Extract optional call flow signal information.
|
||||
if call_details then
|
||||
call_id = call_details["id"]
|
||||
|
||||
if call_details["cancel"] then
|
||||
call_cancel = call_details["cancel"]
|
||||
end
|
||||
end
|
||||
|
||||
presence_stanza:maptags(function (tag)
|
||||
if tag.name == "status" then
|
||||
if call_cancel then
|
||||
-- If call cancel is set then the status should not be changed.
|
||||
return tag
|
||||
end
|
||||
return stanza.stanza("status"):text(status)
|
||||
elseif tag.name == "call_id" then
|
||||
if call_id then
|
||||
return stanza.stanza("call_id"):text(call_id)
|
||||
else
|
||||
-- If no call id is provided the re-use the existing id.
|
||||
return tag
|
||||
end
|
||||
elseif tag.name == "call_cancel" then
|
||||
if call_cancel then
|
||||
return stanza.stanza("call_cancel"):text("true")
|
||||
else
|
||||
return stanza.stanza("call_cancel"):text("false")
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end)
|
||||
|
||||
return presence_stanza
|
||||
end
|
||||
|
||||
-- Updates the presence status of a poltergeist.
|
||||
-- @param room is the room the poltergeist has occupied
|
||||
-- @param nick is the xmpp nick of the poltergeist occupant
|
||||
-- @param status is the status string to set in the presence
|
||||
-- @param call_details is a table of call flow control details
|
||||
local function update(room, nick, status, call_details)
|
||||
local original_presence = get_presence(room, nick)
|
||||
|
||||
if not original_presence then
|
||||
module:log("info", "update issued for a non-existing poltergeist")
|
||||
return
|
||||
end
|
||||
|
||||
-- update occupant presence with appropriate to and from
|
||||
-- so we can send it again
|
||||
update_presence = stanza.clone(original_presence)
|
||||
update_presence.attr.to = room.jid.."/"..nick
|
||||
update_presence.attr.from = component.."/"..nick
|
||||
|
||||
update_presence = update_presence_tags(update_presence, status, call_details)
|
||||
|
||||
module:log("info", "updating poltergeist: %s/%s - %s", room, nick, status)
|
||||
room:handle_normal_presence(
|
||||
prosody.hosts[component],
|
||||
update_presence
|
||||
)
|
||||
end
|
||||
|
||||
-- Removes the poltergeist from the room.
|
||||
-- @param room is the room the poltergeist has occupied
|
||||
-- @param nick is the xmpp nick of the poltergeist occupant
|
||||
-- @param ignore toggles if the leave subsequent leave presence should be tagged
|
||||
local function remove(room, nick, ignore)
|
||||
local original_presence = get_presence(room, nick);
|
||||
if not original_presence then
|
||||
module:log("info", "attempted to remove a poltergeist with no presence")
|
||||
return
|
||||
end
|
||||
|
||||
local leave_presence = stanza.clone(original_presence)
|
||||
leave_presence.attr.to = room.jid.."/"..nick
|
||||
leave_presence.attr.from = component.."/"..nick
|
||||
leave_presence.attr.type = "unavailable"
|
||||
|
||||
if (ignore) then
|
||||
set_ignored(room.jid.."/"..nick)
|
||||
end
|
||||
|
||||
remove_username(room, nick)
|
||||
module:log("info", "removing poltergeist: %s/%s", room, nick)
|
||||
room:handle_normal_presence(
|
||||
prosody.hosts[component],
|
||||
leave_presence
|
||||
)
|
||||
end
|
||||
|
||||
-- Adds a poltergeist to a muc/room.
|
||||
-- @param room is the room the poltergeist will occupy
|
||||
-- @param is the id of the user the poltergeist represents
|
||||
-- @param display_name is the display name to use for the poltergeist
|
||||
-- @param avatar is the avatar link used for the poltergeist display
|
||||
-- @param context is the session context of the user making the request
|
||||
-- @param status is the presence status string to use
|
||||
-- @param resources is a table of resource types and resource ids to correlate.
|
||||
local function add_to_muc(room, user_id, display_name, avatar, context, status, resources)
|
||||
local username = uuid.generate()
|
||||
local presence_stanza = original_presence(
|
||||
room,
|
||||
username,
|
||||
display_name,
|
||||
avatar,
|
||||
context,
|
||||
status
|
||||
)
|
||||
|
||||
module:log("info", "adding poltergeist: %s/%s", room, create_nick(username))
|
||||
store_username(room, user_id, username)
|
||||
for k, v in pairs(resources) do
|
||||
add_call_resource(room, username, k, v)
|
||||
end
|
||||
room:handle_first_presence(
|
||||
prosody.hosts[component],
|
||||
presence_stanza
|
||||
)
|
||||
|
||||
local remove_delay = 5
|
||||
local expiration = expiration_timeout - remove_delay;
|
||||
local nick = create_nick(username)
|
||||
timer.add_task(
|
||||
expiration,
|
||||
function ()
|
||||
update(room, nick, "expired")
|
||||
timer.add_task(
|
||||
remove_delay,
|
||||
function ()
|
||||
if occupies(room, nick) then
|
||||
remove(room, nick, false)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
-- Generates an original presence for a new poltergeist
|
||||
-- @param room is the room the poltergeist will occupy
|
||||
-- @param username is the unique name for the poltergeist
|
||||
-- @param display_name is the display name to use for the poltergeist
|
||||
-- @param avatar is the avatar link used for the poltergeist display
|
||||
-- @param context is the session context of the user making the request
|
||||
-- @param status is the presence status string to use
|
||||
-- @return a presence stanza that can be used to add the poltergeist to the muc
|
||||
function original_presence(room, username, display_name, avatar, context, status)
|
||||
local nick = create_nick(username)
|
||||
local p = stanza.presence({
|
||||
to = room.jid.."/"..nick,
|
||||
from = component.."/"..nick,
|
||||
}):tag("x", { xmlns = MUC_NS }):up();
|
||||
|
||||
p:tag("bot", { type = "poltergeist" }):up();
|
||||
p:tag("call_cancel"):text(nil):up();
|
||||
p:tag("call_id"):text(username):up();
|
||||
|
||||
if status then
|
||||
p:tag("status"):text(status):up();
|
||||
else
|
||||
p:tag("status"):text(nil):up();
|
||||
end
|
||||
|
||||
if display_name then
|
||||
p:tag(
|
||||
"nick",
|
||||
{ xmlns = "http://jabber.org/protocol/nick" }):text(display_name):up();
|
||||
end
|
||||
|
||||
if avatar then
|
||||
p:tag("avatar-url"):text(avatar):up();
|
||||
end
|
||||
|
||||
-- If the room has a password set, let the poltergeist enter using it
|
||||
local room_password = room:get_password();
|
||||
if room_password then
|
||||
local join = p:get_child("x", MUC_NS);
|
||||
join:tag("password", { xmlns = MUC_NS }):text(room_password);
|
||||
end
|
||||
|
||||
update_presence_identity(
|
||||
p,
|
||||
context.user,
|
||||
context.group,
|
||||
context.creator_user,
|
||||
context.creator_group
|
||||
)
|
||||
return p
|
||||
end
|
||||
|
||||
return {
|
||||
get_username = get_username,
|
||||
get_username_from_nick = get_username_from_nick,
|
||||
occupies = occupies,
|
||||
remove_room = remove_room,
|
||||
reset_ignored = reset_ignored,
|
||||
should_ignore = should_ignore,
|
||||
create_nick = create_nick,
|
||||
add_to_muc = add_to_muc,
|
||||
update = update,
|
||||
remove = remove
|
||||
}
|
||||
14
resources/prosody-plugins/stanza_router_no-log.patch
Normal file
14
resources/prosody-plugins/stanza_router_no-log.patch
Normal file
@@ -0,0 +1,14 @@
|
||||
diff -r 423f240d1173 core/stanza_router.lua
|
||||
--- a/core/stanza_router.lua Tue Feb 21 10:06:54 2023 +0000
|
||||
+++ b/core/stanza_router.lua Wed May 24 11:56:02 2023 -0500
|
||||
@@ -207,7 +207,9 @@
|
||||
else
|
||||
local host_session = hosts[from_host];
|
||||
if not host_session then
|
||||
- log("error", "No hosts[from_host] (please report): %s", stanza);
|
||||
+ -- moved it to debug as it fills visitor's prosody logs and this is a situation where we try to send
|
||||
+ -- presence back to the main server and we don't need anyway as it came from there
|
||||
+ log("debug", "No hosts[from_host] (please report): %s", stanza);
|
||||
else
|
||||
local xmlns = stanza.attr.xmlns;
|
||||
stanza.attr.xmlns = nil;
|
||||
539
resources/prosody-plugins/token/util.lib.lua
Normal file
539
resources/prosody-plugins/token/util.lib.lua
Normal file
@@ -0,0 +1,539 @@
|
||||
-- Token authentication
|
||||
-- Copyright (C) 2021-present 8x8, Inc.
|
||||
|
||||
local basexx = require "basexx";
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
local hex = require "util.hex";
|
||||
local jwt = module:require "luajwtjitsi";
|
||||
local jid = require "util.jid";
|
||||
local json_safe = require "cjson.safe";
|
||||
local path = require "util.paths";
|
||||
local sha256 = require "util.hashes".sha256;
|
||||
local main_util = module:require "util";
|
||||
local ends_with = main_util.ends_with;
|
||||
local http_get_with_retry = main_util.http_get_with_retry;
|
||||
local extract_subdomain = main_util.extract_subdomain;
|
||||
local starts_with = main_util.starts_with;
|
||||
local table_shallow_copy = main_util.table_shallow_copy;
|
||||
local cjson_safe = require 'cjson.safe'
|
||||
local timer = require "util.timer";
|
||||
local async = require "util.async";
|
||||
local inspect = require 'inspect';
|
||||
|
||||
local nr_retries = 3;
|
||||
local ssl = require "ssl";
|
||||
|
||||
-- TODO: Figure out a less arbitrary default cache size.
|
||||
local cacheSize = module:get_option_number("jwt_pubkey_cache_size", 128);
|
||||
|
||||
-- the cache for generated asap jwt tokens
|
||||
local jwtKeyCache = require 'util.cache'.new(cacheSize);
|
||||
|
||||
local ASAPTTL_THRESHOLD = module:get_option_number('asap_ttl_threshold', 600);
|
||||
local ASAPTTL = module:get_option_number('asap_ttl', 3600);
|
||||
local ASAPIssuer = module:get_option_string('asap_issuer', 'jitsi');
|
||||
local ASAPAudience = module:get_option_string('asap_audience', 'jitsi');
|
||||
local ASAPKeyId = module:get_option_string('asap_key_id', 'jitsi');
|
||||
local ASAPKeyPath = module:get_option_string('asap_key_path', '/etc/prosody/certs/asap.key');
|
||||
|
||||
local ASAPKey;
|
||||
local f = io.open(ASAPKeyPath, 'r');
|
||||
|
||||
if f then
|
||||
ASAPKey = f:read('*all');
|
||||
f:close();
|
||||
end
|
||||
|
||||
local Util = {}
|
||||
Util.__index = Util
|
||||
|
||||
--- Constructs util class for token verifications.
|
||||
-- Constructor that uses the passed module to extract all the
|
||||
-- needed configurations.
|
||||
-- If configuration is missing returns nil
|
||||
-- @param module the module in which options to check for configs.
|
||||
-- @return the new instance or nil
|
||||
function Util.new(module)
|
||||
local self = setmetatable({}, Util)
|
||||
|
||||
self.appId = module:get_option_string("app_id");
|
||||
self.appSecret = module:get_option_string("app_secret");
|
||||
self.asapKeyServer = module:get_option_string("asap_key_server");
|
||||
-- A URL that will return json file with a mapping between kids and public keys
|
||||
-- If the response Cache-Control header we will respect it and refresh it
|
||||
self.cacheKeysUrl = module:get_option_string("cache_keys_url");
|
||||
self.signatureAlgorithm = module:get_option_string("signature_algorithm");
|
||||
self.allowEmptyToken = module:get_option_boolean("allow_empty_token");
|
||||
|
||||
self.cache = require"util.cache".new(cacheSize);
|
||||
|
||||
--[[
|
||||
Multidomain can be supported in some deployments. In these deployments
|
||||
there is a virtual conference muc, which address contains the subdomain
|
||||
to use. Those deployments are accessible
|
||||
by URL https://domain/subdomain.
|
||||
Then the address of the room will be:
|
||||
roomName@conference.subdomain.domain. This is like a virtual address
|
||||
where there is only one muc configured by default with address:
|
||||
conference.domain and the actual presentation of the room in that muc
|
||||
component is [subdomain]roomName@conference.domain.
|
||||
These setups relay on configuration 'muc_domain_base' which holds
|
||||
the main domain and we use it to subtract subdomains from the
|
||||
virtual addresses.
|
||||
The following configurations are for multidomain setups and domain name
|
||||
verification:
|
||||
--]]
|
||||
|
||||
-- optional parameter for custom muc component prefix,
|
||||
-- defaults to "conference"
|
||||
self.muc_domain_prefix = module:get_option_string(
|
||||
"muc_mapper_domain_prefix", "conference");
|
||||
-- domain base, which is the main domain used in the deployment,
|
||||
-- the main VirtualHost for the deployment
|
||||
self.muc_domain_base = module:get_option_string("muc_mapper_domain_base");
|
||||
-- The "real" MUC domain that we are proxying to
|
||||
if self.muc_domain_base then
|
||||
self.muc_domain = module:get_option_string(
|
||||
"muc_mapper_domain",
|
||||
self.muc_domain_prefix.."."..self.muc_domain_base);
|
||||
end
|
||||
-- whether domain name verification is enabled, by default it is enabled
|
||||
-- when disabled checking domain name and tenant if available will be skipped, we will check only room name.
|
||||
self.enableDomainVerification = module:get_option_boolean('enable_domain_verification', true);
|
||||
|
||||
if self.allowEmptyToken == true then
|
||||
module:log("warn", "WARNING - empty tokens allowed");
|
||||
end
|
||||
|
||||
if self.appId == nil then
|
||||
module:log("error", "'app_id' must not be empty");
|
||||
return nil;
|
||||
end
|
||||
|
||||
if self.appSecret == nil and self.asapKeyServer == nil then
|
||||
module:log("error", "'app_secret' or 'asap_key_server' must be specified");
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Set defaults for signature algorithm
|
||||
if self.signatureAlgorithm == nil then
|
||||
if self.asapKeyServer ~= nil then
|
||||
self.signatureAlgorithm = "RS256"
|
||||
elseif self.appSecret ~= nil then
|
||||
self.signatureAlgorithm = "HS256"
|
||||
end
|
||||
end
|
||||
|
||||
--array of accepted issuers: by default only includes our appId
|
||||
self.acceptedIssuers = module:get_option_array('asap_accepted_issuers',{self.appId})
|
||||
|
||||
--array of accepted audiences: by default only includes our appId
|
||||
self.acceptedAudiences = module:get_option_array('asap_accepted_audiences',{'*'})
|
||||
|
||||
self.requireRoomClaim = module:get_option_boolean('asap_require_room_claim', true);
|
||||
|
||||
if self.asapKeyServer and not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return nil;
|
||||
end
|
||||
|
||||
if self.cacheKeysUrl then
|
||||
self.cachedKeys = {};
|
||||
local update_keys_cache;
|
||||
update_keys_cache = async.runner(function (name)
|
||||
local content, code, cache_for;
|
||||
content, code, cache_for = http_get_with_retry(self.cacheKeysUrl, nr_retries);
|
||||
if content ~= nil then
|
||||
local keys_to_delete = table_shallow_copy(self.cachedKeys);
|
||||
-- Let's convert any certificate to public key
|
||||
for k, v in pairs(cjson_safe.decode(content)) do
|
||||
if starts_with(v, '-----BEGIN CERTIFICATE-----') then
|
||||
self.cachedKeys[k] = ssl.loadcertificate(v):pubkey();
|
||||
-- do not clean this key if it already exists
|
||||
keys_to_delete[k] = nil;
|
||||
end
|
||||
end
|
||||
-- let's schedule the clean in an hour and a half, current tokens will be valid for an hour
|
||||
timer.add_task(90*60, function ()
|
||||
for k, _ in pairs(keys_to_delete) do
|
||||
self.cachedKeys[k] = nil;
|
||||
end
|
||||
end);
|
||||
|
||||
if cache_for then
|
||||
cache_for = tonumber(cache_for);
|
||||
-- let's schedule new update 60 seconds before the cache expiring
|
||||
if cache_for > 60 then
|
||||
cache_for = cache_for - 60;
|
||||
end
|
||||
timer.add_task(cache_for, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
else
|
||||
-- no cache header let's consider updating in 6hours
|
||||
timer.add_task(6*60*60, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
end
|
||||
else
|
||||
module:log('warn', 'Failed to retrieve cached public keys code:%s', code);
|
||||
-- failed let's retry in 30 seconds
|
||||
timer.add_task(30, function ()
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end);
|
||||
end
|
||||
end);
|
||||
update_keys_cache:run("update_keys_cache");
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Util:set_asap_key_server(asapKeyServer)
|
||||
self.asapKeyServer = asapKeyServer;
|
||||
end
|
||||
|
||||
function Util:set_asap_accepted_issuers(acceptedIssuers)
|
||||
self.acceptedIssuers = acceptedIssuers;
|
||||
end
|
||||
|
||||
function Util:set_asap_accepted_audiences(acceptedAudiences)
|
||||
self.acceptedAudiences = acceptedAudiences;
|
||||
end
|
||||
|
||||
function Util:set_asap_require_room_claim(checkRoom)
|
||||
self.requireRoomClaim = checkRoom;
|
||||
end
|
||||
|
||||
function Util:clear_asap_cache()
|
||||
self.cache = require"util.cache".new(cacheSize);
|
||||
end
|
||||
|
||||
--- Returns the public key by keyID
|
||||
-- @param keyId the key ID to request
|
||||
-- @return the public key (the content of requested resource) or nil
|
||||
function Util:get_public_key(keyId)
|
||||
local content = self.cache:get(keyId);
|
||||
local code;
|
||||
if content == nil then
|
||||
-- If the key is not found in the cache.
|
||||
-- module:log("debug", "Cache miss for key: %s", keyId);
|
||||
local keyurl = path.join(self.asapKeyServer, hex.to(sha256(keyId))..'.pem');
|
||||
-- module:log("debug", "Fetching public key from: %s", keyurl);
|
||||
content, code = http_get_with_retry(keyurl, nr_retries);
|
||||
if content ~= nil then
|
||||
self.cache:set(keyId, content);
|
||||
else
|
||||
if code == nil then
|
||||
-- this is timeout after nr_retries retries
|
||||
module:log('warn', 'Timeout retrieving %s from %s', keyId, keyurl);
|
||||
end
|
||||
end
|
||||
return content;
|
||||
else
|
||||
-- If the key is in the cache, use it.
|
||||
-- module:log("debug", "Cache hit for key: %s", keyId);
|
||||
return content;
|
||||
end
|
||||
end
|
||||
|
||||
--- Verifies token and process needed values to be stored in the session.
|
||||
-- Token is obtained from session.auth_token.
|
||||
-- Stores in session the following values:
|
||||
-- session.jitsi_meet_room - the room name value from the token
|
||||
-- session.jitsi_meet_domain - the domain name value from the token
|
||||
-- session.jitsi_meet_context_user - the user details from the token
|
||||
-- session.jitsi_meet_context_room - the room details from the token
|
||||
-- session.jitsi_meet_context_group - the group value from the token
|
||||
-- session.jitsi_meet_context_features - the features value from the token
|
||||
-- @param session the current session
|
||||
-- @return false and error
|
||||
function Util:process_and_verify_token(session)
|
||||
if session.auth_token == nil then
|
||||
if self.allowEmptyToken then
|
||||
return true;
|
||||
else
|
||||
return false, "not-allowed", "token required";
|
||||
end
|
||||
end
|
||||
|
||||
local key;
|
||||
if session.public_key then
|
||||
-- We're using an public key stored in the session
|
||||
-- module:log("debug","Public key was found on the session");
|
||||
key = session.public_key;
|
||||
elseif self.asapKeyServer and session.auth_token ~= nil then
|
||||
-- We're fetching an public key from an ASAP server
|
||||
local dotFirst = session.auth_token:find("%.");
|
||||
if not dotFirst then return false, "not-allowed", "Invalid token" end
|
||||
local header, err = json_safe.decode(basexx.from_url64(session.auth_token:sub(1,dotFirst-1)));
|
||||
if err then
|
||||
return false, "not-allowed", "bad token format";
|
||||
end
|
||||
local kid = header["kid"];
|
||||
if kid == nil then
|
||||
return false, "not-allowed", "'kid' claim is missing";
|
||||
end
|
||||
local alg = header["alg"];
|
||||
if alg == nil then
|
||||
return false, "not-allowed", "'alg' claim is missing";
|
||||
end
|
||||
if alg.sub(alg,1,2) ~= "RS" then
|
||||
return false, "not-allowed", "'kid' claim only support with RS family";
|
||||
end
|
||||
|
||||
if self.cachedKeys and self.cachedKeys[kid] then
|
||||
key = self.cachedKeys[kid];
|
||||
else
|
||||
key = self:get_public_key(kid);
|
||||
end
|
||||
|
||||
if key == nil then
|
||||
return false, "not-allowed", "could not obtain public key";
|
||||
end
|
||||
elseif self.appSecret ~= nil then
|
||||
-- We're using a symmetric secret
|
||||
key = self.appSecret
|
||||
end
|
||||
|
||||
if key == nil then
|
||||
return false, "not-allowed", "signature verification key is missing";
|
||||
end
|
||||
|
||||
-- now verify the whole token
|
||||
local claims, msg = jwt.verify(
|
||||
session.auth_token,
|
||||
self.signatureAlgorithm,
|
||||
key,
|
||||
self.acceptedIssuers,
|
||||
self.acceptedAudiences
|
||||
)
|
||||
if claims ~= nil then
|
||||
if self.requireRoomClaim then
|
||||
local roomClaim = claims["room"];
|
||||
if roomClaim == nil then
|
||||
return false, "'room' claim is missing";
|
||||
end
|
||||
end
|
||||
|
||||
-- Binds room name to the session which is later checked on MUC join
|
||||
session.jitsi_meet_room = claims["room"];
|
||||
-- Binds domain name to the session
|
||||
session.jitsi_meet_domain = claims["sub"];
|
||||
|
||||
-- Binds the user details to the session if available
|
||||
if claims["context"] ~= nil then
|
||||
session.jitsi_meet_str_tenant = claims["context"]["tenant"];
|
||||
|
||||
if claims["context"]["user"] ~= nil then
|
||||
session.jitsi_meet_context_user = claims["context"]["user"];
|
||||
end
|
||||
|
||||
if claims["context"]["group"] ~= nil then
|
||||
-- Binds any group details to the session
|
||||
session.jitsi_meet_context_group = claims["context"]["group"];
|
||||
end
|
||||
|
||||
if claims["context"]["features"] ~= nil then
|
||||
-- Binds any features details to the session
|
||||
session.jitsi_meet_context_features = claims["context"]["features"];
|
||||
end
|
||||
if claims["context"]["room"] ~= nil then
|
||||
session.jitsi_meet_context_room = claims["context"]["room"]
|
||||
end
|
||||
elseif claims["user_id"] then
|
||||
session.jitsi_meet_context_user = {};
|
||||
session.jitsi_meet_context_user.id = claims["user_id"];
|
||||
end
|
||||
|
||||
-- fire event that token has been verified and pass the session and the decoded token
|
||||
prosody.events.fire_event('jitsi-authentication-token-verified', {
|
||||
session = session;
|
||||
claims = claims;
|
||||
});
|
||||
|
||||
if session.contextRequired and claims["context"] == nil then
|
||||
return false, "not-allowed", 'jwt missing required context claim';
|
||||
end
|
||||
|
||||
return true;
|
||||
else
|
||||
return false, "not-allowed", msg;
|
||||
end
|
||||
end
|
||||
|
||||
--- Verifies room name and domain if necessary.
|
||||
-- Checks configs and if necessary checks the room name extracted from
|
||||
-- room_address against the one saved in the session when token was verified.
|
||||
-- Also verifies domain name from token against the domain in the room_address,
|
||||
-- if enableDomainVerification is enabled.
|
||||
-- @param session the current session
|
||||
-- @param room_address the whole room address as received
|
||||
-- @return returns true in case room was verified or there is no need to verify
|
||||
-- it and returns false in case verification was processed
|
||||
-- and was not successful
|
||||
function Util:verify_room(session, room_address)
|
||||
if self.allowEmptyToken and session.auth_token == nil then
|
||||
--module:log("debug", "Skipped room token verification - empty tokens are allowed");
|
||||
return true;
|
||||
end
|
||||
|
||||
-- extract room name using all chars, except the not allowed ones
|
||||
local room,_,_ = jid.split(room_address);
|
||||
if room == nil then
|
||||
log("error",
|
||||
"Unable to get name of the MUC room ? to: %s", room_address);
|
||||
return true;
|
||||
end
|
||||
|
||||
local auth_room = session.jitsi_meet_room;
|
||||
if auth_room then
|
||||
if type(auth_room) == 'string' then
|
||||
auth_room = string.lower(auth_room);
|
||||
else
|
||||
module:log('warn', 'session.jitsi_meet_room not string: %s', inspect(auth_room));
|
||||
end
|
||||
end
|
||||
if not self.enableDomainVerification then
|
||||
-- if auth_room is missing, this means user is anonymous (no token for
|
||||
-- its domain) we let it through, jicofo is verifying creation domain
|
||||
if auth_room and (room ~= auth_room and not ends_with(room, ']'..auth_room)) and auth_room ~= '*' then
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local room_address_to_verify = jid.bare(room_address);
|
||||
local room_node = jid.node(room_address);
|
||||
-- parses bare room address, for multidomain expected format is:
|
||||
-- [subdomain]roomName@conference.domain
|
||||
local target_subdomain, target_room = extract_subdomain(room_node);
|
||||
|
||||
-- if we have '*' as room name in token, this means all rooms are allowed
|
||||
-- so we will use the actual name of the room when constructing strings
|
||||
-- to verify subdomains and domains to simplify checks
|
||||
local room_to_check;
|
||||
if auth_room == '*' then
|
||||
-- authorized for accessing any room assign to room_to_check the actual
|
||||
-- room name
|
||||
if target_room ~= nil then
|
||||
-- we are in multidomain mode and we were able to extract room name
|
||||
room_to_check = target_room;
|
||||
else
|
||||
-- no target_room, room_address_to_verify does not contain subdomain
|
||||
-- so we get just the node which is the room name
|
||||
room_to_check = room_node;
|
||||
end
|
||||
else
|
||||
-- no wildcard, so check room against authorized room from the token
|
||||
if session.jitsi_meet_context_room and (session.jitsi_meet_context_room["regex"] == true or session.jitsi_meet_context_room["regex"] == "true") then
|
||||
if target_room ~= nil then
|
||||
-- room with subdomain
|
||||
room_to_check = target_room:match(auth_room);
|
||||
else
|
||||
room_to_check = room_node:match(auth_room);
|
||||
end
|
||||
else
|
||||
-- not a regex
|
||||
room_to_check = auth_room;
|
||||
end
|
||||
-- module:log("debug", "room to check: %s", room_to_check)
|
||||
if not room_to_check then
|
||||
if not self.requireRoomClaim then
|
||||
-- if we do not require to have the room claim, and it is missing
|
||||
-- there is no point of continue and verifying the roomName and the tenant
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
if session.jitsi_meet_str_tenant
|
||||
and string.lower(session.jitsi_meet_str_tenant) ~= session.jitsi_web_query_prefix then
|
||||
module:log('warn', 'Tenant differs for user:%s group:%s url_tenant:%s token_tenant:%s',
|
||||
session.jitsi_meet_context_user and session.jitsi_meet_context_user.id or '',
|
||||
session.jitsi_meet_context_group,
|
||||
session.jitsi_web_query_prefix, session.jitsi_meet_str_tenant);
|
||||
session.jitsi_meet_tenant_mismatch = true;
|
||||
end
|
||||
|
||||
local auth_domain = string.lower(session.jitsi_meet_domain);
|
||||
local subdomain_to_check;
|
||||
if target_subdomain then
|
||||
if auth_domain == '*' then
|
||||
-- check for wildcard in JWT claim, allow access if found
|
||||
subdomain_to_check = target_subdomain;
|
||||
else
|
||||
-- no wildcard in JWT claim, so check subdomain against sub in token
|
||||
subdomain_to_check = auth_domain;
|
||||
end
|
||||
-- from this point we depend on muc_domain_base,
|
||||
-- deny access if option is missing
|
||||
if not self.muc_domain_base then
|
||||
module:log("warn", "No 'muc_domain_base' option set, denying access!");
|
||||
return false;
|
||||
end
|
||||
|
||||
return room_address_to_verify == jid.join(
|
||||
"["..subdomain_to_check.."]"..room_to_check, self.muc_domain);
|
||||
else
|
||||
if auth_domain == '*' then
|
||||
-- check for wildcard in JWT claim, allow access if found
|
||||
subdomain_to_check = self.muc_domain;
|
||||
else
|
||||
-- no wildcard in JWT claim, so check subdomain against sub in token
|
||||
subdomain_to_check = self.muc_domain_prefix.."."..auth_domain;
|
||||
end
|
||||
-- we do not have a domain part (multidomain is not enabled)
|
||||
-- verify with info from the token
|
||||
return room_address_to_verify == jid.join(room_to_check, subdomain_to_check);
|
||||
end
|
||||
end
|
||||
|
||||
function Util:generateAsapToken(audience)
|
||||
if not ASAPKey then
|
||||
module:log('warn', 'No ASAP Key read, asap key generation is disabled');
|
||||
return ''
|
||||
end
|
||||
|
||||
audience = audience or ASAPAudience
|
||||
local t = os.time()
|
||||
local err
|
||||
local exp_key = 'asap_exp.'..audience
|
||||
local token_key = 'asap_token.'..audience
|
||||
local exp = jwtKeyCache:get(exp_key)
|
||||
local token = jwtKeyCache:get(token_key)
|
||||
|
||||
--if we find a token and it isn't too far from expiry, then use it
|
||||
if token ~= nil and exp ~= nil then
|
||||
exp = tonumber(exp)
|
||||
if (exp - t) > ASAPTTL_THRESHOLD then
|
||||
return token
|
||||
end
|
||||
end
|
||||
|
||||
--expiry is the current time plus TTL
|
||||
exp = t + ASAPTTL
|
||||
local payload = {
|
||||
iss = ASAPIssuer,
|
||||
aud = audience,
|
||||
nbf = t,
|
||||
exp = exp,
|
||||
}
|
||||
|
||||
-- encode
|
||||
local alg = 'RS256'
|
||||
token, err = jwt.encode(payload, ASAPKey, alg, { kid = ASAPKeyId })
|
||||
if not err then
|
||||
token = 'Bearer '..token
|
||||
jwtKeyCache:set(exp_key, exp)
|
||||
jwtKeyCache:set(token_key, token)
|
||||
return token
|
||||
else
|
||||
return ''
|
||||
end
|
||||
end
|
||||
|
||||
return Util;
|
||||
766
resources/prosody-plugins/util.lib.lua
Normal file
766
resources/prosody-plugins/util.lib.lua
Normal file
@@ -0,0 +1,766 @@
|
||||
local http_server = require "net.http.server";
|
||||
local jid = require "util.jid";
|
||||
local st = require 'util.stanza';
|
||||
local timer = require "util.timer";
|
||||
local http = require "net.http";
|
||||
local cache = require "util.cache";
|
||||
local array = require "util.array";
|
||||
local is_set = require 'util.set'.is_set;
|
||||
local usermanager = require 'core.usermanager';
|
||||
|
||||
local config_global_admin_jids = module:context('*'):get_option_set('admins', {}) / jid.prep;
|
||||
local config_admin_jids = module:get_option_inherited_set('admins', {}) / jid.prep;
|
||||
|
||||
local http_timeout = 30;
|
||||
local have_async, async = pcall(require, "util.async");
|
||||
local http_headers = {
|
||||
["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
|
||||
};
|
||||
|
||||
local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
|
||||
|
||||
-- defaults to module.host, the module that uses the utility
|
||||
local muc_domain_base = module:get_option_string("muc_mapper_domain_base", module.host);
|
||||
|
||||
-- The "real" MUC domain that we are proxying to
|
||||
local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
|
||||
|
||||
local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
|
||||
local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
|
||||
-- The pattern used to extract the target subdomain
|
||||
-- (e.g. extract 'foo' from 'conference.foo.example.com')
|
||||
local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
|
||||
|
||||
-- table to store all incoming iqs without roomname in it, like discoinfo to the muc component
|
||||
local roomless_iqs = {};
|
||||
|
||||
local OUTBOUND_SIP_JIBRI_PREFIXES = { 'outbound-sip-jibri@', 'sipjibriouta@', 'sipjibrioutb@' };
|
||||
local INBOUND_SIP_JIBRI_PREFIXES = { 'inbound-sip-jibri@', 'sipjibriina@', 'sipjibriina@' };
|
||||
local RECORDER_PREFIXES = module:get_option_inherited_set('recorder_prefixes', { 'recorder@recorder.', 'jibria@recorder.', 'jibrib@recorder.' });
|
||||
local TRANSCRIBER_PREFIXES = module:get_option_inherited_set('transcriber_prefixes', { 'transcriber@recorder.', 'transcribera@recorder.', 'transcriberb@recorder.' });
|
||||
|
||||
local split_subdomain_cache = cache.new(1000);
|
||||
local extract_subdomain_cache = cache.new(1000);
|
||||
local internal_room_jid_cache = cache.new(1000);
|
||||
|
||||
local moderated_subdomains = module:get_option_set("allowners_moderated_subdomains", {})
|
||||
local moderated_rooms = module:get_option_set("allowners_moderated_rooms", {})
|
||||
|
||||
-- Utility function to split room JID to include room name and subdomain
|
||||
-- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
|
||||
local function room_jid_split_subdomain(room_jid)
|
||||
local ret = split_subdomain_cache:get(room_jid);
|
||||
if ret then
|
||||
return ret.node, ret.host, ret.resource, ret.subdomain;
|
||||
end
|
||||
|
||||
local node, host, resource = jid.split(room_jid);
|
||||
|
||||
local target_subdomain = host and host:match(target_subdomain_pattern);
|
||||
local cache_value = {node=node, host=host, resource=resource, subdomain=target_subdomain};
|
||||
split_subdomain_cache:set(room_jid, cache_value);
|
||||
return node, host, resource, target_subdomain;
|
||||
end
|
||||
|
||||
--- Utility function to check and convert a room JID from
|
||||
--- virtual room1@conference.foo.example.com to real [foo]room1@conference.example.com
|
||||
-- @param room_jid the room jid to match and rewrite if needed
|
||||
-- @param stanza the stanza
|
||||
-- @return returns room jid [foo]room1@conference.example.com when it has subdomain
|
||||
-- otherwise room1@conference.example.com(the room_jid value untouched)
|
||||
local function room_jid_match_rewrite(room_jid, stanza)
|
||||
local node, _, resource, target_subdomain = room_jid_split_subdomain(room_jid);
|
||||
if not target_subdomain then
|
||||
-- module:log("debug", "No need to rewrite out 'to' %s", room_jid);
|
||||
return room_jid;
|
||||
end
|
||||
-- Ok, rewrite room_jid address to new format
|
||||
local new_node, new_host, new_resource;
|
||||
if node then
|
||||
new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
|
||||
else
|
||||
-- module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
|
||||
new_host, new_resource = muc_domain, resource;
|
||||
|
||||
if (stanza and stanza.attr and stanza.attr.id) then
|
||||
roomless_iqs[stanza.attr.id] = stanza.attr.to;
|
||||
end
|
||||
end
|
||||
|
||||
return jid.join(new_node, new_host, new_resource);
|
||||
end
|
||||
|
||||
-- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
|
||||
local function internal_room_jid_match_rewrite(room_jid, stanza)
|
||||
-- first check for roomless_iqs
|
||||
if (stanza and stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
|
||||
local result = roomless_iqs[stanza.attr.id];
|
||||
roomless_iqs[stanza.attr.id] = nil;
|
||||
return result;
|
||||
end
|
||||
|
||||
local ret = internal_room_jid_cache:get(room_jid);
|
||||
if ret then
|
||||
return ret;
|
||||
end
|
||||
|
||||
local node, host, resource = jid.split(room_jid);
|
||||
if host ~= muc_domain or not node then
|
||||
-- module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
|
||||
internal_room_jid_cache:set(room_jid, room_jid);
|
||||
return room_jid;
|
||||
end
|
||||
|
||||
local target_subdomain, target_node = extract_subdomain(node);
|
||||
if not (target_node and target_subdomain) then
|
||||
-- module:log("debug", "Not rewriting... unexpected node format: %s", node);
|
||||
internal_room_jid_cache:set(room_jid, room_jid);
|
||||
return room_jid;
|
||||
end
|
||||
|
||||
-- Ok, rewrite room_jid address to pretty format
|
||||
ret = jid.join(target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource);
|
||||
internal_room_jid_cache:set(room_jid, ret);
|
||||
return ret;
|
||||
end
|
||||
|
||||
--- Finds and returns room by its jid
|
||||
-- @param room_jid the room jid to search in the muc component
|
||||
-- @return returns room if found or nil
|
||||
function get_room_from_jid(room_jid)
|
||||
local _, host = jid.split(room_jid);
|
||||
local component = hosts[host];
|
||||
if component then
|
||||
local muc = component.modules.muc
|
||||
if muc then
|
||||
return muc.get_room_from_jid(room_jid);
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the room if available, work and in multidomain mode
|
||||
-- @param room_name the name of the room
|
||||
-- @param group name of the group (optional)
|
||||
-- @return returns room if found or nil
|
||||
function get_room_by_name_and_subdomain(room_name, subdomain)
|
||||
local room_address;
|
||||
|
||||
-- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
|
||||
if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
|
||||
room_address = jid.join("["..subdomain.."]"..room_name, muc_domain);
|
||||
else
|
||||
room_address = jid.join(room_name, muc_domain);
|
||||
end
|
||||
|
||||
return get_room_from_jid(room_address);
|
||||
end
|
||||
|
||||
function async_handler_wrapper(event, handler)
|
||||
if not have_async then
|
||||
module:log("error", "requires a version of Prosody with util.async");
|
||||
return nil;
|
||||
end
|
||||
|
||||
local runner = async.runner;
|
||||
|
||||
-- Grab a local response so that we can send the http response when
|
||||
-- the handler is done.
|
||||
local response = event.response;
|
||||
local async_func = runner(
|
||||
function (event)
|
||||
local result = handler(event)
|
||||
|
||||
-- If there is a status code in the result from the
|
||||
-- wrapped handler then add it to the response.
|
||||
if tonumber(result.status_code) ~= nil then
|
||||
response.status_code = result.status_code
|
||||
end
|
||||
|
||||
-- If there are headers in the result from the
|
||||
-- wrapped handler then add them to the response.
|
||||
if result.headers ~= nil then
|
||||
response.headers = result.headers
|
||||
end
|
||||
|
||||
-- Send the response to the waiting http client with
|
||||
-- or without the body from the wrapped handler.
|
||||
if result.body ~= nil then
|
||||
response:send(result.body)
|
||||
else
|
||||
response:send();
|
||||
end
|
||||
end
|
||||
)
|
||||
async_func:run(event)
|
||||
-- return true to keep the client http connection open.
|
||||
return true;
|
||||
end
|
||||
|
||||
--- Updates presence stanza, by adding identity node
|
||||
-- @param stanza the presence stanza
|
||||
-- @param user the user to which presence we are updating identity
|
||||
-- @param group the group of the user to which presence we are updating identity
|
||||
-- @param creator_user the user who created the user which presence we
|
||||
-- are updating (this is the poltergeist case, where a user creates
|
||||
-- a poltergeist), optional.
|
||||
-- @param creator_group the group of the user who created the user which
|
||||
-- presence we are updating (this is the poltergeist case, where a user creates
|
||||
-- a poltergeist), optional.
|
||||
function update_presence_identity(stanza, user, group, creator_user, creator_group)
|
||||
|
||||
-- First remove any 'identity' element if it already
|
||||
-- exists, so it cannot be spoofed by a client
|
||||
stanza:maptags(
|
||||
function(tag)
|
||||
for k, v in pairs(tag) do
|
||||
if k == "name" and v == "identity" then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return tag
|
||||
end
|
||||
);
|
||||
|
||||
if not user then
|
||||
return;
|
||||
end
|
||||
|
||||
stanza:tag("identity"):tag("user");
|
||||
for k, v in pairs(user) do
|
||||
v = tostring(v)
|
||||
stanza:tag(k):text(v):up();
|
||||
end
|
||||
stanza:up();
|
||||
|
||||
-- Add the group information if it is present
|
||||
if group then
|
||||
stanza:tag("group"):text(group):up();
|
||||
end
|
||||
|
||||
-- Add the creator user information if it is present
|
||||
if creator_user then
|
||||
stanza:tag("creator_user");
|
||||
for k, v in pairs(creator_user) do
|
||||
stanza:tag(k):text(v):up();
|
||||
end
|
||||
stanza:up();
|
||||
|
||||
-- Add the creator group information if it is present
|
||||
if creator_group then
|
||||
stanza:tag("creator_group"):text(creator_group):up();
|
||||
end
|
||||
end
|
||||
|
||||
stanza:up(); -- Close identity tag
|
||||
end
|
||||
|
||||
-- Utility function to check whether feature is present and enabled. Allow
|
||||
-- a feature if there are features present in the session(coming from
|
||||
-- the token) and the value of the feature is true.
|
||||
-- if features are missing from the token we check whether it is moderator
|
||||
function is_feature_allowed(ft, features, is_moderator)
|
||||
if features then
|
||||
return features[ft] == "true" or features[ft] == true;
|
||||
else
|
||||
return is_moderator;
|
||||
end
|
||||
end
|
||||
|
||||
--- Extracts the subdomain and room name from internal jid node [foo]room1
|
||||
-- @return subdomain(optional, if extracted or nil), the room name, the customer_id in case of vpaas
|
||||
function extract_subdomain(room_node)
|
||||
local ret = extract_subdomain_cache:get(room_node);
|
||||
if ret then
|
||||
return ret.subdomain, ret.room, ret.customer_id;
|
||||
end
|
||||
|
||||
local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
|
||||
|
||||
if not subdomain then
|
||||
room_name = room_node;
|
||||
end
|
||||
|
||||
local _, customer_id = subdomain and subdomain:match("^(vpaas%-magic%-cookie%-)(.*)$") or nil, nil;
|
||||
local cache_value = { subdomain=subdomain, room=room_name, customer_id=customer_id };
|
||||
extract_subdomain_cache:set(room_node, cache_value);
|
||||
return subdomain, room_name, customer_id;
|
||||
end
|
||||
|
||||
function starts_with(str, start)
|
||||
if not str then
|
||||
return false;
|
||||
end
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
|
||||
function starts_with_one_of(str, prefixes)
|
||||
if not str or not prefixes then
|
||||
return false;
|
||||
end
|
||||
|
||||
if is_set(prefixes) then
|
||||
-- set is a table with keys and value of true
|
||||
for k, _ in prefixes:items() do
|
||||
if starts_with(str, k) then
|
||||
return k;
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, v in pairs(prefixes) do
|
||||
if starts_with(str, v) then
|
||||
return v;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function ends_with(str, ending)
|
||||
if not str then
|
||||
return false;
|
||||
end
|
||||
|
||||
return ending == "" or str:sub(-#ending) == ending
|
||||
end
|
||||
|
||||
-- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
|
||||
function is_healthcheck_room(room_jid)
|
||||
return starts_with(room_jid, "__jicofo-health-check");
|
||||
end
|
||||
|
||||
--- Utility function to make an http get request and
|
||||
--- retry @param retry number of times
|
||||
-- @param url endpoint to be called
|
||||
-- @param retry nr of retries, if retry is
|
||||
-- @param auth_token value to be passed as auth Bearer
|
||||
-- nil there will be no retries
|
||||
-- @returns result of the http call or nil if
|
||||
-- the external call failed after the last retry
|
||||
function http_get_with_retry(url, retry, auth_token)
|
||||
local content, code, cache_for;
|
||||
local timeout_occurred;
|
||||
local wait, done = async.waiter();
|
||||
local request_headers = http_headers or {}
|
||||
if auth_token ~= nil then
|
||||
request_headers['Authorization'] = 'Bearer ' .. auth_token
|
||||
end
|
||||
|
||||
local function cb(content_, code_, response_, request_)
|
||||
if timeout_occurred == nil then
|
||||
code = code_;
|
||||
if code == 200 or code == 204 then
|
||||
-- module:log("debug", "External call was successful, content %s", content_);
|
||||
content = content_;
|
||||
|
||||
-- if there is cache-control header, let's return the max-age value
|
||||
if response_ and response_.headers and response_.headers['cache-control'] then
|
||||
local vals = {};
|
||||
for k, v in response_.headers['cache-control']:gmatch('(%w+)=(%w+)') do
|
||||
vals[k] = v;
|
||||
end
|
||||
-- max-age=123 will be parsed by the regex ^ to age=123
|
||||
cache_for = vals.age;
|
||||
end
|
||||
else
|
||||
module:log("warn", "Error on GET request: Code %s, Content %s",
|
||||
code_, content_);
|
||||
end
|
||||
done();
|
||||
else
|
||||
module:log("warn", "External call reply delivered after timeout from: %s", url);
|
||||
end
|
||||
end
|
||||
|
||||
local function call_http()
|
||||
return http.request(url, {
|
||||
headers = request_headers,
|
||||
method = "GET"
|
||||
}, cb);
|
||||
end
|
||||
|
||||
local request = call_http();
|
||||
|
||||
local function cancel()
|
||||
-- TODO: This check is racey. Not likely to be a problem, but we should
|
||||
-- still stick a mutex on content / code at some point.
|
||||
if code == nil then
|
||||
timeout_occurred = true;
|
||||
module:log("warn", "Timeout %s seconds making the external call to: %s", http_timeout, url);
|
||||
-- no longer present in prosody 0.11, so check before calling
|
||||
if http.destroy_request ~= nil then
|
||||
http.destroy_request(request);
|
||||
end
|
||||
if retry == nil then
|
||||
module:log("debug", "External call failed and retry policy is not set");
|
||||
done();
|
||||
elseif retry ~= nil and retry < 1 then
|
||||
module:log("debug", "External call failed after retry")
|
||||
done();
|
||||
else
|
||||
module:log("debug", "External call failed, retry nr %s", retry)
|
||||
retry = retry - 1;
|
||||
request = call_http()
|
||||
return http_timeout;
|
||||
end
|
||||
end
|
||||
end
|
||||
timer.add_task(http_timeout, cancel);
|
||||
wait();
|
||||
|
||||
return content, code, cache_for;
|
||||
end
|
||||
|
||||
-- Checks whether there is status in the <x node
|
||||
-- @param muc_x the <x element from presence
|
||||
-- @param status checks for this status
|
||||
-- @returns true if the status is found, false otherwise or if no muc_x is provided.
|
||||
function presence_check_status(muc_x, status)
|
||||
if not muc_x then
|
||||
return false;
|
||||
end
|
||||
|
||||
for statusNode in muc_x:childtags('status') do
|
||||
if statusNode.attr.code == status then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Retrieves the focus from the room and cache it in the room object
|
||||
-- @param room The room name for which to find the occupant
|
||||
local function get_focus_occupant(room)
|
||||
return room:get_occupant_by_nick(room.jid..'/focus');
|
||||
end
|
||||
|
||||
-- Checks whether the jid is moderated, the room name is in moderated_rooms
|
||||
-- or if the subdomain is in the moderated_subdomains
|
||||
-- @return returns on of the:
|
||||
-- -> false
|
||||
-- -> true, room_name, subdomain
|
||||
-- -> true, room_name, nil (if no subdomain is used for the room)
|
||||
function is_moderated(room_jid)
|
||||
if moderated_subdomains:empty() and moderated_rooms:empty() then
|
||||
return false;
|
||||
end
|
||||
|
||||
local room_node = jid.node(room_jid);
|
||||
-- parses bare room address, for multidomain expected format is:
|
||||
-- [subdomain]roomName@conference.domain
|
||||
local target_subdomain, target_room_name = extract_subdomain(room_node);
|
||||
if target_subdomain then
|
||||
if moderated_subdomains:contains(target_subdomain) then
|
||||
return true, target_room_name, target_subdomain;
|
||||
end
|
||||
elseif moderated_rooms:contains(room_node) then
|
||||
return true, room_node, nil;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
-- check if the room tenant starts with vpaas-magic-cookie-
|
||||
-- @param room the room to check
|
||||
function is_vpaas(room)
|
||||
if not room then
|
||||
return false;
|
||||
end
|
||||
|
||||
-- stored check in room object if it exist
|
||||
if room.is_vpaas ~= nil then
|
||||
return room.is_vpaas;
|
||||
end
|
||||
|
||||
room.is_vpaas = false;
|
||||
|
||||
local node, host = jid.split(room.jid);
|
||||
if host ~= muc_domain or not node then
|
||||
return false;
|
||||
end
|
||||
local tenant, conference_name = node:match('^%[([^%]]+)%](.+)$');
|
||||
if not (tenant and conference_name) then
|
||||
return false;
|
||||
end
|
||||
|
||||
if not starts_with(tenant, 'vpaas-magic-cookie-') then
|
||||
return false;
|
||||
end
|
||||
|
||||
room.is_vpaas = true;
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Returns the initiator extension if the stanza is coming from a sip jigasi
|
||||
function is_sip_jigasi(stanza)
|
||||
if not stanza then
|
||||
return false;
|
||||
end
|
||||
|
||||
return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
|
||||
end
|
||||
|
||||
-- This requires presence stanza being passed
|
||||
function is_transcriber_jigasi(stanza)
|
||||
if not stanza then
|
||||
return false;
|
||||
end
|
||||
|
||||
local features = stanza:get_child('features');
|
||||
if not features then
|
||||
return false;
|
||||
end
|
||||
|
||||
for i = 1, #features do
|
||||
local feature = features[i];
|
||||
if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function is_transcriber(jid)
|
||||
return starts_with_one_of(jid, TRANSCRIBER_PREFIXES);
|
||||
end
|
||||
|
||||
function get_sip_jibri_email_prefix(email)
|
||||
if not email then
|
||||
return nil;
|
||||
elseif starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES) then
|
||||
return starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES);
|
||||
elseif starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES) then
|
||||
return starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES);
|
||||
else
|
||||
return nil;
|
||||
end
|
||||
end
|
||||
|
||||
function is_sip_jibri_join(stanza)
|
||||
if not stanza then
|
||||
return false;
|
||||
end
|
||||
|
||||
local features = stanza:get_child('features');
|
||||
local email = stanza:get_child_text('email');
|
||||
|
||||
if not features or not email then
|
||||
return false;
|
||||
end
|
||||
|
||||
for i = 1, #features do
|
||||
local feature = features[i];
|
||||
if feature.attr and feature.attr.var and feature.attr.var == "http://jitsi.org/protocol/jibri" then
|
||||
if get_sip_jibri_email_prefix(email) then
|
||||
module:log("debug", "Occupant with email %s is a sip jibri ", email);
|
||||
return true;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function is_jibri(occupant)
|
||||
return starts_with_one_of(type(occupant) == "string" and occupant or occupant.jid, RECORDER_PREFIXES)
|
||||
end
|
||||
|
||||
-- process a host module directly if loaded or hooks to wait for its load
|
||||
function process_host_module(name, callback)
|
||||
local function process_host(host)
|
||||
|
||||
if host == name then
|
||||
callback(module:context(host), host);
|
||||
end
|
||||
end
|
||||
|
||||
if prosody.hosts[name] == nil then
|
||||
module:log('info', 'No host/component found, will wait for it: %s', name)
|
||||
|
||||
-- when a host or component is added
|
||||
prosody.events.add_handler('host-activated', process_host, -100); -- make sure everything is loaded
|
||||
else
|
||||
process_host(name);
|
||||
end
|
||||
end
|
||||
|
||||
function table_shallow_copy(t)
|
||||
local t2 = {}
|
||||
for k, v in pairs(t) do
|
||||
t2[k] = v
|
||||
end
|
||||
return t2
|
||||
end
|
||||
|
||||
local function table_find(tab, val)
|
||||
if not tab or val == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
for i, v in ipairs(tab) do
|
||||
if v == val then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Adds second table values to the first table
|
||||
local function table_add(t1, t2)
|
||||
for _,v in ipairs(t2) do
|
||||
table.insert(t1, v);
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns as a first result the removed items and as a second the added items
|
||||
local function table_compare(old_table, new_table)
|
||||
local removed = {}
|
||||
local added = {}
|
||||
local modified = {}
|
||||
|
||||
-- Find removed items (in old but not in new)
|
||||
for id, value in pairs(old_table) do
|
||||
if new_table[id] == nil then
|
||||
table.insert(removed, id)
|
||||
elseif new_table[id] ~= value then
|
||||
table.insert(modified, id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Find added items (in new but not in old)
|
||||
for id, _ in pairs(new_table) do
|
||||
if old_table[id] == nil then
|
||||
table.insert(added, id)
|
||||
end
|
||||
end
|
||||
|
||||
return removed, added, modified
|
||||
end
|
||||
|
||||
local function table_equals(t1, t2)
|
||||
if t1 == nil then
|
||||
return t2 == nil;
|
||||
end
|
||||
if t2 == nil then
|
||||
return t1 == nil;
|
||||
end
|
||||
|
||||
local removed, added, modified = table_compare(t1, t2);
|
||||
|
||||
return next(removed) == nil and next(added) == nil and next(modified) == nil
|
||||
end
|
||||
|
||||
-- Splits a string using delimiter
|
||||
function split_string(str, delimiter)
|
||||
str = str .. delimiter;
|
||||
local result = array();
|
||||
for w in str:gmatch("(.-)" .. delimiter) do
|
||||
result:push(w);
|
||||
end
|
||||
|
||||
return result;
|
||||
end
|
||||
|
||||
-- send iq result that the iq was received and will be processed
|
||||
function respond_iq_result(origin, stanza)
|
||||
-- respond with successful receiving the iq
|
||||
origin.send(st.iq({
|
||||
type = 'result';
|
||||
from = stanza.attr.to;
|
||||
to = stanza.attr.from;
|
||||
id = stanza.attr.id
|
||||
}));
|
||||
end
|
||||
|
||||
-- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3,
|
||||
-- this code provides backwards compatibility with older versions
|
||||
local get_request_from_conn = http_server.get_request_from_conn or function (conn)
|
||||
local response = conn and conn._http_open_response;
|
||||
return response and response.request or nil;
|
||||
end;
|
||||
|
||||
-- Discover real remote IP of a session
|
||||
function get_ip(session)
|
||||
local request = get_request_from_conn(session.conn);
|
||||
return request and request.ip or session.ip;
|
||||
end
|
||||
|
||||
-- Checks whether the provided jid is in the list of admins
|
||||
-- we are not using the new permissions and roles api as we have few global modules which need to be
|
||||
-- refactored into host modules, as that api needs to be executed in host context
|
||||
local function is_admin(_jid)
|
||||
local bare_jid = jid.bare(_jid);
|
||||
|
||||
if config_global_admin_jids:contains(bare_jid) or config_admin_jids:contains(bare_jid) then
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Filter out identity information (nick name, email, etc) from a presence stanza.
|
||||
local function filter_identity_from_presence(orig_stanza)
|
||||
local stanza = st.clone(orig_stanza);
|
||||
|
||||
stanza:remove_children('nick', 'http://jabber.org/protocol/nick');
|
||||
stanza:remove_children('email');
|
||||
stanza:remove_children('stats-id');
|
||||
local identity = stanza:get_child('identity');
|
||||
if identity then
|
||||
local user = identity:get_child('user');
|
||||
local name = identity:get_child('name');
|
||||
if user then
|
||||
user:remove_children('email');
|
||||
user:remove_children('name');
|
||||
end
|
||||
if name then
|
||||
name:remove_children('name'); -- Remove name with no namespace
|
||||
end
|
||||
end
|
||||
|
||||
return stanza;
|
||||
end
|
||||
|
||||
return {
|
||||
OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES;
|
||||
INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES;
|
||||
RECORDER_PREFIXES = RECORDER_PREFIXES;
|
||||
extract_subdomain = extract_subdomain;
|
||||
filter_identity_from_presence = filter_identity_from_presence;
|
||||
is_admin = is_admin;
|
||||
is_feature_allowed = is_feature_allowed;
|
||||
is_jibri = is_jibri;
|
||||
is_healthcheck_room = is_healthcheck_room;
|
||||
is_moderated = is_moderated;
|
||||
is_sip_jibri_join = is_sip_jibri_join;
|
||||
is_sip_jigasi = is_sip_jigasi;
|
||||
is_transcriber = is_transcriber;
|
||||
is_transcriber_jigasi = is_transcriber_jigasi;
|
||||
is_vpaas = is_vpaas;
|
||||
get_focus_occupant = get_focus_occupant;
|
||||
get_ip = get_ip;
|
||||
get_room_from_jid = get_room_from_jid;
|
||||
get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
|
||||
get_sip_jibri_email_prefix = get_sip_jibri_email_prefix;
|
||||
async_handler_wrapper = async_handler_wrapper;
|
||||
presence_check_status = presence_check_status;
|
||||
process_host_module = process_host_module;
|
||||
respond_iq_result = respond_iq_result;
|
||||
room_jid_match_rewrite = room_jid_match_rewrite;
|
||||
room_jid_split_subdomain = room_jid_split_subdomain;
|
||||
internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
|
||||
update_presence_identity = update_presence_identity;
|
||||
http_get_with_retry = http_get_with_retry;
|
||||
ends_with = ends_with;
|
||||
split_string = split_string;
|
||||
starts_with = starts_with;
|
||||
starts_with_one_of = starts_with_one_of;
|
||||
table_add = table_add;
|
||||
table_compare = table_compare;
|
||||
table_shallow_copy = table_shallow_copy;
|
||||
table_find = table_find;
|
||||
table_equals = table_equals;
|
||||
};
|
||||
88
resources/register-jaas-account.sh
Executable file
88
resources/register-jaas-account.sh
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
EMAIL=$1
|
||||
DOMAIN=$2
|
||||
|
||||
if [ -z "${DOMAIN}" ] || [ -z "${EMAIL}" ]; then
|
||||
echo "You need to provide email and domain as parameters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
JITSI_INSTALLATION="DEBIAN"
|
||||
JAAS_ENDPOINT="https://account-provisioning.cloudflare.jitsi.net/operations"
|
||||
CHALLENGE_FILE="/usr/share/jitsi-meet/.well-known/jitsi-challenge.txt"
|
||||
SUPPORT_MSG="Reach out to JaaS support or retry with /usr/share/jitsi-meet/scripts/register-jaas-account.sh"
|
||||
|
||||
create_error=0
|
||||
create_data=$(curl -s -f -X 'POST' "${JAAS_ENDPOINT}" -H 'Content-Type: application/json' -H 'accept: */*' \
|
||||
-d "{ \"domain\": \"${DOMAIN}\", \"email\": \"${EMAIL}\", \"jitsiInstallation\": \"${JITSI_INSTALLATION}\" }") || create_error=$?
|
||||
if [ ${create_error} -ne 0 ]; then
|
||||
echo "Account creation failed. Status: ${create_error}, response: ${create_data}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# make sure .well-known exists
|
||||
mkdir -p "$(dirname "$CHALLENGE_FILE")"
|
||||
# Creating the challenge file
|
||||
echo "${create_data}" | jq -r .challenge > ${CHALLENGE_FILE}
|
||||
|
||||
op_id=$(echo "${create_data}" | jq -r .operationId)
|
||||
ready_error=0
|
||||
ready_data=$(curl -s -f -X 'PUT' "${JAAS_ENDPOINT}/${op_id}/ready") || ready_error=$?
|
||||
if [ ${ready_error} -ne 0 ]; then
|
||||
echo "Validating domain failed. Status: ${ready_error}"
|
||||
echo "Response: "
|
||||
echo "${ready_data}" | jq -r
|
||||
echo "${SUPPORT_MSG}"
|
||||
echo
|
||||
exit 3
|
||||
fi
|
||||
|
||||
SLEEP_TIME=0
|
||||
WAIT_BEFORE_CHECK=10
|
||||
TIMEOUT=60
|
||||
echo -n "Creating..."
|
||||
(while true; do
|
||||
provisioned_data=$(curl -s -f "${JAAS_ENDPOINT}/${op_id}")
|
||||
|
||||
status=$(echo "${provisioned_data}" | jq -r .status)
|
||||
|
||||
if [ "${status}" == "PROVISIONED" ]; then
|
||||
echo ""
|
||||
echo "=================="
|
||||
echo ""
|
||||
echo "JaaS account was created. To finish setup follow the email that was sent."
|
||||
echo ""
|
||||
echo "=================="
|
||||
exit 0;
|
||||
elif [ "${status}" == "FAILED" ]; then
|
||||
echo ""
|
||||
echo "=================="
|
||||
echo ""
|
||||
echo "JaaS account creation failed:${provisioned_data}"
|
||||
echo ""
|
||||
echo "=================="
|
||||
exit 4
|
||||
elif [ "${status}" == "VERIFIED" ] && [ "${verified}" != "true" ]; then
|
||||
echo -n "Account was successfully verified..."
|
||||
verified="true"
|
||||
fi
|
||||
|
||||
if [ ${SLEEP_TIME} -ge ${TIMEOUT} ]; then
|
||||
echo ""
|
||||
echo "=================="
|
||||
echo ""
|
||||
echo "Timeout creating account. ${SUPPORT_MSG}"
|
||||
echo ""
|
||||
echo "=================="
|
||||
exit 5
|
||||
fi
|
||||
|
||||
echo -n "waiting..."
|
||||
sleep ${WAIT_BEFORE_CHECK}
|
||||
SLEEP_TIME=$((SLEEP_TIME+WAIT_BEFORE_CHECK))
|
||||
done)
|
||||
|
||||
rm ${CHALLENGE_FILE} || true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user