daemon: rpc/ws_server — loopback WebSocket transport + pairing (build step 1, second half)
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
This commit is contained in:
+15
-16
@@ -1,19 +1,18 @@
|
||||
# daemon unit + integration tests. Registered with ctest; run via `ctest --preset dev`.
|
||||
# No external test framework — each file is a small self-checking binary (matches the
|
||||
# lightweight style core/ uses, without depending on core's private test support).
|
||||
# No external test framework — each file is a small self-checking binary.
|
||||
|
||||
add_executable(veloxd_ndjson_test ndjson_test.cpp)
|
||||
target_link_libraries(veloxd_ndjson_test PRIVATE veloxd_rpc)
|
||||
target_compile_options(veloxd_ndjson_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
add_test(NAME veloxd.ndjson COMMAND veloxd_ndjson_test)
|
||||
function(veloxd_test name)
|
||||
cmake_parse_arguments(T "" "" "LIBS" ${ARGN})
|
||||
add_executable(veloxd_${name}_test ${name}_test.cpp)
|
||||
target_link_libraries(veloxd_${name}_test PRIVATE ${T_LIBS})
|
||||
target_compile_options(veloxd_${name}_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
add_test(NAME veloxd.${name} COMMAND veloxd_${name}_test)
|
||||
set_tests_properties(veloxd.${name} PROPERTIES TIMEOUT 30)
|
||||
endfunction()
|
||||
|
||||
add_executable(veloxd_uds_roundtrip_test uds_roundtrip_test.cpp)
|
||||
target_link_libraries(veloxd_uds_roundtrip_test PRIVATE veloxd_rpc)
|
||||
target_compile_options(veloxd_uds_roundtrip_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
add_test(NAME veloxd.uds_roundtrip COMMAND veloxd_uds_roundtrip_test)
|
||||
set_tests_properties(veloxd.uds_roundtrip PROPERTIES TIMEOUT 30)
|
||||
|
||||
add_executable(veloxd_store_migrations_test store_migrations_test.cpp)
|
||||
target_link_libraries(veloxd_store_migrations_test PRIVATE veloxd_store)
|
||||
target_compile_options(veloxd_store_migrations_test PRIVATE -Wall -Wextra -Wpedantic -Werror)
|
||||
add_test(NAME veloxd.store_migrations COMMAND veloxd_store_migrations_test)
|
||||
veloxd_test(ndjson LIBS veloxd_rpc)
|
||||
veloxd_test(uds_roundtrip LIBS veloxd_rpc)
|
||||
veloxd_test(store_migrations LIBS veloxd_store)
|
||||
veloxd_test(pairings LIBS veloxd_store veloxd_rpc)
|
||||
veloxd_test(ws_frame LIBS veloxd_rpc)
|
||||
veloxd_test(ws_server LIBS veloxd_rpc)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// The pairings table + the pairing rate limiter.
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "check.hpp"
|
||||
#include "rpc/pairing.hpp"
|
||||
#include "store/migrations.hpp"
|
||||
#include "store/pairings.hpp"
|
||||
#include "store/sqlite.hpp"
|
||||
|
||||
using namespace velox::daemon;
|
||||
|
||||
void run() {
|
||||
// --- store: create -> find-by-token -> revoke ---------------------------------
|
||||
{
|
||||
auto db = store::Db::open(":memory:");
|
||||
CHECK(db.has_value());
|
||||
if (!db) return;
|
||||
CHECK(store::migrate_to_head(*db).has_value());
|
||||
|
||||
store::Pairings p(*db);
|
||||
auto created = p.create("moz-extension://abc", "Velox for Firefox", "2026-09-10T00:00:00Z");
|
||||
CHECK(created.has_value());
|
||||
if (!created) return;
|
||||
CHECK(created->token.size() >= 40); // 32 bytes base64url, unpadded
|
||||
CHECK(!created->pairing_id.empty());
|
||||
|
||||
// The plaintext token is not in the DB — only its hash.
|
||||
auto st = db->prepare("SELECT count(*) FROM pairings WHERE token_sha256 = ?1");
|
||||
CHECK(st.has_value());
|
||||
CHECK(st->bind(1, std::string_view(created->token)).has_value());
|
||||
auto row = st->step();
|
||||
CHECK(row.has_value() && *row);
|
||||
CHECK_EQ(st->column_int(0), 0); // token itself never stored
|
||||
|
||||
auto found = p.find_active_by_token(created->token);
|
||||
CHECK(found.has_value());
|
||||
CHECK(found->has_value());
|
||||
if (found && *found) CHECK_EQ((*found)->origin, std::string("moz-extension://abc"));
|
||||
|
||||
auto missing = p.find_active_by_token("not-the-token");
|
||||
CHECK(missing.has_value() && !missing->has_value());
|
||||
|
||||
auto revoked = p.revoke(created->pairing_id, "2026-09-10T01:00:00Z");
|
||||
CHECK(revoked.has_value() && *revoked == true);
|
||||
|
||||
auto after = p.find_active_by_token(created->token);
|
||||
CHECK(after.has_value() && !after->has_value()); // revoked -> not active
|
||||
|
||||
auto revoke_again = p.revoke(created->pairing_id, "2026-09-10T02:00:00Z");
|
||||
CHECK(revoke_again.has_value() && *revoke_again == false);
|
||||
}
|
||||
|
||||
// --- rate limiter: 5 failures, then a lockout with retryAfter -----------------
|
||||
{
|
||||
rpc::PairingRateLimiter rl;
|
||||
using Clock = rpc::PairingRateLimiter::Clock;
|
||||
const auto t0 = Clock::now();
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
CHECK(rl.check("origin-a", t0).allowed);
|
||||
rl.record_failure("origin-a", t0);
|
||||
}
|
||||
const auto d = rl.check("origin-a", t0);
|
||||
CHECK(!d.allowed);
|
||||
CHECK(d.retry_after_sec > 0 && d.retry_after_sec <= 61);
|
||||
|
||||
// A different origin is unaffected — the lockout is per-origin.
|
||||
CHECK(rl.check("origin-b", t0).allowed);
|
||||
|
||||
// Still locked 30 s later; clear after the lockout elapses.
|
||||
CHECK(!rl.check("origin-a", t0 + std::chrono::seconds(30)).allowed);
|
||||
CHECK(rl.check("origin-a", t0 + std::chrono::seconds(121)).allowed);
|
||||
|
||||
// A success wipes the origin's history.
|
||||
rl.record_failure("origin-c", t0);
|
||||
rl.record_failure("origin-c", t0);
|
||||
rl.record_success("origin-c");
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
CHECK(rl.check("origin-c", t0).allowed);
|
||||
rl.record_failure("origin-c", t0);
|
||||
}
|
||||
CHECK(rl.check("origin-c", t0).allowed); // only 4 since the reset
|
||||
}
|
||||
}
|
||||
|
||||
TEST_MAIN()
|
||||
@@ -0,0 +1,150 @@
|
||||
#include "rpc/ws_frame.hpp"
|
||||
#include "rpc/ws_handshake.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "check.hpp"
|
||||
|
||||
using namespace velox::daemon::rpc;
|
||||
|
||||
namespace {
|
||||
|
||||
// Build a *client* frame: FIN/opcode, mask bit set, a fixed 4-byte mask, masked payload.
|
||||
std::string client_frame(WsOpcode op, std::string_view payload, bool fin = true) {
|
||||
std::string f;
|
||||
f.push_back(static_cast<char>((fin ? 0x80 : 0x00) | static_cast<std::uint8_t>(op)));
|
||||
const std::size_t n = payload.size();
|
||||
if (n < 126) {
|
||||
f.push_back(static_cast<char>(0x80 | n));
|
||||
} else if (n <= 0xFFFF) {
|
||||
f.push_back(static_cast<char>(0x80 | 126));
|
||||
f.push_back(static_cast<char>((n >> 8) & 0xFF));
|
||||
f.push_back(static_cast<char>(n & 0xFF));
|
||||
} else {
|
||||
f.push_back(static_cast<char>(0x80 | 127));
|
||||
for (int i = 7; i >= 0; --i)
|
||||
f.push_back(static_cast<char>((static_cast<std::uint64_t>(n) >> (i * 8)) & 0xFF));
|
||||
}
|
||||
const char key[4] = {0x12, 0x34, 0x56, 0x78};
|
||||
f.append(key, 4);
|
||||
for (std::size_t i = 0; i < n; ++i) f.push_back(static_cast<char>(payload[i] ^ key[i & 3]));
|
||||
return f;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void run() {
|
||||
// --- one text frame ------------------------------------------------------------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
CHECK(r.feed(client_frame(WsOpcode::Text, "{\"a\":1}"), m) == WsFrameReader::Status::Ok);
|
||||
CHECK_EQ(m.size(), 1u);
|
||||
CHECK(m[0].opcode == WsOpcode::Text);
|
||||
CHECK_EQ(m[0].payload, std::string("{\"a\":1}"));
|
||||
}
|
||||
|
||||
// --- fragmented: text (fin=0) + continuation (fin=1) --------------------------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
r.feed(client_frame(WsOpcode::Text, "hel", /*fin=*/false), m);
|
||||
CHECK_EQ(m.size(), 0u);
|
||||
r.feed(client_frame(WsOpcode::Continuation, "lo", /*fin=*/true), m);
|
||||
CHECK_EQ(m.size(), 1u);
|
||||
CHECK_EQ(m[0].payload, std::string("hello"));
|
||||
}
|
||||
|
||||
// --- byte-at-a-time delivery still reassembles -------------------------------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
const std::string frame = client_frame(WsOpcode::Text, "streamed");
|
||||
for (char ch : frame) r.feed(std::string_view(&ch, 1), m);
|
||||
CHECK_EQ(m.size(), 1u);
|
||||
CHECK_EQ(m[0].payload, std::string("streamed"));
|
||||
}
|
||||
|
||||
// --- a 200-byte payload exercises the 16-bit length path --------------------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
const std::string big(200, 'x');
|
||||
r.feed(client_frame(WsOpcode::Text, big), m);
|
||||
CHECK_EQ(m.size(), 1u);
|
||||
CHECK_EQ(m[0].payload.size(), 200u);
|
||||
}
|
||||
|
||||
// --- ping is surfaced so the server can pong -------------------------------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
r.feed(client_frame(WsOpcode::Ping, "hi"), m);
|
||||
CHECK_EQ(m.size(), 1u);
|
||||
CHECK(m[0].opcode == WsOpcode::Ping);
|
||||
}
|
||||
|
||||
// --- an unmasked client frame is a protocol error (RFC 6455 §5.1) ----------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
std::string bad;
|
||||
bad.push_back(static_cast<char>(0x81)); // FIN + text
|
||||
bad.push_back(static_cast<char>(0x03)); // len 3, mask bit clear
|
||||
bad.append("abc");
|
||||
CHECK(r.feed(bad, m) == WsFrameReader::Status::ProtocolError);
|
||||
}
|
||||
|
||||
// --- a declared length past the cap is rejected before allocating ----------
|
||||
{
|
||||
WsFrameReader r;
|
||||
std::vector<WsMessage> m;
|
||||
std::string hdr;
|
||||
hdr.push_back(static_cast<char>(0x82)); // FIN + binary
|
||||
hdr.push_back(static_cast<char>(0x80 | 127));
|
||||
for (int i = 7; i >= 0; --i)
|
||||
hdr.push_back(static_cast<char>((0x0000000001000000ull >> (i * 8)) & 0xFF)); // 16 MiB
|
||||
CHECK(r.feed(hdr, m) == WsFrameReader::Status::MessageTooBig);
|
||||
}
|
||||
|
||||
// --- ws_encode: server frames are unmasked, correct length byte ------------
|
||||
{
|
||||
const std::string f = ws_encode(WsOpcode::Text, "abc");
|
||||
CHECK_EQ(static_cast<std::uint8_t>(f[0]), 0x81u);
|
||||
CHECK_EQ(static_cast<std::uint8_t>(f[1]), 0x03u); // len 3, no mask bit
|
||||
CHECK_EQ(f.substr(2), std::string("abc"));
|
||||
}
|
||||
|
||||
// --- RFC 6455 §1.3 sample accept value ------------------------------------
|
||||
CHECK_EQ(ws_accept_key("dGhlIHNhbXBsZSBub25jZQ=="),
|
||||
std::string("s3pPLMBiTxaQ9kYGzzhZRbK+xOo="));
|
||||
|
||||
// --- handshake: a page origin is refused, an extension origin upgrades -----
|
||||
{
|
||||
const std::string req_page =
|
||||
"GET / HTTP/1.1\r\nHost: 127.0.0.1:52000\r\nUpgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\nOrigin: https://evil.example\r\n\r\n";
|
||||
const auto r = ws_try_handshake(req_page);
|
||||
CHECK(r.complete);
|
||||
CHECK(!r.ok);
|
||||
CHECK(r.response.find("403") != std::string::npos);
|
||||
}
|
||||
{
|
||||
const std::string req_ext =
|
||||
"GET / HTTP/1.1\r\nHost: 127.0.0.1:52000\r\nUpgrade: websocket\r\n"
|
||||
"Connection: Upgrade\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Version: 13\r\n"
|
||||
"Origin: moz-extension://11111111-2222-3333-4444-555555555555\r\n\r\n";
|
||||
const auto r = ws_try_handshake(req_ext);
|
||||
CHECK(r.complete);
|
||||
CHECK(r.ok);
|
||||
CHECK(r.response.find("101") != std::string::npos);
|
||||
CHECK(r.response.find("Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") !=
|
||||
std::string::npos);
|
||||
CHECK_EQ(r.origin, std::string("moz-extension://11111111-2222-3333-4444-555555555555"));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_MAIN()
|
||||
@@ -0,0 +1,216 @@
|
||||
// Integration: a real WsServer on a loopback port, a hand-rolled WebSocket client.
|
||||
// Covers the handshake, the pairing flow, the token gate (-32002), and the
|
||||
// privileged-over-WS refusal (-32003).
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "check.hpp"
|
||||
#include "rpc/dispatcher.hpp"
|
||||
#include "rpc/event_loop.hpp"
|
||||
#include "rpc/pairing.hpp"
|
||||
#include "rpc/runtime_dir.hpp"
|
||||
#include "rpc/ws_frame.hpp"
|
||||
#include "rpc/ws_server.hpp"
|
||||
#include "store/migrations.hpp"
|
||||
#include "store/sqlite.hpp"
|
||||
|
||||
using nlohmann::json;
|
||||
namespace rpc = velox::daemon::rpc;
|
||||
namespace store = velox::daemon::store;
|
||||
|
||||
namespace {
|
||||
|
||||
int dial(int port) {
|
||||
const int fd = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
sockaddr_in a{};
|
||||
a.sin_family = AF_INET;
|
||||
a.sin_addr.s_addr = ::htonl(INADDR_LOOPBACK);
|
||||
a.sin_port = ::htons(static_cast<std::uint16_t>(port));
|
||||
if (::connect(fd, reinterpret_cast<sockaddr*>(&a), sizeof(a)) != 0) {
|
||||
::close(fd);
|
||||
return -1;
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
void write_all(int fd, std::string_view s) {
|
||||
std::size_t off = 0;
|
||||
while (off < s.size()) {
|
||||
const ssize_t n = ::write(fd, s.data() + off, s.size() - off);
|
||||
if (n <= 0) return;
|
||||
off += static_cast<std::size_t>(n);
|
||||
}
|
||||
}
|
||||
|
||||
std::string read_some(int fd) {
|
||||
char buf[8192];
|
||||
const ssize_t n = ::read(fd, buf, sizeof(buf));
|
||||
return n > 0 ? std::string(buf, static_cast<std::size_t>(n)) : std::string{};
|
||||
}
|
||||
|
||||
// A masked client text frame.
|
||||
std::string client_text(std::string_view payload) {
|
||||
std::string f;
|
||||
f.push_back(static_cast<char>(0x81)); // FIN + text
|
||||
const std::size_t n = payload.size();
|
||||
if (n < 126) {
|
||||
f.push_back(static_cast<char>(0x80 | n));
|
||||
} else {
|
||||
f.push_back(static_cast<char>(0x80 | 126));
|
||||
f.push_back(static_cast<char>((n >> 8) & 0xFF));
|
||||
f.push_back(static_cast<char>(n & 0xFF));
|
||||
}
|
||||
const char k[4] = {0x0A, 0x0B, 0x0C, 0x0D};
|
||||
f.append(k, 4);
|
||||
for (std::size_t i = 0; i < n; ++i) f.push_back(static_cast<char>(payload[i] ^ k[i & 3]));
|
||||
return f;
|
||||
}
|
||||
|
||||
// Decode one unmasked server frame from `buf`, consuming it. Returns payload; sets `op`.
|
||||
std::string server_frame(std::string& buf, rpc::WsOpcode& op) {
|
||||
if (buf.size() < 2) return {};
|
||||
op = static_cast<rpc::WsOpcode>(buf[0] & 0x0F);
|
||||
std::size_t len = static_cast<std::uint8_t>(buf[1]) & 0x7F;
|
||||
std::size_t header = 2;
|
||||
if (len == 126) {
|
||||
len = (static_cast<std::size_t>(static_cast<std::uint8_t>(buf[2])) << 8) |
|
||||
static_cast<std::uint8_t>(buf[3]);
|
||||
header = 4;
|
||||
}
|
||||
if (buf.size() < header + len) return {};
|
||||
std::string payload = buf.substr(header, len);
|
||||
buf.erase(0, header + len);
|
||||
return payload;
|
||||
}
|
||||
|
||||
// Send a request frame, wait for one text reply, return its parsed JSON.
|
||||
json rpc_call(int fd, const json& req) {
|
||||
write_all(fd, client_text(req.dump()));
|
||||
std::string buf;
|
||||
for (;;) {
|
||||
buf += read_some(fd);
|
||||
rpc::WsOpcode op{};
|
||||
std::string save = buf;
|
||||
const std::string payload = server_frame(buf, op);
|
||||
if (payload.empty() && buf == save) continue; // need more bytes
|
||||
if (op == rpc::WsOpcode::Text) return json::parse(payload, nullptr, false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void run() {
|
||||
::unsetenv("VELOX_PAIR_AUTO");
|
||||
|
||||
auto db = store::Db::open(":memory:");
|
||||
CHECK(db.has_value());
|
||||
if (!db) return;
|
||||
CHECK(store::migrate_to_head(*db).has_value());
|
||||
|
||||
char tmpl[] = "/tmp/velox-ws-test-XXXXXX";
|
||||
const char* dir = ::mkdtemp(tmpl);
|
||||
CHECK(dir != nullptr);
|
||||
rpc::RuntimeDir rt{dir ? dir : "/tmp"};
|
||||
|
||||
rpc::EventLoop loop;
|
||||
rpc::VeloxDispatcher dispatcher;
|
||||
rpc::EnvAutoApprover approver;
|
||||
rpc::WsServer server(loop, dispatcher, *db, approver, rt);
|
||||
const auto ec = server.start();
|
||||
CHECK(!ec);
|
||||
if (ec) return;
|
||||
CHECK(server.port() >= rpc::WsServer::kPortLo);
|
||||
CHECK(server.port() <= rpc::WsServer::kPortHi);
|
||||
|
||||
std::thread th([&loop] { loop.run(); });
|
||||
|
||||
const std::string origin = "moz-extension://11111111-2222-3333-4444-555555555555";
|
||||
|
||||
// --- handshake ---------------------------------------------------------------
|
||||
const int fd = dial(server.port());
|
||||
CHECK(fd >= 0);
|
||||
write_all(fd,
|
||||
"GET / HTTP/1.1\r\nHost: 127.0.0.1\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n"
|
||||
"Origin: " + origin + "\r\n\r\n");
|
||||
std::string hs;
|
||||
while (hs.find("\r\n\r\n") == std::string::npos) hs += read_some(fd);
|
||||
CHECK(hs.find("101 Switching Protocols") != std::string::npos);
|
||||
|
||||
// --- session.hello with no token -> -32002 --------------------------------
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "session.hello"},
|
||||
{"params", {{"clientType", "extension"},
|
||||
{"clientName", "Velox for Firefox"},
|
||||
{"protocolVersion",
|
||||
std::string(velox::proto::kProtocolVersion)}}}});
|
||||
CHECK(r.contains("error"));
|
||||
CHECK_EQ(r["error"]["code"].get<int>(), -32002);
|
||||
}
|
||||
|
||||
// --- session.pair with approval off -> not approved ----------------------
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 2}, {"method", "session.pair"},
|
||||
{"params", {{"clientName", "Velox for Firefox"},
|
||||
{"extensionId", "11111111-2222-3333-4444-555555555555"}}}});
|
||||
CHECK(r.contains("error"));
|
||||
}
|
||||
|
||||
// --- approval on -> a token, then hello with it succeeds ----------------
|
||||
::setenv("VELOX_PAIR_AUTO", "1", 1);
|
||||
std::string token;
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 3}, {"method", "session.pair"},
|
||||
{"params", {{"clientName", "Velox for Firefox"},
|
||||
{"extensionId", "11111111-2222-3333-4444-555555555555"}}}});
|
||||
CHECK(r.contains("result"));
|
||||
if (r.contains("result")) {
|
||||
token = r["result"]["token"].get<std::string>();
|
||||
CHECK(token.size() >= 40);
|
||||
}
|
||||
}
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 4}, {"method", "session.hello"},
|
||||
{"params", {{"clientType", "extension"},
|
||||
{"clientName", "Velox for Firefox"},
|
||||
{"protocolVersion",
|
||||
std::string(velox::proto::kProtocolVersion)},
|
||||
{"token", token}}}});
|
||||
CHECK(r.contains("result"));
|
||||
if (r.contains("result"))
|
||||
CHECK_EQ(r["result"]["transport"].get<std::string>(), std::string("ws"));
|
||||
}
|
||||
|
||||
// --- privileged method over WS -> -32003 -------------------------------
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 5}, {"method", "settings.get"},
|
||||
{"params", {{"keys", nullptr}}}});
|
||||
CHECK(r.contains("error"));
|
||||
CHECK_EQ(r["error"]["code"].get<int>(), -32003);
|
||||
}
|
||||
|
||||
// --- a non-privileged method while authed -> a real result -----------
|
||||
{
|
||||
const json r = rpc_call(fd, {{"jsonrpc", "2.0"}, {"id", 6}, {"method", "download.list"},
|
||||
{"params", json::object()}});
|
||||
CHECK(r.contains("result"));
|
||||
if (r.contains("result")) CHECK_EQ(r["result"]["total"].get<int>(), 0);
|
||||
}
|
||||
|
||||
::close(fd);
|
||||
loop.stop();
|
||||
th.join();
|
||||
::unlink(rt.ws_port_path().c_str());
|
||||
}
|
||||
|
||||
TEST_MAIN()
|
||||
Reference in New Issue
Block a user