crashpad/util/net/http_transport_socket.cc
Lei Zhang c63c073d27 Do IWYU for check_op.h
Include check_op.h directly, instead of relying on the transitive
include from logging.h. This transitive include does not exist in
Chromium's //base.

Change-Id: I15962a9cdc26ac206032157b8d2659cf263ad695
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/4950200
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
2023-10-18 20:01:37 +00:00

602 lines
16 KiB
C++

// Copyright 2018 The Crashpad Authors
//
// 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 <fcntl.h>
#include <netdb.h>
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <iterator>
#include "base/check_op.h"
#include "base/logging.h"
#include "base/numerics/safe_conversions.h"
#include "base/posix/eintr_wrapper.h"
#include "base/scoped_generic.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "util/file/file_io.h"
#include "util/net/http_body.h"
#include "util/net/http_transport.h"
#include "util/net/url.h"
#include "util/stdlib/string_number_conversion.h"
#include "util/string/split_string.h"
#if defined(CRASHPAD_USE_BORINGSSL)
#include <openssl/ssl.h>
#endif
namespace crashpad {
namespace {
constexpr const char kCRLFTerminator[] = "\r\n";
class HTTPTransportSocket final : public HTTPTransport {
public:
HTTPTransportSocket() = default;
HTTPTransportSocket(const HTTPTransportSocket&) = delete;
HTTPTransportSocket& operator=(const HTTPTransportSocket&) = delete;
~HTTPTransportSocket() override = default;
bool ExecuteSynchronously(std::string* response_body) override;
};
struct ScopedAddrinfoTraits {
static addrinfo* InvalidValue() { return nullptr; }
static void Free(addrinfo* ai) { freeaddrinfo(ai); }
};
using ScopedAddrinfo = base::ScopedGeneric<addrinfo*, ScopedAddrinfoTraits>;
class Stream {
public:
virtual ~Stream() = default;
virtual bool LoggingWrite(const void* data, size_t size) = 0;
virtual bool LoggingRead(void* data, size_t size) = 0;
virtual bool LoggingReadToEOF(std::string* contents) = 0;
};
class FdStream : public Stream {
public:
explicit FdStream(int fd) : fd_(fd) { CHECK(fd_ >= 0); }
FdStream(const FdStream&) = delete;
FdStream& operator=(const FdStream&) = delete;
bool LoggingWrite(const void* data, size_t size) override {
return LoggingWriteFile(fd_, data, size);
}
bool LoggingRead(void* data, size_t size) override {
return LoggingReadFileExactly(fd_, data, size);
}
bool LoggingReadToEOF(std::string* result) override {
return crashpad::LoggingReadToEOF(fd_, result);
}
private:
int fd_;
};
#if defined(CRASHPAD_USE_BORINGSSL)
class SSLStream : public Stream {
public:
SSLStream() = default;
SSLStream(const SSLStream&) = delete;
SSLStream& operator=(const SSLStream&) = delete;
bool Initialize(const base::FilePath& root_cert_path,
int sock,
const std::string& hostname) {
SSL_library_init();
ctx_.reset(SSL_CTX_new(TLS_method()));
if (!ctx_.is_valid()) {
LOG(ERROR) << "SSL_CTX_new";
return false;
}
if (SSL_CTX_set_min_proto_version(ctx_.get(), TLS1_2_VERSION) <= 0) {
LOG(ERROR) << "SSL_CTX_set_min_proto_version";
return false;
}
SSL_CTX_set_verify(ctx_.get(), SSL_VERIFY_PEER, nullptr);
SSL_CTX_set_verify_depth(ctx_.get(), 5);
if (!root_cert_path.empty()) {
if (SSL_CTX_load_verify_locations(
ctx_.get(), root_cert_path.value().c_str(), nullptr) <= 0) {
LOG(ERROR) << "SSL_CTX_load_verify_locations";
return false;
}
} else {
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
if (SSL_CTX_load_verify_locations(
ctx_.get(), nullptr, "/etc/ssl/certs") <= 0) {
LOG(ERROR) << "SSL_CTX_load_verify_locations";
return false;
}
#elif BUILDFLAG(IS_FUCHSIA)
if (SSL_CTX_load_verify_locations(
ctx_.get(), "/config/ssl/cert.pem", nullptr) <= 0) {
LOG(ERROR) << "SSL_CTX_load_verify_locations";
return false;
}
#else
#error cert store location
#endif
}
ssl_.reset(SSL_new(ctx_.get()));
if (!ssl_.is_valid()) {
LOG(ERROR) << "SSL_new";
return false;
}
BIO* bio = BIO_new_socket(sock, BIO_NOCLOSE);
if (!bio) {
LOG(ERROR) << "BIO_new_socket";
return false;
}
// SSL_set_bio() takes ownership of |bio|.
SSL_set_bio(ssl_.get(), bio, bio);
if (SSL_set_tlsext_host_name(ssl_.get(), hostname.c_str()) == 0) {
LOG(ERROR) << "SSL_set_tlsext_host_name";
return false;
}
if (SSL_connect(ssl_.get()) <= 0) {
LOG(ERROR) << "SSL_connect";
return false;
}
return true;
}
bool LoggingWrite(const void* data, size_t size) override {
return SSL_write(ssl_.get(), data, size) != 0;
}
bool LoggingRead(void* data, size_t size) override {
return SSL_read(ssl_.get(), data, size) != 0;
}
bool LoggingReadToEOF(std::string* contents) override {
contents->clear();
char buffer[4096];
FileOperationResult rv;
while ((rv = SSL_read(ssl_.get(), buffer, sizeof(buffer))) > 0) {
DCHECK_LE(static_cast<size_t>(rv), sizeof(buffer));
contents->append(buffer, rv);
}
if (rv < 0) {
LOG(ERROR) << "SSL_read";
contents->clear();
return false;
}
return true;
}
private:
struct ScopedSSLCTXTraits {
static SSL_CTX* InvalidValue() { return nullptr; }
static void Free(SSL_CTX* ctx) { SSL_CTX_free(ctx); }
};
using ScopedSSLCTX = base::ScopedGeneric<SSL_CTX*, ScopedSSLCTXTraits>;
struct ScopedSSLTraits {
static SSL* InvalidValue() { return nullptr; }
static void Free(SSL* ssl) {
SSL_shutdown(ssl);
SSL_free(ssl);
}
};
using ScopedSSL = base::ScopedGeneric<SSL*, ScopedSSLTraits>;
ScopedSSLCTX ctx_;
ScopedSSL ssl_;
};
#endif
bool WaitUntilSocketIsReady(int sock) {
pollfd pollfds;
pollfds.fd = sock;
pollfds.events = POLLIN | POLLPRI | POLLOUT;
constexpr int kTimeoutMS = 1000;
int ret = HANDLE_EINTR(poll(&pollfds, 1, kTimeoutMS));
if (ret < 0) {
PLOG(ERROR) << "poll";
return false;
} else if (ret == 1) {
if (pollfds.revents & POLLERR) {
int err;
socklen_t err_len = sizeof(err);
if (getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &err_len) != 0) {
PLOG(ERROR) << "getsockopt";
} else {
errno = err;
PLOG(ERROR) << "POLLERR";
}
return false;
}
if (pollfds.revents & POLLHUP) {
return false;
}
return (pollfds.revents & POLLIN) != 0 || (pollfds.revents & POLLOUT) != 0;
}
// Timeout.
return false;
}
class ScopedSetNonblocking {
public:
explicit ScopedSetNonblocking(int sock) : sock_(sock) {
int flags = fcntl(sock, F_GETFL, 0);
if (flags < 0) {
PLOG(ERROR) << "fcntl";
sock_ = -1;
return;
}
if (fcntl(sock_, F_SETFL, flags | O_NONBLOCK) < 0) {
PLOG(ERROR) << "fcntl";
sock_ = -1;
}
}
ScopedSetNonblocking(const ScopedSetNonblocking&) = delete;
ScopedSetNonblocking& operator=(const ScopedSetNonblocking&) = delete;
~ScopedSetNonblocking() {
if (sock_ >= 0) {
int flags = fcntl(sock_, F_GETFL, 0);
if (flags < 0) {
PLOG(ERROR) << "fcntl";
return;
}
if (fcntl(sock_, F_SETFL, flags & (~O_NONBLOCK)) < 0) {
PLOG(ERROR) << "fcntl";
}
}
}
private:
int sock_;
};
base::ScopedFD CreateSocket(const std::string& hostname,
const std::string& port) {
addrinfo hints = {};
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = 0;
hints.ai_flags = 0;
addrinfo* addrinfo_raw;
if (getaddrinfo(hostname.c_str(), port.c_str(), &hints, &addrinfo_raw) < 0) {
PLOG(ERROR) << "getaddrinfo";
return base::ScopedFD();
}
ScopedAddrinfo addrinfo(addrinfo_raw);
for (const auto* ap = addrinfo.get(); ap; ap = ap->ai_next) {
base::ScopedFD result(
socket(ap->ai_family, ap->ai_socktype, ap->ai_protocol));
if (!result.is_valid()) {
continue;
}
{
// Set socket to non-blocking to avoid hanging for a long time if the
// network is down.
ScopedSetNonblocking nonblocking(result.get());
if (HANDLE_EINTR(connect(result.get(), ap->ai_addr, ap->ai_addrlen)) <
0) {
if (errno != EINPROGRESS) {
PLOG(ERROR) << "connect";
} else if (WaitUntilSocketIsReady(result.get())) {
return result;
}
return base::ScopedFD();
}
return result;
}
}
return base::ScopedFD();
}
bool WriteRequest(Stream* stream,
const std::string& method,
const std::string& resource,
const HTTPHeaders& headers,
HTTPBodyStream* body_stream) {
std::string request_line = base::StringPrintf(
"%s %s HTTP/1.0\r\n", method.c_str(), resource.c_str());
if (!stream->LoggingWrite(request_line.data(), request_line.size()))
return false;
// Write headers, and determine if Content-Length has been specified.
bool chunked = true;
size_t content_length = 0;
for (const auto& header : headers) {
std::string header_str = base::StringPrintf(
"%s: %s\r\n", header.first.c_str(), header.second.c_str());
if (header.first == kContentLength) {
chunked = !base::StringToSizeT(header.second, &content_length);
DCHECK(!chunked);
}
if (!stream->LoggingWrite(header_str.data(), header_str.size()))
return false;
}
// If no Content-Length, then encode as chunked, so add that header too.
if (chunked) {
static constexpr const char kTransferEncodingChunked[] =
"Transfer-Encoding: chunked\r\n";
if (!stream->LoggingWrite(kTransferEncodingChunked,
strlen(kTransferEncodingChunked))) {
return false;
}
}
if (!stream->LoggingWrite(kCRLFTerminator, strlen(kCRLFTerminator))) {
return false;
}
FileOperationResult data_bytes;
do {
constexpr size_t kCRLFSize = std::size(kCRLFTerminator) - 1;
struct __attribute__((packed)) {
char size[8];
char crlf[2];
uint8_t data[32 * 1024 + kCRLFSize];
} buf;
static_assert(
sizeof(buf) == sizeof(buf.size) + sizeof(buf.crlf) + sizeof(buf.data),
"buf should not have padding");
// Read a block of data.
data_bytes =
body_stream->GetBytesBuffer(buf.data, sizeof(buf.data) - kCRLFSize);
if (data_bytes == -1) {
return false;
}
DCHECK_GE(data_bytes, 0);
DCHECK_LE(static_cast<size_t>(data_bytes), sizeof(buf.data) - kCRLFSize);
void* write_start;
size_t write_size;
if (chunked) {
// Chunked encoding uses the entirety of buf. buf.size is presented in
// hexadecimal without any leading "0x". The terminating CR and LF will be
// placed immediately following the used portion of buf.data, even if
// buf.data is not full.
char tmp[9];
int rv = snprintf(tmp,
sizeof(tmp),
"%08x",
base::checked_cast<unsigned int>(data_bytes));
DCHECK_EQ(static_cast<size_t>(rv), sizeof(buf.size));
strncpy(buf.size, tmp, sizeof(buf.size));
DCHECK_NE(buf.size[sizeof(buf.size) - 1], '\0');
memcpy(&buf.crlf[0], kCRLFTerminator, kCRLFSize);
memcpy(&buf.data[data_bytes], kCRLFTerminator, kCRLFSize);
// Skip leading zeroes in the chunk size.
size_t size_len;
for (size_len = sizeof(buf.size); size_len > 1; --size_len) {
if (buf.size[sizeof(buf.size) - size_len] != '0') {
break;
}
}
write_start = static_cast<char*>(buf.crlf) - size_len;
write_size = size_len + sizeof(buf.crlf) + data_bytes + kCRLFSize;
} else {
// When not using chunked encoding, only use buf.data.
write_start = buf.data;
write_size = data_bytes;
}
// write_size will be 0 at EOF in non-chunked mode. Skip the write in that
// case. In contrast, at EOF in chunked mode, a zero-length chunk must be
// sent to signal EOF. This will happen when processing the EOF indicated by
// a 0 return from body_stream()->GetBytesBuffer() above.
if (write_size != 0) {
if (!stream->LoggingWrite(write_start, write_size))
return false;
}
} while (data_bytes > 0);
return true;
}
bool ReadLine(Stream* stream, std::string* line) {
line->clear();
for (;;) {
char byte;
if (!stream->LoggingRead(&byte, 1)) {
return false;
}
line->append(&byte, 1);
if (byte == '\n')
return true;
}
}
bool StartsWith(const std::string& str, const char* with, size_t len) {
return str.compare(0, len, with) == 0;
}
bool ReadResponseLine(Stream* stream) {
std::string response_line;
if (!ReadLine(stream, &response_line)) {
LOG(ERROR) << "ReadLine";
return false;
}
static constexpr const char kHttp10[] = "HTTP/1.0 ";
static constexpr const char kHttp11[] = "HTTP/1.1 ";
if (!(StartsWith(response_line, kHttp10, strlen(kHttp10)) ||
StartsWith(response_line, kHttp11, strlen(kHttp11))) ||
response_line.size() < strlen(kHttp10) + 3 ||
response_line.at(strlen(kHttp10) + 3) != ' ') {
return false;
}
unsigned int http_status = 0;
return base::StringToUint(response_line.substr(strlen(kHttp10), 3),
&http_status) &&
http_status >= 200 && http_status <= 203;
}
bool ReadResponseHeaders(Stream* stream, HTTPHeaders* headers) {
for (;;) {
std::string line;
if (!ReadLine(stream, &line)) {
return false;
}
if (line == kCRLFTerminator) {
return true;
}
std::string left, right;
if (!SplitStringFirst(line, ':', &left, &right)) {
LOG(ERROR) << "SplitStringFirst";
return false;
}
DCHECK_EQ(right[right.size() - 1], '\n');
DCHECK_EQ(right[right.size() - 2], '\r');
DCHECK_EQ(right[0], ' ');
DCHECK_NE(right[1], ' ');
right = right.substr(1, right.size() - 3);
(*headers)[left] = right;
}
}
bool ReadContentChunked(Stream* stream, std::string* body) {
// TODO(scottmg): https://crashpad.chromium.org/bug/196.
LOG(ERROR) << "TODO(scottmg): chunked response read";
return false;
}
bool ReadResponse(Stream* stream, std::string* response_body) {
response_body->clear();
if (!ReadResponseLine(stream)) {
return false;
}
HTTPHeaders response_headers;
if (!ReadResponseHeaders(stream, &response_headers)) {
return false;
}
auto it = response_headers.find("Content-Length");
size_t len = 0;
if (it != response_headers.end()) {
if (!base::StringToSizeT(it->second, &len)) {
LOG(ERROR) << "invalid Content-Length";
return false;
}
}
if (len) {
response_body->resize(len, 0);
return stream->LoggingRead(&(*response_body)[0], len);
}
it = response_headers.find("Transfer-Encoding");
bool chunked = false;
if (it != response_headers.end() && it->second == "chunked") {
chunked = true;
}
return chunked ? ReadContentChunked(stream, response_body)
: stream->LoggingReadToEOF(response_body);
}
bool HTTPTransportSocket::ExecuteSynchronously(std::string* response_body) {
std::string scheme, hostname, port, resource;
if (!CrackURL(url(), &scheme, &hostname, &port, &resource)) {
return false;
}
#if !defined(CRASHPAD_USE_BORINGSSL)
CHECK(scheme == "http") << "Got " << scheme << " for scheme in '" << url()
<< "'";
#endif
base::ScopedFD sock(CreateSocket(hostname, port));
if (!sock.is_valid()) {
return false;
}
#if defined(CRASHPAD_USE_BORINGSSL)
std::unique_ptr<Stream> stream;
if (scheme == "https") {
auto ssl_stream = std::make_unique<SSLStream>();
if (!ssl_stream->Initialize(
root_ca_certificate_path(), sock.get(), hostname)) {
LOG(ERROR) << "SSLStream Initialize";
return false;
}
stream = std::move(ssl_stream);
} else {
stream = std::make_unique<FdStream>(sock.get());
}
#else // CRASHPAD_USE_BORINGSSL
std::unique_ptr<Stream> stream(std::make_unique<FdStream>(sock.get()));
#endif // CRASHPAD_USE_BORINGSSL
if (!WriteRequest(
stream.get(), method(), resource, headers(), body_stream())) {
return false;
}
if (!ReadResponse(stream.get(), response_body)) {
return false;
}
return true;
}
} // namespace
// static
std::unique_ptr<HTTPTransport> HTTPTransport::Create() {
return std::unique_ptr<HTTPTransportSocket>(new HTTPTransportSocket);
}
} // namespace crashpad