UNPKG

mockttp

Version:

Mock HTTP server for testing HTTP clients and stubbing webservices

526 lines (448 loc) 15.9 kB
import type * as stream from 'stream'; import type * as http from 'http'; import type { EventEmitter } from 'events'; export const DEFAULT_ADMIN_SERVER_PORT = 45454; export enum Method { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS } export enum RulePriority { FALLBACK = 0, DEFAULT = 1 } export interface Headers { // An arbitrary set of headers that are known to // only ever appear once (for valid requests). host?: string; 'content-length'?: string; 'content-type'?: string; 'user-agent'?: string; cookie?: string; ':method'?: string; ':scheme'?: string; ':authority'?: string; ':path'?: string; // In general there may be 0+ of any header [key: string]: undefined | string | string[]; } export interface Trailers { // 0+ of any trailer [key: string]: undefined | string | string[]; } export type RawHeaders = Array<[key: string, value: string]>; export type RawTrailers = RawHeaders; // Just a convenient alias // --- Terminology: --- // Hostname = String of IP or domain name // Host = String of hostname + optional port (if not default for protocol) // Destination = hostname + mandatory port as a structured object // N.b. IPv6 is only [bracketed] in place in URLs/headers, not elsewhere. export interface Destination { hostname: string; port: number; } export interface Request { id: string; matchedRuleId?: string; protocol: string; httpVersion: string; method: string; url: string; path: string; remoteIpAddress?: string; // Not set remotely with older servers or in some error cases remotePort?: number; // Not set remotely with older servers or in some error cases /** * The best guess at the target host + port of the request. This uses tunnelling metadata * wherever possible, or the headers if not. */ destination: Destination; headers: Headers; rawHeaders: RawHeaders; timingEvents: TimingEvents; tags: string[]; } export interface TlsConnectionEvent { remoteIpAddress?: string; // Can be unavailable in some error cases remotePort?: number; // Can be unavailable in some error cases tags: string[]; timingEvents: TlsTimingEvents; destination?: Destination; // Set for tunnelled requests only tlsMetadata: TlsSocketMetadata; } export interface TlsSocketMetadata { sniHostname?: string; clientAlpn?: string[]; ja3Fingerprint?: string; ja4Fingerprint?: string; } export interface TlsPassthroughEvent extends RawPassthroughEvent, TlsConnectionEvent { // Removes ambiguity of the two parent interface fields destination: Destination; remoteIpAddress: string; remotePort: number; timingEvents: TlsTimingEvents; } export interface TlsHandshakeFailure extends TlsConnectionEvent { failureCause: | 'closed' | 'reset' | 'cert-rejected' | 'no-shared-cipher' | 'handshake-timeout' | 'unknown'; timingEvents: TlsFailureTimingEvents; } export interface RawPassthroughEvent { id: string; destination: Destination; /** * The IP address of the remote client that initiated the connection. */ remoteIpAddress: string; /** * The port of the remote client that initiated the connection. */ remotePort: number; tags: string[]; timingEvents: ConnectionTimingEvents; } export interface RawPassthroughDataEvent { /** * The id of the passthrough tunnel. */ id: string; /** * The direction of the message, from the downstream perspective (received from the client, * or sent back to the client). */ direction: 'sent' | 'received'; /** * The contents of the message as a raw buffer. */ content: Uint8Array; /** * A high-precision floating-point monotonically increasing timestamp. * Comparable and precise, but not related to specific current time. * * To link this to the current time, compare it to `timingEvents.startTime`. */ eventTimestamp: number; } export interface ConnectionTimingEvents { /** * When the socket initially connected, in MS since the unix * epoch. */ startTime: number; /** * When the socket initially connected, equivalent to startTime. * * High-precision floating-point monotonically increasing timestamps. * Comparable and precise, but not related to specific current time. */ connectTimestamp: number; /** * When the outer tunnel (e.g. a preceeding CONNECT request/SOCKS * connection) was created, if there was one. */ tunnelTimestamp?: number; /** * When the connection was closed, if it has been closed. */ disconnectTimestamp?: number; } export interface TlsTimingEvents extends ConnectionTimingEvents { /** * When Mockttp's handshake for this connection was completed (if there * was one). This is not set for passed through connections. */ handshakeTimestamp?: number; } export interface TlsFailureTimingEvents extends TlsTimingEvents { /** * When the TLS connection failed. This may be due to a failed handshake * (in which case `handshakeTimestamp` will be undefined) or due to a * subsequent error which means the TLS connection was not usable (like * an immediate closure due to an async certificate rejection). */ failureTimestamp: number; } // Internal representation of an ongoing HTTP request whilst it's being processed export interface OngoingRequest extends Request, stream.Readable { body: OngoingBody; rawTrailers?: RawHeaders; } export interface OngoingBody { asStream: () => stream.Readable; asBuffer: () => Promise<Buffer>; asDecodedBuffer: () => Promise<Buffer>; asText: () => Promise<string>; asJson: () => Promise<object>; asFormData: () => Promise<{ [key: string]: string | string[] | undefined }>; } export interface CompletedBody { /** * The raw bytes of the response. If a content encoding was used, this is * the raw encoded data. */ buffer: Buffer; /** * The decoded bytes of the response. If no encoding was used, this is the * same as `.buffer`. The response is decoded and returned asynchronously * as a Promise. */ getDecodedBuffer(): Promise<Buffer | undefined>; /** * The contents of the response, decoded and parsed as a UTF-8 string. * The response is decoded and returned asynchronously as a Promise. */ getText(): Promise<string | undefined>; /** * The contents of the response, decoded, parsed as UTF-8 string, and * then parsed a JSON. The response is decoded and returned asynchronously * as a Promise. */ getJson(): Promise<object | undefined>; /** * The contents of the response, decoded, and then parsed automatically as * either one of the form encoding types (either URL-encoded or multipart), * determined automatically from the message content-type header. * * This method is convenient and offers a single mechanism to parse both * formats, but you may want to consider parsing on format explicitly with * the `getUrlEncodedFormData()` or `getMultipartFormData()` methods instead. * * After parsing & decoding, the result is returned asynchronously as a * Promise for a key-value(s) object. */ getFormData(): Promise<{ [key: string]: string | string[] | undefined } | undefined>; /** * The contents of the response, decoded, parsed as UTF-8 string, and then * parsed as URL-encoded form data. After parsing & decoding, the result is * returned asynchronously as a Promise for a key-value(s) object. */ getUrlEncodedFormData(): Promise<{ [key: string]: string | string[] | undefined } | undefined>; /** * The contents of the response, decoded, and then parsed as multi-part * form data. The response is result is returned asynchronously as a * Promise for an array of parts with their names, data and metadata. */ getMultipartFormData(): Promise<Array<{ name?: string, filename?: string, type?: string, data: Buffer }> | undefined>; } // Internal & external representation of an initiated (no body yet received) HTTP request. export type InitiatedRequest = Request; export interface AbortedRequest extends InitiatedRequest { error?: { name?: string; code?: string; message?: string; stack?: string; }; } // Internal & external representation of a fully completed HTTP request export interface CompletedRequest extends Request { body: CompletedBody; rawTrailers: RawTrailers; trailers: Trailers; } export interface TimingEvents { // Milliseconds since unix epoch startTime: number; // High-precision floating-point monotonically increasing timestamps. // Comparable and precise, but not related to specific current time. startTimestamp: number; // When the request was initially received bodyReceivedTimestamp?: number; // When the request body was fully received headersSentTimestamp?: number; // When the response headers were sent responseSentTimestamp?: number; // When the response was fully completed wsAcceptedTimestamp?: number; // When the websocket was accepted wsClosedTimestamp?: number; // When the websocket was closed abortedTimestamp?: number; // When the connected was aborted } export interface OngoingResponse extends http.ServerResponse { id: string; getHeaders(): Headers; getRawHeaders(): RawHeaders; body: OngoingBody; getRawTrailers(): RawTrailers; timingEvents: TimingEvents; tags: string[]; } export interface InitiatedResponse { id: string; statusCode: number; statusMessage: string; headers: Headers; rawHeaders: RawHeaders; timingEvents: TimingEvents; tags: string[]; } export interface BodyData { /** * The id of the request or response this data belongs to. */ id: string; /** * The contents of the chunk as a raw buffer. Note that this may be empty, * when the body is finishing, if it wasn't known to be finished at the * preceeding chunk of data. */ content: Uint8Array; /** * Indicates whether the body has completed or whether there's more coming. * This will not be set if the body is aborted incomplete - listen for abort * events separately to check for this. */ isEnded: boolean; /** * A high-precision floating-point monotonically increasing timestamp. * Comparable and precise, but not related to specific current time. * * To link this to the current time, compare it to `timingEvents.startTime`. */ eventTimestamp: number; } export interface CompletedResponse extends InitiatedResponse { body: CompletedBody; rawTrailers: RawTrailers; trailers: Trailers; } export interface WebSocketMessage { /** * The id of this websocket stream. This will match the id of the request, * the initial connection response, and any other WebSocket events for the * same connection stream. */ streamId: string; /** * Whether the message was sent by Mockttp, or received from a Mockttp client. */ direction: 'sent' | 'received'; /** * The contents of the message as a raw buffer. This is already decompressed, * if the WebSocket uses compression. */ content: Uint8Array; /** * Whether this is a string message or a raw binary data message. */ isBinary: boolean; /** * A high-precision floating-point monotonically increasing timestamp. * Comparable and precise, but not related to specific current time. * * To link this to the current time, compare it to `timingEvents.startTime`. */ eventTimestamp: number; timingEvents: TimingEvents; tags: string[]; } export interface WebSocketClose { /** * The id of this websocket stream. This will match the id of the request, * the initial connection response, and any other WebSocket events for the * same connection stream. */ streamId: string; /** * The close code of the shutdown. This is the close code that was received * from the remote client (either initiated remotely, or echoing our own sent * close frame). * * This may be undefined only if a close frame was received but did not contain * any close code. If no close frame was received before the connection was * lost (i.e. the connection was not cleanly closed) this event will not * fire at all, and an 'abort' event will fire instead. */ closeCode: number | undefined; /** * The close reason of the shutdown. */ closeReason: string; timingEvents: TimingEvents; tags: string[]; } /** * A client error event describes a request (or our best guess at parsing it), * that wasn't correctly completed, and the error response it received, or * 'aborted' if the connection was disconnected before we could respond. */ export interface ClientError { errorCode?: string; request: { id: string; timingEvents: TimingEvents; tags: string[]; // All of these are best guess, depending on what's parseable: protocol?: string; httpVersion: string; method?: string; url?: string; path?: string; destination?: Destination; headers: Headers; rawHeaders: RawHeaders; remoteIpAddress?: string; remotePort?: number; }; response: CompletedResponse | 'aborted'; } /** * An event fired from an individual rule during request processing. */ export interface RuleEvent<T = unknown> { requestId: string; ruleId: string; eventType: string; eventData: T; } /** * A mocked endpoint provides methods to see the current state of * a mock rule. */ export interface MockedEndpoint { id: string; /** * Get the requests that this endpoint has seen so far. * * This method returns a promise, which resolves with the requests seen * up until now, once all ongoing requests have terminated. The returned * lists are immutable, so won't change if more requests arrive in future. * Call `getSeenRequests` again later to get an updated list. * * Requests are included here once the response is completed, even if the request * itself failed, the responses failed or exceptions are thrown elsewhere. To * watch for errors or detailed response info, look at the various server.on(event) * methods. */ getSeenRequests(): Promise<CompletedRequest[]>; /** * Reports whether this endpoint is still pending: if it either hasn't seen the * specified number of requests (if one was specified e.g. with .twice()) * or if it hasn't seen at least one request, by default. * * This method returns a promise, which resolves with the result once all * ongoing requests have terminated. */ isPending(): Promise<boolean>; } export interface MockedEndpointData { id: string; explanation?: string; seenRequests: CompletedRequest[]; isPending: boolean; } export interface Explainable { explain(): string; } export interface ProxyEnvConfig { HTTP_PROXY: string; HTTPS_PROXY: string; } // A slightly weird one: this is necessary because we export types that inherit from EventEmitter, // so the docs include EventEmitter's methods, which @link to this type, that's otherwise not // defined in this module. Reexporting the values avoids warnings for that. export type defaultMaxListeners = typeof EventEmitter.defaultMaxListeners;