diff --git a/DEPS b/DEPS index a2def66c..190f5a30 100644 --- a/DEPS +++ b/DEPS @@ -38,7 +38,7 @@ deps = { 'crashpad/third_party/mini_chromium/mini_chromium': Var('chromium_git') + '/chromium/mini_chromium@' + - '3a2d52d74c9af5277bf6456cc00ae728f89c4898', + '9e0d322ae9f87acbe17c4ced025319b4964bf0b7', 'crashpad/third_party/zlib/zlib': Var('chromium_git') + '/chromium/src/third_party/zlib@' + '13dc246a58e4b72104d35f9b1809af95221ebda7', diff --git a/client/crashpad_client.h b/client/crashpad_client.h index f6f3395c..7799bd9c 100644 --- a/client/crashpad_client.h +++ b/client/crashpad_client.h @@ -132,6 +132,27 @@ class CrashpadClient { //! //! \return `true` on success, `false` on failure with a message logged. bool SetHandlerMachPort(base::mac::ScopedMachSendRight exception_port); + + //! \brief Retrieves a send right to the process’ crash handler Mach port. + //! + //! This method is only defined on macOS. + //! + //! This method can be used to obtain the crash handler Mach port when a + //! Crashpad client process wishes to provide a send right to this port to + //! another process. The IPC mechanism used to convey the right is under the + //! application’s control. If the other process wishes to become a client of + //! the same crash handler, it can provide the transferred right to + //! SetHandlerMachPort(). + //! + //! See StartHandler() for more detail on how the port and handler are + //! configured. + //! + //! \return The Mach port set by SetHandlerMachPort(), possibly indirectly by + //! a call to another method such as StartHandler() or + //! SetHandlerMachService(). This method must only be called after a + //! successful call to one of those methods. `MACH_PORT_NULL` on failure + //! with a message logged. + base::mac::ScopedMachSendRight GetHandlerMachPort() const; #endif #if defined(OS_WIN) || DOXYGEN @@ -155,14 +176,15 @@ class CrashpadClient { //! \brief Retrieves the IPC pipe name used to register with the Crashpad //! handler. //! + //! This method is only defined on Windows. + //! //! This method retrieves the IPC pipe name set by SetHandlerIPCPipe(), or a - //! suitable IPC pipe name chosen by StartHandler(). It is intended to be used + //! suitable IPC pipe name chosen by StartHandler(). It must only be called + //! after a successful call to one of those methods. It is intended to be used //! to obtain the IPC pipe name so that it may be passed to other processes, //! so that they may register with an existing Crashpad handler by calling //! SetHandlerIPCPipe(). //! - //! This method is only defined on Windows. - //! //! \return The full name of the crash handler IPC pipe, a string of the form //! `"\\.\pipe\NAME"`. std::wstring GetHandlerIPCPipe() const; @@ -257,10 +279,12 @@ class CrashpadClient { #endif private: -#if defined(OS_WIN) +#if defined(OS_MACOSX) + base::mac::ScopedMachSendRight exception_port_; +#elif defined(OS_WIN) std::wstring ipc_pipe_; ScopedKernelHANDLE handler_start_thread_; -#endif +#endif // OS_MACOSX DISALLOW_COPY_AND_ASSIGN(CrashpadClient); }; diff --git a/client/crashpad_client_mac.cc b/client/crashpad_client_mac.cc index cc50969e..978024e5 100644 --- a/client/crashpad_client_mac.cc +++ b/client/crashpad_client_mac.cc @@ -523,7 +523,7 @@ class HandlerStarter final : public NotifyServer::DefaultInterface { } // namespace -CrashpadClient::CrashpadClient() { +CrashpadClient::CrashpadClient() : exception_port_(MACH_PORT_NULL) { } CrashpadClient::~CrashpadClient() { @@ -569,8 +569,42 @@ bool CrashpadClient::SetHandlerMachService(const std::string& service_name) { bool CrashpadClient::SetHandlerMachPort( base::mac::ScopedMachSendRight exception_port) { + DCHECK(!exception_port_.is_valid()); DCHECK(exception_port.is_valid()); - return SetCrashExceptionPorts(exception_port.get()); + + if (!SetCrashExceptionPorts(exception_port.get())) { + return false; + } + + exception_port_.swap(exception_port); + return true; +} + +base::mac::ScopedMachSendRight CrashpadClient::GetHandlerMachPort() const { + DCHECK(exception_port_.is_valid()); + + // For the purposes of this method, only return a port set by + // SetHandlerMachPort(). + // + // It would be possible to use task_get_exception_ports() to look up the + // EXC_CRASH task exception port, but that’s probably not what users of this + // interface really want. If CrashpadClient is asked for the handler Mach + // port, it should only return a port that it knows about by virtue of having + // set it. It shouldn’t return any EXC_CRASH task exception port in effect if + // SetHandlerMachPort() was never called, and it shouldn’t return any + // EXC_CRASH task exception port that might be set by other code after + // SetHandlerMachPort() is called. + // + // The caller is accepting its own new ScopedMachSendRight, so increment the + // reference count of the underlying right. + kern_return_t kr = mach_port_mod_refs( + mach_task_self(), exception_port_.get(), MACH_PORT_RIGHT_SEND, 1); + if (kr != KERN_SUCCESS) { + MACH_LOG(ERROR, kr) << "mach_port_mod_refs"; + return base::mac::ScopedMachSendRight(MACH_PORT_NULL); + } + + return base::mac::ScopedMachSendRight(exception_port_.get()); } // static diff --git a/codereview.settings b/codereview.settings index 685c904d..616a3be0 100644 --- a/codereview.settings +++ b/codereview.settings @@ -17,3 +17,5 @@ GERRIT_SQUASH_UPLOADS: True CODE_REVIEW_SERVER: https://canary-chromium-review.googlesource.com/ VIEW_VC: https://chromium.googlesource.com/crashpad/crashpad/+/ PROJECT: crashpad +BUG_LINE_FORMAT: Bug: %s +BUG_PREFIX: crashpad: diff --git a/doc/developing.md b/doc/developing.md index 9f881678..4505d610 100644 --- a/doc/developing.md +++ b/doc/developing.md @@ -278,10 +278,25 @@ committing your changes locally with `git commit`. You can then upload a new patch set with `git cl upload` and let your reviewer know you’ve addressed the feedback. +The most recently uploaded patch set on a review may be tested on a [try +server](https://dev.chromium.org/developers/testing/try-server-usage) by running +`git cl try` or by clicking the “CQ Dry Run” button in Gerrit. These set the +“Commit-Queue: +1” label. This does not mean that the patch will be committed, +but the try server and commit queue share infrastructure and a Gerrit label. The +patch will be tested on try bots in a variety of configurations. Status +information will be available on Gerrit. + ### Landing Changes After code review is complete and “Code-Review: +1” has been received from all -reviewers, project members can commit the patch themselves: +reviewers, the patch can be submitted to Crashpad’s [commit +queue](https://dev.chromium.org/developers/testing/commit-queue) by clicking the +“Submit to CQ” button in Gerrit. This sets the “Commit-Queue: +2” label, which +tests the patch on the try server before landing it. + +Although the commit queue is recommended, if needed, project members can bypass +the commit queue and land patches without testing by using the “Submit” button +in Gerrit or by committing via `git cl land`: ``` $ cd ~/crashpad/crashpad @@ -289,15 +304,6 @@ $ git checkout work_branch $ git cl land ``` -Alternatively, patches can be committed by clicking the “Submit” button in the -Gerrit UI. - -Crashpad does not currently have a [commit -queue](https://dev.chromium.org/developers/testing/commit-queue), so -contributors who are not project members will have to ask a project member to -commit the patch for them. Project members can commit changes on behalf of -external contributors by clicking the “Submit” button in the Gerrit UI. - ### External Contributions Copyright holders must complete the [Individual Contributor License diff --git a/snapshot/mac/mach_o_image_annotations_reader_test.cc b/snapshot/mac/mach_o_image_annotations_reader_test.cc index f4f53494..7a94eb2c 100644 --- a/snapshot/mac/mach_o_image_annotations_reader_test.cc +++ b/snapshot/mac/mach_o_image_annotations_reader_test.cc @@ -117,14 +117,14 @@ class TestMachOImageAnnotationsReader final bool* destroy_complex_request) override { *destroy_complex_request = true; - // In 10.12, dyld fatal errors as tested by test_type_ = kCrashDyld are via - // abort_with_payload(). In 10.12.1, the task port delivered in an exception - // message for this termination type is a corpse, even when the exception is - // EXC_CRASH and not EXC_CORPSE_NOTIFY. The corpse task port (here, |task|) - // is distinct from the process’ original task port (ChildTask()). This is - // filed as https://openradar.appspot.com/29079442. - // - // Instead of comparing task ports, compare PIDs. + if (test_type_ != kCrashDyld) { + // In 10.12.1 and later, the task port will not match ChildTask() in the + // kCrashDyld case, because kCrashDyld uses execl(), which results in a + // new task port being assigned. + EXPECT_EQ(ChildTask(), task); + } + + // The process ID should always compare favorably. pid_t task_pid; kern_return_t kr = pid_for_task(task, &task_pid); EXPECT_EQ(KERN_SUCCESS, kr) << MachErrorMessage(kr, "pid_for_task"); diff --git a/tools/crashpad_http_upload.cc b/tools/crashpad_http_upload.cc new file mode 100644 index 00000000..55ab9550 --- /dev/null +++ b/tools/crashpad_http_upload.cc @@ -0,0 +1,206 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// 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 +#include +#include +#include + +#include +#include + +#include "base/files/file_path.h" +#include "tools/tool_support.h" +#include "util/file/file_writer.h" +#include "util/net/http_body.h" +#include "util/net/http_multipart_builder.h" +#include "util/net/http_transport.h" +#include "util/string/split_string.h" + +namespace crashpad { +namespace { + +void Usage(const base::FilePath& me) { + fprintf(stderr, +"Usage: %" PRFilePath " [OPTION]...\n" +"Send an HTTP POST request.\n" +" -f, --file=KEY=PATH upload the file at PATH for the HTTP KEY parameter\n" +" --no-upload-gzip don't use gzip compression when uploading\n" +" -o, --output=FILE write the response body to FILE instead of stdout\n" +" -s, --string=KEY=VALUE set the HTTP KEY parameter to VALUE\n" +" -u, --url=URL send the request to URL\n" +" --help display this help and exit\n" +" --version output version information and exit\n", + me.value().c_str()); + ToolSupport::UsageTail(me); +} + +int HTTPUploadMain(int argc, char* argv[]) { + const base::FilePath argv0( + ToolSupport::CommandLineArgumentToFilePathStringType(argv[0])); + const base::FilePath me(argv0.BaseName()); + + enum OptionFlags { + // “Short” (single-character) options. + kOptionFile = 'f', + kOptionOutput = 'o', + kOptionString = 's', + kOptionURL = 'u', + + // Long options without short equivalents. + kOptionLastChar = 255, + kOptionNoUploadGzip, + + // Standard options. + kOptionHelp = -2, + kOptionVersion = -3, + }; + + struct { + std::string url; + const char* output; + bool upload_gzip; + } options = {}; + options.upload_gzip = true; + + const option long_options[] = { + {"file", required_argument, nullptr, kOptionFile}, + {"no-upload-gzip", no_argument, nullptr, kOptionNoUploadGzip}, + {"output", required_argument, nullptr, kOptionOutput}, + {"string", required_argument, nullptr, kOptionString}, + {"url", required_argument, nullptr, kOptionURL}, + {"help", no_argument, nullptr, kOptionHelp}, + {"version", no_argument, nullptr, kOptionVersion}, + {nullptr, 0, nullptr, 0}, + }; + + HTTPMultipartBuilder http_multipart_builder; + + int opt; + while ((opt = getopt_long(argc, argv, "f:o:s:u:", long_options, nullptr)) != + -1) { + switch (opt) { + case kOptionFile: { + std::string key; + std::string path; + if (!SplitStringFirst(optarg, '=', &key, &path)) { + ToolSupport::UsageHint(me, "--file requires KEY=STRING"); + return EXIT_FAILURE; + } + base::FilePath file_path( + ToolSupport::CommandLineArgumentToFilePathStringType(path)); + std::string file_name( + ToolSupport::FilePathToCommandLineArgument(file_path.BaseName())); + http_multipart_builder.SetFileAttachment( + key, file_name, file_path, "application/octet-stream"); + break; + } + case kOptionNoUploadGzip: { + options.upload_gzip = false; + break; + } + case kOptionOutput: { + options.output = optarg; + break; + } + case kOptionString: { + std::string key; + std::string value; + if (!SplitStringFirst(optarg, '=', &key, &value)) { + ToolSupport::UsageHint(me, "--string requires KEY=VALUE"); + return EXIT_FAILURE; + } + http_multipart_builder.SetFormData(key, value); + break; + } + case kOptionURL: + options.url = optarg; + break; + case kOptionHelp: + Usage(me); + return EXIT_SUCCESS; + case kOptionVersion: + ToolSupport::Version(me); + return EXIT_SUCCESS; + default: + ToolSupport::UsageHint(me, nullptr); + return EXIT_FAILURE; + } + } + argc -= optind; + argv += optind; + + if (options.url.empty()) { + ToolSupport::UsageHint(me, "--url is required"); + return EXIT_FAILURE; + } + + if (argc) { + ToolSupport::UsageHint(me, nullptr); + return EXIT_FAILURE; + } + + std::unique_ptr file_writer; + if (options.output) { + FileWriter* file_writer_impl = new FileWriter(); + file_writer.reset(file_writer_impl); + base::FilePath output_path( + ToolSupport::CommandLineArgumentToFilePathStringType(options.output)); + if (!file_writer_impl->Open(output_path, + FileWriteMode::kTruncateOrCreate, + FilePermissions::kWorldReadable)) { + return EXIT_FAILURE; + } + } else { + file_writer.reset(new WeakStdioFileWriter(stdout)); + } + + http_multipart_builder.SetGzipEnabled(options.upload_gzip); + + std::unique_ptr http_transport(HTTPTransport::Create()); + http_transport->SetURL(options.url); + + HTTPHeaders content_headers; + http_multipart_builder.PopulateContentHeaders(&content_headers); + for (const auto& content_header : content_headers) { + http_transport->SetHeader(content_header.first, content_header.second); + } + + http_transport->SetBodyStream(http_multipart_builder.GetBodyStream()); + + std::string response_body; + if (!http_transport->ExecuteSynchronously(&response_body)) { + return EXIT_FAILURE; + } + + if (!response_body.empty() && + !file_writer->Write(&response_body[0], response_body.size())) { + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} + +} // namespace +} // namespace crashpad + +#if defined(OS_POSIX) +int main(int argc, char* argv[]) { + return crashpad::HTTPUploadMain(argc, argv); +} +#elif defined(OS_WIN) +int wmain(int argc, wchar_t* argv[]) { + return crashpad::ToolSupport::Wmain(argc, argv, crashpad::HTTPUploadMain); +} +#endif // OS_POSIX diff --git a/tools/crashpad_http_upload.md b/tools/crashpad_http_upload.md new file mode 100644 index 00000000..93b87522 --- /dev/null +++ b/tools/crashpad_http_upload.md @@ -0,0 +1,132 @@ + + +# crashpad_http_upload(1) + +## Name + +crashpad_http_upload—Send an HTTP POST request + +## Synopsis + +**crashpad_http_uplaod** [_OPTION…_] + +## Description + +Performs an HTTP or HTTPS POST, building a `multipart/form-data` request from +key-value pairs and files in the manner of an HTML `
` with a POST action. +Provides the response. + +Programs that use the Crashpad client library directly will not normally use +this tool. This tool is provided for debugging and testing as it isolates +Crashpad’s networking implementation normally used to upload crash reports to +a crash report collection server, making it available for more general use. + +## Options + + * **-f**, **--file**=_KEY_=_PATH_ + + Include _PATH_ in the request as a file upload, in the manner of an HTML + `` element. _KEY_ is used as the field name. + + * **--no-upload-gzip** + + Do not use `gzip` compression. Normally, the entire request body is + compressed into a `gzip` stream and transmitted with `Content-Encoding: + gzip`. This option disables compression, and is intended for use with servers + that don’t accept uploads compressed in this way. + + * **-o**, **--output**=_FILE_ + + The response body will be written to _FILE_ instead of standard output. + + * **-s**, **--string**=_KEY_=_VALUE_ + + Include _KEY_ and _VALUE_ in the request as an ordinary form field, in the + manner of an HTML `` element. _KEY_ is used as the field + name, and _VALUE_ is used as its value. + + * **-u**, **--url**=_URL_ + + Send the request to _URL_. This option is required. + + * **--help** + + Display help and exit. + + * **--version** + + Output version information and exit. + +## Examples + +Uploads a file to an HTTP server running on `localhost`. + +``` +$ crashpad_http-upload --url http://localhost/upload_test \ + --string=when=now --file=what=1040.pdf +Thanks for the upload! +``` + +This example corresponds to the HTML form: + +``` + + + + +
+``` + +## Exit Status + + * **0** + + Success. + + * **1** + + Failure, with a message printed to the standard error stream. HTTP error + statuses such as 404 (Not Found) are included in the definition of failure. + +## See Also + +[crashpad_handler(8)](../handler/crashpad_handler.md) + +## Resources + +Crashpad home page: https://crashpad.chromium.org/. + +Report bugs at https://crashpad.chromium.org/bug/new. + +## Copyright + +Copyright 2017 [The Crashpad +Authors](https://chromium.googlesource.com/crashpad/crashpad/+/master/AUTHORS). + +## License + +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. diff --git a/tools/generate_dump.cc b/tools/generate_dump.cc index 11fe0ca5..b98b1f4f 100644 --- a/tools/generate_dump.cc +++ b/tools/generate_dump.cc @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include #include #include #include @@ -48,12 +47,6 @@ namespace crashpad { namespace { -struct Options { - std::string dump_path; - pid_t pid; - bool suspend; -}; - void Usage(const base::FilePath& me) { fprintf(stderr, "Usage: %" PRFilePath " [OPTION]... PID\n" @@ -85,7 +78,11 @@ int GenerateDumpMain(int argc, char* argv[]) { kOptionVersion = -3, }; - Options options = {}; + struct { + std::string dump_path; + pid_t pid; + bool suspend; + } options = {}; options.suspend = true; const option long_options[] = { diff --git a/tools/tool_support.cc b/tools/tool_support.cc index f6d4f84c..15deead0 100644 --- a/tools/tool_support.cc +++ b/tools/tool_support.cc @@ -98,4 +98,14 @@ base::FilePath::StringType ToolSupport::CommandLineArgumentToFilePathStringType( #endif // OS_POSIX } +// static +std::string ToolSupport::FilePathToCommandLineArgument( + const base::FilePath& file_path) { +#if defined(OS_POSIX) + return file_path.value(); +#elif defined(OS_WIN) + return base::UTF16ToUTF8(file_path.value()); +#endif // OS_POSIX +} + } // namespace crashpad diff --git a/tools/tool_support.h b/tools/tool_support.h index e3a50614..48f412f3 100644 --- a/tools/tool_support.h +++ b/tools/tool_support.h @@ -72,9 +72,18 @@ class ToolSupport { //! `char*`. This undoes that transformation. //! //! \sa Wmain() + //! \sa FilePathToCommandLineArgument() static base::FilePath::StringType CommandLineArgumentToFilePathStringType( const base::StringPiece& arg); + //! \brief Converts a base::FilePath to a command line argument. + //! + //! On POSIX, this is a no-op. On Windows, this undoes the transformation done + //! by CommandLineArgumentToFilePathStringType() in the same manner as + //! Wmain(). + static std::string FilePathToCommandLineArgument( + const base::FilePath& file_path); + private: DISALLOW_IMPLICIT_CONSTRUCTORS(ToolSupport); }; diff --git a/tools/tools.gyp b/tools/tools.gyp index 48763005..3639b531 100644 --- a/tools/tools.gyp +++ b/tools/tools.gyp @@ -48,6 +48,22 @@ 'crashpad_database_util.cc', ], }, + { + 'target_name': 'crashpad_http_upload', + 'type': 'executable', + 'dependencies': [ + 'crashpad_tool_support', + '../compat/compat.gyp:crashpad_compat', + '../third_party/mini_chromium/mini_chromium.gyp:base', + '../util/util.gyp:crashpad_util', + ], + 'include_dirs': [ + '..', + ], + 'sources': [ + 'crashpad_http_upload.cc', + ], + }, { 'target_name': 'generate_dump', 'type': 'executable', @@ -77,7 +93,7 @@ }, }], ], - } + }, ], 'conditions': [ ['OS=="mac"', { diff --git a/util/file/file_reader.cc b/util/file/file_reader.cc index 14d06c74..4f5ae773 100644 --- a/util/file/file_reader.cc +++ b/util/file/file_reader.cc @@ -109,10 +109,14 @@ FileOperationResult WeakStdioFileReader::Read(void* data, size_t size) { DCHECK(file_); size_t rv = fread(data, 1, size, file_); - if (rv < size && ferror(file_)) { + if (rv != size && ferror(file_)) { STDIO_PLOG(ERROR) << "fread"; return -1; } + if (rv > size) { + LOG(ERROR) << "fread: expected " << size << ", observed " << rv; + return -1; + } return rv; } diff --git a/util/file/file_writer.cc b/util/file/file_writer.cc index 94f41f14..d22a1a3b 100644 --- a/util/file/file_writer.cc +++ b/util/file/file_writer.cc @@ -56,6 +56,11 @@ bool WeakFileHandleFileWriter::Write(const void* data, size_t size) { bool WeakFileHandleFileWriter::WriteIoVec(std::vector* iovecs) { DCHECK_NE(file_handle_, kInvalidFileHandle); + if (iovecs->empty()) { + LOG(ERROR) << "WriteIoVec(): no iovecs"; + return false; + } + #if defined(OS_POSIX) ssize_t size = 0; @@ -187,4 +192,66 @@ FileOffset FileWriter::Seek(FileOffset offset, int whence) { return weak_file_handle_file_writer_.Seek(offset, whence); } +WeakStdioFileWriter::WeakStdioFileWriter(FILE* file) + : file_(file) { +} + +WeakStdioFileWriter::~WeakStdioFileWriter() { +} + +bool WeakStdioFileWriter::Write(const void* data, size_t size) { + DCHECK(file_); + + size_t rv = fwrite(data, 1, size, file_); + if (rv != size) { + if (ferror(file_)) { + STDIO_PLOG(ERROR) << "fwrite"; + } else { + LOG(ERROR) << "fwrite: expected " << size << ", observed " << rv; + } + return false; + } + + return true; +} + +bool WeakStdioFileWriter::WriteIoVec(std::vector* iovecs) { + DCHECK(file_); + + if (iovecs->empty()) { + LOG(ERROR) << "WriteIoVec(): no iovecs"; + return false; + } + + for (const WritableIoVec& iov : *iovecs) { + if (!Write(iov.iov_base, iov.iov_len)) { + return false; + } + } + +#ifndef NDEBUG + // The interface says that |iovecs| is not sacred, so scramble it to make sure + // that nobody depends on it. + memset(&(*iovecs)[0], 0xa5, sizeof((*iovecs)[0]) * iovecs->size()); +#endif + + return true; +} + +FileOffset WeakStdioFileWriter::Seek(FileOffset offset, int whence) { + DCHECK(file_); + if (fseeko(file_, offset, whence) == -1) { + STDIO_PLOG(ERROR) << "fseeko"; + return -1; + } + + FileOffset new_offset = ftello(file_); + if (new_offset == -1) { + STDIO_PLOG(ERROR) << "ftello"; + return -1; + } + + return new_offset; +} + } // namespace crashpad diff --git a/util/file/file_writer.h b/util/file/file_writer.h index ed261ec5..007e5f3a 100644 --- a/util/file/file_writer.h +++ b/util/file/file_writer.h @@ -167,6 +167,41 @@ class FileWriter : public FileWriterInterface { DISALLOW_COPY_AND_ASSIGN(FileWriter); }; +//! \brief A file writer backed by a standard input/output `FILE*`. +//! +//! This class accepts an already-open `FILE*`. It is not responsible for +//! opening or closing this `FILE*`. Users of this class must ensure that the +//! `FILE*` is closed appropriately elsewhere. Objects of this class may be used +//! to write to `FILE*` objects not associated with filesystem-based files, +//! although special attention should be paid to the Seek() method, which may +//! not function on `FILE*` objects that do not refer to disk-based files. +//! +//! This class is expected to be used when other code is responsible for +//! opening `FILE*` objects and already provides `FILE*` objects. A good use +//! would be a WeakStdioFileWriter for `stdout`. +class WeakStdioFileWriter : public FileWriterInterface { + public: + explicit WeakStdioFileWriter(FILE* file); + ~WeakStdioFileWriter() override; + + // FileWriterInterface: + bool Write(const void* data, size_t size) override; + bool WriteIoVec(std::vector* iovecs) override; + + // FileSeekerInterface: + + //! \copydoc FileWriterInterface::Seek() + //! + //! \note This method is only guaranteed to function on `FILE*` objects + //! referring to disk-based files. + FileOffset Seek(FileOffset offset, int whence) override; + + private: + FILE* file_; // weak + + DISALLOW_COPY_AND_ASSIGN(WeakStdioFileWriter); +}; + } // namespace crashpad #endif // CRASHPAD_UTIL_FILE_FILE_WRITER_H_ diff --git a/util/net/http_transport_libcurl.cc b/util/net/http_transport_libcurl.cc new file mode 100644 index 00000000..96ea7502 --- /dev/null +++ b/util/net/http_transport_libcurl.cc @@ -0,0 +1,366 @@ +// Copyright 2017 The Crashpad Authors. All rights reserved. +// +// 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 "util/net/http_transport.h" + +#include +#include +#include + +#include +#include + +#include "base/logging.h" +#include "base/numerics/safe_math.h" +#include "base/scoped_generic.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "build/build_config.h" +#include "package.h" +#include "util/net/http_body.h" +#include "util/numeric/safe_assignment.h" + +namespace crashpad { + +namespace { + +std::string UserAgent() { + std::string user_agent = base::StringPrintf( + "%s/%s %s", PACKAGE_NAME, PACKAGE_VERSION, curl_version()); + + utsname os; + if (uname(&os) != 0) { + PLOG(WARNING) << "uname"; + } else { + // Match the architecture name that would be used by the kernel, so that the + // strcmp() below can omit the kernel’s architecture name if it’s the same + // as the user process’ architecture. On Linux, these names are normally + // defined in each architecture’s Makefile as UTS_MACHINE, but can be + // overridden in architecture-specific configuration as COMPAT_UTS_MACHINE. + // See linux-4.4.52/arch/*/Makefile and + // linux-4.4.52/arch/*/include/asm/compat.h. In turn, on some systems, these + // names are further overridden or refined in early kernel startup code by + // modifying the string returned by linux-4.4.52/include/linux/utsname.h + // init_utsname() as noted. +#if defined(ARCH_CPU_X86) + // linux-4.4.52/arch/x86/kernel/cpu/bugs.c check_bugs() sets the first digit + // to 4, 5, or 6, but no higher. Assume 6. + const char arch[] = "i686"; +#elif defined(ARCH_CPU_X86_64) + const char arch[] = "x86_64"; +#elif defined(ARCH_CPU_ARMEL) + // linux-4.4.52/arch/arm/kernel/setup.c setup_processor() bases the string + // on the ARM processor name and a character identifying little- or + // big-endian. The processor name comes from a definition in + // arch/arm/mm/proc-*.S. Assume armv7, little-endian. + const char arch[] = "armv7l"; +#elif defined(ARCH_CPU_ARM64) + // ARM64 uses aarch64 or aarch64_be as directed by ELF_PLATFORM. See + // linux-4.4.52/arch/arm64/kernel/setup.c setup_arch(). Assume + // little-endian. + const char arch[] = "aarch64"; +#elif defined(ARCH_CPU_MIPSEL) + const char arch[] = "mips"; +#elif defined(ARCH_CPU_MIPS64EL) + const char arch[] = "mips64"; +#else +#error Port +#endif + + user_agent.append( + base::StringPrintf(" %s/%s (%s", os.sysname, os.release, arch)); + if (strcmp(arch, os.machine) != 0) { + user_agent.append(base::StringPrintf("; %s", os.machine)); + } + user_agent.append(1, ')'); + } + + return user_agent; +} + +std::string CurlErrorMessage(CURLcode curl_err, const std::string& base) { + return base::StringPrintf( + "%s: %s (%d)", base.c_str(), curl_easy_strerror(curl_err), curl_err); +} + +struct ScopedCURLTraits { + static CURL* InvalidValue() { return nullptr; } + static void Free(CURL* curl) { + if (curl) { + curl_easy_cleanup(curl); + } + } +}; +using ScopedCURL = base::ScopedGeneric; + +class CurlSList { + public: + CurlSList() : list_(nullptr) {} + ~CurlSList() { + if (list_) { + curl_slist_free_all(list_); + } + } + + curl_slist* get() const { return list_; } + + bool Append(const char* data) { + curl_slist* list = curl_slist_append(list_, data); + if (!list_) { + list_ = list; + } + return list != nullptr; + } + + private: + curl_slist* list_; + + DISALLOW_COPY_AND_ASSIGN(CurlSList); +}; + +class ScopedClearString { + public: + explicit ScopedClearString(std::string* string) : string_(string) {} + + ~ScopedClearString() { + if (string_) { + string_->clear(); + } + } + + void Disarm() { string_ = nullptr; } + + private: + std::string* string_; + + DISALLOW_COPY_AND_ASSIGN(ScopedClearString); +}; + +class HTTPTransportLibcurl final : public HTTPTransport { + public: + HTTPTransportLibcurl(); + ~HTTPTransportLibcurl() override; + + // HTTPTransport: + bool ExecuteSynchronously(std::string* response_body) override; + + private: + static size_t ReadRequestBody(char* buffer, + size_t size, + size_t nitems, + void* userdata); + static size_t WriteResponseBody(char* buffer, + size_t size, + size_t nitems, + void* userdata); + + DISALLOW_COPY_AND_ASSIGN(HTTPTransportLibcurl); +}; + +HTTPTransportLibcurl::HTTPTransportLibcurl() : HTTPTransport() {} + +HTTPTransportLibcurl::~HTTPTransportLibcurl() {} + +bool HTTPTransportLibcurl::ExecuteSynchronously(std::string* response_body) { + DCHECK(body_stream()); + + response_body->clear(); + + // curl_easy_init() will do this on the first call if it hasn’t been done yet, + // but not in a thread-safe way as is done here. + static CURLcode curl_global_init_err = []() { + return curl_global_init(CURL_GLOBAL_DEFAULT); + }(); + if (curl_global_init_err != CURLE_OK) { + LOG(ERROR) << CurlErrorMessage(curl_global_init_err, "curl_global_init"); + return false; + } + + CurlSList curl_headers; + ScopedCURL curl(curl_easy_init()); + if (!curl.get()) { + LOG(ERROR) << "curl_easy_init"; + return false; + } + +// These macros wrap the repetitive “try something, log an error and return +// false on failure” pattern. Macros are convenient because the log messages +// will point to the correct line number, which can help pinpoint a problem when +// there are as many calls to these functions as there are here. +#define TRY_CURL_EASY_SETOPT(curl, option, parameter) \ + do { \ + CURLcode curl_err = curl_easy_setopt((curl), (option), (parameter)); \ + if (curl_err != CURLE_OK) { \ + LOG(ERROR) << CurlErrorMessage(curl_err, "curl_easy_setopt"); \ + return false; \ + } \ + } while (false) +#define TRY_CURL_SLIST_APPEND(slist, data) \ + do { \ + if (!(slist).Append(data)) { \ + LOG(ERROR) << "curl_slist_append"; \ + return false; \ + } \ + } while (false) + + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_USERAGENT, UserAgent().c_str()); + + // Accept and automatically decode any encoding that libcurl understands. + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_ACCEPT_ENCODING, ""); + + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_URL, url().c_str()); + + const int kMillisecondsPerSecond = 1E3; + TRY_CURL_EASY_SETOPT(curl.get(), + CURLOPT_TIMEOUT_MS, + static_cast(timeout() * kMillisecondsPerSecond)); + + // If the request body size is known ahead of time, a Content-Length header + // field will be present. Store that to use as CURLOPT_POSTFIELDSIZE_LARGE, + // which will both set the Content-Length field in the request header and + // inform libcurl of the request body size. Otherwise, use Transfer-Encoding: + // chunked, which does not require advance knowledge of the request body size. + bool chunked = true; + size_t content_length; + for (const auto& pair : headers()) { + if (pair.first == kContentLength) { + chunked = !base::StringToSizeT(pair.second, &content_length); + DCHECK(!chunked); + } else { + TRY_CURL_SLIST_APPEND(curl_headers, + (pair.first + ": " + pair.second).c_str()); + } + } + + if (method() == "POST") { + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_POST, 1l); + + // By default when sending a POST request, libcurl includes an “Expect: + // 100-continue” header field. Althogh this header is specified in HTTP/1.1 + // (RFC 2616 §8.2.3, RFC 7231 §5.1.1), even collection servers that claim to + // speak HTTP/1.1 may not respond to it. When sending this header field, + // libcurl will wait for one second for the server to respond with a “100 + // Continue” status before continuing to transmit the request body. This + // delay is avoided by telling libcurl not to send this header field at all. + // The drawback is that certain HTTP error statuses may not be received + // until after substantial amounts of data have been sent to the server. + TRY_CURL_SLIST_APPEND(curl_headers, "Expect:"); + + if (chunked) { + TRY_CURL_SLIST_APPEND(curl_headers, "Transfer-Encoding: chunked"); + } else { + curl_off_t content_length_curl; + if (!AssignIfInRange(&content_length_curl, content_length)) { + LOG(ERROR) << base::StringPrintf("Content-Length %zu too large", + content_length); + return false; + } + TRY_CURL_EASY_SETOPT( + curl.get(), CURLOPT_POSTFIELDSIZE_LARGE, content_length_curl); + } + } else if (method() != "GET") { + // Untested. + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_CUSTOMREQUEST, method().c_str()); + } + + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_HTTPHEADER, curl_headers.get()); + + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_READFUNCTION, ReadRequestBody); + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_READDATA, this); + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_WRITEFUNCTION, WriteResponseBody); + TRY_CURL_EASY_SETOPT(curl.get(), CURLOPT_WRITEDATA, response_body); + +#undef TRY_CURL_EASY_SETOPT +#undef TRY_CURL_SLIST_APPEND + + // If a partial response body is received and then a failure occurs, ensure + // that response_body is cleared. + ScopedClearString clear_response_body(response_body); + + // Do it. + CURLcode curl_err = curl_easy_perform(curl.get()); + if (curl_err != CURLE_OK) { + LOG(ERROR) << CurlErrorMessage(curl_err, "curl_easy_perform"); + return false; + } + + long status; + curl_err = curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &status); + if (curl_err != CURLE_OK) { + LOG(ERROR) << CurlErrorMessage(curl_err, "curl_easy_getinfo"); + return false; + } + + if (status != 200) { + LOG(ERROR) << base::StringPrintf("HTTP status %ld", status); + return false; + } + + // The response body is complete. Don’t clear it. + clear_response_body.Disarm(); + + return true; +} + +// static +size_t HTTPTransportLibcurl::ReadRequestBody(char* buffer, + size_t size, + size_t nitems, + void* userdata) { + HTTPTransportLibcurl* self = + reinterpret_cast(userdata); + + // This libcurl callback mimics the silly stdio-style fread() interface: size + // and nitems have been separated and must be multiplied. + base::CheckedNumeric checked_len = base::CheckMul(size, nitems); + size_t len = checked_len.ValueOrDefault(std::numeric_limits::max()); + + // Limit the read to what can be expressed in a FileOperationResult. + len = std::min( + len, + static_cast(std::numeric_limits::max())); + + FileOperationResult bytes_read = self->body_stream()->GetBytesBuffer( + reinterpret_cast(buffer), len); + if (bytes_read < 0) { + return CURL_READFUNC_ABORT; + } + + return bytes_read; +} + +// static +size_t HTTPTransportLibcurl::WriteResponseBody(char* buffer, + size_t size, + size_t nitems, + void* userdata) { + std::string* response_body = reinterpret_cast(userdata); + + // This libcurl callback mimics the silly stdio-style fread() interface: size + // and nitems have been separated and must be multiplied. + base::CheckedNumeric checked_len = base::CheckMul(size, nitems); + size_t len = checked_len.ValueOrDefault(std::numeric_limits::max()); + + response_body->append(buffer, len); + return len; +} + +} // namespace + +// static +std::unique_ptr HTTPTransport::Create() { + return std::unique_ptr(new HTTPTransportLibcurl()); +} + +} // namespace crashpad diff --git a/util/util.gyp b/util/util.gyp index 9e57b442..f1f004c7 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -119,6 +119,7 @@ 'net/http_multipart_builder.h', 'net/http_transport.cc', 'net/http_transport.h', + 'net/http_transport_libcurl.cc', 'net/http_transport_mac.mm', 'net/http_transport_win.cc', 'numeric/checked_address_range.cc', @@ -300,6 +301,17 @@ 'win/capture_context.asm', ], }], + ['OS=="linux"', { + 'link_settings': { + 'libraries': [ + '-lcurl', + ], + }, + }, { # else: OS!="linux" + 'sources!': [ + 'net/http_transport_libcurl.cc', + ], + }], ], }, ],