From d88711adfa1c930cb709b7b7f5d52c8534a72c00 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Fri, 31 Oct 2014 12:17:32 -0400 Subject: [PATCH] Add HTTPTransport, a Mac implementation, and an end-to-end test. BUG=https://crbug.com/415544 R=mark@chromium.org Review URL: https://codereview.chromium.org/692963002 --- util/net/http_headers.cc | 23 +++ util/net/http_headers.h | 34 ++++ util/net/http_multipart_builder.cc | 13 +- util/net/http_multipart_builder.h | 5 +- util/net/http_transport.cc | 53 ++++++ util/net/http_transport.h | 102 +++++++++++ util/net/http_transport_mac.mm | 192 ++++++++++++++++++++ util/net/http_transport_test.cc | 242 +++++++++++++++++++++++++ util/net/http_transport_test_server.py | 137 ++++++++++++++ util/util.gyp | 6 + 10 files changed, 802 insertions(+), 5 deletions(-) create mode 100644 util/net/http_headers.cc create mode 100644 util/net/http_headers.h create mode 100644 util/net/http_transport.cc create mode 100644 util/net/http_transport.h create mode 100644 util/net/http_transport_mac.mm create mode 100644 util/net/http_transport_test.cc create mode 100755 util/net/http_transport_test_server.py diff --git a/util/net/http_headers.cc b/util/net/http_headers.cc new file mode 100644 index 00000000..09d61b3e --- /dev/null +++ b/util/net/http_headers.cc @@ -0,0 +1,23 @@ +// 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_headers.h" + +namespace crashpad { + +const char kContentType[] = "Content-Type"; + +const char kContentLength[] = "Content-Length"; + +} // namespace crashpad diff --git a/util/net/http_headers.h b/util/net/http_headers.h new file mode 100644 index 00000000..3633cb2d --- /dev/null +++ b/util/net/http_headers.h @@ -0,0 +1,34 @@ +// 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. + +#ifndef CRASHPAD_UTIL_NET_HTTP_HEADERS_H_ +#define CRASHPAD_UTIL_NET_HTTP_HEADERS_H_ + +#include +#include + +namespace crashpad { + +//! \brief A map of HTTP header fields to their values. +using HTTPHeaders = std::map; + +//! \brief The header name `"Content-Type"`. +extern const char kContentType[]; + +//! \brief The header name `"Content-Length"`. +extern const char kContentLength[]; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_NET_HTTP_HEADERS_H_ diff --git a/util/net/http_multipart_builder.cc b/util/net/http_multipart_builder.cc index 07dd837f..3f66658c 100644 --- a/util/net/http_multipart_builder.cc +++ b/util/net/http_multipart_builder.cc @@ -14,6 +14,7 @@ #include "util/net/http_multipart_builder.h" +#include #include #include "base/logging.h" @@ -152,7 +153,7 @@ scoped_ptr HTTPMultipartBuilder::GetBodyStream() { std::vector streams; for (const auto& pair : form_data_) { - std::string field = GetFormDataBoundary(boundary(), pair.first); + std::string field = GetFormDataBoundary(boundary_, pair.first); field += kBoundaryCRLF; field += pair.second; field += kCRLF; @@ -161,7 +162,7 @@ scoped_ptr HTTPMultipartBuilder::GetBodyStream() { for (const auto& pair : file_attachments_) { const FileAttachment& attachment = pair.second; - std::string header = GetFormDataBoundary(boundary(), pair.first); + std::string header = GetFormDataBoundary(boundary_, pair.first); header += base::StringPrintf("; filename=\"%s\"%s", attachment.filename.c_str(), kCRLF); header += base::StringPrintf("Content-Type: %s%s", @@ -173,11 +174,17 @@ scoped_ptr HTTPMultipartBuilder::GetBodyStream() { } streams.push_back( - new StringHTTPBodyStream("--" + boundary() + "--" + kCRLF)); + new StringHTTPBodyStream("--" + boundary_ + "--" + kCRLF)); return scoped_ptr(new CompositeHTTPBodyStream(streams)); } +HTTPHeaders::value_type HTTPMultipartBuilder::GetContentType() const { + std::string content_type = + base::StringPrintf("multipart/form-data; boundary=%s", boundary_.c_str()); + return std::make_pair(kContentType, content_type); +} + void HTTPMultipartBuilder::EraseKey(const std::string& key) { auto data_it = form_data_.find(key); if (data_it != form_data_.end()) diff --git a/util/net/http_multipart_builder.h b/util/net/http_multipart_builder.h index d5588209..c9e03ce7 100644 --- a/util/net/http_multipart_builder.h +++ b/util/net/http_multipart_builder.h @@ -21,6 +21,7 @@ #include "base/basictypes.h" #include "base/files/file_path.h" #include "base/memory/scoped_ptr.h" +#include "util/net/http_headers.h" namespace crashpad { @@ -63,8 +64,8 @@ class HTTPMultipartBuilder { //! \return A caller-owned HTTPBodyStream object. scoped_ptr GetBodyStream(); - //! \brief Gets the boundary that will be used in GetBodyStream(). - std::string boundary() const { return boundary_; } + //! \brief Gets the header pair for `"Content-Type"`. + HTTPHeaders::value_type GetContentType() const; private: struct FileAttachment { diff --git a/util/net/http_transport.cc b/util/net/http_transport.cc new file mode 100644 index 00000000..f11ee95c --- /dev/null +++ b/util/net/http_transport.cc @@ -0,0 +1,53 @@ +// 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 "util/net/http_body.h" + +namespace crashpad { + +HTTPTransport::HTTPTransport() + : url_(), + method_("POST"), + headers_(), + body_stream_(), + timeout_(15.0) { +} + +HTTPTransport::~HTTPTransport() { +} + +void HTTPTransport::SetURL(const std::string& url) { + url_ = url; +} + +void HTTPTransport::SetMethod(const std::string& method) { + method_ = method; +} + +void HTTPTransport::SetHeader(const std::string& header, + const std::string& value) { + headers_[header] = value; +} + +void HTTPTransport::SetBodyStream(scoped_ptr stream) { + body_stream_ = stream.Pass(); +} + +void HTTPTransport::SetTimeout(double timeout) { + timeout_ = timeout; +} + +} // namespace crashpad diff --git a/util/net/http_transport.h b/util/net/http_transport.h new file mode 100644 index 00000000..55482153 --- /dev/null +++ b/util/net/http_transport.h @@ -0,0 +1,102 @@ +// 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. + +#ifndef CRASHPAD_UTIL_NET_HTTP_TRANSPORT_H_ +#define CRASHPAD_UTIL_NET_HTTP_TRANSPORT_H_ + +#include + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "util/net/http_headers.h" + +namespace crashpad { + +class HTTPBodyStream; + +//! \brief HTTPTransport executes a HTTP request using the specified URL, HTTP +//! method, headers, and body. This class can only issue a synchronous +//! HTTP request. +//! +//! This class cannot be instantiated directly. A concrete subclass must be +//! instantiated instead, which provides an implementation to execute the +//! request that is appropriate for the host operating system. +class HTTPTransport { + public: + virtual ~HTTPTransport(); + + //! \brief Instantiates a concrete HTTPTransport class for the current + //! operating system. + //! + //! \return A new caller-owned HTTPTransport object. + static scoped_ptr Create(); + + //! \brief Sets URL to which the request will be made. + //! + //! \param[in] url The request URL. + void SetURL(const std::string& url); + + //! \brief Sets the HTTP method to execute. E.g., GET, POST, etc. The default + //! method is `"POST"`. + //! + //! \param[in] http_method The HTTP method. + void SetMethod(const std::string& http_method); + + //! \brief Sets a HTTP header-value pair. + //! + //! \param[in] header The HTTP header name. Any previous value set at this + //! name will be overwritten. + //! \param[in] value The value to set for the header. + void SetHeader(const std::string& header, const std::string& value); + + //! \brief Sets the stream object from which to generate the HTTP body. + //! + //! \param[in] stream A HTTPBodyStream, of which this class will take + //! ownership. + void SetBodyStream(scoped_ptr stream); + + //! \brief Sets the timeout for the HTTP request. The default is 15 seconds. + //! + //! \param[in] timeout The request timeout, in seconds. + void SetTimeout(double timeout); + + //! \brief Performs the HTTP request with the configured parameters and waits + //! for the execution to complete. + //! + //! \return Whether or not the request was successful, defined as returning + //! a HTTP status 200 (OK) code. + virtual bool ExecuteSynchronously() = 0; + + protected: + HTTPTransport(); + + const std::string& url() const { return url_; } + const std::string& method() const { return method_; } + const HTTPHeaders& headers() const { return headers_; } + HTTPBodyStream* body_stream() const { return body_stream_.get(); } + double timeout() const { return timeout_; } + + private: + std::string url_; + std::string method_; + HTTPHeaders headers_; + scoped_ptr body_stream_; + double timeout_; + + DISALLOW_COPY_AND_ASSIGN(HTTPTransport); +}; + +} // namespace crashpad + +#endif // CRASHPAD_UTIL_NET_HTTP_TRANSPORT_H_ diff --git a/util/net/http_transport_mac.mm b/util/net/http_transport_mac.mm new file mode 100644 index 00000000..a56499a6 --- /dev/null +++ b/util/net/http_transport_mac.mm @@ -0,0 +1,192 @@ +// 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" + +#import + +#include "base/mac/foundation_util.h" +#import "base/mac/scoped_nsobject.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "util/net/http_body.h" + +@interface CrashpadHTTPBodyStreamTransport : NSInputStream { + @private + NSStreamStatus streamStatus_; + id delegate_; + crashpad::HTTPBodyStream* bodyStream_; // weak +} +- (instancetype)initWithBodyStream:(crashpad::HTTPBodyStream*)bodyStream; +@end + +@implementation CrashpadHTTPBodyStreamTransport + +- (instancetype)initWithBodyStream:(crashpad::HTTPBodyStream*)bodyStream { + if ((self = [super init])) { + streamStatus_ = NSStreamStatusNotOpen; + bodyStream_ = bodyStream; + } + return self; +} + +// NSInputStream: + +- (BOOL)hasBytesAvailable { + // Per Apple's documentation: "May also return YES if a read must be attempted + // in order to determine the availability of bytes." + switch (streamStatus_) { + case NSStreamStatusAtEnd: + case NSStreamStatusClosed: + case NSStreamStatusError: + return NO; + default: + return YES; + } +} + +- (NSInteger)read:(uint8_t*)buffer maxLength:(NSUInteger)maxLen { + streamStatus_ = NSStreamStatusReading; + + NSInteger rv = bodyStream_->GetBytesBuffer(buffer, maxLen); + + if (rv == 0) + streamStatus_ = NSStreamStatusAtEnd; + else if (rv < 0) + streamStatus_ = NSStreamStatusError; + else + streamStatus_ = NSStreamStatusOpen; + + return rv; +} + +- (BOOL)getBuffer:(uint8_t**)buffer length:(NSUInteger*)length { + return NO; +} + +// NSStream: + +- (void)scheduleInRunLoop:(NSRunLoop*)runLoop + forMode:(NSString*)mode { +} + +- (void)removeFromRunLoop:(NSRunLoop*)runLoop + forMode:(NSString*)mode { +} + +- (void)open { + streamStatus_ = NSStreamStatusOpen; +} + +- (void)close { + streamStatus_ = NSStreamStatusClosed; +} + +- (NSStreamStatus)streamStatus { + return streamStatus_; +} + +- (id)delegate { + return delegate_; +} + +- (void)setDelegate:(id)delegate { + delegate_ = delegate; +} + +@end + +namespace crashpad { + +namespace { + +class HTTPTransportMac final : public HTTPTransport { + public: + HTTPTransportMac(); + ~HTTPTransportMac() override; + + bool ExecuteSynchronously() override; + + private: + DISALLOW_COPY_AND_ASSIGN(HTTPTransportMac); +}; + +HTTPTransportMac::HTTPTransportMac() : HTTPTransport() { +} + +HTTPTransportMac::~HTTPTransportMac() { +} + +bool HTTPTransportMac::ExecuteSynchronously() { + DCHECK(body_stream()); + + @autoreleasepool { + NSString* url_ns_string = base::SysUTF8ToNSString(url()); + NSURL* url = [NSURL URLWithString:url_ns_string]; + NSMutableURLRequest* request = + [NSMutableURLRequest requestWithURL:url + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:timeout()]; + [request setHTTPMethod:base::SysUTF8ToNSString(method())]; + + for (const auto& pair : headers()) { + [request setValue:base::SysUTF8ToNSString(pair.second) + forHTTPHeaderField:base::SysUTF8ToNSString(pair.first)]; + } + + base::scoped_nsobject transport( + [[CrashpadHTTPBodyStreamTransport alloc] initWithBodyStream: + body_stream()]); + [request setHTTPBodyStream:transport.get()]; + + NSURLResponse* response = nil; + NSError* error = nil; + [NSURLConnection sendSynchronousRequest:request + returningResponse:&response + error:&error]; + + if (error) { + LOG(ERROR) << [[error localizedDescription] UTF8String] << " (" + << [[error domain] UTF8String] << " " << [error code] << ")"; + return false; + } + if (!response) { + LOG(ERROR) << "no response"; + return false; + } + NSHTTPURLResponse* http_response = + base::mac::ObjCCast(response); + if (!http_response) { + LOG(ERROR) << "no http_response"; + return false; + } + NSInteger http_status = [http_response statusCode]; + if (http_status != 200) { + LOG(ERROR) << base::StringPrintf("HTTP status %ld", + static_cast(http_status)); + return false; + } + + return true; + } +} + +} // namespace + +// static +scoped_ptr HTTPTransport::Create() { + return scoped_ptr(new HTTPTransportMac()); +} + +} // namespace crashpad diff --git a/util/net/http_transport_test.cc b/util/net/http_transport_test.cc new file mode 100644 index 00000000..974e9f05 --- /dev/null +++ b/util/net/http_transport_test.cc @@ -0,0 +1,242 @@ +// 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 "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/stringprintf.h" +#include "build/build_config.h" +#include "gtest/gtest.h" +#include "util/file/fd_io.h" +#include "util/net/http_body.h" +#include "util/net/http_headers.h" +#include "util/net/http_multipart_builder.h" +#include "util/test/multiprocess_exec.h" + +namespace crashpad { +namespace test { +namespace { + +class HTTPTransportTestFixture : public MultiprocessExec { + public: + using RequestValidator = + void(*)(HTTPTransportTestFixture*, const std::string&); + + HTTPTransportTestFixture(const HTTPHeaders& headers, + scoped_ptr body_stream, + uint16_t http_response_code, + RequestValidator request_validator) + : MultiprocessExec(), + headers_(headers), + body_stream_(body_stream.Pass()), + response_code_(http_response_code), + request_validator_(request_validator) { + // TODO(rsesek): Use a more robust mechanism to locate testdata + // . + SetChildCommand("util/net/http_transport_test_server.py", nullptr); + } + + const HTTPHeaders& headers() { return headers_; } + + private: + void MultiprocessParent() override { + // The child will write the HTTP server port number as a packed unsigned + // short to stdout. + uint16_t port; + CheckedReadFD(ReadPipeFD(), &port, sizeof(port)); + + // Then the parent will tell the web server what response code to send + // for the HTTP request. + CheckedWriteFD(WritePipeFD(), &response_code_, sizeof(response_code_)); + + // Now execute the HTTP request. + scoped_ptr transport(HTTPTransport::Create()); + transport->SetMethod("POST"); + transport->SetURL(base::StringPrintf("http://127.0.0.1:%d/upload", port)); + for (const auto& pair : headers_) { + transport->SetHeader(pair.first, pair.second); + } + transport->SetBodyStream(body_stream_.Pass()); + + EXPECT_EQ(transport->ExecuteSynchronously(), (response_code_ == 200)); + + // Read until the child's stdout closes. + std::string request; + char buf[32]; + ssize_t bytes_read; + while ((bytes_read = ReadFD(ReadPipeFD(), buf, sizeof(buf))) != 0) { + ASSERT_GE(bytes_read, 0); + request.append(buf, bytes_read); + } + + if (request_validator_) + request_validator_(this, request); + } + + HTTPHeaders headers_; + scoped_ptr body_stream_; + uint16_t response_code_; + RequestValidator request_validator_; +}; + +const 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(std::string::npos, index); + // Since the header is never the first line of the request, it should always + // be preceded by a CRLF. + EXPECT_EQ('\n', request[index - 1]); + EXPECT_EQ('\r', request[index - 2]); + + 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(std::string::npos, header_end); + + *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(kMultipartFormData, content_type.substr(0, index)); + + EXPECT_EQ(';', content_type[index++]); + + size_t boundary_begin = content_type.find('=', index); + ASSERT_NE(std::string::npos, boundary_begin); + EXPECT_EQ('=', content_type[boundary_begin++]); + if (multipart_boundary) { + *multipart_boundary = content_type.substr(boundary_begin); + } +} + +const 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(fixture->headers().end(), content_type); + + size_t boundary = content_type->second.find(kBoundaryEq); + ASSERT_NE(std::string::npos, boundary); + std::string expected_boundary = + content_type->second.substr(boundary + strlen(kBoundaryEq)); + EXPECT_EQ(expected_boundary, actual_boundary); + + size_t body_start = request.find("\r\n\r\n"); + ASSERT_NE(std::string::npos, body_start); + 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(expected, request.substr(body_start, expected.length())); + + 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(body_start + expected.length(), request.length()); + EXPECT_EQ(expected, request.substr(body_start)); +} + +TEST(HTTPTransport, ValidFormData) { + HTTPMultipartBuilder builder; + builder.SetFormData("key1", "test"); + builder.SetFormData("key2", "--abcdefg123"); + + HTTPHeaders headers; + headers.insert(builder.GetContentType()); + + HTTPTransportTestFixture test(headers, builder.GetBodyStream(), 200, + &ValidFormData); + test.Run(); +} + +const char kTextPlain[] = "text/plain"; + +void ErrorResponse(HTTPTransportTestFixture* fixture, + const std::string& request) { + std::string content_type; + GetHeaderField(request, kContentType, &content_type); + EXPECT_EQ(kTextPlain, content_type); +} + +TEST(HTTPTransport, ErrorResponse) { + HTTPMultipartBuilder builder; + HTTPHeaders headers; + headers[kContentType] = kTextPlain; + HTTPTransportTestFixture test(headers, builder.GetBodyStream(), + 404, &ErrorResponse); + test.Run(); +} + +const char kTextBody[] = "hello world"; + +void UnchunkedPlainText(HTTPTransportTestFixture* fixture, + const std::string& request) { + std::string header_value; + GetHeaderField(request, kContentType, &header_value); + EXPECT_EQ(kTextPlain, header_value); + + GetHeaderField(request, kContentLength, &header_value); + const auto& content_length = fixture->headers().find(kContentLength); + ASSERT_NE(fixture->headers().end(), content_length); + EXPECT_EQ(content_length->second, header_value); + + size_t body_start = request.rfind("\r\n"); + ASSERT_NE(std::string::npos, body_start); + + EXPECT_EQ(kTextBody, request.substr(body_start + 2)); +} + +TEST(HTTPTransport, UnchunkedPlainText) { + scoped_ptr body_stream(new StringHTTPBodyStream(kTextBody)); + + HTTPHeaders headers; + headers[kContentType] = kTextPlain; + headers[kContentLength] = base::StringPrintf("%zu", strlen(kTextBody)); + + HTTPTransportTestFixture test(headers, body_stream.Pass(), 200, + &UnchunkedPlainText); + test.Run(); +} + +} // namespace +} // namespace test +} // namespace crashpad diff --git a/util/net/http_transport_test_server.py b/util/net/http_transport_test_server.py new file mode 100755 index 00000000..ac6c834c --- /dev/null +++ b/util/net/http_transport_test_server.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# coding: utf-8 + +# 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. + +"""A one-shot testing webserver. + +When invoked, this server will write an integer to stdout, indiciating on which +port the server is listening. It will then read one integer from stdin, +indiciating the response code to set for a request. The server will process +one HTTP request, writing it out entirely to stdout, and will then terminate. + +This server is written in Python since it provides a simple HTTP stack, and +because parsing Chunked encoding is safer and easier in a memory-safe language. +This could easily have been written in C++ instead. +""" + +import BaseHTTPServer +import struct +import sys + +class BufferedReadFile(object): + """A File-like object that stores all read contents into a buffer.""" + + def __init__(self, real_file): + self.file = real_file + self.buffer = "" + + def read(self, size=-1): + buf = self.file.read(size) + self.buffer += buf + return buf + + def readline(self, size=-1): + buf = self.file.readline(size) + self.buffer += buf + return buf + + def flush(self): + self.file.flush() + + def close(self): + self.file.close() + + +class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + response_code = 500 + + def handle_one_request(self): + # Wrap the rfile in the buffering file object so that the raw header block + # can be written to stdout after it is parsed. + self.rfile = BufferedReadFile(self.rfile) + BaseHTTPServer.BaseHTTPRequestHandler.handle_one_request(self) + + def do_POST(self): + writer = sys.stdout + + writer.write(self.rfile.buffer) + self.rfile.buffer = '' + + if self.headers.get('Transfer-Encoding', '') == 'Chunked': + body = self.handle_chunked_encoding() + else: + length = int(self.headers.get('Content-Length', -1)) + body = self.rfile.read(length) + + self.send_response(self.response_code) + writer.write(body) + writer.flush() + + def handle_chunked_encoding(self): + """This parses a "Transfer-Encoding: Chunked" body in accordance with + RFC 7230 §4.1. This returns the result as a string. + """ + body = '' + chunk_size = self.read_chunk_size() + while chunk_size > 0: + # Read the body. + data = self.rfile.read(chunk_size) + chunk_size -= len(data) + body += data + + # Finished reading this chunk. + if chunk_size == 0: + # Read through any trailer fields. + trailer_line = self.rfile.readline() + while trailer_line.strip() != '': + trailer_line = self.rfile.readline() + + # Read the chunk size. + chunk_size = self.read_chunk_size() + return body + + def read_chunk_size(self): + # Read the whole line, including the \r\n. + chunk_size_and_ext_line = self.rfile.readline() + # Look for a chunk extension. + chunk_size_end = chunk_size_and_ext_line.find(';') + if chunk_size_end == -1: + # No chunk extensions; just encounter the end of line. + chunk_size_end = chunk_size_and_ext_line.find('\r') + if chunk_size_end == -1: + self.send_response(400) # Bad request. + return -1 + return int(chunk_size_and_ext_line[:chunk_size_end], base=16) + + +def Main(): + # Start the server. + server = BaseHTTPServer.HTTPServer(('127.0.0.1', 0), RequestHandler) + + # Write the port as an unsigned short to the parent process. + sys.stdout.write(struct.pack('=H', server.server_address[1])) + sys.stdout.flush() + + # Read the desired test response code as an unsigned short from the parent + # process. + RequestHandler.response_code = \ + struct.unpack('=H', sys.stdin.read(struct.calcsize('=H')))[0] + + # Handle the request. + server.handle_request() + +if __name__ == '__main__': + Main() diff --git a/util/util.gyp b/util/util.gyp index c36732fe..d5ee3496 100644 --- a/util/util.gyp +++ b/util/util.gyp @@ -71,8 +71,13 @@ 'misc/uuid.h', 'net/http_body.cc', 'net/http_body.h', + 'net/http_headers.cc', + 'net/http_headers.h', 'net/http_multipart_builder.cc', 'net/http_multipart_builder.h', + 'net/http_transport.cc', + 'net/http_transport.h', + 'net/http_transport_mac.mm', 'numeric/checked_range.h', 'numeric/in_range_cast.h', 'numeric/int128.h', @@ -220,6 +225,7 @@ 'net/http_body_test_util.cc', 'net/http_body_test_util.h', 'net/http_multipart_builder_test.cc', + 'net/http_transport_test.cc', 'numeric/checked_range_test.cc', 'numeric/in_range_cast_test.cc', 'numeric/int128_test.cc',