/********************************************************************************* Type Definitions *********************************************************************************/ import { IncomingMessage } from 'http' type Cookies = { [key: string]: string; } /********************************************************************************* Internal Definitions Unreserved characters according to RFC3986 ยง2.3: => unreserved := ALPHA | DIGIT | '-' | '.' | '_' | '~'. Equivalent to the regular expression: [-\w\.~]. Forbidden segments: => forbidden := BEGIN '.' END | BEGIN '..' END The dot character is allowed in any part of the segment as shown in the first note; it is forbidden only when there are no other characters other than the dot character (single or double), as shown in this note. *********************************************************************************/ import { struct } from 'superstruct' import * as cookie from 'cookie' import * as urlParser from 'url' import * as utils from './utils' export const internal = { payload: { get: (request: IncomingMessage): Promise => { function payloadParse(contentType, payload) { return (contentType === 'application/json') ? JSON.parse(Buffer.concat(payload).toString()) : new Error('Only JSON data is supported for now') } return request instanceof IncomingMessage ? new Promise((resolve, reject) => { let data: any = [] request.on('error', reject) request.on('data', chunk => data.push(chunk)) request.on('end', () => { const result = payloadParse(request.headers['content-type'], data) result instanceof Error ? reject(result) : resolve(result) }) }) : Promise.reject(new Error('The request must be an instance of stream.Writable')) } }, cookies: { parse: (cookies): Cookies => typeof cookies === 'string' ? cookie.parse(cookies) : {} }, url: { parse: (url: string) => { const parsedURL = urlParser.parse(url || '', true) const path = parsedURL.pathname || '/' return { path: path, query: parsedURL.query, pathSegments: internal.path.sanitize(internal.path.tokenize(path)) } } }, method: { acceptsPayload: (method: string): boolean => ['POST', 'PUT', 'PATCH'].some(x => x === method) }, segment: { isParameter: (x: string) => /^\/:/.test(x), isRest: (x: string) => /^\/\.\.\.$/.test(x), validate: (x: string): string | boolean => x === '/' || (x && !/^\/(\.\.|\.|%2E%2E|%2E){1}$/.test(x) && /^\/[-\w\.~]+$/.test(x)), matchPattern: (x: string, xs: Array) => xs.find(a => a === x) || xs.find(internal.segment.isParameter) || xs.find(internal.segment.isRest) }, path: { head: (x: string): string => { if (x === '') return x const endAt = x.indexOf('/', 1) return endAt < 0 ? x : x.slice(0, endAt) }, tail: (x: string): string => { if (x === '') return x const beginAt = x.indexOf('/', 1) return beginAt < 0 ? '' : x.slice(x.indexOf('/', 1)) }, tokenize: (path: string): Array => { if (path === '/') return [path] const segment = decodeURI(internal.path.head(path)).trim() const newPath = internal.path.tail(path) return segment === '' ? [] : [segment].concat(internal.path.tokenize(newPath)) }, sanitize: (segments: Array) => segments.filter(internal.segment.validate) }, resource: { getResource: (resourceRoute, resourceTree) => { const segment = resourceRoute && resourceRoute[0] return segment ? internal.resource.getResource(resourceRoute.slice(1), resourceTree[segment]) : resourceTree }, getRoute: (requestRoute: Array, resourceTree): Array | undefined => { const requestSegment = requestRoute && requestRoute[0] const segmentPatterns = resourceTree && Object.keys(resourceTree) if (!requestSegment || !segmentPatterns) return [] const pattern = internal.segment.matchPattern(requestSegment, segmentPatterns) if (internal.segment.isRest(pattern)) return [pattern].concat(requestRoute.slice(-1)) // Attach the request method after the rest pattern const route = pattern ? [pattern].concat(internal.resource.getRoute(requestRoute.slice(1), resourceTree[pattern])) : [undefined] return route.every(x => typeof x === 'string') ? route : undefined } }, getParameters: (requestPath, resourcePath) => { if (!requestPath[0] || !resourcePath[0]) return [] const segment = requestPath[0] const segmentPattern = resourcePath[0] const parameter = internal.segment.isParameter(segmentPattern) && { [segmentPattern.slice(2)]: segment.slice(1) } const rest = internal.segment.isRest(segmentPattern) && { rest: requestPath.reduce((x, xs) => x + xs) } return rest ? rest : Object.assign( parameter || {}, internal.getParameters(requestPath.slice(1), resourcePath.slice(1)) ) }, validate: (rules, data) => !rules || struct(rules).test(data) } /********************************************************************************* Method Definitions *********************************************************************************/ export function parseRequest(data) { return { request: { raw: data.httpRequest, headers: data.httpRequest.headers, method: data.httpRequest.method, url: internal.url.parse(data.httpRequest.url), cookies: internal.cookies.parse(data.httpRequest.headers.cookies), // payload: internal.method.acceptsPayload(data.httpRequest.method) // ? await internal.payload.get(data.httpRequest) // : '' }, server: data.server } } export function findResource(data) { if (data.error) return data const requestRoute = data.request.url.pathSegments.concat(data.request.method) const resourceRoute = internal.resource.getRoute(requestRoute, data.server.resources) return resourceRoute ? utils.set(data, 'resourceRoute', resourceRoute) : utils.set(data, 'error', 404) } export function getResource(data) { if (data.error) return data const resource = internal.resource.getResource(data.resourceRoute, data.server.resources) return resource ? utils.set(data, 'resource', resource) : utils.set(data, 'error', 500) } export function getParams(data) { if (data.error) return data const params = internal.getParameters(data.request.url.pathSegments, data.resourceRoute) return params ? utils.set(data, 'request.params', params) : utils.set(data, 'error', 500) } export function checkAccess(data) { if (data.error) return data const hasAccess = !data.resource.accessRole || (data.request.session && (data.request.session.accessRole === 'admin' || data.request.session.accessRole === data.resource.accessRole)) return hasAccess ? data : utils.set(data, 'error', 401) } export function checkValidity(data) { if (data.error) return data const isValid = !data.resource.validation || ( internal.validate(data.resource.validation.payload, data.request.payload) && internal.validate(data.resource.validation.params, data.request.params) && internal.validate(data.resource.validation.query, data.request.url.query) ) return isValid ? data : utils.set(data, 'error', 400) }