mirror of
https://github.com/kidfromjupiter/nearby.git
synced 2026-09-16 15:36:12 -04:00
rewrote bluetooth classic and ble l2cap sockets. added test for l2cap socket
This commit is contained in:
@@ -27,236 +27,93 @@
|
||||
#include "absl/time/time.h"
|
||||
#include "internal/platform/implementation/crypto.h"
|
||||
#include "gtest/gtest.h"
|
||||
#include "proto/mediums/ble_frames.pb.h"
|
||||
|
||||
namespace nearby {
|
||||
namespace linux {
|
||||
namespace {
|
||||
class BleL2capSocketTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ASSERT_EQ(socketpair(AF_UNIX, SOCK_STREAM, 0, fds_), 0);
|
||||
|
||||
using ::location::nearby::mediums::SocketControlFrame;
|
||||
using ::location::nearby::mediums::SocketVersion;
|
||||
socket_fd_ = fds_[0];
|
||||
peer_fd_ = fds_[1];
|
||||
|
||||
constexpr uint8_t kRequestDataConnection = 3;
|
||||
constexpr uint8_t kResponseDataConnectionReady = 23;
|
||||
|
||||
class SocketPair final {
|
||||
public:
|
||||
SocketPair() {
|
||||
int fds[2] = {-1, -1};
|
||||
ASSERT_EQ(0, socketpair(AF_UNIX, SOCK_STREAM, 0, fds));
|
||||
left_ = fds[0];
|
||||
right_ = fds[1];
|
||||
socket_ = std::make_unique<BleL2capSocket>(socket_fd_, 1);
|
||||
}
|
||||
|
||||
~SocketPair() {
|
||||
if (left_ >= 0) close(left_);
|
||||
if (right_ >= 0) close(right_);
|
||||
}
|
||||
void TearDown() override {
|
||||
if (socket_) {
|
||||
socket_->Close();
|
||||
}
|
||||
|
||||
int left() const { return left_; }
|
||||
int right() const { return right_; }
|
||||
|
||||
private:
|
||||
int left_ = -1;
|
||||
int right_ = -1;
|
||||
};
|
||||
|
||||
bool ReadExact(int fd, char* data, size_t length) {
|
||||
size_t offset = 0;
|
||||
while (offset < length) {
|
||||
ssize_t read_count = recv(fd, data + offset, length - offset, 0);
|
||||
if (read_count <= 0) {
|
||||
return false;
|
||||
if (peer_fd_ >= 0) {
|
||||
close(peer_fd_);
|
||||
}
|
||||
offset += static_cast<size_t>(read_count);
|
||||
}
|
||||
return true;
|
||||
int fds_[2]{-1, -1};
|
||||
int socket_fd_{-1};
|
||||
int peer_fd_{-1};
|
||||
std::unique_ptr<BleL2capSocket> socket_;
|
||||
};
|
||||
TEST_F(BleL2capSocketTest, ReturnsInputAndOutputStreams) {
|
||||
InputStream& input = socket_->GetInputStream();
|
||||
OutputStream& output = socket_->GetOutputStream();
|
||||
|
||||
EXPECT_NE(&input, nullptr);
|
||||
EXPECT_NE(&output, nullptr);
|
||||
}
|
||||
TEST_F(BleL2capSocketTest, ReturnsSameStreamInstancesAcrossCalls) {
|
||||
InputStream& input1 = socket_->GetInputStream();
|
||||
InputStream& input2 = socket_->GetInputStream();
|
||||
|
||||
std::optional<std::string> ReceiveFrame(int fd) {
|
||||
std::array<char, 4> header;
|
||||
if (!ReadExact(fd, header.data(), header.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto* bytes = reinterpret_cast<const uint8_t*>(header.data());
|
||||
uint32_t payload_size = (static_cast<uint32_t>(bytes[0]) << 24) |
|
||||
(static_cast<uint32_t>(bytes[1]) << 16) |
|
||||
(static_cast<uint32_t>(bytes[2]) << 8) |
|
||||
static_cast<uint32_t>(bytes[3]);
|
||||
std::string payload(payload_size, '\0');
|
||||
if (payload_size > 0 && !ReadExact(fd, payload.data(), payload.size())) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return payload;
|
||||
OutputStream& output1 = socket_->GetOutputStream();
|
||||
OutputStream& output2 = socket_->GetOutputStream();
|
||||
|
||||
EXPECT_EQ(&input1, &input2);
|
||||
EXPECT_EQ(&output1, &output2);
|
||||
}
|
||||
TEST_F(BleL2capSocketTest, ReadReceivesExactBytesFromPeer) {
|
||||
std::string message = "hello into l2cap socket";
|
||||
|
||||
bool SendFrame(int fd, const std::string& payload) {
|
||||
std::array<char, 4> header = {
|
||||
static_cast<char>((payload.size() >> 24) & 0xFF),
|
||||
static_cast<char>((payload.size() >> 16) & 0xFF),
|
||||
static_cast<char>((payload.size() >> 8) & 0xFF),
|
||||
static_cast<char>(payload.size() & 0xFF),
|
||||
};
|
||||
if (send(fd, header.data(), header.size(), 0) != static_cast<ssize_t>(header.size())) {
|
||||
return false;
|
||||
}
|
||||
if (!payload.empty() &&
|
||||
send(fd, payload.data(), payload.size(), 0) != static_cast<ssize_t>(payload.size())) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
ASSERT_EQ(
|
||||
write(peer_fd_, message.data(), message.size()),
|
||||
static_cast<ssize_t>(message.size())
|
||||
);
|
||||
|
||||
InputStream& input = socket_->GetInputStream();
|
||||
|
||||
std::vector<char> buffer(message.size());
|
||||
auto out = input.Read(buffer.size()).GetResult();
|
||||
|
||||
EXPECT_EQ(
|
||||
out,
|
||||
ByteArray(message)
|
||||
);
|
||||
|
||||
EXPECT_EQ(
|
||||
out.AsStringView(),
|
||||
message
|
||||
);
|
||||
}
|
||||
TEST_F(BleL2capSocketTest, WriteSendsExactBytesToPeer) {
|
||||
std::string message = "hello from l2cap socket";
|
||||
|
||||
ByteArray ServiceHash(absl::string_view service_id) {
|
||||
ByteArray full_hash = Crypto::Sha256(service_id);
|
||||
EXPECT_GE(full_hash.size(), 3);
|
||||
return ByteArray(full_hash.data(), 3);
|
||||
OutputStream& output = socket_->GetOutputStream();
|
||||
|
||||
EXPECT_EQ(output.Write(message).value, Exception::kSuccess);
|
||||
|
||||
std::vector<char> received(message.size());
|
||||
|
||||
ssize_t n = read(peer_fd_, received.data(), received.size());
|
||||
|
||||
ASSERT_EQ(n, static_cast<ssize_t>(message.size()));
|
||||
|
||||
EXPECT_EQ(
|
||||
std::string(received.begin(), received.end()),
|
||||
message
|
||||
);
|
||||
}
|
||||
|
||||
std::string BuildLegacyControlPacket(uint8_t command) {
|
||||
return std::string(1, static_cast<char>(command));
|
||||
}
|
||||
|
||||
std::optional<uint8_t> ParseLegacyControlCommand(absl::string_view payload) {
|
||||
if (payload.empty()) return std::nullopt;
|
||||
if (payload.size() != 1 && payload.size() < 3) return std::nullopt;
|
||||
return static_cast<uint8_t>(payload[0]);
|
||||
}
|
||||
|
||||
std::string BuildLegacyIntroPacket(const ByteArray& service_hash) {
|
||||
SocketControlFrame frame;
|
||||
frame.set_type(SocketControlFrame::INTRODUCTION);
|
||||
auto* intro = frame.mutable_introduction();
|
||||
intro->set_service_id_hash(service_hash.AsStringView());
|
||||
intro->set_socket_version(SocketVersion::V2);
|
||||
|
||||
std::string serialized(frame.ByteSizeLong(), '\0');
|
||||
EXPECT_TRUE(frame.SerializeToArray(serialized.data(), serialized.size()));
|
||||
|
||||
std::string packet("\x00\x00\x00", 3);
|
||||
packet.append(serialized);
|
||||
return packet;
|
||||
}
|
||||
|
||||
bool IsLegacyIntroPacketForServiceHash(absl::string_view payload,
|
||||
const ByteArray& service_hash) {
|
||||
if (payload.size() <= 3 || payload.substr(0, 3) != "\x00\x00\x00") return false;
|
||||
|
||||
SocketControlFrame frame;
|
||||
if (!frame.ParseFromArray(payload.data() + 3, payload.size() - 3)) return false;
|
||||
return frame.type() == SocketControlFrame::INTRODUCTION &&
|
||||
frame.has_introduction() &&
|
||||
frame.introduction().socket_version() == SocketVersion::V2 &&
|
||||
frame.introduction().service_id_hash() == service_hash.AsStringView();
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, RefactoredOutputStreamWritesFramedPayload) {
|
||||
SocketPair pair;
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1);
|
||||
|
||||
ASSERT_TRUE(socket.GetOutputStream().Write(ByteArray("hello")).Ok());
|
||||
auto payload = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(payload.has_value());
|
||||
EXPECT_EQ(*payload, "hello");
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, RefactoredInputStreamReadsFramedPayload) {
|
||||
SocketPair pair;
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1);
|
||||
|
||||
ASSERT_TRUE(SendFrame(pair.right(), "world"));
|
||||
ExceptionOr<ByteArray> read_result = socket.GetInputStream().Read(5);
|
||||
ASSERT_TRUE(read_result.ok());
|
||||
EXPECT_EQ(read_result.result().string_data(), "world");
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, LegacyWritePrefixesServiceHash) {
|
||||
SocketPair pair;
|
||||
ByteArray service_hash = ServiceHash("service");
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1,
|
||||
BleL2capSocket::ProtocolMode::kLegacy,
|
||||
/*service_id=*/"service", /*incoming_connection=*/false);
|
||||
|
||||
ASSERT_TRUE(socket.GetOutputStream().Write(ByteArray("abc")).Ok());
|
||||
auto payload = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(payload.has_value());
|
||||
ASSERT_EQ(payload->size(), service_hash.size() + 3);
|
||||
EXPECT_EQ(payload->substr(0, service_hash.size()), service_hash.AsStringView());
|
||||
EXPECT_EQ(payload->substr(service_hash.size()), "abc");
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, LegacyOutgoingHandshakeSucceeds) {
|
||||
SocketPair pair;
|
||||
ByteArray service_hash = ServiceHash("service");
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1,
|
||||
BleL2capSocket::ProtocolMode::kLegacy,
|
||||
/*service_id=*/"service", /*incoming_connection=*/false);
|
||||
|
||||
std::thread peer([&]() {
|
||||
auto request = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(request.has_value());
|
||||
auto request_command = ParseLegacyControlCommand(*request);
|
||||
ASSERT_TRUE(request_command.has_value());
|
||||
EXPECT_EQ(*request_command, kRequestDataConnection);
|
||||
|
||||
ASSERT_TRUE(SendFrame(pair.right(), BuildLegacyControlPacket(kResponseDataConnectionReady)));
|
||||
|
||||
auto intro = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(intro.has_value());
|
||||
EXPECT_TRUE(IsLegacyIntroPacketForServiceHash(*intro, service_hash));
|
||||
});
|
||||
|
||||
EXPECT_TRUE(socket.PerformLegacyOutgoingHandshake(absl::Seconds(2)));
|
||||
peer.join();
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, LegacyOutgoingHandshakeTimesOutWithoutResponse) {
|
||||
SocketPair pair;
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1,
|
||||
BleL2capSocket::ProtocolMode::kLegacy,
|
||||
/*service_id=*/"service", /*incoming_connection=*/false);
|
||||
|
||||
std::thread peer([&]() {
|
||||
auto request = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(request.has_value());
|
||||
auto request_command = ParseLegacyControlCommand(*request);
|
||||
ASSERT_TRUE(request_command.has_value());
|
||||
EXPECT_EQ(*request_command, kRequestDataConnection);
|
||||
});
|
||||
|
||||
EXPECT_FALSE(socket.PerformLegacyOutgoingHandshake(absl::Milliseconds(200)));
|
||||
peer.join();
|
||||
}
|
||||
|
||||
TEST(BleL2capSocketTest, LegacyIncomingConnectionHandlesHandshakeAndData) {
|
||||
SocketPair pair;
|
||||
ByteArray service_hash = ServiceHash("service");
|
||||
BleL2capSocket socket(pair.left(), /*peripheral_id=*/1,
|
||||
BleL2capSocket::ProtocolMode::kLegacy,
|
||||
/*service_id=*/"service", /*incoming_connection=*/true);
|
||||
|
||||
std::thread peer([&]() {
|
||||
ASSERT_TRUE(SendFrame(pair.right(), BuildLegacyControlPacket(kRequestDataConnection)));
|
||||
|
||||
auto response = ReceiveFrame(pair.right());
|
||||
ASSERT_TRUE(response.has_value());
|
||||
auto response_command = ParseLegacyControlCommand(*response);
|
||||
ASSERT_TRUE(response_command.has_value());
|
||||
EXPECT_EQ(*response_command, kResponseDataConnectionReady);
|
||||
|
||||
ASSERT_TRUE(SendFrame(pair.right(), BuildLegacyIntroPacket(service_hash)));
|
||||
|
||||
std::string data_payload(service_hash.AsStringView());
|
||||
data_payload.append("hello");
|
||||
ASSERT_TRUE(SendFrame(pair.right(), data_payload));
|
||||
});
|
||||
|
||||
ExceptionOr<ByteArray> read_result = socket.GetInputStream().Read(5);
|
||||
ASSERT_TRUE(read_result.ok());
|
||||
EXPECT_EQ(read_result.result().string_data(), "hello");
|
||||
peer.join();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace linux
|
||||
} // namespace nearby
|
||||
|
||||
Reference in New Issue
Block a user