commit d3cff1554556475480f81d338eab9619a04c1ba7 Author: Bojan Kucera Date: Thu Jun 5 21:14:29 2025 -0400 basic proxy ip detection test tool diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50973e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Dependencies +node_modules/ +bun.lockb + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Storybook build outputs +.out +.storybook-out + +# Temporary folders +tmp/ +temp/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Docker +.dockerignore + +# Kubernetes secrets (keep template) +k8s-secret-production.yaml + +# Local development +*.local diff --git a/.gitlab-ci-simple.yml b/.gitlab-ci-simple.yml new file mode 100644 index 0000000..6d95c53 --- /dev/null +++ b/.gitlab-ci-simple.yml @@ -0,0 +1,42 @@ +# Simple GitLab CI/CD Pipeline - Build and Deploy only +stages: + - build + - deploy + +variables: + DOCKER_DRIVER: overlay2 + IMAGE_NAME: $CI_REGISTRY_IMAGE + IMAGE_TAG: $CI_COMMIT_SHA + +# Build and push Docker image +build: + stage: build + image: docker:24.0.5 + services: + - docker:24.0.5-dind + before_script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + variables: + DOCKER_TLS_CERTDIR: "/certs" + script: + - docker info + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker pull $CI_REGISTRY_IMAGE:latest || true + - docker build --cache-from $CI_REGISTRY_IMAGE:latest --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA --tag $CI_REGISTRY_IMAGE:latest . + - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA + - docker push $CI_REGISTRY_IMAGE:latest + + only: + - master + +# Deploy to production +deploy: + stage: deploy + image: ictu/sshpass + script: + - sshpass -p c4c5f47918a6310d79de188465218b440c69570c ssh -o StrictHostKeyChecking=no kubeuser@stare.gg 'kubectl rollout restart deployments/stare-site -n stare' + + environment: + name: production + only: + - master diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..454aa44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Use the official Bun image as base - much smaller than Node.js +FROM oven/bun:1-slim as base + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package.json bun.lockb* ./ + +# Install dependencies +RUN bun install --frozen-lockfile --production + +# Copy source code +COPY src/ ./src/ + +# Expose the port +EXPOSE 9999 + +# Create non-root user for security +RUN addgroup --system --gid 1001 bunuser && \ + adduser --system --uid 1001 bunuser + +# Change ownership of the app directory +RUN chown -R bunuser:bunuser /app +USER bunuser + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD bun --version || exit 1 + +# Start the application +CMD ["bun", "run", "start"] diff --git a/KUBERNETES.md b/KUBERNETES.md new file mode 100644 index 0000000..aae41d6 --- /dev/null +++ b/KUBERNETES.md @@ -0,0 +1,81 @@ +# Kubernetes Deployment + +## Quick Start + +### Prerequisites +- Kubernetes cluster running +- `kubectl` configured to access your cluster +- Docker registry access (for GitLab CI) + +### Deploy to Kubernetes + +**Option 1: Use deployment script** +```bash +# Linux/Mac +chmod +x deploy.sh +./deploy.sh + +# Windows +deploy.bat +``` + +**Option 2: Manual deployment** +```bash +# 1. Create secret for API key +kubectl apply -f k8s-secret.yaml + +# 2. Deploy application +kubectl apply -f k8s-deployment.yaml + +# 3. Optional: Deploy ingress for external access +kubectl apply -f k8s-ingress.yaml +``` + +### Test the deployment +```bash +# Port forward to test locally +kubectl port-forward service/proxy-detection-service 8080:80 + +# Test health endpoint (no auth required) +curl http://localhost:8080/health + +# Test main endpoint (requires API key) +curl -H "X-API-Key: bd406bf53ddc6abe1d9de5907830a955" http://localhost:8080/ +``` + +### Configuration + +#### Update API Key +1. Generate base64 encoded API key: + ```bash + echo -n "your-new-api-key" | base64 + ``` +2. Update `k8s-secret.yaml` with the new value +3. Apply changes: + ```bash + kubectl apply -f k8s-secret.yaml + kubectl rollout restart deployment/proxy-detection-api + ``` + +#### Update Image +Update image in `k8s-deployment.yaml` or use kubectl: +```bash +kubectl set image deployment/proxy-detection-api proxy-detection-api=your-registry/proxy-detection-api:new-tag +``` + +### GitLab CI/CD + +The `.gitlab-ci-simple.yml` provides: +- **Build stage**: Builds and pushes Docker image to GitLab registry +- **Deploy stage**: Updates Kubernetes deployment (manual trigger) + +Replace `.gitlab-ci.yml` with `.gitlab-ci-simple.yml` for the minimal pipeline. + +### Resources + +The deployment is configured with minimal resources: +- **Requests**: 64Mi RAM, 50m CPU +- **Limits**: 128Mi RAM, 100m CPU +- **Replicas**: 2 (for basic high availability) + +Adjust these values in `k8s-deployment.yaml` based on your needs. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e21b3cd --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# Proxy Detection API + +A simple Bun and Fastify API that extracts IP addresses from HTTP request headers to detect potential proxy leaks. + +## Features + +- **Built-in IP Detection**: Fastify's `trustProxy` automatically parses proxy headers +- **Full IP Chain**: Get the complete proxy chain, not just the client IP +- **Custom Header Analysis**: Extracts IPs from 17+ different proxy headers +- **No External Dependencies**: Works entirely by analyzing request headers +- **Fast Performance**: Fastify is one of the fastest Node.js frameworks + +## Installation + +```bash +bun install +``` + +## Usage + +### Development +```bash +bun run dev +``` + +### Production +```bash +bun run start +``` + +## Docker Deployment + +### Quick Start with Docker +```bash +# Build the image +npm run docker:build + +# Run the container +npm run docker:run +``` + +### Using Docker Compose +```bash +# Development +npm run docker:dev + +# Production (with nginx reverse proxy) +npm run docker:prod +``` + +### Manual Docker Commands +```bash +# Build +docker build -t proxy-detection-api . + +# Run +docker run -p 9999:9999 proxy-detection-api + +# Run with environment variables +docker run -p 9999:9999 -e NODE_ENV=production proxy-detection-api +``` + +## GitLab CI/CD + +This project includes a complete GitLab CI/CD pipeline that: + +1. **Tests** - Runs tests and TypeScript compilation +2. **Builds** - Creates Docker images and pushes to GitLab Container Registry +3. **Deploys** - Deploys to staging/production environments + +### Pipeline Stages: +- `test` - Runs on MRs and main branch +- `build` - Builds Docker images on main/develop/tags +- `deploy_staging` - Manual deployment to staging (develop branch) +- `deploy_production` - Manual deployment to production (main branch/tags) + +### Required GitLab Variables: +- `CI_REGISTRY_USER` - GitLab registry username (auto-provided) +- `CI_REGISTRY_PASSWORD` - GitLab registry password (auto-provided) +- Add your deployment-specific variables in GitLab CI/CD settings + +The API will be available at `http://localhost:9999` + +### Endpoints + +#### `GET /` +Returns API information and available endpoints. + +#### `GET /detect` +Extracts all IP addresses found in request headers with full proxy chain detection. + +**Response:** +```json +{ + "success": true, + "clientIP": "203.0.113.45", // Fastify's auto-detected client IP + "ipChain": ["203.0.113.45", "192.168.1.100"], // Complete proxy chain + "foundIPs": ["192.168.1.100", "203.0.113.45"], // IPs found by custom parser + "totalFound": 2, + "headersSources": { + "x-forwarded-for": ["192.168.1.100", "203.0.113.45"], + "x-real-ip": ["203.0.113.45"] + }, + "allHeaders": { ... }, + "timestamp": 1672531200000 +} +``` + +## What's New with Fastify + +### Trust Proxy Support +Fastify automatically handles proxy headers when `trustProxy: true` is enabled: +- Parses `X-Forwarded-For` header automatically +- Provides `request.ip` (client IP) and `request.ips` (full chain) +- More secure than manual header parsing + +### Built-in IP Chain Detection +```javascript +// Fastify automatically provides: +request.ip // "203.0.113.45" (real client) +request.ips // ["203.0.113.45", "10.0.0.1", "192.168.1.100"] (full chain) +``` + +## Installation + +```bash +# Install dependencies +bun install +``` + +## Usage + +```bash +# Development (with auto-reload) +bun run dev + +# Production +bun run start +``` + +## API Endpoints + +### `GET /` +Returns API information and available endpoints. + +### `GET /detect` +Performs a comprehensive proxy detection check: +```json +{ + "success": true, + "data": { + "yourIP": "1.2.3.4", + "externalIPs": [ + { + "ip": "1.2.3.4", + "source": "ipify", + "timestamp": 1234567890 + } + ], + "isProxyWorking": true, + "leakedIPs": [], + "timestamp": 1234567890 + }, + "message": "Proxy is working correctly" +} +``` + +### `GET /quick` +Quick IP check using a single service: +```json +{ + "ip": "1.2.3.4", + "timestamp": 1234567890 +} +``` + +### `GET /headers` +Shows request headers and client IP information: +```json +{ + "clientIP": "1.2.3.4", + "headers": { + "x-forwarded-for": "1.2.3.4", + "user-agent": "Mozilla/5.0..." + } +} +``` + +## How It Works + +1. **Multiple IP Sources**: Queries several IP detection services (ipify, httpbin, ipapi, jsonip) +2. **Comparison**: Compares results to detect inconsistencies +3. **Leak Detection**: If different services return different IPs, it indicates a potential proxy leak +4. **Analysis**: Determines if your proxy is working correctly + +## Use Cases + +- Test if your VPN/proxy is working +- Detect DNS leaks +- Monitor IP consistency across services +- Verify anonymity tools + +## Example Response (Proxy Working) +```json +{ + "yourIP": "192.168.1.100", + "isProxyWorking": true, + "leakedIPs": [], + "message": "Proxy is working correctly" +} +``` + +## Example Response (IP Leak Detected) +```json +{ + "yourIP": "192.168.1.100", + "isProxyWorking": false, + "leakedIPs": ["203.0.113.5"], + "message": "Potential IP leak detected: 203.0.113.5" +} +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..e8831e7 --- /dev/null +++ b/bun.lock @@ -0,0 +1,126 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "proxy-detection-api", + "dependencies": { + "@fastify/cors": "^11.0.1", + "fastify": "^5.3.3", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.15.30", + "typescript": "^5.8.3", + }, + }, + }, + "packages": { + "@fastify/ajv-compiler": ["@fastify/ajv-compiler@4.0.2", "", { "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0" } }, "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ=="], + + "@fastify/cors": ["@fastify/cors@11.0.1", "", { "dependencies": { "fastify-plugin": "^5.0.0", "toad-cache": "^3.7.0" } }, "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ=="], + + "@fastify/error": ["@fastify/error@4.2.0", "", {}, "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ=="], + + "@fastify/fast-json-stringify-compiler": ["@fastify/fast-json-stringify-compiler@5.0.3", "", { "dependencies": { "fast-json-stringify": "^6.0.0" } }, "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ=="], + + "@fastify/forwarded": ["@fastify/forwarded@3.0.0", "", {}, "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA=="], + + "@fastify/merge-json-schemas": ["@fastify/merge-json-schemas@0.2.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A=="], + + "@fastify/proxy-addr": ["@fastify/proxy-addr@5.0.0", "", { "dependencies": { "@fastify/forwarded": "^3.0.0", "ipaddr.js": "^2.1.0" } }, "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA=="], + + "@types/bun": ["@types/bun@1.2.15", "", { "dependencies": { "bun-types": "1.2.15" } }, "sha512-U1ljPdBEphF0nw1MIk0hI7kPg7dFdPyM7EenHsp6W5loNHl7zqy6JQf/RKCgnUn2KDzUpkBwHPnEJEjII594bA=="], + + "@types/node": ["@types/node@22.15.30", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA=="], + + "abstract-logging": ["abstract-logging@2.0.1", "", {}, "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "avvio": ["avvio@9.1.0", "", { "dependencies": { "@fastify/error": "^4.0.0", "fastq": "^1.17.1" } }, "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw=="], + + "bun-types": ["bun-types@1.2.15", "", { "dependencies": { "@types/node": "*" } }, "sha512-NarRIaS+iOaQU1JPfyKhZm4AsUOrwUOqRNHY0XxI8GI8jYxiLXLcdjYMG9UKS+fwWasc1uw1htV9AX24dD+p4w=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stringify": ["fast-json-stringify@6.0.1", "", { "dependencies": { "@fastify/merge-json-schemas": "^0.2.0", "ajv": "^8.12.0", "ajv-formats": "^3.0.1", "fast-uri": "^3.0.0", "json-schema-ref-resolver": "^2.0.0", "rfdc": "^1.2.0" } }, "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg=="], + + "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="], + + "fastify": ["fastify@5.3.3", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-nCBiBCw9q6jPx+JJNVgO8JVnTXeUyrGcyTKPQikRkA/PanrFcOIo4R+ZnLeOLPZPGgzjomqfVarzE0kYx7qWiQ=="], + + "fastify-plugin": ["fastify-plugin@5.0.1", "", {}, "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ=="], + + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], + + "find-my-way": ["find-my-way@9.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", "safe-regex2": "^5.0.0" } }, "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg=="], + + "ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], + + "json-schema-ref-resolver": ["json-schema-ref-resolver@2.0.1", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "light-my-request": ["light-my-request@6.6.0", "", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "ret": ["ret@0.5.0", "", {}, "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "safe-regex2": ["safe-regex2@5.0.0", "", { "dependencies": { "ret": "~0.5.0" } }, "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], + + "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "toad-cache": ["toad-cache@3.7.0", "", {}, "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "light-my-request/process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + } +} diff --git a/deploy.bat b/deploy.bat new file mode 100644 index 0000000..db1d3d6 --- /dev/null +++ b/deploy.bat @@ -0,0 +1,32 @@ +@echo off +echo 🚀 Deploying Proxy Detection API to Kubernetes... + +REM Apply secret (API key) +echo 📋 Creating secret... +kubectl apply -f k8s-secret.yaml + +REM Apply deployment and service +echo 🔧 Deploying application... +kubectl apply -f k8s-deployment.yaml + +REM Optional: Apply ingress +set /p deploy_ingress="Do you want to deploy ingress? (y/N): " +if /i "%deploy_ingress%"=="y" ( + echo 🌐 Deploying ingress... + kubectl apply -f k8s-ingress.yaml +) + +REM Check deployment status +echo 📊 Checking deployment status... +kubectl rollout status deployment/proxy-detection-api + +REM Show service info +echo 📋 Service information: +kubectl get service proxy-detection-service + +echo ✅ Deployment completed! +echo 💡 To test the API: +echo kubectl port-forward service/proxy-detection-service 8080:80 +echo curl http://localhost:8080/health + +pause diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..1ed3b0a --- /dev/null +++ b/deploy.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +# Simple Kubernetes deployment script +echo "🚀 Deploying Proxy Detection API to Kubernetes..." + +# Apply secret (API key) +echo "📋 Creating secret..." +kubectl apply -f k8s-secret.yaml + +# Apply deployment and service +echo "🔧 Deploying application..." +kubectl apply -f k8s-deployment.yaml + +# Optional: Apply ingress +read -p "Do you want to deploy ingress? (y/N): " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "🌐 Deploying ingress..." + kubectl apply -f k8s-ingress.yaml +fi + +# Check deployment status +echo "📊 Checking deployment status..." +kubectl rollout status deployment/proxy-detection-api + +# Show service info +echo "📋 Service information:" +kubectl get service proxy-detection-service + +echo "✅ Deployment completed!" +echo "💡 To test the API:" +echo " kubectl port-forward service/proxy-detection-service 8080:80" +echo " curl http://localhost:8080/health" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..750914d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: '3.8' + +services: + proxy-detection: + build: . + ports: + - "9999:9999" + environment: + - NODE_ENV=production + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9999/"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + networks: + - proxy-net + + # Optional: Add a reverse proxy for production + nginx: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + # - ./ssl:/etc/nginx/ssl:ro # Add SSL certificates here + depends_on: + - proxy-detection + networks: + - proxy-net + profiles: + - production + +networks: + proxy-net: + driver: bridge diff --git a/k8s-deployment.yaml b/k8s-deployment.yaml new file mode 100644 index 0000000..7c011e9 --- /dev/null +++ b/k8s-deployment.yaml @@ -0,0 +1,69 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: proxy-detection-api + labels: + app: proxy-detection-api +spec: + replicas: 2 + selector: + matchLabels: + app: proxy-detection-api + template: + metadata: + labels: + app: proxy-detection-api + spec: + containers: + - name: proxy-detection-api + image: your-registry/proxy-detection-api:latest + ports: + - containerPort: 2424 + env: + - name: PORT + value: "2424" + - name: HOST + value: "0.0.0.0" + - name: NODE_ENV + value: "production" + - name: TRUST_PROXY + value: "true" + - name: API_KEY + valueFrom: + secretKeyRef: + name: proxy-detection-secret + key: api-key + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + livenessProbe: + httpGet: + path: /health + port: 2424 + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /health + port: 2424 + initialDelaySeconds: 5 + periodSeconds: 5 +--- +apiVersion: v1 +kind: Service +metadata: + name: proxy-detection-service + labels: + app: proxy-detection-api +spec: + selector: + app: proxy-detection-api + ports: + - port: 80 + targetPort: 2424 + protocol: TCP + type: ClusterIP diff --git a/k8s-ingress.yaml b/k8s-ingress.yaml new file mode 100644 index 0000000..d29ad8f --- /dev/null +++ b/k8s-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: proxy-detection-ingress + labels: + app: proxy-detection-api +spec: + rules: + - host: proxy-detection.stare.gg + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: proxy-detection-service + port: + number: 80 diff --git a/k8s-secret.yaml b/k8s-secret.yaml new file mode 100644 index 0000000..cdf2de3 --- /dev/null +++ b/k8s-secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: proxy-detection-secret +type: Opaque +data: + # Base64 encoded API key (replace with your actual base64 encoded API key) + # To encode: echo -n "your-api-key" | base64 + api-key: YmQ0MDZiZjUzZGRjNmFiZTFkOWRlNTkwNzgzMGE5NTU= diff --git a/package.json b/package.json new file mode 100644 index 0000000..7f3c983 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "proxy-detection-api", + "version": "1.0.0", + "main": "src/index.ts", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.15.30", + "typescript": "^5.8.3" + }, + "description": "Simple API to detect if IP is exposed when using a proxy", + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "build": "bun build src/index.ts --outdir ./dist --target bun", + "test": "echo \"No tests specified yet\" && exit 0", + "docker:build": "docker build -t proxy-detection-api .", + "docker:run": "docker run -p 9999:9999 proxy-detection-api", + "docker:dev": "docker-compose up --build", + "docker:prod": "docker-compose --profile production up -d" + }, + "dependencies": { + "@fastify/cors": "^11.0.1", + "fastify": "^5.3.3" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c1ce5c8 --- /dev/null +++ b/src/index.ts @@ -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; + allHeaders: Record; + 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)?.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(); diff --git a/src/proxyService.ts b/src/proxyService.ts new file mode 100644 index 0000000..b799146 --- /dev/null +++ b/src/proxyService.ts @@ -0,0 +1,100 @@ +export interface HeaderIPResult { + ips: string[]; + sources: Record; +} + +export function extractIPsFromHeaders(headers: Record): HeaderIPResult { + const ips: string[] = []; + const sources: Record = {}; + + // 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; + }); +}