remote_interaction_helper.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. /*
  2. This script is meant to to be used in an `iframe` with the sole purpose of doing webfinger queries
  3. client-side without being restricted by a strict `connect-src` Content-Security-Policy directive.
  4. It communicates with the parent window through message events that are authenticated by origin,
  5. and performs no other task.
  6. */
  7. import './public-path';
  8. import axios from 'axios';
  9. interface JRDLink {
  10. rel: string;
  11. template?: string;
  12. href?: string;
  13. }
  14. const isJRDLink = (link: unknown): link is JRDLink =>
  15. typeof link === 'object' &&
  16. link !== null &&
  17. 'rel' in link &&
  18. typeof link.rel === 'string' &&
  19. (!('template' in link) || typeof link.template === 'string') &&
  20. (!('href' in link) || typeof link.href === 'string');
  21. const findLink = (rel: string, data: unknown): JRDLink | undefined => {
  22. if (
  23. typeof data === 'object' &&
  24. data !== null &&
  25. 'links' in data &&
  26. data.links instanceof Array
  27. ) {
  28. return data.links.find(
  29. (link): link is JRDLink => isJRDLink(link) && link.rel === rel,
  30. );
  31. } else {
  32. return undefined;
  33. }
  34. };
  35. const findTemplateLink = (data: unknown) =>
  36. findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template;
  37. const fetchInteractionURLSuccess = (
  38. uri_or_domain: string,
  39. template: string,
  40. ) => {
  41. window.parent.postMessage(
  42. {
  43. type: 'fetchInteractionURL-success',
  44. uri_or_domain,
  45. template,
  46. },
  47. window.origin,
  48. );
  49. };
  50. const fetchInteractionURLFailure = () => {
  51. window.parent.postMessage(
  52. {
  53. type: 'fetchInteractionURL-failure',
  54. },
  55. window.origin,
  56. );
  57. };
  58. const isValidDomain = (value: string) => {
  59. const url = new URL('https:///path');
  60. url.hostname = value;
  61. return url.hostname === value;
  62. };
  63. // Attempt to find a remote interaction URL from a domain
  64. const fromDomain = (domain: string) => {
  65. const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
  66. axios
  67. .get(`https://${domain}/.well-known/webfinger`, {
  68. params: { resource: `https://${domain}` },
  69. })
  70. .then(({ data }) => {
  71. const template = findTemplateLink(data);
  72. fetchInteractionURLSuccess(domain, template ?? fallbackTemplate);
  73. return;
  74. })
  75. .catch(() => {
  76. fetchInteractionURLSuccess(domain, fallbackTemplate);
  77. });
  78. };
  79. // Attempt to find a remote interaction URL from an arbitrary URL
  80. const fromURL = (url: string) => {
  81. const domain = new URL(url).host;
  82. const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
  83. axios
  84. .get(`https://${domain}/.well-known/webfinger`, {
  85. params: { resource: url },
  86. })
  87. .then(({ data }) => {
  88. const template = findTemplateLink(data);
  89. fetchInteractionURLSuccess(url, template ?? fallbackTemplate);
  90. return;
  91. })
  92. .catch(() => {
  93. fromDomain(domain);
  94. });
  95. };
  96. // Attempt to find a remote interaction URL from a `user@domain` string
  97. const fromAcct = (acct: string) => {
  98. acct = acct.replace(/^@/, '');
  99. const segments = acct.split('@');
  100. if (segments.length !== 2 || !segments[0] || !isValidDomain(segments[1])) {
  101. fetchInteractionURLFailure();
  102. return;
  103. }
  104. const domain = segments[1];
  105. const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
  106. axios
  107. .get(`https://${domain}/.well-known/webfinger`, {
  108. params: { resource: `acct:${acct}` },
  109. })
  110. .then(({ data }) => {
  111. const template = findTemplateLink(data);
  112. fetchInteractionURLSuccess(acct, template ?? fallbackTemplate);
  113. return;
  114. })
  115. .catch(() => {
  116. // TODO: handle host-meta?
  117. fromDomain(domain);
  118. });
  119. };
  120. const fetchInteractionURL = (uri_or_domain: string) => {
  121. if (uri_or_domain === '') {
  122. fetchInteractionURLFailure();
  123. } else if (/^https?:\/\//.test(uri_or_domain)) {
  124. fromURL(uri_or_domain);
  125. } else if (uri_or_domain.includes('@')) {
  126. fromAcct(uri_or_domain);
  127. } else {
  128. fromDomain(uri_or_domain);
  129. }
  130. };
  131. window.addEventListener('message', (event: MessageEvent<unknown>) => {
  132. // Check message origin
  133. if (
  134. !window.origin ||
  135. window.parent !== event.source ||
  136. event.origin !== window.origin
  137. ) {
  138. return;
  139. }
  140. if (
  141. event.data &&
  142. typeof event.data === 'object' &&
  143. 'type' in event.data &&
  144. event.data.type === 'fetchInteractionURL' &&
  145. 'uri_or_domain' in event.data &&
  146. typeof event.data.uri_or_domain === 'string'
  147. ) {
  148. fetchInteractionURL(event.data.uri_or_domain);
  149. }
  150. });