Add support for responses
This commit is contained in:
parent
e298319bf5
commit
94add3d471
@ -1,5 +1,5 @@
|
|||||||
import {Request} from "../lifecycle/Request";
|
import {Request} from "../lifecycle/Request";
|
||||||
import {infer} from "@extollo/util";
|
import {uninfer, infer, uuid_v4} from "@extollo/util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base type representing a parsed cookie.
|
* Base type representing a parsed cookie.
|
||||||
@ -9,10 +9,22 @@ export interface HTTPCookie {
|
|||||||
originalValue: string,
|
originalValue: string,
|
||||||
value: any,
|
value: any,
|
||||||
exists: boolean,
|
exists: boolean,
|
||||||
|
options?: HTTPCookieOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeHTTPCookie = HTTPCookie | undefined;
|
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 {
|
export class HTTPCookieJar {
|
||||||
protected parsed: {[key: string]: HTTPCookie} = {}
|
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() {
|
private parseCookies() {
|
||||||
const cookies = String(this.request.getHeader('cookie'))
|
const cookies = String(this.request.getHeader('cookie'))
|
||||||
cookies.split(';').forEach(cookie => {
|
cookies.split(';').forEach(cookie => {
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import {Injectable, ScopedContainer, Container} from "@extollo/di"
|
import {Injectable, ScopedContainer, Container} from "@extollo/di"
|
||||||
import {infer} from "@extollo/util"
|
import {infer} from "@extollo/util"
|
||||||
import {IncomingMessage} from "http"
|
import {IncomingMessage, ServerResponse} from "http"
|
||||||
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
import {HTTPCookieJar} from "../kernel/HTTPCookieJar";
|
||||||
import {TLSSocket} from "tls";
|
import {TLSSocket} from "tls";
|
||||||
import * as url from "url";
|
import * as url from "url";
|
||||||
|
import {Response} from "./Response";
|
||||||
|
|
||||||
// FIXME - add others?
|
// FIXME - add others?
|
||||||
export type HTTPMethod = 'post' | 'get' | 'patch' | 'put' | 'delete' | 'unknown';
|
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 query: {[key: string]: any};
|
||||||
public readonly isXHR: boolean;
|
public readonly isXHR: boolean;
|
||||||
public readonly address: HTTPSourceAddress;
|
public readonly address: HTTPSourceAddress;
|
||||||
|
public readonly response: Response;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected clientRequest: IncomingMessage
|
protected clientRequest: IncomingMessage,
|
||||||
|
protected serverResponse: ServerResponse,
|
||||||
) {
|
) {
|
||||||
super(Container.getContainer())
|
super(Container.getContainer())
|
||||||
|
|
||||||
@ -93,6 +96,8 @@ export class Request extends ScopedContainer {
|
|||||||
family,
|
family,
|
||||||
port
|
port
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.response = new Response(this, serverResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
public async prepare() {
|
public async prepare() {
|
||||||
|
111
src/http/lifecycle/Response.ts
Normal file
111
src/http/lifecycle/Response.ts
Normal 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?
|
||||||
|
}
|
@ -38,10 +38,9 @@ export class HTTPServer extends Unit {
|
|||||||
|
|
||||||
public get handler() {
|
public get handler() {
|
||||||
return (request: IncomingMessage, response: ServerResponse) => {
|
return (request: IncomingMessage, response: ServerResponse) => {
|
||||||
const extolloReq = new Request(request)
|
const extolloReq = new Request(request, response)
|
||||||
console.log(extolloReq)
|
extolloReq.cookies.set('testing123', {foo: 'bar', bob: 123})
|
||||||
console.log(extolloReq.protocol)
|
extolloReq.response.send('Hi, from Extollo!!')
|
||||||
response.end('Hi, from Extollo!');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user