socket broker
Build and Push Docker Images / build-and-push (push) Failing after 1m39s

This commit is contained in:
2025-08-21 10:59:09 +02:00
parent 4470eb400a
commit bf404135b5
9 changed files with 2910 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy application code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
# Change ownership of the app directory
RUN chown -R nodejs:nodejs /app
USER nodejs
# Expose ports
EXPOSE 3000 8080
# Set default environment variables
ENV WS_PORT=3000
ENV API_PORT=8080
ENV API_KEY=default-api-key-change-me
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:8080/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
# Start the application
CMD ["npm", "start"]
+514
View File
@@ -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
```
+35
View File
@@ -0,0 +1,35 @@
version: "3.8"
services:
socket-broker:
build: .
container_name: socket-broker
ports:
- "3000:3000" # WebSocket server
- "8080:8080" # HTTP API
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
networks:
- socket-broker-network
env_file:
- .env
networks:
socket-broker-network:
driver: bridge
+16
View File
@@ -0,0 +1,16 @@
# Socket Broker Environment Configuration
# Copy this file to .env and update the values
# WebSocket server port
WS_PORT=3000
# HTTP API server port
API_PORT=8080
# API Key for authentication (REQUIRED for production)
# This key must be provided in HTTP requests to publish messages
# Generate a strong, random key for production use
API_KEY=your-secure-api-key-here
# Node environment
NODE_ENV=production
+281
View File
@@ -0,0 +1,281 @@
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
// Create Express app and HTTP server
const app = express();
const server = createServer(app);
// Create Socket.IO server
const io = new Server(server, {
cors: {
origin: "*", // Allow all origins for development
methods: ["GET", "POST"]
}
});
// Store topic subscriptions
const topicSubscriptions = new Map(); // topic -> Set of socket IDs
const socketTopics = new Map(); // socket ID -> Set of topics
// Get API key from environment variable
const API_KEY = process.env.API_KEY || 'default-api-key-change-me';
// API Key authentication middleware
const authenticateApiKey = (req, res, next) => {
const authHeader = req.headers.authorization;
const apiKey = req.headers['x-api-key'] || req.query.api_key;
if (!authHeader && !apiKey) {
return res.status(401).json({
error: 'Unauthorized',
message: 'API key is required. Use Authorization header or x-api-key header or api_key query parameter.'
});
}
// Check Authorization header (Bearer token format)
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.substring(7);
if (token === API_KEY) {
return next();
}
}
// Check x-api-key header
if (apiKey && apiKey === API_KEY) {
return next();
}
return res.status(401).json({
error: 'Unauthorized',
message: 'Invalid API key'
});
};
// Middleware
app.use(cors());
app.use(express.json());
// WebSocket connection handling (Port 3000)
io.on('connection', (socket) => {
console.log(`Client connected: ${socket.id}`);
// Handle topic subscription
socket.on('subscribe', (topic) => {
if (!topic || typeof topic !== 'string') {
socket.emit('error', { message: 'Invalid topic' });
return;
}
// Add socket to topic
if (!topicSubscriptions.has(topic)) {
topicSubscriptions.set(topic, new Set());
}
topicSubscriptions.get(topic).add(socket.id);
// Add topic to socket
if (!socketTopics.has(socket.id)) {
socketTopics.set(socket.id, new Set());
}
socketTopics.get(socket.id).add(topic);
console.log(`Socket ${socket.id} subscribed to topic: ${topic}`);
socket.emit('subscribed', { topic, message: `Successfully subscribed to ${topic}` });
});
// Handle topic unsubscription
socket.on('unsubscribe', (topic) => {
if (!topic || typeof topic !== 'string') {
socket.emit('error', { message: 'Invalid topic' });
return;
}
// Remove socket from topic
if (topicSubscriptions.has(topic)) {
topicSubscriptions.get(topic).delete(socket.id);
if (topicSubscriptions.get(topic).size === 0) {
topicSubscriptions.delete(topic);
}
}
// Remove topic from socket
if (socketTopics.has(socket.id)) {
socketTopics.get(socket.id).delete(topic);
if (socketTopics.get(socket.id).size === 0) {
socketTopics.delete(socket.id);
}
}
console.log(`Socket ${socket.id} unsubscribed from topic: ${topic}`);
socket.emit('unsubscribed', { topic, message: `Successfully unsubscribed from ${topic}` });
});
// Handle disconnect
socket.on('disconnect', () => {
console.log(`Client disconnected: ${socket.id}`);
// Clean up subscriptions
if (socketTopics.has(socket.id)) {
const topics = socketTopics.get(socket.id);
topics.forEach(topic => {
if (topicSubscriptions.has(topic)) {
topicSubscriptions.get(topic).delete(socket.id);
if (topicSubscriptions.get(topic).size === 0) {
topicSubscriptions.delete(topic);
}
}
});
socketTopics.delete(socket.id);
}
});
});
// HTTP API endpoints (Port 8080)
// Public endpoints (no authentication required)
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
connections: io.engine.clientsCount,
topics: topicSubscriptions.size,
timestamp: new Date().toISOString(),
apiKeyConfigured: !!API_KEY && API_KEY !== 'default-api-key-change-me'
});
});
app.get('/topics', (req, res) => {
const topics = Array.from(topicSubscriptions.keys()).map(topic => ({
topic,
subscribers: topicSubscriptions.get(topic).size
}));
res.json({ topics });
});
app.get('/topics/:topic', (req, res) => {
const { topic } = req.params;
if (!topicSubscriptions.has(topic)) {
return res.status(404).json({ error: 'Topic not found' });
}
const subscribers = topicSubscriptions.get(topic).size;
res.json({ topic, subscribers });
});
// Protected endpoints (require API key authentication)
app.post('/publish', authenticateApiKey, (req, res) => {
const { topic, message, data } = req.body;
if (!topic || typeof topic !== 'string') {
return res.status(400).json({ error: 'Topic is required and must be a string' });
}
if (!message && !data) {
return res.status(400).json({ error: 'Either message or data is required' });
}
// Check if topic has subscribers
if (!topicSubscriptions.has(topic)) {
// Return 204 No Content when there are no subscribers
// The operation succeeded, there just weren't any recipients
return res.status(204).send();
}
const payload = {
topic,
message: message || null,
data: data || null,
timestamp: new Date().toISOString()
};
// Emit to all subscribers of the topic
const subscribers = topicSubscriptions.get(topic);
subscribers.forEach(socketId => {
io.to(socketId).emit('message', payload);
});
console.log(`Message published to topic '${topic}' for ${subscribers.size} subscribers`);
res.json({
success: true,
message: `Message published to topic '${topic}'`,
subscribers: subscribers.size,
payload
});
});
// Alternative endpoint with topic in URL (also protected)
app.post('/publish/:topic', authenticateApiKey, (req, res) => {
const { topic } = req.params;
const { message, data } = req.body;
if (!message && !data) {
return res.status(400).json({ error: 'Either message or data is required' });
}
// Check if topic has subscribers
if (!topicSubscriptions.has(topic)) {
// Return 204 No Content when there are no subscribers
// The operation succeeded, there just weren't any recipients
return res.status(204).send();
}
const payload = {
topic,
message: message || null,
data: data || null,
timestamp: new Date().toISOString()
};
// Emit to all subscribers of the topic
const subscribers = topicSubscriptions.get(topic);
subscribers.forEach(socketId => {
io.to(socketId).emit('message', payload);
});
console.log(`Message published to topic '${topic}' for ${subscribers.size} subscribers`);
res.json({
success: true,
message: `Message published to topic '${topic}'`,
subscribers: subscribers.size,
payload
});
});
// Start servers
const WS_PORT = process.env.WS_PORT || 3000;
const API_PORT = process.env.API_PORT || 8080;
// Start WebSocket server
io.listen(WS_PORT);
console.log(`WebSocket server running on port ${WS_PORT}`);
// Start HTTP API server
app.listen(API_PORT, () => {
console.log(`HTTP API server running on port ${API_PORT}`);
console.log(`Health check: http://localhost:${API_PORT}/health`);
console.log(`Topics: http://localhost:${API_PORT}/topics`);
console.log(`Publish: POST http://localhost:${API_PORT}/publish`);
console.log(`API Key configured: ${API_KEY !== 'default-api-key-change-me' ? 'Yes' : 'No (using default)'}`);
if (API_KEY === 'default-api-key-change-me') {
console.log(`⚠️ WARNING: Using default API key. Set API_KEY environment variable for production!`);
}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
process.exit(0);
});
});
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "socket-broker",
"version": "1.0.0",
"description": "A WebSocket broker with HTTP API for topic-based messaging",
"main": "index.js",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"dependencies": {
"express": "^4.18.2",
"socket.io": "^4.7.2",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.0.1"
},
"keywords": [
"websocket",
"broker",
"socket.io",
"express"
],
"author": "",
"license": "MIT"
}
+251
View File
@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
Test script for Socket Broker HTTP API
"""
import requests
import json
import time
import os
from datetime import datetime
# Configuration
API_BASE_URL = "http://localhost:8080"
TOPIC_NAME = "test-topic"
# Get API key from environment variable or use default
API_KEY = os.getenv('API_KEY', 'default-api-key-change-me')
def get_auth_headers():
"""Get authentication headers for API requests"""
return {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
def test_health():
"""Test the health endpoint"""
print("Testing health endpoint...")
try:
response = requests.get(f"{API_BASE_URL}/health")
if response.status_code == 200:
data = response.json()
print(f"✅ Health check passed")
print(f" Status: {data['status']}")
print(f" Connections: {data['connections']}")
print(f" Topics: {data['topics']}")
print(f" Timestamp: {data['timestamp']}")
print(f" API Key Configured: {data.get('apiKeyConfigured', 'Unknown')}")
return True
else:
print(f"❌ Health check failed: {response.status_code}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Health check error: {e}")
return False
def test_topics():
"""Test the topics endpoint"""
print("\nTesting topics endpoint...")
try:
response = requests.get(f"{API_BASE_URL}/topics")
if response.status_code == 200:
data = response.json()
print(f"✅ Topics retrieved successfully")
if data['topics']:
for topic in data['topics']:
print(f" Topic: {topic['topic']}, Subscribers: {topic['subscribers']}")
else:
print(" No topics found")
return True
else:
print(f"❌ Topics request failed: {response.status_code}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Topics request error: {e}")
return False
def test_topic_info(topic):
"""Test getting info for a specific topic"""
print(f"\nTesting topic info for '{topic}'...")
try:
response = requests.get(f"{API_BASE_URL}/topics/{topic}")
if response.status_code == 200:
data = response.json()
print(f"✅ Topic info retrieved successfully")
print(f" Topic: {data['topic']}")
print(f" Subscribers: {data['subscribers']}")
return True
elif response.status_code == 404:
print(f"️ Topic '{topic}' not found (no subscribers)")
return True
else:
print(f"❌ Topic info request failed: {response.status_code}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Topic info request error: {e}")
return False
def test_publish_message(topic, message, data=None):
"""Test publishing a message to a topic"""
print(f"\nTesting message publish to topic '{topic}'...")
payload = {
"topic": topic,
"message": message
}
if data:
payload["data"] = data
try:
response = requests.post(f"{API_BASE_URL}/publish", json=payload, headers=get_auth_headers())
if response.status_code == 200:
response_data = response.json()
print(f"✅ Message published successfully")
print(f" Subscribers: {response_data['subscribers']}")
print(f" Message: {response_data['payload']['message']}")
if response_data['payload']['data']:
print(f" Data: {json.dumps(response_data['payload']['data'], indent=2)}")
return True
elif response.status_code == 204:
print(f"️ Message published successfully but no subscribers for topic '{topic}'")
return True
elif response.status_code == 401:
print(f"❌ Authentication failed: {response.json().get('message', 'Unknown error')}")
return False
else:
print(f"❌ Message publish failed: {response.status_code}")
print(f" Response: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Message publish error: {e}")
return False
def test_publish_with_url_topic(topic, message, data=None):
"""Test publishing using the URL-based endpoint"""
print(f"\nTesting message publish to topic '{topic}' (URL endpoint)...")
payload = {}
if message:
payload["message"] = message
if data:
payload["data"] = data
try:
response = requests.post(f"{API_BASE_URL}/publish/{topic}", json=payload, headers=get_auth_headers())
if response.status_code == 200:
response_data = response.json()
print(f"✅ Message published successfully (URL endpoint)")
print(f" Subscribers: {response_data['subscribers']}")
print(f" Message: {response_data['payload']['message']}")
if response_data['payload']['data']:
print(f" Data: {json.dumps(response_data['payload']['data'], indent=2)}")
return True
elif response.status_code == 204:
print(f"️ Message published successfully but no subscribers for topic '{topic}'")
return True
elif response.status_code == 401:
print(f"❌ Authentication failed: {response.json().get('message', 'Unknown error')}")
return False
else:
print(f"❌ Message publish failed (URL endpoint): {response.status_code}")
print(f" Response: {response.text}")
return False
except requests.exceptions.RequestException as e:
print(f"❌ Message publish error (URL endpoint): {e}")
return False
def test_authentication_errors():
"""Test authentication error cases"""
print(f"\n🧪 Testing authentication error cases...")
# Test without API key
print("Testing request without API key...")
try:
response = requests.post(f"{API_BASE_URL}/publish", json={
"topic": "test-topic",
"message": "This should fail"
})
if response.status_code == 401:
print("✅ Correctly rejected request without API key")
else:
print(f"❌ Expected 401, got {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"❌ Request error: {e}")
# Test with wrong API key
print("Testing request with wrong API key...")
try:
response = requests.post(f"{API_BASE_URL}/publish", json={
"topic": "test-topic",
"message": "This should fail"
}, headers={'Authorization': 'Bearer wrong-key'})
if response.status_code == 401:
print("✅ Correctly rejected request with wrong API key")
else:
print(f"❌ Expected 401, got {response.status_code}")
except requests.exceptions.RequestException as e:
print(f"❌ Request error: {e}")
def run_demo():
"""Run a complete demo"""
print("🚀 Socket Broker HTTP API Test")
print("=" * 50)
print(f"🔑 Using API Key: {API_KEY[:10]}{'...' if len(API_KEY) > 10 else ''}")
print(f"🌐 API Base URL: {API_BASE_URL}")
print("=" * 50)
# Test basic endpoints
if not test_health():
print("❌ Cannot proceed - health check failed")
return
test_topics()
# Test topic info
test_topic_info(TOPIC_NAME)
# Test publishing messages
print(f"\n📤 Publishing test messages...")
# Simple message
test_publish_message(TOPIC_NAME, "Hello from Python test script!")
# Message with data
test_data = {
"source": "python-test",
"timestamp": datetime.now().isoformat(),
"user": "test-user",
"action": "demo"
}
test_publish_message(TOPIC_NAME, "Test message with data", test_data)
# Test URL-based endpoint
test_publish_with_url_topic(TOPIC_NAME, "Message via URL endpoint", {"method": "url"})
# Test error cases
print(f"\n🧪 Testing error cases...")
# Empty topic
test_publish_message("", "This should fail")
# No message or data
test_publish_message(TOPIC_NAME, None)
# Test authentication errors
test_authentication_errors()
print(f"\n✅ Demo completed!")
print(f"\n💡 To see real-time messages, open test_client.html in a browser")
print(f" and subscribe to topic '{TOPIC_NAME}'")
print(f"\n🔐 API Key is required for publishing messages")
print(f" Set API_KEY environment variable or use the default key")
if __name__ == "__main__":
try:
run_demo()
except KeyboardInterrupt:
print("\n\n⏹️ Demo interrupted by user")
except Exception as e:
print(f"\n❌ Demo failed with error: {e}")
+287
View File
@@ -0,0 +1,287 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Socket Broker Test Client</title>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 20px;
font-weight: bold;
}
.connected {
background-color: #d4edda;
color: #155724;
}
.disconnected {
background-color: #f8d7da;
color: #721c24;
}
.connecting {
background-color: #fff3cd;
color: #856404;
}
.controls {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
.controls input,
.controls button {
margin: 5px;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
}
.controls button {
background-color: #007bff;
color: white;
cursor: pointer;
}
.controls button:hover {
background-color: #0056b3;
}
.controls button:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
.messages {
max-height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
background-color: #f8f9fa;
}
.message {
margin: 10px 0;
padding: 10px;
background-color: white;
border-left: 4px solid #007bff;
border-radius: 4px;
}
.message .topic {
font-weight: bold;
color: #007bff;
}
.message .timestamp {
font-size: 0.8em;
color: #6c757d;
}
.subscriptions {
margin-top: 20px;
}
.subscription {
display: inline-block;
background-color: #28a745;
color: white;
padding: 5px 10px;
margin: 2px;
border-radius: 15px;
font-size: 0.9em;
}
</style>
</head>
<body>
<div class="container">
<h1>Socket Broker Test Client</h1>
<div id="status" class="status connecting">Connecting...</div>
<div class="controls">
<h3>Topic Management</h3>
<input
type="text"
id="topicInput"
placeholder="Enter topic name"
value="test-topic"
/>
<button onclick="subscribe()" id="subscribeBtn">Subscribe</button>
<button onclick="unsubscribe()" id="unsubscribeBtn" disabled>
Unsubscribe
</button>
<button onclick="clearMessages()">Clear Messages</button>
</div>
<div class="subscriptions">
<h3>Active Subscriptions</h3>
<div id="subscriptionList"></div>
</div>
<div class="messages">
<h3>Messages</h3>
<div id="messageList">No messages yet...</div>
</div>
</div>
<script>
let socket;
let currentTopic = "";
let subscriptions = new Set();
// Initialize socket connection
function initSocket() {
socket = io("http://localhost:3000");
socket.on("connect", () => {
updateStatus("Connected", "connected");
document.getElementById("subscribeBtn").disabled = false;
});
socket.on("disconnect", () => {
updateStatus("Disconnected", "disconnected");
document.getElementById("subscribeBtn").disabled = true;
document.getElementById("unsubscribeBtn").disabled = true;
});
socket.on("subscribed", (data) => {
console.log("Subscribed to topic:", data.topic);
subscriptions.add(data.topic);
currentTopic = data.topic;
updateSubscriptionList();
document.getElementById("unsubscribeBtn").disabled = false;
addMessage(
"System",
`Subscribed to topic: ${data.topic}`,
"subscription"
);
});
socket.on("unsubscribed", (data) => {
console.log("Unsubscribed from topic:", data.topic);
subscriptions.delete(data.topic);
if (currentTopic === data.topic) {
currentTopic = "";
}
updateSubscriptionList();
if (subscriptions.size === 0) {
document.getElementById("unsubscribeBtn").disabled = true;
}
addMessage(
"System",
`Unsubscribed from topic: ${data.topic}`,
"subscription"
);
});
socket.on("message", (data) => {
console.log("Received message:", data);
addMessage(
data.topic,
data.message || "No message content",
"message",
data.data,
data.timestamp
);
});
socket.on("error", (error) => {
console.error("Socket error:", error);
addMessage("Error", error.message, "error");
});
}
function updateStatus(text, className) {
const statusEl = document.getElementById("status");
statusEl.textContent = text;
statusEl.className = `status ${className}`;
}
function updateSubscriptionList() {
const listEl = document.getElementById("subscriptionList");
if (subscriptions.size === 0) {
listEl.innerHTML = "<em>No active subscriptions</em>";
} else {
listEl.innerHTML = Array.from(subscriptions)
.map((topic) => `<span class="subscription">${topic}</span>`)
.join("");
}
}
function subscribe() {
const topic = document.getElementById("topicInput").value.trim();
if (!topic) {
alert("Please enter a topic name");
return;
}
if (subscriptions.has(topic)) {
alert("Already subscribed to this topic");
return;
}
socket.emit("subscribe", topic);
}
function unsubscribe() {
const topic = document.getElementById("topicInput").value.trim();
if (!topic) {
alert("Please enter a topic name");
return;
}
if (!subscriptions.has(topic)) {
alert("Not subscribed to this topic");
return;
}
socket.emit("unsubscribe", topic);
}
function addMessage(topic, message, type, data = null, timestamp = null) {
const messageList = document.getElementById("messageList");
const messageEl = document.createElement("div");
messageEl.className = `message ${type}`;
let content = `<div class="topic">${topic}</div>`;
content += `<div>${message}</div>`;
if (data) {
content += `<div><small>Data: ${JSON.stringify(data)}</small></div>`;
}
content += `<div class="timestamp">${
timestamp || new Date().toLocaleTimeString()
}</div>`;
messageEl.innerHTML = content;
if (messageList.textContent === "No messages yet...") {
messageList.innerHTML = "";
}
messageList.appendChild(messageEl);
messageList.scrollTop = messageList.scrollHeight;
}
function clearMessages() {
document.getElementById("messageList").innerHTML = "No messages yet...";
}
// Initialize when page loads
window.onload = function () {
initSocket();
};
</script>
</body>
</html>