| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "src/developer/forensics/crash_reports/crash_server.h" |
| |
| #include <fuchsia/net/http/cpp/fidl.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <lib/zx/time.h> |
| |
| #include <memory> |
| #include <variant> |
| |
| #include <src/lib/fostr/fidl/fuchsia/net/http/formatting.h> |
| |
| #include "src/developer/forensics/crash_reports/sized_data_reader.h" |
| #include "src/developer/forensics/utils/sized_data.h" |
| #include "src/lib/fsl/socket/blocking_drain.h" |
| #include "src/lib/fsl/vmo/sized_vmo.h" |
| #include "src/lib/fsl/vmo/vector.h" |
| #include "third_party/crashpad/util/net/http_body.h" |
| #include "third_party/crashpad/util/net/http_headers.h" |
| #include "third_party/crashpad/util/net/http_multipart_builder.h" |
| #include "third_party/crashpad/util/net/http_transport.h" |
| #include "third_party/crashpad/util/net/url.h" |
| |
| namespace forensics { |
| namespace crash_reports { |
| namespace { |
| |
| // Builds a fuchsia::net::http::Request. crashpad::HTTPTransport is used as the base class so |
| // standard HTTP request building functionality doesn't need to be reimplemented. |
| class HttpRequestBuilder : public crashpad::HTTPTransport { |
| public: |
| std::optional<fuchsia::net::http::Request> Build() && { |
| using namespace fuchsia::net::http; |
| |
| // Create the headers for the request. |
| std::vector<Header> http_headers; |
| for (const auto& [name, value] : headers()) { |
| http_headers.push_back(Header{ |
| .name = std::vector<uint8_t>(name.begin(), name.end()), |
| .value = std::vector<uint8_t>(value.begin(), value.end()), |
| }); |
| } |
| |
| // Create the request body as a single VMO. |
| // TODO(fxbug.dev/59191): Consider using a zx::socket to transmit the HTTP request body to the |
| // server piecewise. |
| std::vector<uint8_t> body; |
| |
| // Reserve 256 kb for the request body. |
| body.reserve(256 * 1024); |
| while (true) { |
| // Copy the body in 32 kb chunks. |
| std::array<uint8_t, 32 * 1024> buf; |
| const auto result = body_stream()->GetBytesBuffer(buf.data(), buf.max_size()); |
| |
| FX_CHECK(result >= 0); |
| if (result == 0) { |
| break; |
| } |
| |
| body.insert(body.end(), buf.data(), buf.data() + result); |
| } |
| |
| fsl::SizedVmo body_vmo; |
| if (!fsl::VmoFromVector(body, &body_vmo)) { |
| return std::nullopt; |
| } |
| |
| // Create the request. |
| Request request; |
| request.set_method(method()) |
| .set_url(url()) |
| .set_deadline(zx::deadline_after(zx::sec((uint64_t)timeout())).get()) |
| .set_headers(std::move(http_headers)) |
| .set_body(Body::WithBuffer(std::move(body_vmo).ToTransport())); |
| |
| return request; |
| } |
| |
| private: |
| bool ExecuteSynchronously(std::string* response_body) override { |
| FX_LOGS(FATAL) << "Not implemented"; |
| return false; |
| } |
| }; |
| |
| } // namespace |
| |
| CrashServer::CrashServer(async_dispatcher_t* dispatcher, |
| std::shared_ptr<sys::ServiceDirectory> services, const std::string& url, |
| LogTags* tags) |
| : dispatcher_(dispatcher), services_(services), url_(url), tags_(tags) { |
| services_->Connect(loader_.NewRequest(dispatcher_)); |
| loader_.set_error_handler([](const zx_status_t status) { |
| FX_PLOGS(WARNING, status) << "Lost connection to fuchsia.net.http.Loader"; |
| }); |
| } |
| |
| void CrashServer::MakeRequest(const Report& report, const Snapshot& snapshot, |
| ::fit::function<void(UploadStatus, std::string)> callback) { |
| // Make sure a call to fuchsia.net.http.Loader/Fetch isn't outstanding. |
| FX_CHECK(!pending_request_); |
| |
| std::vector<SizedDataReader> attachment_readers; |
| attachment_readers.reserve(report.Attachments().size() + 2u /*minidump and snapshot*/); |
| |
| std::map<std::string, crashpad::FileReaderInterface*> file_readers; |
| |
| for (const auto& [k, v] : report.Attachments()) { |
| if (k.empty()) { |
| continue; |
| } |
| attachment_readers.emplace_back(v); |
| file_readers.emplace(k, &attachment_readers.back()); |
| } |
| |
| if (report.Minidump().has_value()) { |
| attachment_readers.emplace_back(report.Minidump().value()); |
| file_readers.emplace("uploadFileMinidump", &attachment_readers.back()); |
| } |
| |
| // We have to build the MIME multipart message ourselves as all the public Crashpad helpers are |
| // asynchronous and we won't be able to know the upload status nor the server report ID. |
| crashpad::HTTPMultipartBuilder http_multipart_builder; |
| http_multipart_builder.SetGzipEnabled(true); |
| |
| for (const auto& [key, value] : PrepareAnnotations(report, snapshot)) { |
| http_multipart_builder.SetFormData(key, value); |
| } |
| |
| // Add the snapshot archive (only relevant for ManagedSnapshots). |
| if (std::holds_alternative<ManagedSnapshot>(snapshot)) { |
| const auto& s = std::get<ManagedSnapshot>(snapshot); |
| if (const auto archive = s.LockArchive(); archive) { |
| attachment_readers.emplace_back(archive->value); |
| file_readers.emplace(archive->key, &attachment_readers.back()); |
| } |
| } |
| |
| for (const auto& [filename, content] : file_readers) { |
| http_multipart_builder.SetFileAttachment(filename, filename, content, |
| "application/octet-stream"); |
| } |
| crashpad::HTTPHeaders headers; |
| http_multipart_builder.PopulateContentHeaders(&headers); |
| |
| HttpRequestBuilder request_builder; |
| for (const auto& header : headers) { |
| request_builder.SetHeader(header.first, header.second); |
| } |
| request_builder.SetBodyStream(http_multipart_builder.GetBodyStream()); |
| request_builder.SetTimeout(60.0); // 1 minute. |
| request_builder.SetURL(url_); |
| |
| auto request = std::move(request_builder).Build(); |
| if (!request.has_value()) { |
| callback(CrashServer::UploadStatus::kFailure, ""); |
| return; |
| } |
| |
| if (!loader_) { |
| services_->Connect(loader_.NewRequest(dispatcher_)); |
| } |
| |
| const std::string tags = tags_->Get(report.Id()); |
| loader_->Fetch(std::move(request.value()), [this, tags, callback = std::move(callback)]( |
| fuchsia::net::http::Response response) mutable { |
| pending_request_ = false; |
| |
| if (response.has_error()) { |
| FX_LOGST(WARNING, tags.c_str()) << "Experienced network error: " << response.error(); |
| if (response.error() == fuchsia::net::http::Error::DEADLINE_EXCEEDED) { |
| callback(CrashServer::UploadStatus::kTimedOut, ""); |
| } else { |
| callback(CrashServer::UploadStatus::kFailure, ""); |
| } |
| return; |
| } |
| |
| std::string response_body; |
| if (response.has_body()) { |
| if (!fsl::BlockingDrainFrom(std::move(*response.mutable_body()), |
| [&response_body](const void* data, uint32_t len) { |
| const char* begin = static_cast<const char*>(data); |
| response_body.insert(response_body.end(), begin, begin + len); |
| return len; |
| })) { |
| FX_LOGST(WARNING, tags.c_str()) << "Failed to read http body"; |
| response_body.clear(); |
| } |
| } else { |
| FX_LOGST(WARNING, tags.c_str()) << "Http response is missing body"; |
| } |
| |
| if (!response.has_status_code()) { |
| FX_LOGST(ERROR, tags.c_str()) << "No status code received: " << response_body; |
| callback(CrashServer::UploadStatus::kFailure, ""); |
| return; |
| } |
| |
| if (response.status_code() == 429) { |
| FX_LOGST(WARNING, tags.c_str()) << "Upload throttled by server: " << response_body; |
| callback(CrashServer::UploadStatus::kThrottled, ""); |
| return; |
| } |
| |
| if (response.status_code() < 200 || response.status_code() >= 204) { |
| FX_LOGST(WARNING, tags.c_str()) << "Failed to upload report, received HTTP status code " |
| << response.status_code() << ": " << response_body; |
| callback(CrashServer::UploadStatus::kFailure, ""); |
| return; |
| } |
| |
| if (response_body.empty()) { |
| callback(CrashServer::UploadStatus::kFailure, ""); |
| } else { |
| callback(CrashServer::UploadStatus::kSuccess, std::move(response_body)); |
| } |
| }); |
| |
| pending_request_ = true; |
| } |
| |
| std::map<std::string, std::string> CrashServer::PrepareAnnotations(const Report& report, |
| const Snapshot& snapshot) { |
| // Start with annotations from |report| and only add "presence" annotations. |
| // |
| // * If |snapshot| is a MissingSnapshot, they contain potentially new information about why the |
| // underlying data was dropped by the SnapshotManager. |
| // |
| // * If |snapshot| is a ManagedSnapshot, they're either empty or contain potentially new |
| // information about why the underlying archive (the annotations are still present) was dropped |
| // by the SnapshotManager. |
| auto annotations = report.Annotations(); |
| |
| if (std::holds_alternative<MissingSnapshot>(snapshot)) { |
| const auto& s = std::get<MissingSnapshot>(snapshot); |
| annotations.Set(s.PresenceAnnotations()); |
| return annotations.Raw(); |
| } |
| |
| const auto& s = std::get<ManagedSnapshot>(snapshot); |
| if (const auto presence_annotations = s.LockPresenceAnnotations(); presence_annotations) { |
| annotations.Set(*presence_annotations); |
| } |
| |
| return annotations.Raw(); |
| } |
| |
| } // namespace crash_reports |
| } // namespace forensics |