endpoint-sentinel
Version:
User-friendly security scanner with interactive setup that scales from beginner to expert
232 lines • 8.14 kB
JavaScript
"use strict";
/**
* Production-grade Token Bucket Rate Limiter
* Implements ethical scanning boundaries with configurable limits
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RateLimiterFactory = exports.AdaptiveRateLimiter = exports.TokenBucketRateLimiter = void 0;
class TokenBucketRateLimiter {
requestsPerSecond;
burstSize;
tokens;
lastRefill;
totalRequests = 0;
throttledRequests = 0;
constructor(requestsPerSecond = 2, burstSize = 5) {
this.requestsPerSecond = requestsPerSecond;
this.burstSize = burstSize;
this.tokens = this.burstSize;
this.lastRefill = new Date();
}
/**
* Throttles requests according to token bucket algorithm
* Respects server capabilities and ethical boundaries
*/
async throttle(_request) {
this.refillTokens();
this.totalRequests++;
if (this.tokens >= 1) {
this.tokens--;
return; // Request can proceed immediately
}
// Need to wait for tokens to be available
this.throttledRequests++;
const waitTime = this.calculateWaitTime();
if (waitTime > 0) {
await this.sleep(waitTime);
this.refillTokens();
this.tokens = Math.max(0, this.tokens - 1);
}
}
/**
* Refills tokens based on elapsed time
*/
refillTokens() {
const now = new Date();
const elapsed = (now.getTime() - this.lastRefill.getTime()) / 1000;
const tokensToAdd = elapsed * this.requestsPerSecond;
this.tokens = Math.min(this.burstSize, this.tokens + tokensToAdd);
this.lastRefill = now;
}
/**
* Calculates wait time until next token is available
*/
calculateWaitTime() {
if (this.tokens >= 1)
return 0;
const tokensNeeded = 1 - this.tokens;
return (tokensNeeded / this.requestsPerSecond) * 1000; // Convert to milliseconds
}
/**
* Sleep utility for throttling
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Gets current rate limiting statistics
*/
getStats() {
this.refillTokens(); // Update tokens before returning stats
return {
requestsPerSecond: this.requestsPerSecond,
burstSize: this.burstSize,
tokensAvailable: this.tokens,
lastRefill: this.lastRefill,
totalRequests: this.totalRequests,
throttledRequests: this.throttledRequests
};
}
/**
* Updates rate limiting parameters
*/
updateLimits(requestsPerSecond, burstSize) {
if (requestsPerSecond <= 0 || burstSize <= 0) {
throw new Error('Rate limit parameters must be positive');
}
this.requestsPerSecond = requestsPerSecond;
this.burstSize = burstSize;
this.tokens = Math.min(this.tokens, this.burstSize);
}
/**
* Calculates optimal rate limit based on server response headers
*/
adaptToServerCapabilities(responseHeaders) {
// Check for server-provided rate limit hints
const retryAfter = responseHeaders['retry-after'];
const rateLimitRemaining = responseHeaders['x-ratelimit-remaining'];
const rateLimitReset = responseHeaders['x-ratelimit-reset'];
if (retryAfter) {
const retrySeconds = parseInt(retryAfter, 10);
if (!isNaN(retrySeconds) && retrySeconds > 0) {
// Temporarily reduce rate if server indicates we should wait
this.updateLimits(Math.min(0.5, this.requestsPerSecond), this.burstSize);
}
}
if (rateLimitRemaining && rateLimitReset) {
const remaining = parseInt(rateLimitRemaining, 10);
const resetTime = parseInt(rateLimitReset, 10);
if (!isNaN(remaining) && !isNaN(resetTime) && remaining < 10) {
// Slow down if we're approaching rate limits
this.updateLimits(this.requestsPerSecond * 0.5, this.burstSize);
}
}
}
/**
* Resets rate limiter state
*/
reset() {
this.tokens = this.burstSize;
this.lastRefill = new Date();
this.totalRequests = 0;
this.throttledRequests = 0;
}
}
exports.TokenBucketRateLimiter = TokenBucketRateLimiter;
/**
* Advanced Rate Limiter with domain-specific controls
*/
class AdaptiveRateLimiter extends TokenBucketRateLimiter {
domainLimits = new Map();
serverResponseTimes = new Map();
constructor(requestsPerSecond = 2, burstSize = 5) {
super(requestsPerSecond, burstSize);
}
/**
* Sets domain-specific rate limits
*/
setDomainLimit(domain, requestsPerSecond, burstSize) {
this.domainLimits.set(domain, { requestsPerSecond, burstSize });
}
/**
* Throttles with domain-aware rate limiting
*/
async throttle(request) {
const domain = this.extractDomain(request.url);
const domainLimits = this.domainLimits.get(domain);
if (domainLimits) {
const originalRps = this.requestsPerSecond;
const originalBurst = this.burstSize;
this.updateLimits(domainLimits.requestsPerSecond, domainLimits.burstSize);
await super.throttle(request);
// Restore original limits
this.updateLimits(originalRps, originalBurst);
}
else {
await super.throttle(request);
}
}
/**
* Records server response time for adaptive rate limiting
*/
recordResponseTime(url, responseTime) {
const domain = this.extractDomain(url);
const times = this.serverResponseTimes.get(domain) || [];
times.push(responseTime);
// Keep only last 10 response times
if (times.length > 10) {
times.shift();
}
this.serverResponseTimes.set(domain, times);
this.adaptRateToServerPerformance(domain);
}
/**
* Adapts rate limiting based on server performance
*/
adaptRateToServerPerformance(domain) {
const times = this.serverResponseTimes.get(domain);
if (!times || times.length < 3)
return;
const avgResponseTime = times.reduce((sum, time) => sum + time, 0) / times.length;
let adaptedRate = this.requestsPerSecond;
if (avgResponseTime > 5000) { // Slow server (>5s)
adaptedRate = Math.max(0.5, this.requestsPerSecond * 0.3);
}
else if (avgResponseTime > 2000) { // Moderate server (>2s)
adaptedRate = Math.max(1, this.requestsPerSecond * 0.6);
}
else if (avgResponseTime < 500) { // Fast server (<500ms)
adaptedRate = Math.min(5, this.requestsPerSecond * 1.5);
}
this.setDomainLimit(domain, adaptedRate, this.burstSize);
}
/**
* Extracts domain from URL
*/
extractDomain(url) {
try {
return new URL(url).hostname;
}
catch {
return 'unknown';
}
}
}
exports.AdaptiveRateLimiter = AdaptiveRateLimiter;
/**
* Rate Limiter Factory
*/
class RateLimiterFactory {
static createBasic(requestsPerSecond = 2, burstSize = 5) {
return new TokenBucketRateLimiter(requestsPerSecond, burstSize);
}
static createAdaptive(requestsPerSecond = 2, burstSize = 5) {
return new AdaptiveRateLimiter(requestsPerSecond, burstSize);
}
static createEthical() {
// Conservative settings for ethical scanning
return new TokenBucketRateLimiter(1, 3);
}
static createFromConfig(config) {
switch (config.type) {
case 'adaptive':
return this.createAdaptive(config.requestsPerSecond, config.burstSize);
case 'ethical':
return this.createEthical();
default:
return this.createBasic(config.requestsPerSecond, config.burstSize);
}
}
}
exports.RateLimiterFactory = RateLimiterFactory;
//# sourceMappingURL=rate-limiter.js.map