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.

uri.ts 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692
  1. import { sanitizeUrl as _sanitizeUrl } from '@braintree/sanitize-url';
  2. import { parseURLParams } from './parseURLParams';
  3. import { normalizeNFKC } from './strings';
  4. /**
  5. * Http status codes.
  6. */
  7. export enum StatusCode {
  8. PaymentRequired = 402
  9. }
  10. /**
  11. * The app linking scheme.
  12. * TODO: This should be read from the manifest files later.
  13. */
  14. export const APP_LINK_SCHEME = 'org.jitsi.meet:';
  15. /**
  16. * A list of characters to be excluded/removed from the room component/segment
  17. * of a conference/meeting URI/URL. The list is based on RFC 3986 and the jxmpp
  18. * library utilized by jicofo.
  19. */
  20. const _ROOM_EXCLUDE_PATTERN = '[\\:\\?#\\[\\]@!$&\'()*+,;=></"]';
  21. /**
  22. * The {@link RegExp} pattern of the authority of a URI.
  23. *
  24. * @private
  25. * @type {string}
  26. */
  27. const _URI_AUTHORITY_PATTERN = '(//[^/?#]+)';
  28. /**
  29. * The {@link RegExp} pattern of the path of a URI.
  30. *
  31. * @private
  32. * @type {string}
  33. */
  34. const _URI_PATH_PATTERN = '([^?#]*)';
  35. /**
  36. * The {@link RegExp} pattern of the protocol of a URI.
  37. *
  38. * FIXME: The URL class exposed by JavaScript will not include the colon in
  39. * the protocol field. Also in other places (at the time of this writing:
  40. * the DeepLinkingMobilePage.js) the APP_LINK_SCHEME does not include
  41. * the double dots, so things are inconsistent.
  42. *
  43. * @type {string}
  44. */
  45. export const URI_PROTOCOL_PATTERN = '^([a-z][a-z0-9\\.\\+-]*:)';
  46. /**
  47. * Excludes/removes certain characters from a specific path part which are
  48. * incompatible with Jitsi Meet on the client and/or server sides. The main
  49. * use case for this method is to clean up the room name and the tenant.
  50. *
  51. * @param {?string} pathPart - The path part to fix.
  52. * @private
  53. * @returns {?string}
  54. */
  55. function _fixPathPart(pathPart?: string) {
  56. return pathPart
  57. ? pathPart.replace(new RegExp(_ROOM_EXCLUDE_PATTERN, 'g'), '')
  58. : pathPart;
  59. }
  60. /**
  61. * Fixes the scheme part of a specific URI (string) so that it contains a
  62. * well-known scheme such as HTTP(S). For example, the mobile app implements an
  63. * app-specific URI scheme in addition to Universal Links. The app-specific
  64. * scheme may precede or replace the well-known scheme. In such a case, dealing
  65. * with the app-specific scheme only complicates the logic and it is simpler to
  66. * get rid of it (by translating the app-specific scheme into a well-known
  67. * scheme).
  68. *
  69. * @param {string} uri - The URI (string) to fix the scheme of.
  70. * @private
  71. * @returns {string}
  72. */
  73. function _fixURIStringScheme(uri: string) {
  74. const regex = new RegExp(`${URI_PROTOCOL_PATTERN}+`, 'gi');
  75. const match: Array<string> | null = regex.exec(uri);
  76. if (match) {
  77. // As an implementation convenience, pick up the last scheme and make
  78. // sure that it is a well-known one.
  79. let protocol = match[match.length - 1].toLowerCase();
  80. if (protocol !== 'http:' && protocol !== 'https:') {
  81. protocol = 'https:';
  82. }
  83. /* eslint-disable no-param-reassign */
  84. uri = uri.substring(regex.lastIndex);
  85. if (uri.startsWith('//')) {
  86. // The specified URL was not a room name only, it contained an
  87. // authority.
  88. uri = protocol + uri;
  89. }
  90. /* eslint-enable no-param-reassign */
  91. }
  92. return uri;
  93. }
  94. /**
  95. * Converts a path to a backend-safe format, by splitting the path '/' processing each part.
  96. * Properly lowercased and url encoded.
  97. *
  98. * @param {string?} path - The path to convert.
  99. * @returns {string?}
  100. */
  101. export function getBackendSafePath(path?: string): string | undefined {
  102. if (!path) {
  103. return path;
  104. }
  105. return path
  106. .split('/')
  107. .map(getBackendSafeRoomName)
  108. .join('/');
  109. }
  110. /**
  111. * Converts a room name to a backend-safe format. Properly lowercased and url encoded.
  112. *
  113. * @param {string?} room - The room name to convert.
  114. * @returns {string?}
  115. */
  116. export function getBackendSafeRoomName(room?: string): string | undefined {
  117. if (!room) {
  118. return room;
  119. }
  120. /* eslint-disable no-param-reassign */
  121. try {
  122. // We do not know if we get an already encoded string at this point
  123. // as different platforms do it differently, but we need a decoded one
  124. // for sure. However since decoding a non-encoded string is a noop, we're safe
  125. // doing it here.
  126. room = decodeURIComponent(room);
  127. } catch (e) {
  128. // This can happen though if we get an unencoded string and it contains
  129. // some characters that look like an encoded entity, but it's not.
  130. // But in this case we're fine going on...
  131. }
  132. // Normalize the character set.
  133. room = normalizeNFKC(room);
  134. // Only decoded and normalized strings can be lowercased properly.
  135. room = room?.toLowerCase();
  136. // But we still need to (re)encode it.
  137. room = encodeURIComponent(room ?? '');
  138. /* eslint-enable no-param-reassign */
  139. // Unfortunately we still need to lowercase it, because encoding a string will
  140. // add some uppercase characters, but some backend services
  141. // expect it to be full lowercase. However lowercasing an encoded string
  142. // doesn't change the string value.
  143. return room.toLowerCase();
  144. }
  145. /**
  146. * Gets the (Web application) context root defined by a specific location (URI).
  147. *
  148. * @param {Object} location - The location (URI) which defines the (Web
  149. * application) context root.
  150. * @public
  151. * @returns {string} - The (Web application) context root defined by the
  152. * specified {@code location} (URI).
  153. */
  154. export function getLocationContextRoot({ pathname }: { pathname: string; }) {
  155. const contextRootEndIndex = pathname.lastIndexOf('/');
  156. return (
  157. contextRootEndIndex === -1
  158. ? '/'
  159. : pathname.substring(0, contextRootEndIndex + 1));
  160. }
  161. /**
  162. * Constructs a new {@code Array} with URL parameter {@code String}s out of a
  163. * specific {@code Object}.
  164. *
  165. * @param {Object} obj - The {@code Object} to turn into URL parameter
  166. * {@code String}s.
  167. * @returns {Array<string>} The {@code Array} with URL parameter {@code String}s
  168. * constructed out of the specified {@code obj}.
  169. */
  170. function _objectToURLParamsArray(obj = {}) {
  171. const params = [];
  172. for (const key in obj) { // eslint-disable-line guard-for-in
  173. try {
  174. params.push(
  175. `${key}=${encodeURIComponent(JSON.stringify(obj[key as keyof typeof obj]))}`);
  176. } catch (e) {
  177. console.warn(`Error encoding ${key}: ${e}`);
  178. }
  179. }
  180. return params;
  181. }
  182. /**
  183. * Parses a specific URI string into an object with the well-known properties of
  184. * the {@link Location} and/or {@link URL} interfaces implemented by Web
  185. * browsers. The parsing attempts to be in accord with IETF's RFC 3986.
  186. *
  187. * @param {string} str - The URI string to parse.
  188. * @public
  189. * @returns {{
  190. * hash: string,
  191. * host: (string|undefined),
  192. * hostname: (string|undefined),
  193. * pathname: string,
  194. * port: (string|undefined),
  195. * protocol: (string|undefined),
  196. * search: string
  197. * }}
  198. */
  199. export function parseStandardURIString(str: string) {
  200. /* eslint-disable no-param-reassign */
  201. const obj: { [key: string]: any; } = {
  202. toString: _standardURIToString
  203. };
  204. let regex;
  205. let match: Array<string> | null;
  206. // XXX A URI string as defined by RFC 3986 does not contain any whitespace.
  207. // Usually, a browser will have already encoded any whitespace. In order to
  208. // avoid potential later problems related to whitespace in URI, strip any
  209. // whitespace. Anyway, the Jitsi Meet app is not known to utilize unencoded
  210. // whitespace so the stripping is deemed safe.
  211. str = str.replace(/\s/g, '');
  212. // protocol
  213. regex = new RegExp(URI_PROTOCOL_PATTERN, 'gi');
  214. match = regex.exec(str);
  215. if (match) {
  216. obj.protocol = match[1].toLowerCase();
  217. str = str.substring(regex.lastIndex);
  218. }
  219. // authority
  220. regex = new RegExp(`^${_URI_AUTHORITY_PATTERN}`, 'gi');
  221. match = regex.exec(str);
  222. if (match) {
  223. let authority: string = match[1].substring(/* // */ 2);
  224. str = str.substring(regex.lastIndex);
  225. // userinfo
  226. const userinfoEndIndex = authority.indexOf('@');
  227. if (userinfoEndIndex !== -1) {
  228. authority = authority.substring(userinfoEndIndex + 1);
  229. }
  230. obj.host = authority;
  231. // port
  232. const portBeginIndex = authority.lastIndexOf(':');
  233. if (portBeginIndex !== -1) {
  234. obj.port = authority.substring(portBeginIndex + 1);
  235. authority = authority.substring(0, portBeginIndex);
  236. }
  237. // hostname
  238. obj.hostname = authority;
  239. }
  240. // pathname
  241. regex = new RegExp(`^${_URI_PATH_PATTERN}`, 'gi');
  242. match = regex.exec(str);
  243. let pathname: string | undefined;
  244. if (match) {
  245. pathname = match[1];
  246. str = str.substring(regex.lastIndex);
  247. }
  248. if (pathname) {
  249. pathname.startsWith('/') || (pathname = `/${pathname}`);
  250. } else {
  251. pathname = '/';
  252. }
  253. obj.pathname = pathname;
  254. // query
  255. if (str.startsWith('?')) {
  256. let hashBeginIndex = str.indexOf('#', 1);
  257. if (hashBeginIndex === -1) {
  258. hashBeginIndex = str.length;
  259. }
  260. obj.search = str.substring(0, hashBeginIndex);
  261. str = str.substring(hashBeginIndex);
  262. } else {
  263. obj.search = ''; // Google Chrome
  264. }
  265. // fragment
  266. obj.hash = str.startsWith('#') ? str : '';
  267. /* eslint-enable no-param-reassign */
  268. return obj;
  269. }
  270. /**
  271. * Parses a specific URI which (supposedly) references a Jitsi Meet resource
  272. * (location).
  273. *
  274. * @param {(string|undefined)} uri - The URI to parse which (supposedly)
  275. * references a Jitsi Meet resource (location).
  276. * @public
  277. * @returns {{
  278. * contextRoot: string,
  279. * hash: string,
  280. * host: string,
  281. * hostname: string,
  282. * pathname: string,
  283. * port: string,
  284. * protocol: string,
  285. * room: (string|undefined),
  286. * search: string
  287. * }}
  288. */
  289. export function parseURIString(uri?: string): any {
  290. if (typeof uri !== 'string') {
  291. return undefined;
  292. }
  293. const obj = parseStandardURIString(_fixURIStringScheme(uri));
  294. // XXX While the components/segments of pathname are URI encoded, Jitsi Meet
  295. // on the client and/or server sides still don't support certain characters.
  296. obj.pathname = obj.pathname.split('/').map((pathPart: any) => _fixPathPart(pathPart))
  297. .join('/');
  298. // Add the properties that are specific to a Jitsi Meet resource (location)
  299. // such as contextRoot, room:
  300. // contextRoot
  301. // @ts-ignore
  302. obj.contextRoot = getLocationContextRoot(obj);
  303. // The room (name) is the last component/segment of pathname.
  304. const { pathname } = obj;
  305. const contextRootEndIndex = pathname.lastIndexOf('/');
  306. obj.room = pathname.substring(contextRootEndIndex + 1) || undefined;
  307. if (contextRootEndIndex > 1) {
  308. // The part of the pathname from the beginning to the room name is the tenant.
  309. obj.tenant = pathname.substring(1, contextRootEndIndex);
  310. }
  311. return obj;
  312. }
  313. /**
  314. * Implements {@code href} and {@code toString} for the {@code Object} returned
  315. * by {@link #parseStandardURIString}.
  316. *
  317. * @param {Object} [thiz] - An {@code Object} returned by
  318. * {@code #parseStandardURIString} if any; otherwise, it is presumed that the
  319. * function is invoked on such an instance.
  320. * @returns {string}
  321. */
  322. function _standardURIToString(thiz?: Object) {
  323. // @ts-ignore
  324. // eslint-disable-next-line @typescript-eslint/no-invalid-this
  325. const { hash, host, pathname, protocol, search } = thiz || this;
  326. let str = '';
  327. protocol && (str += protocol);
  328. // TODO userinfo
  329. host && (str += `//${host}`);
  330. str += pathname || '/';
  331. search && (str += search);
  332. hash && (str += hash);
  333. return str;
  334. }
  335. /**
  336. * Sometimes we receive strings that we don't know if already percent-encoded, or not, due to the
  337. * various sources we get URLs or room names. This function encapsulates the decoding in a safe way.
  338. *
  339. * @param {string} text - The text to decode.
  340. * @returns {string}
  341. */
  342. export function safeDecodeURIComponent(text: string) {
  343. try {
  344. return decodeURIComponent(text);
  345. } catch (e) {
  346. // The text wasn't encoded.
  347. }
  348. return text;
  349. }
  350. /**
  351. * Attempts to return a {@code String} representation of a specific
  352. * {@code Object} which is supposed to represent a URL. Obviously, if a
  353. * {@code String} is specified, it is returned. If a {@code URL} is specified,
  354. * its {@code URL#href} is returned. Additionally, an {@code Object} similar to
  355. * the one accepted by the constructor of Web's ExternalAPI is supported on both
  356. * mobile/React Native and Web/React.
  357. *
  358. * @param {Object|string} obj - The URL to return a {@code String}
  359. * representation of.
  360. * @returns {string} - A {@code String} representation of the specified
  361. * {@code obj} which is supposed to represent a URL.
  362. */
  363. export function toURLString(obj?: (Object | string)) {
  364. let str;
  365. switch (typeof obj) {
  366. case 'object':
  367. if (obj) {
  368. if (obj instanceof URL) {
  369. str = obj.href;
  370. } else {
  371. str = urlObjectToString(obj);
  372. }
  373. }
  374. break;
  375. case 'string':
  376. str = String(obj);
  377. break;
  378. }
  379. return str;
  380. }
  381. /**
  382. * Attempts to return a {@code String} representation of a specific
  383. * {@code Object} similar to the one accepted by the constructor
  384. * of Web's ExternalAPI.
  385. *
  386. * @param {Object} o - The URL to return a {@code String} representation of.
  387. * @returns {string} - A {@code String} representation of the specified
  388. * {@code Object}.
  389. */
  390. export function urlObjectToString(o: { [key: string]: any; }): string | undefined {
  391. // First normalize the given url. It come as o.url or split into o.serverURL
  392. // and o.room.
  393. let tmp;
  394. if (o.serverURL && o.room) {
  395. tmp = new URL(o.room, o.serverURL).toString();
  396. } else if (o.room) {
  397. tmp = o.room;
  398. } else {
  399. tmp = o.url || '';
  400. }
  401. const url = parseStandardURIString(_fixURIStringScheme(tmp));
  402. // protocol
  403. if (!url.protocol) {
  404. let protocol: string | undefined = o.protocol || o.scheme;
  405. if (protocol) {
  406. // Protocol is supposed to be the scheme and the final ':'. Anyway,
  407. // do not make a fuss if the final ':' is not there.
  408. protocol.endsWith(':') || (protocol += ':');
  409. url.protocol = protocol;
  410. }
  411. }
  412. // authority & pathname
  413. let { pathname } = url;
  414. if (!url.host) {
  415. // Web's ExternalAPI domain
  416. //
  417. // It may be host/hostname and pathname with the latter denoting the
  418. // tenant.
  419. const domain: string | undefined = o.domain || o.host || o.hostname;
  420. if (domain) {
  421. const { host, hostname, pathname: contextRoot, port }
  422. = parseStandardURIString(
  423. // XXX The value of domain in supposed to be host/hostname
  424. // and, optionally, pathname. Make sure it is not taken for
  425. // a pathname only.
  426. _fixURIStringScheme(`${APP_LINK_SCHEME}//${domain}`));
  427. // authority
  428. if (host) {
  429. url.host = host;
  430. url.hostname = hostname;
  431. url.port = port;
  432. }
  433. // pathname
  434. pathname === '/' && contextRoot !== '/' && (pathname = contextRoot);
  435. }
  436. }
  437. // pathname
  438. // Web's ExternalAPI roomName
  439. const room = o.roomName || o.room;
  440. if (room
  441. && (url.pathname.endsWith('/')
  442. || !url.pathname.endsWith(`/${room}`))) {
  443. pathname.endsWith('/') || (pathname += '/');
  444. pathname += room;
  445. }
  446. url.pathname = pathname;
  447. // query/search
  448. // Web's ExternalAPI jwt and lang
  449. const { jwt, lang, release } = o;
  450. const search = new URLSearchParams(url.search);
  451. // TODO: once all available versions are updated to support the jwt in the hash, remove this
  452. if (jwt) {
  453. search.set('jwt', jwt);
  454. }
  455. const { defaultLanguage } = o.configOverwrite || {};
  456. if (lang || defaultLanguage) {
  457. search.set('lang', lang || defaultLanguage);
  458. }
  459. if (release) {
  460. search.set('release', release);
  461. }
  462. const searchString = search.toString();
  463. if (searchString) {
  464. url.search = `?${searchString}`;
  465. }
  466. // fragment/hash
  467. let { hash } = url;
  468. if (jwt) {
  469. if (hash.length) {
  470. hash = `${hash}&jwt=${JSON.stringify(jwt)}`;
  471. } else {
  472. hash = `#jwt=${JSON.stringify(jwt)}`;
  473. }
  474. }
  475. for (const urlPrefix of [ 'config', 'iceServers', 'interfaceConfig', 'devices', 'userInfo', 'appData' ]) {
  476. const urlParamsArray
  477. = _objectToURLParamsArray(
  478. o[`${urlPrefix}Overwrite`]
  479. || o[urlPrefix]
  480. || o[`${urlPrefix}Override`]);
  481. if (urlParamsArray.length) {
  482. let urlParamsString
  483. = `${urlPrefix}.${urlParamsArray.join(`&${urlPrefix}.`)}`;
  484. if (hash.length) {
  485. urlParamsString = `&${urlParamsString}`;
  486. } else {
  487. hash = '#';
  488. }
  489. hash += urlParamsString;
  490. }
  491. }
  492. url.hash = hash;
  493. return url.toString() || undefined;
  494. }
  495. /**
  496. * Adds hash params to URL.
  497. *
  498. * @param {URL} url - The URL.
  499. * @param {Object} hashParamsToAdd - A map with the parameters to be set.
  500. * @returns {URL} - The new URL.
  501. */
  502. export function addHashParamsToURL(url: URL, hashParamsToAdd: Object = {}) {
  503. const params = parseURLParams(url);
  504. const urlParamsArray = _objectToURLParamsArray({
  505. ...params,
  506. ...hashParamsToAdd
  507. });
  508. if (urlParamsArray.length) {
  509. url.hash = `#${urlParamsArray.join('&')}`;
  510. }
  511. return url;
  512. }
  513. /**
  514. * Returns the decoded URI.
  515. *
  516. * @param {string} uri - The URI to decode.
  517. * @returns {string}
  518. */
  519. export function getDecodedURI(uri: string) {
  520. return decodeURI(uri.replace(/^https?:\/\//i, ''));
  521. }
  522. /**
  523. * Adds new param to a url string. Checks whether to use '?' or '&' as a separator (checks for already existing params).
  524. *
  525. * @param {string} url - The url to modify.
  526. * @param {string} name - The param name to add.
  527. * @param {string} value - The value for the param.
  528. *
  529. * @returns {string} - The modified url.
  530. */
  531. export function appendURLParam(url: string, name: string, value: string) {
  532. const newUrl = new URL(url);
  533. newUrl.searchParams.append(name, value);
  534. return newUrl.toString();
  535. }
  536. /**
  537. * Adds new hash param to a url string.
  538. * Checks whether to use '?' or '&' as a separator (checks for already existing params).
  539. *
  540. * @param {string} url - The url to modify.
  541. * @param {string} name - The param name to add.
  542. * @param {string} value - The value for the param.
  543. *
  544. * @returns {string} - The modified url.
  545. */
  546. export function appendURLHashParam(url: string, name: string, value: string) {
  547. const newUrl = new URL(url);
  548. const dummyUrl = new URL('https://example.com');
  549. // Copy current hash-parameters without the '#' as search-parameters.
  550. dummyUrl.search = newUrl.hash.substring(1);
  551. // Set or update value with the searchParams-API.
  552. dummyUrl.searchParams.append(name, value);
  553. // Write back as hash parameters.
  554. newUrl.hash = dummyUrl.searchParams.toString();
  555. return newUrl.toString();
  556. }
  557. /**
  558. * Sanitizes the given URL so that it's safe to use. If it's unsafe, null is returned.
  559. *
  560. * @param {string|URL} url - The URL that needs to be sanitized.
  561. *
  562. * @returns {URL?} - The sanitized URL, or null otherwise.
  563. */
  564. export function sanitizeUrl(url?: string | URL): URL | null {
  565. if (!url) {
  566. return null;
  567. }
  568. const urlStr = url.toString();
  569. const result = _sanitizeUrl(urlStr);
  570. if (result === 'about:blank') {
  571. return null;
  572. }
  573. return new URL(result);
  574. }