http.js

'use strict';

/**
 * These function are not required if the javascript environment has its own HTTP parser. Just make sure the request from the HTTP parser has the [structure]{@link HttpRequest} the WebSocket functions expect.
 * <br>
 * To parse an HTTP buffer, use [parseHttp]{@link module:http~parseHttp}.
 * <br>
 * To construct HTTP responses, use [makeHttpResponse]{@link module:http~makeHttpResponse}, [makeHttpHtmlResponse]{@link module:http~makeHttpHtmlResponse} or [makeHttpHeaderResponse]{@link module:http~makeHttpHeaderResponse}
 * @module http
 */

/**
 * It can be anything that is indexed containing integers, has a length property and is iterable.
 * @typedef {number[]} ArrayBuffer
 * @global
 */

/**
 * Converts a string to a buffer
 * @function stringToBuffer
 * @param {string} str The string to convert to a buffer
 * @returns {Uint8Array} The string converted to a buffer
 */
export function stringToBuffer(str) {
	const len = str.length;
	const buffer = new Uint8Array(len);
	for (let i = 0; i < len; i++) {
		buffer[i] = str.charCodeAt(i);
	}
	return buffer;
}

/**
 * The Request information for an HTTP request
 * All header keys must be lower case
 * @global
 * @typedef {Object} HttpRequest
 * @property {string} method The method for the HTTP request. Required to be {@linkcode GET} if upgrading to WebSocket
 * @property {number|string} httpVersion The version of the HTTP protocol used. Required to be {@linkcode 1.1} or higher if upgrading to WebSocket
 * @property {string} [headers.connection] The header defining the connection type. Required to be {@linkcode Upgrade} if upgrading to WebSocket
 * @property {string} [headers.upgrade] The header defining what protocol to upgrade to if connection is upgrade. Required to be {@linkcode websocket} if upgrading to WebSocket
 * @property {string} [headers.sec-websocket-version] The version of the WebSocket protocol to use. Required to be {@linkcode 13} if upgrading to WebSocket
 * @property {string} [headers.sec-websocket-key] The base64 encoded 16 bytes random key generated by the client
 */

/**
 * Parses an incomming HTTP request from a TCP socket
 * @function parseHttp
 * @param {ArrayBuffer} buffer The HTTP buffer received from the client
 * @returns {HttpRequest} The parsed HTTP request
 * @todo this assumes that the http request is contained within a single tcp packet
 * @todo add some error handling to make sure it is http protocol
 */
export function parseHttp(buffer) {
	// Splits up data on linebreaks
	const splitted = String.fromCharCode(...buffer).split('\r\n');

	// Gets data from status line
	const [ method, url, httpVersion ] = splitted.shift().split(' ');

	// Remove last empty line
	splitted.pop();

	// Converts header lines to key and value on object
	const headers = {};
	splitted.forEach((header) => {
		const index = header.indexOf(':');
		headers[header.slice(0, index).trim().toLowerCase()] = header.slice(index + 1).trim();
	});

	return {
		method: method.toUpperCase(),
		url: url.toLowerCase(),
		httpVersion: httpVersion.slice(5),
		headers
	}
}

/**
 * Makes sure HTTP request comes from same origin
 * @function isSameOrigin
 * @param {string} origin The current origin
 * @param {HttpRequest} req The HTTP request to ensure comes from same origin
 * @returns {boolean} If the request is from same origin
 */
export function isSameOrigin(origin, req) {
	return headers.origin === origin.toLowerCase();
}

/**
 * The HTTP reasons. Can be extended for HTTP codes that are not implemented
 * @name httpStatusCodes
 * @enum {string}
 */
export const httpStatusCodes = {
	200: "OK",
	400: "Bad Request",
	403: "Forbidden",
	404: "Not Found",
	426: "Upgrade Required"
}

/**
* Makes a simple HTTP response without a body and only required headers
* @function makeHttpResponse
* @param {number} code The HTTP status code to use
* @param {boolan} done Indicates if the HTTP response is complete or should be concatenatable
* @returns {string} The HTTP response
*/
export function makeHttpResponse(code, done) {
	return "HTTP/1.1 " + code + " " + httpStatusCodes[code] + "\r\n" +
		"Connection: close\r\n" +
		"Date: " + new Date() + "\r\n" +
		((done !== false) ? "\r\n" : "");
}

/**
 * Makes a simple HTTP response with HTML content
 * @function makeHttpHtmlResponse
 * @param {string} body The HTML content to make an HTTP response for
 * @param {number} [code=200] The HTTP status code to use
 * @returns {string} The HTTP response containing the body
 */
export function makeHttpHtmlResponse(body, code = 200) {
	return makeHttpResponse(code, false) +
		"Content-Type: text/html\r\n" +
		"Content-Length: " + body.length + "\r\n" +
		"\r\n" +
		body;
}

/**
 * Makes a simple HTTP response with customizable headers. A body can be concatenated onto the response if the correct headers are set manually
 * @function makeHttpHeaderResponse
 * @param {number} code The HTTP status code to use
 * @param {Object} headers The headers to insert into the HTTP response
 * @returns {string} The HTTP response containing the headers
 */
export function makeHttpHeaderResponse(code, headers) {
	return makeHttpResponse(code, false) +
		Object.entries(headers).map(([ key, value ]) => key + ': ' + value).join('\r\n') +
		"\r\n\r\n";
}