blob: bf06fcb797a61537270057b846b26944bf00c355 [file]
/*
* Copyright (C) 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "src/trace_processor/rpc/rpc_transport.h"
#include <cstddef>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include "perfetto/base/compiler.h"
#include "perfetto/base/status.h"
#include "perfetto/ext/base/base64.h"
#include "perfetto/ext/base/status_macros.h"
#include "perfetto/ext/base/status_or.h"
#include "perfetto/ext/base/string_utils.h"
#include "perfetto/ext/base/unix_socket.h"
#include "src/trace_processor/rpc/session_paths.h"
namespace perfetto::trace_processor {
namespace {
// ---- AF_UNIX byte-pipe transport ------------------------------------------
class UnixByteTransport : public RpcTransport {
public:
explicit UnixByteTransport(base::UnixSocketRaw sock)
: sock_(std::move(sock)) {}
base::Status Send(const uint8_t* data, size_t len) override {
size_t off = 0;
while (off < len) {
ssize_t n = sock_.Send(data + off, len - off);
if (n <= 0)
return base::ErrStatus("Failed to send request to session");
off += static_cast<size_t>(n);
}
return base::OkStatus();
}
base::StatusOr<size_t> Recv(uint8_t* buf, size_t len) override {
ssize_t n = sock_.Receive(buf, len);
if (n < 0)
return base::ErrStatus("Error reading from session");
return static_cast<size_t>(n);
}
private:
base::UnixSocketRaw sock_;
};
// ---- WebSocket transport (to the http server's /websocket) ----------------
class WebSocketTransport : public RpcTransport {
public:
explicit WebSocketTransport(base::UnixSocketRaw sock)
: sock_(std::move(sock)) {}
base::Status Handshake(const std::string& host_port);
base::Status Send(const uint8_t* data, size_t len) override;
base::StatusOr<size_t> Recv(uint8_t* buf, size_t len) override;
private:
// The decoded fixed-size part of a WebSocket frame header: everything up to,
// but not including, the payload bytes. See RFC 6455 ยง5.2.
struct FrameHeader {
uint8_t opcode = 0; // 0x1 text, 0x2 binary, 0x8 close, etc.
bool masked = false; // Whether the payload is XOR-masked.
uint64_t payload_len = 0; // Number of payload bytes that follow.
uint8_t mask[4] = {0, 0, 0, 0}; // Mask key, valid only if |masked|.
};
// Blocking read of exactly |n| bytes. Returns false on EOF/error.
bool ReadExactly(void* dst, size_t n);
// Reads and decodes the next frame header off the wire (base bytes, extended
// length and, if present, the mask key). Returns false on EOF/error.
bool ReadFrameHeader(FrameHeader* out);
// Reads the |hdr.payload_len| payload bytes that follow |hdr|, unmasking them
// in place if needed. Returns false on EOF/error.
bool ReadFramePayload(const FrameHeader& hdr, std::string* payload);
// Reads complete WebSocket frames until a data frame arrives, appending its
// payload to inbound_. Control frames (ping/pong) are skipped. Returns false
// if the connection closed.
bool ReadFrameIntoBuffer();
base::UnixSocketRaw sock_;
std::string inbound_; // Un-consumed payload bytes from received frames.
size_t inbound_pos_ = 0;
};
bool WebSocketTransport::ReadExactly(void* dst, size_t n) {
auto* p = static_cast<uint8_t*>(dst);
size_t got = 0;
while (got < n) {
ssize_t r = sock_.Receive(p + got, n - got);
if (r <= 0)
return false;
got += static_cast<size_t>(r);
}
return true;
}
base::Status WebSocketTransport::Handshake(const std::string& host_port) {
// The server requires a Sec-WebSocket-Key of 16 base64-decoded bytes and an
// Origin in its allow-list (defaults include http://localhost:10000). It does
// not validate the key's value, so a fixed key is fine.
std::string key = base::Base64Encode("perfetto-tp-wsclient", 16);
std::string req =
"GET /websocket HTTP/1.1\r\n"
"Host: " +
host_port +
"\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Version: 13\r\n"
"Origin: http://localhost:10000\r\n"
"Sec-WebSocket-Key: " +
key + "\r\n\r\n";
if (sock_.Send(req.data(), req.size()) < 0)
return base::ErrStatus("WebSocket handshake send failed");
// Read response headers up to the blank line. Keep it simple: read byte by
// byte until "\r\n\r\n". Handshake responses are small.
std::string resp;
char c;
while (resp.find("\r\n\r\n") == std::string::npos) {
ssize_t r = sock_.Receive(&c, 1);
if (r <= 0)
return base::ErrStatus("WebSocket handshake closed by peer");
resp.push_back(c);
if (resp.size() > 8192)
return base::ErrStatus("WebSocket handshake response too large");
}
if (resp.find("101") == std::string::npos) {
size_t eol = resp.find("\r\n");
return base::ErrStatus("WebSocket upgrade rejected: %s",
resp.substr(0, eol).c_str());
}
return base::OkStatus();
}
base::Status WebSocketTransport::Send(const uint8_t* data, size_t len) {
// Client frames must be masked (RFC 6455). A zero mask leaves the payload
// unchanged, which the server unmasks back to the original bytes.
std::string frame;
frame.push_back(static_cast<char>(0x82)); // FIN | binary opcode.
if (len < 126) {
frame.push_back(static_cast<char>(0x80 | len));
} else if (len <= 0xFFFF) {
frame.push_back(static_cast<char>(0x80 | 126));
frame.push_back(static_cast<char>((len >> 8) & 0xFF));
frame.push_back(static_cast<char>(len & 0xFF));
} else {
frame.push_back(static_cast<char>(0x80 | 127));
for (int shift = 56; shift >= 0; shift -= 8)
frame.push_back(static_cast<char>((len >> shift) & 0xFF));
}
frame.append(4, '\0'); // 4-byte mask key, all zeros.
frame.append(reinterpret_cast<const char*>(data), len);
size_t off = 0;
while (off < frame.size()) {
ssize_t n = sock_.Send(frame.data() + off, frame.size() - off);
if (n <= 0)
return base::ErrStatus("Failed to send to WebSocket session");
off += static_cast<size_t>(n);
}
return base::OkStatus();
}
PERFETTO_ALWAYS_INLINE bool WebSocketTransport::ReadFrameHeader(
FrameHeader* out) {
// Byte 0 holds FIN/RSV/opcode; byte 1 holds the MASK bit and the 7-bit base
// payload length. We ignore FIN and assume unfragmented frames (the server
// never fragments our RPC responses).
uint8_t base[2];
if (!ReadExactly(base, 2))
return false;
out->opcode = base[0] & 0x0F;
out->masked = (base[1] & 0x80) != 0;
// The 7-bit length is either the real length, or a sentinel saying the real
// length follows in a 16-bit (126) or 64-bit (127) big-endian field.
uint64_t len = base[1] & 0x7F;
if (len == 126) {
uint8_t ext[2];
if (!ReadExactly(ext, 2))
return false;
len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
} else if (len == 127) {
uint8_t ext[8];
if (!ReadExactly(ext, 8))
return false;
len = 0;
for (uint8_t b : ext)
len = (len << 8) | b;
}
out->payload_len = len;
// A masked frame is followed by a 4-byte mask key, used to XOR the payload.
if (out->masked && !ReadExactly(out->mask, 4))
return false;
return true;
}
PERFETTO_ALWAYS_INLINE bool WebSocketTransport::ReadFramePayload(
const FrameHeader& hdr,
std::string* payload) {
payload->resize(static_cast<size_t>(hdr.payload_len));
if (hdr.payload_len > 0 && !ReadExactly(payload->data(), payload->size()))
return false;
// Each payload byte is XORed with mask[i % 4] to recover the original bytes.
if (hdr.masked) {
for (size_t i = 0; i < payload->size(); ++i)
(*payload)[i] = static_cast<char>((*payload)[i] ^ hdr.mask[i % 4]);
}
return true;
}
bool WebSocketTransport::ReadFrameIntoBuffer() {
// The server can interleave control frames (ping/pong) at any point, so loop
// until we get a data frame, skipping anything that isn't payload for us.
for (;;) {
FrameHeader hdr;
if (!ReadFrameHeader(&hdr))
return false;
switch (hdr.opcode) {
case 0x1: // Text.
case 0x2: // Binary.
// Read straight into inbound_, which Recv has already emptied before
// calling us, so the payload is never copied a second time.
if (!ReadFramePayload(hdr, &inbound_))
return false;
inbound_pos_ = 0;
return true;
case 0x8: // Close.
return false;
case 0x9: // Ping: ignore (server tolerates a missing pong for our use).
case 0xA: // Pong.
default: {
// Drain and discard the control frame's payload, then read the next.
std::string discard;
if (!ReadFramePayload(hdr, &discard))
return false;
continue;
}
}
}
}
base::StatusOr<size_t> WebSocketTransport::Recv(uint8_t* buf, size_t len) {
if (inbound_pos_ >= inbound_.size()) {
inbound_.clear();
inbound_pos_ = 0;
if (!ReadFrameIntoBuffer())
return size_t{0}; // Closed.
}
size_t avail = inbound_.size() - inbound_pos_;
size_t n = avail < len ? avail : len;
memcpy(buf, inbound_.data() + inbound_pos_, n);
inbound_pos_ += n;
return n;
}
} // namespace
RpcTransport::~RpcTransport() = default;
base::StatusOr<std::unique_ptr<RpcTransport>> ConnectRpcTransport(
const std::string& addr) {
switch (session::ClassifyRemoteAddr(addr)) {
case session::RemoteAddrKind::kHttp: {
auto sock = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kInet,
base::SockType::kStream);
sock.SetBlocking(true);
if (!sock || !sock.Connect(addr)) {
return base::ErrStatus("Could not connect to '%s'", addr.c_str());
}
auto ws = std::make_unique<WebSocketTransport>(std::move(sock));
RETURN_IF_ERROR(ws->Handshake(addr));
return std::unique_ptr<RpcTransport>(std::move(ws));
}
case session::RemoteAddrKind::kUnixPath:
case session::RemoteAddrKind::kSessionName: {
std::string socket_path = addr;
if (session::ClassifyRemoteAddr(addr) ==
session::RemoteAddrKind::kSessionName) {
ASSIGN_OR_RETURN(socket_path, session::SessionSocketPath(addr));
}
auto sock = base::UnixSocketRaw::CreateMayFail(base::SockFamily::kUnix,
base::SockType::kStream);
if (!sock || !sock.Connect(socket_path)) {
return base::ErrStatus(
"No live session at '%s'. Start one with: tp server unix --name "
"<name> <trace>",
addr.c_str());
}
sock.SetBlocking(true);
return std::unique_ptr<RpcTransport>(
std::make_unique<UnixByteTransport>(std::move(sock)));
}
}
PERFETTO_FATAL("Unreachable");
}
} // namespace perfetto::trace_processor