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

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

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