Compare commits
2 Commits
mimo/code/
...
fix/1514
| Author | SHA1 | Date | |
|---|---|---|---|
| 688e3cd771 | |||
|
|
61738bac04 |
20
app.js
20
app.js
@@ -2203,7 +2203,27 @@ function connectHermes() {
|
|||||||
console.log(`Connecting to Hermes at ${wsUrl}...`);
|
console.log(`Connecting to Hermes at ${wsUrl}...`);
|
||||||
hermesWs = new WebSocket(wsUrl);
|
hermesWs = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
// Send authentication if connecting to external host
|
||||||
hermesWs.onopen = () => {
|
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.');
|
console.log('Hermes connected.');
|
||||||
wsConnected = true;
|
wsConnected = true;
|
||||||
addChatMessage('system', 'Hermes link established.');
|
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 asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from typing import Set
|
from typing import Set
|
||||||
@@ -15,8 +16,9 @@ from typing import Set
|
|||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
PORT = 8765
|
PORT = int(os.environ.get("NEXUS_WS_PORT", "8765"))
|
||||||
HOST = "0.0.0.0" # Allow external connections if needed
|
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 setup
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@@ -31,6 +33,33 @@ clients: Set[websockets.WebSocketServerProtocol] = set()
|
|||||||
|
|
||||||
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
async def broadcast_handler(websocket: websockets.WebSocketServerProtocol):
|
||||||
"""Handles individual client connections and message broadcasting."""
|
"""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)
|
clients.add(websocket)
|
||||||
addr = websocket.remote_address
|
addr = websocket.remote_address
|
||||||
logger.info(f"Client connected from {addr}. Total clients: {len(clients)}")
|
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