import {Injectable, ScopedContainer, Container} from "@extollo/di" import {infer} from "@extollo/util" import {IncomingMessage, ServerResponse} from "http" import {HTTPCookieJar} from "../kernel/HTTPCookieJar"; import {TLSSocket} from "tls"; import * as url from "url"; import {Response} from "./Response"; import * as Negotiator from "negotiator"; // FIXME - add others? export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown'; export function isHTTPMethod(what: any): what is HTTPMethod { return ['post', 'get', 'patch', 'put', 'delete'].includes(what) } export interface HTTPProtocol { string: string, major: number, minor: number, } export interface HTTPSourceAddress { address: string; family: 'IPv4' | 'IPv6'; port: number; } @Injectable() export class Request extends ScopedContainer { public readonly cookies: HTTPCookieJar; public readonly url: string; public readonly fullUrl: string; public readonly method: HTTPMethod; public readonly secure: boolean; public readonly protocol: HTTPProtocol; public readonly path: string; public readonly rawQueryData: {[key: string]: string | string[] | undefined}; public readonly query: {[key: string]: any}; public readonly isXHR: boolean; public readonly address: HTTPSourceAddress; public readonly response: Response; public readonly mediaTypes: string[]; constructor( protected clientRequest: IncomingMessage, protected serverResponse: ServerResponse, ) { super(Container.getContainer()) this.secure = !!(clientRequest.connection as TLSSocket).encrypted this.cookies = new HTTPCookieJar(this) this.url = String(clientRequest.url) this.fullUrl = (this.secure ? 'https' : 'http') + `://${this.getHeader('host')}${this.url}` const method = clientRequest.method?.toLowerCase() this.method = isHTTPMethod(method) ? method : 'unknown' this.protocol = { string: clientRequest.httpVersion, major: clientRequest.httpVersionMajor, minor: clientRequest.httpVersionMinor, } this.register(Request) this.instances.push({ key: Request, value: this, }) const parts = url.parse(this.url, true) this.path = parts.pathname ?? '/' this.rawQueryData = parts.query const query: {[key: string]: any} = {} for ( const key in this.rawQueryData ) { const value = this.rawQueryData[key] if ( Array.isArray(value) ) { query[key] = value.map(x => infer(x)) } else if ( value ) { query[key] = infer(value) } else { query[key] = value } } this.query = query this.isXHR = String(this.clientRequest.headers['x-requested-with']).toLowerCase() === 'xmlhttprequest' // @ts-ignore const {address = '0.0.0.0', family = 'IPv4', port = 0} = this.clientRequest.connection.address() this.address = { address, family, port } this.mediaTypes = (new Negotiator(clientRequest)).mediaTypes() this.response = new Response(this, serverResponse) } public getHeader(name: string) { return this.clientRequest.headers[name.toLowerCase()] } public toNative() { return this.clientRequest } public input(key: string) { if ( key in this.query ) { return this.query[key] } } accepts(type: string) { if ( type === 'json' ) type = 'application/json' else if ( type === 'xml' ) type = 'application/xml' else if ( type === 'html' ) type = 'text/html' type = type.toLowerCase() const possible = [ type, type.split('/')[0] + '/*', '*/*' ] return this.mediaTypes.some(media => possible.includes(media.toLowerCase())) } wants(): 'html' | 'json' | 'xml' | 'unknown' { const jsonIdx = this.mediaTypes.indexOf('application/json') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*') const xmlIdx = this.mediaTypes.indexOf('application/xml') ?? this.mediaTypes.indexOf('application/*') ?? this.mediaTypes.indexOf('*/*') const htmlIdx = this.mediaTypes.indexOf('text/html') ?? this.mediaTypes.indexOf('text/*') ?? this.mediaTypes.indexOf('*/*') if ( htmlIdx >= 0 && htmlIdx <= jsonIdx && htmlIdx <= xmlIdx ) return 'html' if ( jsonIdx >= 0 && jsonIdx <= htmlIdx && jsonIdx <= xmlIdx ) return 'json' if ( xmlIdx >= 0 && xmlIdx <= jsonIdx && xmlIdx <= htmlIdx ) return 'xml' return 'unknown' } // hostname /* param json fresh/stale - cache remote ips (proxy) signedCookies accepts charsets, encodings, languages range header parser */ }