mirror of
https://github.com/JBuildr/nubix.git
synced 2026-10-17 15:00:49 +02:00
545 lines
25 KiB
C++
545 lines
25 KiB
C++
// Nubix — protocol byte-level tests (input packets, vibration, teredo, JSON messages).
|
|
// Expected values derived from green-nx and xbox-xcloud-player.
|
|
// Portions derived from green-nx (https://github.com/rmrf404/green-nx), Copyright (C) the green-nx authors, GPL-3.0; modified by Nubix contributors, 2026.
|
|
// Copyright (C) 2026 Nubix contributors
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
#include <cstring>
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include "core/protocol.hpp"
|
|
#include "test.hpp"
|
|
|
|
using namespace xc;
|
|
using nlohmann::json;
|
|
|
|
namespace {
|
|
uint16_t u16(const std::vector<uint8_t>& b, size_t o) { return uint16_t(b[o] | (b[o + 1] << 8)); }
|
|
uint32_t u32(const std::vector<uint8_t>& b, size_t o) {
|
|
return uint32_t(b[o]) | (uint32_t(b[o + 1]) << 8) | (uint32_t(b[o + 2]) << 16) | (uint32_t(b[o + 3]) << 24);
|
|
}
|
|
} // namespace
|
|
|
|
XC_TEST(protocol, client_metadata_layout) {
|
|
proto::InputSerializer s;
|
|
auto p = s.clientMetadata(1);
|
|
XC_REQUIRE(p.size() == 15u);
|
|
XC_CHECK_EQ(u16(p, 0), uint16_t(proto::report::ClientMetadata));
|
|
XC_CHECK_EQ(p[14], uint8_t(1));
|
|
}
|
|
|
|
XC_TEST(protocol, gamepad_layout) {
|
|
proto::InputSerializer s;
|
|
s.clientMetadata(1);
|
|
GamepadState st;
|
|
st.buttons = btn::A | btn::DUp | btn::RS;
|
|
st.lx = 1000; st.ly = -2000; st.rx = 32767; st.ry = -32767;
|
|
st.lt = 65535; st.rt = 1234;
|
|
auto p = s.gamepad(st, 0);
|
|
XC_REQUIRE(p.size() == 38u);
|
|
XC_CHECK_EQ(u16(p, 0), uint16_t(proto::report::Gamepad));
|
|
XC_CHECK_EQ(p[14], uint8_t(1)); // count
|
|
XC_CHECK_EQ(p[15], uint8_t(0)); // gamepad index
|
|
XC_CHECK_EQ(u16(p, 16), uint16_t(btn::A | btn::DUp | btn::RS));
|
|
XC_CHECK_EQ(int16_t(u16(p, 18)), int16_t(1000));
|
|
XC_CHECK_EQ(int16_t(u16(p, 20)), int16_t(-2000));
|
|
XC_CHECK_EQ(int16_t(u16(p, 22)), int16_t(32767));
|
|
XC_CHECK_EQ(int16_t(u16(p, 24)), int16_t(-32767));
|
|
XC_CHECK_EQ(u16(p, 26), uint16_t(65535));
|
|
XC_CHECK_EQ(u16(p, 28), uint16_t(1234));
|
|
XC_CHECK_EQ(u32(p, 30), 1u); // PhysicalPhysicality, LE 1
|
|
// VirtualPhysicality: big-endian 1 -> bytes 00 00 00 01
|
|
XC_CHECK_EQ(p[34], uint8_t(0));
|
|
XC_CHECK_EQ(p[35], uint8_t(0));
|
|
XC_CHECK_EQ(p[36], uint8_t(0));
|
|
XC_CHECK_EQ(p[37], uint8_t(1));
|
|
}
|
|
|
|
XC_TEST(protocol, sequence_increments_and_rollback) {
|
|
proto::InputSerializer s;
|
|
auto a = s.clientMetadata(1);
|
|
auto b = s.gamepad(GamepadState{}, 0);
|
|
auto c = s.gamepad(GamepadState{}, 0);
|
|
XC_REQUIRE(a.size() >= 6 && b.size() >= 6 && c.size() >= 6);
|
|
XC_CHECK_EQ(u32(b, 2), u32(a, 2) + 1);
|
|
XC_CHECK_EQ(u32(c, 2), u32(b, 2) + 1);
|
|
s.rollback();
|
|
auto d = s.gamepad(GamepadState{}, 0);
|
|
XC_REQUIRE(d.size() >= 6);
|
|
XC_CHECK_EQ(u32(d, 2), u32(c, 2));
|
|
}
|
|
|
|
XC_TEST(protocol, timestamp_is_monotonic_double) {
|
|
proto::InputSerializer s;
|
|
auto a = s.gamepad(GamepadState{}, 0);
|
|
auto b = s.gamepad(GamepadState{}, 0);
|
|
XC_REQUIRE(a.size() >= 14 && b.size() >= 14);
|
|
double ta, tb;
|
|
std::memcpy(&ta, &a[6], 8);
|
|
std::memcpy(&tb, &b[6], 8);
|
|
XC_CHECK(ta >= 0.0);
|
|
XC_CHECK(tb >= ta);
|
|
}
|
|
|
|
XC_TEST(protocol, parse_vibration) {
|
|
const uint8_t pkt[] = {0x80, 0x00, 0x00, 0x00, 100, 50, 10, 20, 0xE8, 0x03, 0x05, 0x00, 0x02};
|
|
Vibration v;
|
|
XC_REQUIRE(proto::parseVibration(pkt, sizeof(pkt), v));
|
|
XC_CHECK_EQ(v.pad, uint8_t(0));
|
|
XC_CHECK_EQ(v.left, uint8_t(100));
|
|
XC_CHECK_EQ(v.right, uint8_t(50));
|
|
XC_CHECK_EQ(v.lt, uint8_t(10));
|
|
XC_CHECK_EQ(v.rt, uint8_t(20));
|
|
XC_CHECK_EQ(v.durationMs, uint16_t(1000));
|
|
XC_CHECK_EQ(v.delayMs, uint16_t(5));
|
|
XC_CHECK_EQ(v.repeat, uint8_t(2));
|
|
}
|
|
|
|
XC_TEST(protocol, parse_vibration_rejects_other_reports) {
|
|
const uint8_t pkt[] = {0x10, 0x00, 0x38, 0x04, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00};
|
|
Vibration v;
|
|
XC_CHECK(!proto::parseVibration(pkt, sizeof(pkt), v));
|
|
XC_CHECK(!proto::parseVibration(pkt, 1, v));
|
|
}
|
|
|
|
XC_TEST(protocol, parse_server_metadata) {
|
|
// reportType 16, u32 height @2, u32 width @6
|
|
const uint8_t pkt[] = {0x10, 0x00, 0x38, 0x04, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00};
|
|
uint32_t w = 0, h = 0;
|
|
XC_REQUIRE(proto::parseServerMetadata(pkt, sizeof(pkt), w, h));
|
|
XC_CHECK_EQ(w, 1920u);
|
|
XC_CHECK_EQ(h, 1080u);
|
|
}
|
|
|
|
XC_TEST(protocol, teredo_rfc4380_example) {
|
|
std::string ip;
|
|
uint16_t port = 0;
|
|
XC_REQUIRE(proto::teredoDecode("2001:0000:4136:e378:8000:63bf:3fff:fdd2", ip, port));
|
|
XC_CHECK_EQ(ip, std::string("192.0.2.45"));
|
|
XC_CHECK_EQ(port, uint16_t(40000));
|
|
XC_REQUIRE(proto::teredoDecode("2001:0:4136:e378:8000:63bf:3fff:fdd2", ip, port));
|
|
XC_CHECK_EQ(ip, std::string("192.0.2.45"));
|
|
XC_CHECK_EQ(port, uint16_t(40000));
|
|
}
|
|
|
|
XC_TEST(protocol, teredo_rejects_non_teredo) {
|
|
std::string ip;
|
|
uint16_t port = 0;
|
|
XC_CHECK(!proto::teredoDecode("2a01:4f8::1", ip, port));
|
|
XC_CHECK(!proto::teredoDecode("192.168.1.1", ip, port));
|
|
XC_CHECK(!proto::teredoDecode("garbage", ip, port));
|
|
}
|
|
|
|
XC_TEST(protocol, teredo_candidate_expansion) {
|
|
std::vector<std::string> in = {
|
|
"a=candidate:1 1 UDP 100 2001:0:4136:e378:8000:63bf:3fff:fdd2 1234 typ host",
|
|
"a=candidate:2 1 UDP 50 20.1.2.3 1000 typ host"};
|
|
auto out = proto::expandTeredoCandidates(in);
|
|
bool sawOrig = false, saw40000 = false, saw9002 = false, sawPlain = false;
|
|
for (const auto& c : out) {
|
|
if (c.find("2001:0:4136:e378") != std::string::npos) sawOrig = true;
|
|
if (c.find(" 192.0.2.45 40000 ") != std::string::npos) saw40000 = true;
|
|
if (c.find(" 192.0.2.45 9002 ") != std::string::npos) saw9002 = true;
|
|
if (c.find(" 20.1.2.3 1000 ") != std::string::npos) sawPlain = true;
|
|
}
|
|
XC_CHECK(sawOrig);
|
|
XC_CHECK(saw40000);
|
|
XC_CHECK(saw9002);
|
|
XC_CHECK(sawPlain);
|
|
}
|
|
|
|
XC_TEST(protocol, control_messages_are_json) {
|
|
auto kf = json::parse(proto::controlKeyframeRequest(), nullptr, false);
|
|
XC_REQUIRE(!kf.is_discarded());
|
|
XC_CHECK_EQ(kf.value("message", std::string()), std::string("videoKeyframeRequested"));
|
|
XC_CHECK(kf.value("ifrRequested", false));
|
|
XC_CHECK(!json::parse(proto::controlAuthorization(), nullptr, false).is_discarded());
|
|
XC_CHECK(!json::parse(proto::controlGamepadChanged(0, true), nullptr, false).is_discarded());
|
|
XC_CHECK(!json::parse(proto::controlResolution("1080"), nullptr, false).is_discarded());
|
|
XC_CHECK(!json::parse(proto::messageHandshake(), nullptr, false).is_discarded());
|
|
}
|
|
|
|
XC_TEST(protocol, message_init_sequence) {
|
|
Settings s;
|
|
auto msgs = proto::messageInitSequence(s, "00000000-0000-4000-8000-000000000000", 1920, 1080);
|
|
XC_REQUIRE(!msgs.empty());
|
|
for (const auto& m : msgs) XC_CHECK(!json::parse(m, nullptr, false).is_discarded());
|
|
auto tc = json::parse(proto::transactionComplete("abc", "{}"), nullptr, false);
|
|
XC_CHECK(!tc.is_discarded());
|
|
}
|
|
|
|
XC_TEST(protocol, ice_post_body_green_nx_shape) {
|
|
std::vector<std::string> cands = {"candidate:1 1 UDP 2122260223 192.168.1.2 50000 typ host"};
|
|
auto body = json::parse(proto::icePostBody(cands, "UFRAG"), nullptr, false);
|
|
XC_REQUIRE(!body.is_discarded());
|
|
XC_CHECK_EQ(body.value("messageType", std::string()), std::string("iceCandidate"));
|
|
XC_REQUIRE(body.contains("candidate") && body["candidate"].is_array());
|
|
XC_REQUIRE(body["candidate"].size() == 2u); // candidate + end-of-candidates
|
|
auto first = json::parse(body["candidate"][0].get<std::string>(), nullptr, false);
|
|
XC_REQUIRE(!first.is_discarded());
|
|
XC_CHECK_EQ(first.value("candidate", std::string()), cands[0]);
|
|
XC_CHECK_EQ(first.value("sdpMid", std::string()), std::string("0"));
|
|
XC_CHECK_EQ(first.value("sdpMLineIndex", -1), 0);
|
|
XC_CHECK_EQ(first.value("usernameFragment", std::string()), std::string("UFRAG"));
|
|
auto last = json::parse(body["candidate"][1].get<std::string>(), nullptr, false);
|
|
XC_CHECK_EQ(last.value("candidate", std::string()), std::string("a=end-of-candidates"));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------------------------
|
|
// Byte-exact vectors (derived from green-nx xcloud_protocol.cpp / xcloud-player packet.ts)
|
|
// ---------------------------------------------------------------------------------------------
|
|
|
|
namespace {
|
|
// Compare everything except the 8 timestamp bytes at [6, 14).
|
|
bool sameIgnoringTimestamp(const std::vector<uint8_t>& got, const std::vector<uint8_t>& want) {
|
|
if (got.size() != want.size()) return false;
|
|
for (size_t i = 0; i < got.size(); ++i) {
|
|
if (i >= 6 && i < 14) continue;
|
|
if (got[i] != want[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
std::string hex(const std::vector<uint8_t>& b) {
|
|
static const char* d = "0123456789abcdef";
|
|
std::string s;
|
|
for (uint8_t c : b) {
|
|
s += d[c >> 4];
|
|
s += d[c & 15];
|
|
s += ' ';
|
|
}
|
|
return s;
|
|
}
|
|
} // namespace
|
|
|
|
XC_TEST(protocol, client_metadata_exact_bytes_green_nx_sequence) {
|
|
proto::InputSerializer s;
|
|
XC_CHECK_EQ(s.nextSequence(), 1u);
|
|
auto p = s.clientMetadata(0);
|
|
// u16 8 | u32 seq=1 (green-nx pre-increments) | f64 ts | u8 maxTouchpoints
|
|
const std::vector<uint8_t> want = {0x08, 0x00, 0x01, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x00};
|
|
XC_CHECK(sameIgnoringTimestamp(p, want));
|
|
if (!sameIgnoringTimestamp(p, want)) std::printf(" got %s\n want %s\n", hex(p).c_str(), hex(want).c_str());
|
|
XC_CHECK_EQ(s.nextSequence(), 2u);
|
|
}
|
|
|
|
XC_TEST(protocol, gamepad_exact_bytes) {
|
|
proto::InputSerializer s;
|
|
s.clientMetadata(1); // seq 1
|
|
GamepadState st;
|
|
st.buttons = btn::Nexus | btn::A | btn::LB | btn::RS; // 0x9012
|
|
st.lx = 0x1234; st.ly = -1; st.rx = -32767; st.ry = 32767;
|
|
st.lt = 0xABCD; st.rt = 0x0001;
|
|
auto p = s.gamepad(st, 2);
|
|
const std::vector<uint8_t> want = {
|
|
0x02, 0x00, // reportType Gamepad
|
|
0x02, 0x00, 0x00, 0x00, // seq 2
|
|
0, 0, 0, 0, 0, 0, 0, 0, // timestamp (ignored)
|
|
0x01, // count
|
|
0x02, // +0 index
|
|
0x12, 0x90, // +1 buttons
|
|
0x34, 0x12, // +3 lx
|
|
0xFF, 0xFF, // +5 ly = -1
|
|
0x01, 0x80, // +7 rx = -32767
|
|
0xFF, 0x7F, // +9 ry = 32767
|
|
0xCD, 0xAB, // +11 lt
|
|
0x01, 0x00, // +13 rt
|
|
0x01, 0x00, 0x00, 0x00, // +15 PhysicalPhysicality LE 1
|
|
0x00, 0x00, 0x00, 0x01, // +19 VirtualPhysicality BE 1
|
|
};
|
|
XC_REQUIRE(want.size() == proto::kGamepadPacketSize);
|
|
XC_CHECK(sameIgnoringTimestamp(p, want));
|
|
if (!sameIgnoringTimestamp(p, want)) std::printf(" got %s\n want %s\n", hex(p).c_str(), hex(want).c_str());
|
|
}
|
|
|
|
XC_TEST(protocol, rollback_never_underflows_and_reset) {
|
|
proto::InputSerializer s;
|
|
s.rollback();
|
|
XC_CHECK_EQ(s.nextSequence(), 1u);
|
|
s.gamepad(GamepadState{}, 0);
|
|
s.gamepad(GamepadState{}, 0);
|
|
XC_CHECK_EQ(s.nextSequence(), 3u);
|
|
s.rollback();
|
|
XC_CHECK_EQ(s.nextSequence(), 2u);
|
|
auto p = s.gamepad(GamepadState{}, 0);
|
|
XC_CHECK_EQ(u32(p, 2), 2u);
|
|
s.reset();
|
|
XC_CHECK_EQ(s.nextSequence(), 1u);
|
|
XC_CHECK_EQ(u32(s.clientMetadata(), 2), 1u);
|
|
}
|
|
|
|
XC_TEST(protocol, control_messages_exact) {
|
|
XC_CHECK_EQ(proto::controlAuthorization(),
|
|
std::string(R"({"accessKey":"4BDB3609-C1F1-4195-9B37-FEFF45DA8B8E","message":"authorizationRequest"})"));
|
|
XC_CHECK_EQ(proto::controlGamepadChanged(0, true),
|
|
std::string(R"({"gamepadIndex":0,"message":"gamepadChanged","wasAdded":true})"));
|
|
XC_CHECK_EQ(proto::controlGamepadChanged(1, false),
|
|
std::string(R"({"gamepadIndex":1,"message":"gamepadChanged","wasAdded":false})"));
|
|
XC_CHECK_EQ(proto::controlResolution("1080HQ"),
|
|
std::string(R"({"message":"userRequestedResolutionUpdate","resolutionAlias":"1080HQ"})"));
|
|
XC_CHECK_EQ(proto::controlKeyframeRequest(),
|
|
std::string(R"({"ifrRequested":true,"message":"videoKeyframeRequested"})"));
|
|
}
|
|
|
|
namespace {
|
|
bool isUuidV4(const std::string& u) {
|
|
if (u.size() != 36) return false;
|
|
for (size_t i = 0; i < u.size(); ++i) {
|
|
if (i == 8 || i == 13 || i == 18 || i == 23) {
|
|
if (u[i] != '-') return false;
|
|
} else if (!((u[i] >= '0' && u[i] <= '9') || (u[i] >= 'a' && u[i] <= 'f'))) {
|
|
return false;
|
|
}
|
|
}
|
|
return u[14] == '4' && (u[19] == '8' || u[19] == '9' || u[19] == 'a' || u[19] == 'b');
|
|
}
|
|
} // namespace
|
|
|
|
XC_TEST(protocol, uuid_and_handshake) {
|
|
auto a = proto::newUuid(), b = proto::newUuid();
|
|
XC_CHECK(isUuidV4(a));
|
|
XC_CHECK(isUuidV4(b));
|
|
XC_CHECK(a != b);
|
|
auto h = json::parse(proto::messageHandshake());
|
|
XC_CHECK_EQ(h["type"].get<std::string>(), std::string("Handshake"));
|
|
XC_CHECK_EQ(h["version"].get<std::string>(), std::string("messageV1"));
|
|
XC_CHECK_EQ(h["cv"].get<std::string>(), std::string("0"));
|
|
XC_CHECK(isUuidV4(h["id"].get<std::string>()));
|
|
XC_CHECK_EQ(h.size(), size_t(4));
|
|
}
|
|
|
|
XC_TEST(protocol, message_init_sequence_exact_content) {
|
|
Settings s;
|
|
s.bitrateKbps = 15000;
|
|
const std::string install = "c97d7ee0-73b2-4239-bf1d-9d805a338429";
|
|
auto msgs = proto::messageInitSequence(s, install, 1280, 720);
|
|
XC_REQUIRE(msgs.size() == 6u);
|
|
const char* targets[] = {"/streaming/systemUi/configuration",
|
|
"/streaming/properties/clientappinstallidchanged",
|
|
"/streaming/characteristics/orientationchanged",
|
|
"/streaming/characteristics/touchinputenabledchanged",
|
|
"/streaming/characteristics/clientdevicecapabilities",
|
|
"/streaming/characteristics/dimensionschanged"};
|
|
const char* contents[] = {
|
|
R"({"systemUis":[],"version":[0,2,0]})",
|
|
R"({"clientAppInstallId":"c97d7ee0-73b2-4239-bf1d-9d805a338429"})",
|
|
R"({"orientation":0})",
|
|
R"({"touchInputEnabled":false})",
|
|
R"({"maxBitrateKbps":15000,"maxHeight":720,"maxWidth":1280,"supportsCustomResolution":true,"supportsFps":60,"supportsHdr":false,"supportsHevc":false,"video":{"height":720,"maxBitrateKbps":15000,"maxHeight":720,"maxWidth":1280,"width":1280}})",
|
|
R"({"horizontal":1280,"preferredHeight":720,"preferredWidth":1280,"safeAreaBottom":720,"safeAreaLeft":0,"safeAreaRight":1280,"safeAreaTop":0,"supportsCustomResolution":true,"vertical":720})",
|
|
};
|
|
std::vector<std::string> ids;
|
|
for (size_t i = 0; i < msgs.size(); ++i) {
|
|
auto j = json::parse(msgs[i]);
|
|
XC_CHECK_EQ(j.size(), size_t(5));
|
|
XC_CHECK_EQ(j["type"].get<std::string>(), std::string("Message"));
|
|
XC_CHECK_EQ(j["target"].get<std::string>(), std::string(targets[i]));
|
|
XC_CHECK_EQ(j["cv"].get<std::string>(), std::string(""));
|
|
XC_REQUIRE(j["content"].is_string()); // content is a JSON *string*
|
|
XC_CHECK_EQ(j["content"].get<std::string>(), std::string(contents[i]));
|
|
XC_CHECK(isUuidV4(j["id"].get<std::string>()));
|
|
ids.push_back(j["id"].get<std::string>());
|
|
}
|
|
for (size_t i = 0; i < ids.size(); ++i)
|
|
for (size_t k = i + 1; k < ids.size(); ++k) XC_CHECK(ids[i] != ids[k]);
|
|
|
|
// empty install id -> generated uuid
|
|
auto gen = proto::messageInitSequence(s, "", 1920, 1080);
|
|
auto c = json::parse(json::parse(gen[1])["content"].get<std::string>());
|
|
XC_CHECK(isUuidV4(c["clientAppInstallId"].get<std::string>()));
|
|
}
|
|
|
|
XC_TEST(protocol, transaction_complete_and_unhandled_exact) {
|
|
XC_CHECK_EQ(proto::transactionComplete("abc", R"({"Result":0})"),
|
|
std::string(R"({"content":"{\"Result\":0}","cv":"","id":"abc","type":"TransactionComplete"})"));
|
|
XC_CHECK_EQ(proto::transactionComplete("x", "{}", "cv.1"),
|
|
std::string(R"({"content":"{}","cv":"cv.1","id":"x","type":"TransactionComplete"})"));
|
|
XC_CHECK_EQ(proto::messageUnhandled("i", "/t", "c"),
|
|
std::string(R"({"cv":"c","id":"i","target":"/t","type":"Unhandled"})"));
|
|
auto env = json::parse(proto::messageEnvelope("/a/b", R"({"k":1})"));
|
|
XC_CHECK_EQ(env["content"].get<std::string>(), std::string(R"({"k":1})"));
|
|
XC_CHECK_EQ(env["target"].get<std::string>(), std::string("/a/b"));
|
|
}
|
|
|
|
XC_TEST(protocol, parse_channel_message) {
|
|
proto::ChannelMessage m;
|
|
XC_REQUIRE(proto::parseChannelMessage(
|
|
R"({"type":"TransactionStart","id":"42","target":"/streaming/systemUi/messages/ShowMessageDialog","content":"{\"TitleText\":\"Hi\"}","cv":"abc.1"})",
|
|
m));
|
|
XC_CHECK_EQ(m.type, std::string("TransactionStart"));
|
|
XC_CHECK_EQ(m.id, std::string("42"));
|
|
XC_CHECK_EQ(m.target, std::string(proto::target::kShowMessageDialog));
|
|
XC_CHECK_EQ(m.content, std::string(R"({"TitleText":"Hi"})"));
|
|
XC_CHECK_EQ(m.cv, std::string("abc.1"));
|
|
XC_REQUIRE(proto::parseChannelMessage(R"({"type":"Message","content":{"reason":"x"}})", m));
|
|
XC_CHECK_EQ(m.content, std::string(R"({"reason":"x"})"));
|
|
XC_CHECK(m.id.empty());
|
|
XC_CHECK(proto::isHandshakeAck(R"({"type":"HandshakeAck","version":"messageV1","id":"x","cv":"0"})"));
|
|
XC_CHECK(!proto::isHandshakeAck(R"({"type":"Handshake"})"));
|
|
XC_CHECK(!proto::isHandshakeAck("not json"));
|
|
XC_CHECK(!proto::parseChannelMessage("[1,2]", m));
|
|
XC_CHECK(!proto::parseChannelMessage(R"({"type":5})", m));
|
|
}
|
|
|
|
XC_TEST(protocol, parse_vibration_after_server_metadata) {
|
|
// reportType 0x90 = ServerMetadata | Vibration: 8 bytes of size, then the vibration section
|
|
const uint8_t pkt[] = {0x90, 0x00, 0xD0, 0x02, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00,
|
|
0x00, 0x01, 7, 8, 9, 10, 0x10, 0x00, 0x20, 0x00, 3};
|
|
Vibration v;
|
|
XC_REQUIRE(proto::parseVibration(pkt, sizeof(pkt), v));
|
|
XC_CHECK_EQ(v.pad, uint8_t(1));
|
|
XC_CHECK_EQ(v.left, uint8_t(7));
|
|
XC_CHECK_EQ(v.right, uint8_t(8));
|
|
XC_CHECK_EQ(v.lt, uint8_t(9));
|
|
XC_CHECK_EQ(v.rt, uint8_t(10));
|
|
XC_CHECK_EQ(v.durationMs, uint16_t(16));
|
|
XC_CHECK_EQ(v.delayMs, uint16_t(32));
|
|
XC_CHECK_EQ(v.repeat, uint8_t(3));
|
|
uint32_t w = 0, h = 0;
|
|
XC_REQUIRE(proto::parseServerMetadata(pkt, sizeof(pkt), w, h));
|
|
XC_CHECK_EQ(w, 1280u);
|
|
XC_CHECK_EQ(h, 720u);
|
|
XC_CHECK(!proto::parseVibration(pkt, sizeof(pkt) - 1, v)); // truncated
|
|
const uint8_t only[] = {0x80, 0x00, 0, 0, 1, 2, 3, 4, 0, 0, 0, 0}; // 12 bytes, one short
|
|
XC_CHECK(!proto::parseVibration(only, sizeof(only), v));
|
|
XC_CHECK(!proto::parseServerMetadata(only, sizeof(only), w, h));
|
|
XC_CHECK(!proto::parseVibration(nullptr, 0, v));
|
|
}
|
|
|
|
XC_TEST(protocol, teredo_forms) {
|
|
std::string ip;
|
|
uint16_t port = 0;
|
|
// compressed zero run + upper-case + zone id
|
|
XC_REQUIRE(proto::teredoDecode("2001::4136:E378:8000:63BF:3FFF:FDD2", ip, port));
|
|
XC_CHECK_EQ(ip, std::string("192.0.2.45"));
|
|
XC_CHECK_EQ(port, uint16_t(40000));
|
|
XC_REQUIRE(proto::teredoDecode(" 2001:0:4136:e378:8000:63bf:3fff:fdd2%en0 ", ip, port));
|
|
XC_CHECK_EQ(ip, std::string("192.0.2.45"));
|
|
// all-ones fields invert to zero
|
|
XC_REQUIRE(proto::teredoDecode("2001:0:0:0:0:ffff:ffff:ffff", ip, port));
|
|
XC_CHECK_EQ(ip, std::string("0.0.0.0"));
|
|
XC_CHECK_EQ(port, uint16_t(0));
|
|
// 2001:db8::/32 is documentation space, not Teredo
|
|
XC_CHECK(!proto::teredoDecode("2001:db8::1", ip, port));
|
|
XC_CHECK(ip.empty());
|
|
XC_CHECK(!proto::teredoDecode("", ip, port));
|
|
}
|
|
|
|
XC_TEST(protocol, teredo_expansion_exact) {
|
|
std::vector<std::string> in = {
|
|
"a=candidate:1 1 UDP 100 13.104.1.2 1234 typ host",
|
|
"a=candidate:2 1 UDP 2130706431 2001:0:4136:e378:8000:63bf:3fff:fdd2 9002 typ host",
|
|
"candidate:3 1 udp 50 2001:0:4136:e378:8000:63bf:3fff:fdd2 9003 typ srflx raddr 0.0.0.0 rport 0",
|
|
"a=end-of-candidates",
|
|
};
|
|
auto out = proto::expandTeredoCandidates(in);
|
|
const std::vector<std::string> want = {
|
|
"a=candidate:1 1 UDP 100 13.104.1.2 1234 typ host",
|
|
"a=candidate:20 1 UDP 1 192.0.2.45 40000 typ host",
|
|
"a=candidate:21 1 UDP 1 192.0.2.45 9002 typ host",
|
|
"a=candidate:2 1 UDP 2130706431 2001:0:4136:e378:8000:63bf:3fff:fdd2 9002 typ host",
|
|
"candidate:22 1 UDP 1 192.0.2.45 40000 typ host",
|
|
"candidate:23 1 UDP 1 192.0.2.45 9002 typ host",
|
|
"candidate:3 1 udp 50 2001:0:4136:e378:8000:63bf:3fff:fdd2 9003 typ srflx raddr 0.0.0.0 rport 0",
|
|
"a=end-of-candidates",
|
|
};
|
|
XC_REQUIRE(out.size() == want.size());
|
|
for (size_t i = 0; i < want.size(); ++i) XC_CHECK_EQ(out[i], want[i]);
|
|
}
|
|
|
|
XC_TEST(protocol, ice_post_body_exact) {
|
|
std::vector<std::string> cands = {
|
|
"a=candidate:1 1 UDP 2122260223 192.168.1.2 50000 typ host\r\n",
|
|
"candidate:2 1 TCP 1518280447 192.168.1.2 9 typ host tcptype active",
|
|
"garbage",
|
|
};
|
|
const std::string want =
|
|
R"({"candidate":[)"
|
|
R"("{\"candidate\":\"candidate:1 1 UDP 2122260223 192.168.1.2 50000 typ host\",\"sdpMLineIndex\":0,\"sdpMid\":\"0\",\"usernameFragment\":\"Uf\"}",)"
|
|
R"("{\"candidate\":\"a=end-of-candidates\",\"sdpMLineIndex\":0,\"sdpMid\":\"0\",\"usernameFragment\":\"Uf\"}"],)"
|
|
R"("messageType":"iceCandidate"})";
|
|
XC_CHECK_EQ(proto::icePostBody(cands, "Uf"), want);
|
|
}
|
|
|
|
XC_TEST(protocol, sdp_post_body_exact) {
|
|
const std::string body = proto::sdpPostBody("v=0\r\n");
|
|
const std::string want =
|
|
R"({"configuration":{"chat":{"maxVersion":1,"minVersion":1},)"
|
|
R"("chatConfiguration":{"bytesPerSample":2,"expectedClipDurationMs":20,"format":{"codec":"opus","container":"webm"},"numChannels":1,"sampleFrequencyHz":24000},)"
|
|
R"("control":{"maxVersion":3,"minVersion":1},"input":{"maxVersion":9,"minVersion":1},"message":{"maxVersion":1,"minVersion":1},)"
|
|
R"("reliableinput":{"maxVersion":9,"minVersion":9},"unreliableinput":{"maxVersion":9,"minVersion":9}},)"
|
|
R"("messageType":"offer","requestId":"1","sdp":"v=0\r\n"})";
|
|
XC_CHECK_EQ(body, want);
|
|
}
|
|
|
|
namespace {
|
|
std::string expectedOffer(const char* profile, const char* caps) {
|
|
return std::string("v=0\r\n"
|
|
"o=- 4611731400430051 2 IN IP4 127.0.0.1\r\n"
|
|
"s=-\r\n"
|
|
"t=0 0\r\n"
|
|
"a=group:BUNDLE video audio 0\r\n"
|
|
"a=ice-ufrag:uFrAg\r\n"
|
|
"a=ice-pwd:pWd\r\n"
|
|
"a=fingerprint:sha-256 AB:CD\r\n"
|
|
"a=setup:actpass\r\n"
|
|
"m=video 9 UDP/TLS/RTP/SAVPF 102\r\n"
|
|
"c=IN IP4 0.0.0.0\r\n"
|
|
"a=mid:video\r\n"
|
|
"a=rtcp-mux\r\n"
|
|
"a=rtcp-rsize\r\n"
|
|
"a=rtpmap:102 H264/90000\r\n"
|
|
"a=fmtp:102 level-asymmetry-allowed=0;packetization-mode=1;profile-level-id=") +
|
|
profile + ";" + caps +
|
|
"\r\n"
|
|
"a=rtcp-fb:102 goog-remb\r\n"
|
|
"a=rtcp-fb:102 ccm fir\r\n"
|
|
"a=rtcp-fb:102 nack\r\n"
|
|
"a=rtcp-fb:102 nack pli\r\n"
|
|
"a=recvonly\r\n"
|
|
"m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n"
|
|
"c=IN IP4 0.0.0.0\r\n"
|
|
"a=mid:audio\r\n"
|
|
"a=rtcp-mux\r\n"
|
|
"a=rtcp-rsize\r\n"
|
|
"a=rtpmap:111 opus/48000/2\r\n"
|
|
"a=fmtp:111 minptime=10;useinbandfec=1;stereo=1\r\n"
|
|
"a=recvonly\r\n"
|
|
"m=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\n"
|
|
"c=IN IP4 0.0.0.0\r\n"
|
|
"a=mid:0\r\n"
|
|
"a=sctp-port:5000\r\n"
|
|
"a=max-message-size:262144\r\n";
|
|
}
|
|
} // namespace
|
|
|
|
XC_TEST(protocol, offer_exact_template) {
|
|
XC_CHECK_EQ(proto::buildOffer("uFrAg", "pWd", "AB:CD", false, "1080"),
|
|
expectedOffer("42e01f", "max-fs=8160;max-mbps=489600"));
|
|
XC_CHECK_EQ(proto::buildOffer("uFrAg", "pWd", "AB:CD", false, "1080HQ"),
|
|
expectedOffer("42e01f", "max-fs=8160;max-mbps=489600"));
|
|
XC_CHECK_EQ(proto::buildOffer("uFrAg", "pWd", "AB:CD", false, "720"),
|
|
expectedOffer("42e01f", "max-fs=3600;max-mbps=108000"));
|
|
XC_CHECK_EQ(proto::buildOffer("uFrAg", "pWd", "AB:CD", true, "1080"),
|
|
expectedOffer("42e020", "max-fs=3600;max-mbps=108000"));
|
|
XC_CHECK_EQ(proto::buildOffer("uFrAg\r\n", " pWd", "sha-256 AB:CD", true, "720"),
|
|
expectedOffer("42e020", "max-fs=3600;max-mbps=108000"));
|
|
XC_CHECK(proto::buildOffer("", "p", "AB", false, "720").empty());
|
|
XC_CHECK(proto::buildOffer("u", "", "AB", false, "720").empty());
|
|
XC_CHECK(proto::buildOffer("u", "p", "", false, "720").empty());
|
|
}
|
|
|
|
XC_TEST(protocol, invalid_utf8_never_throws) {
|
|
bool threw = false;
|
|
std::string out;
|
|
try {
|
|
out = proto::transactionComplete(std::string("id\xff"), std::string("{\"x\":\"\xc3\"}"));
|
|
out += proto::messageUnhandled(std::string("\xfe"), "/t");
|
|
out += proto::controlResolution(std::string("\xff"));
|
|
out += proto::icePostBody({std::string("candidate:1 1 UDP 1 1.2.3.4 5 typ host \xff")}, std::string("\xff"));
|
|
} catch (...) {
|
|
threw = true;
|
|
}
|
|
XC_CHECK(!threw);
|
|
XC_CHECK(!out.empty());
|
|
}
|