Package Exports
- simple-proxy-id
- simple-proxy-id/attack-detector
- simple-proxy-id/cors
- simple-proxy-id/logger
Readme
simple-proxy-id
🔒 A secure HTTP/HTTPS proxy for Node.js with zero dependencies and fixed target. Think of it as a safe reverse proxy that prevents open proxy abuse.
⚡ High Performance: Optimized with connection pooling and keep-alive, achieving ~1,660 req/s with 60ms average latency.
✨ Features
- Standalone HTTP/HTTPS proxy server
- Express middleware support
- Fixed target (secure by default, cannot be changed from requests)
- Path rewriting support (object rules or function)
- Optional
changeOriginto set Host header - Automatic error handling
- CORS plugin with flexible origin validation
- Logger plugin with daily rotating logs
- Attack detector plugin for brute force protection
- IP detection (Cloudflare Tunnel compatible)
- High performance with HTTP Agent connection pooling
- TypeScript definitions included
- Zero dependencies
📦 Installation
npm install simple-proxy-id🚀 Usage
Basic Usage (Standalone)
const { createProxy } = require('simple-proxy-id');
// Create proxy server
const server = createProxy({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
port: 3000
});
// Access: http://localhost:3000/postsExpress Middleware
const express = require('express');
const { createProxyMiddleware } = require('simple-proxy-id');
const app = express();
// Proxy for path /api/*
app.use('/api', createProxyMiddleware({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true
}));
app.listen(3000);
// Access: http://localhost:3000/api/postsWith Path Rewrite
const { createProxy } = require('simple-proxy-id');
// Using object rules (regex patterns)
const server = createProxy({
target: 'https://api.example.com',
changeOrigin: true,
port: 3000,
pathRewrite: {
'^/backend': '/api', // /backend/users → /api/users
'^/old-api': '/new-api', // /old-api/posts → /new-api/posts
'^/v1': '/api/v1' // /v1/data → /api/v1/data
}
});
// Or using a function for custom logic
const server2 = createProxy({
target: 'https://api.example.com',
changeOrigin: true,
port: 3001,
pathRewrite: (path) => {
// Custom path transformation logic
return path.replace(/^\/legacy/, '/modern');
}
});
// With Express middleware
const express = require('express');
const { createProxyMiddleware } = require('simple-proxy-id');
const app = express();
app.use('/api', createProxyMiddleware({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
pathRewrite: {
'^/api': '' // Strip /api prefix
}
}));
app.listen(3000);With Logger Plugin
const { createProxy } = require('simple-proxy-id');
// Enable request logging
const server = createProxy({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
port: 3000,
logger: {
logDir: './logs',
maxDays: 7
}
});
// Or with Express
const express = require('express');
const { createProxyMiddleware } = require('simple-proxy-id');
const createLogger = require('simple-proxy-id/logger');
const app = express();
app.use(createLogger({
logDir: './logs',
maxDays: 7
}));
app.use('/api', createProxyMiddleware({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true
}));
app.listen(3000);With Attack Detector Plugin
const { createProxy } = require('simple-proxy-id');
// Standalone with attack detector
const server = createProxy({
target: 'https://jsonplaceholder.typicode.com',
changeOrigin: true,
port: 3000,
attackDetector: {
path: '/api/login',
statusCode: 401,
threshold: 5,
timeWindow: 1000,
onTrigger: (data) => {
console.log('Attack detected from IP:', data.ip);
// Block IP via your firewall API
}
}
});
// Or with Express
const express = require('express');
const { createProxyMiddleware } = require('simple-proxy-id');
const createAttackDetector = require('simple-proxy-id/attack-detector');
const app = express();
app.use(createAttackDetector({
path: '/api/login',
statusCode: 401,
threshold: 5,
timeWindow: 1000,
onTrigger: (data) => {
console.log('Attack detected from IP:', data.ip);
// Block IP via Cloudflare API, Mikrotik, iptables, etc.
}
}));
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true
}));
app.listen(3000);With CORS Plugin
const { createProxy } = require('simple-proxy-id');
// Standalone with CORS
const server = createProxy({
target: 'https://api.example.com',
changeOrigin: true,
port: 3000,
cors: {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}
});
// Or with Express
const express = require('express');
const { createProxyMiddleware } = require('simple-proxy-id');
const createCors = require('simple-proxy-id/cors');
const app = express();
// Enable CORS for all routes
app.use(createCors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true
}));
app.listen(3000);
// Dynamic origin validation
const server2 = createProxy({
target: 'https://api.example.com',
port: 3001,
cors: {
origin: (requestOrigin) => {
// Allow all subdomains of example.com
return /^https:\/\/([a-z0-9-]+\.)?example\.com$/.test(requestOrigin);
},
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}
});
// Allow all origins (development only)
const server3 = createProxy({
target: 'https://api.example.com',
port: 3002,
cors: {
origin: '*',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type']
}
});🧪 Testing
npm testJest is used for testing. All tests must pass before publishing.
⚡ Performance
Benchmarked with autocannon on localhost (100 concurrent connections):
npm run benchmarkResults:
- Throughput: ~1,660 requests/second
- Latency (avg): 60ms
- Latency (p50): 52ms
- Latency (p99): 138ms
- Errors: 0
Optimizations:
- HTTP Agent with
keepAlive: truefor connection pooling - Cached target URL parsing (no re-parsing per request)
- Pre-computed error responses
- TCP_NODELAY enabled for lower latency
- Connection reuse across requests
📂 Project Structure
src/ → main source code
plugins/ → cors, logger, and attack-detector plugins
test/ → jest test suite
example/ → usage examples
benchmark/ → performance benchmarks
.github/ → CI workflows📜 API
⚠️ Important: The
targetparameter only uses hostname and port. Any path in the target URL is ignored. UsepathRewritefor path transformations. Read more
createProxy(options)
Create a standalone HTTP/HTTPS proxy server.
Parameters:
target(string, required): Target URL to proxy (hostname and port only, path is ignored)changeOrigin(boolean, optional): Set Host header to target (default: false)port(number, optional): Port for proxy server (default: 3000)pathRewrite(object|function, optional): Path rewrite rules- Object: Key-value pairs where keys are regex patterns and values are replacements
- Function: Custom function that takes path and returns rewritten path
cors(object, optional): CORS configurationorigin(string|string[]|function|''): Allowed origin(s) (default: '')methods(string[]): Allowed HTTP methods (default:['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'])allowedHeaders(string[]): Allowed request headers (default:['Content-Type', 'Authorization'])
logger(object, optional): Logger configurationlogDir(string): Directory to store log files (default: './logs')maxDays(number): Maximum days to keep logs (default: 7)
attackDetector(object|array, optional): Attack detector configuration (single or array)path(string|RegExp): Path to monitorstatusCode(number): Status code to monitorthreshold(number): Max hits before triggertimeWindow(number): Time window in ms (default: 1000)onTrigger(function): Callback function
Returns: http.Server instance
Example:
const server = createProxy({
target: 'https://api.example.com',
changeOrigin: true,
port: 8080,
pathRewrite: {
'^/backend': '/api',
'^/v1': '/api/v1'
},
cors: {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
},
logger: {
logDir: './logs',
maxDays: 14
},
attackDetector: [
{
path: '/api/login',
statusCode: 401,
threshold: 5,
timeWindow: 1000,
onTrigger: (data) => console.log('Login attack:', data.ip)
},
{
path: /^\/api\/.*/,
statusCode: 404,
threshold: 10,
timeWindow: 2000,
onTrigger: (data) => console.log('Scan attack:', data.ip)
}
]
});createProxyMiddleware(options)
Create Express middleware for proxy.
Parameters:
target(string, required): Target URL to proxy (hostname and port only, path is ignored)changeOrigin(boolean, optional): Set Host header to target (default: false)pathRewrite(object|function, optional): Path rewrite rules- Object: Key-value pairs where keys are regex patterns and values are replacements
- Function: Custom function that takes path and returns rewritten path
Returns: Express middleware function
Example:
// Basic usage
app.use('/api', createProxyMiddleware({
target: 'https://api.github.com',
changeOrigin: true
}));
// With path rewrite
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': '/v2/api' // Rewrite /api/* to /v2/api/*
}
}));
// With function
app.use('/backend', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: (path) => path.replace(/^\/old/, '/new')
}));createLogger(options)
Create logger middleware for tracking requests.
Parameters:
logDir(string, optional): Directory to store log files (default: './logs')maxDays(number, optional): Days to keep logs before auto-cleanup (default: 7)
Returns: Express/Connect middleware function
Example:
const createLogger = require('simple-proxy-id/logger');
app.use(createLogger({
logDir: './logs',
maxDays: 14
}));Log Format:
[2025-10-03 14:30:45] 192.168.1.100 GET /api/users 200 125msFeatures:
- Daily rotating log files (YYYY-MM-DD.log)
- Captures real IP (supports Cloudflare Tunnel)
- Automatic cleanup of old logs
- Zero dependencies
createAttackDetector(options)
Create attack detector middleware for brute force protection.
Parameters:
path(string|RegExp, required): Path to monitor (string for exact match, RegExp for pattern)statusCode(number, required): HTTP status code to monitor (e.g., 401, 403, 404)threshold(number, required): Maximum allowed hits within time windowtimeWindow(number, optional): Time window in milliseconds (default: 1000)onTrigger(function, required): Callback function triggered when threshold exceeded
Callback receives:
{
ip: '192.168.1.100',
hits: 5,
path: '/api/login',
timestamp: 1696234567890,
userAgent: 'Mozilla/5.0...'
}Returns: Express/Connect middleware function
Example:
const createAttackDetector = require('simple-proxy-id/attack-detector');
app.use(createAttackDetector({
path: /^\/api\/.*/, // Monitor all API paths with RegExp
statusCode: 404, // Monitor not found responses
threshold: 10, // Trigger after 10 hits
timeWindow: 2000, // Within 2 seconds
onTrigger: (data) => {
// Block IP via your firewall API
console.log(`Blocking IP: ${data.ip}`);
}
}));Features:
- Per-IP tracking and rate limiting
- Support exact path or RegExp pattern matching
- Automatic cleanup of old tracking data
- Custom callback for any blocking mechanism (Cloudflare, Mikrotik, iptables, etc.)
- Zero dependencies
createCors(options)
Create CORS middleware for handling cross-origin requests.
Parameters:
origin(string|string[]|function|'', optional): Allowed origin(s) (default: '')- String: Single domain
'https://example.com' - Array: Multiple domains
['https://example.com', 'https://app.example.com'] - Function: Dynamic validation
(requestOrigin) => boolean - Wildcard:
'*'for allow all origins
- String: Single domain
methods(string[], optional): Allowed HTTP methods (default:['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'])allowedHeaders(string[], optional): Allowed request headers (default:['Content-Type', 'Authorization'])
Returns: Express/Connect middleware function
Example:
const createCors = require('simple-proxy-id/cors');
// Single origin
app.use(createCors({
origin: 'https://example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Multiple origins
app.use(createCors({
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header']
}));
// Dynamic origin validation
app.use(createCors({
origin: (requestOrigin) => {
// Allow all subdomains of example.com
return /^https:\/\/([a-z0-9-]+\.)?example\.com$/.test(requestOrigin);
},
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
// Allow all origins (development only)
app.use(createCors({
origin: '*',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type']
}));Features:
- Flexible origin validation (string, array, function, wildcard)
- Automatic OPTIONS preflight handling (204 No Content)
- Support for dynamic origin validation with custom logic
- Per-request origin validation for security
- Zero dependencies
Use Cases:
- JWT authentication APIs (token in Authorization header)
- Public APIs with domain whitelist
- Microservices with cross-origin communication
- Development environments with wildcard CORS
🔒 Security
This library is designed with security-first principles:
The proxy target is fixed in code and cannot be changed by external requests.
| Attack Vector | Protected |
|---|---|
| Request headers manipulation | ✅ |
| Query string injection | ✅ |
| Request body tampering | ✅ |
| Open proxy abuse | ✅ |
IP Detection Priority:
When logging requests or detecting attacks, the library detects the real client IP in this order:
CF-Connecting-IPheader (Cloudflare Tunnel)X-Forwarded-Forheader (Proxy/Load Balancer)X-Real-IPheader (Nginx proxy)socket.remoteAddress(Direct connection)
🎯 Target URL Behavior (Important!)
Security-First Design
This library follows a security-first principle where the target parameter only uses hostname and port. Any path in the target URL is intentionally ignored.
Why?
- ✅ Explicit Control: Forces developers to explicitly define all path transformations
- ✅ Single Source of Truth: All path logic centralized in
pathRewritefor easier security audits - ✅ Prevents Hidden Behavior: No automatic path concatenation that could lead to confusion
- ✅ Reduces Attack Surface: Clear separation between target destination and path transformation
Common Mistake
// ❌ WRONG: Path in target will be IGNORED
createProxyMiddleware({
target: 'https://api.example.com/v2/backend',
// Path '/v2/backend' is IGNORED!
// Requests will go to: https://api.example.com/your-path
})
// ✅ CORRECT: Use pathRewrite for path transformations
createProxyMiddleware({
target: 'https://api.example.com',
pathRewrite: (path) => '/v2/backend' + path
// Requests will go to: https://api.example.com/v2/backend/your-path
})Understanding Express Routing Behavior
Express handles app.use() and router.all() differently when it comes to URL paths. Understanding this is crucial for proper proxy configuration.
1. app.use() - Full URL Forwarding
When using app.use('/prefix', middleware), Express strips the prefix and passes the FULL remaining URL to the middleware. This is perfect for simple forwarding scenarios.
app.use('/uwupay', createProxyMiddleware({
target: 'http://backend:3800'
}));
// Request: GET /uwupay/api/v1/transactions?status=active
// Express strips: /uwupay
// Middleware receives: req.url = '/api/v1/transactions?status=active'
// Proxies to: http://backend:3800/api/v1/transactions?status=active ✅Key Points:
- ✅ Middleware gets full remaining path after prefix
- ✅ Query string preserved
- ✅ Path structure maintained
- ✅ No
pathRewriteneeded for simple forwarding
2. router.all('/pattern/:param') - Full URL Retained
When using router with path parameters, Express keeps the full URL path in req.url and extracts parameters to req.params. You MUST use pathRewrite to transform the path.
const router = express.Router();
router.all('/api/v2/:path', createProxyMiddleware({
target: 'http://backend:7707',
pathRewrite: (path) => path.replace('/api/v2', '/backend') // ⚠️ Required!
}));
// Request: GET /api/v2/test-koneksi?foo=bar
// Express matches: /api/v2/:path
// Middleware receives: req.url = '/api/v2/test-koneksi?foo=bar' (FULL path!)
// req.params = { path: 'test-koneksi' } (extracted param)
// pathRewrite transforms: '/api/v2/test-koneksi' → '/backend/test-koneksi'
// Proxies to: http://backend:7707/backend/test-koneksi?foo=bar ✅Key Points:
- ⚠️ Middleware receives FULL URL path (not stripped like app.use)
- ✅ Parameters extracted to
req.paramsfor handler use - ✅ Query string preserved
- ⚠️
pathRewriteis REQUIRED to transform route pattern to backend path
Comparison Table
| Method | Express Behavior | req.url Value | Need pathRewrite? | Use Case |
|---|---|---|---|---|
app.use('/prefix') |
Strip prefix, forward remaining URL | Path after prefix (e.g., /users) |
❌ Optional | Simple forwarding |
router.all('/pattern/:param') |
Keep full path, extract params to req.params | Full path (e.g., /api/v2/users) |
✅ Required | Pattern transformation |
Why Different Behavior?
These are two different Express concepts serving different purposes:
app.use() = Middleware Mounting 🔌
- Purpose: Attach middleware at a specific path (mounting point)
- Design: Strip prefix so middleware is portable and reusable
- Analogy: Like mounting a USB drive at
/mnt/usb- files inside don't know they're mounted there - Use case: Forward entire service/path to another backend
router.all() = Route Pattern Matching 🎯
- Purpose: Match URL patterns and extract parameters (data extraction)
- Design: Only pass matched parameter values, not routing metadata
- Analogy: Like function parameters
getUserById(id)- only receives the value123, not the function signature - Use case: Handle specific API endpoints with parameter extraction
This is not a bug or inconsistency - it's Express core design for two different use cases!
Why This Matters
Wrong approach (missing pathRewrite):
// ❌ This will NOT work as expected
router.all('/api/v2/:path', createProxyMiddleware({
target: 'http://backend:7707'
// Missing pathRewrite!
}));
// Request: /api/v2/test-koneksi
// Proxies to: http://backend:7707/test-koneksi
// Backend expects: http://backend:7707/backend/test-koneksi ❌Correct approach:
// ✅ Use pathRewrite to prepend required path
router.all('/api/v2/:path', createProxyMiddleware({
target: 'http://backend:7707',
pathRewrite: (path) => '/backend' + path
}));
// Request: /api/v2/test-koneksi
// Proxies to: http://backend:7707/backend/test-koneksi ✅Preserve Prefix in Target
If you want to preserve the Express route prefix in your target URL:
// Request: GET /api/users → Proxy to: https://backend.com/api/users
app.use('/api', createProxyMiddleware({
target: 'https://backend.com',
pathRewrite: (path) => '/api' + path // Add /api back
}));
// Middleware receives: '/users'
// pathRewrite transforms: '/users' → '/api/users'
// Proxies to: https://backend.com/api/usersReal-World Examples
Example 1: Simple forwarding (Express strips prefix)
// Request: /uwupay/transactions → Proxy to: http://backend:3800/transactions
app.use('/uwupay', createProxyMiddleware({
target: 'http://backend:3800'
// No pathRewrite needed, Express already stripped /uwupay
}));Example 2: Add path prefix to target
// Request: /api/users → Proxy to: https://backend.com/v2/users
app.use('/api', createProxyMiddleware({
target: 'https://backend.com',
pathRewrite: (path) => '/v2' + path
// Express strips /api, we add /v2
}));Example 3: Multiple path transformations
// Without app.use (handle routing manually)
createProxyMiddleware({
target: 'https://api.example.com',
pathRewrite: (path) => {
// /old-api/users → /new-api/v2/users
if (path.startsWith('/old-api')) {
return path.replace('/old-api', '/new-api/v2');
}
// /public/data → /api/public/data
if (path.startsWith('/public')) {
return '/api' + path;
}
// Default: /something → /api/v1/something
return '/api/v1' + path;
}
});Example 4: Complex routing with backend path
// Request: /api/v2/users → Proxy to: http://backend:7707/backend/users
app.use('/api/v2', createProxyMiddleware({
target: 'http://backend:7707',
pathRewrite: (path) => '/backend' + path
// Express strips /api/v2
// Middleware receives: /users
// pathRewrite adds: /backend/users
}));Security Benefits
Scenario: Path Traversal Prevention
// With path ignored (SECURE ✅)
target: 'https://api.example.com'
pathRewrite: (path) => '/admin/../public' + path
// Result: https://api.example.com/admin/../public/users
// Server will reject invalid path
// If path was auto-appended (VULNERABLE ❌)
target: 'https://api.example.com/public'
pathRewrite: (path) => '/../admin' + path
// Result: https://api.example.com/public/../admin/users
// = https://api.example.com/admin/users (PATH TRAVERSAL!)By ignoring path in target URL, this library forces explicit path control and prevents accidental security vulnerabilities.
🤝 Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Ways to contribute:
- Report bugs and suggest features
- Submit pull requests
- Improve documentation
- Develop plugins
📄 License
MIT © 2025