ninja-reverse-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +480 -0
- package/config.example.yaml +99 -0
- package/dist/Serviceregistry.d.ts +31 -0
- package/dist/Serviceregistry.d.ts.map +1 -0
- package/dist/Serviceregistry.js +95 -0
- package/dist/Serviceregistry.js.map +1 -0
- package/dist/auto-scaler.d.ts +27 -0
- package/dist/auto-scaler.d.ts.map +1 -0
- package/dist/auto-scaler.js +133 -0
- package/dist/auto-scaler.js.map +1 -0
- package/dist/cache.d.ts +22 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +112 -0
- package/dist/cache.js.map +1 -0
- package/dist/config-schema.d.ts +52 -0
- package/dist/config-schema.d.ts.map +1 -0
- package/dist/config-schema.js +77 -0
- package/dist/config-schema.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +73 -0
- package/dist/config.js.map +1 -0
- package/dist/health.d.ts +5 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +78 -0
- package/dist/health.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/loadBalancer.d.ts +31 -0
- package/dist/loadBalancer.d.ts.map +1 -0
- package/dist/loadBalancer.js +158 -0
- package/dist/loadBalancer.js.map +1 -0
- package/dist/rate-limiter.d.ts +16 -0
- package/dist/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limiter.js +59 -0
- package/dist/rate-limiter.js.map +1 -0
- package/dist/server-schema.d.ts +25 -0
- package/dist/server-schema.d.ts.map +1 -0
- package/dist/server-schema.js +18 -0
- package/dist/server-schema.js.map +1 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +518 -0
- package/dist/server.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# Ninja Reverse Proxy
|
|
2
|
+
|
|
3
|
+
A production-grade, backend-agnostic **Layer 7 Reverse Proxy** built from scratch in **TypeScript** and **Node.js**.
|
|
4
|
+
|
|
5
|
+
Designed in the same philosophy as **Nginx**, **Traefik**, and **HAProxy** — configure it once, point it at any backend, and it handles everything else.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Developer → GitHub → Jenkins CI/CD → Docker → Any Backend
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## What it is
|
|
14
|
+
|
|
15
|
+
Ninja Reverse Proxy is a fully configurable, self-contained reverse proxy that works with **any HTTP backend**:
|
|
16
|
+
|
|
17
|
+
- Express · Fastify · NestJS
|
|
18
|
+
- Django · FastAPI · Flask
|
|
19
|
+
- Spring Boot · Quarkus
|
|
20
|
+
- Go (net/http, Gin, Echo)
|
|
21
|
+
- ASP.NET Core
|
|
22
|
+
- Kubernetes Services
|
|
23
|
+
- Docker Compose services
|
|
24
|
+
- Any service that speaks HTTP
|
|
25
|
+
|
|
26
|
+
The proxy never cares what technology runs behind the URLs. You configure upstreams in `config.yaml` and the proxy routes, balances, caches, rate-limits, and health-checks them automatically.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
| Feature | Details |
|
|
33
|
+
|---|---|
|
|
34
|
+
| **Cluster Architecture** | Master/Worker pattern via Node.js `cluster` — uses all CPU cores |
|
|
35
|
+
| **Round-Robin Load Balancing** | Equal request distribution across healthy upstreams |
|
|
36
|
+
| **Circuit Breaker** | Marks upstreams DOWN after configurable failure threshold |
|
|
37
|
+
| **Redis Response Cache** | GET response caching with configurable TTL; auto-invalidated on writes |
|
|
38
|
+
| **Per-Route Rate Limiting** | Sliding window rate limiter per client IP per route |
|
|
39
|
+
| **Service Registry** | Backends self-register, deregister, and send heartbeats |
|
|
40
|
+
| **Continuous Health Checks** | Every 10 seconds — auto-removes and auto-recovers upstreams |
|
|
41
|
+
| **HTTPS / TLS Termination** | Full SSL at the proxy; all HTTP auto-redirected (301) |
|
|
42
|
+
| **Auto Scaling** | Optionally spawns/kills upstream servers dynamically based on load |
|
|
43
|
+
| **Retry Logic** | Up to 2 retries on upstream failure, each on a different worker |
|
|
44
|
+
| **Graceful Shutdown** | Drains all connections on SIGTERM / SIGINT |
|
|
45
|
+
| **Admin API** | Live stats for load balancer, cache, registry, auto scaler |
|
|
46
|
+
| **YAML Configuration** | One file, fully validated with Zod — no source code changes needed |
|
|
47
|
+
| **Docker + Kubernetes** | Ships with Compose and K8s manifests out of the box |
|
|
48
|
+
| **Jenkins CI/CD** | Declarative pipeline included |
|
|
49
|
+
| **SonarQube** | `sonar-project.properties` included for static analysis |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Architecture
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
┌─────────────────────────────────────────────────┐
|
|
57
|
+
│ Ninja Reverse Proxy │
|
|
58
|
+
│ │
|
|
59
|
+
Client ──HTTPS──► │ Master Process │
|
|
60
|
+
│ ├── Rate Limiter (per-IP, per-route) │
|
|
61
|
+
│ ├── Redis Cache (GET responses) │
|
|
62
|
+
│ ├── Load Balancer (round-robin / ip-hash / …) │
|
|
63
|
+
│ ├── Health Checker (every 10s) │
|
|
64
|
+
│ ├── Service Registry │
|
|
65
|
+
│ └── Auto Scaler (optional) │
|
|
66
|
+
│ │
|
|
67
|
+
│ Worker Processes (one per CPU core) │
|
|
68
|
+
│ └── Forward requests via keepAlive TCP │
|
|
69
|
+
└───────────┬─────────────────────────────────────┘
|
|
70
|
+
│
|
|
71
|
+
┌───────────────┼───────────────┐
|
|
72
|
+
▼ ▼ ▼
|
|
73
|
+
backend-a backend-b backend-c
|
|
74
|
+
(Express) (Django) (Spring Boot)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### CI/CD Flow
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
Developer
|
|
81
|
+
│ edits code / config
|
|
82
|
+
▼
|
|
83
|
+
GitHub
|
|
84
|
+
▼
|
|
85
|
+
Jenkins (Jenkinsfile)
|
|
86
|
+
├── Setup (npm ci)
|
|
87
|
+
├── Static Analysis — Lint + npm audit (parallel)
|
|
88
|
+
├── Unit Tests
|
|
89
|
+
├── Build Artifact (tsc)
|
|
90
|
+
└── Deploy to Production (main branch, prod env)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
### 1. Clone and generate TLS certificates
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git clone https://github.com/praveenkumar-co/reverse-proxy.git
|
|
101
|
+
cd reverse-proxy
|
|
102
|
+
|
|
103
|
+
# Generate a self-signed certificate (for development)
|
|
104
|
+
openssl req -x509 -newkey rsa:4096 \
|
|
105
|
+
-keyout key.pem -out cert.pem \
|
|
106
|
+
-days 365 -nodes
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 2. Configure your backends
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
cp config.example.yaml config.yaml
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Edit `config.yaml` — replace the example upstreams with your real backend URLs:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
upstreams:
|
|
119
|
+
- id: my-api
|
|
120
|
+
url: http://my-api:8000
|
|
121
|
+
|
|
122
|
+
- id: my-frontend
|
|
123
|
+
url: http://my-frontend:3000
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### 3. Run
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Proxy + Redis only (you bring your own backends)
|
|
130
|
+
docker-compose up --build
|
|
131
|
+
|
|
132
|
+
# Or run the built-in demo (backend-a + backend-b included)
|
|
133
|
+
cd examples/docker-compose && docker-compose up --build
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 4. Test
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
curl -k https://localhost:8443/
|
|
140
|
+
curl -k https://localhost:8443/__lb-stats
|
|
141
|
+
curl -k https://localhost:8443/__registry
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Configuration Reference
|
|
147
|
+
|
|
148
|
+
`config.example.yaml` is the fully commented template. Copy it to `config.yaml` to get started.
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
server:
|
|
152
|
+
|
|
153
|
+
listen: 8080 # HTTP port — redirects all traffic to HTTPS
|
|
154
|
+
httpsPort: 8443 # HTTPS port — main entry point
|
|
155
|
+
workers: 2 # Set to your CPU core count (run: nproc)
|
|
156
|
+
|
|
157
|
+
loadBalancing:
|
|
158
|
+
strategy: round-robin # round-robin | least-connections | ip-hash | random
|
|
159
|
+
failureThreshold: 3 # Mark upstream DOWN after 3 consecutive failures
|
|
160
|
+
recoveryTimeMs: 15000 # Retry a DOWN upstream after 15 seconds
|
|
161
|
+
|
|
162
|
+
autoScaling:
|
|
163
|
+
enabled: false # false → static backends (like Nginx)
|
|
164
|
+
# true → dynamic server spawning based on load
|
|
165
|
+
minServers: 2
|
|
166
|
+
maxServers: 10
|
|
167
|
+
scaleUpAt: 10 # Spawn a server when connections exceed this
|
|
168
|
+
scaleDownAt: 2 # Kill a server when connections drop below this
|
|
169
|
+
cooldownMs: 60000
|
|
170
|
+
startPort: 9000
|
|
171
|
+
proxyPort: 8080
|
|
172
|
+
|
|
173
|
+
cache:
|
|
174
|
+
enabled: false # true → cache GET responses in Redis
|
|
175
|
+
host: redis
|
|
176
|
+
port: 6379
|
|
177
|
+
ttlSeconds: 60
|
|
178
|
+
|
|
179
|
+
upstreams:
|
|
180
|
+
- id: backend-a # Any name — used to reference in paths
|
|
181
|
+
url: http://backend-a:3001 # Any HTTP URL — any technology
|
|
182
|
+
|
|
183
|
+
- id: backend-b
|
|
184
|
+
url: http://backend-b:3002
|
|
185
|
+
|
|
186
|
+
paths:
|
|
187
|
+
- path: / # Route all traffic to both backends
|
|
188
|
+
upstream:
|
|
189
|
+
- backend-a
|
|
190
|
+
- backend-b
|
|
191
|
+
rateLimit:
|
|
192
|
+
windowMs: 60000 # 1-minute window
|
|
193
|
+
maxRequests: 100000 # per client IP
|
|
194
|
+
|
|
195
|
+
- path: /api # Route /api to a specific backend only
|
|
196
|
+
upstream:
|
|
197
|
+
- backend-a
|
|
198
|
+
|
|
199
|
+
headers:
|
|
200
|
+
- key: X-Forwarded-For
|
|
201
|
+
value: client_ip
|
|
202
|
+
- key: X-Real-IP
|
|
203
|
+
value: client_ip
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Docker
|
|
209
|
+
|
|
210
|
+
### Build the proxy image
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
docker build -t ninja-reverse-proxy:latest .
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Run with Docker Compose (proxy + Redis only)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Uses docker-compose.yml in project root
|
|
220
|
+
docker-compose up --build
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Your own backend services connect to the `proxy-network` and are listed in `config.yaml`.
|
|
224
|
+
|
|
225
|
+
### Run the full built-in demo
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Spins up proxy + redis + backend-a + backend-b
|
|
229
|
+
cd examples/docker-compose
|
|
230
|
+
docker-compose up --build
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Connect your real app
|
|
234
|
+
|
|
235
|
+
Add your app as a service in your own `docker-compose.yml` and connect it to the proxy network:
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
services:
|
|
239
|
+
|
|
240
|
+
my-express-app:
|
|
241
|
+
image: my-express-app:latest
|
|
242
|
+
networks:
|
|
243
|
+
- proxy-network
|
|
244
|
+
|
|
245
|
+
networks:
|
|
246
|
+
proxy-network:
|
|
247
|
+
external: true
|
|
248
|
+
name: reverse-proxy_proxy-network
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Then add it to `config.yaml`:
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
upstreams:
|
|
255
|
+
- id: my-express-app
|
|
256
|
+
url: http://my-express-app:3000
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Kubernetes
|
|
262
|
+
|
|
263
|
+
See [`k8s/README.md`](k8s/README.md) for the full deployment guide.
|
|
264
|
+
|
|
265
|
+
Quick overview:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
# Apply all manifests
|
|
269
|
+
kubectl apply -f k8s/
|
|
270
|
+
|
|
271
|
+
# Port-forward for local testing
|
|
272
|
+
kubectl port-forward svc/ninja-reverse-proxy-svc 8080:8080 8443:8443
|
|
273
|
+
|
|
274
|
+
# Verify
|
|
275
|
+
curl -k https://localhost:8443/__lb-stats
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The proxy config is mounted as a Kubernetes ConfigMap — change it without rebuilding the image.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Auto Scaler
|
|
283
|
+
|
|
284
|
+
The Auto Scaler is **fully optional** and controlled entirely by `config.yaml`.
|
|
285
|
+
|
|
286
|
+
### Static mode (default — works like Nginx)
|
|
287
|
+
|
|
288
|
+
```yaml
|
|
289
|
+
autoScaling:
|
|
290
|
+
enabled: false
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The proxy uses only the upstream servers listed in `config.yaml`. This is appropriate for production setups where you manage your own backend services.
|
|
294
|
+
|
|
295
|
+
### Dynamic mode
|
|
296
|
+
|
|
297
|
+
```yaml
|
|
298
|
+
autoScaling:
|
|
299
|
+
enabled: true
|
|
300
|
+
minServers: 2
|
|
301
|
+
maxServers: 10
|
|
302
|
+
scaleUpAt: 10 # spawn a new server when total active connections exceed this
|
|
303
|
+
scaleDownAt: 2 # kill the oldest server when connections drop below this
|
|
304
|
+
cooldownMs: 60000 # minimum wait between scale events
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
When enabled, the proxy dynamically spawns (`server-template.js`) and kills backend servers based on active connection count. See `examples/docker-compose/` for a working demonstration.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Admin API
|
|
312
|
+
|
|
313
|
+
All admin endpoints are available on the HTTPS port.
|
|
314
|
+
|
|
315
|
+
| Endpoint | Method | Description |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| `/__lb-stats` | GET | Load balancer stats + healthy upstreams |
|
|
318
|
+
| `/__cache-stats` | GET | Redis cache hit/miss stats |
|
|
319
|
+
| `/__registry` | GET | All registered services |
|
|
320
|
+
| `/__autoscaler-stats` | GET | Auto scaler status |
|
|
321
|
+
| `/__registry/register` | POST | Register a new upstream |
|
|
322
|
+
| `/__registry/deregister/:id` | DELETE | Deregister an upstream |
|
|
323
|
+
| `/__registry/heartbeat/:id` | PUT | Upstream heartbeat ping |
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
curl -k https://localhost:8443/__lb-stats
|
|
327
|
+
curl -k https://localhost:8443/__cache-stats
|
|
328
|
+
curl -k https://localhost:8443/__registry
|
|
329
|
+
curl -k https://localhost:8443/__autoscaler-stats
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Jenkins CI/CD Pipeline
|
|
335
|
+
|
|
336
|
+
A declarative `Jenkinsfile` is included in the project root.
|
|
337
|
+
|
|
338
|
+
**Pipeline stages:**
|
|
339
|
+
|
|
340
|
+
1. **Setup** — `npm ci` (deterministic install)
|
|
341
|
+
2. **Static Analysis** — Lint + `npm audit` (parallel)
|
|
342
|
+
3. **Unit Tests** — conditional on `RUN_TESTS` parameter
|
|
343
|
+
4. **Build Artifact** — `npm run build` (TypeScript → JavaScript)
|
|
344
|
+
5. **Deploy to Production** — branch `main` + `ENV_TYPE=prod` + manual approval gate
|
|
345
|
+
|
|
346
|
+
**Global options:** 1-hour timeout · last 10 builds retained · no concurrent builds · timestamps on every log line.
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## SonarQube
|
|
351
|
+
|
|
352
|
+
`sonar-project.properties` is included. To run a scan:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
sonar-scanner \
|
|
356
|
+
-Dsonar.host.url=http://localhost:9000 \
|
|
357
|
+
-Dsonar.login=YOUR_SONAR_TOKEN
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Scans the `src/` directory. Excludes `node_modules/`, `dist/`, certificates, and CI files.
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Security Scanning (Trivy)
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
# Scan dependencies
|
|
368
|
+
trivy fs .
|
|
369
|
+
|
|
370
|
+
# Scan the Docker image
|
|
371
|
+
docker build -t ninja-reverse-proxy:latest .
|
|
372
|
+
trivy image ninja-reverse-proxy:latest
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Project Structure
|
|
378
|
+
|
|
379
|
+
```
|
|
380
|
+
ninja-reverse-proxy/
|
|
381
|
+
├── src/ ← Proxy source (TypeScript)
|
|
382
|
+
│ ├── index.ts → CLI entry point
|
|
383
|
+
│ ├── server.ts → Master + Worker proxy engine
|
|
384
|
+
│ ├── loadBalancer.ts → Round-robin with circuit breaker
|
|
385
|
+
│ ├── auto-scaler.ts → Dynamic server scaling
|
|
386
|
+
│ ├── health.ts → Health checker (initial + continuous)
|
|
387
|
+
│ ├── Serviceregistry.ts → Service registry (register/heartbeat/deregister)
|
|
388
|
+
│ ├── cache.ts → Redis cache (get/set/invalidate/stats)
|
|
389
|
+
│ ├── rate-limiter.ts → Sliding window rate limiter
|
|
390
|
+
│ ├── config-schema.ts → Zod config validation schema
|
|
391
|
+
│ ├── config.ts → YAML parser
|
|
392
|
+
│ └── server-schema.ts → Worker IPC message schema
|
|
393
|
+
│
|
|
394
|
+
├── k8s/ ← Kubernetes manifests
|
|
395
|
+
│ ├── configmap.yaml
|
|
396
|
+
│ ├── tls-secret.yaml
|
|
397
|
+
│ ├── proxy-deployment.yaml
|
|
398
|
+
│ ├── proxy-service.yaml
|
|
399
|
+
│ └── README.md
|
|
400
|
+
│
|
|
401
|
+
├── examples/ ← Integration examples (NOT part of the proxy)
|
|
402
|
+
│ ├── docker-compose/ ← Full demo stack (backend-a + backend-b)
|
|
403
|
+
│ │ ├── server-template.js → Minimal demo backend
|
|
404
|
+
│ │ ├── Dockerfile.server → Demo backend image
|
|
405
|
+
│ │ ├── docker-compose.yml → Full demo stack
|
|
406
|
+
│ │ ├── config.yaml → Demo config
|
|
407
|
+
│ │ └── README.md
|
|
408
|
+
│ └── express/ ← Express.js integration example
|
|
409
|
+
│ ├── server.js
|
|
410
|
+
│ └── README.md
|
|
411
|
+
│
|
|
412
|
+
├── Dockerfile ← Proxy image (multi-stage build)
|
|
413
|
+
├── Jenkinsfile ← CI/CD declarative pipeline
|
|
414
|
+
├── sonar-project.properties ← SonarQube config
|
|
415
|
+
├── docker-compose.yml ← Proxy + Redis only
|
|
416
|
+
├── config.example.yaml ← Fully commented configuration template
|
|
417
|
+
├── .dockerignore
|
|
418
|
+
├── .gitignore
|
|
419
|
+
├── package.json
|
|
420
|
+
├── tsconfig.json
|
|
421
|
+
└── README.md
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Tech Stack
|
|
427
|
+
|
|
428
|
+
| Technology | Purpose |
|
|
429
|
+
|---|---|
|
|
430
|
+
| TypeScript | Full type safety |
|
|
431
|
+
| Node.js Cluster | Master/Worker multi-process architecture |
|
|
432
|
+
| Redis | Response caching |
|
|
433
|
+
| Zod | Schema validation for config and worker messages |
|
|
434
|
+
| YAML | Human-readable configuration |
|
|
435
|
+
| Commander | CLI entry point |
|
|
436
|
+
| Docker Compose | Orchestration |
|
|
437
|
+
| Kubernetes | Production cluster deployment |
|
|
438
|
+
| Jenkins | CI/CD pipeline |
|
|
439
|
+
| SonarQube | Static code analysis |
|
|
440
|
+
| Trivy | Container security scanning |
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Request Lifecycle
|
|
445
|
+
|
|
446
|
+
```
|
|
447
|
+
Client → :8080 HTTP → 301 redirect to HTTPS
|
|
448
|
+
Client → :8443 HTTPS
|
|
449
|
+
1. Rate limiter checks client IP — 429 if exceeded
|
|
450
|
+
2. GET requests → Redis cache checked — HIT returns instantly
|
|
451
|
+
3. Write requests (POST/PUT/PATCH/DELETE) → cache invalidated
|
|
452
|
+
4. Request body assembled from chunks
|
|
453
|
+
5. Load balancer picks a healthy upstream
|
|
454
|
+
6. Worker process forwards request over keepAlive TCP
|
|
455
|
+
7. Upstream responds → reply sent back via IPC
|
|
456
|
+
8. Master sends response to client + caches (GET)
|
|
457
|
+
9. On failure → circuit breaker records it, retry up to 2 times
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
---
|
|
461
|
+
|
|
462
|
+
## Performance Tips
|
|
463
|
+
|
|
464
|
+
- Set `workers` to `nproc` — never exceed your CPU core count
|
|
465
|
+
- Set `cache.enabled: true` for read-heavy APIs — reduces upstream load significantly
|
|
466
|
+
- Keep `autoScaling.maxServers` realistic for your hardware (3–4 for a 4-core machine)
|
|
467
|
+
- `keepAlive: true` is set by default — avoids TCP handshake overhead per request
|
|
468
|
+
- Monitor `/__lb-stats` in production to see which upstreams are under load
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## Contributing
|
|
473
|
+
|
|
474
|
+
Pull requests are welcome. Please open an issue first to discuss significant changes.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## License
|
|
479
|
+
|
|
480
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# ============================================================
|
|
2
|
+
# Ninja Reverse Proxy — Configuration Template
|
|
3
|
+
# ============================================================
|
|
4
|
+
#
|
|
5
|
+
# USAGE:
|
|
6
|
+
# Copy this file to config.yaml and edit it for your setup.
|
|
7
|
+
# cp config.example.yaml config.yaml
|
|
8
|
+
#
|
|
9
|
+
# The proxy works with any HTTP backend — Express, Django,
|
|
10
|
+
# Spring Boot, FastAPI, Go, ASP.NET, Kubernetes Services, etc.
|
|
11
|
+
# ============================================================
|
|
12
|
+
|
|
13
|
+
server:
|
|
14
|
+
|
|
15
|
+
# ── Ports ──────────────────────────────────────────────────
|
|
16
|
+
listen: 8080 # HTTP port — all traffic redirected to HTTPS (301)
|
|
17
|
+
httpsPort: 8443 # HTTPS port — main entry point for all requests
|
|
18
|
+
|
|
19
|
+
# Number of worker processes to fork.
|
|
20
|
+
# Best practice: set this to the number of CPU cores (run `nproc`).
|
|
21
|
+
# Never exceed your core count — it causes context switching overhead.
|
|
22
|
+
workers: 2
|
|
23
|
+
|
|
24
|
+
# ── Load Balancing ─────────────────────────────────────────
|
|
25
|
+
loadBalancing:
|
|
26
|
+
strategy: round-robin # Options: round-robin | least-connections | ip-hash | random
|
|
27
|
+
failureThreshold: 3 # Mark an upstream DOWN after this many consecutive failures
|
|
28
|
+
recoveryTimeMs: 15000 # Attempt to recover a DOWN upstream after this delay (ms)
|
|
29
|
+
|
|
30
|
+
# ── Redis Cache ────────────────────────────────────────────
|
|
31
|
+
#
|
|
32
|
+
# GET responses are cached. Write requests (POST/PUT/PATCH/DELETE)
|
|
33
|
+
# automatically invalidate the cache for that path.
|
|
34
|
+
#
|
|
35
|
+
cache:
|
|
36
|
+
enabled: false # Set to true to enable Redis response caching
|
|
37
|
+
host: redis # Redis hostname (use 'localhost' outside Docker)
|
|
38
|
+
port: 6379 # Redis port
|
|
39
|
+
ttlSeconds: 60 # Cache TTL — responses expire after this many seconds
|
|
40
|
+
|
|
41
|
+
# ── Upstream Backends ──────────────────────────────────────
|
|
42
|
+
#
|
|
43
|
+
# List every backend service this proxy should route traffic to.
|
|
44
|
+
# The proxy does not care what technology these services use.
|
|
45
|
+
#
|
|
46
|
+
# Examples:
|
|
47
|
+
# - id: api-server
|
|
48
|
+
# url: http://api-server:8000
|
|
49
|
+
#
|
|
50
|
+
# - id: user-service
|
|
51
|
+
# url: http://user-service:8080
|
|
52
|
+
#
|
|
53
|
+
# - id: my-django-app
|
|
54
|
+
# url: http://django:8000
|
|
55
|
+
#
|
|
56
|
+
# - id: my-spring-boot
|
|
57
|
+
# url: http://spring-boot:8080
|
|
58
|
+
#
|
|
59
|
+
upstreams:
|
|
60
|
+
- id: backend-a
|
|
61
|
+
url: http://backend-a:3001
|
|
62
|
+
|
|
63
|
+
- id: backend-b
|
|
64
|
+
url: http://backend-b:3002
|
|
65
|
+
|
|
66
|
+
# ── Path Routing Rules ─────────────────────────────────────
|
|
67
|
+
#
|
|
68
|
+
# Each path entry defines:
|
|
69
|
+
# path → URL prefix to match
|
|
70
|
+
# upstream → list of upstream IDs that serve this route
|
|
71
|
+
# rateLimit → optional per-IP sliding window rate limit for this route
|
|
72
|
+
#
|
|
73
|
+
paths:
|
|
74
|
+
- path: /
|
|
75
|
+
upstream:
|
|
76
|
+
- backend-a
|
|
77
|
+
- backend-b
|
|
78
|
+
rateLimit:
|
|
79
|
+
windowMs: 60000 # 1-minute sliding window
|
|
80
|
+
maxRequests: 100000 # maximum requests per IP per window
|
|
81
|
+
|
|
82
|
+
- path: /api
|
|
83
|
+
upstream:
|
|
84
|
+
- backend-a
|
|
85
|
+
- backend-b
|
|
86
|
+
rateLimit:
|
|
87
|
+
windowMs: 60000
|
|
88
|
+
maxRequests: 50000
|
|
89
|
+
|
|
90
|
+
# ── Custom Request Headers ─────────────────────────────────
|
|
91
|
+
#
|
|
92
|
+
# Headers appended to every proxied request before forwarding upstream.
|
|
93
|
+
# 'client_ip' is a special value — replaced with the real client IP.
|
|
94
|
+
#
|
|
95
|
+
headers:
|
|
96
|
+
- key: X-Forwarded-For
|
|
97
|
+
value: client_ip
|
|
98
|
+
- key: X-Real-IP
|
|
99
|
+
value: client_ip
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ServiceInstance {
|
|
2
|
+
id: string;
|
|
3
|
+
url: string;
|
|
4
|
+
registeredAt: number;
|
|
5
|
+
lastHeartbeat: number;
|
|
6
|
+
status: 'UP' | 'DOWN';
|
|
7
|
+
metadata?: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
export interface RegistryConfig {
|
|
10
|
+
heartbeatTimeoutMs?: number;
|
|
11
|
+
cleanupIntervalMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class ServiceRegistry {
|
|
14
|
+
private services;
|
|
15
|
+
private heartbeatTimeoutMs;
|
|
16
|
+
private onRegisterCallbacks;
|
|
17
|
+
private onDeregisterCallbacks;
|
|
18
|
+
constructor(config: RegistryConfig);
|
|
19
|
+
register(instance: Omit<ServiceInstance, 'registeredAt' | 'lastHeartbeat' | 'status'>): ServiceInstance;
|
|
20
|
+
deregister(id: string): boolean;
|
|
21
|
+
heartbeat(id: string): boolean;
|
|
22
|
+
getHealthy(): ServiceInstance[];
|
|
23
|
+
getAll(): ServiceInstance[];
|
|
24
|
+
get(id: string): ServiceInstance | undefined;
|
|
25
|
+
private checkHeartbeats;
|
|
26
|
+
onRegister(cb: (service: ServiceInstance) => void): void;
|
|
27
|
+
onDeregister(cb: (service: ServiceInstance) => void): void;
|
|
28
|
+
getStats(): object;
|
|
29
|
+
}
|
|
30
|
+
export declare const registry: ServiceRegistry;
|
|
31
|
+
//# sourceMappingURL=Serviceregistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Serviceregistry.d.ts","sourceRoot":"","sources":["../src/Serviceregistry.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAA2C;IAC3D,OAAO,CAAC,kBAAkB,CAAS;IAEnC,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,qBAAqB,CAA8C;gBAE/D,MAAM,EAAE,cAAc;IAMlC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,GAAG,eAAe,GAAG,QAAQ,CAAC,GAAG,eAAe;IAavG,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAW/B,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAU9B,UAAU,IAAI,eAAe,EAAE;IAG/B,MAAM,IAAI,eAAe,EAAE;IAI3B,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAI5C,OAAO,CAAC,eAAe;IAcvB,UAAU,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI;IAIxD,YAAY,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,GAAG,IAAI;IAG1D,QAAQ,IAAI,MAAM;CAcnB;AACD,eAAO,MAAM,QAAQ,iBAGnB,CAAA"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class ServiceRegistry {
|
|
2
|
+
services = new Map();
|
|
3
|
+
heartbeatTimeoutMs;
|
|
4
|
+
// on every registry of a function
|
|
5
|
+
onRegisterCallbacks = [];
|
|
6
|
+
onDeregisterCallbacks = [];
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.heartbeatTimeoutMs = config.heartbeatTimeoutMs ?? 30_000;
|
|
9
|
+
setInterval(() => {
|
|
10
|
+
this.checkHeartbeats();
|
|
11
|
+
}, config.cleanupIntervalMs ?? 10_000);
|
|
12
|
+
}
|
|
13
|
+
register(instance) {
|
|
14
|
+
const service = {
|
|
15
|
+
...instance,
|
|
16
|
+
registeredAt: Date.now(),
|
|
17
|
+
lastHeartbeat: Date.now(),
|
|
18
|
+
// mark the incoming server to UP
|
|
19
|
+
status: 'UP',
|
|
20
|
+
};
|
|
21
|
+
this.services.set(instance.id, service);
|
|
22
|
+
console.log(`[Registry] Service REGISTERED: ${instance.id} → ${instance.url}`);
|
|
23
|
+
this.onRegisterCallbacks.forEach(callback => callback(service));
|
|
24
|
+
return service;
|
|
25
|
+
}
|
|
26
|
+
deregister(id) {
|
|
27
|
+
const service = this.services.get(id);
|
|
28
|
+
if (!service) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
service.status = 'DOWN';
|
|
32
|
+
this.services.delete(id);
|
|
33
|
+
console.log(`[Registry] Service DEREGISTERED: ${id}`);
|
|
34
|
+
this.onDeregisterCallbacks.forEach(callback => callback(service));
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
heartbeat(id) {
|
|
38
|
+
const service = this.services.get(id);
|
|
39
|
+
if (!service) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
service.lastHeartbeat = Date.now();
|
|
43
|
+
service.status = 'UP';
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
// to get all up services
|
|
47
|
+
getHealthy() {
|
|
48
|
+
return [...this.services.values()].filter(s => s.status === 'UP');
|
|
49
|
+
}
|
|
50
|
+
getAll() {
|
|
51
|
+
return [...this.services.values()];
|
|
52
|
+
}
|
|
53
|
+
// get single service
|
|
54
|
+
get(id) {
|
|
55
|
+
return this.services.get(id);
|
|
56
|
+
}
|
|
57
|
+
// to check heartbeat is consistent or not
|
|
58
|
+
checkHeartbeats() {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
for (const [id, service] of this.services) {
|
|
61
|
+
if (service.metadata?.dynamic === "true" &&
|
|
62
|
+
service.status === 'UP' &&
|
|
63
|
+
now - service.lastHeartbeat > this.heartbeatTimeoutMs) {
|
|
64
|
+
console.log(`[Registry] Service TIMED OUT (no heartbeat): ${id}`);
|
|
65
|
+
service.status = 'DOWN';
|
|
66
|
+
this.onDeregisterCallbacks.forEach(callback => callback(service));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
onRegister(cb) {
|
|
71
|
+
this.onRegisterCallbacks.push(cb);
|
|
72
|
+
}
|
|
73
|
+
onDeregister(cb) {
|
|
74
|
+
this.onDeregisterCallbacks.push(cb);
|
|
75
|
+
}
|
|
76
|
+
getStats() {
|
|
77
|
+
return {
|
|
78
|
+
total: this.services.size,
|
|
79
|
+
healthy: this.getHealthy().length,
|
|
80
|
+
services: this.getAll().map(s => ({
|
|
81
|
+
id: s.id,
|
|
82
|
+
url: s.url,
|
|
83
|
+
status: s.status,
|
|
84
|
+
uptime: `${Math.floor((Date.now() - s.registeredAt) / 1000)}s`,
|
|
85
|
+
lastHeartbeat: `${Math.floor((Date.now() - s.lastHeartbeat) / 1000)}s ago`,
|
|
86
|
+
metadata: s.metadata,
|
|
87
|
+
})),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export const registry = new ServiceRegistry({
|
|
92
|
+
heartbeatTimeoutMs: 30_000,
|
|
93
|
+
cleanupIntervalMs: 10_000
|
|
94
|
+
});
|
|
95
|
+
//# sourceMappingURL=Serviceregistry.js.map
|