Add support for responses

This commit is contained in:
Garrett Mills 2021-03-07 09:58:21 -06:00
parent e298319bf5
commit 94add3d471
Signed by: garrettmills
GPG Key ID: D2BF5FBA8298F246
4 changed files with 207 additions and 7 deletions

View File

@ -1,5 +1,5 @@
import {Request} from "../lifecycle/Request";
import {infer} from "@extollo/util";
import {uninfer, infer, uuid_v4} from "@extollo/util";
/**
* Base type representing a parsed cookie.
@ -9,10 +9,22 @@ export interface HTTPCookie {
originalValue: string,
value: any,
exists: boolean,
options?: HTTPCookieOptions,
}
export type MaybeHTTPCookie = HTTPCookie | undefined;
export interface HTTPCookieOptions {
domain?: string,
expires?: Date, // encodeURIComponent
httpOnly?: boolean,
maxAge?: number,
path?: string,
secure?: boolean,
signed?: boolean,
sameSite?: 'strict' | 'lax' | 'none-secure',
}
export class HTTPCookieJar {
protected parsed: {[key: string]: HTTPCookie} = {}
@ -28,6 +40,79 @@ export class HTTPCookieJar {
}
}
set(name: string, value: any, options?: HTTPCookieOptions) {
this.parsed[name] = {
key: name,
value,
originalValue: uninfer(value),
exists: false,
options,
}
}
clear(name: string, options?: HTTPCookieOptions) {
if ( !options ) options = {}
options.expires = new Date(0)
this.parsed[name] = {
key: name,
value: undefined,
originalValue: uuid_v4(),
exists: false,
options,
}
}
getSetCookieHeaders(): string[] {
const headers: string[] = []
for ( const key in this.parsed ) {
if ( !this.parsed.hasOwnProperty(key) ) continue
const cookie = this.parsed[key]
const parts = []
parts.push(`${key}=${encodeURIComponent(cookie.originalValue)}`)
if ( cookie.options?.expires ) {
parts.push(`Expires=${cookie.options.expires.toUTCString()}`)
}
if ( cookie.options?.maxAge ) {
parts.push(`Max-Age=${Math.floor(cookie.options.maxAge)}`)
}
if ( cookie.options?.domain ) {
parts.push(`Domain=${cookie.options.domain}`)
}
if ( cookie.options?.path ) {
parts.push(`Path=${cookie.options.path}`)
}
if ( cookie.options?.secure ) {
parts.push('Secure')
}
if ( cookie.options?.httpOnly ) {
parts.push('HttpOnly')
}
if ( cookie.options?.sameSite ) {
const map = {
strict: 'Strict',
lax: 'Lax',
'none-secure': 'None; Secure'
}
parts.push(map[cookie.options.sameSite])
}
headers.push(parts.join('; '))
}
return headers
}
private parseCookies() {
const cookies = String(this.request.getHeader('cookie'))
cookies.split(';').forEach(cookie => {

View File

@ -1,9 +1,10 @@
import {Injectable, ScopedContainer, Container} from "@extollo/di"
import {infer} from "@extollo/util"
import {IncomingMessage} from "http"
import {IncomingMessage, ServerResponse} from "http"
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
import {TLSSocket} from "tls";
import * as url from "url";
import {Response} from "./Response";
// FIXME - add others?
export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown';
@ -38,9 +39,11 @@ export class Request extends ScopedContainer {
public readonly query: {[key: string]: any};
public readonly isXHR: boolean;
public readonly address: HTTPSourceAddress;
public readonly response: Response;
constructor(
protected clientRequest: IncomingMessage
protected clientRequest: IncomingMessage,
protected serverResponse: ServerResponse,
) {
super(Container.getContainer())
@ -93,6 +96,8 @@ export class Request extends ScopedContainer {
family,
port
}
this.response = new Response(this, serverResponse)
}
public async prepare() {

View File

@ -0,0 +1,111 @@
import {Request} from "./Request";
import {ErrorWithContext, HTTPStatus} from "@extollo/util"
import {ServerResponse} from "http"
export class HeadersAlreadySentError extends ErrorWithContext {
constructor(response: Response, headerName?: string) {
super(`Cannot modify or re-send headers for this request as they have already been sent.`);
this.context = { headerName }
}
}
export class ResponseAlreadySentError extends ErrorWithContext {
constructor(response: Response) {
super(`Cannot modify or re-send response as it has already ended.`);
}
}
export class Response {
private headers: {[key: string]: string | string[]} = {}
private _sentHeaders: boolean = false
private _responseEnded: boolean = false
private _status: HTTPStatus = HTTPStatus.OK
public body: any
constructor(
public readonly request: Request,
protected readonly serverResponse: ServerResponse,
) { }
public getStatus() {
return this._status
}
public setStatus(status: HTTPStatus) {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, 'status')
this._status = status
}
public get cookies() {
return this.request.cookies
}
public getHeader(name: string): string | string[] | undefined {
return this.headers[name]
}
public setHeader(name: string, value: string | string[]) {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
this.headers[name] = value
return this
}
public setHeaders(data: {[name: string]: string | string[]}) {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this)
this.headers = {...this.headers, ...data}
return this
}
public appendHeader(name: string, value: string | string[]) {
if ( this._sentHeaders ) throw new HeadersAlreadySentError(this, name)
if ( !Array.isArray(value) ) value = [value]
let existing = this.headers[name] ?? []
if ( !Array.isArray(existing) ) existing = [existing]
existing = [...existing, ...value]
if ( existing.length === 1 ) existing = existing[0]
this.headers[name] = existing
}
public sendHeaders() {
const headers = {} as any
const setCookieHeaders = this.cookies.getSetCookieHeaders()
if ( setCookieHeaders.length ) headers['Set-Cookie'] = setCookieHeaders
for ( const key in this.headers ) {
if ( !this.headers.hasOwnProperty(key) ) continue
headers[key] = this.headers[key]
}
this.serverResponse.writeHead(this._status, headers)
this._sentHeaders = true
}
public hasSentHeaders() {
return this._sentHeaders
}
public async write(data: any) {
return new Promise<void>((res, rej) => {
if ( !this._sentHeaders ) this.sendHeaders()
this.serverResponse.write(data, error => {
if ( error ) rej(error)
else res()
})
})
}
public async send(data?: any) {
await this.write(data ?? this.body ?? '')
this.end()
}
public end() {
if ( this._responseEnded ) throw new ResponseAlreadySentError(this)
this._sentHeaders = true
this.serverResponse.end()
}
// location?
}

View File

@ -38,10 +38,9 @@ export class HTTPServer extends Unit {
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!');
const extolloReq = new Request(request, response)
extolloReq.cookies.set('testing123', {foo: 'bar', bob: 123})
extolloReq.response.send('Hi, from Extollo!!')
}
}