The extension's fallback transport (docs/05 §4). veloxd now also listens on 127.0.0.1, first free port in 52000-52016, and writes it to <runtime>/ws.port (0600). - rpc/ws_frame — RFC 6455 frame codec. Incremental; reassembles continuation frames; enforces "client frames MUST be masked" (§5.1); caps a reassembled message at 8 MiB. This is the attacker-adjacent parser, so it has its own test table. - rpc/ws_handshake — HTTP upgrade parse, Sec-WebSocket-Accept (SHA-1 + base64 via libcrypto), and the two non-negotiable checks: an Origin header must be present and must be moz-extension:// (a page cannot pair). Version must be 13. - rpc/ws_server — per-connection Handshake -> Open state machine on the shared EventLoop. Token gate: session.pair mints a token behind the approver + rate limiter; session.hello must present a valid one; every other method is -32002 until authed. Privileged methods are refused -32003 by the generated dispatch(). Ping -> Pong; Close echoed. session.hello major-version mismatch -> -32001. - rpc/pairing — PairingApprover interface + EnvAutoApprover dev stub (approves iff VELOX_PAIR_AUTO=1); PairingRateLimiter (5 failures / 60 s per origin, then 60 s lockout -> -32014, survives reconnect); a four-digit code generator. - store/pairings — the pairings table: create() returns the plaintext token once and stores only its SHA-256; find_active_by_token, touch, revoke, list_active. - util/crypto — sha1 / sha256_hex / base64 / random_token over libcrypto. - store/sqlite — pin the DB file (and -wal/-shm) to 0600. - runtime_dir — resolve_data_dir() for $XDG_DATA_HOME/velox (velox.db). - main.cpp — opens + migrates velox.db, starts both transports; a WS bind failure is logged, not fatal (capture must fail open, the Unix socket still serves the GUI/CLI). Real gap, flagged not hidden: the pairing prompt is EnvAutoApprover for now — a GUI dialog / desktop notification is build step 7. Pairing needs VELOX_PAIR_AUTO=1 until then. Tests (ASan+UBSan and TSan clean): veloxd.ws_frame (codec + handshake vectors incl. the RFC 6455 §1.3 accept sample), veloxd.pairings (token create/find/revoke, hash-not-token, rate-limit window + lockout + per-origin isolation + success reset), veloxd.ws_server (full flow: 101 handshake, -32002 gate, deny-then-approve pairing, hello-with-token, -32003 privileged refusal, real download.list). 27 daemon/cli tests green; full tree green. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upd9WhG9oppieig5nRDLig
93 lines
2.8 KiB
C++
93 lines
2.8 KiB
C++
#pragma once
|
|
|
|
// The loopback WebSocket transport (docs/05 §4). Binds 127.0.0.1 only, on the first free
|
|
// port in 52000-52016, and writes the chosen port to <runtime>/ws.port. Any local process
|
|
// can connect, so a token is mandatory: session.pair mints one (behind a user prompt,
|
|
// rate-limited), session.hello must present it, and every other method is refused -32002
|
|
// until it does. Privileged methods are refused -32003 by the generated dispatch().
|
|
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <string>
|
|
#include <system_error>
|
|
#include <unordered_map>
|
|
|
|
#include <nlohmann/json_fwd.hpp>
|
|
|
|
#include "rpc/pairing.hpp"
|
|
#include "rpc/runtime_dir.hpp"
|
|
#include "rpc/ws_frame.hpp"
|
|
#include "velox_proto.hpp"
|
|
|
|
namespace velox::daemon::store {
|
|
class Db;
|
|
}
|
|
|
|
namespace velox::daemon::rpc {
|
|
|
|
class EventLoop;
|
|
|
|
class WsServer {
|
|
public:
|
|
WsServer(EventLoop& loop, velox::proto::Dispatcher& dispatcher, velox::daemon::store::Db& db,
|
|
PairingApprover& approver, RuntimeDir runtime);
|
|
~WsServer();
|
|
|
|
WsServer(const WsServer&) = delete;
|
|
WsServer& operator=(const WsServer&) = delete;
|
|
|
|
// Pick a port, bind loopback, write ws.port, listen, register with the loop.
|
|
std::error_code start();
|
|
|
|
int port() const noexcept { return port_; }
|
|
std::size_t connection_count() const noexcept { return conns_.size(); }
|
|
|
|
static constexpr int kPortLo = 52000;
|
|
static constexpr int kPortHi = 52016;
|
|
|
|
private:
|
|
enum class Phase { Handshake, Open, Closing };
|
|
|
|
struct Conn {
|
|
int fd;
|
|
Phase phase = Phase::Handshake;
|
|
std::string in_raw; // bytes before the upgrade completes
|
|
WsFrameReader frames;
|
|
std::string outbuf;
|
|
std::size_t out_off = 0;
|
|
bool close_after_flush = false;
|
|
std::string origin;
|
|
bool authed = false;
|
|
std::string pairing_id;
|
|
std::string session_id;
|
|
};
|
|
|
|
void on_listener_readable();
|
|
void on_conn_event(int fd, unsigned events);
|
|
void progress_handshake(Conn& c);
|
|
void on_ws_bytes(Conn& c, std::string_view bytes);
|
|
void handle_rpc(Conn& c, const std::string& text);
|
|
bool handle_session_ws(Conn& c, const std::string& method, const nlohmann::json& request,
|
|
nlohmann::json& reply);
|
|
|
|
void send_text(Conn& c, const nlohmann::json& value);
|
|
void send_frame(Conn& c, WsOpcode op, std::string_view payload);
|
|
void begin_close(Conn& c, std::uint16_t code, std::string_view reason);
|
|
void flush(Conn& c);
|
|
void close_conn(int fd);
|
|
|
|
EventLoop& loop_;
|
|
velox::proto::Dispatcher& dispatcher_;
|
|
velox::daemon::store::Db& db_;
|
|
PairingApprover& approver_;
|
|
RuntimeDir runtime_;
|
|
PairingRateLimiter rate_limiter_;
|
|
|
|
int listen_fd_ = -1;
|
|
int port_ = 0;
|
|
bool wrote_port_file_ = false;
|
|
std::unordered_map<int, std::unique_ptr<Conn>> conns_;
|
|
};
|
|
|
|
} // namespace velox::daemon::rpc
|