This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
# Socket Broker
|
||||
|
||||
A WebSocket broker application that handles topic-based messaging between frontend clients and backend services with API key authentication.
|
||||
|
||||
## Features
|
||||
|
||||
- **WebSocket Server** (Port 3000): Handles client connections and topic subscriptions
|
||||
- **HTTP API** (Port 8080): Allows backend services to publish messages to topics
|
||||
- **API Key Authentication**: Secure access control for publishing messages
|
||||
- **Topic-based messaging**: Clients subscribe to topics and receive messages when published
|
||||
- **Real-time communication**: Instant message delivery to all subscribed clients
|
||||
- **Health monitoring**: Built-in health checks and status endpoints
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Frontend Clients ←→ WebSocket Server (Port 3000)
|
||||
↕
|
||||
Backend Services ←→ HTTP API (Port 8080) [🔐 API Key Required]
|
||||
```
|
||||
|
||||
## Security
|
||||
|
||||
**⚠️ IMPORTANT**: The HTTP API endpoints that publish messages require a valid API key for authentication. This ensures that only authorized backend services can send messages to your WebSocket clients.
|
||||
|
||||
### API Key Configuration
|
||||
|
||||
Set the `API_KEY` environment variable to secure your application:
|
||||
|
||||
```bash
|
||||
# Set a strong, random API key
|
||||
export API_KEY="your-secure-api-key-here"
|
||||
|
||||
# Or use in docker-compose
|
||||
API_KEY=your-secure-api-key-here docker-compose up
|
||||
```
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
The API accepts the API key in three ways:
|
||||
|
||||
1. **Authorization Header** (Recommended):
|
||||
|
||||
```bash
|
||||
Authorization: Bearer your-api-key-here
|
||||
```
|
||||
|
||||
2. **X-API-Key Header**:
|
||||
|
||||
```bash
|
||||
X-API-Key: your-api-key-here
|
||||
```
|
||||
|
||||
3. **Query Parameter**:
|
||||
```bash
|
||||
POST /publish?api_key=your-api-key-here
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using Docker
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t socket-broker .
|
||||
|
||||
# Run the container with API key
|
||||
docker run -d \
|
||||
--name socket-broker \
|
||||
-p 3000:3000 \
|
||||
-p 8080:8080 \
|
||||
-e API_KEY="your-secure-api-key" \
|
||||
socket-broker
|
||||
```
|
||||
|
||||
### Using Docker Compose
|
||||
|
||||
```bash
|
||||
# Copy environment file
|
||||
cp env.example .env
|
||||
|
||||
# Edit .env and set your API key
|
||||
nano .env
|
||||
|
||||
# Start the service
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Using Node.js directly
|
||||
|
||||
```bash
|
||||
# Set API key
|
||||
export API_KEY="your-secure-api-key"
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start the application
|
||||
npm start
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------- | --------------------------- | ---------------------------------------- |
|
||||
| `WS_PORT` | `3000` | WebSocket server port |
|
||||
| `API_PORT` | `8080` | HTTP API server port |
|
||||
| `API_KEY` | `default-api-key-change-me` | **REQUIRED**: API key for authentication |
|
||||
|
||||
## WebSocket API (Port 3000)
|
||||
|
||||
### Connection
|
||||
|
||||
Connect to `ws://localhost:3000` using Socket.IO client.
|
||||
|
||||
### Events
|
||||
|
||||
#### Subscribe to a topic
|
||||
|
||||
```javascript
|
||||
socket.emit("subscribe", "user-notifications");
|
||||
socket.on("subscribed", (data) => {
|
||||
console.log(`Subscribed to ${data.topic}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### Unsubscribe from a topic
|
||||
|
||||
```javascript
|
||||
socket.emit("unsubscribe", "user-notifications");
|
||||
socket.on("unsubscribed", (data) => {
|
||||
console.log(`Unsubscribed from ${data.topic}`);
|
||||
});
|
||||
```
|
||||
|
||||
#### Receive messages
|
||||
|
||||
```javascript
|
||||
socket.on("message", (data) => {
|
||||
console.log(`Message from topic ${data.topic}:`, data.message);
|
||||
console.log("Data:", data.data);
|
||||
console.log("Timestamp:", data.timestamp);
|
||||
});
|
||||
```
|
||||
|
||||
#### Error handling
|
||||
|
||||
```javascript
|
||||
socket.on("error", (error) => {
|
||||
console.error("Socket error:", error.message);
|
||||
});
|
||||
```
|
||||
|
||||
## HTTP API (Port 8080)
|
||||
|
||||
### Public Endpoints (No Authentication Required)
|
||||
|
||||
#### Health Check
|
||||
|
||||
```bash
|
||||
GET /health
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"connections": 5,
|
||||
"topics": 3,
|
||||
"timestamp": "2024-01-01T12:00:00.000Z",
|
||||
"apiKeyConfigured": true
|
||||
}
|
||||
```
|
||||
|
||||
#### List Topics
|
||||
|
||||
```bash
|
||||
GET /topics
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"topics": [
|
||||
{
|
||||
"topic": "user-notifications",
|
||||
"subscribers": 2
|
||||
},
|
||||
{
|
||||
"topic": "system-alerts",
|
||||
"subscribers": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Get Topic Info
|
||||
|
||||
```bash
|
||||
GET /topics/{topic}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"topic": "user-notifications",
|
||||
"subscribers": 2
|
||||
}
|
||||
```
|
||||
|
||||
### Protected Endpoints (API Key Required)
|
||||
|
||||
#### Response Codes
|
||||
|
||||
- **200 OK**: Message published successfully to subscribers
|
||||
- **204 No Content**: Message published successfully but no subscribers for the topic
|
||||
- **400 Bad Request**: Invalid request (missing topic, message, or data)
|
||||
- **401 Unauthorized**: Invalid or missing API key
|
||||
- **500 Internal Server Error**: Server error
|
||||
|
||||
#### Publish Message
|
||||
|
||||
```bash
|
||||
POST /publish
|
||||
Authorization: Bearer your-api-key-here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"topic": "user-notifications",
|
||||
"message": "New message received",
|
||||
"data": { "userId": 123, "type": "email" }
|
||||
}
|
||||
```
|
||||
|
||||
Alternative endpoint:
|
||||
|
||||
```bash
|
||||
POST /publish/{topic}
|
||||
Authorization: Bearer your-api-key-here
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "New message received",
|
||||
"data": { "userId": 123, "type": "email" }
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Message published to topic 'user-notifications'",
|
||||
"subscribers": 2,
|
||||
"payload": {
|
||||
"topic": "user-notifications",
|
||||
"message": "New message received",
|
||||
"data": { "userId": 123, "type": "email" },
|
||||
"timestamp": "2024-01-01T12:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: If there are no subscribers for the topic, the API returns `204 No Content` instead of an error. This indicates that the message was published successfully, but there were no recipients to receive it.
|
||||
|
||||
### Authentication Errors
|
||||
|
||||
If you don't provide a valid API key, you'll get:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Unauthorized",
|
||||
"message": "API key is required. Use Authorization header or x-api-key header or api_key query parameter."
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend Example (JavaScript)
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Socket Broker Client</title>
|
||||
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Socket Broker Client</h1>
|
||||
<div id="status">Connecting...</div>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
const socket = io("http://localhost:3000");
|
||||
|
||||
socket.on("connect", () => {
|
||||
document.getElementById("status").textContent = "Connected";
|
||||
|
||||
// Subscribe to a topic
|
||||
socket.emit("subscribe", "user-notifications");
|
||||
});
|
||||
|
||||
socket.on("subscribed", (data) => {
|
||||
console.log("Subscribed:", data);
|
||||
});
|
||||
|
||||
socket.on("message", (data) => {
|
||||
const messagesDiv = document.getElementById("messages");
|
||||
const messageElement = document.createElement("div");
|
||||
messageElement.innerHTML = `
|
||||
<strong>${data.topic}:</strong> ${data.message}
|
||||
<br><small>${data.timestamp}</small>
|
||||
`;
|
||||
messagesDiv.appendChild(messageElement);
|
||||
});
|
||||
|
||||
socket.on("disconnect", () => {
|
||||
document.getElementById("status").textContent = "Disconnected";
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Backend Example (Python)
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
|
||||
# Get API key from environment variable
|
||||
API_KEY = os.getenv('API_KEY', 'your-api-key-here')
|
||||
|
||||
def publish_message(topic, message, data=None):
|
||||
url = "http://localhost:8080/publish"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {API_KEY}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
"topic": topic,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers)
|
||||
return response.json()
|
||||
|
||||
# Example usage
|
||||
result = publish_message(
|
||||
topic="user-notifications",
|
||||
message="New email received",
|
||||
data={"userId": 123, "emailId": "abc123"}
|
||||
)
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Backend Example (Node.js)
|
||||
|
||||
```javascript
|
||||
const axios = require("axios");
|
||||
|
||||
const API_KEY = process.env.API_KEY || "your-api-key-here";
|
||||
|
||||
async function publishMessage(topic, message, data = null) {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
"http://localhost:8080/publish",
|
||||
{
|
||||
topic,
|
||||
message,
|
||||
data,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${API_KEY}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Error publishing message:",
|
||||
error.response?.data || error.message
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Example usage
|
||||
publishMessage("user-notifications", "New notification", { userId: 123 })
|
||||
.then((result) => console.log("Message published:", result))
|
||||
.catch((error) => console.error("Failed to publish:", error));
|
||||
```
|
||||
|
||||
## Docker Compose Example
|
||||
|
||||
```yaml
|
||||
version: "3.8"
|
||||
services:
|
||||
socket-broker:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- WS_PORT=3000
|
||||
- API_PORT=8080
|
||||
- API_KEY=${API_KEY:-default-api-key-change-me}
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"node",
|
||||
"-e",
|
||||
"require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })",
|
||||
]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
The application provides several monitoring endpoints:
|
||||
|
||||
- `/health` - Overall health status and API key configuration status
|
||||
- `/topics` - List of active topics and subscriber counts
|
||||
- `/topics/{topic}` - Specific topic information
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Generate Strong API Keys**: Use cryptographically secure random strings
|
||||
|
||||
```bash
|
||||
# Generate a secure API key
|
||||
openssl rand -base64 32
|
||||
```
|
||||
|
||||
2. **Environment Variables**: Never hardcode API keys in your code
|
||||
|
||||
```bash
|
||||
# Good
|
||||
export API_KEY="$(openssl rand -base64 32)"
|
||||
|
||||
# Bad
|
||||
export API_KEY="my-secret-key-123"
|
||||
```
|
||||
|
||||
3. **Rotate Keys Regularly**: Change your API keys periodically
|
||||
|
||||
4. **Limit Access**: Only share API keys with trusted backend services
|
||||
|
||||
5. **HTTPS in Production**: Use HTTPS/WSS in production environments
|
||||
|
||||
6. **Network Security**: Consider using internal networks for backend communication
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **401 Unauthorized**: Check that you're providing the correct API key
|
||||
2. **Port conflicts**: Ensure ports 3000 and 8080 are available
|
||||
3. **CORS errors**: Check that the frontend origin is allowed
|
||||
4. **Connection refused**: Verify the container is running and ports are exposed
|
||||
|
||||
### Testing Authentication
|
||||
|
||||
Test your API key authentication:
|
||||
|
||||
```bash
|
||||
# Test without API key (should fail with 401)
|
||||
curl -X POST http://localhost:8080/publish \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic":"test","message":"hello"}'
|
||||
|
||||
# Test with correct API key but no subscribers (should succeed with 204)
|
||||
curl -X POST http://localhost:8080/publish \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic":"test","message":"hello"}'
|
||||
|
||||
# Test with correct API key and subscribers (should succeed with 200)
|
||||
curl -X POST http://localhost:8080/publish \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"topic":"test","message":"hello"}'
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
Check application logs for detailed information:
|
||||
|
||||
```bash
|
||||
docker logs socket-broker
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
Verify the application is healthy and API key is configured:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
Reference in New Issue
Block a user