UNPKG

twilio

Version:
282 lines (281 loc) 12.2 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; const axios_1 = __importDefault(require("axios")); const fs = __importStar(require("fs")); const https_proxy_agent_1 = __importDefault(require("https-proxy-agent")); const qs_1 = __importDefault(require("qs")); const https = __importStar(require("https")); const response_1 = __importDefault(require("../http/response")); const request_1 = __importDefault(require("../http/request")); const ValidationToken_1 = __importDefault(require("../jwt/validation/ValidationToken")); const DEFAULT_CONTENT_TYPE = "application/x-www-form-urlencoded"; const DEFAULT_TIMEOUT = 30000; const DEFAULT_INITIAL_RETRY_INTERVAL_MILLIS = 100; const DEFAULT_MAX_RETRY_DELAY = 3000; const DEFAULT_MAX_RETRIES = 3; const DEFAULT_MAX_SOCKETS = 20; const DEFAULT_MAX_FREE_SOCKETS = 5; const DEFAULT_MAX_TOTAL_SOCKETS = 100; function getExponentialBackoffResponseHandler(axios, opts) { const maxIntervalMillis = opts.maxIntervalMillis; const maxRetries = opts.maxRetries; return function (res) { const config = res.config; if (res.status !== 429) { return res; } const retryCount = (config.retryCount || 0) + 1; if (retryCount <= maxRetries) { config.retryCount = retryCount; const baseDelay = Math.min(maxIntervalMillis, DEFAULT_INITIAL_RETRY_INTERVAL_MILLIS * Math.pow(2, retryCount)); const delay = Math.floor(baseDelay * Math.random()); // Full jitter backoff return new Promise((resolve) => { setTimeout(() => resolve(axios(config)), delay); }); } return res; }; } class RequestClient { /** * Make http request * @param opts - The options passed to https.Agent * @param opts.timeout - https.Agent timeout option. Used as the socket timeout, AND as the default request timeout. * @param opts.keepAlive - https.Agent keepAlive option * @param opts.keepAliveMsecs - https.Agent keepAliveMsecs option * @param opts.maxSockets - https.Agent maxSockets option * @param opts.maxTotalSockets - https.Agent maxTotalSockets option * @param opts.maxFreeSockets - https.Agent maxFreeSockets option * @param opts.scheduling - https.Agent scheduling option * @param opts.autoRetry - Enable auto-retry requests with exponential backoff on 429 responses. Defaults to false. * @param opts.maxRetryDelay - Max retry delay in milliseconds for 429 Too Many Request response retries. Defaults to 3000. * @param opts.maxRetries - Max number of request retries for 429 Too Many Request responses. Defaults to 3. * @param opts.validationClient - Validation client for PKCV */ constructor(opts) { opts = opts || {}; this.defaultTimeout = opts.timeout || DEFAULT_TIMEOUT; this.autoRetry = opts.autoRetry || false; this.maxRetryDelay = opts.maxRetryDelay || DEFAULT_MAX_RETRY_DELAY; this.maxRetries = opts.maxRetries || DEFAULT_MAX_RETRIES; this.keepAlive = opts.keepAlive !== false; // construct an https agent let agentOpts = { timeout: this.defaultTimeout, keepAlive: this.keepAlive, keepAliveMsecs: opts.keepAliveMsecs, maxSockets: opts.maxSockets || DEFAULT_MAX_SOCKETS, // no of sockets open per host maxTotalSockets: opts.maxTotalSockets || DEFAULT_MAX_TOTAL_SOCKETS, // no of sockets open in total maxFreeSockets: opts.maxFreeSockets || DEFAULT_MAX_FREE_SOCKETS, // no of free sockets open per host scheduling: opts.scheduling, ca: opts.ca, }; // sets https agent CA bundle if defined in CA bundle filepath env variable if (process.env.TWILIO_CA_BUNDLE !== undefined) { if (agentOpts.ca === undefined) { agentOpts.ca = fs.readFileSync(process.env.TWILIO_CA_BUNDLE); } } let agent; if (process.env.HTTP_PROXY) { // Note: if process.env.HTTP_PROXY is set, we're not able to apply the given // socket timeout. See: https://github.com/TooTallNate/node-https-proxy-agent/pull/96 agent = (0, https_proxy_agent_1.default)(process.env.HTTP_PROXY); } else { agent = new https.Agent(agentOpts); } // construct an axios instance this.axios = axios_1.default.create(); this.axios.defaults.headers.post["Content-Type"] = DEFAULT_CONTENT_TYPE; this.axios.defaults.httpsAgent = agent; if (opts.autoRetry) { this.axios.interceptors.response.use(getExponentialBackoffResponseHandler(this.axios, { maxIntervalMillis: this.maxRetryDelay, maxRetries: this.maxRetries, })); } // if validation client is set, intercept the request using ValidationInterceptor if (opts.validationClient) { this.axios.interceptors.request.use(this.validationInterceptor(opts.validationClient)); } } /** * Make http request * @param opts - The options argument * @param opts.method - The http method * @param opts.uri - The request uri * @param opts.username - The username used for auth * @param opts.password - The password used for auth * @param opts.authStrategy - The authStrategy for API call * @param opts.headers - The request headers * @param opts.params - The request params * @param opts.data - The request data * @param opts.timeout - The request timeout in milliseconds (default 30000) * @param opts.allowRedirects - Should the client follow redirects * @param opts.forever - Set to true to use the forever-agent * @param opts.logLevel - Show debug logs */ async request(opts) { if (!opts.method) { throw new Error("http method is required"); } if (!opts.uri) { throw new Error("uri is required"); } var headers = opts.headers || {}; if (!headers.Connection && !headers.connection) headers.Connection = this.keepAlive ? "keep-alive" : "close"; let auth = undefined; if (opts.username && opts.password) { auth = Buffer.from(opts.username + ":" + opts.password).toString("base64"); headers.Authorization = "Basic " + auth; } else if (opts.authStrategy) { headers.Authorization = await opts.authStrategy.getAuthString(); } const options = { timeout: opts.timeout || this.defaultTimeout, maxRedirects: opts.allowRedirects ? 10 : 0, url: opts.uri, method: opts.method, headers: opts.headers, proxy: false, validateStatus: (status) => status >= 100 && status < 600, }; if (opts.data && options.headers) { if (options.headers["Content-Type"] === "application/x-www-form-urlencoded") { options.data = qs_1.default.stringify(opts.data, { arrayFormat: "repeat" }); } else if (options.headers["Content-Type"] === "application/json") { options.data = opts.data; } } if (opts.params) { options.params = opts.params; options.paramsSerializer = (params) => { return qs_1.default.stringify(params, { arrayFormat: "repeat" }); }; } const requestOptions = { method: opts.method, url: opts.uri, auth: auth, params: options.params, data: opts.data, headers: opts.headers, }; if (opts.logLevel === "debug") { this.logRequest(requestOptions); } const _this = this; this.lastResponse = undefined; this.lastRequest = new request_1.default(requestOptions); return this.axios(options) .then((response) => { if (opts.logLevel === "debug") { console.log(`response.statusCode: ${response.status}`); console.log(`response.headers: ${JSON.stringify(response.headers)}`); } _this.lastResponse = new response_1.default(response.status, response.data, response.headers); return { statusCode: response.status, body: response.data, headers: response.headers, }; }) .catch((error) => { _this.lastResponse = undefined; throw error; }); } filterLoggingHeaders(headers) { return Object.keys(headers).filter((header) => { return !"authorization".includes(header.toLowerCase()); }); } /** * ValidationInterceptor adds the Twilio-Client-Validation header to the request * @param validationClient - The validation client for PKCV * <p>Usage Example:</p> * ```javascript * import axios from "axios"; * // Initialize validation client with credentials * const validationClient = { * accountSid: "ACXXXXXXXXXXXXXXXX", * credentialSid: "CRXXXXXXXXXXXXXXXX", * signingKey: "SKXXXXXXXXXXXXXXXX", * privateKey: "private key", * algorithm: "PS256", * } * // construct an axios instance * const instance = axios.create(); * instance.interceptors.request.use( * ValidationInterceptor(opts.validationClient) * ); * ``` */ validationInterceptor(validationClient) { return function (config) { config.headers = config.headers || {}; try { config.headers["Twilio-Client-Validation"] = new ValidationToken_1.default(validationClient).fromHttpRequest(config); } catch (err) { console.log("Error creating Twilio-Client-Validation header:", err); throw err; } return config; }; } logRequest(options) { console.log("-- BEGIN Twilio API Request --"); console.log(`${options.method} ${options.url}`); if (options.params) { console.log("Querystring:"); console.log(options.params); } if (options.headers) { console.log("Headers:"); const filteredHeaderKeys = this.filterLoggingHeaders(options.headers); filteredHeaderKeys.forEach((header) => console.log(`${header}: ${options.headers?.header}`)); } console.log("-- END Twilio API Request --"); } } module.exports = RequestClient;