Files
MosisService/docs/SANDBOX_MILESTONE_10.md

13 KiB

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

// websocket_manager.h
#pragma once

#include <string>
#include <map>
#include <memory>
#include <mutex>
#include <functional>
#include <vector>
#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<void(const std::string& data, bool binary)>;
    using CloseCallback = std::function<void(int code, const std::string& reason)>;
    using ErrorCallback = std::function<void(const std::string& error)>;
    using OpenCallback = std::function<void()>;

    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<std::string>& domains);
    void ClearDomainRestrictions();

    // Create new WebSocket connection
    // Returns connection ID on success, -1 on failure (sets error)
    std::shared_ptr<WebSocket> Connect(const std::string& url, std::string& error);

    // Get connection by ID
    std::shared_ptr<WebSocket> 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<int, std::shared_ptr<WebSocket>> 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:

// In Validate():
if (parsed->scheme != "https" && parsed->scheme != "wss") {
    error = "HTTPS or WSS required, got: " + parsed->scheme;
    return std::nullopt;
}

3. Lua API

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

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

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

bool Test_WebSocketBlocksPrivateIP(std::string& error_msg) {
    mosis::WebSocketManager manager("test.app");
    manager.ClearDomainRestrictions();

    std::string err;

    std::vector<std::string> 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

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

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

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

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:

  • Test_WebSocketUrlValidation - WSS required, WS blocked
  • Test_WebSocketConnectionLimits - Per-app connection limits enforced
  • Test_WebSocketBlocksPrivateIP - SSRF prevention works
  • Test_WebSocketDomainWhitelist - Domain restrictions work
  • Test_WebSocketMessageLimits - Message size limits enforced
  • Test_WebSocketCloseAll - Cleanup works
  • 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