Files
Bram bf404135b5
Build and Push Docker Images / build-and-push (push) Failing after 1m39s
socket broker
2025-08-21 10:59:09 +02:00

11 KiB

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:

# 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):

    Authorization: Bearer your-api-key-here
    
  2. X-API-Key Header:

    X-API-Key: your-api-key-here
    
  3. Query Parameter:

    POST /publish?api_key=your-api-key-here
    

Quick Start

Using Docker

# 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

# 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

# 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

socket.emit("subscribe", "user-notifications");
socket.on("subscribed", (data) => {
  console.log(`Subscribed to ${data.topic}`);
});

Unsubscribe from a topic

socket.emit("unsubscribe", "user-notifications");
socket.on("unsubscribed", (data) => {
  console.log(`Unsubscribed from ${data.topic}`);
});

Receive messages

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

socket.on("error", (error) => {
  console.error("Socket error:", error.message);
});

HTTP API (Port 8080)

Public Endpoints (No Authentication Required)

Health Check

GET /health

Response:

{
  "status": "healthy",
  "connections": 5,
  "topics": 3,
  "timestamp": "2024-01-01T12:00:00.000Z",
  "apiKeyConfigured": true
}

List Topics

GET /topics

Response:

{
  "topics": [
    {
      "topic": "user-notifications",
      "subscribers": 2
    },
    {
      "topic": "system-alerts",
      "subscribers": 1
    }
  ]
}

Get Topic Info

GET /topics/{topic}

Response:

{
  "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

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:

POST /publish/{topic}
Authorization: Bearer your-api-key-here
Content-Type: application/json

{
  "message": "New message received",
  "data": { "userId": 123, "type": "email" }
}

Response:

{
  "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:

{
  "error": "Unauthorized",
  "message": "API key is required. Use Authorization header or x-api-key header or api_key query parameter."
}

Frontend Example (JavaScript)

<!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)

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)

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

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

    # Generate a secure API key
    openssl rand -base64 32
    
  2. Environment Variables: Never hardcode API keys in your code

    # 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:

# 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:

docker logs socket-broker

Health Check

Verify the application is healthy and API key is configured:

curl http://localhost:8080/health