aws-crt
Version:
NodeJS bindings to the aws-c-* libraries
418 lines • 13.9 kB
JavaScript
"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