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.
172 lines
4.3 KiB
172 lines
4.3 KiB
/* |
|
|
|
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); |
|
} |
|
});
|
|
|