559 lines
17 KiB
TypeScript
559 lines
17 KiB
TypeScript
/*
|
|
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
* SPDX-License-Identifier: Apache-2.0.
|
|
*/
|
|
|
|
/**
|
|
*
|
|
* A module containing support for creating http connections and making requests on them.
|
|
*
|
|
* @packageDocumentation
|
|
* @module http
|
|
* @mergeTarget
|
|
*/
|
|
|
|
import crt_native from './binding';
|
|
import { NativeResource, NativeResourceMixin } from "./native_resource";
|
|
import { ResourceSafe } from '../common/resource_safety';
|
|
import { ClientBootstrap, SocketOptions, TlsConnectionOptions, InputStream } from './io';
|
|
import { CrtError } from './error';
|
|
import {
|
|
CommonHttpProxyOptions,
|
|
HttpProxyAuthenticationType,
|
|
HttpClientConnectionConnected,
|
|
HttpClientConnectionError,
|
|
HttpClientConnectionClosed,
|
|
HttpStreamComplete,
|
|
HttpStreamData,
|
|
HttpStreamError
|
|
} from '../common/http';
|
|
|
|
/** @internal */
|
|
export {HttpHeader} from '../common/http';
|
|
|
|
/** @internal */
|
|
export { HttpProxyAuthenticationType } from '../common/http';
|
|
|
|
import { BufferedEventEmitter } from '../common/event';
|
|
|
|
/**
|
|
* @category HTTP
|
|
*/
|
|
export type HttpHeaders = crt_native.HttpHeaders;
|
|
|
|
/**
|
|
* @category HTTP
|
|
*/
|
|
export const HttpHeaders = crt_native.HttpHeaders;
|
|
|
|
/** @internal */
|
|
type nativeHttpRequest = crt_native.HttpRequest;
|
|
/** @internal */
|
|
const nativeHttpRequest = crt_native.HttpRequest;
|
|
|
|
/**
|
|
* @category HTTP
|
|
*/
|
|
export class HttpRequest extends nativeHttpRequest {
|
|
constructor(method: string, path: string, headers?: HttpHeaders, body?: InputStream) {
|
|
super(method, path, headers, body?.native_handle());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for HTTP connections
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpConnection extends NativeResourceMixin(BufferedEventEmitter) implements ResourceSafe {
|
|
|
|
protected constructor(native_handle: any) {
|
|
super();
|
|
this._super(native_handle);
|
|
}
|
|
|
|
/**
|
|
* Close the connection.
|
|
* Shutdown is asynchronous. This call has no effect if the connection is already
|
|
* closing.
|
|
*/
|
|
close() {
|
|
crt_native.http_connection_close(this.native_handle());
|
|
}
|
|
|
|
/**
|
|
* Emitted when the connection is connected and ready to start streams
|
|
*
|
|
* @event
|
|
*/
|
|
static CONNECT = 'connect';
|
|
|
|
/**
|
|
* Emitted when an error occurs on the connection
|
|
*
|
|
* @event
|
|
*/
|
|
static ERROR = 'error';
|
|
|
|
/**
|
|
* Emitted when the connection has completed
|
|
*
|
|
* @event
|
|
*/
|
|
static CLOSE = 'close';
|
|
|
|
on(event: 'connect', listener: HttpClientConnectionConnected): this;
|
|
|
|
on(event: 'error', listener: HttpClientConnectionError): this;
|
|
|
|
on(event: 'close', listener: HttpClientConnectionClosed): this;
|
|
|
|
// Overridden to allow uncorking on ready
|
|
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
super.on(event, listener);
|
|
if (event == 'connect') {
|
|
process.nextTick(() => {
|
|
this.uncork();
|
|
})
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Proxy connection types.
|
|
*
|
|
* The original behavior was to make a tunneling connection if TLS was used, and a forwarding connection if it was not.
|
|
* There are legitimate use cases for plaintext tunneling connections, and so the implicit behavior has now
|
|
* been replaced by this setting, with a default that maps to the old behavior.
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export enum HttpProxyConnectionType {
|
|
/**
|
|
* (Default for backwards compatibility). If Tls options are supplied then the connection will be a tunneling
|
|
* one, otherwise it will be a forwarding one.
|
|
*/
|
|
Legacy = 0,
|
|
|
|
/**
|
|
* Establish a forwarding-based connection with the proxy. Tls is not allowed in this case.
|
|
*/
|
|
Forwarding = 1,
|
|
|
|
/**
|
|
* Establish a tunneling-based connection with the proxy.
|
|
*/
|
|
Tunneling = 2,
|
|
};
|
|
|
|
/**
|
|
* Proxy options for HTTP clients.
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpProxyOptions extends CommonHttpProxyOptions {
|
|
/**
|
|
*
|
|
* @param host_name Name of the proxy server to connect through
|
|
* @param port Port number of the proxy server to connect through
|
|
* @param auth_method Type of proxy authentication to use. Default is {@link HttpProxyAuthenticationType.None}
|
|
* @param auth_username Username to use when `auth_type` is {@link HttpProxyAuthenticationType.Basic}
|
|
* @param auth_password Password to use when `auth_type` is {@link HttpProxyAuthenticationType.Basic}
|
|
* @param tls_opts Optional TLS connection options for the connection to the proxy host.
|
|
* Must be distinct from the {@link TlsConnectionOptions} provided to
|
|
* the HTTP connection
|
|
* @param connection_type Optional Type of connection to make. If not specified,
|
|
* {@link HttpProxyConnectionType.Legacy} will be used.
|
|
*/
|
|
constructor(
|
|
host_name: string,
|
|
port: number,
|
|
auth_method = HttpProxyAuthenticationType.None,
|
|
auth_username?: string,
|
|
auth_password?: string,
|
|
public tls_opts?: TlsConnectionOptions,
|
|
public connection_type? : HttpProxyConnectionType
|
|
) {
|
|
super(host_name, port, auth_method, auth_username, auth_password);
|
|
}
|
|
|
|
/** @internal */
|
|
create_native_handle() {
|
|
return crt_native.http_proxy_options_new(
|
|
this.host_name,
|
|
this.port,
|
|
this.auth_method,
|
|
this.auth_username,
|
|
this.auth_password,
|
|
this.tls_opts ? this.tls_opts.native_handle() : undefined,
|
|
this.connection_type ? this.connection_type : HttpProxyConnectionType.Legacy
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an HTTP connection from a client to a server
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpClientConnection extends HttpConnection {
|
|
private _on_setup(native_handle: any, error_code: number) {
|
|
if (error_code) {
|
|
this.emit('error', new CrtError(error_code));
|
|
return;
|
|
}
|
|
|
|
this.emit('connect');
|
|
}
|
|
|
|
private _on_shutdown(native_handle: any, error_code: number) {
|
|
if (error_code) {
|
|
this.emit('error', new CrtError(error_code));
|
|
return;
|
|
}
|
|
this.emit('close');
|
|
}
|
|
|
|
/** Asynchronously establish a new HttpClientConnection.
|
|
* @param bootstrap Client bootstrap to use when initiating socket connection. Leave undefined to use the
|
|
* default system-wide bootstrap (recommended).
|
|
* @param host_name Host to connect to
|
|
* @param port Port to connect to on host
|
|
* @param socket_options Socket options
|
|
* @param tls_opts Optional TLS connection options
|
|
* @param proxy_options Optional proxy options
|
|
*/
|
|
constructor(
|
|
protected bootstrap: ClientBootstrap | undefined,
|
|
host_name: string,
|
|
port: number,
|
|
protected socket_options: SocketOptions,
|
|
protected tls_opts?: TlsConnectionOptions,
|
|
proxy_options?: HttpProxyOptions,
|
|
handle?: any) {
|
|
|
|
if (socket_options == null || socket_options == undefined) {
|
|
throw new CrtError("HttpClientConnection constructor: socket_options not defined");
|
|
}
|
|
|
|
super(handle
|
|
? handle
|
|
: crt_native.http_connection_new(
|
|
bootstrap != null ? bootstrap.native_handle() : null,
|
|
(handle: any, error_code: number) => {
|
|
this._on_setup(handle, error_code);
|
|
},
|
|
(handle: any, error_code: number) => {
|
|
this._on_shutdown(handle, error_code);
|
|
},
|
|
host_name,
|
|
port,
|
|
socket_options.native_handle(),
|
|
tls_opts ? tls_opts.native_handle() : undefined,
|
|
proxy_options ? proxy_options.create_native_handle() : undefined,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Create {@link HttpClientStream} to carry out the request/response exchange.
|
|
*
|
|
* NOTE: The stream sends no data until :meth:`HttpClientStream.activate()`
|
|
* is called. Call {@link HttpStream.activate} when you're ready for
|
|
* callbacks and events to fire.
|
|
* @param request - The HttpRequest to attempt on this connection
|
|
* @returns A new stream that will deliver events for the request
|
|
*/
|
|
request(request: HttpRequest) {
|
|
let stream: HttpClientStream;
|
|
const on_response_impl = (status_code: Number, headers: [string, string][]) => {
|
|
stream._on_response(status_code, headers);
|
|
}
|
|
|
|
const on_body_impl = (data: ArrayBuffer) => {
|
|
stream._on_body(data);
|
|
}
|
|
|
|
const on_complete_impl = (error_code: Number) => {
|
|
stream._on_complete(error_code);
|
|
}
|
|
const native_handle = crt_native.http_stream_new(
|
|
this.native_handle(),
|
|
request,
|
|
on_complete_impl,
|
|
on_response_impl,
|
|
on_body_impl
|
|
);
|
|
return stream = new HttpClientStream(
|
|
native_handle,
|
|
this,
|
|
request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents a single http message exchange (request/response) in HTTP/1.1. In H2, it may
|
|
* also represent a PUSH_PROMISE followed by the accompanying response.
|
|
*
|
|
* NOTE: Binding either the ready or response event will uncork any buffered events and start
|
|
* event delivery
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpStream extends NativeResourceMixin(BufferedEventEmitter) implements ResourceSafe {
|
|
protected constructor(
|
|
native_handle: any,
|
|
readonly connection: HttpConnection) {
|
|
super();
|
|
this._super(native_handle);
|
|
this.cork();
|
|
}
|
|
|
|
/**
|
|
* Begin sending the request.
|
|
*
|
|
* The stream does nothing until this is called. Call activate() when you
|
|
* are ready for its callbacks and events to fire.
|
|
*/
|
|
activate() {
|
|
crt_native.http_stream_activate(this.native_handle());
|
|
}
|
|
|
|
/**
|
|
* Closes and ends all communication on this stream. Called automatically after the 'end'
|
|
* event is delivered. Calling this manually is only necessary if you wish to terminate
|
|
* communication mid-request/response.
|
|
*/
|
|
close() {
|
|
crt_native.http_stream_close(this.native_handle());
|
|
}
|
|
|
|
/** @internal */
|
|
_on_body(data: ArrayBuffer) {
|
|
this.emit('data', data);
|
|
}
|
|
|
|
/** @internal */
|
|
_on_complete(error_code: Number) {
|
|
if (error_code) {
|
|
this.emit('error', new CrtError(error_code));
|
|
this.close();
|
|
return;
|
|
}
|
|
// schedule death after end is delivered
|
|
this.on('end', () => {
|
|
this.close();
|
|
})
|
|
this.emit('end');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Listener signature for event emitted from an {@link HttpClientStream} when inline headers are delivered while communicating over H2
|
|
*
|
|
* @param headers the set of headers
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export type HttpStreamHeaders = (headers: HttpHeaders) => void;
|
|
|
|
/**
|
|
* Listener signature for event emitted from an {@link HttpClientStream} when the http response headers have arrived.
|
|
*
|
|
* @param status_code http response status code
|
|
* @param headers the response's set of headers
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export type HttpStreamResponse = (status_code: number, headers: HttpHeaders) => void;
|
|
|
|
/**
|
|
* Stream that sends a request and receives a response.
|
|
*
|
|
* Create an HttpClientStream with {@link HttpClientConnection.request}.
|
|
*
|
|
* NOTE: The stream sends no data until {@link HttpStream.activate} is called.
|
|
* Call {@link HttpStream.activate} when you're ready for callbacks and events to fire.
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpClientStream extends HttpStream {
|
|
private response_status_code?: Number;
|
|
constructor(
|
|
native_handle: any,
|
|
connection: HttpClientConnection,
|
|
readonly request: HttpRequest) {
|
|
super(native_handle, connection);
|
|
}
|
|
|
|
/**
|
|
* HTTP status code returned from the server.
|
|
* @return Either the status code, or undefined if the server response has not arrived yet.
|
|
*/
|
|
status_code() {
|
|
return this.response_status_code;
|
|
}
|
|
|
|
/**
|
|
* Emitted when the http response headers have arrived.
|
|
*
|
|
* @event
|
|
*/
|
|
static RESPONSE = 'response';
|
|
|
|
/**
|
|
* Emitted when http response data is available.
|
|
*
|
|
* @event
|
|
*/
|
|
static DATA = 'data';
|
|
|
|
/**
|
|
* Emitted when an error occurs in stream processing
|
|
*
|
|
* @event
|
|
*/
|
|
static ERROR = 'error';
|
|
|
|
/**
|
|
* Emitted when the stream has completed
|
|
*
|
|
* @event
|
|
*/
|
|
static END = 'end';
|
|
|
|
/**
|
|
* Emitted when inline headers are delivered while communicating over H2
|
|
*
|
|
* @event
|
|
*/
|
|
static HEADERS = 'headers';
|
|
|
|
on(event: 'response', listener: HttpStreamResponse): this;
|
|
|
|
on(event: 'data', listener: HttpStreamData): this;
|
|
|
|
on(event: 'error', listener: HttpStreamError): this;
|
|
|
|
on(event: 'end', listener: HttpStreamComplete): this;
|
|
|
|
on(event: 'headers', listener: HttpStreamHeaders): this;
|
|
|
|
// Overridden to allow uncorking on ready and response
|
|
on(event: string | symbol, listener: (...args: any[]) => void): this {
|
|
super.on(event, listener);
|
|
if (event == 'response') {
|
|
process.nextTick(() => {
|
|
this.uncork();
|
|
})
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/** @internal */
|
|
_on_response(status_code: Number, header_array: [string, string][]) {
|
|
this.response_status_code = status_code;
|
|
let headers = new HttpHeaders(header_array);
|
|
this.emit('response', status_code, headers);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates, manages, and vends connections to a given host/port endpoint
|
|
*
|
|
* @category HTTP
|
|
*/
|
|
export class HttpClientConnectionManager extends NativeResource {
|
|
private connections = new Map<any, HttpClientConnection>();
|
|
|
|
/**
|
|
* @param bootstrap Client bootstrap to use when initiating socket connections. Leave undefined to use the
|
|
* default system-wide bootstrap (recommended).
|
|
* @param host Host to connect to
|
|
* @param port Port to connect to on host
|
|
* @param max_connections Maximum number of connections to pool
|
|
* @param initial_window_size Optional initial window size
|
|
* @param socket_options Socket options to use when initiating socket connections
|
|
* @param tls_opts Optional TLS connection options
|
|
* @param proxy_options Optional proxy options
|
|
*/
|
|
constructor(
|
|
readonly bootstrap: ClientBootstrap | undefined,
|
|
readonly host: string,
|
|
readonly port: number,
|
|
readonly max_connections: number,
|
|
readonly initial_window_size: number,
|
|
readonly socket_options: SocketOptions,
|
|
readonly tls_opts?: TlsConnectionOptions,
|
|
readonly proxy_options?: HttpProxyOptions,
|
|
) {
|
|
|
|
if (socket_options == null || socket_options == undefined) {
|
|
throw new CrtError("HttpClientConnectionManager constructor: socket_options not defined");
|
|
}
|
|
|
|
super(crt_native.http_connection_manager_new(
|
|
bootstrap != null ? bootstrap.native_handle() : null,
|
|
host,
|
|
port,
|
|
max_connections,
|
|
initial_window_size,
|
|
socket_options.native_handle(),
|
|
tls_opts ? tls_opts.native_handle() : undefined,
|
|
proxy_options ? proxy_options.create_native_handle() : undefined,
|
|
undefined /* on_shutdown */
|
|
));
|
|
}
|
|
|
|
/**
|
|
* Vends a connection from the pool
|
|
* @returns A promise that results in an HttpClientConnection. When done with the connection, return
|
|
* it via {@link release}
|
|
*/
|
|
acquire(): Promise<HttpClientConnection> {
|
|
return new Promise((resolve, reject) => {
|
|
// Only create 1 connection in JS/TS from each native connection
|
|
const on_acquired = (handle: any, error_code: number) => {
|
|
if (error_code) {
|
|
reject(new CrtError(error_code));
|
|
return;
|
|
}
|
|
let connection = this.connections.get(handle);
|
|
if (!connection) {
|
|
connection = new HttpClientConnection(
|
|
this.bootstrap,
|
|
this.host,
|
|
this.port,
|
|
this.socket_options,
|
|
this.tls_opts,
|
|
this.proxy_options,
|
|
handle
|
|
);
|
|
this.connections.set(handle, connection as HttpClientConnection);
|
|
connection.on('close', () => {
|
|
this.connections.delete(handle);
|
|
})
|
|
}
|
|
resolve(connection);
|
|
};
|
|
crt_native.http_connection_manager_acquire(this.native_handle(), on_acquired);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns an unused connection to the pool
|
|
* @param connection - The connection to return
|
|
*/
|
|
release(connection: HttpClientConnection) {
|
|
if (connection == null || connection == undefined) {
|
|
throw new CrtError("HttpClientConnectionManager release: connection not defined");
|
|
}
|
|
crt_native.http_connection_manager_release(this.native_handle(), connection.native_handle());
|
|
}
|
|
|
|
/** Closes all connections and rejects all pending requests */
|
|
close() {
|
|
crt_native.http_connection_manager_close(this.native_handle());
|
|
}
|
|
}
|