// Copyright 2014 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 #include #include "base/files/file_path.h" #include "base/format_macros.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" #include "build/build_config.h" #include "gtest/gtest.h" #include "test/multiprocess_exec.h" #include "test/test_paths.h" #include "util/file/file_io.h" #include "util/misc/random_string.h" #include "util/net/http_body.h" #include "util/net/http_headers.h" #include "util/net/http_multipart_builder.h" namespace crashpad { namespace test { namespace { #if defined(OS_WIN) std::string ToUTF8IfWin(const std::wstring& x) { return base::WideToUTF8(x); } #else std::string ToUTF8IfWin(const std::string& x) { return x; } #endif class HTTPTransportTestFixture : public MultiprocessExec { public: using RequestValidator = void (*)(HTTPTransportTestFixture*, const std::string&); HTTPTransportTestFixture(const std::string& scheme, const HTTPHeaders& headers, std::unique_ptr body_stream, uint16_t http_response_code, RequestValidator request_validator) : MultiprocessExec(), headers_(headers), body_stream_(std::move(body_stream)), response_code_(http_response_code), request_validator_(request_validator), cert_(), scheme_and_host_() { base::FilePath server_path = TestPaths::Executable().DirName().Append( FILE_PATH_LITERAL("http_transport_test_server") #if defined(OS_WIN) FILE_PATH_LITERAL(".exe") #endif ); if (scheme == "http") { scheme_and_host_ = "http://localhost"; SetChildCommand(server_path, nullptr); } else { std::vector args; cert_ = TestPaths::TestDataRoot().Append( FILE_PATH_LITERAL("util/net/testdata/crashpad_util_test_cert.pem")); args.push_back(ToUTF8IfWin(cert_.value())); args.emplace_back( ToUTF8IfWin(TestPaths::TestDataRoot() .Append(FILE_PATH_LITERAL( "util/net/testdata/crashpad_util_test_key.pem")) .value())); SetChildCommand(server_path, &args); scheme_and_host_ = "https://localhost"; } } const HTTPHeaders& headers() { return headers_; } private: void MultiprocessParent() override { // Use Logging*File() instead of Checked*File() so that the test can fail // gracefully with a Google Test assertion if the child does not execute // properly. // The child will write the HTTP server port number as a packed unsigned // short to stdout. uint16_t port; ASSERT_TRUE(LoggingReadFileExactly(ReadPipeHandle(), &port, sizeof(port))); // Then the parent will tell the web server what response code to send // for the HTTP request. ASSERT_TRUE(LoggingWriteFile( WritePipeHandle(), &response_code_, sizeof(response_code_))); // The parent will also tell the web server what response body to send back. // The web server will only send the response body if the response code is // 200. const std::string random_string = RandomString(); ASSERT_TRUE(LoggingWriteFile( WritePipeHandle(), random_string.c_str(), random_string.size())); // Now execute the HTTP request. std::unique_ptr transport(HTTPTransport::Create()); transport->SetMethod("POST"); if (!cert_.empty()) { transport->SetRootCACertificatePath(cert_); } transport->SetURL( base::StringPrintf("%s:%d/upload", scheme_and_host_.c_str(), port)); for (const auto& pair : headers_) { transport->SetHeader(pair.first, pair.second); } transport->SetBodyStream(std::move(body_stream_)); std::string response_body; bool success = transport->ExecuteSynchronously(&response_body); if (response_code_ >= 200 && response_code_ <= 203) { EXPECT_TRUE(success); std::string expect_response_body = random_string + "\r\n"; EXPECT_EQ(response_body, expect_response_body); } else { EXPECT_FALSE(success); EXPECT_TRUE(response_body.empty()); } // Read until the child's stdout closes. std::string request; char buf[32]; FileOperationResult bytes_read; while ((bytes_read = ReadFile(ReadPipeHandle(), buf, sizeof(buf))) != 0) { ASSERT_GE(bytes_read, 0); request.append(buf, bytes_read); } if (request_validator_) request_validator_(this, request); } HTTPHeaders headers_; std::unique_ptr body_stream_; uint16_t response_code_; RequestValidator request_validator_; base::FilePath cert_; std::string scheme_and_host_; }; constexpr char kMultipartFormData[] = "multipart/form-data"; void GetHeaderField(const std::string& request, const std::string& header, std::string* value) { size_t index = request.find(header); ASSERT_NE(index, std::string::npos); // Since the header is never the first line of the request, it should always // be preceded by a CRLF. EXPECT_EQ(request[index - 1], '\n'); EXPECT_EQ(request[index - 2], '\r'); index += header.length(); EXPECT_EQ(request[index++], ':'); // Per RFC 7230 §3.2, there can be one or more spaces or horizontal tabs. // For testing purposes, just assume one space. EXPECT_EQ(request[index++], ' '); size_t header_end = request.find('\r', index); ASSERT_NE(header_end, std::string::npos); *value = request.substr(index, header_end - index); } void GetMultipartBoundary(const std::string& request, std::string* multipart_boundary) { std::string content_type; GetHeaderField(request, kContentType, &content_type); ASSERT_GE(content_type.length(), strlen(kMultipartFormData)); size_t index = strlen(kMultipartFormData); EXPECT_EQ(content_type.substr(0, index), kMultipartFormData); EXPECT_EQ(content_type[index++], ';'); size_t boundary_begin = content_type.find('=', index); ASSERT_NE(boundary_begin, std::string::npos); EXPECT_EQ(content_type[boundary_begin++], '='); if (multipart_boundary) { *multipart_boundary = content_type.substr(boundary_begin); } } constexpr char kBoundaryEq[] = "boundary="; void ValidFormData(HTTPTransportTestFixture* fixture, const std::string& request) { std::string actual_boundary; GetMultipartBoundary(request, &actual_boundary); const auto& content_type = fixture->headers().find(kContentType); ASSERT_NE(content_type, fixture->headers().end()); size_t boundary = content_type->second.find(kBoundaryEq); ASSERT_NE(boundary, std::string::npos); std::string expected_boundary = content_type->second.substr(boundary + strlen(kBoundaryEq)); EXPECT_EQ(actual_boundary, expected_boundary); size_t body_start = request.find("\r\n\r\n"); ASSERT_NE(body_start, std::string::npos); body_start += 4; std::string expected = "--" + expected_boundary + "\r\n"; expected += "Content-Disposition: form-data; name=\"key1\"\r\n\r\n"; expected += "test\r\n"; ASSERT_LT(body_start + expected.length(), request.length()); EXPECT_EQ(request.substr(body_start, expected.length()), expected); body_start += expected.length(); expected = "--" + expected_boundary + "\r\n"; expected += "Content-Disposition: form-data; name=\"key2\"\r\n\r\n"; expected += "--abcdefg123\r\n"; expected += "--" + expected_boundary + "--\r\n"; ASSERT_EQ(request.length(), body_start + expected.length()); EXPECT_EQ(request.substr(body_start), expected); } class HTTPTransport : public testing::TestWithParam {}; TEST_P(HTTPTransport, ValidFormData) { HTTPMultipartBuilder builder; builder.SetFormData("key1", "test"); builder.SetFormData("key2", "--abcdefg123"); HTTPHeaders headers; builder.PopulateContentHeaders(&headers); HTTPTransportTestFixture test( GetParam(), headers, builder.GetBodyStream(), 200, &ValidFormData); test.Run(); } TEST_P(HTTPTransport, ValidFormData_Gzip) { HTTPMultipartBuilder builder; builder.SetGzipEnabled(true); builder.SetFormData("key1", "test"); builder.SetFormData("key2", "--abcdefg123"); HTTPHeaders headers; builder.PopulateContentHeaders(&headers); HTTPTransportTestFixture test( GetParam(), headers, builder.GetBodyStream(), 200, &ValidFormData); test.Run(); } constexpr char kTextPlain[] = "text/plain"; void ErrorResponse(HTTPTransportTestFixture* fixture, const std::string& request) { std::string content_type; GetHeaderField(request, kContentType, &content_type); EXPECT_EQ(content_type, kTextPlain); } TEST_P(HTTPTransport, ErrorResponse) { HTTPMultipartBuilder builder; HTTPHeaders headers; headers[kContentType] = kTextPlain; HTTPTransportTestFixture test( GetParam(), headers, builder.GetBodyStream(), 404, &ErrorResponse); test.Run(); } constexpr char kTextBody[] = "hello world"; void UnchunkedPlainText(HTTPTransportTestFixture* fixture, const std::string& request) { std::string header_value; GetHeaderField(request, kContentType, &header_value); EXPECT_EQ(header_value, kTextPlain); GetHeaderField(request, kContentLength, &header_value); const auto& content_length = fixture->headers().find(kContentLength); ASSERT_NE(content_length, fixture->headers().end()); EXPECT_EQ(header_value, content_length->second); size_t body_start = request.rfind("\r\n"); ASSERT_NE(body_start, std::string::npos); EXPECT_EQ(request.substr(body_start + 2), kTextBody); } TEST_P(HTTPTransport, UnchunkedPlainText) { std::unique_ptr body_stream( new StringHTTPBodyStream(kTextBody)); HTTPHeaders headers; headers[kContentType] = kTextPlain; headers[kContentLength] = base::StringPrintf("%" PRIuS, strlen(kTextBody)); HTTPTransportTestFixture test( GetParam(), headers, std::move(body_stream), 200, &UnchunkedPlainText); test.Run(); } void RunUpload33k(const std::string& scheme, bool has_content_length) { // On macOS, NSMutableURLRequest winds up calling into a CFReadStream’s Read() // callback with a 32kB buffer. Make sure that it’s able to get everything // when enough is available to fill this buffer, requiring more than one // Read(). std::string request_string(33 * 1024, 'a'); std::unique_ptr body_stream( new StringHTTPBodyStream(request_string)); HTTPHeaders headers; headers[kContentType] = "application/octet-stream"; if (has_content_length) { headers[kContentLength] = base::StringPrintf("%" PRIuS, request_string.size()); } HTTPTransportTestFixture test( scheme, headers, std::move(body_stream), 200, [](HTTPTransportTestFixture* fixture, const std::string& request) { size_t body_start = request.rfind("\r\n"); EXPECT_EQ(request.size() - body_start, 33 * 1024u + 2); }); test.Run(); } TEST_P(HTTPTransport, Upload33k) { RunUpload33k(GetParam(), true); } TEST_P(HTTPTransport, Upload33k_LengthUnknown) { // The same as Upload33k, but without declaring Content-Length ahead of time. RunUpload33k(GetParam(), false); } // This should be on for Fuchsia, but DX-382. Debug and re-enabled. #if defined(CRASHPAD_USE_BORINGSSL) && !defined(OS_FUCHSIA) // The test server requires BoringSSL or OpenSSL, so https in tests can only be // enabled where that's readily available. Additionally on Linux, the bots fail // lacking libcrypto.so.1.1, so disabled there for now. On Mac, they could also // likely be enabled relatively easily, if HTTPTransportMac learned to respect // the user-supplied cert. // // If tests with boringssl are failing because of expired certificates, try // re-running generate_test_server_key.py. INSTANTIATE_TEST_SUITE_P(HTTPTransport, HTTPTransport, testing::Values("http", "https"), [](const testing::TestParamInfo& info) { return info.param; }); #else INSTANTIATE_TEST_SUITE_P(HTTPTransport, HTTPTransport, testing::Values("http"), [](const testing::TestParamInfo& info) { return info.param; }); #endif } // namespace } // namespace test } // namespace crashpad