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
77 lines
2.4 KiB
C++
77 lines
2.4 KiB
C++
#include "rpc/runtime_dir.hpp"
|
|
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
#include <unistd.h>
|
|
|
|
#include <cerrno>
|
|
#include <cstdlib>
|
|
#include <string>
|
|
|
|
namespace velox::daemon::rpc {
|
|
|
|
namespace {
|
|
|
|
std::error_code errc(int e) { return std::error_code(e, std::generic_category()); }
|
|
|
|
// Ensure `dir` exists as a directory we own with mode 0700. Creates it if absent.
|
|
std::error_code ensure_private_dir(const std::string& dir) {
|
|
if (::mkdir(dir.c_str(), 0700) != 0 && errno != EEXIST) return errc(errno);
|
|
|
|
struct stat st{};
|
|
if (::lstat(dir.c_str(), &st) != 0) return errc(errno);
|
|
if (!S_ISDIR(st.st_mode)) return errc(ENOTDIR);
|
|
if (st.st_uid != ::geteuid()) return errc(EPERM);
|
|
|
|
// Tighten if a prior run (or umask) left it looser. Group/other bits must be clear:
|
|
// the socket is 0600 but a traversable parent still lets another user stat it.
|
|
if ((st.st_mode & 077) != 0 && ::chmod(dir.c_str(), 0700) != 0) return errc(errno);
|
|
return {};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::error_code resolve_runtime_dir(RuntimeDir& out) {
|
|
std::string base;
|
|
if (const char* xdg = ::getenv("XDG_RUNTIME_DIR"); xdg != nullptr && xdg[0] != '\0') {
|
|
base = xdg;
|
|
} else {
|
|
base = "/run/user/" + std::to_string(::geteuid());
|
|
struct stat st{};
|
|
if (::stat(base.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) {
|
|
// No XDG_RUNTIME_DIR and no /run/user/<uid>: we refuse rather than pick an
|
|
// insecure fallback. The caller surfaces this as "cannot start".
|
|
return errc(ENOENT);
|
|
}
|
|
}
|
|
if (!base.empty() && base.back() == '/') base.pop_back();
|
|
|
|
const std::string dir = base + "/velox";
|
|
if (auto ec = ensure_private_dir(dir)) return ec;
|
|
|
|
out.path = dir;
|
|
return {};
|
|
}
|
|
|
|
std::error_code resolve_data_dir(std::string& out) {
|
|
std::string base;
|
|
if (const char* xdg = ::getenv("XDG_DATA_HOME"); xdg != nullptr && xdg[0] != '\0') {
|
|
base = xdg;
|
|
} else if (const char* home = ::getenv("HOME"); home != nullptr && home[0] != '\0') {
|
|
base = std::string(home) + "/.local/share";
|
|
} else {
|
|
return errc(ENOENT);
|
|
}
|
|
if (!base.empty() && base.back() == '/') base.pop_back();
|
|
|
|
// Create the XDG base components leniently, then the velox dir with a strict check.
|
|
::mkdir(base.c_str(), 0700);
|
|
const std::string dir = base + "/velox";
|
|
if (auto ec = ensure_private_dir(dir)) return ec;
|
|
|
|
out = dir;
|
|
return {};
|
|
}
|
|
|
|
} // namespace velox::daemon::rpc
|