UNPKG

aws-crt

Version:

NodeJS bindings to the aws-c-* libraries

418 lines 13.9 kB
"use strict"; /* * Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const http_1 = require("../common/http"); var http_2 = require("../common/http"); exports.HttpProxyOptions = http_2.HttpProxyOptions; exports.HttpProxyAuthenticationType = http_2.HttpProxyAuthenticationType; const event_1 = require("../common/event"); const error_1 = require("./error"); const axios = __importStar(require("axios")); class HttpHeaders { /** Construct from a collection of [name, value] pairs */ constructor(headers = []) { // Map from "header": [["HeAdEr", "value1"], ["HEADER", "value2"], ["header", "value3"]] this.headers = {}; for (const header of headers) { this.add(header[0], header[1]); } } get length() { 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, value) { 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, value) { 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) { 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, default_value = "") { 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) { 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, value) { 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; } } } _flatten() { let flattened = []; for (const pair of this) { flattened.push(pair); } return flattened; } } exports.HttpHeaders = HttpHeaders; /** Represents a request to a web server from a client */ class HttpRequest { constructor( /** The verb to use for the request (i.e. GET, POST, PUT, DELETE, HEAD) */ method, /** The URI of the request */ path, /** Additional custom headers to send to the server */ headers = new HttpHeaders(), /** The request body, in the case of a POST or PUT request */ body) { this.method = method; this.path = path; this.headers = headers; this.body = body; } } exports.HttpRequest = HttpRequest; class HttpClientConnection extends event_1.BufferedEventEmitter { constructor(host_name, port, scheme, proxy_options) { super(); if (!scheme) { scheme = (port == 443) ? 'https' : 'http'; } let axios_options = { baseURL: `${scheme}://${host_name}:${port}/` }; if (proxy_options) { axios_options.proxy = { host: proxy_options.host_name, port: proxy_options.port, }; if (proxy_options.auth_method == http_1.HttpProxyAuthenticationType.Basic) { axios_options.proxy.auth = { username: proxy_options.auth_username || "", password: proxy_options.auth_password || "", }; } } this.axios = axios.default.create(axios_options); setTimeout(() => { this.emit('connect'); }, 0); } // Override to allow uncorking on ready on(event, listener) { 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) { return stream_request(this, request); } _on_end(stream) { this.emit('close'); } } exports.HttpClientConnection = HttpClientConnection; function stream_request(connection, request) { const _to_object = (headers) => { // browsers refuse to let users configure host or user-agent const forbidden_headers = ['host', 'user-agent']; let obj = {}; 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.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) => { stream._on_response(response); }).catch((error) => { stream._on_error(error); }); return stream; } /** * 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 */ class HttpClientStream extends event_1.BufferedEventEmitter { constructor(connection) { super(); this.connection = connection; this.encoder = new TextEncoder(); } /** * 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; } on(event, listener) { super.on(event, listener); if (event == 'ready' || event == 'response') { setTimeout(() => { this.uncork(); }, 0); } return this; } // Private helpers for stream_request() static _create(connection) { return new HttpClientStream(connection); } // Convert axios' single response into a series of events _on_response(response) { 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 = this.encoder.encode(data.toString()); } this.emit('data', data); this.emit('end'); this.connection._on_end(this); } // Gather as much information as possible from the axios error // and pass it on to the user _on_error(error) { let info = ""; if (error.response) { this.response_status_code = error.response.status; info += `status_code=${error.response.status}`; if (error.response.headers) { info += `headers=${error.response.headers}`; } if (error.response.data) { info += `data=${error.response.data}`; } } else { info = "No response from server"; } this.emit('error', new Error(`msg=${error.message}, XHR=${error.request}, info=${info}`)); } } exports.HttpClientStream = HttpClientStream; /** Creates, manages, and vends connections to a given host/port endpoint */ class HttpClientConnectionManager { constructor(host, port, max_connections) { this.host = host; this.port = port; this.max_connections = max_connections; this.pending_connections = new Set(); this.live_connections = new Set(); this.free_connections = []; this.pending_requests = []; } remove(connection) { 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); } } resolve(connection) { const request = this.pending_requests.shift(); if (request) { request.resolve(connection); } else { this.free_connections.push(connection); } } reject(error) { const request = this.pending_requests.shift(); if (request) { request.reject(error); } } 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(this.host, this.port); 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) => { if (this.pending_connections.has(connection)) { // Connection never connected, error it out return this.reject(new error_1.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() { 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) { this.free_connections.push(connection); this.pump(); } /** Closes all connections and rejects all pending requests */ close() { this.pending_requests.forEach((request) => { request.reject(new error_1.CrtError('HttpClientConnectionManager shutting down')); }); } } exports.HttpClientConnectionManager = HttpClientConnectionManager; //# sourceMappingURL=http.js.map