@amadeus-it-group/kassette
Version:
Development server, used mainly for testing, which proxies requests and is able to easily manage local mocks.
445 lines (444 loc) • 19.8 kB
TypeScript
import { HookAPI } from '../mocking';
import { IProxyConnectAPI } from '../server/proxy';
import { ConsoleSpec } from '../logger';
import { HarKeyManager } from '../../lib/har/harFile';
/**
* The main working mode of the proxy. It can be defined globally through
* the {@link CLIConfigurationSpec.mode|mode} setting
* or per-request from the {@link ConfigurationSpec.hook|hook} method through
* {@link IMock.setMode|setMode}.
*
* @remarks
*
* The mode drives how {@link IMock.getPayloadAndFillResponse|getPayloadAndFillResponse} and {@link IMock.process|process}
* will behave. Here are the possible modes:
*
* - `manual`: don't do anything, leaving the responsibility to the user to call proper APIs to manage local files and/or
* backend querying, and response filling
*
* - `remote`: forward the request to the remote backend and never touch the local mock
*
* - `download`: get payload from remote backend by forwarding request, create the local mock from this payload, and fill
* the response with it
*
* - `local_or_remote`: if local mock exists, read it and fill the response with it, if local mock doesn't exist, do as for `remote` mode
*
* - `local_or_download`: if local mock exists, read it and fill the response with it, if local mock doesn't exist, do as for `download` mode
*
* - `local`: if local mock exists, read it and fill the response with it, if local mock doesn't exist, create a minimal payload with a 404 status
* code, do not persist it and fill the response with it
*
* @public
*/
export type Mode = 'local' | 'remote' | 'download' | 'local_or_remote' | 'local_or_download' | 'manual';
/**
* Specifies the mocks format to use. It can be defined globally through
* the {@link CLIConfigurationSpec.mocksFormat|mocksFormat} setting
* or per-request from the {@link ConfigurationSpec.hook|hook} method through
* {@link IMock.setMocksFormat|setMocksFormat}.
*
* @remarks
*
* Here are the two possible formats:
*
* - the `folder` format is specific to kassette and stores each request with its response in
* one folder containing up to 7 files:
* `data.json` (headers, status code and message of the response, as specified in {@link MockData}),
* `body.[ext]` (content of the body of the backend response),
* `input-request.json` (headers, method, URL and body file name of the input request, from client to proxy, for debug),
* `input-request-body.[ext]` (content of the body of the input request),
* `forwarded-request.json` (headers, method, URL, whether body was eventually a string or a Buffer and body file name
* of the forwarded request, from proxy to backend, for debug),
* `forwarded-request-body.[ext]` (the content of the body of the forwarded request),
* `checksum` (if a checksum was computed, the content generated to compute it).
* `[ext]` is an extension computed based on the actual type of the content in the file.
*
* - the `har` {@link http://www.softwareishard.com/blog/har-12-spec | format } is supported
* {@link http://www.softwareishard.com/blog/har-adopters/ | by several other tools} and can store
* multiple requests with their responses in a single (json-based) file. In this file, each
* request/response couple follows the structure specified in {@link HarFormatEntry}.
*
* @public
*/
export type MocksFormat = 'folder' | 'har';
/**
* Delay that will be used to send the response to the client
* when the data is taken from the local mock.
*
* @remarks
*
* It can be expressed either as a direct value (in milliseconds) or as the
* `'recorded'` string, which means to use the delay recorded
* in the local mock (in the {@link MockData.time|time} field).
*
* @public
*/
export type Delay = 'recorded' | number;
/**
* The mode describing how to process `CONNECT` requests. It can be defined globally through
* the {@link CLIConfigurationSpec.proxyConnectMode|proxyConnectMode} setting
* or per-request from the {@link ConfigurationSpec.onProxyConnect|onProxyConnect} method through
* {@link IProxyConnectAPI.setMode|setMode}.
*
* @remarks
*
* Here are the possible modes for `CONNECT` requests:
*
* - `intercept`: kassette answers with `HTTP/1.1 200 Connection established` and pretends to be the target server. If the browser then makes http or https requests on the socket after this `CONNECT` request, they will be processed by kassette and pass through the {@link ConfigurationSpec.hook|hook} method (if any). That's the default mode.
*
* - `forward`: kassette blindly connects to the remote destination {@link IProxyConnectAPI.hostname|hostname} and {@link IProxyConnectAPI.port|port} specified in the `CONNECT` request and forwards all data in both directions. This is what a normal proxy server is supposed to do. The destination hostname and port can optionally be modified in the {@link ConfigurationSpec.onProxyConnect|onProxyConnect} method through the {@link IProxyConnectAPI.setDestination|setDestination} method.
*
* - `close`: kassette simply closes the underlying socket. This is what servers which do not support the `CONNECT` method do.
*
* - `manual`: kassette does nothing special with the socket, leaving it in its current state. This setting allows to use any custom logic in the {@link ConfigurationSpec.onProxyConnect|onProxyConnect} callback. It only makes sense if the {@link ConfigurationSpec.onProxyConnect|onProxyConnect} callback is implemented, otherwise the browser will wait indefinitely for an answer.
*
* @public
*/
export type ProxyConnectMode = 'close' | 'intercept' | 'forward' | 'manual';
/**
* The set of possible properties defined through the CLI
* (it is reduced since it can't contain runtime values)
*
* @public
*/
export interface CLIConfigurationSpec {
/**
* If true, will simplify the logging output, logging only one line
* when the request is received but nothing else afterwards.
*/
readonly skipLog?: boolean;
/**
* The port on which the proxy should listen. Note that kassette accepts both http and https connections on this port.
*
* @remarks
*
* If the port is not available, it will fail and stop the program; try again with another, available port.
* If the port is set to 0, the proxy will listen on a random port
* (actually depends on the OS implementation): use the callback {@link ConfigurationSpec.onListen|onListen} to catch its value.
*/
readonly port?: number;
/**
* The hostname on which the proxy should listen.
* Uses `127.0.0.1` by default, which only allows local connections.
* To allow remote connections, use the ip address of the specific network interface that should be allowed to connect
* or the unspecified IPv4 (`0.0.0.0`) or IPv6 (`::`) address.
*
* @remarks
*
* Note that kassette has not been reviewed for security issues.
* It is intended to be used in a safe local/testing environment.
* Binding it to an open connection can result in compromising your
* computer or your network.
*/
readonly hostname?: string;
/**
* The default mode.
*
* @remarks
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setMode|mock.setMode}.
*/
readonly mode?: Mode;
/**
* The default mocks format.
*
* @remarks
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setMocksFormat|mock.setMocksFormat}.
*
* The default value of the global `mocksFormat` setting is `folder`, except in the following case:
* if the global {@link CLIConfigurationSpec.mocksHarFile|mocksHarFile} setting is defined
* and the global {@link CLIConfigurationSpec.mocksFolder|mocksFolder} setting is not defined,
* then the default value of the global `mocksFormat` setting is `har`.
*/
readonly mocksFormat?: MocksFormat;
/**
* Whether to save the content used to create a checksum when creating a new mock with a checksum.
*
* @remarks
*
* The default value of the global `saveChecksumContent` setting is `true`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveChecksumContent|mock.setSaveChecksumContent}.
*/
readonly saveChecksumContent?: boolean;
/**
* Whether to save {@link RequestTimings | detailed timings} when creating a new mock.
*
* @remarks
*
* The default value of the global `saveDetailedTimings` setting is `true`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveDetailedTimings|mock.setSaveDetailedTimings}.
*/
readonly saveDetailedTimings?: boolean;
/**
* Whether to save the input request data (headers, method, URL) when creating a new mock.
*
* @remarks
*
* The default value of the global `saveInputRequestData` setting is `true`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveInputRequestData|mock.setSaveInputRequestData}.
*/
readonly saveInputRequestData?: boolean;
/**
* Whether to save the content of the input request body when creating a new mock.
*
* @remarks
*
* The default value of the global `saveInputRequestBody` setting is `true`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveInputRequestBody|mock.setSaveInputRequestBody}.
*/
readonly saveInputRequestBody?: boolean;
/**
* Whether to save the forwarded request data (headers, method, URL) when creating a new mock.
*
* @remarks
*
* The default value of the global `saveForwardedRequestData` setting is `null`, which means
* `true` when `mocksFormat` is `folder` (for backward-compatibility), and
* `false` when `mocksFormat` is `har`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveForwardedRequestData|mock.setSaveForwardedRequestData}.
*/
readonly saveForwardedRequestData?: boolean | null;
/**
* Whether to save the forwarded request body when creating a new mock.
*
* @remarks
*
* The default value of the global `saveForwardedRequestData` setting is `null`, which means
* `true` when `mocksFormat` is `folder` (for backward-compatibility), and
* `false` when `mocksFormat` is `har`.
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setSaveForwardedRequestBody|mock.setSaveForwardedRequestBody}.
*/
readonly saveForwardedRequestBody?: boolean | null;
/**
* The default delay.
*
* @remarks
*
* It can be changed at request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setDelay|mock.setDelay}.
*/
readonly delay?: Delay;
/**
* When the {@link CLIConfigurationSpec.mocksFormat|mocks format} is 'folder', specifies
* the default root folder of all mocks, from which specific mocks paths will be resolved.
*
* @remarks
*
* It can be changed at a request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setMocksFolder|mock.setMocksFolder}.
*/
readonly mocksFolder?: string;
/**
* When the {@link CLIConfigurationSpec.mocksFormat|mocks format} is 'har', specifies
* the default har file to use. If the file name has the `.yml` or `.yaml` extension, the YAML format
* is used instead of the standard JSON format to read and write the file.
*
* @remarks
*
* It can be changed at a request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setMocksHarFile|mock.setMocksHarFile}.
*/
readonly mocksHarFile?: string;
/**
* Time in milliseconds during which a har file is kept in memory after its last usage.
*/
readonly harFileCacheTime?: number;
/**
* The URL of the remote backend, from which only the protocol, hostname and port are used.
* Can be left `null`, in which case anything leading to sending the request to the remote backend will trigger an exception.
* Can also contain the special `"*"` value, which means reading from the request the remote backend to target. This is useful when using kassette as a browser proxy.
*
* @remarks
*
* It can be changed at a request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setRemoteURL|mock.setRemoteURL}.
*/
readonly remoteURL?: string | null;
/**
* Default mode for `CONNECT` requests.
*
* @remarks
*
* It can be changed at a request level in the {@link ConfigurationSpec.onProxyConnect|onProxyConnect} method
* through {@link IProxyConnectAPI.setMode|setMode}.
*/
readonly proxyConnectMode?: ProxyConnectMode;
/**
* Path to a PEM-encoded CA (Certificate Authority) certificate and key file,
* created if it does not exist. If not provided, the certificate and key are
* generated but only kept in memory. This certificate and key are used as needed
* to sign certificates generated on the fly for any HTTPS connection intercepted
* by kassette.
*
* @remarks
*
* You can optionally import in the browser the TLS certificate from this
* file in order to remove the warning when connecting to HTTPS websites
* through kassette.
*/
readonly tlsCAKeyPath?: string | null;
/**
* Size in bits of generated RSA keys.
*/
readonly tlsKeySize?: number;
/**
* Enables http/2.0 protocol in the kassette server.
*/
readonly http2?: boolean;
}
/**
* Augments the CLI spec to add all pure runtime properties,
* that can be defined through the configuration file only
*
* @public
*/
export interface ConfigurationSpec extends CLIConfigurationSpec {
/**
* Callback called for every HTTP request that kassette receives (with the exception of `CONNECT` requests,
* which trigger the call of {@link ConfigurationSpec.onProxyConnect|onProxyConnect} instead).
* @param parameters - exposes the API to control how to process the request
*/
hook?(parameters: HookAPI): void | Promise<void>;
/**
* Function called to get or set the key of a mock in a har file, as explained in {@link HarKeyManager}.
*
* @remarks
*
* It can be changed at a request level in the {@link ConfigurationSpec.hook|hook} method
* through {@link IMock.setMocksHarKeyManager|mock.setMocksHarKeyManager}.
*/
readonly mocksHarKeyManager?: HarKeyManager;
/**
* Callback called when kassette receives a request with the
* {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT|HTTP CONNECT method},
* which usually happens when kassette is used as a browser proxy and the browser is trying to
* connect to a secure web site with the https protocol.
* @param parameters - exposes the API to control how to process the request
*/
onProxyConnect?(parameters: IProxyConnectAPI): void | Promise<void>;
/**
* Callback called when the proxy is started and listening.
*
* @param parameters - proxy parameters
* @param parameters.port - port on which the proxy is listening
*/
onListen?(parameters: {
port: number;
}): void;
/**
* Callback called when the proxy is programmatically closed (which can
* be done by using the callback returned from {@link runFromAPI})
*/
onExit?(): void;
/**
* Custom implementation of the {@link ConsoleSpec} interface, with methods
* {@link ConsoleSpec.log|log} and {@link ConsoleSpec.error|error},
* each receiving one single argument of any type.
* Useful to capture the logs of the application.
*/
readonly console?: ConsoleSpec;
/**
* Used only when the {@link IMock.mocksFormat|mocks format} is 'har',
* specifies a list of mime types that will attempt to parse the request/response body as JSON.
* If the list includes an empty string: '' and there is no mimeType set in the request, it will attempt to parse the body as JSON.
* This will only be applicable to request bodies if {@link IMock.saveInputRequestBody|saveInputRequestBody} is set to true
* Default value will be [] and will only be overridden by {@link IMock.setHarMimeTypesParseJson|setHarMimeTypesParseJson}
*/
readonly harMimeTypesParseJson?: string[];
}
/**
* The id of the source of the resolved value.
*
* @public
*/
export type ConfigurationPropertySource = 'cli' | 'file' | 'api' | 'default';
/**
* Contains the value and origin of a configuration property.
* @public
*/
export interface IConfigurationProperty<PropertyType> {
/** The resolved value of the property */
readonly value: PropertyType;
/** The id of the source of the resolved value of the property */
readonly origin: ConfigurationPropertySource;
}
export interface ConfigurationPropertySpec<PropertyType> {
/** The value of the property as defined from the API */
readonly apiValue: PropertyType | null | undefined;
/** The value of the property as defined from the CLI */
readonly cliValue: PropertyType | null | undefined;
/** The value of the property as defined from the file configuration CLI */
readonly fileValue: PropertyType | null | undefined;
/** The default value this property should have */
readonly defaultValue: PropertyType;
}
/**
* The resulting configuration that was merged from its different {@link ConfigurationPropertySource|sources}.
*
* @remarks
*
* It contains the path to the configuration file in `filePath`, and also for each property defined in {@link ConfigurationSpec},
* there is an {@link IConfigurationProperty} object describing the {@link IConfigurationProperty.origin|origin} of the property
* and its {@link IConfigurationProperty.value|value}.
*
* @public
*/
export type IMergedConfiguration = {
/**
* The path of the configuration file
*/
readonly filePath: string | null;
} & {
readonly [k in keyof ConfigurationSpec]-?: IConfigurationProperty<Required<ConfigurationSpec>[k]>;
};
/**
* Parameter of the {@link IConfigurationFile.getConfiguration|getConfiguration} function
* (which a kassette configuration file is supposed to export).
*
* @public
*/
export interface GetConfigurationProps {
/**
* If run from the CLI, this is the configuration coming from the CLI.
* Otherwise it is an empty object.
*/
cliConfiguration: CLIConfigurationSpec;
/**
* If run from the API, this is the configuration coming from the {@link runFromAPI} call.
* Otherwise it is an empty object.
*/
apiConfiguration: ConfigurationSpec;
/**
* If run from the API, this is the context value provided (if any) through {@link runFromAPI}.
* Otherwise it is undefined.
*/
context: any;
}
/**
* Interface that a kassette configuration file should export.
*
* @public
*/
export interface IConfigurationFile {
/**
* Function returning the configuration to be used by kassette.
* @param arg - contains information that can be used to build
* the configuration.
*/
getConfiguration(arg: GetConfigurationProps): ConfigurationSpec | Promise<ConfigurationSpec>;
}