Add support for responses
This commit is contained in:
		
							parent
							
								
									e298319bf5
								
							
						
					
					
						commit
						94add3d471
					
				@ -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 => {
 | 
			
		||||
 | 
			
		||||
@ -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() {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										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() {
 | 
			
		||||
        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!!')
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user