Compare commits

...

3 commits

Author SHA1 Message Date
87cc109bf1
[__jitsi_meet*] Make rooms on different domains not equivalent
This is a backwards-compatible change.

We switch the approach from "treat all domains as if they were the main domain"
to: "each domain has its own prosody settings".

This works perfectly fine, even with secured domains.

There is a caveat with secured domains, in that they use the main domain to log
in; this means that users are shared across all domains (as they were before
this commit).

This is due to jicofo refusing to start meetings from a domain that is not
configured, and it only accepting one domain.

Right now, this is acceptable, however we could want to authenticate against
e.g. different LDAP / IMAP servers in the future, so this would need addressing
at that stage.

Probably the best way to solve it is by patching jicofo, so it accepts starting
conferences from multiple domains and getting that patch upstream.

Sponsored by:   camilion.eu, eXO.cat
2022-04-21 13:20:30 +02:00
a12b343660
[__jitsi_meet_domain] Add analytics settings parameter
with this, admins can take advantage of e.g. matomo to have some usage
statistics.

The parameter defaults to `disabled: true`, which is the most privacy-friendly!

Sponsored by:   camilion.eu
2022-04-21 13:13:12 +02:00
29cafd4f9a
[__jitsi_meet_domain] Simplify logic for secured domains 2022-04-16 13:22:16 +02:00
12 changed files with 413 additions and 37 deletions

View file

@ -0,0 +1 @@
../../__jitsi_meet_domain/files/prosody.cfg.lua.sh

View file

@ -4,8 +4,7 @@ if grep -qE "^__file/etc/nginx" "${__messages_in}"; then
echo "service nginx reload"
fi
JITSI_HOST="${__object_id}"
if grep -qE "^(__line/jitsi_jicofo_secured_domains|__file/etc/prosody/conf.d/${JITSI_HOST}.zauth.cfg.lua|__file/etc/jitsi/jicofo/jicofo.conf)" "${__messages_in}"; then
if grep -qE "^(__line/jitsi_jicofo_secured_domains|(__file|__link)/etc/prosody/conf.d/|__file/etc/jitsi/jicofo/jicofo.conf)" "${__messages_in}"; then
echo "systemctl restart prosody"
echo "systemctl restart jicofo"
echo "systemctl restart jitsi-videobridge2"

View file

@ -161,18 +161,22 @@ else
SECURED_DOMAINS_STATE='absent'
fi
__file "/etc/prosody/conf.d/${JITSI_HOST}.zauth.cfg.lua" \
--owner prosody --group prosody --mode 0440 \
--state ${SECURED_DOMAINS_STATE} \
--source - <<EOF
VirtualHost "${JITSI_HOST}"
authentication = "internal_plain"
VirtualHost "guest.${JITSI_HOST}"
authentication = "anonymous"
c2s_require_encryption = false
# This is the main host config
PROSODY_MAIN_CONFIG="YES"
# Prosody settings for common components (jvb, focus, ...)
# shellcheck source=type/__jitsi_meet/files/prosody.cfg.lua.sh
. "${__type}/files/prosody.cfg.lua.sh" # This defines PROSODY_CONFIG
__file "/etc/prosody/conf.d/00_jitsi_base.cfg.lua" \
--group prosody \
--mode 0440 \
--source - <<EOF
${PROSODY_CONFIG}
EOF
# Clean up zauth.cfg.lua file, which we don't use now
__file "/etc/prosody/conf.d/${JITSI_HOST}.zauth.cfg.lua" \
--state absent
export SECURED_DOMAINS_STATE
export JITSI_HOST
"${__type}/files/jicofo.conf.sh" | \

View file

@ -28,3 +28,4 @@ download_file() {
download_file config.js
download_file interface_config.js
download_file doc/debian/jitsi-meet/jitsi-meet.example nginx.sh.orig
download_file doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example prosody.cfg.lua.sh.orig

View file

@ -10,26 +10,21 @@ var config = {
hosts: {
// XMPP domain.
domain: '${JITSI_HOST}',
domain: '${DOMAIN}',
// When using authentication, domain for guest users.
$( if [ -n "${SECURED_DOMAINS}" ]; then cat<<EOF2
anonymousdomain: 'guest.${JITSI_HOST}',
EOF2
else cat <<EOF2
// anonymousdomain: 'guest.example.com',
EOF2
fi
)
$( if [ -z "${SECURED_DOMAINS}" ]; then printf "// "
fi)anonymousdomain: 'guest.${DOMAIN}',
// Domain for authenticated users. Defaults to <domain>.
// authdomain: '${JITSI_HOST}',
// NOTE [cdist]: if we use '${DOMAIN}', jicofo won't start the meeting
authdomain: '${JITSI_HOST}',
// Focus component domain. Defaults to focus.<domain>.
// focus: 'focus.${JITSI_HOST}',
focus: 'focus.${JITSI_HOST}',
// XMPP MUC domain. FIXME: use XEP-0030 to discover it.
muc: 'conference.${JITSI_HOST}'
muc: 'conference.${DOMAIN}'
},
// BOSH URL. FIXME: use XEP-0156 to discover it.
@ -37,12 +32,12 @@ fi
bosh: '//<!--# echo var="http_host" -->/<!--# echo var="subdir" default="" -->http-bind',
// Websocket URL
// websocket: 'wss://${JITSI_HOST}/xmpp-websocket',
// websocket: 'wss://${DOMAIN}/xmpp-websocket',
// The real JID of focus participant - can be overridden here
// Do not change username - FIXME: Make focus username configurable
// https://github.com/jitsi/jitsi-meet/issues/7376
// focusUserJid: 'focus@auth.${JITSI_HOST}',
focusUserJid: 'focus@auth.${JITSI_HOST}',
// Testing / experimental features.
@ -276,9 +271,9 @@ $(if [ -n "${VIDEO_CONSTRAINTS}" ]; then echo "${VIDEO_CONSTRAINTS},"; fi)
// appKey: '<APP_KEY>' // Specify your app key here.
// // A URL to redirect the user to, after authenticating
// // by default uses:
// // 'https://${JITSI_HOST}/static/oauth.html'
// // 'https://${DOMAIN}/static/oauth.html'
// redirectURI:
// 'https://${JITSI_HOST}/subfolder/static/oauth.html'
// 'https://${DOMAIN}/subfolder/static/oauth.html'
// },
// When integrations like dropbox are enabled only that will be shown,
// by enabling fileRecordingsServiceEnabled, we show both the integrations
@ -823,6 +818,7 @@ $(if [ -n "${VIDEO_CONSTRAINTS}" ]; then echo "${VIDEO_CONSTRAINTS},"; fi)
},
analytics: {
${ANALYTICS_SETTINGS}
// True if the analytics should be disabled
// disabled: false,

View file

@ -100,7 +100,7 @@ server {
proxy_set_header X-Forwarded-For \$remote_addr;
# Prevision for 'multi-domain' jitsi instances
# https://community.jitsi.org/t/same-jitsi-meet-instance-with-multiple-domain-names/17391
proxy_set_header Host ${JITSI_HOST};
proxy_set_header Host ${DOMAIN};
}
# xmpp websockets
@ -111,7 +111,7 @@ server {
proxy_set_header Connection "upgrade";
# Prevision for 'multi-domain' jitsi instances
# https://community.jitsi.org/t/same-jitsi-meet-instance-with-multiple-domain-names/17391
proxy_set_header Host ${JITSI_HOST};
proxy_set_header Host ${DOMAIN};
tcp_nodelay on;
}

View file

@ -0,0 +1,199 @@
#!/bin/sh -eu
# Source:
# https://github.com/jitsi/jitsi-meet/blob/master/doc/debian/jitsi-meet-prosody/prosody.cfg.lua-jvb.example
FOCUS_USER="focus"
JITSI_DOMAIN="${JITSI_DOMAIN:-${JITSI_HOST:?}}"
# PROSODY_MAIN_CONFIG: defined in __jitsi_meet, empty in __jitsi_meet_domain
PROSODY_SECUREDOMAIN_START="--[["
PROSODY_SECUREDOMAIN_END="--]]"
if [ -n "${PROSODY_MAIN_CONFIG}" ]; then
PROSODY_MAIN_START=""
PROSODY_MAIN_END=""
PROSODY_DOMAIN_START="--[["
PROSODY_DOMAIN_END="--]]"
else
PROSODY_MAIN_START="--[["
PROSODY_MAIN_END="--]]"
PROSODY_DOMAIN_START=""
PROSODY_DOMAIN_END=""
if [ -n "${SECURED_DOMAINS}" ]; then
PROSODY_SECUREDOMAIN_START=""
PROSODY_SECUREDOMAIN_END=""
fi
fi
# Websockets haven't been fully tested in this type and don't work reliably
PROSODY_WEBSOCKET="-- "
# shellcheck disable=SC2034 # This is intended to be included
PROSODY_CONFIG="$(cat <<EOFPROSODY
-- Managed remotely, changes will be lost
${PROSODY_MAIN_START}
-- This will be managed by __jitsi_meet
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
-- domain mapper options, must at least have domain base set to use the mapper
muc_mapper_domain_base = "${JITSI_HOST:?}";
external_service_secret = "${TURN_SECRET:-TurnSecret}";
external_services = {
{ type = "stun", host = "${JITSI_HOST:?}", port = 3478 },
{ type = "turn", host = "${JITSI_HOST:?}", port = 3478, transport = "udp", secret = true, ttl = 86400, algorithm = "turn" },
{ type = "turns", host = "${JITSI_HOST:?}", port = 5349, transport = "tcp", secret = true, ttl = 86400, algorithm = "turn" }
};
cross_domain_bosh = false;
consider_bosh_secure = true;
-- Use websockets
-- https://community.jitsi.org/t/how-to-how-to-enable-websockets-xmpp-websocket-and-smacks-for-prosody/87920
${PROSODY_WEBSOCKET}consider_websocket_secure = true;
-- https_ports = { }; -- Remove this line to prevent listening on port 5284
-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
ssl = {
protocol = "tlsv1_2+";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
}
unlimited_jids = {
"${FOCUS_USER:?}@auth.${JITSI_HOST:?}",
"jvb@auth.${JITSI_HOST:?}"
}
${PROSODY_MAIN_END}
${PROSODY_DOMAIN_START}
-- This will be managed by __jitsi_meet_domain
VirtualHost "${JITSI_DOMAIN:?}"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/${JITSI_DOMAIN:?}.key";
certificate = "/etc/prosody/certs/${JITSI_DOMAIN:?}.crt";
}
av_moderation_component = "avmoderation.${JITSI_DOMAIN:?}"
speakerstats_component = "speakerstats.${JITSI_DOMAIN:?}"
conference_duration_component = "conferenceduration.${JITSI_DOMAIN:?}"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"external_services";
"conference_duration";
"muc_lobby_rooms";
"muc_breakout_rooms";
"av_moderation";
${PROSODY_WEBSOCKET} "websocket";
${PROSODY_WEBSOCKET} "smacks";
}
smacks_max_unacked_stanzas = 5;
smacks_hibernation_time = 60;
smacks_max_hibernated_sessions = 1;
smacks_max_old_sessions = 1;
c2s_require_encryption = false
lobby_muc = "lobby.${JITSI_DOMAIN:?}"
breakout_rooms_muc = "breakout.${JITSI_DOMAIN:?}"
main_muc = "conference.${JITSI_DOMAIN:?}"
-- muc_lobby_whitelist = { "recorder.${JITSI_DOMAIN:?}" } -- Here we can whitelist jibri to enter lobby enabled rooms
Component "conference.${JITSI_DOMAIN:?}" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
"polls";
--"token_verification";
"muc_rate_limit";
}
admins = { "${FOCUS_USER:?}@auth.${JITSI_HOST:?}" }
muc_room_locking = false
muc_room_default_public_jids = true
Component "breakout.${JITSI_DOMAIN:?}" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
--"token_verification";
"muc_rate_limit";
"polls";
}
admins = { "${FOCUS_USER:?}@auth.${JITSI_HOST:?}" }
muc_room_locking = false
muc_room_default_public_jids = true
-- internal muc component
Component "internal.auth.${JITSI_DOMAIN:?}" "muc"
storage = "memory"
modules_enabled = {
"ping";
}
admins = { "${FOCUS_USER:?}@auth.${JITSI_HOST:?}", "jvb@auth.${JITSI_HOST:?}" }
muc_room_locking = false
muc_room_default_public_jids = true
${PROSODY_DOMAIN_END}
${PROSODY_MAIN_START}
-- This will be managed by __jitsi_meet
VirtualHost "auth.${JITSI_DOMAIN:?}"
ssl = {
key = "/etc/prosody/certs/auth.${JITSI_DOMAIN:?}.key";
certificate = "/etc/prosody/certs/auth.${JITSI_DOMAIN:?}.crt";
}
modules_enabled = {
"limits_exception";
}
authentication = "internal_hashed"
${PROSODY_MAIN_END}
${PROSODY_DOMAIN_START}
-- This will be managed by __jitsi_meet_domain
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
Component "focus.${JITSI_DOMAIN:?}" "client_proxy"
-- Single focus user for the whole instance
target_address = "${FOCUS_USER:?}@auth.${JITSI_HOST:?}"
Component "speakerstats.${JITSI_DOMAIN:?}" "speakerstats_component"
muc_component = "conference.${JITSI_DOMAIN:?}"
Component "conferenceduration.${JITSI_DOMAIN:?}" "conference_duration_component"
muc_component = "conference.${JITSI_DOMAIN:?}"
Component "avmoderation.${JITSI_DOMAIN:?}" "av_moderation_component"
muc_component = "conference.${JITSI_DOMAIN:?}"
Component "lobby.${JITSI_DOMAIN:?}" "muc"
storage = "memory"
restrict_room_creation = true
muc_room_locking = false
muc_room_default_public_jids = true
modules_enabled = {
"muc_rate_limit";
"polls";
}
${PROSODY_DOMAIN_END}
${PROSODY_SECUREDOMAIN_START}
-- Only used on secured domains
VirtualHost "${JITSI_DOMAIN}"
authentication = "internal_plain"
VirtualHost "guest.${JITSI_DOMAIN}"
authentication = "anonymous"
c2s_require_encryption = false
${PROSODY_SECUREDOMAIN_END}
EOFPROSODY
)"

View file

@ -0,0 +1,129 @@
plugin_paths = { "/usr/share/jitsi-meet/prosody-plugins/" }
-- domain mapper options, must at least have domain base set to use the mapper
muc_mapper_domain_base = "jitmeet.example.com";
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" }
};
cross_domain_bosh = false;
consider_bosh_secure = true;
-- https_ports = { }; -- Remove this line to prevent listening on port 5284
-- https://ssl-config.mozilla.org/#server=haproxy&version=2.1&config=intermediate&openssl=1.1.0g&guideline=5.4
ssl = {
protocol = "tlsv1_2+";
ciphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"
}
unlimited_jids = {
"focusUser@auth.jitmeet.example.com",
"jvb@auth.jitmeet.example.com"
}
VirtualHost "jitmeet.example.com"
-- enabled = false -- Remove this line to enable this host
authentication = "anonymous"
-- Properties below are modified by jitsi-meet-tokens package config
-- and authentication above is switched to "token"
--app_id="example_app_id"
--app_secret="example_app_secret"
-- Assign this host a certificate for TLS, otherwise it would use the one
-- set in the global section (if any).
-- Note that old-style SSL on port 5223 only supports one certificate, and will always
-- use the global one.
ssl = {
key = "/etc/prosody/certs/jitmeet.example.com.key";
certificate = "/etc/prosody/certs/jitmeet.example.com.crt";
}
av_moderation_component = "avmoderation.jitmeet.example.com"
speakerstats_component = "speakerstats.jitmeet.example.com"
conference_duration_component = "conferenceduration.jitmeet.example.com"
-- we need bosh
modules_enabled = {
"bosh";
"pubsub";
"ping"; -- Enable mod_ping
"speakerstats";
"external_services";
"conference_duration";
"muc_lobby_rooms";
"muc_breakout_rooms";
"av_moderation";
}
c2s_require_encryption = false
lobby_muc = "lobby.jitmeet.example.com"
breakout_rooms_muc = "breakout.jitmeet.example.com"
main_muc = "conference.jitmeet.example.com"
-- muc_lobby_whitelist = { "recorder.jitmeet.example.com" } -- Here we can whitelist jibri to enter lobby enabled rooms
Component "conference.jitmeet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
"polls";
--"token_verification";
"muc_rate_limit";
}
admins = { "focusUser@auth.jitmeet.example.com" }
muc_room_locking = false
muc_room_default_public_jids = true
Component "breakout.jitmeet.example.com" "muc"
restrict_room_creation = true
storage = "memory"
modules_enabled = {
"muc_meeting_id";
"muc_domain_mapper";
--"token_verification";
"muc_rate_limit";
"polls";
}
admins = { "focusUser@auth.jitmeet.example.com" }
muc_room_locking = false
muc_room_default_public_jids = true
-- internal muc component
Component "internal.auth.jitmeet.example.com" "muc"
storage = "memory"
modules_enabled = {
"ping";
}
admins = { "focusUser@auth.jitmeet.example.com", "jvb@auth.jitmeet.example.com" }
muc_room_locking = false
muc_room_default_public_jids = true
VirtualHost "auth.jitmeet.example.com"
modules_enabled = {
"limits_exception";
}
authentication = "internal_hashed"
-- Proxy to jicofo's user JID, so that it doesn't have to register as a component.
Component "focus.jitmeet.example.com" "client_proxy"
target_address = "focusUser@auth.jitmeet.example.com"
Component "speakerstats.jitmeet.example.com" "speakerstats_component"
muc_component = "conference.jitmeet.example.com"
Component "conferenceduration.jitmeet.example.com" "conference_duration_component"
muc_component = "conference.jitmeet.example.com"
Component "avmoderation.jitmeet.example.com" "av_moderation_component"
muc_component = "conference.jitmeet.example.com"
Component "lobby.jitmeet.example.com" "muc"
storage = "memory"
restrict_room_creation = true
muc_room_locking = false
muc_room_default_public_jids = true
modules_enabled = {
"muc_rate_limit";
"polls";
}

View file

@ -11,14 +11,18 @@ DESCRIPTION
-----------
This type installs and configures the frontend for Jitsi-Meet.
This supports "multi-domain" installations, notice that in such a setup, all
rooms are shared across the different URLs, e.g.
https://jitsi1.example.org/room1 and https://jitsi2.example.org/room1 are
equivalent.
This supports "multi-domain" installations.
New in April 2022: rooms are independent for each domain, that is:
https://jitsi1.example.org/room1 and https://jitsi2.example.org/room1 are
different rooms.
Note however, that right now if using secured domains, users are still shared
across any domains hosted in the same instance.
One way to work around that could be to run multiple jicofos, but we do not
want to bloat the servers.
A better way is to patch jicofo, get in touch with the type authors if you want
the gory details.
This is due to the underlying XMPP and signaling rooms being common.
There might be a way to perform tricks on the Nginx-side to avoid this, but
time is lacking :-).
This assumes `__jitsi_meet` has already been ran on the target host, and,
amongst others, that Jitsi was set up with `__target_host` as the Jitsi domain.
@ -41,6 +45,11 @@ admin-email
OPTIONAL PARAMETERS
-------------------
analytics-settings
This goes inside the `analytics` part of `config.js`.
Defaults to: `disabled: true`.
See: https://github.com/jitsi/jitsi-meet/blob/master/config.js
channel-last-n
Default value for the "last N" attribute.
Defaults to 20. Set to -1 for unlimited.

View file

@ -18,6 +18,7 @@ NOTICE_MESSAGE="$(cat "${__object}/parameter/notice-message")"
START_VIDEO_MUTED="$(cat "${__object}/parameter/start-video-muted")"
TURN_SERVER="$(cat "${__object}/parameter/turn-server")"
VIDEO_CONSTRAINTS="$(cat "${__object}/parameter/video-constraints")"
ANALYTICS_SETTINGS="$(cat "${__object}/parameter/analytics-settings")"
BRANDING_INDEX="$(cat "${__object}/parameter/branding-index")"
BRANDING_JSON="$(cat "${__object}/parameter/branding-json")"
BRANDING_WATERMARK="$(cat "${__object}/parameter/branding-watermark")"
@ -130,3 +131,38 @@ __file "/usr/share/jitsi-meet/images/watermark-${DOMAIN}.png" \
--mode 0644 \
--state "$(_var_state "${BRANDING_WATERMARK}")" \
--source "${BRANDING_WATERMARK}"
#
# Take care of prosody settings for the domain
#
JITSI_DOMAIN="${DOMAIN}"
# Prosody settings for common components (jvb, focus, ...)
# shellcheck source=type/__jitsi_meet_domain/files/prosody.cfg.lua.sh
. "${__type}/files/prosody.cfg.lua.sh" # This defines PROSODY_CONFIG
__file "/etc/prosody/conf.avail/${DOMAIN}.cfg.lua" \
--group prosody \
--mode 0440 \
--state "${STATE}" \
--source '-' <<EOF
${PROSODY_CONFIG}
EOF
__link "/etc/prosody/conf.d/${DOMAIN}.cfg.lua" \
--source "/etc/prosody/conf.avail/${DOMAIN}.cfg.lua" \
--state "${STATE}" \
--type symbolic
if [ "${STATE}" = "present" ]; then
export require="${require} __file/etc/prosody/conf.avail/${DOMAIN}.cfg.lua __link/etc/prosody/conf.d/${DOMAIN}.cfg.lua"
__check_messages "prosody/${DOMAIN}" \
--pattern '^(__file|__link)/etc/prosody/conf[.](avail|d)/' \
--execute "$(cat <<EOF
if [ ! -f "/var/lib/prosody/${DOMAIN}.crt" ]; then
echo | prosodyctl cert generate '${DOMAIN}';
ln -sf '/var/lib/prosody/${DOMAIN}.key' '/etc/prosody/certs/${DOMAIN}.key'
ln -sf '/var/lib/prosody/${DOMAIN}.crt' '/etc/prosody/certs/${DOMAIN}.crt'
fi
# Surprisingly, a reload is not enough
service prosody restart
EOF
)"
fi

View file

@ -0,0 +1 @@
disabled: true

View file

@ -1,3 +1,4 @@
analytics-settings
channel-last-n
default-language
notice-message