Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 688e3cd771 | |||
|
|
61738bac04 |
20
app.js
20
app.js
@@ -2203,7 +2203,27 @@ function connectHermes() {
|
||||
console.log(`Connecting to Hermes at ${wsUrl}...`);
|
||||
hermesWs = new WebSocket(wsUrl);
|
||||
|
||||
// Send authentication if connecting to external host
|
||||
hermesWs.onopen = () => {
|
||||
// Check if we need to authenticate (external connection)
|
||||
const isLocalhost = window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1' ||
|
||||
window.location.hostname === '::1';
|
||||
|
||||
if (!isLocalhost) {
|
||||
// Send authentication token for external connections
|
||||
const authToken = localStorage.getItem('nexus-ws-auth-token');
|
||||
if (authToken) {
|
||||
hermesWs.send(JSON.stringify({
|
||||
type: 'auth',
|
||||
token: authToken
|
||||
}));
|
||||
console.log('Sent authentication token');
|
||||
} else {
|
||||
console.warn('No authentication token found for external connection');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Hermes connected.');
|
||||
wsConnected = true;
|
||||
addChatMessage('system', 'Hermes link established.');
|
||||
|
||||
33
server.py
33
server.py
@@ -7,6 +7,7 @@ the body (Evennia/Morrowind), and the visualization surface.
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
from typing import Set
|
||||
@@ -15,8 +16,9 @@ from typing import Set
|
||||
import websockets
|
||||
|
||||
# Configuration
|
||||
PORT = 8765
|
||||
HOST = "0.0.0.0" # Allow external connections if needed
|
||||
PORT = int(os.environ.get("NEXUS_WS_PORT", "8765"))
|
||||
HOST = os.environ.get("NEXUS_WS_HOST", "127.0.0.1") # Local-only by default
|
||||
# Set NEXUS_WS_HOST=0.0.0.0 to allow external connections (requires authentication)
|
||||
|
||||
# Logging setup
|
||||
logging.basicConfig(
|
||||
@@ -31,6 +33,33 @@ clients: Set[websockets.WebSocketServerProtocol] = set()
|
||||
|
||||
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
||||
"""Handles individual client connections and message broadcasting."""
|
||||
|
||||
# Authentication check for external connections
|
||||
if HOST != "127.0.0.1":
|
||||
# Require authentication token for external connections
|
||||
auth_token = os.environ.get("NEXUS_WS_AUTH_TOKEN")
|
||||
if not auth_token:
|
||||
logger.warning("External connections require NEXUS_WS_AUTH_TOKEN to be set")
|
||||
await websocket.close(1008, "Authentication required")
|
||||
return
|
||||
|
||||
# Check for authentication in first message
|
||||
try:
|
||||
auth_message = await asyncio.wait_for(websocket.recv(), timeout=5.0)
|
||||
auth_data = json.loads(auth_message)
|
||||
|
||||
if auth_data.get("type") != "auth" or auth_data.get("token") != auth_token:
|
||||
logger.warning(f"Authentication failed from {websocket.remote_address}")
|
||||
await websocket.close(1008, "Authentication failed")
|
||||
return
|
||||
|
||||
logger.info(f"Authenticated connection from {websocket.remote_address}")
|
||||
|
||||
except (asyncio.TimeoutError, json.JSONDecodeError, Exception) as e:
|
||||
logger.warning(f"Authentication error from {websocket.remote_address}: {e}")
|
||||
await websocket.close(1008, "Authentication required")
|
||||
return
|
||||
|
||||
clients.add(websocket)
|
||||
addr = websocket.remote_address
|
||||
logger.info(f"Client connected from {addr}. Total clients: {len(clients)}")
|
||||
|
||||
167
tests/test_websocket_security.py
Normal file
167
tests/test_websocket_security.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test WebSocket gateway security configuration.
|
||||
Issue #1514: [Security] WebSocket gateway listens on 0.0.0.0
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def test_server_binding():
|
||||
"""Test that server binds to correct address."""
|
||||
print("Testing server binding configuration...")
|
||||
|
||||
# Check server.py for HOST configuration
|
||||
server_path = os.path.join(os.path.dirname(__file__), '..', 'server.py')
|
||||
|
||||
if not os.path.exists(server_path):
|
||||
print(f"❌ ERROR: server.py not found at {server_path}")
|
||||
return False
|
||||
|
||||
with open(server_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for HOST configuration
|
||||
if 'HOST = os.environ.get("NEXUS_WS_HOST", "127.0.0.1")' in content:
|
||||
print("✅ HOST configured to use environment variable with 127.0.0.1 default")
|
||||
else:
|
||||
print("❌ HOST not properly configured")
|
||||
return False
|
||||
|
||||
# Check for authentication code
|
||||
if 'Authentication check for external connections' in content:
|
||||
print("✅ Authentication check implemented")
|
||||
else:
|
||||
print("❌ Authentication check not found")
|
||||
return False
|
||||
|
||||
# Check for token validation
|
||||
if 'auth_data.get("token")' in content:
|
||||
print("✅ Token validation implemented")
|
||||
else:
|
||||
print("❌ Token validation not found")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_environment_variables():
|
||||
"""Test environment variable configuration."""
|
||||
print("\nTesting environment variable configuration...")
|
||||
|
||||
# Check server.py for environment variable usage
|
||||
server_path = os.path.join(os.path.dirname(__file__), '..', 'server.py')
|
||||
|
||||
if not os.path.exists(server_path):
|
||||
print(f"❌ ERROR: server.py not found at {server_path}")
|
||||
return False
|
||||
|
||||
with open(server_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for environment variable usage
|
||||
if 'os.environ.get("NEXUS_WS_HOST"' in content:
|
||||
print("✅ HOST uses environment variable")
|
||||
else:
|
||||
print("❌ HOST does not use environment variable")
|
||||
return False
|
||||
|
||||
if '"127.0.0.1"' in content:
|
||||
print("✅ Default HOST is 127.0.0.1")
|
||||
else:
|
||||
print("❌ Default HOST is not 127.0.0.1")
|
||||
return False
|
||||
|
||||
if 'os.environ.get("NEXUS_WS_PORT"' in content:
|
||||
print("✅ PORT uses environment variable")
|
||||
else:
|
||||
print("❌ PORT does not use environment variable")
|
||||
return False
|
||||
|
||||
if 'os.environ.get("NEXUS_WS_AUTH_TOKEN"' in content:
|
||||
print("✅ Auth token uses environment variable")
|
||||
else:
|
||||
print("❌ Auth token does not use environment variable")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test_client_authentication():
|
||||
"""Test client authentication code."""
|
||||
print("\nTesting client authentication code...")
|
||||
|
||||
# Check app.js for authentication code
|
||||
app_path = os.path.join(os.path.dirname(__file__), '..', 'app.js')
|
||||
|
||||
if not os.path.exists(app_path):
|
||||
print(f"❌ ERROR: app.js not found at {app_path}")
|
||||
return False
|
||||
|
||||
with open(app_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for authentication code
|
||||
if 'isLocalhost' in content:
|
||||
print("✅ Client checks for localhost")
|
||||
else:
|
||||
print("❌ Client does not check for localhost")
|
||||
return False
|
||||
|
||||
if 'nexus-ws-auth-token' in content:
|
||||
print("✅ Client checks for auth token in localStorage")
|
||||
else:
|
||||
print("❌ Client does not check for auth token")
|
||||
return False
|
||||
|
||||
if 'type: \'auth\'' in content:
|
||||
print("✅ Client sends auth message")
|
||||
else:
|
||||
print("❌ Client does not send auth message")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
print("=" * 60)
|
||||
print("WebSocket Gateway Security Tests")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
("Server binding configuration", test_server_binding),
|
||||
("Environment variable configuration", test_environment_variables),
|
||||
("Client authentication code", test_client_authentication),
|
||||
]
|
||||
|
||||
results = []
|
||||
for name, test_func in tests:
|
||||
print(f"\n{name}:")
|
||||
try:
|
||||
result = test_func()
|
||||
results.append((name, result))
|
||||
except Exception as e:
|
||||
print(f"❌ Test failed with exception: {e}")
|
||||
results.append((name, False))
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test Results:")
|
||||
print("=" * 60)
|
||||
|
||||
passed = 0
|
||||
for name, result in results:
|
||||
status = "✅ PASS" if result else "❌ FAIL"
|
||||
print(f"{status}: {name}")
|
||||
if result:
|
||||
passed += 1
|
||||
|
||||
print(f"\nPassed: {passed}/{len(results)}")
|
||||
|
||||
if passed == len(results):
|
||||
print("\n✅ All tests passed!")
|
||||
return 0
|
||||
else:
|
||||
print("\n❌ Some tests failed!")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user