@sectester/repeater
Version:
Package for managing repeaters, which are mandatory for scanning targets on a local network.
251 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpRequestRunner = void 0;
const tslib_1 = require("tslib");
const Response_1 = require("./Response");
const models_1 = require("../models");
const RequestRunnerOptions_1 = require("./RequestRunnerOptions");
const utils_1 = require("../utils");
const core_1 = require("@sectester/core");
const tsyringe_1 = require("tsyringe");
const iconv_lite_1 = tslib_1.__importDefault(require("iconv-lite"));
const node_url_1 = require("node:url");
const node_http_1 = tslib_1.__importDefault(require("node:http"));
const node_https_1 = tslib_1.__importDefault(require("node:https"));
const node_events_1 = require("node:events");
const node_zlib_1 = require("node:zlib");
const node_util_1 = require("node:util");
let HttpRequestRunner = class HttpRequestRunner {
get protocol() {
return models_1.Protocol.HTTP;
}
constructor(logger, proxyFactory, options) {
this.logger = logger;
this.proxyFactory = proxyFactory;
this.options = options;
this.DEFAULT_MIME_TYPE = 'application/octet-stream';
this.DEFAULT_ENCODING = 'utf8';
if (this.options.proxyUrl) {
({ httpsAgent: this.httpsProxyAgent, httpAgent: this.httpProxyAgent } =
this.proxyFactory.createProxy({ proxyUrl: this.options.proxyUrl }));
}
if (this.options.reuseConnection) {
const agentOptions = {
keepAlive: true,
maxSockets: 100,
timeout: this.options.timeout
};
this.httpsAgent = new node_https_1.default.Agent(agentOptions);
this.httpAgent = new node_http_1.default.Agent(agentOptions);
}
}
async run(options) {
var _a;
try {
if (this.options.headers) {
options.setHeaders(this.options.headers);
}
this.logger.debug('Executing HTTP request with following params: %j', options);
const { res, body } = await this.request(options);
return new Response_1.Response({
body,
protocol: this.protocol,
statusCode: res.statusCode,
headers: this.convertHeaders(res.headers),
encoding: options.encoding
});
}
catch (err) {
const { cause } = err;
const { message, code, syscall, name } = cause !== null && cause !== void 0 ? cause : err;
const errorCode = (_a = code !== null && code !== void 0 ? code : syscall) !== null && _a !== void 0 ? _a : name;
this.logger.error('Error executing request: "%s %s HTTP/1.1"', options.method, options.url);
this.logger.error('Cause: %s', message);
return new Response_1.Response({
message,
errorCode,
protocol: this.protocol
});
}
}
convertHeaders(headers) {
return Object.fromEntries(Object.entries(headers).map(([name, value]) => [
name,
value !== null && value !== void 0 ? value : ''
]));
}
async request(options) {
let timer;
let res;
try {
const req = this.createRequest(options);
process.nextTick(() => req.end(options.encoding && options.body
? iconv_lite_1.default.encode(options.body, options.encoding)
: options.body));
timer = this.setTimeout(req, options.timeout);
[res] = (await (0, node_events_1.once)(req, 'response'));
}
finally {
clearTimeout(timer);
}
return this.truncateResponse(options, res);
}
createRequest(request) {
const protocol = request.secureEndpoint ? node_https_1.default : node_http_1.default;
const outgoingMessage = protocol.request(this.createRequestOptions(request));
this.setHeaders(outgoingMessage, request);
if (!outgoingMessage.hasHeader('accept-encoding')) {
outgoingMessage.setHeader('accept-encoding', 'gzip, deflate');
}
return outgoingMessage;
}
setTimeout(req, timeout) {
timeout !== null && timeout !== void 0 ? timeout : (timeout = this.options.timeout);
if (typeof timeout === 'number') {
return setTimeout(() => req.destroy(new Error('Waiting response has timed out')), timeout);
}
}
createRequestOptions(request) {
var _a;
const { auth, hostname, port, hash = '', pathname = '/', search = '' } = (0, node_url_1.parse)(request.url);
const path = `${pathname !== null && pathname !== void 0 ? pathname : '/'}${search !== null && search !== void 0 ? search : ''}${hash !== null && hash !== void 0 ? hash : ''}`;
const agent = this.getRequestAgent(request);
const timeout = (_a = request.timeout) !== null && _a !== void 0 ? _a : this.options.timeout;
return {
hostname,
port,
path,
auth,
agent,
timeout,
method: request.method,
rejectUnauthorized: false
};
}
getRequestAgent(options) {
var _a, _b;
return options.secureEndpoint
? ((_a = this.httpsProxyAgent) !== null && _a !== void 0 ? _a : this.httpsAgent)
: ((_b = this.httpProxyAgent) !== null && _b !== void 0 ? _b : this.httpAgent);
}
async truncateResponse({ decompress, encoding, maxContentSize }, res) {
var _a;
if (this.responseHasNoBody(res)) {
this.logger.debug('The response does not contain any body.');
return { res, body: '' };
}
const contentType = this.parseContentType(res);
const { type } = contentType;
const requiresTruncating = this.options.maxContentLength !== -1 &&
!((_a = this.options.allowedMimes) === null || _a === void 0 ? void 0 : _a.some((mime) => type.startsWith(mime)));
const maxBodySize = typeof maxContentSize === 'number'
? maxContentSize * 1024
: this.options.maxContentLength
? Math.abs(this.options.maxContentLength) * 1024
: undefined;
const body = await this.parseBody(res, {
decompress,
maxBodySize: requiresTruncating ? maxBodySize : undefined
});
res.headers['content-length'] = body.byteLength.toFixed();
if (decompress) {
delete res.headers['content-encoding'];
}
return { res, body: iconv_lite_1.default.decode(body, encoding !== null && encoding !== void 0 ? encoding : contentType.encoding) };
}
parseContentType(res) {
const contentType = res.headers['content-type'] || this.DEFAULT_MIME_TYPE;
try {
const { params, essence: type } = new node_util_1.MIMEType(contentType);
let encoding = params.get('charset');
if (!encoding || !iconv_lite_1.default.encodingExists(encoding)) {
encoding = this.DEFAULT_ENCODING;
}
return { type, encoding };
}
catch (err) {
this.logger.debug('Invalid content-type header "%s", falling back to defaults: %s', contentType, err instanceof Error ? err.message : String(err));
return {
type: this.DEFAULT_MIME_TYPE,
encoding: this.DEFAULT_ENCODING
};
}
}
unzipBody(response) {
let body = response;
if (!this.responseHasNoBody(response)) {
let contentEncoding = response.headers['content-encoding'] || 'identity';
contentEncoding = contentEncoding.trim().toLowerCase();
// Always using Z_SYNC_FLUSH is what cURL does.
const zlibOptions = {
flush: node_zlib_1.constants.Z_SYNC_FLUSH,
finishFlush: node_zlib_1.constants.Z_SYNC_FLUSH
};
switch (contentEncoding) {
case 'gzip':
body = response.pipe((0, node_zlib_1.createGunzip)(zlibOptions));
break;
case 'deflate':
body = response
.pipe(new utils_1.NormalizeZlibDeflateTransformStream())
.pipe((0, node_zlib_1.createInflate)(zlibOptions));
break;
case 'br':
body = response.pipe((0, node_zlib_1.createBrotliDecompress)());
break;
}
}
return body;
}
responseHasNoBody(response) {
return (response.method === 'HEAD' ||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(response.statusCode >= 100 && response.statusCode < 200) ||
response.statusCode === 204 ||
response.statusCode === 304);
}
async parseBody(res, options) {
const chunks = [];
const stream = options.decompress ? this.unzipBody(res) : res;
for await (const chuck of stream) {
chunks.push(chuck);
}
let body = Buffer.concat(chunks);
const truncated = typeof options.maxBodySize === 'number' &&
body.byteLength > options.maxBodySize;
if (truncated) {
this.logger.debug('Truncate original response body to %i bytes', options.maxBodySize);
body = body.subarray(0, options.maxBodySize);
}
return body;
}
/**
* Allows to attack headers. Node.js does not accept any other characters
* which violate [rfc7230](https://tools.ietf.org/html/rfc7230#section-3.2.6).
* To override default behavior bypassing {@link OutgoingMessage.setHeader} method we have to set headers via internal symbol.
*/
setHeaders(req, options) {
var _a;
const symbols = Object.getOwnPropertySymbols(req);
const headersSymbol = symbols.find(
// ADHOC: Node.js version < 12 uses "outHeadersKey" symbol to set headers
item => ['Symbol(kOutHeaders)', 'Symbol(outHeadersKey)'].includes(item.toString()));
if (!req.headersSent && headersSymbol && options.headers) {
const headers = (req[headersSymbol] =
(_a = req[headersSymbol]) !== null && _a !== void 0 ? _a : Object.create(null));
Object.entries(options.headers).forEach(([key, value]) => {
if (key) {
headers[key.toLowerCase()] = [key.toLowerCase(), value !== null && value !== void 0 ? value : ''];
}
});
}
}
};
exports.HttpRequestRunner = HttpRequestRunner;
exports.HttpRequestRunner = HttpRequestRunner = tslib_1.__decorate([
(0, tsyringe_1.injectable)(),
tslib_1.__param(1, (0, tsyringe_1.inject)(utils_1.ProxyFactory)),
tslib_1.__param(2, (0, tsyringe_1.inject)(RequestRunnerOptions_1.RequestRunnerOptions)),
tslib_1.__metadata("design:paramtypes", [core_1.Logger, Object, Object])
], HttpRequestRunner);
//# sourceMappingURL=HttpRequestRunner.js.map