endpoint-sentinel
Version:
User-friendly security scanner with interactive setup that scales from beginner to expert
334 lines • 12.3 kB
JavaScript
;
/**
* Production-ready HTTP Client for Endpoint Sentinel
* Handles cookies, authentication, retries, and security headers
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpClientFactory = exports.HttpClientError = exports.EnhancedCookieJar = exports.SecurityHttpClient = void 0;
const axios_1 = __importDefault(require("axios"));
const tough_cookie_1 = require("tough-cookie");
class SecurityHttpClient {
axiosInstance;
cookieJar;
logger;
constructor(cookieJar, logger, options = {}) {
this.cookieJar = cookieJar;
this.logger = logger;
this.axiosInstance = axios_1.default.create({
timeout: options.timeout || 30000,
maxRedirects: options.maxRedirects || 5,
validateStatus: () => true, // Accept all status codes
headers: {
'User-Agent': options.userAgent || 'Endpoint-Sentinel/1.0.0 (Security Scanner)',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'Accept-Encoding': 'gzip, deflate',
'DNT': '1',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
});
this.setupInterceptors();
}
/**
* Setup request/response interceptors
*/
setupInterceptors() {
// Request interceptor for cookie handling
this.axiosInstance.interceptors.request.use((config) => {
if (config.url) {
const cookies = this.cookieJar.getCookies(config.url);
if (cookies.length > 0) {
config.headers = config.headers || {};
config.headers['Cookie'] = cookies.join('; ');
}
}
return config;
});
// Response interceptor for cookie extraction and logging
this.axiosInstance.interceptors.response.use((response) => {
this.extractCookies(response);
this.logResponse(response);
return response;
}, (error) => {
this.logger.error('HTTP request failed', error, {
url: error.config?.url,
method: error.config?.method,
status: error.response?.status
});
return Promise.reject(error);
});
}
/**
* Extract cookies from response
*/
extractCookies(response) {
const setCookieHeaders = response.headers['set-cookie'];
if (setCookieHeaders && response.config.url) {
setCookieHeaders.forEach(cookie => {
this.cookieJar.setCookie(cookie, response.config.url);
});
}
}
/**
* Log response for audit purposes
*/
logResponse(response) {
this.logger.debug('HTTP response received', {
url: response.config.url,
method: response.config.method,
status: response.status,
responseTime: response.headers['x-response-time'],
contentLength: response.headers['content-length']
});
}
/**
* Execute HTTP request with comprehensive error handling
*/
async request(request) {
const startTime = Date.now();
try {
const config = {
url: request.url,
method: request.method,
headers: { ...request.headers },
timeout: request.timeout,
data: request.data
};
const response = await this.axiosInstance.request(config);
const endTime = Date.now();
return {
status: response.status,
statusText: response.statusText,
headers: this.normalizeHeaders(response.headers),
data: this.safeStringify(response.data),
responseTime: endTime - startTime,
size: this.calculateResponseSize(response)
};
}
catch (error) {
const endTime = Date.now();
if (axios_1.default.isAxiosError(error) && error.response) {
// Server responded with error status
return {
status: error.response.status,
statusText: error.response.statusText,
headers: this.normalizeHeaders(error.response.headers),
data: this.safeStringify(error.response.data),
responseTime: endTime - startTime,
size: this.calculateResponseSize(error.response)
};
}
// Network error or timeout
throw new HttpClientError(`Request failed: ${error instanceof Error ? error.message : 'Unknown error'}`, request.url, request.method);
}
}
/**
* Execute multiple requests with concurrency control
*/
async requestBatch(requests, concurrency = 5) {
const results = [];
for (let i = 0; i < requests.length; i += concurrency) {
const batch = requests.slice(i, i + concurrency);
const batchPromises = batch.map(req => this.request(req));
try {
const batchResults = await Promise.allSettled(batchPromises);
batchResults.forEach((result, index) => {
if (result.status === 'fulfilled') {
results.push(result.value);
}
else {
this.logger.error('Batch request failed', result.reason, {
url: batch[index]?.url,
batchIndex: i + index
});
// Create error response
results.push({
status: 0,
statusText: 'Request Failed',
headers: {},
data: '',
responseTime: 0,
size: 0
});
}
});
}
catch (error) {
this.logger.error('Batch execution failed', error);
throw error;
}
}
return results;
}
/**
* Normalize response headers to lowercase
*/
normalizeHeaders(headers) {
const normalized = {};
Object.entries(headers).forEach(([key, value]) => {
normalized[key.toLowerCase()] = String(value);
});
return normalized;
}
/**
* Safely convert response data to string
*/
safeStringify(data) {
if (typeof data === 'string') {
return data;
}
try {
return JSON.stringify(data);
}
catch {
return String(data);
}
}
/**
* Calculate response size
*/
calculateResponseSize(response) {
const contentLength = response.headers['content-length'];
if (contentLength) {
return parseInt(contentLength, 10);
}
return Buffer.byteLength(this.safeStringify(response.data), 'utf8');
}
/**
* Check if response indicates rate limiting
*/
isRateLimited(response) {
return response.status === 429 ||
response.status === 503 ||
response.headers['retry-after'] !== undefined;
}
/**
* Check if response indicates authentication requirement
*/
requiresAuthentication(response) {
return response.status === 401 ||
response.status === 403 ||
response.data.toLowerCase().includes('login') ||
response.data.toLowerCase().includes('unauthorized');
}
/**
* Extract security headers for analysis
*/
extractSecurityHeaders(response) {
const headers = response.headers;
return {
...(headers['content-security-policy'] && { contentSecurityPolicy: headers['content-security-policy'] }),
...(headers['strict-transport-security'] && { strictTransportSecurity: headers['strict-transport-security'] }),
...(headers['x-content-type-options'] && { xContentTypeOptions: headers['x-content-type-options'] }),
...(headers['x-frame-options'] && { xFrameOptions: headers['x-frame-options'] }),
...(headers['x-xss-protection'] && { xXssProtection: headers['x-xss-protection'] }),
...(headers['referrer-policy'] && { referrerPolicy: headers['referrer-policy'] }),
...(headers['permissions-policy'] && { permissionsPolicy: headers['permissions-policy'] }),
...(headers['cross-origin-embedder-policy'] && { crossOriginEmbedderPolicy: headers['cross-origin-embedder-policy'] }),
...(headers['cross-origin-opener-policy'] && { crossOriginOpenerPolicy: headers['cross-origin-opener-policy'] }),
...(headers['cross-origin-resource-policy'] && { crossOriginResourcePolicy: headers['cross-origin-resource-policy'] })
};
}
/**
* Clean up and close connections
*/
destroy() {
// Clear any pending timeouts or intervals
this.cookieJar.clear();
}
}
exports.SecurityHttpClient = SecurityHttpClient;
/**
* Enhanced Cookie Jar implementation
*/
class EnhancedCookieJar {
toughCookieJar;
constructor(initialCookies) {
this.toughCookieJar = new tough_cookie_1.CookieJar();
if (initialCookies) {
this.deserialize(initialCookies);
}
}
setCookie(cookie, url) {
try {
this.toughCookieJar.setCookieSync(cookie, url);
}
catch (error) {
// Silently ignore invalid cookies to maintain robustness
}
}
getCookies(url) {
try {
const cookies = this.toughCookieJar.getCookiesSync(url);
return cookies.map(cookie => cookie.toString());
}
catch {
return [];
}
}
serialize() {
return JSON.stringify(this.toughCookieJar.serializeSync());
}
clear() {
this.toughCookieJar = new tough_cookie_1.CookieJar();
}
deserialize(cookieString) {
try {
// Handle both JSON format and simple cookie string
if (cookieString.startsWith('{')) {
const serialized = JSON.parse(cookieString);
this.toughCookieJar = tough_cookie_1.CookieJar.deserializeSync(serialized);
}
else {
// Parse simple cookie string format
const cookies = cookieString.split(';').map(c => c.trim());
cookies.forEach(cookie => {
if (cookie) {
// Assume these are for a default domain
this.setCookie(cookie, 'http://localhost');
}
});
}
}
catch (error) {
throw new Error(`Failed to parse cookie string: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
}
exports.EnhancedCookieJar = EnhancedCookieJar;
/**
* Custom HTTP Client Error
*/
class HttpClientError extends Error {
url;
method;
constructor(message, url, method) {
super(message);
this.url = url;
this.method = method;
this.name = 'HttpClientError';
}
}
exports.HttpClientError = HttpClientError;
/**
* HTTP Client Factory
*/
class HttpClientFactory {
static create(cookieJar, logger, options) {
return new SecurityHttpClient(cookieJar, logger, options);
}
static createWithDefaults(logger) {
const cookieJar = new EnhancedCookieJar();
return new SecurityHttpClient(cookieJar, logger, {
timeout: 30000,
maxRedirects: 5,
userAgent: 'Endpoint-Sentinel/1.0.0 (Security Scanner)'
});
}
}
exports.HttpClientFactory = HttpClientFactory;
//# sourceMappingURL=http-client.js.map