You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/http/kernel/HTTPCookieJar.ts

191 lines
4.7 KiB

import {Request} from '../lifecycle/Request'
import {uninfer, infer, uuid4} from '../../util'
/**
* Base type representing a parsed cookie.
*/
export interface HTTPCookie {
key: string,
originalValue: string,
value: any,
exists: boolean,
options?: HTTPCookieOptions,
}
/**
* Type alias for something that is either an HTTP cookie, or undefined.
*/
export type MaybeHTTPCookie = HTTPCookie | undefined;
/**
* Interface describing the available cookie options.
*/
export interface HTTPCookieOptions {
domain?: string,
expires?: Date, // encodeURIComponent
httpOnly?: boolean,
maxAge?: number,
path?: string,
secure?: boolean,
signed?: boolean,
sameSite?: 'strict' | 'lax' | 'none-secure',
}
/**
* Class for accessing and managing cookies in the associated request.
*/
export class HTTPCookieJar {
/** The cookies parsed from the request. */
protected parsed: {[key: string]: HTTPCookie} = {}
constructor(
/** The request whose cookies should be loaded. */
protected request: Request,
) {
this.parseCookies()
}
/**
* Gets the HTTPCookie by name, if it exists.
* @param name
*/
get(name: string): MaybeHTTPCookie {
if ( name in this.parsed ) {
return this.parsed[name]
}
}
/**
* Set a new cookie using the specified options.
* @param name
* @param value
* @param options
*/
set(name: string, value: unknown, options?: HTTPCookieOptions): this {
this.parsed[name] = {
key: name,
value,
originalValue: uninfer(value),
exists: false,
options,
}
return this
}
/**
* Returns true if a cookie exists with the given name.
* @param name
*/
has(name: string): boolean {
return Boolean(this.parsed[name])
}
/**
* Clears the given cookie.
*
* Important: if the cookie was set with any `options`, the SAME options
* must be provided here in order for the cookie to be cleared on the client.
*
* @param name
* @param options
*/
clear(name: string, options?: HTTPCookieOptions): this {
if ( !options ) {
options = {}
}
options.expires = new Date(0)
this.parsed[name] = {
key: name,
value: undefined,
originalValue: uuid4(),
exists: false,
options,
}
return this
}
/**
* Get an array of `Set-Cookie` headers to include in the response.
*/
getSetCookieHeaders(): string[] {
const headers: string[] = []
for ( const key in this.parsed ) {
if ( !Object.prototype.hasOwnProperty.call(this.parsed, key) ) {
continue
}
const cookie = this.parsed[key]
if ( cookie.exists ) {
continue
}
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
}
/** Parse the cookies from the request. */
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,
}
})
}
}