UNPKG

endpoint-sentinel

Version:

User-friendly security scanner with interactive setup that scales from beginner to expert

334 lines 12.3 kB
"use strict"; /** * 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