| // 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 <stdint.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| |
| #include <memory> |
| #include <utility> |
| #include <vector> |
| |
| #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<HTTPBodyStream> 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<std::string> 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<HTTPTransport> 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<HTTPBodyStream> 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<std::string> {}; |
| |
| 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<HTTPBodyStream> 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<HTTPBodyStream> 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<std::string>& info) { |
| return info.param; |
| }); |
| #else |
| INSTANTIATE_TEST_SUITE_P(HTTPTransport, |
| HTTPTransport, |
| testing::Values("http"), |
| [](const testing::TestParamInfo<std::string>& info) { |
| return info.param; |
| }); |
| #endif |
| |
| } // namespace |
| } // namespace test |
| } // namespace crashpad |