File: crash_file_uploader.cc

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (435 lines) | stat: -rw-r--r-- 16,393 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "remoting/host/crash/crash_file_uploader.h"

#include <list>
#include <memory>
#include <string>
#include <utility>

#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/single_thread_task_runner_thread_mode.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/time/time.h"
#include "build/buildflag.h"
#include "net/base/load_flags.h"
#include "net/base/mime_util.h"
#include "net/base/net_errors.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "remoting/base/crash/breakpad_utils.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_response_head.mojom.h"

namespace remoting {

namespace {

// Post multi-part form-data names for the upload request.
constexpr char kProductNameKey[] = "prod";
constexpr char kProductVersionKey[] = "ver";
constexpr char kProcessUptimeKey[] = "ptime";
constexpr char kMinidumpFileKey[] = "upload_file_minidump";
constexpr char kMinidumpFileName[] = "dump";

#if BUILDFLAG(IS_WIN)
constexpr char kProductNameValue[] = "Chromoting";
#elif BUILDFLAG(IS_LINUX)
constexpr char kProductNameValue[] = "Chromoting_Linux";
#elif BUILDFLAG(IS_MAC)
constexpr char kProductNameValue[] = "Chromoting_Mac";
#else
#error Platform not supported
#endif

const base::FilePath::CharType kDumpExtension[] = FILE_PATH_LITERAL("dmp");
const base::FilePath::CharType kJsonExtension[] = FILE_PATH_LITERAL("json");
const base::FilePath::CharType kLastUploadTimeFilePath[] =
    FILE_PATH_LITERAL("last_upload_time.txt");
const base::FilePath::CharType kUploadResultFilePath[] =
    FILE_PATH_LITERAL("upload_result.txt");

constexpr char kCrashReportUploadUrl[] =
    "https://clients2.google.com/cr/report";

// Used to reserve space for the non-file data in the post form to prevent
// string re-allocations which building up the multi-part post form.
constexpr int kPostFormReservationSize = 4096;

// A successful response should only contain the report_id so this is bigger
// than is needed for a typical response but it prevents a ridiculously large
// value and will ensure we don't reject the response if the format changes in
// the future.
constexpr size_t kMaxResponseSize = 1024;

// Throttle uploads to 1 per hour.
constexpr base::TimeDelta kUploadRateLimitWindow = base::Hours(1);

constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("crash_file_uploader",
                                        R"(
        semantics {
          sender: "Chrome Remote Desktop"
          description:
            "Uploads crash reports generated by Chrome Remote Desktop."
          trigger:
            "The upload request is made when a minidump file is generated by "
            "the Chrome Remote Desktop host service after the user has opted "
            "into crash reporting."
          data: "Minidump file and product-specific info (e.g. version)."
          destination: GOOGLE_OWNED_SERVICE
          internal {
            contacts {
              email: "chromoting-team@google.com"
            }
          }
          user_data {
            type: ARBITRARY_DATA
          }
          last_reviewed: "2023-05-12"
        }
        policy {
          cookies_allowed: NO
          setting:
            "This request will not be sent if crash reporting is not enabled "
            "for Chrome Remote Desktop."
          policy_exception_justification:
            "Not implemented."
        })");

base::FilePath GetLastUploadTimeFilePath() {
  return GetMinidumpDirectoryPath().Append(kLastUploadTimeFilePath);
}

base::FilePath GetCrashDirectoryPath(const base::FilePath& crash_guid) {
  return GetMinidumpDirectoryPath().Append(crash_guid);
}

base::FilePath GetCrashFileBase(const base::FilePath& crash_guid) {
  return GetCrashDirectoryPath(crash_guid).Append(crash_guid);
}

base::FilePath GetDumpFilePath(const base::FilePath& crash_guid) {
  return GetCrashFileBase(crash_guid).AddExtension(kDumpExtension);
}

base::FilePath GetMetadataFilePath(const base::FilePath& crash_guid) {
  return GetCrashFileBase(crash_guid).AddExtension(kJsonExtension);
}

bool RetrieveCrashReportDetails(const base::FilePath& crash_guid,
                                std::string& minidump_file_contents,
                                base::Value::Dict& metadata,
                                std::string& error_reason) {
  base::FilePath minidump_file_path = GetDumpFilePath(crash_guid);
  if (!base::PathExists(minidump_file_path)) {
    error_reason = "Upload directory is missing the minidump file";
    return false;
  }

  std::string minidump_string;
  if (!base::ReadFileToString(minidump_file_path, &minidump_string)) {
    error_reason = "Failed to read dump file contents";
    return false;
  }

  base::FilePath metadata_file = GetMetadataFilePath(crash_guid);
  if (!base::PathExists(metadata_file)) {
    error_reason = "Upload directory is missing the metadata file";
    return false;
  }

  std::string metadata_file_contents;
  if (!base::ReadFileToString(metadata_file, &metadata_file_contents)) {
    error_reason = "Failed to read metadata file";
    return false;
  }

  std::optional<base::Value::Dict> opt_metadata =
      base::JSONReader::ReadDict(metadata_file_contents);
  if (!opt_metadata.has_value()) {
    error_reason = "Failed to parse metadata file contents";
    return false;
  }

  // Ensure the metadata file has the required fields. Note that the metadata
  // file may contain additional fields (e.g. for troubleshooting or manual
  // uploading) but there are only a few which are required for the upload.
  const std::string* version =
      opt_metadata->FindString(kBreakpadProductVersionKey);
  if (version == nullptr || version->empty()) {
    error_reason = "Metadata file is missing the product version field";
    return false;
  }
  const std::string* uptime =
      opt_metadata->FindString(kBreakpadProcessUptimeKey);
  if (uptime == nullptr || uptime->empty()) {
    error_reason = "Metadata file is missing the process uptime field";
    return false;
  }

  metadata = std::move(*opt_metadata);
  minidump_file_contents = std::move(minidump_string);
  return true;
}

void DeleteDumpFileAndWriteResult(const base::FilePath& crash_file,
                                  const std::string& result) {
  if (!base::DeleteFile(crash_file)) {
    LOG(WARNING) << "Failed to delete minidump file: " << crash_file;
  }

  base::FilePath result_file =
      crash_file.DirName().Append(kUploadResultFilePath);
  if (!base::WriteFile(result_file, result + "\r\n")) {
    LOG(WARNING) << "Failed to write upload result to: " << result_file;
  }
}

std::unique_ptr<network::SimpleURLLoader> CreateSimpleUrlLoader() {
  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = GURL(kCrashReportUploadUrl);
  resource_request->load_flags =
      net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  resource_request->method = net::HttpRequestHeaders::kPostMethod;

  std::unique_ptr<network::SimpleURLLoader> simple_url_loader =
      network::SimpleURLLoader::Create(std::move(resource_request),
                                       kTrafficAnnotation);
  simple_url_loader->SetTimeoutDuration(base::Seconds(60));
  simple_url_loader->SetAllowHttpErrorResults(false);
  simple_url_loader->SetRetryOptions(
      3, network::SimpleURLLoader::RETRY_ON_5XX |
             network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);

  return simple_url_loader;
}

void GenerateMultiPartPostData(const base::Value::Dict& metadata,
                               const std::string& minidump_data,
                               std::string& post_data,
                               std::string& content_type) {
  post_data.clear();
  content_type.clear();

  post_data.reserve(minidump_data.size() + kPostFormReservationSize);

  std::string mime_boundary = net::GenerateMimeMultipartBoundary();

  // Add the product name.
  net::AddMultipartValueForUpload(kProductNameKey, kProductNameValue,
                                  mime_boundary, std::string(), &post_data);
  // Add the product version.
  const std::string* version = metadata.FindString(kBreakpadProductVersionKey);
  net::AddMultipartValueForUpload(kProductVersionKey, *version, mime_boundary,
                                  std::string(), &post_data);
  // Add the process uptime.
  const std::string* uptime = metadata.FindString(kBreakpadProcessUptimeKey);
  net::AddMultipartValueForUpload(kProcessUptimeKey, *uptime, mime_boundary,
                                  std::string(), &post_data);
  // Add the minidump file.
  net::AddMultipartValueForUploadWithFileName(
      kMinidumpFileKey, kMinidumpFileName, minidump_data, mime_boundary,
      "application/octet-stream", &post_data);
  // Add the final delimiter.
  net::AddMultipartFinalDelimiterForUpload(mime_boundary, &post_data);
  post_data.shrink_to_fit();

  content_type = "multipart/form-data; boundary=" + mime_boundary;
}

void DeleteFileWithLogging(const base::FilePath& file_to_delete) {
  if (!base::DeleteFile(file_to_delete)) {
    LOG(WARNING) << "Failed to delete file: " << file_to_delete;
  }
}

bool SkipUploadDueToRateLimiting() {
  base::FilePath last_upload_time_file = GetLastUploadTimeFilePath();
  if (!base::PathExists(last_upload_time_file)) {
    return false;
  }

  std::string last_upload_time_str;
  if (!base::ReadFileToString(last_upload_time_file, &last_upload_time_str)) {
    LOG(WARNING) << "Failed to read file: " << last_upload_time_file;
    DeleteFileWithLogging(last_upload_time_file);
    return false;
  }

  time_t last_upload_time_t = 0;
  if (!base::StringToInt64(last_upload_time_str, &last_upload_time_t)) {
    LOG(WARNING) << "Failed to convert last_upload_time: "
                 << last_upload_time_str;
    DeleteFileWithLogging(last_upload_time_file);
    return false;
  }

  auto time_since_last_upload = base::Time::NowFromSystemTime() -
                                base::Time::FromTimeT(last_upload_time_t);
  return time_since_last_upload < kUploadRateLimitWindow;
}

void UpdateLastUploadTimeFile() {
  std::string now =
      base::NumberToString(base::Time::NowFromSystemTime().ToTimeT());
  if (!base::WriteFile(GetLastUploadTimeFilePath(), now)) {
    LOG(WARNING) << "Failed to write current time to upload rate limiting file "
                 << ", crash reporting will not be rate limited correctly";
  }
}

}  // namespace

class CrashFileUploader::Core {
 public:
  explicit Core(
      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory);
  Core(const Core&) = delete;
  Core& operator=(const Core&) = delete;
  ~Core();

  void Upload(const base::FilePath& crash_guid);

 private:
  using SimpleURLLoaderList =
      std::list<std::unique_ptr<network::SimpleURLLoader>>;
  void OnUploadComplete(SimpleURLLoaderList::iterator it,
                        base::FilePath crash_guid,
                        std::optional<std::string> response_body);

  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
  SimpleURLLoaderList simple_url_loaders_;

  THREAD_CHECKER(thread_checker_);

  base::WeakPtrFactory<Core> weak_ptr_factory_{this};
};

CrashFileUploader::Core::Core(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : url_loader_factory_(url_loader_factory) {
  DETACH_FROM_THREAD(thread_checker_);
}

CrashFileUploader::Core::~Core() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

void CrashFileUploader::Core::Upload(const base::FilePath& crash_guid) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  base::FilePath crash_report_directory = GetCrashDirectoryPath(crash_guid);
  LOG(INFO) << "Validating crash report files in " << crash_report_directory;

  if (!base::DirectoryExists(crash_report_directory)) {
    LOG(ERROR) << "Upload directory does not exist for report: " << crash_guid;
    return;
  }

  if (SkipUploadDueToRateLimiting()) {
    std::string rate_limit_error("Upload skipped due to rate limiting");
    LOG(WARNING) << rate_limit_error;
    DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), rate_limit_error);
    return;
  }

  base::Value::Dict metadata;
  std::string minidump_data;
  std::string error;
  if (!RetrieveCrashReportDetails(crash_guid, minidump_data, metadata, error)) {
    LOG(ERROR) << "Failed to retrieve crash report details: " << error;
    DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), error);
    return;
  }

  LOG(INFO) << "Sending crash report (" << crash_guid << ") to "
            << kCrashReportUploadUrl;

  auto simple_url_loader = CreateSimpleUrlLoader();
  auto* simple_url_loader_ptr = simple_url_loader.get();
  auto it = simple_url_loaders_.insert(simple_url_loaders_.begin(),
                                       std::move(simple_url_loader));

  std::string post_data;
  std::string content_type;
  GenerateMultiPartPostData(metadata, minidump_data, post_data, content_type);
  simple_url_loader_ptr->AttachStringForUpload(post_data, content_type);
  simple_url_loader_ptr->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&CrashFileUploader::Core::OnUploadComplete,
                     weak_ptr_factory_.GetWeakPtr(), std::move(it),
                     std::move(crash_guid)),
      kMaxResponseSize);
}

void CrashFileUploader::Core::OnUploadComplete(
    SimpleURLLoaderList::iterator it,
    base::FilePath crash_guid,
    std::optional<std::string> response_body) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  std::string upload_result;
  base::FilePath crash_dump = crash_guid.AddExtension(kDumpExtension);
  if ((*it)->NetError() == net::OK) {
    std::string report_id = std::move(response_body).value_or("empty");
    // Result file format looks like:
    // report_id: <id_from_crash_service>
    // go/crash/<id_from_crash_service>
    upload_result =
        "report_id: " + report_id + "\r\n" + "http://go/crash/" + report_id;
    // Include the crash report id and go link in the host log.
    LOG(INFO) << "Successfully uploaded: " << crash_dump << "\r\n"
              << "    report_id: " << report_id << "\r\n"
              << "    http://go/crash/" << report_id << "\r\n"
              << "    Please note that it may take a few minutes to finish "
              << "processing the report.";
    UpdateLastUploadTimeFile();
  } else {
    std::string response_code = "<unknown>";
    auto* response_info = (*it)->ResponseInfo();
    if (response_info && response_info->headers) {
      response_code =
          base::NumberToString(response_info->headers->response_code());
    }
    LOG(ERROR) << "Failed to upload crash report: " << crash_dump
               << ", response_code: " << response_code;
    upload_result = "Upload failed, response code: " + response_code;
  }

  simple_url_loaders_.erase(it);
  DeleteDumpFileAndWriteResult(GetDumpFilePath(crash_guid), upload_result);
}

CrashFileUploader::CrashFileUploader(
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
    scoped_refptr<base::SingleThreadTaskRunner> core_task_runner)
    : core_(std::make_unique<CrashFileUploader::Core>(url_loader_factory)),
      core_task_runner_(core_task_runner) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

CrashFileUploader::~CrashFileUploader() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  core_task_runner_->DeleteSoon(FROM_HERE, core_.release());
}

void CrashFileUploader::Upload(const base::FilePath& crash_guid) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  core_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&CrashFileUploader::Core::Upload,
                                base::Unretained(core_.get()), crash_guid));
}

}  // namespace remoting