Continue fleshing out HTTP request classes
This commit is contained in:
parent
cd387af2c6
commit
e298319bf5
49
src/http/kernel/HTTPCookieJar.ts
Normal file
49
src/http/kernel/HTTPCookieJar.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import {Request} from "../lifecycle/Request";
|
||||||
|
import {infer} from "@extollo/util";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base type representing a parsed cookie.
|
||||||
|
*/
|
||||||
|
export interface HTTPCookie {
|
||||||
|
key: string,
|
||||||
|
originalValue: string,
|
||||||
|
value: any,
|
||||||
|
exists: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaybeHTTPCookie = HTTPCookie | undefined;
|
||||||
|
|
||||||
|
export class HTTPCookieJar {
|
||||||
|
protected parsed: {[key: string]: HTTPCookie} = {}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected request: Request,
|
||||||
|
) {
|
||||||
|
this.parseCookies()
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): MaybeHTTPCookie {
|
||||||
|
if ( name in this.parsed ) {
|
||||||
|
return this.parsed[name]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseCookies() {
|
||||||
|
const cookies = String(this.request.getHeader('cookie'))
|
||||||
|
cookies.split(';').forEach(cookie => {
|
||||||
|
const parts = cookie.split('=')
|
||||||
|
|
||||||
|
const key = parts.shift()?.trim()
|
||||||
|
if ( !key ) return;
|
||||||
|
|
||||||
|
const value = decodeURI(parts.join('='))
|
||||||
|
|
||||||
|
this.parsed[key] = {
|
||||||
|
key,
|
||||||
|
originalValue: value,
|
||||||
|
value: infer(value),
|
||||||
|
exists: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
150
src/http/kernel/HTTPKernel.ts
Normal file
150
src/http/kernel/HTTPKernel.ts
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
import {Instantiable, Singleton, Inject} from "@extollo/di"
|
||||||
|
import {Collection} from "@extollo/util"
|
||||||
|
import {HTTPKernelModule} from "./HTTPKernelModule";
|
||||||
|
import {Logging} from "../../service/Logging";
|
||||||
|
import {AppClass} from "../../lifecycle/AppClass";
|
||||||
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for fluently registering kernel modules into the kernel.
|
||||||
|
*/
|
||||||
|
export interface ModuleRegistrationFluency {
|
||||||
|
before: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
||||||
|
after: (other?: Instantiable<HTTPKernelModule>) => HTTPKernel,
|
||||||
|
first: () => HTTPKernel,
|
||||||
|
last: () => HTTPKernel,
|
||||||
|
core: () => HTTPKernel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error thrown when a kernel module is requested that does not exist w/in the kernel.
|
||||||
|
* @extends Error
|
||||||
|
*/
|
||||||
|
export class KernelModuleNotFoundError extends Error {
|
||||||
|
constructor(name: string) {
|
||||||
|
super(`The kernel module ${name} is not registered with the kernel.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class HTTPKernel extends AppClass {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of preflight modules to apply.
|
||||||
|
* @type Collection<HTTPKernelModule>
|
||||||
|
*/
|
||||||
|
protected preflight: Collection<HTTPKernelModule> = new Collection<HTTPKernelModule>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module considered to be the main handler.
|
||||||
|
* @type HTTPKernelModule
|
||||||
|
*/
|
||||||
|
protected inflight?: HTTPKernelModule
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection of postflight modules to apply.
|
||||||
|
* @type Collection<HTTPKernelModule>
|
||||||
|
*/
|
||||||
|
protected postflight: Collection<HTTPKernelModule> = new Collection<HTTPKernelModule>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the incoming request, applying the preflight modules, inflight module, then postflight modules.
|
||||||
|
* @param {Request} request
|
||||||
|
* @return Promise<Request>
|
||||||
|
*/
|
||||||
|
public async handle(request: Request): Promise<Request> {
|
||||||
|
try {
|
||||||
|
for (const module of this.preflight.toArray()) {
|
||||||
|
this.logging.verbose(`Applying pre-flight HTTP kernel module: ${module.constructor.name}`)
|
||||||
|
request = await module.apply(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.inflight) {
|
||||||
|
this.logging.verbose(`Applying core HTTP kernel module: ${this.inflight.constructor.name}`)
|
||||||
|
request = await this.inflight.apply(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const module of this.postflight.toArray()) {
|
||||||
|
this.logging.verbose(`Applying post-flight HTTP kernel module: ${module.constructor.name}`)
|
||||||
|
request = await module.apply(request)
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
this.logging.error(e)
|
||||||
|
// FIXME handle error response
|
||||||
|
// const error_response = error(e)
|
||||||
|
// await error_response.write(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a fluent interface for registering the given kernel module.
|
||||||
|
* @param {Instantiable<HTTPKernelModule>} module
|
||||||
|
* @return ModuleRegistrationFluency
|
||||||
|
*/
|
||||||
|
public register(module: Instantiable<HTTPKernelModule>): ModuleRegistrationFluency {
|
||||||
|
this.logging.verbose(`Registering HTTP kernel module: ${module.name}`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
before: (other?: Instantiable<HTTPKernelModule>): HTTPKernel => {
|
||||||
|
if ( !other ) {
|
||||||
|
this.preflight = this.preflight.push(this.app().make(module))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||||
|
if ( typeof found_index !== 'undefined' ) {
|
||||||
|
this.preflight = this.preflight.put(found_index, this.app().make(module))
|
||||||
|
return this
|
||||||
|
} else {
|
||||||
|
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof found_index !== 'undefined' ) {
|
||||||
|
this.postflight = this.postflight.put(found_index, this.app().make(module))
|
||||||
|
} else {
|
||||||
|
throw new KernelModuleNotFoundError(other.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
after: (other?: Instantiable<HTTPKernelModule>): HTTPKernel => {
|
||||||
|
if ( !other ) {
|
||||||
|
this.postflight = this.postflight.push(this.app().make(module))
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
let found_index = this.preflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||||
|
if ( typeof found_index !== 'undefined' ) {
|
||||||
|
this.preflight = this.preflight.put(found_index + 1, this.app().make(module))
|
||||||
|
return this
|
||||||
|
} else {
|
||||||
|
found_index = this.postflight.find((mod: HTTPKernelModule) => mod instanceof other)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof found_index !== 'undefined' ) {
|
||||||
|
this.postflight = this.postflight.put(found_index + 1, this.app().make(module))
|
||||||
|
} else {
|
||||||
|
throw new KernelModuleNotFoundError(other.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
first: (): HTTPKernel => {
|
||||||
|
this.preflight = this.preflight.put(0, this.app().make(module))
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
last: (): HTTPKernel => {
|
||||||
|
this.postflight = this.postflight.push(this.app().make(module))
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
core: (): HTTPKernel => {
|
||||||
|
this.inflight = this.app().make(module)
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
src/http/kernel/HTTPKernelModule.ts
Normal file
33
src/http/kernel/HTTPKernelModule.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {Injectable} from "@extollo/di";
|
||||||
|
import {AppClass} from "../../lifecycle/AppClass";
|
||||||
|
import {HTTPKernel} from "./HTTPKernel";
|
||||||
|
import {Request} from "../lifecycle/Request";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HTTPKernelModule extends AppClass {
|
||||||
|
/**
|
||||||
|
* Returns true if the given module should be applied to the incoming request.
|
||||||
|
* @param {Request} request
|
||||||
|
* @return Promise<boolean>
|
||||||
|
*/
|
||||||
|
public async match(request: Request): Promise<boolean> {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the module to the incoming request.
|
||||||
|
* @param {Request} request
|
||||||
|
* @return Promise<Request>
|
||||||
|
*/
|
||||||
|
public async apply(request: Request): Promise<Request> {
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register this module with the given HTTP kernel.
|
||||||
|
* @param {HTTPKernel} kernel
|
||||||
|
*/
|
||||||
|
public static register(kernel: HTTPKernel) {
|
||||||
|
kernel.register(this).before()
|
||||||
|
}
|
||||||
|
}
|
14
src/http/kernel/module/PrepareRequestHTTPModule.ts
Normal file
14
src/http/kernel/module/PrepareRequestHTTPModule.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import {HTTPKernelModule} from "../HTTPKernelModule";
|
||||||
|
import {Request} from "../../lifecycle/Request";
|
||||||
|
import {HTTPKernel} from "../HTTPKernel";
|
||||||
|
|
||||||
|
export class PrepareRequestHTTPModule extends HTTPKernelModule {
|
||||||
|
public static register(kernel: HTTPKernel) {
|
||||||
|
kernel.register(this).first()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async apply(request: Request) {
|
||||||
|
await request.prepare()
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
}
|
132
src/http/lifecycle/Request.ts
Normal file
132
src/http/lifecycle/Request.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import {Injectable, ScopedContainer, Container} from "@extollo/di"
|
||||||
|
import {infer} from "@extollo/util"
|
||||||
|
import {IncomingMessage} from "http"
|
||||||
|
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||||
|
import {TLSSocket} from "tls";
|
||||||
|
import * as url from "url";
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected clientRequest: IncomingMessage
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async prepare() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// session
|
||||||
|
// route
|
||||||
|
// respond
|
||||||
|
// body
|
||||||
|
// hostname
|
||||||
|
|
||||||
|
/*
|
||||||
|
param
|
||||||
|
json
|
||||||
|
fresh/stale - cache
|
||||||
|
remote ips (proxy)
|
||||||
|
signedCookies
|
||||||
|
accepts content type, charsets, encodings, languages
|
||||||
|
is content type (wants)
|
||||||
|
range header parser
|
||||||
|
*/
|
||||||
|
}
|
@ -5,6 +5,9 @@ export * from './lifecycle/Application'
|
|||||||
export * from './lifecycle/AppClass'
|
export * from './lifecycle/AppClass'
|
||||||
export * from './lifecycle/Unit'
|
export * from './lifecycle/Unit'
|
||||||
|
|
||||||
|
export * from './http/kernel/HTTPKernel'
|
||||||
|
export * from './http/kernel/HTTPKernelModule'
|
||||||
|
|
||||||
export * from './http/Controller'
|
export * from './http/Controller'
|
||||||
|
|
||||||
export * from './service/Canonical'
|
export * from './service/Canonical'
|
||||||
@ -14,3 +17,4 @@ export * from './service/CanonicalStatic'
|
|||||||
export * from './service/FakeCanonical'
|
export * from './service/FakeCanonical'
|
||||||
export * from './service/Config'
|
export * from './service/Config'
|
||||||
export * from './service/Controllers'
|
export * from './service/Controllers'
|
||||||
|
export * from './service/HTTPServer'
|
||||||
|
48
src/service/HTTPServer.ts
Normal file
48
src/service/HTTPServer.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import {Singleton, Inject} from "@extollo/di"
|
||||||
|
import {Unit} from "../lifecycle/Unit";
|
||||||
|
import {createServer, IncomingMessage, ServerResponse, Server} from "http";
|
||||||
|
import {Logging} from "./Logging";
|
||||||
|
import {Request} from "../http/lifecycle/Request";
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class HTTPServer extends Unit {
|
||||||
|
@Inject()
|
||||||
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
protected server?: Server
|
||||||
|
|
||||||
|
public async up() {
|
||||||
|
const port = 8000
|
||||||
|
|
||||||
|
await new Promise<void>((res, rej) => {
|
||||||
|
this.server = createServer(this.handler)
|
||||||
|
|
||||||
|
this.server.listen(port, undefined, undefined, () => {
|
||||||
|
this.logging.success(`Server listening on port ${port}. Press ^C to stop.`)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down() {
|
||||||
|
if ( this.server ) {
|
||||||
|
this.server.close(err => {
|
||||||
|
if ( err ) {
|
||||||
|
this.logging.error(`Error encountered while closing HTTP server: ${err.message}`)
|
||||||
|
this.logging.debug(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get handler() {
|
||||||
|
return (request: IncomingMessage, response: ServerResponse) => {
|
||||||
|
const extolloReq = new Request(request)
|
||||||
|
console.log(extolloReq)
|
||||||
|
console.log(extolloReq.protocol)
|
||||||
|
response.end('Hi, from Extollo!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user