File Explorer

/var/runtime/node_modules/@aws-sdk/node_modules/aws-crt/lib/browser

This explorer reads the filesystem of the server it runs on, so /workspace/user isn't present here. Browsing and the terminal still work against this server's own disk from /.

http.ts18.7 KB · 641 lines
/* * 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 {    CommonHttpProxyOptions,    HttpHeader,    HttpHeaders as CommonHttpHeaders,    HttpProxyAuthenticationType,    HttpClientConnectionConnected,    HttpClientConnectionError,    HttpClientConnectionClosed,    HttpStreamComplete,    HttpStreamData,    HttpStreamError} from '../common/http';export { HttpHeader, HttpProxyAuthenticationType } from '../common/http';import { BufferedEventEmitter } from '../common/event';import { CrtError } from './error';import * as axios from "axios";import { ClientBootstrap, InputStream, SocketOptions, TlsConnectionOptions } from './io';import { fromUtf8 } from '@aws-sdk/util-utf8-browser'; /** * A collection of HTTP headers * * @category HTTP */export class HttpHeaders implements CommonHttpHeaders {    // Map from "header": [["HeAdEr", "value1"], ["HEADER", "value2"], ["header", "value3"]]    private headers: { [index: string]: [HttpHeader] } = {};     /** Construct from a collection of [name, value] pairs     *     * @param headers list of HttpHeader values to seat in this object     */    constructor(headers: HttpHeader[] = []) {        for (const header of headers) {            this.add(header[0], header[1]);        }    }     /**     * Fetches the total length of all headers     *     * @returns the total length of all headers     */    get length(): number {        let length = 0;        for (let key in this.headers) {            length += this.headers[key].length;        }        return length;    }     /**     * Add a name/value pair     * @param name The header name     * @param value The header value    */    add(name: string, value: string) {        let values = this.headers[name.toLowerCase()];        if (values) {            values.push([name, value]);        } else {            this.headers[name.toLowerCase()] = [[name, value]];        }    }     /**     * Set a name/value pair, replacing any existing values for the name     * @param name - The header name     * @param value - The header value    */    set(name: string, value: string) {        this.headers[name.toLowerCase()] = [[name, value]];    }     /**     * Get the list of values for the given name     * @param name - The header name to look for     * @return List of values, or empty list if none exist     */    get_values(name: string) {        const values = [];        const values_list = this.headers[name.toLowerCase()] || [];        for (const entry of values_list) {            values.push(entry[1]);        }        return values;    }     /**     * Gets the first value for the given name, ignoring any additional values     * @param name - The header name to look for     * @param default_value - Value returned if no values are found for the given name     * @return The first header value, or default if no values exist     */    get(name: string, default_value: string = "") {        const values = this.headers[name.toLowerCase()];        if (!values) {            return default_value;        }        return values[0][1] || default_value;    }     /**     * Removes all values for the given name     * @param name - The header to remove all values for     */    remove(name: string) {        delete this.headers[name.toLowerCase()];    }     /**     * Removes a specific name/value pair     * @param name - The header name to remove     * @param value - The header value to remove     */    remove_value(name: string, value: string) {        const key = name.toLowerCase();         let values = this.headers[key];        for (let idx = 0; idx < values.length; ++idx) {            const entry = values[idx];            if (entry[1] === value) {                if (values.length === 1) {                    delete this.headers[key];                } else {                    delete values[idx];                }                return;            }        }    }     /** Clears the entire header set */    clear() {        this.headers = {};    }     /**     * Iterator. Allows for:     * let headers = new HttpHeaders();     * ...     * for (const header of headers) { }    */    *[Symbol.iterator]() {        for (const key in this.headers) {            const values = this.headers[key];            for (let entry of values) {                yield entry;            }        }    }     /** @internal */    _flatten(): HttpHeader[] {        let flattened = [];        for (const pair of this) {            flattened.push(pair);        }        return flattened;    }} /** * Options used when connecting to an HTTP endpoint via a proxy * * @category HTTP */export class HttpProxyOptions extends CommonHttpProxyOptions {} /** * Represents a request to a web server from a client * * @category HTTP */export class HttpRequest {     /**     * Constructor for the HttpRequest class     *     * @param method The verb to use for the request (i.e. GET, POST, PUT, DELETE, HEAD)     * @param path The URI of the request     * @param headers Additional custom headers to send to the server     * @param body The request body, in the case of a POST or PUT request     */    constructor(        public method: string,        public path: string,        public headers = new HttpHeaders(),        public body?: InputStream) {    }} /** * Represents an HTTP connection from a client to a server * * @category HTTP */export class HttpClientConnection extends BufferedEventEmitter {    public _axios: any;    private axios_options: axios.AxiosRequestConfig;    protected bootstrap: ClientBootstrap | undefined;    protected socket_options?: SocketOptions;    protected tls_options?: TlsConnectionOptions;    protected proxy_options?: HttpProxyOptions;     /**     * Http connection constructor, signature synced to native version for compatibility     *     * @param bootstrap - (native only) leave undefined     * @param host_name - endpoint to connection with     * @param port - port to connect to     * @param socketOptions - (native only) leave undefined     * @param tlsOptions - instantiate for TLS, but actual value is unused in browse implementation     * @param proxyOptions - options to control proxy usage when establishing the connection     */    constructor(        bootstrap: ClientBootstrap | undefined,        host_name: string,        port: number,        socketOptions?: SocketOptions,        tlsOptions?: TlsConnectionOptions,        proxyOptions?: HttpProxyOptions,    ) {        super();        this.cork();         this.bootstrap = bootstrap;        this.socket_options = socketOptions;        this.tls_options = tlsOptions;        this.proxy_options = proxyOptions;        const scheme = (this.tls_options || port === 443) ? 'https' : 'http'         this.axios_options = {            baseURL: `${scheme}://${host_name}:${port}/`        };         if (this.proxy_options) {            this.axios_options.proxy = {                host: this.proxy_options.host_name,                port: this.proxy_options.port,            };             if (this.proxy_options.auth_method == HttpProxyAuthenticationType.Basic) {                this.axios_options.proxy.auth = {                    username: this.proxy_options.auth_username || "",                    password: this.proxy_options.auth_password || "",                };            }        }        this._axios = axios.default.create(this.axios_options);        setTimeout(() => {            this.emit('connect');        }, 0);    }     /**     * 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;     // Override to allow uncorking on ready    on(event: string | symbol, listener: (...args: any[]) => void): this {        super.on(event, listener);        if (event == 'connect') {            setTimeout(() => {                this.uncork();            }, 0);        }        return this;    }     /**     * Make a client initiated request to this connection.     * @param request - The HttpRequest to attempt on this connection     * @returns A new stream that will deliver events for the request     */    request(request: HttpRequest) {        return stream_request(this, request);    }     /**     * Ends the connection     */    close() {        this.emit('close');        this._axios = undefined;    }} function stream_request(connection: HttpClientConnection, request: HttpRequest) {    if (request == null || request == undefined) {        throw new CrtError("HttpClientConnection stream_request: request not defined");    }     const _to_object = (headers: HttpHeaders) => {        // browsers refuse to let users configure host or user-agent        const forbidden_headers = ['host', 'user-agent'];        let obj: { [index: string]: string } = {};        for (const header of headers) {            if (forbidden_headers.indexOf(header[0].toLowerCase()) != -1) {                continue;            }            obj[header[0]] = headers.get(header[0]);        }        return obj;    }    let body = (request.body) ? (request.body as InputStream).data : undefined;    let stream = HttpClientStream._create(connection);    stream.connection._axios.request({        url: request.path,        method: request.method.toLowerCase(),        headers: _to_object(request.headers),        body: body    }).then((response: any) => {        stream._on_response(response);    }).catch((error: any) => {        stream._on_error(error);    });    return stream;}  /** * 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; /** * Represents a single http message exchange (request/response) in HTTP. * * NOTE: Binding either the ready or response event will uncork any buffered events and start * event delivery * * @category HTTP */export class HttpClientStream extends BufferedEventEmitter {    private response_status_code?: number;     private constructor(readonly connection: HttpClientConnection) {        super();        this.cork();    }     /**     * 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;    }     /**     * 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() {        setTimeout(() => {            this.uncork();        }, 0);    }     /**     * 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';     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: string | symbol, listener: (...args: any[]) => void): this {        return super.on(event, listener);    }     // Private helpers for stream_request()    /** @internal */    static _create(connection: HttpClientConnection) {        return new HttpClientStream(connection);    }     // Convert axios' single response into a series of events    /** @internal */    _on_response(response: any) {        this.response_status_code = response.status;        let headers = new HttpHeaders();        for (let header in response.headers) {            headers.add(header, response.headers[header]);        }        this.emit('response', this.response_status_code, headers);        let data = response.data;        if (data && !(data instanceof ArrayBuffer)) {            data = fromUtf8(data.toString());        }        this.emit('data', data);        this.emit('end');    }     // Gather as much information as possible from the axios error    // and pass it on to the user    /** @internal */    _on_error(error: any) {        let info = "";        if (error.response) {            this.response_status_code = error.response.status;            info += `status_code=${error.response.status}`;            if (error.response.headers) {                info += ` headers=${JSON.stringify(error.response.headers)}`;            }            if (error.response.data) {                info += ` data=${error.response.data}`;            }        } else {            info = "No response from server";        }         this.connection.close();        this.emit('error', new Error(`msg=${error.message}, connection=${JSON.stringify(this.connection)}, info=${info}`));    }} interface PendingRequest {    resolve: (connection: HttpClientConnection) => void;    reject: (error: CrtError) => void;} /** * Creates, manages, and vends connections to a given host/port endpoint * * @category HTTP */export class HttpClientConnectionManager {    private pending_connections = new Set<HttpClientConnection>();    private live_connections = new Set<HttpClientConnection>();    private free_connections: HttpClientConnection[] = [];    private pending_requests: PendingRequest[] = [];      /**     * Constructor for the HttpClientConnectionManager class.  Signature stays in sync with native implementation     * for compatibility purposes (leads to some useless params)     *     * @param bootstrap - (native only) leave undefined     * @param host - endpoint to pool connections for     * @param port - port to connect to     * @param max_connections - maximum allowed connection count     * @param initial_window_size - (native only) leave as zero     * @param socket_options - (native only) leave null     * @param tls_opts - if not null TLS will be used, otherwise plain http will be used     * @param proxy_options - configuration for establishing connections through a proxy     */    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    ) {     }     private remove(connection: HttpClientConnection) {        this.pending_connections.delete(connection);        this.live_connections.delete(connection);        const free_idx = this.free_connections.indexOf(connection);        if (free_idx != -1) {            this.free_connections.splice(free_idx, 1);        }    }     private resolve(connection: HttpClientConnection) {        const request = this.pending_requests.shift();        if (request) {            request.resolve(connection);        } else {            this.free_connections.push(connection);        }    }     private reject(error: CrtError) {        const request = this.pending_requests.shift();        if (request) {            request.reject(error);        }    }     private pump() {        if (this.pending_requests.length == 0) {            return;        }        // Try to service the request with a free connection        {            let connection = this.free_connections.pop();            if (connection) {                return this.resolve(connection);            }        }         // If there's no more room, nothing can be resolved right now        if ((this.live_connections.size + this.pending_connections.size) == this.max_connections) {            return;        }         // There's room, create a new connection        let connection = new HttpClientConnection(            new ClientBootstrap(),            this.host,            this.port,            this.socket_options,            this.tls_opts,            this.proxy_options);        this.pending_connections.add(connection);        const on_connect = () => {            this.pending_connections.delete(connection);            this.live_connections.add(connection);            this.free_connections.push(connection);            this.resolve(connection);        }        const on_error = (error: any) => {            if (this.pending_connections.has(connection)) {                // Connection never connected, error it out                return this.reject(new CrtError(error));            }            // If the connection errors after use, get it out of rotation and replace it            this.remove(connection);            this.pump();        }        const on_close = () => {            this.remove(connection);            this.pump();        }        connection.on('connect', on_connect);        connection.on('error', on_error);        connection.on('close', on_close);    }     /**     * 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) => {            this.pending_requests.push({                resolve: resolve,                reject: reject            });            this.pump();        });    }     /**     * Returns an unused connection to the pool     * @param connection - The connection to return    */    release(connection: HttpClientConnection) {        this.free_connections.push(connection);        this.pump();    }     /** Closes all connections and rejects all pending requests */    close() {        this.pending_requests.forEach((request) => {            request.reject(new CrtError('HttpClientConnectionManager shutting down'));        })    }}