feat: add reverse proxy support with configurable base path and trust proxy settings (#47)
This commit is contained in:
parent
70154165fa
commit
62860a658c
6 changed files with 204 additions and 9 deletions
|
|
@ -24,4 +24,8 @@ AUTO_START_SESSIONS=TRUE # OPTIONAL, AUTO START SESSIONS ON SERVER STARTUP(TRUE
|
||||||
|
|
||||||
## Session File Storage ##
|
## Session File Storage ##
|
||||||
SESSIONS_PATH=./sessions # OPTIONAL
|
SESSIONS_PATH=./sessions # OPTIONAL
|
||||||
ENABLE_SWAGGER_ENDPOINT=TRUE # OPTIONAL, ENABLE SWAGGER ENDPOINT FOR API DOCUMENTATION
|
ENABLE_SWAGGER_ENDPOINT=TRUE # OPTIONAL, ENABLE SWAGGER ENDPOINT FOR API DOCUMENTATION
|
||||||
|
|
||||||
|
## Reverse Proxy / Load Balancer ##
|
||||||
|
BASE_PATH= # OPTIONAL, BASE PATH FOR MOUNTING ROUTES (e.g., /api/v1/whatsapp)
|
||||||
|
TRUST_PROXY=FALSE # OPTIONAL, ENABLE WHEN BEHIND REVERSE PROXY/LOAD BALANCER
|
||||||
169
REVERSE_PROXY_SETUP.md
Normal file
169
REVERSE_PROXY_SETUP.md
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
# Reverse Proxy / Kong Setup Guide
|
||||||
|
|
||||||
|
This document provides configuration examples for deploying WWebJS API behind reverse proxies like Kong, Nginx, or other load balancers.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The following environment variables have been added to support reverse proxy deployments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Base path for mounting all routes (optional)
|
||||||
|
BASE_PATH=/api/v1/whatsapp
|
||||||
|
|
||||||
|
# Enable trust proxy for proper IP forwarding (required for reverse proxy)
|
||||||
|
TRUST_PROXY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kong Configuration
|
||||||
|
|
||||||
|
### Basic Kong Route Setup
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Kong route configuration
|
||||||
|
routes:
|
||||||
|
- name: wwebjs-api
|
||||||
|
paths: ["/api/v1/whatsapp"]
|
||||||
|
strip_path: true # Important: removes the prefix before forwarding
|
||||||
|
preserve_host: false
|
||||||
|
protocols: ["http", "https"]
|
||||||
|
service: wwebjs-service
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: wwebjs-service
|
||||||
|
url: http://wwebjs-api:3000
|
||||||
|
connect_timeout: 60000
|
||||||
|
write_timeout: 60000
|
||||||
|
read_timeout: 60000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Kong with WebSocket Support
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Kong route for WebSocket connections
|
||||||
|
routes:
|
||||||
|
- name: wwebjs-websocket
|
||||||
|
paths: ["/api/v1/whatsapp/ws"]
|
||||||
|
strip_path: true
|
||||||
|
protocols: ["ws", "wss"]
|
||||||
|
service: wwebjs-websocket-service
|
||||||
|
|
||||||
|
services:
|
||||||
|
- name: wwebjs-websocket-service
|
||||||
|
url: http://wwebjs-api:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nginx Configuration
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
upstream wwebjs_backend {
|
||||||
|
server wwebjs-api:3000;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name api.yourdomain.com;
|
||||||
|
|
||||||
|
location /api/v1/whatsapp/ {
|
||||||
|
proxy_pass http://wwebjs_backend/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
|
||||||
|
# WebSocket support
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
# Timeouts for long-running operations
|
||||||
|
proxy_connect_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose with Reverse Proxy
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
wwebjs-api:
|
||||||
|
image: avoylenko/wwebjs-api:latest
|
||||||
|
container_name: wwebjs-api
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
# Reverse proxy configuration
|
||||||
|
- BASE_PATH=/api/v1/whatsapp
|
||||||
|
- TRUST_PROXY=true
|
||||||
|
|
||||||
|
# Other configurations
|
||||||
|
- BASE_WEBHOOK_URL=https://api.yourdomain.com/api/v1/whatsapp/localCallbackExample
|
||||||
|
- API_KEY=your_secure_api_key
|
||||||
|
- ENABLE_LOCAL_CALLBACK_EXAMPLE=false
|
||||||
|
- ENABLE_SWAGGER_ENDPOINT=true
|
||||||
|
volumes:
|
||||||
|
- ./sessions:/usr/src/app/sessions
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
image: nginx:alpine
|
||||||
|
container_name: nginx-proxy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
depends_on:
|
||||||
|
- wwebjs-api
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
api-network:
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoint Examples
|
||||||
|
|
||||||
|
With `BASE_PATH=/api/v1/whatsapp` configured:
|
||||||
|
|
||||||
|
### Original endpoints:
|
||||||
|
- `GET /session/start/ABCD`
|
||||||
|
- `GET /client/getContacts/ABCD`
|
||||||
|
- `WebSocket: ws://localhost:3000/ws/ABCD`
|
||||||
|
|
||||||
|
### Behind reverse proxy:
|
||||||
|
- `GET https://api.yourdomain.com/api/v1/whatsapp/session/start/ABCD`
|
||||||
|
- `GET https://api.yourdomain.com/api/v1/whatsapp/client/getContacts/ABCD`
|
||||||
|
- `WebSocket: wss://api.yourdomain.com/api/v1/whatsapp/ws/ABCD`
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
1. **Strip Path**: Always configure your reverse proxy to strip the base path before forwarding to the application
|
||||||
|
2. **Trust Proxy**: Set `TRUST_PROXY=true` to ensure proper IP detection for rate limiting
|
||||||
|
3. **WebSocket Headers**: Ensure `X-Forwarded-Host` header is properly forwarded for WebSocket connections
|
||||||
|
4. **Timeouts**: Configure appropriate timeouts for WhatsApp operations which can take time
|
||||||
|
5. **HTTPS**: Use HTTPS in production and update `BASE_WEBHOOK_URL` accordingly
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues:
|
||||||
|
|
||||||
|
1. **404 Errors**: Check if `strip_path` is enabled in your reverse proxy
|
||||||
|
2. **WebSocket Connection Failed**: Ensure WebSocket upgrade headers are properly forwarded
|
||||||
|
3. **Rate Limiting Issues**: Verify `TRUST_PROXY=true` is set and `X-Forwarded-For` header is forwarded
|
||||||
|
4. **Webhook Callbacks**: Update `BASE_WEBHOOK_URL` to use the external domain with base path
|
||||||
|
|
||||||
|
### Testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test API endpoint
|
||||||
|
curl https://api.yourdomain.com/api/v1/whatsapp/ping
|
||||||
|
|
||||||
|
# Test WebSocket connection
|
||||||
|
wscat -c wss://api.yourdomain.com/api/v1/whatsapp/ws/test
|
||||||
|
```
|
||||||
13
src/app.js
13
src/app.js
|
|
@ -1,14 +1,23 @@
|
||||||
require('./routes')
|
require('./routes')
|
||||||
const express = require('express')
|
const express = require('express')
|
||||||
const { routes } = require('./routes')
|
const { routes } = require('./routes')
|
||||||
const { maxAttachmentSize } = require('./config')
|
const { maxAttachmentSize, basePath, trustProxy } = require('./config')
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
|
|
||||||
// Initialize Express app
|
// Initialize Express app
|
||||||
app.disable('x-powered-by')
|
app.disable('x-powered-by')
|
||||||
|
|
||||||
|
// Configure trust proxy for reverse proxy compatibility
|
||||||
|
if (trustProxy) {
|
||||||
|
app.set('trust proxy', true)
|
||||||
|
}
|
||||||
|
|
||||||
app.use(express.json({ limit: maxAttachmentSize + 1000000 }))
|
app.use(express.json({ limit: maxAttachmentSize + 1000000 }))
|
||||||
app.use(express.urlencoded({ limit: maxAttachmentSize + 1000000, extended: true }))
|
app.use(express.urlencoded({ limit: maxAttachmentSize + 1000000, extended: true }))
|
||||||
app.use('/', routes)
|
|
||||||
|
// Mount routes with configurable base path
|
||||||
|
const mountPath = basePath || '/'
|
||||||
|
app.use(mountPath, routes)
|
||||||
|
|
||||||
module.exports = app
|
module.exports = app
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ const logLevel = process.env.LOG_LEVEL || 'info'
|
||||||
const enableWebHook = process.env.ENABLE_WEBHOOK ? (process.env.ENABLE_WEBHOOK).toLowerCase() === 'true' : true
|
const enableWebHook = process.env.ENABLE_WEBHOOK ? (process.env.ENABLE_WEBHOOK).toLowerCase() === 'true' : true
|
||||||
const enableWebSocket = process.env.ENABLE_WEBSOCKET ? (process.env.ENABLE_WEBSOCKET).toLowerCase() === 'true' : false
|
const enableWebSocket = process.env.ENABLE_WEBSOCKET ? (process.env.ENABLE_WEBSOCKET).toLowerCase() === 'true' : false
|
||||||
const autoStartSessions = process.env.AUTO_START_SESSIONS ? (process.env.AUTO_START_SESSIONS).toLowerCase() === 'true' : true
|
const autoStartSessions = process.env.AUTO_START_SESSIONS ? (process.env.AUTO_START_SESSIONS).toLowerCase() === 'true' : true
|
||||||
|
const basePath = process.env.BASE_PATH || ''
|
||||||
|
const trustProxy = process.env.TRUST_PROXY ? (process.env.TRUST_PROXY).toLowerCase() === 'true' : false
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sessionFolderPath,
|
sessionFolderPath,
|
||||||
|
|
@ -43,5 +45,7 @@ module.exports = {
|
||||||
logLevel,
|
logLevel,
|
||||||
enableWebHook,
|
enableWebHook,
|
||||||
enableWebSocket,
|
enableWebSocket,
|
||||||
autoStartSessions
|
autoStartSessions,
|
||||||
|
basePath,
|
||||||
|
trustProxy
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,11 @@ const sessionValidation = async (req, res, next) => {
|
||||||
const rateLimiter = rateLimiting({
|
const rateLimiter = rateLimiting({
|
||||||
limit: rateLimitMax,
|
limit: rateLimitMax,
|
||||||
windowMs: rateLimitWindowMs,
|
windowMs: rateLimitWindowMs,
|
||||||
message: "You can't make any more requests at the moment. Try again later"
|
message: "You can't make any more requests at the moment. Try again later",
|
||||||
|
// Use real client IP when behind reverse proxy
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
return req.ip || req.connection.remoteAddress
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const sessionSwagger = async (req, res, next) => {
|
const sessionSwagger = async (req, res, next) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
const { WebSocketServer } = require('ws')
|
const { WebSocketServer } = require('ws')
|
||||||
const { enableWebSocket } = require('./config')
|
const { enableWebSocket, basePath } = require('./config')
|
||||||
const { logger } = require('./logger')
|
const { logger } = require('./logger')
|
||||||
const wssMap = new Map()
|
const wssMap = new Map()
|
||||||
|
|
||||||
|
|
@ -52,10 +52,15 @@ const triggerWebSocket = (sessionId, dataType, data) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpgrade = (request, socket, head) => {
|
const handleUpgrade = (request, socket, head) => {
|
||||||
const baseUrl = 'ws://' + request.headers.host + '/'
|
const host = request.headers['x-forwarded-host'] || request.headers.host
|
||||||
|
const baseUrl = 'ws://' + host + '/'
|
||||||
const { pathname } = new URL(request.url, baseUrl)
|
const { pathname } = new URL(request.url, baseUrl)
|
||||||
if (pathname.startsWith('/ws/')) {
|
|
||||||
const sessionId = pathname.split('/')[2]
|
// Handle base path for WebSocket connections
|
||||||
|
const wsPath = basePath ? `${basePath}/ws/` : '/ws/'
|
||||||
|
if (pathname.startsWith(wsPath)) {
|
||||||
|
const pathParts = pathname.split('/')
|
||||||
|
const sessionId = basePath ? pathParts[3] : pathParts[2]
|
||||||
const server = wssMap.get(sessionId)
|
const server = wssMap.get(sessionId)
|
||||||
if (server) {
|
if (server) {
|
||||||
server.handleUpgrade(request, socket, head, (ws) => {
|
server.handleUpgrade(request, socket, head, (ws) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue