Source: router.js

import querystring from 'querystring';
import finalhandler from 'finalhandler';
import util from 'util';
import { Resolver } from 'dns';

const trimQ = str => str.replace(/^"(.+)"$/g, '$1');

class Router {
	/**
	 * Routes HTTP requests
	 * @param {Resolver} resolver The resolver object
	 */
	constructor(resolver) {
		this._resolver = resolver;
		this._attached = false;
	}

	/**
	 * Handle webfinger request
	 * @param URL url The URL of the incoming request
	 * @param	https.ServerResponse res The response to send back to the server
	 */
	async getWebfinger(url, res) {
		const query = querystring.parse(url.search.slice(1));
		const resource = Array.isArray(query.resource) ? query.resource[0] : query.resource;

		const [,username,domain] = resource.match(/^acct\:([^@]+)@(.+)$/i);

		// resolveWebfinger gets resolver from name config
		const actor = await this._resolver.resolveWebfinger(username, domain);
		if(!actor) {
			throw void 0;
		}
		const href = actor.id;

		const body = JSON.stringify({
			subject: resource,
			links: [
				{
					rel: "self",
					type: "application/activity+json",
					href
				}
			]
		});
		res.setHeader('Content-Type', 'application/json');
		res.end(body);
	}

	/**
	 * Handle a request for an activitypub object
	 * @param {URL} url The URL of the incoming request
	 * @param {https.ClientRequest} req The incoming request object
	 * @param {https.ServerResponse} res The resonse to send back
	 * @returns true if the request was handled successfuly, else false
	 */
	async getObject(url, req, res) {
		const o = await this._resolver.objectFor(url);
		if (!o) return false;
		if(req.method === 'POST') {
			try {
				const actor = await this.verifySignature(url, req);
			} catch(e) {
				console.log(e);
				throw e;
			}
			const data = await new Promise((res, rej) => {
				let str = '';
				req.on('data', d => str += d);
				req.on('end', () => res(str));
			});
			const json = JSON.parse(data);
			await o.post(json);
		}
		if(o.load) {
			// E.g. get items
			await o.load();
		}
		res.setHeader('Content-Type', 'application/activity+json');
		res.end(JSON.stringify(o));
		return true;
	}

	/**
	 * Handle an incoming HTTP request
	 * @param {https.ClientRequest} request The incoming request
	 * @param {https.ServerResponse} response The response to return
	 * @returns true if the request was handled successfuly, else false
	 */
	async handleRequest(request, response) {
		const url = new URL(request.url, this._resolver.appRoot);
		if(url.pathname === '/.well-known/webfinger') {
			await this.getWebfinger(url, response);
			return true;
		} else {
			return this.getObject(url, request, response);
		}

		// If method === post && request.header('content-type') === activity+json
		// handlePost(object, request, response)
		//
		// If method === get && request.accepts('activity+json')
		// handleGet(object, request, response)
		//
		// callback(object, request, response)
	}

	/**
	 * Attach request handlers to a server
	 * @param {https.Server} server The server to attach to
	 * @param	{function} onRequest A request handler with signature (request, response, next)
	 * @returns	{https.Server} The server (chainable)
	 */
	attach(server, onRequest) {
		if(this._attached === true) {
			throw new Error('You can only attach to one server at a time.');
		}

		server.on('request', async (req, res) => {
			console.log('In request');
			const done = finalhandler(req, res);

			try {
				if(onRequest) {
					console.log('onRequest', onRequest);
					await util.promisify(onRequest)(req, res);
				}
				console.log('Try handle');
				const handled = await this.handleRequest(req, res);
				console.log('Handled?', handled);
				if(!handled) {
					done();
				}
			} catch (e) {
				console.log('Request erreur', e);
				done(e);
			}
		});

		return server;
	}

	/**
	* Verify signature of incoming request is valid
	* @param {url} url URL of incoming request
	* @param {http.IncomingRequest} req Request to verify
	*/
	async verifySignature(url, req) {
		// TODO: Require (request-target), host, date to be present
		// TODO: Make sure sign date is not too old.
		const signatureHeader = {};

		if(!req.headers.signature) {
			throw new Error('Request not signed');
		}

		// Extract keys and values from signature header
		for(const pair of req.headers.signature.split(',')) {
			const [, key, value] = pair.match(/^([^=]+)=(.+)/);
			signatureHeader[key] = trimQ(value);
		}

		// Signature tells us where to find public key and
		// which headers were used to sign the request
		const {keyId, headers} = signatureHeader;

		if(!keyId) {
			throw new Error(`Request signature has no keyId`);
		}

		const signature = Buffer.from(signatureHeader.signature, 'base64');

		// Get the public key
		const actor = await new Promise((resolve, reject) => {
			const options = {
				headers: {
					"Accept": "application/json"
				}
			};
			options.rejectUnauthorized =  process.env.NODE_TLS_REJECT_UNAUTHORIZED !== 'false';
			require('https').get(keyId, options, response => {
				const { statusCode } = response;
				const contentType = response.headers['content-type'];

				let error;
				if (statusCode >= 400) {
					error = new Error(
						'Request Failed.\n' +
						`Status Code: ${statusCode}`
					);
				} else if(!/\/(.+\+)?json(;|$)/.test(response.headers['content-type'])) {
					error = new Error(
						'Invalid content-type.\n' +
						`Expected some kind of json but received ${contentType}`
					);
				}
				if (error) {
					reject(error);
					// Consume response data to free up memory
					response.resume();
					return;
				}

				response.setEncoding('utf8');
				let rawData = '';
				response.on('data', (chunk) => { rawData += chunk; });
				response.on('end', () => {
					try {
						const parsedData = JSON.parse(rawData);
						resolve(parsedData);
					} catch (e) {
						reject(e);
					}
				});
			}).on('error', reject);
		});

		const key = actor.publicKey.publicKeyPem;

		// Build the string to match the signature against
		const comparisonString = headers.split(' ').map(name => {
			if(name === '(request-target)') {
				return `${name}: post ${url.pathname}`;
			} else {
				return `${name}: ${req.headers[name]}`;
			}
		}).join('\n');

		const verifier = require('crypto').createVerify('SHA256');
		verifier.write(comparisonString);
		verifier.end();

		if(verifier.verify(key, signature)) {
			return actor;
		}
		throw new Error('Actor signature could not be verified');
	}
}

export default Router;