basic proxy ip detection test tool

This commit is contained in:
Bojan Kucera 2025-06-05 21:14:29 -04:00
commit d3cff15545
15 changed files with 1075 additions and 0 deletions

134
src/index.ts Normal file
View file

@ -0,0 +1,134 @@
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
import { extractIPsFromHeaders } from './proxyService';
// Type definitions
interface ApiResponse {
success: boolean;
clientIP: string;
ipChain: string[];
foundIPs: string[];
totalFound: number;
headersSources: Record<string, string[]>;
allHeaders: Record<string, string>;
timestamp: number;
}
// Environment configuration
const API_KEY = process.env.API_KEY;
const PORT = parseInt(process.env.PORT || '2424');
const HOST = process.env.HOST || '0.0.0.0';
const NODE_ENV = process.env.NODE_ENV || 'production';
const objs: ApiResponse[] = []
// Create Fastify instance with trust proxy enabled
const fastify = Fastify({
logger: NODE_ENV === 'development',
trustProxy: process.env.TRUST_PROXY === 'true' || true
});
// API Key authentication middleware
fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => {
// Skip auth for health check endpoint
if (request.url === '/health') {
return;
}
const apiKey = request.headers['x-api-key'] || (request.query as Record<string, any>)?.api_key;
if (!apiKey) {
reply.code(401).send({
error: 'API key required',
message: 'Please provide API key in X-API-Key header or api_key query parameter'
});
return;
}
if (apiKey !== API_KEY) {
reply.code(403).send({
error: 'Invalid API key',
message: 'The provided API key is not valid'
});
return;
}
});
// Helper function to get full IP chain
function getFullIPChain(request: FastifyRequest): { clientIP: string, ipChain: string[] } {
// Fastify automatically parses X-Forwarded-For when trustProxy is enabled
const clientIP = request.ip || 'unknown';
const ipChain = request.ips || []; // Full chain of IPs
return {
clientIP,
ipChain: ipChain.length > 0 ? ipChain : [clientIP]
};
}
// Start the server
const start = async () => {
try {
// Register CORS plugin
await fastify.register(import('@fastify/cors'), {
origin: true,
methods: ['GET', 'POST', 'OPTIONS']
});
// Health check endpoint (bypasses authentication)
fastify.get('/health', async () => {
return {
status: 'healthy',
timestamp: Date.now(),
uptime: process.uptime(),
version: '1.0.0'
};
});
// Random endpoint for testing
fastify.get('/random', async () => {
return objs[Math.round(objs.length * Math.random())] || { message: 'No data yet' };
});
// Main detection endpoint - extracts all IPs from headers
fastify.get('/', async (request) => {
const headers = request.headers;
const normalizedHeaders = Object.fromEntries(
Object.entries(headers).map(([key, value]) => [
key.toLowerCase(),
typeof value === 'string' ? value : String(value || '')
])
);
const result = extractIPsFromHeaders(normalizedHeaders);
const ipInfo = getFullIPChain(request);
const obj = {
success: true,
clientIP: ipInfo.clientIP, // Fastify's auto-detected client IP
ipChain: ipInfo.ipChain, // Full proxy chain
foundIPs: result.ips, // IPs found by our custom parser
totalFound: result.ips.length,
headersSources: result.sources,
allHeaders: normalizedHeaders,
timestamp: Date.now()
};
if( objs.length > 1000) {
objs.shift();
}
objs.push(obj);
return obj;
});
await fastify.listen({ port: PORT, host: HOST });
console.log(`🚀 Proxy Detection API running on http://localhost:${PORT}`);
console.log(`📍 Available endpoints:`);
console.log(` GET /health - Health check (no auth required)`);
console.log(` GET / - Extract all IP addresses from request headers`);
console.log(` GET /random - Get random cached result`);
console.log(`🔑 API Key required for protected endpoints`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
};
start();

100
src/proxyService.ts Normal file
View file

@ -0,0 +1,100 @@
export interface HeaderIPResult {
ips: string[];
sources: Record<string, string[]>;
}
export function extractIPsFromHeaders(headers: Record<string, string | undefined>): HeaderIPResult {
const ips: string[] = [];
const sources: Record<string, string[]> = {};
// Common headers that might contain IP addresses
const ipHeaders = [
'x-forwarded-for',
'x-real-ip',
'x-client-ip',
'x-cluster-client-ip',
'x-forwarded',
'forwarded-for',
'forwarded',
'cf-connecting-ip',
'true-client-ip',
'x-originating-ip',
'x-remote-ip',
'x-remote-addr',
'client-ip',
'remote-addr',
'http-client-ip',
'http-x-forwarded-for',
'http-x-real-ip'
];
// IP regex pattern (basic IPv4 validation)
const ipRegex = /\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g;
// Check each header for IP addresses
for (const [headerName, headerValue] of Object.entries(headers)) {
if (!headerValue) continue;
// Check if this is a known IP header
if (ipHeaders.includes(headerName.toLowerCase())) {
const foundIPs = extractIPsFromValue(headerValue);
if (foundIPs.length > 0) {
sources[headerName] = foundIPs;
ips.push(...foundIPs);
}
}
// Also check for IP patterns in any header value
const matches = headerValue.match(ipRegex);
if (matches) {
const validIPs = matches.filter(isValidIP);
if (validIPs.length > 0) {
if (!sources[headerName]) {
sources[headerName] = [];
}
sources[headerName].push(...validIPs);
ips.push(...validIPs);
}
}
}
// Remove duplicates while preserving order
const uniqueIPs = [...new Set(ips)];
return {
ips: uniqueIPs,
sources
};
}
function extractIPsFromValue(value: string): string[] {
const ips: string[] = [];
// Handle comma-separated values (common in X-Forwarded-For)
const parts = value.split(',').map(part => part.trim());
for (const part of parts) {
// Handle "for=" syntax in Forwarded header
if (part.startsWith('for=')) {
const ip = part.substring(4).replace(/"/g, '');
if (isValidIP(ip)) {
ips.push(ip);
}
} else if (isValidIP(part)) {
ips.push(part);
}
}
return ips;
}
function isValidIP(ip: string): boolean {
// Basic IPv4 validation
const parts = ip.split('.');
if (parts.length !== 4) return false;
return parts.every(part => {
const num = parseInt(part, 10);
return !isNaN(num) && num >= 0 && num <= 255;
});
}