webfinger-handler
Version:
A library that generates a handler for webfinger requests. The created handler works with Node JS HTTP request and response objects, and is otherwise framework agnostic.
187 lines (186 loc) • 5.57 kB
JavaScript
/**
* Webfinger
* @module webfinger-handler
* @see {@link module:webfinger-handler.default}
*/
/**
* Create a handler for incoming webfinger requests
* @param {getDescriptor} getDescriptor Returns a JSON Resource Descriptor object
* @returns {webfingerHandler} Method for handling webifinger requests
*/
export default class WebfingerHandler {
#getDescriptor;
constructor(desc) {
this.#getDescriptor = desc;
}
/**
* Webfinger handler
* @async
* @param {IncomingMessage} req The incomming HTTP request
* @param {OutgoingMessage} res The outgoing HTTP response
* @returns {Boolean} True if the request was handled, false if the request path did not match the webfinger endpoint
*/
async handle(req, res) {
try {
const subject = parse(req);
if (!subject) {
return false;
}
const links = (await this.#getDescriptor(subject)) || [];
const descriptor = Array.isArray(links) ?
{ subject: subject.uri, links } : links;
send(res, descriptor);
}
catch (e) {
if (e instanceof WebfingerError) {
sendError(res, e);
}
else {
throw e;
}
}
return true;
}
;
static activitypubResponse = activitypubResponse;
}
/**
* An error encountered while processing a webfinger request
*/
class WebfingerError extends Error {
}
/**
* An error thrown when the incomming Webfinger request uses the wrong HTTP method
*/
class WebfingerMethodError extends WebfingerError {
}
/**
* An error thrown when the webfinger `resource` paramter is incorrectly formatted
*/
class WebfingerResourceError extends WebfingerError {
}
const matcher = /^acct:([^@]+)@(.+)$/i;
/**
* An object representing an acct: URI
*/
export class AcctUri {
user;
host;
/**
* Construct an instance of AcctUri
* @param {Object} options The properties to set on the AcctUri instance
* @param {string} user The user part of the account name
* @param {string} host The hostname part of the account name
*/
constructor(user, host) {
this.user = user;
this.host = host;
}
/**
* Parse a string into an AcctUri object
* @param {string} resource An acct: URI
* @returns {AcctUri}
*/
static parse(resource) {
const match = resource?.match(matcher);
if (!match) {
throw new WebfingerResourceError('Expected a resource of the form "acct:username@domain.example"');
}
const [, user, host] = match;
return new AcctUri(user, host);
}
/**
* The scheme of the URI. This is always `acct:`.
*/
get scheme() {
return 'acct:';
}
/**
* Alias for `user`
*/
get userpart() {
return this.user;
}
/**
* The account string
* Made of the user and host concatenated with the `@` symbol
*/
get account() {
return this.userpart + '@' + this.host;
}
/**
* The full URI including the scheme
*/
get uri() {
return this.scheme + this.account;
}
toString() {
return this.uri;
}
}
export const endpoint = '/.well-known/webfinger';
/**
* Parses an incoming http request for the requested acct: URI
* @param {IncomingMessage} req The incoming HTTP request
* @returns {false} If the request is not for the webfinger endpoint
* @returns {AcctUri} An object representing the requested account
* @throws {WebfingerMethodError} If the GET method is not used in the request
*/
function parse(req) {
const url = new URL(req.url, 'file://');
if (url.pathname !== endpoint) {
return false;
}
if (req.method !== 'GET') {
throw new WebfingerMethodError('This endpoint only accepts GET requests');
}
const resource = url.searchParams.get('resource');
return AcctUri.parse(resource);
}
/**
* Informs the user about an error that occured with the webfinger request
* @param {HttpResponse} res The node HTTP response object
* @param {WebfingerError} error The error to report back to the user
*/
function sendError(res, error) {
res.setHeader('Access-Control-Allow-Origin', '*');
if (error instanceof WebfingerMethodError) {
res.statusCode = 405;
}
else {
res.statusCode = 400;
}
res.statusMessage = error.message;
res.end(error.message);
}
/**
* Sends a JSON RD response body to the client
* @param {HttpResponse} res The node HTTP response object
* @param {object} body The payload to return to the user
*/
function send(res, body) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/jrd+json');
res.end(JSON.stringify(body));
}
/**
* Get a link for an activitypub actor
* @callback getActorLink
* @async
* @param {AcctUri} resource The acct: uri of the resource to fetch for
* @returns {(string|object)} Either the href of the actor, or the JRD link representation of the actor
*/
/**
* Generates a getDescriptor method specifically for activitypub resource
* @param {getActorLink} getActivitypubLink Method that returns an href representing an activitypub actor
* @returns {getDescriptor} Method that can be used as the Webfinger descriptor getter
*/
export function activitypubResponse(href) {
return [
{
rel: 'self',
type: 'application/activity+json',
href
}
];
}