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.
261 lines (216 loc) • 6.18 kB
text/typescript
/**
* Webfinger
* @module webfinger-handler
* @see {@link module:webfinger-handler.default}
*/
type DescriptorLinks = {
rel: string,
href: string
}[];
type Descriptor = {
subject: string,
links: DescriptorLinks
};
type LookupResult = Descriptor | DescriptorLinks | null;
/**
* Return a JRD descriptor object for agiven acct: URI
* @callback getDescriptor
* @async
* @param {AcctUri} resource The acct: URI of the requested resource
* @returns Either: a JRD descriptor object, a JRD links array, or null if no resource was found
*/
type GetDescriptor = (acct: AcctUri) => LookupResult | Promise<LookupResult>;
/**
* 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: GetDescriptor
constructor(desc: GetDescriptor) {
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: Req, res: 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: string
host: string
/**
* 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: string, host: string) {
this.user = user;
this.host = host;
}
/**
* Parse a string into an AcctUri object
* @param {string} resource An acct: URI
* @returns {AcctUri}
*/
static parse(resource: string|null) {
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';
/**
* Incoming http request
* @external IncomingMessage
* @see {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage}
*/
type Req = {
url?: string,
method?: string
}
/**
* Outgoing http response
* @external OutgoingMessage
* @see {@link https://nodejs.org/api/http.html#http_class_http_incomingmessage}
*/
type Res = {
setHeader(k: string, v: string): unknown
statusCode: number,
statusMessage: string,
end(msg: string): unknown
}
/**
* 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: 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: Res, error: WebfingerError) {
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: Res, body: Descriptor) {
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: string) {
return [
{
rel: 'self',
type: 'application/activity+json',
href
}
];
}