reserve
Version:
Lightweight http server statically configurable using regular expressions
498 lines (425 loc) • 16.6 kB
TypeScript
import { IncomingMessage, ServerResponse, Server as HttpServer } from 'http'
import { Server as HttpsServer } from 'https'
import { Http2Server } from 'http2'
declare module 'reserve' {
class Request extends IncomingMessage {
/** Sets a forged (unverified) URL for the request */
setForgedUrl: (url: string) => void
}
type RedirectResponse =
| void
/** Ends the response with corresponding status code */
| number
/** Triggers an internal redirect */
| string
type IfMatcher = (request: IncomingMessage, url: string, match: RegExpMatchArray) => boolean | RedirectResponse
interface BaseMapping {
/** URL matching, capturing groups are allowed */
match?: string | RegExp
/** Request method matching */
method?: string
/** Inverts the matching process when set to true, enabling the implementation of an 'all but' pattern */
'invert-match'?: boolean
/** Executed only if the mapping matches the request, enabling finer control */
'if-match'?: IfMatcher
/** Ignore any request processed by this mapping when updating the list of mappings */
'exclude-from-holding-list'?: boolean
/** Current working folder */
cwd?: string
}
type ExternalModule = string
// region custom
type CustomRedirectResponse =
| RedirectResponse
/** Handles response through send */
| [ ReadableStream | string | object ]
/** Handles response through send */
| [ ReadableStream | string | object, SendOptions ]
interface CustomMapping extends BaseMapping {
custom:
| ExternalModule
| [string]
| [object]
| [string, SendOptions]
| [object, SendOptions]
| ((request: IncomingMessage, response: ServerResponse, ...capturedGroups: string[]) => CustomRedirectResponse | Promise<CustomRedirectResponse>)
}
// endregion custom
// region file
interface ReadStreamOptions {
/** Start position in the file */
start: number
/** End position in the file, inclusive */
end: number
}
interface CustomFileSystemStat {
/** Returns true if the path is a directory */
isDirectory: () => boolean
/** Returns file size */
size: number
/** Returns file modification time */
mtime: Date
}
interface CustomFileSystem {
/** Reads directory content */
readdir: (folderPath: string) => Promise<string[]>
/** Gets path stat */
stat: (filePath: string) => Promise<CustomFileSystemStat>
createReadStream: (filePath: string, options?: ReadStreamOptions) => Promise<ReadableStream>
}
interface PunycacheOptions {
/** Time to live in the cache, expressed in milliseconds (default: Number.POSITIVE_INFINITY) */
ttl?: number
/** Maximum number of keys to be stored in the cache (default: Number.POSITIVE_INFINITY) */
max?: number
/** Cache replacement policy (default: lru) */
policy?:
/** Least Recently Used (each get / set updates used timestamp) */
| 'lru'
/** Least Frequently Used (each get / set increments usage frequency) */
| 'lfu'
}
interface FileMapping extends BaseMapping {
/** Path to the file to be served (may contain capturing groups placeholders such as $1) */
file: string
/** Dictionary indexed by file extension that overrides mime type resolution */
'mime-types'?: { [key: string]: string }
/** Configures caching strategy, see documentation */
'caching-strategy'?: 'modified' | number
/** Custom file system implementation */
'custom-file-system'?: ExternalModule | CustomFileSystem
/** Cache file system information for performance (disabled by default) */
'static'?: boolean | PunycacheOptions
}
// endregion file
type Headers = { [key in string]?: string | string[]}
// region status
interface StatusMapping extends BaseMapping {
/** HTTP status code for the response */
status: number
/** Additional headers */
headers?: Headers
}
// endregion status
/** REserve configuration information */
interface IConfiguration {
/** Dictionary of handlers indexed by their prefix */
readonly handlers: { [key in string]?: Handler }
/** List of active mappings */
readonly mappings: Mapping[]
readonly protocol: 'http' | 'https'
/** HTTP/2 is enabled */
readonly http2: boolean
/** Validate and update the list of active mappings */
setMappings: (mappings: Mapping[], request: IncomingMessage, timeout?: number) => Promise<void>
/** Dispatch a request internally */
dispatch: (request: IncomingMessage, response: ServerResponse) => Promise<void>
}
// region url
interface RequestSummary {
/** Request method */
method: string
/** Request URL */
url: string
/** Request headers */
headers: Headers
}
interface ForwardRequestContext {
/** REserve configuration information */
configuration: IConfiguration
/** Placeholder allocated to transmit additional information to the response */
context: object
/** Mapping being executed */
mapping: UrlMapping
/** Current mapping's regular expression match */
match: RegExpMatchArray
/** Request description (may be changed) */
request: RequestSummary
/** Incoming request object */
incoming: IncomingMessage
}
interface ForwardResponseContext {
/** REserve configuration information */
configuration: IConfiguration
/** Placeholder allocated to transmit additional information to the response */
context: object
/** Mapping being executed */
mapping: UrlMapping
/** Current mapping's regular expression match */
match: RegExpMatchArray
/** Request description */
request: RequestSummary
/** Response status code */
statusCode: number
/** Response headers */
headers: Headers
}
interface UrlMapping extends BaseMapping {
/** URL to redirect to (may contain capturing groups placeholders such as $1) */
url: string
/** Convert secure cookies to unsecure one (useful when switching between http and https) */
'unsecure-cookies'?: boolean
/** Hook before forwarding the request */
'forward-request'?: ExternalModule | ((context: ForwardRequestContext) => Promise<void>)
/** Hook before forwarding the response */
'forward-response'?: ExternalModule | ((context: ForwardResponseContext) => Promise<RedirectResponse>)
/** Ignore unverifiable SSL certificates */
'ignore-unverifiable-certificate'?: boolean
/** Converts 'location' header from relative to absolute */
'absolute-location'?: boolean
}
// endregion url
// region use
interface UseMapping extends BaseMapping {
use: ExternalModule | ((options?: object) => ((request: IncomingMessage, response: ServerResponse, next: (err: Error) => void) => void))
options?: object
}
// endregion use
// region helpers
/** Listen to server events and log them */
function log (server: Server, verbose?: boolean): Server
/** Interpolation helper */
function interpolate (match: RegExpMatchArray, pattern: string): string
function interpolate (match: RegExpMatchArray, pattern: object): object
interface BodyOptions {
/** Ignores response's Content-Length header */
ignoreContentLength?: boolean
}
type BodyResult = Promise<Buffer | string | object> & {
/** Returns the body as a Buffer */
buffer: () => Promise<Buffer>
/** Returns the body as a string */
text: () => Promise<string>
/** Returns the body as a parsed JSON */
json: () => Promise<any>
}
/** Body deserialization helper */
function body (request: IncomingMessage, options?: BodyOptions): BodyResult
interface SendOptions {
/** Status code (defaulted to 200) */
statusCode?: number
/** Header (might override 'content-type' and 'content-length') */
headers?: Headers
/** Do not send body (only headers) */
noBody?: boolean
}
/** Response sending helper */
function send (response: ServerResponse, data: ReadableStream, options?: SendOptions): Promise<void>
function send (response: ServerResponse, data?: string | object, options?: SendOptions): void
/** Response capturing helper (check documentation) */
function capture (response: ServerResponse, stream: WritableStream): Promise<void>
interface PunycacheCache {
/** Sets the value in the cache */
set (key: string, value: any): void
/** Gets a value from the cache */
get (key: string): any
/** Deletes a value from the cache */
del (key: string): void
/** Returns the list of keys in the cache */
keys (): string[]
}
/** Cache factory */
function punycache (options?: PunycacheOptions): PunycacheCache
// endregion helpers
interface SSLSettings {
/** a relative or absolute path to the certificate file */
cert: string
/** a relative or absolute path to the key file */
key: string
}
type JavaScriptType = 'boolean' | 'number' | 'string' | 'object' | 'function';
interface PropertySchema {
type?: JavaScriptType
types?: JavaScriptType[]
defaultValue?: boolean | number | string | object | Function
}
interface RedirectContext {
/** REserve configuration information */
configuration: IConfiguration
/** Mapping being executed */
mapping: BaseMapping
/** Current mapping's regular expression match */
match: RegExpMatchArray
/** URL to process (placeholders are substitued) */
redirect: string
/** Current request */
request: IncomingMessage
/** Current response */
response: ServerResponse
}
interface Handler {
/** Handler schema, used to validate properties */
readonly schema?: { [key: string]: string | string[] | PropertySchema }
/** When specified, restricts which methods it applies to */
readonly method?: string
/** Validation function */
readonly validate?: (mapping: BaseMapping, configuration: IConfiguration) => void
/** Handler's implementation */
readonly redirect: (context: RedirectContext) => Promise<RedirectResponse>
}
type Handlers = { [key in string]?: Handler }
type Listener = ExternalModule | ServerListener
type Mapping = BaseMapping | CustomMapping | FileMapping | StatusMapping | UrlMapping | UseMapping
interface Configuration {
/** Used to set the host parameter when calling http(s) server's listen */
hostname?: string
/** Used to set the port parameter when calling http(s) server's listen, use 0 to automatically allocates a free port */
port?: number
/** Limits the number of internal redirections (defaulted to 10) */
'max-redirect'?: number
/** Certificate information when building an https server */
ssl?: SSLSettings
/** Allocates an HTTP/2 server when set to true */
http2?: boolean
/** Additional server creation options (not validated) being passed to the appropriate native API */
httpOptions?: object
/** Mapping associating a handler prefix to a handler definition */
handlers?: Handlers
/** List of handlers to be executed upon server creation (see created event) */
listeners?: Listener[]
/** List of mappings to be used by the server */
mappings: Mapping[]
}
/** Reads and validate JSON configuration file */
function read (filename: string): Promise<Configuration>
/** Validate configuration */
function check (configuration: Configuration): Promise<Configuration>
type ServerEventName =
/** Emitted after the HTTP(S) server is created and before it accepts requests */
| 'created'
/** The server is listening and ready to receive requests */
| 'ready'
/** New request received */
| 'incoming'
/** An error occurred */
| 'error'
/** Request will be processed by a handler */
| 'redirecting'
/** Request is fully processed */
| 'redirected'
/** Request was aborted */
| 'aborted'
/** Request was closed */
| 'closed'
type ServerEventCommon = {
method: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'OPTIONS' | 'TRACE' | 'PATCH' | string
/** Before URL normalization */
incomingUrl: string
/** After URL normalization */
url: string
/** Request headers */
headers: Headers
/** When the request was received by REserve */
start: Date
/** Request unique ID (this ID is internal to REserve) */
id: number
/** Internal request (generated with IConfiguration.dispatch) */
internal: boolean
}
type ServerEventCreated = {
eventName: 'created'
server: HttpServer | HttpsServer | Http2Server
configuration: IConfiguration
};
type ServerEventReady = {
eventName: 'ready'
/** URL to connect to the server */
url: string
/** Configured or allocated port */
port: number
/** HTTP/2 is enabled */
http2 : boolean
}
type ServerEventIncoming = {
eventName: 'incoming'
} & ServerEventCommon
type ServerEventError = {
eventName: 'error'
error: any
} & ServerEventCommon
type ServerEventRedirecting = {
eventName: 'redirecting'
/** Handler prefix */
type: string
/** Redirection value */
redirect: string | number
} & ServerEventCommon
type ServerEventRedirected = {
eventName: 'redirected'
/** When the request was fully processed */
end: Date
/** Comparison of end - start (not a high resolution timer) */
timeSpent: number
/** Response status code */
statusCode: number
} & ServerEventCommon
type ServerEventAborted = {
eventName: 'aborted'
} & ServerEventCommon
type ServerEventClosed = {
eventName: 'closed'
} & ServerEventCommon
type ServerEvent<eventName = unknown> = eventName extends 'created'
? ServerEventCreated
: eventName extends 'ready'
? ServerEventReady
: eventName extends 'incoming'
? ServerEventIncoming
: eventName extends 'error'
? ServerEventError
: eventName extends 'redirecting'
? ServerEventRedirecting
: eventName extends 'redirected'
? ServerEventRedirected
: eventName extends 'aborted'
? ServerEventAborted
: eventName extends 'closed'
? ServerEventClosed
: ServerEventCreated | ServerEventReady | ServerEventIncoming | ServerEventError | ServerEventRedirecting | ServerEventRedirected | ServerEventAborted | ServerEventClosed
type ServerListener<EventName = ServerEventName> = (event: ServerEvent<EventName>) => void
type ServerCloseOptions = {
/** If set, waits up to the timeout (ms) for the active requests to terminate */
timeout?: number
/** If set, terminate the active requests (after the timeout if specified) */
force?: true
}
interface Server {
/** Register listener for the given event */
on: <EventName extends ServerEventName>(eventName: EventName, listener: ServerListener<EventName>) => Server
/** Terminate the server */
close: (options?: ServerCloseOptions) => Promise<void>
}
/** Validate configuration, allocate a server and start listening */
function serve (configuration: Configuration): Server
class Response extends ServerResponse {
constructor(request?: Request)
/** Waits for the response to be completed */
waitForFinish: () => Promise<void>
/** Checks if the response is still in the initial state */
isInitial: () => boolean
/** Sets the response' writes asynchronous */
setAsynchronous: () => void
}
type MockedRequestDefinition = {
/** Request method (defaulted to 'GET') */
method?: string,
/** Request URL */
url: string,
/** Request headers */
headers?: Headers,
/** Request body */
body?: string,
/** Additional properties (directly set on the request instance) */
properties?: object
}
interface MockServer extends Server {
/** Simulate request and generates a response */
request: ((method: string, url: string) => Promise<Response>) &
((method: string, url: string, headers: Headers) => Promise<Response>) &
((method: string, url: string, headers: Headers, body: string) => Promise<Response>) &
((method: string, url: string, headers: Headers, body: string, properties: object) => Promise<Response>) &
((definition: MockedRequestDefinition) => Promise<Response>)
}
/** Validate configuration, simulate a server */
function mock (configuration: Configuration, mockedHandlers?: Handlers): MockServer
}