You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

util.lib.lua 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. local http_server = require "net.http.server";
  2. local jid = require "util.jid";
  3. local st = require 'util.stanza';
  4. local timer = require "util.timer";
  5. local http = require "net.http";
  6. local cache = require "util.cache";
  7. local array = require "util.array";
  8. local http_timeout = 30;
  9. local have_async, async = pcall(require, "util.async");
  10. local http_headers = {
  11. ["User-Agent"] = "Prosody ("..prosody.version.."; "..prosody.platform..")"
  12. };
  13. local muc_domain_prefix = module:get_option_string("muc_mapper_domain_prefix", "conference");
  14. -- defaults to module.host, the module that uses the utility
  15. local muc_domain_base = module:get_option_string("muc_mapper_domain_base", module.host);
  16. -- The "real" MUC domain that we are proxying to
  17. local muc_domain = module:get_option_string("muc_mapper_domain", muc_domain_prefix.."."..muc_domain_base);
  18. local escaped_muc_domain_base = muc_domain_base:gsub("%p", "%%%1");
  19. local escaped_muc_domain_prefix = muc_domain_prefix:gsub("%p", "%%%1");
  20. -- The pattern used to extract the target subdomain
  21. -- (e.g. extract 'foo' from 'conference.foo.example.com')
  22. local target_subdomain_pattern = "^"..escaped_muc_domain_prefix..".([^%.]+)%."..escaped_muc_domain_base;
  23. -- table to store all incoming iqs without roomname in it, like discoinfo to the muc component
  24. local roomless_iqs = {};
  25. local OUTBOUND_SIP_JIBRI_PREFIXES = { 'outbound-sip-jibri@', 'sipjibriouta@', 'sipjibrioutb@' };
  26. local INBOUND_SIP_JIBRI_PREFIXES = { 'inbound-sip-jibri@', 'sipjibriina@', 'sipjibriina@' };
  27. local RECORDER_PREFIXES = module:get_option_inherited_set('recorder_prefixes', { 'recorder@recorder.', 'jibria@recorder.', 'jibrib@recorder.' });
  28. local split_subdomain_cache = cache.new(1000);
  29. local extract_subdomain_cache = cache.new(1000);
  30. local internal_room_jid_cache = cache.new(1000);
  31. local moderated_subdomains = module:get_option_set("allowners_moderated_subdomains", {})
  32. local moderated_rooms = module:get_option_set("allowners_moderated_rooms", {})
  33. -- Utility function to split room JID to include room name and subdomain
  34. -- (e.g. from room1@conference.foo.example.com/res returns (room1, example.com, res, foo))
  35. local function room_jid_split_subdomain(room_jid)
  36. local ret = split_subdomain_cache:get(room_jid);
  37. if ret then
  38. return ret.node, ret.host, ret.resource, ret.subdomain;
  39. end
  40. local node, host, resource = jid.split(room_jid);
  41. local target_subdomain = host and host:match(target_subdomain_pattern);
  42. local cache_value = {node=node, host=host, resource=resource, subdomain=target_subdomain};
  43. split_subdomain_cache:set(room_jid, cache_value);
  44. return node, host, resource, target_subdomain;
  45. end
  46. --- Utility function to check and convert a room JID from
  47. --- virtual room1@conference.foo.example.com to real [foo]room1@conference.example.com
  48. -- @param room_jid the room jid to match and rewrite if needed
  49. -- @param stanza the stanza
  50. -- @return returns room jid [foo]room1@conference.example.com when it has subdomain
  51. -- otherwise room1@conference.example.com(the room_jid value untouched)
  52. local function room_jid_match_rewrite(room_jid, stanza)
  53. local node, _, resource, target_subdomain = room_jid_split_subdomain(room_jid);
  54. if not target_subdomain then
  55. -- module:log("debug", "No need to rewrite out 'to' %s", room_jid);
  56. return room_jid;
  57. end
  58. -- Ok, rewrite room_jid address to new format
  59. local new_node, new_host, new_resource;
  60. if node then
  61. new_node, new_host, new_resource = "["..target_subdomain.."]"..node, muc_domain, resource;
  62. else
  63. -- module:log("debug", "No room name provided so rewriting only host 'to' %s", room_jid);
  64. new_host, new_resource = muc_domain, resource;
  65. if (stanza and stanza.attr and stanza.attr.id) then
  66. roomless_iqs[stanza.attr.id] = stanza.attr.to;
  67. end
  68. end
  69. return jid.join(new_node, new_host, new_resource);
  70. end
  71. -- Utility function to check and convert a room JID from real [foo]room1@muc.example.com to virtual room1@muc.foo.example.com
  72. local function internal_room_jid_match_rewrite(room_jid, stanza)
  73. -- first check for roomless_iqs
  74. if (stanza and stanza.attr and stanza.attr.id and roomless_iqs[stanza.attr.id]) then
  75. local result = roomless_iqs[stanza.attr.id];
  76. roomless_iqs[stanza.attr.id] = nil;
  77. return result;
  78. end
  79. local ret = internal_room_jid_cache:get(room_jid);
  80. if ret then
  81. return ret;
  82. end
  83. local node, host, resource = jid.split(room_jid);
  84. if host ~= muc_domain or not node then
  85. -- module:log("debug", "No need to rewrite %s (not from the MUC host)", room_jid);
  86. internal_room_jid_cache:set(room_jid, room_jid);
  87. return room_jid;
  88. end
  89. local target_subdomain, target_node = extract_subdomain(node);
  90. if not (target_node and target_subdomain) then
  91. -- module:log("debug", "Not rewriting... unexpected node format: %s", node);
  92. internal_room_jid_cache:set(room_jid, room_jid);
  93. return room_jid;
  94. end
  95. -- Ok, rewrite room_jid address to pretty format
  96. ret = jid.join(target_node, muc_domain_prefix..".".. target_subdomain.."."..muc_domain_base, resource);
  97. internal_room_jid_cache:set(room_jid, ret);
  98. return ret;
  99. end
  100. --- Finds and returns room by its jid
  101. -- @param room_jid the room jid to search in the muc component
  102. -- @return returns room if found or nil
  103. function get_room_from_jid(room_jid)
  104. local _, host = jid.split(room_jid);
  105. local component = hosts[host];
  106. if component then
  107. local muc = component.modules.muc
  108. if muc and rawget(muc,"rooms") then
  109. -- We're running 0.9.x or 0.10 (old MUC API)
  110. return muc.rooms[room_jid];
  111. elseif muc and rawget(muc,"get_room_from_jid") then
  112. -- We're running >0.10 (new MUC API)
  113. return muc.get_room_from_jid(room_jid);
  114. else
  115. return
  116. end
  117. end
  118. end
  119. -- Returns the room if available, work and in multidomain mode
  120. -- @param room_name the name of the room
  121. -- @param group name of the group (optional)
  122. -- @return returns room if found or nil
  123. function get_room_by_name_and_subdomain(room_name, subdomain)
  124. local room_address;
  125. -- if there is a subdomain we are in multidomain mode and that subdomain is not our main host
  126. if subdomain and subdomain ~= "" and subdomain ~= muc_domain_base then
  127. room_address = jid.join("["..subdomain.."]"..room_name, muc_domain);
  128. else
  129. room_address = jid.join(room_name, muc_domain);
  130. end
  131. return get_room_from_jid(room_address);
  132. end
  133. function async_handler_wrapper(event, handler)
  134. if not have_async then
  135. module:log("error", "requires a version of Prosody with util.async");
  136. return nil;
  137. end
  138. local runner = async.runner;
  139. -- Grab a local response so that we can send the http response when
  140. -- the handler is done.
  141. local response = event.response;
  142. local async_func = runner(
  143. function (event)
  144. local result = handler(event)
  145. -- If there is a status code in the result from the
  146. -- wrapped handler then add it to the response.
  147. if tonumber(result.status_code) ~= nil then
  148. response.status_code = result.status_code
  149. end
  150. -- If there are headers in the result from the
  151. -- wrapped handler then add them to the response.
  152. if result.headers ~= nil then
  153. response.headers = result.headers
  154. end
  155. -- Send the response to the waiting http client with
  156. -- or without the body from the wrapped handler.
  157. if result.body ~= nil then
  158. response:send(result.body)
  159. else
  160. response:send();
  161. end
  162. end
  163. )
  164. async_func:run(event)
  165. -- return true to keep the client http connection open.
  166. return true;
  167. end
  168. --- Updates presence stanza, by adding identity node
  169. -- @param stanza the presence stanza
  170. -- @param user the user to which presence we are updating identity
  171. -- @param group the group of the user to which presence we are updating identity
  172. -- @param creator_user the user who created the user which presence we
  173. -- are updating (this is the poltergeist case, where a user creates
  174. -- a poltergeist), optional.
  175. -- @param creator_group the group of the user who created the user which
  176. -- presence we are updating (this is the poltergeist case, where a user creates
  177. -- a poltergeist), optional.
  178. function update_presence_identity(
  179. stanza, user, group, creator_user, creator_group)
  180. -- First remove any 'identity' element if it already
  181. -- exists, so it cannot be spoofed by a client
  182. stanza:maptags(
  183. function(tag)
  184. for k, v in pairs(tag) do
  185. if k == "name" and v == "identity" then
  186. return nil
  187. end
  188. end
  189. return tag
  190. end
  191. )
  192. stanza:tag("identity"):tag("user");
  193. for k, v in pairs(user) do
  194. v = tostring(v)
  195. stanza:tag(k):text(v):up();
  196. end
  197. stanza:up();
  198. -- Add the group information if it is present
  199. if group then
  200. stanza:tag("group"):text(group):up();
  201. end
  202. -- Add the creator user information if it is present
  203. if creator_user then
  204. stanza:tag("creator_user");
  205. for k, v in pairs(creator_user) do
  206. stanza:tag(k):text(v):up();
  207. end
  208. stanza:up();
  209. -- Add the creator group information if it is present
  210. if creator_group then
  211. stanza:tag("creator_group"):text(creator_group):up();
  212. end
  213. end
  214. stanza:up(); -- Close identity tag
  215. end
  216. -- Utility function to check whether feature is present and enabled. Allow
  217. -- a feature if there are features present in the session(coming from
  218. -- the token) and the value of the feature is true.
  219. -- If features are missing but we have granted_features check that
  220. -- if features are missing from the token we check whether it is moderator
  221. function is_feature_allowed(ft, features, granted_features, is_moderator)
  222. if features then
  223. return features[ft] == "true" or features[ft] == true;
  224. elseif granted_features then
  225. return granted_features[ft] == "true" or granted_features[ft] == true;
  226. else
  227. return is_moderator;
  228. end
  229. end
  230. --- Extracts the subdomain and room name from internal jid node [foo]room1
  231. -- @return subdomain(optional, if extracted or nil), the room name
  232. function extract_subdomain(room_node)
  233. local ret = extract_subdomain_cache:get(room_node);
  234. if ret then
  235. return ret.subdomain, ret.room;
  236. end
  237. local subdomain, room_name = room_node:match("^%[([^%]]+)%](.+)$");
  238. local cache_value = {subdomain=subdomain, room=room_name};
  239. extract_subdomain_cache:set(room_node, cache_value);
  240. return subdomain, room_name;
  241. end
  242. function starts_with(str, start)
  243. if not str then
  244. return false;
  245. end
  246. return str:sub(1, #start) == start
  247. end
  248. function starts_with_one_of(str, prefixes)
  249. if not str then
  250. return false;
  251. end
  252. for i=1,#prefixes do
  253. if starts_with(str, prefixes[i]) then
  254. return prefixes[i];
  255. end
  256. end
  257. return false
  258. end
  259. function ends_with(str, ending)
  260. return ending == "" or str:sub(-#ending) == ending
  261. end
  262. -- healthcheck rooms in jicofo starts with a string '__jicofo-health-check'
  263. function is_healthcheck_room(room_jid)
  264. return starts_with(room_jid, "__jicofo-health-check");
  265. end
  266. --- Utility function to make an http get request and
  267. --- retry @param retry number of times
  268. -- @param url endpoint to be called
  269. -- @param retry nr of retries, if retry is
  270. -- @param auth_token value to be passed as auth Bearer
  271. -- nil there will be no retries
  272. -- @returns result of the http call or nil if
  273. -- the external call failed after the last retry
  274. function http_get_with_retry(url, retry, auth_token)
  275. local content, code, cache_for;
  276. local timeout_occurred;
  277. local wait, done = async.waiter();
  278. local request_headers = http_headers or {}
  279. if auth_token ~= nil then
  280. request_headers['Authorization'] = 'Bearer ' .. auth_token
  281. end
  282. local function cb(content_, code_, response_, request_)
  283. if timeout_occurred == nil then
  284. code = code_;
  285. if code == 200 or code == 204 then
  286. -- module:log("debug", "External call was successful, content %s", content_);
  287. content = content_;
  288. -- if there is cache-control header, let's return the max-age value
  289. if response_ and response_.headers and response_.headers['cache-control'] then
  290. local vals = {};
  291. for k, v in response_.headers['cache-control']:gmatch('(%w+)=(%w+)') do
  292. vals[k] = v;
  293. end
  294. -- max-age=123 will be parsed by the regex ^ to age=123
  295. cache_for = vals.age;
  296. end
  297. else
  298. module:log("warn", "Error on GET request: Code %s, Content %s",
  299. code_, content_);
  300. end
  301. done();
  302. else
  303. module:log("warn", "External call reply delivered after timeout from: %s", url);
  304. end
  305. end
  306. local function call_http()
  307. return http.request(url, {
  308. headers = request_headers,
  309. method = "GET"
  310. }, cb);
  311. end
  312. local request = call_http();
  313. local function cancel()
  314. -- TODO: This check is racey. Not likely to be a problem, but we should
  315. -- still stick a mutex on content / code at some point.
  316. if code == nil then
  317. timeout_occurred = true;
  318. module:log("warn", "Timeout %s seconds making the external call to: %s", http_timeout, url);
  319. -- no longer present in prosody 0.11, so check before calling
  320. if http.destroy_request ~= nil then
  321. http.destroy_request(request);
  322. end
  323. if retry == nil then
  324. module:log("debug", "External call failed and retry policy is not set");
  325. done();
  326. elseif retry ~= nil and retry < 1 then
  327. module:log("debug", "External call failed after retry")
  328. done();
  329. else
  330. module:log("debug", "External call failed, retry nr %s", retry)
  331. retry = retry - 1;
  332. request = call_http()
  333. return http_timeout;
  334. end
  335. end
  336. end
  337. timer.add_task(http_timeout, cancel);
  338. wait();
  339. return content, code, cache_for;
  340. end
  341. -- Checks whether there is status in the <x node
  342. -- @param muc_x the <x element from presence
  343. -- @param status checks for this status
  344. -- @returns true if the status is found, false otherwise or if no muc_x is provided.
  345. function presence_check_status(muc_x, status)
  346. if not muc_x then
  347. return false;
  348. end
  349. for statusNode in muc_x:childtags('status') do
  350. if statusNode.attr.code == status then
  351. return true;
  352. end
  353. end
  354. return false;
  355. end
  356. -- Retrieves the focus from the room and cache it in the room object
  357. -- @param room The room name for which to find the occupant
  358. local function get_focus_occupant(room)
  359. return room:get_occupant_by_nick(room.jid..'/focus');
  360. end
  361. -- Checks whether the jid is moderated, the room name is in moderated_rooms
  362. -- or if the subdomain is in the moderated_subdomains
  363. -- @return returns on of the:
  364. -- -> false
  365. -- -> true, room_name, subdomain
  366. -- -> true, room_name, nil (if no subdomain is used for the room)
  367. function is_moderated(room_jid)
  368. if moderated_subdomains:empty() and moderated_rooms:empty() then
  369. return false;
  370. end
  371. local room_node = jid.node(room_jid);
  372. -- parses bare room address, for multidomain expected format is:
  373. -- [subdomain]roomName@conference.domain
  374. local target_subdomain, target_room_name = extract_subdomain(room_node);
  375. if target_subdomain then
  376. if moderated_subdomains:contains(target_subdomain) then
  377. return true, target_room_name, target_subdomain;
  378. end
  379. elseif moderated_rooms:contains(room_node) then
  380. return true, room_node, nil;
  381. end
  382. return false;
  383. end
  384. -- check if the room tenant starts with vpaas-magic-cookie-
  385. -- @param room the room to check
  386. function is_vpaas(room)
  387. if not room then
  388. return false;
  389. end
  390. -- stored check in room object if it exist
  391. if room.is_vpaas ~= nil then
  392. return room.is_vpaas;
  393. end
  394. room.is_vpaas = false;
  395. local node, host = jid.split(room.jid);
  396. if host ~= muc_domain or not node then
  397. return false;
  398. end
  399. local tenant, conference_name = node:match('^%[([^%]]+)%](.+)$');
  400. if not (tenant and conference_name) then
  401. return false;
  402. end
  403. if not starts_with(tenant, 'vpaas-magic-cookie-') then
  404. return false;
  405. end
  406. room.is_vpaas = true;
  407. return true;
  408. end
  409. -- Returns the initiator extension if the stanza is coming from a sip jigasi
  410. function is_sip_jigasi(stanza)
  411. if not stanza then
  412. return false;
  413. end
  414. return stanza:get_child('initiator', 'http://jitsi.org/protocol/jigasi');
  415. end
  416. function is_transcriber_jigasi(stanza)
  417. if not stanza then
  418. return false;
  419. end
  420. local features = stanza:get_child('features');
  421. if not features then
  422. return false;
  423. end
  424. for i = 1, #features do
  425. local feature = features[i];
  426. if feature.attr and feature.attr.var and feature.attr.var == 'http://jitsi.org/protocol/transcriber' then
  427. return true;
  428. end
  429. end
  430. return false;
  431. end
  432. function get_sip_jibri_email_prefix(email)
  433. if not email then
  434. return nil;
  435. elseif starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES) then
  436. return starts_with_one_of(email, INBOUND_SIP_JIBRI_PREFIXES);
  437. elseif starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES) then
  438. return starts_with_one_of(email, OUTBOUND_SIP_JIBRI_PREFIXES);
  439. else
  440. return nil;
  441. end
  442. end
  443. function is_sip_jibri_join(stanza)
  444. if not stanza then
  445. return false;
  446. end
  447. local features = stanza:get_child('features');
  448. local email = stanza:get_child_text('email');
  449. if not features or not email then
  450. return false;
  451. end
  452. for i = 1, #features do
  453. local feature = features[i];
  454. if feature.attr and feature.attr.var and feature.attr.var == "http://jitsi.org/protocol/jibri" then
  455. if get_sip_jibri_email_prefix(email) then
  456. module:log("debug", "Occupant with email %s is a sip jibri ", email);
  457. return true;
  458. end
  459. end
  460. end
  461. return false
  462. end
  463. function is_jibri(occupant)
  464. return starts_with_one_of(type(occupant) == "string" and occupant or occupant.jid, RECORDER_PREFIXES)
  465. end
  466. -- process a host module directly if loaded or hooks to wait for its load
  467. function process_host_module(name, callback)
  468. local function process_host(host)
  469. if host == name then
  470. callback(module:context(host), host);
  471. end
  472. end
  473. if prosody.hosts[name] == nil then
  474. module:log('info', 'No host/component found, will wait for it: %s', name)
  475. -- when a host or component is added
  476. prosody.events.add_handler('host-activated', process_host);
  477. else
  478. process_host(name);
  479. end
  480. end
  481. function table_shallow_copy(t)
  482. local t2 = {}
  483. for k, v in pairs(t) do
  484. t2[k] = v
  485. end
  486. return t2
  487. end
  488. -- Splits a string using delimiter
  489. function split_string(str, delimiter)
  490. str = str .. delimiter;
  491. local result = array();
  492. for w in str:gmatch("(.-)" .. delimiter) do
  493. result:push(w);
  494. end
  495. return result;
  496. end
  497. -- send iq result that the iq was received and will be processed
  498. function respond_iq_result(origin, stanza)
  499. -- respond with successful receiving the iq
  500. origin.send(st.iq({
  501. type = 'result';
  502. from = stanza.attr.to;
  503. to = stanza.attr.from;
  504. id = stanza.attr.id
  505. }));
  506. end
  507. -- Note: http_server.get_request_from_conn() was added in Prosody 0.12.3,
  508. -- this code provides backwards compatibility with older versions
  509. local get_request_from_conn = http_server.get_request_from_conn or function (conn)
  510. local response = conn and conn._http_open_response;
  511. return response and response.request or nil;
  512. end;
  513. -- Discover real remote IP of a session
  514. function get_ip(session)
  515. local request = get_request_from_conn(session.conn);
  516. return request and request.ip or session.ip;
  517. end
  518. return {
  519. OUTBOUND_SIP_JIBRI_PREFIXES = OUTBOUND_SIP_JIBRI_PREFIXES;
  520. INBOUND_SIP_JIBRI_PREFIXES = INBOUND_SIP_JIBRI_PREFIXES;
  521. RECORDER_PREFIXES = RECORDER_PREFIXES;
  522. extract_subdomain = extract_subdomain;
  523. is_feature_allowed = is_feature_allowed;
  524. is_jibri = is_jibri;
  525. is_healthcheck_room = is_healthcheck_room;
  526. is_moderated = is_moderated;
  527. is_sip_jibri_join = is_sip_jibri_join;
  528. is_sip_jigasi = is_sip_jigasi;
  529. is_transcriber_jigasi = is_transcriber_jigasi;
  530. is_vpaas = is_vpaas;
  531. get_focus_occupant = get_focus_occupant;
  532. get_ip = get_ip;
  533. get_room_from_jid = get_room_from_jid;
  534. get_room_by_name_and_subdomain = get_room_by_name_and_subdomain;
  535. get_sip_jibri_email_prefix = get_sip_jibri_email_prefix;
  536. async_handler_wrapper = async_handler_wrapper;
  537. presence_check_status = presence_check_status;
  538. process_host_module = process_host_module;
  539. respond_iq_result = respond_iq_result;
  540. room_jid_match_rewrite = room_jid_match_rewrite;
  541. room_jid_split_subdomain = room_jid_split_subdomain;
  542. internal_room_jid_match_rewrite = internal_room_jid_match_rewrite;
  543. update_presence_identity = update_presence_identity;
  544. http_get_with_retry = http_get_with_retry;
  545. ends_with = ends_with;
  546. split_string = split_string;
  547. starts_with = starts_with;
  548. starts_with_one_of = starts_with_one_of;
  549. table_shallow_copy = table_shallow_copy;
  550. };