f5-conx-core
Version:
F5 SDK for JavaScript with Typescript type definitions
305 lines • 14.5 kB
JavaScript
/*
* Copyright 2020. F5 Networks, Inc. See End User License Agreement ("EULA") for
* license terms. Notwithstanding anything to the contrary in the EULA, Licensee
* may copy and modify this software product for its internal business purposes.
* Further, Licensee may upload, publish and distribute the modified version of
* the software product on devcentral.f5.com.
*/
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExtHttp = void 0;
const https_1 = __importDefault(require("https"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const events_1 = require("events");
const axios_1 = __importDefault(require("axios"));
const misc_1 = require("./utils/misc");
const constants_1 = require("./constants");
// /**
// * Used to inject http call timers
// * transport:request: httpsWithTimer
// * @szmarczak/http-timer
// */
// const transport = {
// request: function httpsWithTimer(...args: unknown[]): AxiosRequestConfig {
// const request = https.request.apply(null, args)
// timer(request);
// return request;
// }
// };
// // by default axios will refuse self-signed certs. We disable this for all F5 connections. For external connections, we leave this on, but wrap it in a switch for easy access
// interface extRegCfg extends uuidAxiosRequestConfig {
// rejectUnauthorized?: boolean,
// }
/**
* Class for making all external HTTP calls
* @constructor options.rejectUnauthorized - set to false to allow self-signed certs (default true)
*/
class ExtHttp {
constructor(options) {
this.events = (options === null || options === void 0 ? void 0 : options.eventEmitter) ? options.eventEmitter : new events_1.EventEmitter;
options === null || options === void 0 ? true : delete options.eventEmitter; // delete eventEmitter from object before was pass to axios
// set cache directory
this.cacheDir = process.env.F5_CONX_CORE_CACHE || path_1.default.join(process.cwd(), constants_1.TMP_DIR);
// set the user-agent, required for github connections
this.userAgent = process.env.F5_CONX_CORE_EXT_HTTP_AGENT || 'F5 Conx Core';
this.axios = this.createAxiosInstance(options);
// todo: setup proxy details, probably capture from env's
}
/**
* core external axios instance
* @param reqBase
*/
createAxiosInstance(reqBase = {}) {
// #####################################
// Request timings are disabled for external resolution. There was a corner case where they are breaking the download function with github and redirects.
// add request timinings
// reqBase.transport = transport;
// #####################################
// // add option to allow self-signed cert
// if (reqBase?.rejectUnauthorized === false) {
// // add agent option
// reqBase.httpsAgent = new https.Agent({ rejectUnauthorized: false });
// }
reqBase.httpsAgent =
(reqBase === null || reqBase === void 0 ? void 0 : reqBase.rejectUnauthorized) === false ?
new https_1.default.Agent({ rejectUnauthorized: false }) :
new https_1.default.Agent({ rejectUnauthorized: true });
// remove param
reqBase === null || reqBase === void 0 ? true : delete reqBase.rejectUnauthorized;
// // open up the allowed responses to include redirects.
// reqBase.validateStatus = function (status) {
// return status >= 200 && status <= 302;
// }
// set user agent
reqBase.headers = {
'User-Agent': this.userAgent
};
// create axsios instance with collected params
const axInstance = axios_1.default.create(reqBase);
// re-assign parent this objects needed within the parent instance objects...
const events = this.events;
// // ---- https://github.com/axios/axios#interceptors
// // Add a request interceptor
axInstance.interceptors.request.use(function (config) {
// adjust tcp timeout, default=0, which relies on host system
config.timeout = Number(process.env.F5_CONX_CORE_TCP_TIMEOUT);
config.uuid = (config === null || config === void 0 ? void 0 : config.uuid) ? config.uuid : (0, misc_1.getRandomUUID)(4, { simple: true });
// events.emit('log-info', `EXTERNAL-HTTPS-REQU [${config.uuid}]: ${config.method} -> ${config.url}`)
events.emit('log-http-request', config);
return config;
}, function (err) {
// Do something with request error
// not sure how to test this, but it is probably handled up the chain
return Promise.reject(err);
});
// response interceptor
axInstance.interceptors.response.use(function (resp) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
// events.emit('log-info', `EXTERNAL-HTTPS-RESP [${resp.config.uuid}]: ${resp.status} - ${resp.statusText}`);
events.emit('log-http-response', resp);
return resp;
}, function (err) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
return Promise.reject(err);
});
return axInstance;
}
/**
* Make External HTTP request
*
* @param url absolute url
* @param options axios options
*
* @returns request response
*/
makeRequest(options) {
return __awaiter(this, void 0, void 0, function* () {
// another way to get the base protocol http vs https
// const baseURL = new URL(options.url)
return yield this.axios.request(options)
.then((resp) => __awaiter(this, void 0, void 0, function* () {
const respSimplified = yield (0, misc_1.simplifyHttpResponse)(resp);
// only return the things we need
return respSimplified;
}))
.catch(err => {
// todo: rework this to build a singe err-response object to be passed back as an event
// https://github.com/axios/axios#handling-errors
if (err.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
this.events.emit('log-info', `EXTERNAL-HTTPS-RESP [${err.response.config.uuid}]: ${err.response.status} - ${JSON.stringify(err.response.data)}`);
// only return the things we need... we'll see...
return Promise.reject({
data: err.response.data,
headers: err.response.headers,
status: err.response.status,
statusText: err.response.statusText,
request: {
uuid: err.response.config.uuid,
baseURL: err.response.config.baseURL,
url: err.response.config.url,
method: err.request.method,
headers: err.response.config.headers,
protocol: err.response.config.httpsAgent.protocol,
timings: err.request.timings
}
});
}
else if (err.request) {
// The request was made but no response was received
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
// http.ClientRequest in node.js
this.events.emit('log-error', {
message: 'EXTERNAL-HTTPS-REQUEST-FAILED',
path: err.request.path,
err: err.message
});
// return Promise.reject(err.request)
}
else {
// got a lower level (config) failure
// not sure how to test this...
/* istanbul ignore next */
this.events.emit('log-error', {
message: 'EXTERNAL-HTTPS request failed',
// uuid: err.response.config.uuid,
err
});
}
return Promise.reject(err);
// thought: just log the individual situations and have a single reject clause like below
// return Promise.reject({
// message: 'HTTPS request failed',
// uuid,
// err
// })
});
});
}
/**
* download file from external (not f5)
* @param url fully qualified URL
* @param fileName (optional) destination file name - if you want it different than url
* @param destPath (optional) where to put the file (default is local project cache folder)
* @param options axios requestion options
*/
download(url, fileName, destPath, options = {}) {
return __awaiter(this, void 0, void 0, function* () {
// recreate cache dir
if (!fs_1.default.existsSync(this.cacheDir)) {
console.log(`creating ${this.cacheDir} directory for file upload/download tests`);
fs_1.default.mkdirSync(this.cacheDir);
}
// extract path from URL
const urlPath = new URL(url).pathname;
// extract file name from url path
fileName = fileName ? fileName : path_1.default.basename(urlPath);
// set the response type for file download
options['responseType'] = 'stream';
// move url into options object
options.url = url;
destPath = destPath ? destPath : this.cacheDir;
this.events.emit('log-debug', {
message: 'pending external download',
destPath,
options
});
const writable = fs_1.default.createWriteStream(`${destPath}/${fileName}`);
return new Promise(((resolve, reject) => {
this.makeRequest(options)
.then(resp => {
resp.data.pipe(writable)
.on('finish', () => {
// over-write response data
resp.data = {
file: writable.path,
bytes: writable.bytesWritten
};
this.events.emit('log-debug', {
message: 'download complete',
data: resp.data
});
return resolve(resp);
});
})
.catch(err => {
// look at adding more failure details, like,
// was it tcp, dns, dest url problem, write file problem, ...
return reject(err);
});
}));
});
}
/**
*
* @param url
* @param localSourcePathFilename
*/
upload(url, localSourcePathFilename) {
return __awaiter(this, void 0, void 0, function* () {
// array to hold responses
const responses = [];
const fileName = path_1.default.parse(localSourcePathFilename).base;
const fileStats = fs_1.default.statSync(localSourcePathFilename);
const chunkSize = 1024 * 1024;
let start = 0;
let end = Math.min(chunkSize, fileStats.size - 1);
this.events.emit('log-debug', {
message: 'pending upload',
localSourcePathFilename,
url
});
while (end <= fileStats.size - 1 && start < end) {
const resp = yield this.makeRequest({
url,
method: 'POST',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Range': `${start}-${end}/${fileStats.size}`,
'Content-Length': end - start + 1
},
data: fs_1.default.createReadStream(localSourcePathFilename, { start, end }),
});
start += chunkSize;
if (end + chunkSize < fileStats.size - 1) { // more to go
end += chunkSize;
}
else if (end + chunkSize > fileStats.size - 1) { // last chunk
end = fileStats.size - 1;
}
else { // done - could use do..while loop instead of this
end = fileStats.size;
}
responses.push(resp);
}
// get the last response
const lastResponse = responses.pop();
// inject file stream information
lastResponse.data.fileName = fileName;
lastResponse.data.bytes = fileStats.size;
this.events.emit('log-debug', {
message: 'upload complete',
data: lastResponse.data
});
return lastResponse;
});
}
}
exports.ExtHttp = ExtHttp;
//# sourceMappingURL=externalHttps.js.map