Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

mod_visitors_component.lua 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. module:log('info', 'Starting visitors_component at %s', module.host);
  2. local jid = require 'util.jid';
  3. local st = require 'util.stanza';
  4. local util = module:require 'util';
  5. local is_healthcheck_room = util.is_healthcheck_room;
  6. local room_jid_match_rewrite = util.room_jid_match_rewrite;
  7. local get_room_from_jid = util.get_room_from_jid;
  8. local get_focus_occupant = util.get_focus_occupant;
  9. local get_room_by_name_and_subdomain = util.get_room_by_name_and_subdomain;
  10. local internal_room_jid_match_rewrite = util.internal_room_jid_match_rewrite;
  11. local is_vpaas = util.is_vpaas;
  12. local is_sip_jibri_join = util.is_sip_jibri_join;
  13. local process_host_module = util.process_host_module;
  14. local new_id = require 'util.id'.medium;
  15. local um_is_admin = require 'core.usermanager'.is_admin;
  16. local json = require 'cjson.safe';
  17. local inspect = require 'inspect';
  18. local MUC_NS = 'http://jabber.org/protocol/muc';
  19. local muc_domain_prefix = module:get_option_string('muc_mapper_domain_prefix', 'conference');
  20. local muc_domain_base = module:get_option_string('muc_mapper_domain_base');
  21. if not muc_domain_base then
  22. module:log('warn', 'No muc_domain_base option set.');
  23. return;
  24. end
  25. -- A list of domains which to be ignored for visitors. The config is set under the main virtual host
  26. local ignore_list = module:context(muc_domain_base):get_option_set('visitors_ignore_list', {});
  27. local auto_allow_promotion = module:get_option_boolean('auto_allow_visitor_promotion', false);
  28. -- whether to always advertise that visitors feature is enabled for rooms
  29. -- can be set to off and being controlled by another module, turning it on and off for rooms
  30. local always_visitors_enabled = module:get_option_boolean('always_visitors_enabled', true);
  31. local function is_admin(jid)
  32. return um_is_admin(jid, module.host);
  33. end
  34. -- This is a map to keep data for room and the jids that were allowed to join after visitor mode is enabled
  35. -- automatically allowed or allowed by a moderator
  36. local visitors_promotion_map = {};
  37. -- A map with key room jid. The content is a map with key jid from which the request is received
  38. -- and the value is a table that has the json message that needs to be sent to any future moderator that joins
  39. -- and the vnode from which the request is received and where the response will be sent
  40. local visitors_promotion_requests = {};
  41. local cache = require 'util.cache';
  42. local sent_iq_cache = cache.new(200);
  43. -- send iq result that the iq was received and will be processed
  44. local function respond_iq_result(origin, stanza)
  45. -- respond with successful receiving the iq
  46. origin.send(st.iq({
  47. type = 'result';
  48. from = stanza.attr.to;
  49. to = stanza.attr.from;
  50. id = stanza.attr.id
  51. }));
  52. end
  53. -- Sends a json-message to the destination jid
  54. -- @param to_jid the destination jid
  55. -- @param json_message the message content to send
  56. function send_json_message(to_jid, json_message)
  57. local stanza = st.message({ from = module.host; to = to_jid; })
  58. :tag('json-message', { xmlns = 'http://jitsi.org/jitmeet' }):text(json_message):up();
  59. module:send(stanza);
  60. end
  61. local function request_promotion_received(room, from_jid, from_vnode, nick, time, user_id, force_promote)
  62. -- if visitors is enabled for the room
  63. if visitors_promotion_map[room.jid] then
  64. -- only for raise hand, ignore lowering the hand
  65. if time and time > 0 and (
  66. auto_allow_promotion
  67. or (user_id and user_id == room._data.moderator_id)
  68. or force_promote == 'true') then
  69. -- we are in auto-allow mode, let's reply with accept
  70. -- we store where the request is coming from so we can send back the response
  71. local username = new_id():lower();
  72. visitors_promotion_map[room.jid][username] = {
  73. from = from_vnode;
  74. jid = from_jid;
  75. };
  76. local req_from = visitors_promotion_map[room.jid][username].from;
  77. local req_jid = visitors_promotion_map[room.jid][username].jid;
  78. local focus_occupant = get_focus_occupant(room);
  79. local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
  80. local iq_id = new_id();
  81. sent_iq_cache:set(iq_id, socket.gettime());
  82. local node = jid.node(room.jid);
  83. module:send(st.iq({
  84. type='set', to = req_from, from = module.host, id = iq_id })
  85. :tag('visitors', {
  86. xmlns='jitsi:visitors',
  87. room = jid.join(node, muc_domain_prefix..'.'..req_from),
  88. focusjid = focus_jid })
  89. :tag('promotion-response', {
  90. xmlns='jitsi:visitors',
  91. jid = req_jid,
  92. username = username ,
  93. allow = 'true' }):up());
  94. return true;
  95. else
  96. -- send promotion request to all moderators
  97. local body_json = {};
  98. body_json.type = 'visitors';
  99. body_json.room = internal_room_jid_match_rewrite(room.jid);
  100. body_json.action = 'promotion-request';
  101. body_json.nick = nick;
  102. body_json.from = from_jid;
  103. if time and time > 0 then
  104. -- raise hand
  105. body_json.on = true;
  106. else
  107. -- lower hand, we want to inform interested parties that
  108. -- the visitor is no longer interested in joining the main call
  109. body_json.on = false;
  110. end
  111. local msg_to_send, error = json.encode(body_json);
  112. if not msg_to_send then
  113. module:log('error', 'Error encoding msg room:%s error:%s', room.jid, error)
  114. return true;
  115. end
  116. if visitors_promotion_requests[room.jid] then
  117. visitors_promotion_requests[room.jid][from_jid] = {
  118. msg = msg_to_send;
  119. from = from_vnode;
  120. };
  121. else
  122. module:log('warn', 'Received promotion request for room %s with visitors not enabled. %s',
  123. room.jid, msg_to_send);
  124. end
  125. -- let's send a notification to every moderator
  126. for _, occupant in room:each_occupant() do
  127. if occupant.role == 'moderator' and not is_admin(occupant.bare_jid) then
  128. send_json_message(occupant.jid, msg_to_send);
  129. end
  130. end
  131. return true;
  132. end
  133. end
  134. module:log('warn', 'Received promotion request from %s for room %s without active visitors', from, room.jid);
  135. end
  136. local function connect_vnode_received(room, vnode)
  137. module:context(muc_domain_base):fire_event('jitsi-connect-vnode', { room = room; vnode = vnode; });
  138. if not visitors_promotion_map[room.jid] then
  139. -- visitors is enabled
  140. visitors_promotion_map[room.jid] = {};
  141. visitors_promotion_requests[room.jid] = {};
  142. room._connected_vnodes = cache.new(16); -- we up to 16 vnodes for this prosody
  143. end
  144. room._connected_vnodes:set(vnode..'.meet.jitsi', 'connected');
  145. end
  146. local function disconnect_vnode_received(room, vnode)
  147. module:context(muc_domain_base):fire_event('jitsi-disconnect-vnode', { room = room; vnode = vnode; });
  148. room._connected_vnodes:set(vnode..'.meet.jitsi', nil);
  149. if room._connected_vnodes:count() == 0 then
  150. visitors_promotion_map[room.jid] = nil;
  151. visitors_promotion_requests[room.jid] = nil;
  152. room._connected_vnodes = nil;
  153. end
  154. end
  155. -- listens for iq request for promotion and forward it to moderators in the meeting for approval
  156. -- or auto-allow it if such the config is set enabling it
  157. local function stanza_handler(event)
  158. local origin, stanza = event.origin, event.stanza;
  159. if stanza.name ~= 'iq' then
  160. return;
  161. end
  162. if stanza.attr.type == 'result' and sent_iq_cache:get(stanza.attr.id) then
  163. sent_iq_cache:set(stanza.attr.id, nil);
  164. return true;
  165. end
  166. if stanza.attr.type ~= 'set' and stanza.attr.type ~= 'get' then
  167. return; -- We do not want to reply to these, so leave.
  168. end
  169. local visitors_iq = event.stanza:get_child('visitors', 'jitsi:visitors');
  170. if not visitors_iq then
  171. return;
  172. end
  173. -- set stanzas are coming from s2s connection
  174. if stanza.attr.type == 'set' and origin.type ~= 's2sin' then
  175. module:log('warn', 'not from s2s session, ignore! %s', stanza);
  176. return true;
  177. end
  178. local room_jid = visitors_iq.attr.room;
  179. local room = get_room_from_jid(room_jid_match_rewrite(room_jid));
  180. if not room then
  181. -- this maybe as we receive the iq from jicofo after the room is already destroyed
  182. module:log('debug', 'No room found %s', room_jid);
  183. return;
  184. end
  185. local processed;
  186. -- promotion request is coming from visitors and is a set and is over the s2s connection
  187. local request_promotion = visitors_iq:get_child('promotion-request');
  188. if request_promotion then
  189. if not (room._connected_vnodes and room._connected_vnodes:get(stanza.attr.from)) then
  190. module:log('warn', 'Received forged promotion-request: %s %s %s', stanza, inspect(room._connected_vnodes), room._connected_vnodes:get(stanza.attr.from));
  191. return true; -- stop processing
  192. end
  193. local force_promote = request_promotion.attr.forcePromote;
  194. if force_promote == 'true' and not is_vpaas(room) then
  195. -- allow force promote only in case there are no moderators in the room
  196. for _, occupant in room:each_occupant() do
  197. if occupant.role == 'moderator' and not is_admin(occupant.bare_jid) then
  198. force_promote = false;
  199. break;
  200. end
  201. end
  202. end
  203. local display_name = visitors_iq:get_child_text('nick', 'http://jabber.org/protocol/nick');
  204. processed = request_promotion_received(
  205. room,
  206. request_promotion.attr.jid,
  207. stanza.attr.from,
  208. display_name,
  209. tonumber(request_promotion.attr.time),
  210. request_promotion.attr.userId,
  211. force_promote
  212. );
  213. end
  214. -- connect and disconnect are only received from jicofo
  215. if is_admin(jid.bare(stanza.attr.from)) then
  216. for item in visitors_iq:childtags('connect-vnode') do
  217. connect_vnode_received(room, item.attr.vnode);
  218. processed = true;
  219. end
  220. for item in visitors_iq:childtags('disconnect-vnode') do
  221. disconnect_vnode_received(room, item.attr.vnode);
  222. processed = true;
  223. end
  224. end
  225. if not processed then
  226. module:log('warn', 'Unknown iq received for %s: %s', module.host, stanza);
  227. end
  228. respond_iq_result(origin, stanza);
  229. return processed;
  230. end
  231. local function process_promotion_response(room, id, approved)
  232. -- lets reply to participant that requested promotion
  233. local username = new_id():lower();
  234. visitors_promotion_map[room.jid][username] = {
  235. from = visitors_promotion_requests[room.jid][id].from;
  236. jid = id;
  237. };
  238. local req_from = visitors_promotion_map[room.jid][username].from;
  239. local req_jid = visitors_promotion_map[room.jid][username].jid;
  240. local focus_occupant = get_focus_occupant(room);
  241. local focus_jid = focus_occupant and focus_occupant.bare_jid or nil;
  242. local iq_id = new_id();
  243. sent_iq_cache:set(iq_id, socket.gettime());
  244. local node = jid.node(room.jid);
  245. module:send(st.iq({
  246. type='set', to = req_from, from = module.host, id = iq_id })
  247. :tag('visitors', {
  248. xmlns='jitsi:visitors',
  249. room = jid.join(node, muc_domain_prefix..'.'..req_from),
  250. focusjid = focus_jid })
  251. :tag('promotion-response', {
  252. xmlns='jitsi:visitors',
  253. jid = req_jid,
  254. username = username,
  255. allow = approved }):up());
  256. end
  257. module:hook('iq/host', stanza_handler, 10);
  258. process_host_module(muc_domain_prefix..'.'..muc_domain_base, function(host_module, host)
  259. -- if visitor mode is started, then you are not allowed to join without request/response exchange of iqs -> deny access
  260. -- check list of allowed jids for the room
  261. host_module:hook('muc-occupant-pre-join', function (event)
  262. local room, stanza, occupant, origin = event.room, event.stanza, event.occupant, event.origin;
  263. if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) then
  264. return;
  265. end
  266. -- visitors were already in the room one way or another they have access
  267. -- skip password challenge
  268. local join = stanza:get_child('x', MUC_NS);
  269. if join and room:get_password() and
  270. visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] then
  271. join:tag('password', { xmlns = MUC_NS }):text(room:get_password());
  272. end
  273. -- we skip any checks when auto-allow is enabled
  274. if auto_allow_promotion
  275. or ignore_list:contains(jid.host(stanza.attr.from)) -- jibri or other domains to ignore
  276. or stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi')
  277. or is_sip_jibri_join(stanza) then
  278. return;
  279. end
  280. if visitors_promotion_map[room.jid] then
  281. -- now let's check for jid
  282. if visitors_promotion_map[room.jid][jid.node(stanza.attr.from)] -- promotion was approved
  283. or ignore_list:contains(jid.host(stanza.attr.from)) then -- jibri or other domains to ignore
  284. -- allow join
  285. return;
  286. end
  287. module:log('error', 'Visitor needs to be allowed by a moderator %s', stanza.attr.from);
  288. origin.send(st.error_reply(stanza, 'cancel', 'not-allowed', 'Visitor needs to be allowed by a moderator'));
  289. return true;
  290. end
  291. end, 7); -- after muc_meeting_id, the logic for not joining before jicofo
  292. host_module:hook('muc-room-destroyed', function (event)
  293. visitors_promotion_map[event.room.jid] = nil;
  294. visitors_promotion_requests[event.room.jid] = nil;
  295. end);
  296. host_module:hook('muc-occupant-joined', function (event)
  297. local room, occupant = event.room, event.occupant;
  298. if is_healthcheck_room(room.jid) or is_admin(occupant.bare_jid) or occupant.role ~= 'moderator' -- luacheck: ignore
  299. or not visitors_promotion_requests[event.room.jid] then
  300. return;
  301. end
  302. for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
  303. send_json_message(occupant.jid, value.msg);
  304. end
  305. end);
  306. host_module:hook('muc-set-affiliation', function (event)
  307. -- the actor can be nil if is coming from allowners or similar module we want to skip it here
  308. -- as we will handle it in occupant_joined
  309. local actor, affiliation, jid, room = event.actor, event.affiliation, event.jid, event.room;
  310. if is_admin(jid) or is_healthcheck_room(room.jid) or not actor or not affiliation == 'owner' -- luacheck: ignore
  311. or not visitors_promotion_requests[event.room.jid] then
  312. return;
  313. end
  314. -- event.jid is the bare jid of participant
  315. for _, occupant in room:each_occupant() do
  316. if occupant.bare_jid == event.jid then
  317. for _,value in pairs(visitors_promotion_requests[event.room.jid]) do
  318. send_json_message(occupant.jid, value.msg);
  319. end
  320. end
  321. end
  322. end);
  323. host_module:hook("message/bare", function(event)
  324. local stanza = event.stanza;
  325. if stanza.attr.type ~= "groupchat" then
  326. return;
  327. end
  328. local json_data = stanza:get_child_text("json-message", "http://jitsi.org/jitmeet");
  329. if json_data == nil then
  330. return;
  331. end
  332. local data, error = json.decode(json_data);
  333. if not data or data.type ~= 'visitors'
  334. or (data.action ~= "promotion-response" and data.action ~= "demote-request") then
  335. if error then
  336. module:log('error', 'Error decoding error:%s', error);
  337. end
  338. return;
  339. end
  340. local room = get_room_from_jid(event.stanza.attr.to);
  341. local occupant_jid = event.stanza.attr.from;
  342. local occupant = room:get_occupant_by_real_jid(occupant_jid);
  343. if not occupant then
  344. module:log("error", "Occupant %s was not found in room %s", occupant_jid, room.jid)
  345. return
  346. end
  347. if occupant.role ~= 'moderator' then
  348. module:log('error', 'Occupant %s sending response message but not moderator in room %s',
  349. occupant_jid, room.jid);
  350. return false;
  351. end
  352. if data.action == "demote-request" then
  353. if occupant.nick ~= room.jid..'/'..data.actor then
  354. module:log('error', 'Bad actor in demote request %s', stanza);
  355. event.origin.send(st.error_reply(stanza, "cancel", "bad-request"));
  356. return true;
  357. end
  358. -- when demoting we want to send message to the demoted participant and to moderators
  359. local target_jid = room.jid..'/'..data.id;
  360. stanza.attr.type = 'chat'; -- it is safe as we are not using this stanza instance anymore
  361. stanza.attr.from = module.host;
  362. for _, room_occupant in room:each_occupant() do
  363. -- do not send it to jicofo or back to the sender
  364. if room_occupant.jid ~= occupant.jid and not is_admin(room_occupant.bare_jid) then
  365. if room_occupant.role == 'moderator'
  366. or room_occupant.nick == target_jid then
  367. stanza.attr.to = room_occupant.jid;
  368. room:route_stanza(stanza);
  369. end
  370. end
  371. end
  372. else
  373. if data.id then
  374. process_promotion_response(room, data.id, data.approved and 'true' or 'false');
  375. else
  376. -- we are in the case with admit all, we need to read data.ids
  377. for _,value in pairs(data.ids) do
  378. process_promotion_response(room, value, data.approved and 'true' or 'false');
  379. end
  380. end
  381. end
  382. return true; -- halt processing, but return true that we handled it
  383. end);
  384. if always_visitors_enabled then
  385. local visitorsEnabledField = {
  386. name = "muc#roominfo_visitorsEnabled";
  387. type = "boolean";
  388. label = "Whether visitors are enabled.";
  389. value = 1;
  390. };
  391. -- Append "visitors enabled" to the MUC config form.
  392. host_module:context(host):hook("muc-disco#info", function(event)
  393. table.insert(event.form, visitorsEnabledField);
  394. end);
  395. host_module:context(host):hook("muc-config-form", function(event)
  396. table.insert(event.form, visitorsEnabledField);
  397. end);
  398. end
  399. end);
  400. prosody.events.add_handler('pre-jitsi-authentication', function(session)
  401. if not session.customusername or not session.jitsi_web_query_room then
  402. return nil;
  403. end
  404. local room = get_room_by_name_and_subdomain(session.jitsi_web_query_room, session.jitsi_web_query_prefix);
  405. if not room then
  406. return nil;
  407. end
  408. if visitors_promotion_map[room.jid] and visitors_promotion_map[room.jid][session.customusername] then
  409. -- user was previously allowed to join, let him use the requested jid
  410. return session.customusername;
  411. end
  412. end);