diff --git a/.env.example b/.env.example index 1a11646..6d71edd 100644 --- a/.env.example +++ b/.env.example @@ -24,4 +24,8 @@ AUTO_START_SESSIONS=TRUE # OPTIONAL, AUTO START SESSIONS ON SERVER STARTUP(TRUE ## Session File Storage ## SESSIONS_PATH=./sessions # OPTIONAL -ENABLE_SWAGGER_ENDPOINT=TRUE # OPTIONAL, ENABLE SWAGGER ENDPOINT FOR API DOCUMENTATION \ No newline at end of file +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 \ No newline at end of file diff --git a/REVERSE_PROXY_SETUP.md b/REVERSE_PROXY_SETUP.md new file mode 100644 index 0000000..28564d5 --- /dev/null +++ b/REVERSE_PROXY_SETUP.md @@ -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 +``` diff --git a/src/app.js b/src/app.js index 1a3041b..6f01123 100644 --- a/src/app.js +++ b/src/app.js @@ -1,14 +1,23 @@ require('./routes') const express = require('express') const { routes } = require('./routes') -const { maxAttachmentSize } = require('./config') +const { maxAttachmentSize, basePath, trustProxy } = require('./config') const app = express() // Initialize Express app 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.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 diff --git a/src/config.js b/src/config.js index 3cd8f26..82a57dc 100644 --- a/src/config.js +++ b/src/config.js @@ -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 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 basePath = process.env.BASE_PATH || '' +const trustProxy = process.env.TRUST_PROXY ? (process.env.TRUST_PROXY).toLowerCase() === 'true' : false module.exports = { sessionFolderPath, @@ -43,5 +45,7 @@ module.exports = { logLevel, enableWebHook, enableWebSocket, - autoStartSessions + autoStartSessions, + basePath, + trustProxy } diff --git a/src/middleware.js b/src/middleware.js index 122f0f7..722c5bb 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -72,7 +72,11 @@ const sessionValidation = async (req, res, next) => { const rateLimiter = rateLimiting({ limit: rateLimitMax, 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) => { diff --git a/src/websocket.js b/src/websocket.js index 5006cc3..019bf31 100644 --- a/src/websocket.js +++ b/src/websocket.js @@ -1,5 +1,5 @@ const { WebSocketServer } = require('ws') -const { enableWebSocket } = require('./config') +const { enableWebSocket, basePath } = require('./config') const { logger } = require('./logger') const wssMap = new Map() @@ -52,10 +52,15 @@ const triggerWebSocket = (sessionId, dataType, data) => { } 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) - 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) if (server) { server.handleUpgrade(request, socket, head, (ws) => {