- Implement tier-based rate limiting with rate-limiter-flexible - Add Redis-backed rate limiters for different user tiers (free/pro/enterprise) - Create comprehensive header redaction service for security - Implement burst protection with per-minute limits - Add organization and project-based rate limiting keys - Create rate limiting middleware with proper error handling - Integrate rate limits with tracking, bulk, and export endpoints - Add header redaction to redirect tracking service - Implement request logging with redacted sensitive headers - Add comprehensive rate limit headers (limit, remaining, reset, tier) - Support for anonymous vs authenticated rate limits - Legacy endpoint rate limiting preserved for backward compatibility - Admin functions for rate limit management and statistics - Comprehensive test suite for all rate limiting scenarios Security improvements: - Sensitive header redaction (auth tokens, cookies, secrets) - Partial redaction for debugging (admin mode) - URL parameter redaction for sensitive data - Request/response body redaction - Configurable redaction levels Backward compatibility: Maintained 100/hr rate limit for legacy endpoints
456 lines
14 KiB
JavaScript
456 lines
14 KiB
JavaScript
/**
|
||
* Test script for Phase 7: Advanced Rate Limiting + Header Redaction
|
||
* Tests the new rate limiting system and header redaction functionality
|
||
*/
|
||
|
||
const http = require('http');
|
||
|
||
const BASE_URL = 'http://localhost:3333';
|
||
|
||
// Helper function to make HTTP requests
|
||
function makeRequest(options, data = null) {
|
||
return new Promise((resolve, reject) => {
|
||
const req = http.request(options, (res) => {
|
||
let body = '';
|
||
res.on('data', (chunk) => body += chunk);
|
||
res.on('end', () => {
|
||
try {
|
||
const result = {
|
||
statusCode: res.statusCode,
|
||
headers: res.headers,
|
||
body: res.headers['content-type'] && res.headers['content-type'].includes('application/json')
|
||
? JSON.parse(body)
|
||
: body
|
||
};
|
||
resolve(result);
|
||
} catch (error) {
|
||
resolve({
|
||
statusCode: res.statusCode,
|
||
headers: res.headers,
|
||
body: body
|
||
});
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', reject);
|
||
|
||
if (data) {
|
||
if (typeof data === 'string') {
|
||
req.write(data);
|
||
} else {
|
||
req.write(JSON.stringify(data));
|
||
}
|
||
}
|
||
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
// Helper function to sleep
|
||
function sleep(ms) {
|
||
return new Promise(resolve => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function runTests() {
|
||
console.log('🧪 Starting Phase 7: Advanced Rate Limiting + Header Redaction Tests\n');
|
||
|
||
let authToken = null;
|
||
|
||
// Test 1: User Registration/Login
|
||
console.log('1️⃣ Testing user authentication...');
|
||
try {
|
||
const loginResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v1/auth/login',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}, {
|
||
email: 'rate-test@example.com',
|
||
password: 'ratetest123'
|
||
});
|
||
|
||
if (loginResult.statusCode === 200 && loginResult.body.success) {
|
||
authToken = loginResult.body.data.token;
|
||
console.log('✅ User login successful');
|
||
} else {
|
||
// Try to register if login fails
|
||
const registerResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v1/auth/register',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}, {
|
||
email: 'rate-test@example.com',
|
||
name: 'Rate Test User',
|
||
password: 'ratetest123',
|
||
organizationName: 'Rate Test Org'
|
||
});
|
||
|
||
if (registerResult.statusCode === 201 && registerResult.body.success) {
|
||
authToken = registerResult.body.data.token;
|
||
console.log('✅ User registration successful');
|
||
} else {
|
||
console.log('❌ Authentication failed:', registerResult.body);
|
||
return;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Authentication error:', error.message);
|
||
return;
|
||
}
|
||
|
||
// Test 2: Rate Limit Headers
|
||
console.log('\n2️⃣ Testing rate limit headers...');
|
||
try {
|
||
const trackResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
},
|
||
}, {
|
||
url: 'https://example.com'
|
||
});
|
||
|
||
console.log('Status Code:', trackResult.statusCode);
|
||
console.log('Rate Limit Headers:');
|
||
console.log(' X-RateLimit-Limit:', trackResult.headers['x-ratelimit-limit']);
|
||
console.log(' X-RateLimit-Remaining:', trackResult.headers['x-ratelimit-remaining']);
|
||
console.log(' X-RateLimit-Reset:', trackResult.headers['x-ratelimit-reset']);
|
||
console.log(' X-RateLimit-Tier:', trackResult.headers['x-ratelimit-tier']);
|
||
|
||
if (trackResult.headers['x-ratelimit-limit']) {
|
||
console.log('✅ Rate limit headers present');
|
||
} else {
|
||
console.log('⚠️ Rate limit headers missing');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Rate limit headers test error:', error.message);
|
||
}
|
||
|
||
// Test 3: Anonymous vs Authenticated Rate Limits
|
||
console.log('\n3️⃣ Testing anonymous vs authenticated rate limits...');
|
||
try {
|
||
// Anonymous request
|
||
const anonResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}, {
|
||
url: 'https://httpbin.org/get'
|
||
});
|
||
|
||
console.log('Anonymous Request:');
|
||
console.log(' Limit:', anonResult.headers['x-ratelimit-limit']);
|
||
console.log(' Tier:', anonResult.headers['x-ratelimit-tier']);
|
||
|
||
// Authenticated request
|
||
const authResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
},
|
||
}, {
|
||
url: 'https://httpbin.org/get'
|
||
});
|
||
|
||
console.log('Authenticated Request:');
|
||
console.log(' Limit:', authResult.headers['x-ratelimit-limit']);
|
||
console.log(' Tier:', authResult.headers['x-ratelimit-tier']);
|
||
|
||
const anonLimit = parseInt(anonResult.headers['x-ratelimit-limit'] || '0');
|
||
const authLimit = parseInt(authResult.headers['x-ratelimit-limit'] || '0');
|
||
|
||
if (authLimit > anonLimit) {
|
||
console.log('✅ Authenticated users have higher rate limits');
|
||
} else {
|
||
console.log('⚠️ Rate limit difference not detected');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Rate limit comparison error:', error.message);
|
||
}
|
||
|
||
// Test 4: Legacy Endpoint Rate Limiting
|
||
console.log('\n4️⃣ Testing legacy endpoint rate limiting...');
|
||
try {
|
||
const legacyResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}, {
|
||
url: 'https://example.com'
|
||
});
|
||
|
||
console.log('Legacy endpoint status:', legacyResult.statusCode);
|
||
console.log('Legacy rate limit:', legacyResult.headers['x-ratelimit-limit']);
|
||
|
||
if (legacyResult.headers['x-ratelimit-limit']) {
|
||
console.log('✅ Legacy endpoints have rate limiting');
|
||
} else {
|
||
console.log('⚠️ Legacy endpoints missing rate limiting');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Legacy rate limiting test error:', error.message);
|
||
}
|
||
|
||
// Test 5: Burst Protection
|
||
console.log('\n5️⃣ Testing burst protection...');
|
||
try {
|
||
console.log('Making rapid authenticated requests...');
|
||
const requests = [];
|
||
|
||
for (let i = 0; i < 15; i++) {
|
||
requests.push(
|
||
makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
},
|
||
}, {
|
||
url: `https://httpbin.org/delay/0`,
|
||
timeout: 5000
|
||
})
|
||
);
|
||
}
|
||
|
||
const results = await Promise.allSettled(requests);
|
||
const statusCodes = results.map(r => r.status === 'fulfilled' ? r.value.statusCode : 'error');
|
||
const rateLimited = statusCodes.filter(code => code === 429).length;
|
||
const successful = statusCodes.filter(code => code === 200 || code === 201).length;
|
||
|
||
console.log(`Results: ${successful} successful, ${rateLimited} rate limited`);
|
||
|
||
if (rateLimited > 0) {
|
||
console.log('✅ Burst protection is working');
|
||
} else {
|
||
console.log('⚠️ Burst protection may not be active (or limits are very high)');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Burst protection test error:', error.message);
|
||
}
|
||
|
||
// Test 6: Header Redaction (indirect test via response logs)
|
||
console.log('\n6️⃣ Testing header redaction...');
|
||
try {
|
||
const sensitiveHeadersResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'X-Secret-Key': 'secret123',
|
||
'Cookie': 'session=abc123',
|
||
'X-API-Key': 'api_key_12345',
|
||
'User-Agent': 'RedirectIntelligence Test Client v1.0',
|
||
},
|
||
}, {
|
||
url: 'https://httpbin.org/headers'
|
||
});
|
||
|
||
console.log('Request with sensitive headers sent');
|
||
console.log('Status:', sensitiveHeadersResult.statusCode);
|
||
|
||
// Check if the request was processed (indicating headers were handled properly)
|
||
if (sensitiveHeadersResult.statusCode === 200 || sensitiveHeadersResult.statusCode === 201) {
|
||
console.log('✅ Request with sensitive headers processed successfully');
|
||
console.log(' (Header redaction occurs server-side in logs/storage)');
|
||
} else {
|
||
console.log('⚠️ Request with sensitive headers failed');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Header redaction test error:', error.message);
|
||
}
|
||
|
||
// Test 7: Bulk Rate Limiting
|
||
console.log('\n7️⃣ Testing bulk endpoint rate limiting...');
|
||
try {
|
||
const bulkResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/bulk/jobs',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
},
|
||
}, {
|
||
urls: [
|
||
{ url: 'https://example.com', label: 'Test 1' }
|
||
]
|
||
});
|
||
|
||
console.log('Bulk endpoint status:', bulkResult.statusCode);
|
||
console.log('Bulk rate limit tier:', bulkResult.headers['x-ratelimit-tier']);
|
||
console.log('Bulk rate limit:', bulkResult.headers['x-ratelimit-limit']);
|
||
|
||
if (bulkResult.headers['x-ratelimit-tier']) {
|
||
console.log('✅ Bulk endpoints have tier-based rate limiting');
|
||
} else {
|
||
console.log('⚠️ Bulk rate limiting not detected');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Bulk rate limiting test error:', error.message);
|
||
}
|
||
|
||
// Test 8: Rate Limit Exceeded Response
|
||
console.log('\n8️⃣ Testing rate limit exceeded response...');
|
||
try {
|
||
console.log('Making requests to approach rate limit...');
|
||
let lastResponse = null;
|
||
|
||
// Make many requests quickly to trigger rate limiting
|
||
for (let i = 0; i < 60; i++) {
|
||
try {
|
||
const response = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/track', // Use legacy endpoint for predictable low limits
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
}, {
|
||
url: 'https://httpbin.org/get'
|
||
});
|
||
|
||
lastResponse = response;
|
||
|
||
if (response.statusCode === 429) {
|
||
console.log('✅ Rate limit exceeded (429) response received');
|
||
console.log('Response body:', response.body);
|
||
console.log('Retry-After header:', response.headers['retry-after']);
|
||
console.log('X-RateLimit headers:', {
|
||
limit: response.headers['x-ratelimit-limit'],
|
||
remaining: response.headers['x-ratelimit-remaining'],
|
||
reset: response.headers['x-ratelimit-reset'],
|
||
});
|
||
break;
|
||
}
|
||
|
||
// Small delay to avoid overwhelming the server
|
||
await sleep(50);
|
||
} catch (error) {
|
||
console.log('Request error (expected for rate limiting):', error.message);
|
||
}
|
||
}
|
||
|
||
if (!lastResponse || lastResponse.statusCode !== 429) {
|
||
console.log('⚠️ Rate limit not triggered (limits may be too high for testing)');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Rate limit exceeded test error:', error.message);
|
||
}
|
||
|
||
// Test 9: Different User Agent Handling
|
||
console.log('\n9️⃣ Testing different user agent handling...');
|
||
try {
|
||
const userAgents = [
|
||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||
'curl/7.68.0',
|
||
'RedirectIntelligence/2.0 TestClient',
|
||
];
|
||
|
||
for (const ua of userAgents) {
|
||
const result = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/api/v2/track',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`,
|
||
'User-Agent': ua,
|
||
},
|
||
}, {
|
||
url: 'https://httpbin.org/user-agent'
|
||
});
|
||
|
||
console.log(`User-Agent: ${ua.substring(0, 30)}... -> Status: ${result.statusCode}`);
|
||
}
|
||
|
||
console.log('✅ User-Agent handling test completed');
|
||
} catch (error) {
|
||
console.log('❌ User-Agent test error:', error.message);
|
||
}
|
||
|
||
// Test 10: Health Check (should not be rate limited)
|
||
console.log('\n🔟 Testing health check endpoint...');
|
||
try {
|
||
const healthResult = await makeRequest({
|
||
hostname: 'localhost',
|
||
port: 3333,
|
||
path: '/health',
|
||
method: 'GET',
|
||
});
|
||
|
||
console.log('Health check status:', healthResult.statusCode);
|
||
console.log('Health check rate limited:', !!healthResult.headers['x-ratelimit-limit']);
|
||
|
||
if (healthResult.statusCode === 200 && !healthResult.headers['x-ratelimit-limit']) {
|
||
console.log('✅ Health check endpoint is not rate limited');
|
||
} else {
|
||
console.log('⚠️ Health check endpoint may have rate limiting');
|
||
}
|
||
} catch (error) {
|
||
console.log('❌ Health check test error:', error.message);
|
||
}
|
||
|
||
console.log('\n🎉 Phase 7 testing completed!');
|
||
console.log('\nKey features tested:');
|
||
console.log('✓ Advanced rate limiting with user tiers');
|
||
console.log('✓ Rate limit headers in responses');
|
||
console.log('✓ Anonymous vs authenticated rate limits');
|
||
console.log('✓ Legacy endpoint rate limiting');
|
||
console.log('✓ Burst protection for rapid requests');
|
||
console.log('✓ Header redaction (server-side)');
|
||
console.log('✓ Bulk endpoint tier-based limiting');
|
||
console.log('✓ Rate limit exceeded responses');
|
||
console.log('✓ User-Agent handling');
|
||
console.log('✓ Health check endpoint exclusion');
|
||
}
|
||
|
||
// Error handling
|
||
process.on('uncaughtException', (error) => {
|
||
console.log('\n💥 Uncaught Exception:', error.message);
|
||
process.exit(1);
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason) => {
|
||
console.log('\n💥 Unhandled Rejection:', reason);
|
||
process.exit(1);
|
||
});
|
||
|
||
// Run tests
|
||
runTests().catch(error => {
|
||
console.log('\n💥 Test execution failed:', error.message);
|
||
process.exit(1);
|
||
});
|