173 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			173 lines
		
	
	
		
			4.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| /*
 | |
| 
 | |
| This script is meant to to be used in an `iframe` with the sole purpose of doing webfinger queries
 | |
| client-side without being restricted by a strict `connect-src` Content-Security-Policy directive.
 | |
| 
 | |
| It communicates with the parent window through message events that are authenticated by origin,
 | |
| and performs no other task.
 | |
| 
 | |
| */
 | |
| 
 | |
| import './public-path';
 | |
| 
 | |
| import axios from 'axios';
 | |
| 
 | |
| interface JRDLink {
 | |
|   rel: string;
 | |
|   template?: string;
 | |
|   href?: string;
 | |
| }
 | |
| 
 | |
| const isJRDLink = (link: unknown): link is JRDLink =>
 | |
|   typeof link === 'object' &&
 | |
|   link !== null &&
 | |
|   'rel' in link &&
 | |
|   typeof link.rel === 'string' &&
 | |
|   (!('template' in link) || typeof link.template === 'string') &&
 | |
|   (!('href' in link) || typeof link.href === 'string');
 | |
| 
 | |
| const findLink = (rel: string, data: unknown): JRDLink | undefined => {
 | |
|   if (
 | |
|     typeof data === 'object' &&
 | |
|     data !== null &&
 | |
|     'links' in data &&
 | |
|     data.links instanceof Array
 | |
|   ) {
 | |
|     return data.links.find(
 | |
|       (link): link is JRDLink => isJRDLink(link) && link.rel === rel,
 | |
|     );
 | |
|   } else {
 | |
|     return undefined;
 | |
|   }
 | |
| };
 | |
| 
 | |
| const findTemplateLink = (data: unknown) =>
 | |
|   findLink('http://ostatus.org/schema/1.0/subscribe', data)?.template;
 | |
| 
 | |
| const fetchInteractionURLSuccess = (
 | |
|   uri_or_domain: string,
 | |
|   template: string,
 | |
| ) => {
 | |
|   window.parent.postMessage(
 | |
|     {
 | |
|       type: 'fetchInteractionURL-success',
 | |
|       uri_or_domain,
 | |
|       template,
 | |
|     },
 | |
|     window.origin,
 | |
|   );
 | |
| };
 | |
| 
 | |
| const fetchInteractionURLFailure = () => {
 | |
|   window.parent.postMessage(
 | |
|     {
 | |
|       type: 'fetchInteractionURL-failure',
 | |
|     },
 | |
|     window.origin,
 | |
|   );
 | |
| };
 | |
| 
 | |
| const isValidDomain = (value: string) => {
 | |
|   const url = new URL('https:///path');
 | |
|   url.hostname = value;
 | |
|   return url.hostname === value;
 | |
| };
 | |
| 
 | |
| // Attempt to find a remote interaction URL from a domain
 | |
| const fromDomain = (domain: string) => {
 | |
|   const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
 | |
| 
 | |
|   axios
 | |
|     .get(`https://${domain}/.well-known/webfinger`, {
 | |
|       params: { resource: `https://${domain}` },
 | |
|     })
 | |
|     .then(({ data }) => {
 | |
|       const template = findTemplateLink(data);
 | |
|       fetchInteractionURLSuccess(domain, template ?? fallbackTemplate);
 | |
|       return;
 | |
|     })
 | |
|     .catch(() => {
 | |
|       fetchInteractionURLSuccess(domain, fallbackTemplate);
 | |
|     });
 | |
| };
 | |
| 
 | |
| // Attempt to find a remote interaction URL from an arbitrary URL
 | |
| const fromURL = (url: string) => {
 | |
|   const domain = new URL(url).host;
 | |
|   const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
 | |
| 
 | |
|   axios
 | |
|     .get(`https://${domain}/.well-known/webfinger`, {
 | |
|       params: { resource: url },
 | |
|     })
 | |
|     .then(({ data }) => {
 | |
|       const template = findTemplateLink(data);
 | |
|       fetchInteractionURLSuccess(url, template ?? fallbackTemplate);
 | |
|       return;
 | |
|     })
 | |
|     .catch(() => {
 | |
|       fromDomain(domain);
 | |
|     });
 | |
| };
 | |
| 
 | |
| // Attempt to find a remote interaction URL from a `user@domain` string
 | |
| const fromAcct = (acct: string) => {
 | |
|   acct = acct.replace(/^@/, '');
 | |
| 
 | |
|   const segments = acct.split('@');
 | |
| 
 | |
|   if (segments.length !== 2 || !segments[0] || !isValidDomain(segments[1])) {
 | |
|     fetchInteractionURLFailure();
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const domain = segments[1];
 | |
|   const fallbackTemplate = `https://${domain}/authorize_interaction?uri={uri}`;
 | |
| 
 | |
|   axios
 | |
|     .get(`https://${domain}/.well-known/webfinger`, {
 | |
|       params: { resource: `acct:${acct}` },
 | |
|     })
 | |
|     .then(({ data }) => {
 | |
|       const template = findTemplateLink(data);
 | |
|       fetchInteractionURLSuccess(acct, template ?? fallbackTemplate);
 | |
|       return;
 | |
|     })
 | |
|     .catch(() => {
 | |
|       // TODO: handle host-meta?
 | |
|       fromDomain(domain);
 | |
|     });
 | |
| };
 | |
| 
 | |
| const fetchInteractionURL = (uri_or_domain: string) => {
 | |
|   if (/^https?:\/\//.test(uri_or_domain)) {
 | |
|     fromURL(uri_or_domain);
 | |
|   } else if (uri_or_domain.includes('@')) {
 | |
|     fromAcct(uri_or_domain);
 | |
|   } else {
 | |
|     fromDomain(uri_or_domain);
 | |
|   }
 | |
| };
 | |
| 
 | |
| window.addEventListener('message', (event: MessageEvent<unknown>) => {
 | |
|   // Check message origin
 | |
|   if (
 | |
|     !window.origin ||
 | |
|     window.parent !== event.source ||
 | |
|     event.origin !== window.origin
 | |
|   ) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   if (
 | |
|     event.data &&
 | |
|     typeof event.data === 'object' &&
 | |
|     'type' in event.data &&
 | |
|     event.data.type === 'fetchInteractionURL' &&
 | |
|     'uri_or_domain' in event.data &&
 | |
|     typeof event.data.uri_or_domain === 'string'
 | |
|   ) {
 | |
|     fetchInteractionURL(event.data.uri_or_domain);
 | |
|   }
 | |
| });
 |