# Milestone 10: Network - WebSocket **Status**: Complete **Goal**: Secure WebSocket connections with same validation as HTTP. --- ## Overview This milestone extends the network system with WebSocket support: - Reuse HttpValidator for URL/domain validation (wss:// only) - Connection limits per app - Message size limits - Idle timeout handling - Clean disconnection on app stop ### Key Deliverables 1. **WebSocketManager class** - Connection pool management 2. **WebSocket class** - Individual connection wrapper 3. **Lua websocket API** - `network.websocket()` function 4. **Mock implementation** - For desktop testing --- ## File Structure ``` src/main/cpp/sandbox/ ├── http_validator.h # Existing - add WSS support ├── http_validator.cpp # Existing - add WSS support ├── websocket_manager.h # NEW - WebSocket pool └── websocket_manager.cpp # NEW - Connection management ``` --- ## Implementation Details ### 1. WebSocketManager Class ```cpp // websocket_manager.h #pragma once #include #include #include #include #include #include #include "http_validator.h" struct lua_State; namespace mosis { struct WebSocketLimits { int max_connections_per_app = 5; size_t max_message_size = 1 * 1024 * 1024; // 1 MB int idle_timeout_ms = 5 * 60 * 1000; // 5 minutes int connect_timeout_ms = 30000; // 30 seconds }; enum class WebSocketState { Connecting, Open, Closing, Closed }; class WebSocket { public: using MessageCallback = std::function; using CloseCallback = std::function; using ErrorCallback = std::function; using OpenCallback = std::function; WebSocket(int id, const std::string& url); ~WebSocket(); int GetId() const { return m_id; } const std::string& GetUrl() const { return m_url; } WebSocketState GetState() const { return m_state; } // Send message (returns false if not connected or message too large) bool Send(const std::string& data, bool binary = false); // Close connection void Close(int code = 1000, const std::string& reason = ""); // Event callbacks void SetOnOpen(OpenCallback cb) { m_on_open = std::move(cb); } void SetOnMessage(MessageCallback cb) { m_on_message = std::move(cb); } void SetOnClose(CloseCallback cb) { m_on_close = std::move(cb); } void SetOnError(ErrorCallback cb) { m_on_error = std::move(cb); } // For mock mode - simulate events void SimulateOpen(); void SimulateMessage(const std::string& data, bool binary); void SimulateClose(int code, const std::string& reason); void SimulateError(const std::string& error); private: int m_id; std::string m_url; WebSocketState m_state; size_t m_max_message_size; OpenCallback m_on_open; MessageCallback m_on_message; CloseCallback m_on_close; ErrorCallback m_on_error; }; class WebSocketManager { public: WebSocketManager(const std::string& app_id, const WebSocketLimits& limits = WebSocketLimits{}); ~WebSocketManager(); // Configure domain restrictions (reuses HttpValidator) void SetAllowedDomains(const std::vector& domains); void ClearDomainRestrictions(); // Create new WebSocket connection // Returns connection ID on success, -1 on failure (sets error) std::shared_ptr Connect(const std::string& url, std::string& error); // Get connection by ID std::shared_ptr GetConnection(int id); // Close specific connection void CloseConnection(int id, int code = 1000, const std::string& reason = ""); // Close all connections (called on app stop) void CloseAll(); // Stats int GetActiveConnectionCount() const; // Access validator for testing HttpValidator& GetValidator() { return m_validator; } // For testing: set mock mode void SetMockMode(bool enabled) { m_mock_mode = enabled; } bool IsMockMode() const { return m_mock_mode; } private: std::string m_app_id; WebSocketLimits m_limits; HttpValidator m_validator; std::map> m_connections; int m_next_id = 1; mutable std::mutex m_mutex; bool m_mock_mode = true; bool ValidateUrl(const std::string& url, std::string& error); }; // Register websocket APIs void RegisterWebSocketAPI(lua_State* L, WebSocketManager* manager); } // namespace mosis ``` ### 2. URL Validation Changes The HttpValidator needs to accept both `https://` and `wss://` schemes: ```cpp // In Validate(): if (parsed->scheme != "https" && parsed->scheme != "wss") { error = "HTTPS or WSS required, got: " + parsed->scheme; return std::nullopt; } ``` ### 3. Lua API ```lua -- Create WebSocket connection local ws, err = network.websocket("wss://api.example.com/socket") if not ws then print("Failed to connect:", err) return end -- Event handlers ws:on("open", function() print("Connected!") ws:send("Hello server") end) ws:on("message", function(data, binary) print("Received:", data) end) ws:on("close", function(code, reason) print("Closed:", code, reason) end) ws:on("error", function(err) print("Error:", err) end) -- Send message ws:send("Hello") ws:send(binaryData, true) -- binary mode -- Close connection ws:close() ws:close(1000, "Normal closure") -- Get state local state = ws:state() -- "connecting", "open", "closing", "closed" ``` ### 4. Connection Lifecycle ``` Connect(url) ──► Validating ──► Connecting ──► Open │ │ │ │ error │ error │ message ▼ ▼ ▼ Closed Closed Handler │ │ close/error ▼ Closed ``` ### 5. Limits Enforcement | Limit | Default | Description | |-------|---------|-------------| | max_connections_per_app | 5 | Maximum concurrent WebSocket connections | | max_message_size | 1 MB | Maximum message size (send or receive) | | idle_timeout_ms | 5 min | Close idle connections | | connect_timeout_ms | 30 sec | Connection establishment timeout | --- ## Test Cases ### Test 1: WebSocket URL Validation ```cpp bool Test_WebSocketUrlValidation(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; // WSS should be allowed auto ws = manager.Connect("wss://example.com/socket", err); // In mock mode, connection will fail but validation should pass EXPECT_TRUE(err.find("mock") != std::string::npos || err.find("disabled") != std::string::npos || ws != nullptr); // WS (plain) should be blocked ws = manager.Connect("ws://example.com/socket", err); EXPECT_TRUE(ws == nullptr); EXPECT_TRUE(err.find("WSS") != std::string::npos || err.find("HTTPS") != std::string::npos || err.find("required") != std::string::npos); return true; } ``` ### Test 2: Connection Limits ```cpp bool Test_WebSocketConnectionLimits(std::string& error_msg) { mosis::WebSocketLimits limits; limits.max_connections_per_app = 2; mosis::WebSocketManager manager("test.app", limits); manager.ClearDomainRestrictions(); std::string err; // Create max connections auto ws1 = manager.Connect("wss://example.com/socket1", err); auto ws2 = manager.Connect("wss://example.com/socket2", err); // Third should fail (or be limited) auto ws3 = manager.Connect("wss://example.com/socket3", err); EXPECT_TRUE(ws3 == nullptr); EXPECT_TRUE(err.find("limit") != std::string::npos || err.find("connections") != std::string::npos); return true; } ``` ### Test 3: WebSocket Blocks Private IPs ```cpp bool Test_WebSocketBlocksPrivateIP(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; std::vector private_urls = { "wss://127.0.0.1/socket", "wss://localhost/socket", "wss://10.0.0.1/socket", "wss://192.168.1.1/socket", "wss://169.254.169.254/socket" }; for (const auto& url : private_urls) { auto ws = manager.Connect(url, err); EXPECT_TRUE(ws == nullptr); } return true; } ``` ### Test 4: Domain Whitelist ```cpp bool Test_WebSocketDomainWhitelist(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.SetAllowedDomains({"api.example.com"}); std::string err; // Allowed domain - should pass validation (may fail in mock mode for other reasons) auto ws1 = manager.Connect("wss://api.example.com/socket", err); bool allowed_passed = (ws1 != nullptr) || (err.find("mock") != std::string::npos) || (err.find("disabled") != std::string::npos); EXPECT_TRUE(allowed_passed); // Disallowed domain - should fail validation auto ws2 = manager.Connect("wss://evil.com/socket", err); EXPECT_TRUE(ws2 == nullptr); EXPECT_TRUE(err.find("allowed") != std::string::npos || err.find("whitelist") != std::string::npos || err.find("Domain") != std::string::npos); return true; } ``` ### Test 5: Message Size Limits ```cpp bool Test_WebSocketMessageLimits(std::string& error_msg) { mosis::WebSocketLimits limits; limits.max_message_size = 1024; // 1 KB for testing mosis::WebSocketManager manager("test.app", limits); manager.ClearDomainRestrictions(); // Create a mock WebSocket directly to test send limits mosis::WebSocket ws(1, "wss://example.com/socket"); // Small message should work (if connected) // Large message should fail std::string large_message(2048, 'X'); // 2 KB bool send_result = ws.Send(large_message); EXPECT_FALSE(send_result); // Should fail - not connected and/or too large return true; } ``` ### Test 6: Close All Connections ```cpp bool Test_WebSocketCloseAll(std::string& error_msg) { mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); std::string err; // Create some connections (may fail in mock but that's ok) manager.Connect("wss://example.com/socket1", err); manager.Connect("wss://example.com/socket2", err); // Close all manager.CloseAll(); // Should have no active connections EXPECT_TRUE(manager.GetActiveConnectionCount() == 0); return true; } ``` ### Test 7: Lua Integration ```cpp bool Test_WebSocketLuaIntegration(std::string& error_msg) { SandboxContext ctx = TestContext(); LuaSandbox sandbox(ctx); mosis::WebSocketManager manager("test.app"); manager.ClearDomainRestrictions(); mosis::RegisterWebSocketAPI(sandbox.GetState(), &manager); std::string script = R"lua( -- Test that network.websocket exists if not network then error("network global not found") end if not network.websocket then error("network.websocket not found") end -- Test validation rejection (private IP) local ws, err = network.websocket("wss://127.0.0.1/socket") if ws then error("expected private IP to be blocked") end )lua"; bool ok = sandbox.LoadString(script, "websocket_test"); if (!ok) { error_msg = "Lua test failed: " + sandbox.GetLastError(); return false; } return true; } ``` --- ## Acceptance Criteria All tests must pass: - [x] `Test_WebSocketUrlValidation` - WSS required, WS blocked - [x] `Test_WebSocketConnectionLimits` - Per-app connection limits enforced - [x] `Test_WebSocketBlocksPrivateIP` - SSRF prevention works - [x] `Test_WebSocketDomainWhitelist` - Domain restrictions work - [x] `Test_WebSocketMessageLimits` - Message size limits enforced - [x] `Test_WebSocketCloseAll` - Cleanup works - [x] `Test_WebSocketLuaIntegration` - Lua API works --- ## Dependencies - Milestone 1 (LuaSandbox) - Milestone 9 (HttpValidator) --- ## Notes ### Desktop vs Android Implementation For desktop testing, WebSocketManager operates in mock mode: - URL validation runs normally - Connection limits are tracked - Actual WebSocket connections are not made - Events can be simulated for testing On Android, the real implementation would: 1. Use Java WebSocket client through JNI 2. Handle background thread for socket I/O 3. Marshal events back to Lua thread ### Security Considerations 1. **Same-origin**: WSS URLs validated same as HTTPS 2. **Connection limits**: Prevent resource exhaustion 3. **Message limits**: Prevent memory exhaustion 4. **Idle timeout**: Automatic cleanup of abandoned connections 5. **Clean shutdown**: All connections closed on app stop --- ## Next Steps After Milestone 10 passes: 1. Milestone 11: Virtual Hardware - Camera