diff options
author | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-07 17:50:26 +0000 |
---|---|---|
committer | phajdan.jr@chromium.org <phajdan.jr@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-05-07 17:50:26 +0000 |
commit | b6e5b6c6feec3301c6e74ae76e1df1762b150c3a (patch) | |
tree | 2c69567a4b2ca6b86d4e563e05dd3cbfcee4160a /net | |
parent | 04e51aa91468b3ad25e36ab7ac712be285495104 (diff) | |
download | chromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.zip chromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.tar.gz chromium_src-b6e5b6c6feec3301c6e74ae76e1df1762b150c3a.tar.bz2 |
GTTF: move chrome/browser/google_apis/test_server to net/test
This is a part of replacing most usages of the Python test server
with an in-process C++ test server that should be easier to debug.
BUG=96594
R=rch@chromium.org, satorux@chromium.org
Review URL: https://codereview.chromium.org/14971002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@198762 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net')
-rw-r--r-- | net/net.gyp | 11 | ||||
-rw-r--r-- | net/test/embedded_test_server/OWNERS | 2 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_connection.cc | 35 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_connection.h | 59 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_request.cc | 202 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_request.h | 115 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_request_unittest.cc | 82 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_response.cc | 54 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_response.h | 71 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_response_unittest.cc | 31 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_server.cc | 213 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_server.h | 161 | ||||
-rw-r--r-- | net/test/embedded_test_server/http_server_unittest.cc | 223 |
13 files changed, 1259 insertions, 0 deletions
diff --git a/net/net.gyp b/net/net.gyp index d13d4e8..75179aa 100644 --- a/net/net.gyp +++ b/net/net.gyp @@ -1771,6 +1771,9 @@ 'ssl/ssl_cipher_suite_names_unittest.cc', 'ssl/ssl_client_auth_cache_unittest.cc', 'ssl/ssl_config_service_unittest.cc', + 'test/embedded_test_server/http_request_unittest.cc', + 'test/embedded_test_server/http_response_unittest.cc', + 'test/embedded_test_server/http_server_unittest.cc', 'test/python_utils_unittest.cc', 'test/run_all_unittests.cc', 'test/test_certificate_data.h', @@ -2137,6 +2140,14 @@ 'test/base_test_server.h', 'test/cert_test_util.cc', 'test/cert_test_util.h', + 'test/embedded_test_server/http_connection.cc', + 'test/embedded_test_server/http_connection.h', + 'test/embedded_test_server/http_request.cc', + 'test/embedded_test_server/http_request.h', + 'test/embedded_test_server/http_response.cc', + 'test/embedded_test_server/http_response.h', + 'test/embedded_test_server/http_server.cc', + 'test/embedded_test_server/http_server.h', 'test/local_test_server_posix.cc', 'test/local_test_server_win.cc', 'test/local_test_server.cc', diff --git a/net/test/embedded_test_server/OWNERS b/net/test/embedded_test_server/OWNERS new file mode 100644 index 0000000..e0abbce --- /dev/null +++ b/net/test/embedded_test_server/OWNERS @@ -0,0 +1,2 @@ +mtomasz@chromium.org +satorux@chromium.org diff --git a/net/test/embedded_test_server/http_connection.cc b/net/test/embedded_test_server/http_connection.cc new file mode 100644 index 0000000..3207aac --- /dev/null +++ b/net/test/embedded_test_server/http_connection.cc @@ -0,0 +1,35 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_connection.h" + +#include "net/socket/stream_listen_socket.h" +#include "net/test/embedded_test_server/http_response.h" + +namespace google_apis { +namespace test_server { + +HttpConnection::HttpConnection(net::StreamListenSocket* socket, + const HandleRequestCallback& callback) + : socket_(socket), + callback_(callback) { +} + +HttpConnection::~HttpConnection() { +} + +void HttpConnection::SendResponse(scoped_ptr<HttpResponse> response) const { + const std::string response_string = response->ToResponseString(); + socket_->Send(response_string.c_str(), response_string.length()); +} + +void HttpConnection::ReceiveData(const base::StringPiece& data) { + request_parser_.ProcessChunk(data); + if (request_parser_.ParseRequest() == HttpRequestParser::ACCEPTED) { + callback_.Run(this, request_parser_.GetRequest()); + } +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_connection.h b/net/test/embedded_test_server/http_connection.h new file mode 100644 index 0000000..2561a3f --- /dev/null +++ b/net/test/embedded_test_server/http_connection.h @@ -0,0 +1,59 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_ +#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_ + +#include "base/basictypes.h" +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/strings/string_piece.h" +#include "net/test/embedded_test_server/http_request.h" + +namespace net { +class StreamListenSocket; +} + +namespace google_apis { +namespace test_server { + +class HttpConnection; +class HttpResponse; + +// Calblack called when a request is parsed. Response should be sent +// using HttpConnection::SendResponse() on the |connection| argument. +typedef base::Callback<void(HttpConnection* connection, + scoped_ptr<HttpRequest> request)> + HandleRequestCallback; + +// Wraps the connection socket. Accepts incoming data and sends responses. +// If a valid request is parsed, then |callback_| is invoked. +class HttpConnection { + public: + HttpConnection(net::StreamListenSocket* socket, + const HandleRequestCallback& callback); + ~HttpConnection(); + + // Sends the HTTP response to the client. + void SendResponse(scoped_ptr<HttpResponse> response) const; + + private: + friend class HttpServer; + + // Accepts raw chunk of data from the client. Internally, passes it to the + // HttpRequestParser class. If a request is parsed, then |callback_| is + // called. + void ReceiveData(const base::StringPiece& data); + + scoped_refptr<net::StreamListenSocket> socket_; + const HandleRequestCallback callback_; + HttpRequestParser request_parser_; + + DISALLOW_COPY_AND_ASSIGN(HttpConnection); +}; + +} // namespace test_server +} // namespace google_apis + +#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_CONNECTION_H_ diff --git a/net/test/embedded_test_server/http_request.cc b/net/test/embedded_test_server/http_request.cc new file mode 100644 index 0000000..39c8512 --- /dev/null +++ b/net/test/embedded_test_server/http_request.cc @@ -0,0 +1,202 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_request.h" + +#include <algorithm> + +#include "base/logging.h" +#include "base/string_util.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_split.h" + +namespace google_apis { +namespace test_server { + +namespace { + +size_t kRequestSizeLimit = 64 * 1024 * 1024; // 64 mb. + +// Helper function used to trim tokens in http request headers. +std::string Trim(const std::string& value) { + std::string result; + TrimString(value, " \t", &result); + return result; +} + +} // namespace + +HttpRequest::HttpRequest() : method(METHOD_UNKNOWN), + has_content(false) { +} + +HttpRequest::~HttpRequest() { +} + +HttpRequestParser::HttpRequestParser() + : http_request_(new HttpRequest()), + buffer_position_(0), + state_(STATE_HEADERS), + declared_content_length_(0) { +} + +HttpRequestParser::~HttpRequestParser() { +} + +void HttpRequestParser::ProcessChunk(const base::StringPiece& data) { + data.AppendToString(&buffer_); + DCHECK_LE(buffer_.size() + data.size(), kRequestSizeLimit) << + "The HTTP request is too large."; +} + +std::string HttpRequestParser::ShiftLine() { + size_t eoln_position = buffer_.find("\r\n", buffer_position_); + DCHECK_NE(std::string::npos, eoln_position); + const int line_length = eoln_position - buffer_position_; + std::string result = buffer_.substr(buffer_position_, line_length); + buffer_position_ += line_length + 2; + return result; +} + +HttpRequestParser::ParseResult HttpRequestParser::ParseRequest() { + DCHECK_NE(STATE_ACCEPTED, state_); + // Parse the request from beginning. However, entire request may not be + // available in the buffer. + if (state_ == STATE_HEADERS) { + if (ParseHeaders() == ACCEPTED) + return ACCEPTED; + } + // This should not be 'else if' of the previous block, as |state_| can be + // changed in ParseHeaders(). + if (state_ == STATE_CONTENT) { + if (ParseContent() == ACCEPTED) + return ACCEPTED; + } + return WAITING; +} + +HttpRequestParser::ParseResult HttpRequestParser::ParseHeaders() { + // Check if the all request headers are available. + if (buffer_.find("\r\n\r\n", buffer_position_) == std::string::npos) + return WAITING; + + // Parse request's the first header line. + // Request main main header, eg. GET /foobar.html HTTP/1.1 + { + const std::string header_line = ShiftLine(); + std::vector<std::string> header_line_tokens; + base::SplitString(header_line, ' ', &header_line_tokens); + DCHECK_EQ(3u, header_line_tokens.size()); + // Method. + http_request_->method = GetMethodType(StringToLowerASCII( + header_line_tokens[0])); + // Address. + // Don't build an absolute URL as the parser does not know (should not + // know) anything about the server address. + http_request_->relative_url = header_line_tokens[1]; + // Protocol. + const std::string protocol = StringToLowerASCII(header_line_tokens[2]); + CHECK(protocol == "http/1.0" || protocol == "http/1.1") << + "Protocol not supported: " << protocol; + } + + // Parse further headers. + { + std::string header_name; + while (true) { + std::string header_line = ShiftLine(); + if (header_line.empty()) + break; + + if (header_line[0] == ' ' || header_line[0] == '\t') { + // Continuation of the previous multi-line header. + std::string header_value = + Trim(header_line.substr(1, header_line.size() - 1)); + http_request_->headers[header_name] += " " + header_value; + } else { + // New header. + size_t delimiter_pos = header_line.find(":"); + DCHECK_NE(std::string::npos, delimiter_pos) << "Syntax error."; + header_name = Trim(header_line.substr(0, delimiter_pos)); + std::string header_value = Trim(header_line.substr( + delimiter_pos + 1, + header_line.size() - delimiter_pos - 1)); + http_request_->headers[header_name] = header_value; + } + } + } + + // Headers done. Is any content data attached to the request? + declared_content_length_ = 0; + if (http_request_->headers.count("Content-Length") > 0) { + http_request_->has_content = true; + const bool success = base::StringToSizeT( + http_request_->headers["Content-Length"], + &declared_content_length_); + DCHECK(success) << "Malformed Content-Length header's value."; + } + if (declared_content_length_ == 0) { + // No content data, so parsing is finished. + state_ = STATE_ACCEPTED; + return ACCEPTED; + } + + // The request has not yet been parsed yet, content data is still to be + // processed. + state_ = STATE_CONTENT; + return WAITING; +} + +HttpRequestParser::ParseResult HttpRequestParser::ParseContent() { + const size_t available_bytes = buffer_.size() - buffer_position_; + const size_t fetch_bytes = std::min( + available_bytes, + declared_content_length_ - http_request_->content.size()); + http_request_->content.append(buffer_.data() + buffer_position_, + fetch_bytes); + buffer_position_ += fetch_bytes; + + if (declared_content_length_ == http_request_->content.size()) { + state_ = STATE_ACCEPTED; + return ACCEPTED; + } + + state_ = STATE_CONTENT; + return WAITING; +} + +scoped_ptr<HttpRequest> HttpRequestParser::GetRequest() { + DCHECK_EQ(STATE_ACCEPTED, state_); + scoped_ptr<HttpRequest> result = http_request_.Pass(); + + // Prepare for parsing a new request. + state_ = STATE_HEADERS; + http_request_.reset(new HttpRequest()); + buffer_.clear(); + buffer_position_ = 0; + declared_content_length_ = 0; + + return result.Pass(); +} + +HttpMethod HttpRequestParser::GetMethodType(const std::string& token) const { + if (token == "get") { + return METHOD_GET; + } else if (token == "head") { + return METHOD_HEAD; + } else if (token == "post") { + return METHOD_POST; + } else if (token == "put") { + return METHOD_PUT; + } else if (token == "delete") { + return METHOD_DELETE; + } else if (token == "patch") { + return METHOD_PATCH; + } + NOTREACHED() << "Method not implemented: " << token; + return METHOD_UNKNOWN; +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_request.h b/net/test/embedded_test_server/http_request.h new file mode 100644 index 0000000..796d41a --- /dev/null +++ b/net/test/embedded_test_server/http_request.h @@ -0,0 +1,115 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_ +#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_ + +#include <map> +#include <string> + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_piece.h" + +namespace google_apis { +namespace test_server { + +// Methods of HTTP requests supported by the test HTTP server. +enum HttpMethod { + METHOD_UNKNOWN, + METHOD_GET, + METHOD_HEAD, + METHOD_POST, + METHOD_PUT, + METHOD_DELETE, + METHOD_PATCH, +}; + +// Represents a HTTP request. Since it can be big, use scoped_ptr to pass it +// instead of copying. However, the struct is copyable so tests can save and +// examine a HTTP request. +struct HttpRequest { + HttpRequest(); + ~HttpRequest(); + + std::string relative_url; // Starts with '/'. Example: "/test?query=foo" + HttpMethod method; + std::map<std::string, std::string> headers; + std::string content; + bool has_content; +}; + +// Parses the input data and produces a valid HttpRequest object. If there is +// more than one request in one chunk, then only the first one will be parsed. +// The common use is as below: +// HttpRequestParser parser; +// (...) +// void OnDataChunkReceived(Socket* socket, const char* data, int size) { +// parser.ProcessChunk(std::string(data, size)); +// if (parser.ParseRequest() == HttpRequestParser::ACCEPTED) { +// scoped_ptr<HttpRequest> request = parser.GetRequest(); +// (... process the request ...) +// } +class HttpRequestParser { + public: + // Parsing result. + enum ParseResult { + WAITING, // A request is not completed yet, waiting for more data. + ACCEPTED, // A request has been parsed and it is ready to be processed. + }; + + // Parser state. + enum State { + STATE_HEADERS, // Waiting for a request headers. + STATE_CONTENT, // Waiting for content data. + STATE_ACCEPTED, // Request has been parsed. + }; + + HttpRequestParser(); + ~HttpRequestParser(); + + // Adds chunk of data into the internal buffer. + void ProcessChunk(const base::StringPiece& data); + + // Parses the http request (including data - if provided). + // If returns ACCEPTED, then it means that the whole request has been found + // in the internal buffer (and parsed). After calling GetRequest(), it will be + // ready to parse another request. + ParseResult ParseRequest(); + + // Retrieves parsed request. Can be only called, when the parser is in + // STATE_ACCEPTED state. After calling it, the parser is ready to parse + // another request. + scoped_ptr<HttpRequest> GetRequest(); + + private: + HttpMethod GetMethodType(const std::string& token) const; + + // Parses headers and returns ACCEPTED if whole request was parsed. Otherwise + // returns WAITING. + ParseResult ParseHeaders(); + + // Parses request's content data and returns ACCEPTED if all of it have been + // processed. Chunked Transfer Encoding *is not* supported. + ParseResult ParseContent(); + + // Fetches the next line from the buffer. Result does not contain \r\n. + // Returns an empty string for an empty line. It will assert if there is + // no line available. + std::string ShiftLine(); + + scoped_ptr<HttpRequest> http_request_; + std::string buffer_; + size_t buffer_position_; // Current position in the internal buffer. + State state_; + // Content length of the request currently being parsed. + size_t declared_content_length_; + + DISALLOW_COPY_AND_ASSIGN(HttpRequestParser); +}; + +} // namespace test_server +} // namespace google_apis + +#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_REQUEST_H_ diff --git a/net/test/embedded_test_server/http_request_unittest.cc b/net/test/embedded_test_server/http_request_unittest.cc new file mode 100644 index 0000000..7229bf4 --- /dev/null +++ b/net/test/embedded_test_server/http_request_unittest.cc @@ -0,0 +1,82 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_request.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { +namespace test_server { + +TEST(HttpRequestTest, ParseRequest) { + HttpRequestParser parser; + + // Process request in chunks to check if the parser deals with border cases. + // Also, check multi-line headers as well as multiple requests in the same + // chunk. This basically should cover all the simplest border cases. + parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + parser.ProcessChunk("Host: localhost:1234\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + parser.ProcessChunk("Multi-line-header: abcd\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + parser.ProcessChunk(" efgh\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + parser.ProcessChunk(" ijkl\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + parser.ProcessChunk("Content-Length: 10\r\n\r\n"); + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); + // Content data and another request in the same chunk (possible in http/1.1). + parser.ProcessChunk("1234567890GET /another.html HTTP/1.1\r\n\r\n"); + ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest()); + + // Fetch the first request and validate it. + { + scoped_ptr<HttpRequest> request = parser.GetRequest(); + EXPECT_EQ("/foobar.html", request->relative_url); + EXPECT_EQ(METHOD_POST, request->method); + EXPECT_EQ("1234567890", request->content); + ASSERT_EQ(3u, request->headers.size()); + + EXPECT_EQ(1u, request->headers.count("Host")); + EXPECT_EQ(1u, request->headers.count("Multi-line-header")); + EXPECT_EQ(1u, request->headers.count("Content-Length")); + + EXPECT_EQ("localhost:1234", request->headers["Host"]); + EXPECT_EQ("abcd efgh ijkl", request->headers["Multi-line-header"]); + EXPECT_EQ("10", request->headers["Content-Length"]); + } + + // No other request available yet since we do not support multiple requests + // per connection. + EXPECT_EQ(HttpRequestParser::WAITING, parser.ParseRequest()); +} + +TEST(HttpRequestTest, ParseRequestWithEmptyBody) { + HttpRequestParser parser; + + parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n"); + parser.ProcessChunk("Content-Length: 0\r\n\r\n"); + ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest()); + + scoped_ptr<HttpRequest> request = parser.GetRequest(); + EXPECT_EQ("", request->content); + EXPECT_TRUE(request->has_content); + EXPECT_EQ(1u, request->headers.count("Content-Length")); + EXPECT_EQ("0", request->headers["Content-Length"]); +} + +TEST(HttpRequestTest, ParseRequestWithoutBody) { + HttpRequestParser parser; + + parser.ProcessChunk("POST /foobar.html HTTP/1.1\r\n\r\n"); + ASSERT_EQ(HttpRequestParser::ACCEPTED, parser.ParseRequest()); + + scoped_ptr<HttpRequest> request = parser.GetRequest(); + EXPECT_EQ("", request->content); + EXPECT_FALSE(request->has_content); +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_response.cc b/net/test/embedded_test_server/http_response.cc new file mode 100644 index 0000000..72f465d --- /dev/null +++ b/net/test/embedded_test_server/http_response.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_response.h" + +#include "base/format_macros.h" +#include "base/logging.h" +#include "base/stringprintf.h" + +namespace google_apis { +namespace test_server { + +HttpResponse::HttpResponse() : code_(SUCCESS) { +} + +HttpResponse::~HttpResponse() { +} + +std::string HttpResponse::ToResponseString() const { + // Response line with headers. + std::string response_builder; + + // TODO(mtomasz): For http/1.0 requests, send http/1.0. + // TODO(mtomasz): For different codes, send a corrent string instead of OK. + base::StringAppendF(&response_builder, "HTTP/1.1 %d OK\r\n", code_); + base::StringAppendF(&response_builder, "Connection: closed\r\n"); + base::StringAppendF(&response_builder, + "Content-Length: %"PRIuS"\r\n", + content_.size()); + base::StringAppendF(&response_builder, + "Content-Type: %s\r\n", + content_type_.c_str()); + for (std::map<std::string, std::string>::const_iterator it = + custom_headers_.begin(); + it != custom_headers_.end(); + ++it) { + // Multi-line header value support. + const std::string& header_name = it->first; + const std::string& header_value = it->second; + DCHECK(header_value.find_first_of("\n\r") == std::string::npos) << + "Malformed header value."; + base::StringAppendF(&response_builder, + "%s: %s\r\n", + header_name.c_str(), + header_value.c_str()); + } + base::StringAppendF(&response_builder, "\r\n"); + + return response_builder + content_; +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_response.h b/net/test/embedded_test_server/http_response.h new file mode 100644 index 0000000..4598587 --- /dev/null +++ b/net/test/embedded_test_server/http_response.h @@ -0,0 +1,71 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_ +#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_ + +#include <map> +#include <string> + +#include "base/basictypes.h" + +namespace google_apis { +namespace test_server { + +enum ResponseCode { + SUCCESS = 200, + CREATED = 201, + NO_CONTENT = 204, + MOVED = 301, + RESUME_INCOMPLETE = 308, + NOT_FOUND = 404, + PRECONDITION = 412, + ACCESS_DENIED = 500, +}; + +// Respresents a HTTP response. Since it can be big, it may be better to use +// scoped_ptr to pass it instead of copying. +class HttpResponse { + public: + HttpResponse(); + ~HttpResponse(); + + // The response code. + ResponseCode code() const { return code_; } + void set_code(ResponseCode code) { code_ = code; } + + // The content of the response. + const std::string& content() const { return content_; } + void set_content(const std::string& content) { content_ = content; } + + // The content type. + const std::string& content_type() const { return content_type_; } + void set_content_type(const std::string& content_type) { + content_type_ = content_type; + } + + // The custom headers to be sent to the client. + const std::map<std::string, std::string>& custom_headers() const { + return custom_headers_; + } + + // Adds a custom header. + void AddCustomHeader(const std::string& key, const std::string& value) { + custom_headers_[key] = value; + } + + // Generates and returns a http response string. + std::string ToResponseString() const; + + private: + ResponseCode code_; + std::string content_; + std::string content_type_; + std::map<std::string, std::string> custom_headers_; +}; + +} // namespace test_servers +} // namespace google_apis + +#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_RESPONSE_H_ diff --git a/net/test/embedded_test_server/http_response_unittest.cc b/net/test/embedded_test_server/http_response_unittest.cc new file mode 100644 index 0000000..b8f1a4b --- /dev/null +++ b/net/test/embedded_test_server/http_response_unittest.cc @@ -0,0 +1,31 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_response.h" + +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { +namespace test_server { + +TEST(HttpResponseTest, GenerateResponse) { + HttpResponse response; + response.set_code(SUCCESS); + response.set_content("Sample content - Hello world!"); + response.set_content_type("text/plain"); + response.AddCustomHeader("Simple-Header", "Simple value."); + + std::string kExpectedResponseString = + "HTTP/1.1 200 OK\r\n" + "Connection: closed\r\n" + "Content-Length: 29\r\n" + "Content-Type: text/plain\r\n" + "Simple-Header: Simple value.\r\n\r\n" + "Sample content - Hello world!"; + + EXPECT_EQ(kExpectedResponseString, response.ToResponseString()); +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_server.cc b/net/test/embedded_test_server/http_server.cc new file mode 100644 index 0000000..165ac1b --- /dev/null +++ b/net/test/embedded_test_server/http_server.cc @@ -0,0 +1,213 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_server.h" + +#include "base/bind.h" +#include "base/run_loop.h" +#include "base/stl_util.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "net/test/embedded_test_server/http_connection.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/tools/fetch/http_listen_socket.h" + +namespace google_apis { +namespace test_server { + +namespace { + +const int kPort = 8040; +const char kIp[] = "127.0.0.1"; +const int kRetries = 10; + +// Callback to handle requests with default predefined response for requests +// matching the address |url|. +scoped_ptr<HttpResponse> HandleDefaultRequest(const GURL& url, + const HttpResponse& response, + const HttpRequest& request) { + const GURL request_url = url.Resolve(request.relative_url); + if (url.path() != request_url.path()) + return scoped_ptr<HttpResponse>(NULL); + return scoped_ptr<HttpResponse>(new HttpResponse(response)); +} + +} // namespace + +HttpListenSocket::HttpListenSocket(const SocketDescriptor socket_descriptor, + net::StreamListenSocket::Delegate* delegate) + : net::TCPListenSocket(socket_descriptor, delegate) { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +void HttpListenSocket::Listen() { + DCHECK(thread_checker_.CalledOnValidThread()); + net::TCPListenSocket::Listen(); +} + +HttpListenSocket::~HttpListenSocket() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +HttpServer::HttpServer( + const scoped_refptr<base::SingleThreadTaskRunner>& io_thread) + : io_thread_(io_thread), + port_(-1), + weak_factory_(this) { + DCHECK(io_thread_); + DCHECK(thread_checker_.CalledOnValidThread()); +} + +HttpServer::~HttpServer() { + DCHECK(thread_checker_.CalledOnValidThread()); +} + +bool HttpServer::InitializeAndWaitUntilReady() { + DCHECK(thread_checker_.CalledOnValidThread()); + + base::RunLoop run_loop; + if (!io_thread_->PostTaskAndReply( + FROM_HERE, + base::Bind(&HttpServer::InitializeOnIOThread, base::Unretained(this)), + run_loop.QuitClosure())) { + return false; + } + run_loop.Run(); + + return Started(); +} + +bool HttpServer::ShutdownAndWaitUntilComplete() { + DCHECK(thread_checker_.CalledOnValidThread()); + + base::RunLoop run_loop; + if (!io_thread_->PostTaskAndReply( + FROM_HERE, + base::Bind(&HttpServer::ShutdownOnIOThread, base::Unretained(this)), + run_loop.QuitClosure())) { + return false; + } + run_loop.Run(); + + return true; +} + +void HttpServer::InitializeOnIOThread() { + DCHECK(io_thread_->BelongsToCurrentThread()); + DCHECK(!Started()); + + int retries_left = kRetries + 1; + int try_port = kPort; + + while (retries_left > 0) { + SocketDescriptor socket_descriptor = net::TCPListenSocket::CreateAndBind( + kIp, + try_port); + if (socket_descriptor != net::TCPListenSocket::kInvalidSocket) { + listen_socket_ = new HttpListenSocket(socket_descriptor, this); + listen_socket_->Listen(); + base_url_ = GURL(base::StringPrintf("http://%s:%d", kIp, try_port)); + port_ = try_port; + break; + } + retries_left--; + try_port++; + } +} + +void HttpServer::ShutdownOnIOThread() { + DCHECK(io_thread_->BelongsToCurrentThread()); + + listen_socket_ = NULL; // Release the listen socket. + STLDeleteContainerPairSecondPointers(connections_.begin(), + connections_.end()); + connections_.clear(); +} + +void HttpServer::HandleRequest(HttpConnection* connection, + scoped_ptr<HttpRequest> request) { + DCHECK(io_thread_->BelongsToCurrentThread()); + + for (size_t i = 0; i < request_handlers_.size(); ++i) { + scoped_ptr<HttpResponse> response = + request_handlers_[i].Run(*request.get()); + if (response.get()) { + connection->SendResponse(response.Pass()); + return; + } + } + + LOG(WARNING) << "Request not handled. Returning 404: " + << request->relative_url; + scoped_ptr<HttpResponse> not_found_response(new HttpResponse()); + not_found_response->set_code(NOT_FOUND); + connection->SendResponse(not_found_response.Pass()); + + // Drop the connection, since we do not support multiple requests per + // connection. + connections_.erase(connection->socket_.get()); + delete connection; +} + +GURL HttpServer::GetURL(const std::string& relative_url) const { + DCHECK(StartsWithASCII(relative_url, "/", true /* case_sensitive */)) + << relative_url; + return base_url_.Resolve(relative_url); +} + +void HttpServer::RegisterRequestHandler( + const HandleRequestCallback& callback) { + request_handlers_.push_back(callback); +} + +void HttpServer::DidAccept(net::StreamListenSocket* server, + net::StreamListenSocket* connection) { + DCHECK(io_thread_->BelongsToCurrentThread()); + + HttpConnection* http_connection = new HttpConnection( + connection, + base::Bind(&HttpServer::HandleRequest, weak_factory_.GetWeakPtr())); + connections_[connection] = http_connection; +} + +void HttpServer::DidRead(net::StreamListenSocket* connection, + const char* data, + int length) { + DCHECK(io_thread_->BelongsToCurrentThread()); + + HttpConnection* http_connection = FindConnection(connection); + if (http_connection == NULL) { + LOG(WARNING) << "Unknown connection."; + return; + } + http_connection->ReceiveData(std::string(data, length)); +} + +void HttpServer::DidClose(net::StreamListenSocket* connection) { + DCHECK(io_thread_->BelongsToCurrentThread()); + + HttpConnection* http_connection = FindConnection(connection); + if (http_connection == NULL) { + LOG(WARNING) << "Unknown connection."; + return; + } + delete http_connection; + connections_.erase(connection); +} + +HttpConnection* HttpServer::FindConnection( + net::StreamListenSocket* socket) { + DCHECK(io_thread_->BelongsToCurrentThread()); + + std::map<net::StreamListenSocket*, HttpConnection*>::iterator it = + connections_.find(socket); + if (it == connections_.end()) { + return NULL; + } + return it->second; +} + +} // namespace test_server +} // namespace google_apis diff --git a/net/test/embedded_test_server/http_server.h b/net/test/embedded_test_server/http_server.h new file mode 100644 index 0000000..d0e262e --- /dev/null +++ b/net/test/embedded_test_server/http_server.h @@ -0,0 +1,161 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_ +#define NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/threading/thread_checker.h" +#include "googleurl/src/gurl.h" +#include "net/socket/tcp_listen_socket.h" + +namespace google_apis { +namespace test_server { + +class HttpConnection; +class HttpResponse; +struct HttpRequest; + +// This class is required to be able to have composition instead of inheritance, +class HttpListenSocket : public net::TCPListenSocket { + public: + HttpListenSocket(const SocketDescriptor socket_descriptor, + net::StreamListenSocket::Delegate* delegate); + virtual void Listen(); + + private: + virtual ~HttpListenSocket(); + + base::ThreadChecker thread_checker_; +}; + +// Class providing an HTTP server for testing purpose. This is a basic server +// providing only an essential subset of HTTP/1.1 protocol. Especially, +// it assumes that the request syntax is correct. It *does not* support +// a Chunked Transfer Encoding. +// +// The common use case is below: +// +// base::Thread io_thread_; +// scoped_ptr<HttpServer> test_server_; +// +// void SetUp() { +// base::Thread::Options thread_options; +// thread_options.message_loop_type = MessageLoop::TYPE_IO; +// ASSERT_TRUE(io_thread_.StartWithOptions(thread_options)); +// +// test_server_.reset(new HttpServer(io_thread_.message_loop_proxy())); +// ASSERT_TRUE(test_server_.InitializeAndWaitUntilReady()); +// test_server_->RegisterRequestHandler( +// base::Bind(&FooTest::HandleRequest, base::Unretained(this))); +// } +// +// scoped_ptr<HttpResponse> HandleRequest(const HttpRequest& request) { +// GURL absolute_url = test_server_->GetURL(request.relative_url); +// if (absolute_url.path() != "/test") +// return scoped_ptr<HttpResponse>(); +// +// scoped_ptr<HttpResponse> http_response(new HttpResponse()); +// http_response->set_code(test_server::SUCCESS); +// http_response->set_content("hello"); +// http_response->set_content_type("text/plain"); +// return http_response.Pass(); +// } +// +class HttpServer : public net::StreamListenSocket::Delegate { + public: + typedef base::Callback<scoped_ptr<HttpResponse>(const HttpRequest& request)> + HandleRequestCallback; + + // Creates a http test server. |io_thread| is a task runner + // with IO message loop, used as a backend thread. + // InitializeAndWaitUntilReady() must be called to start the server. + explicit HttpServer( + const scoped_refptr<base::SingleThreadTaskRunner>& io_thread); + virtual ~HttpServer(); + + // Initializes and waits until the server is ready to accept requests. + bool InitializeAndWaitUntilReady() WARN_UNUSED_RESULT; + + // Shuts down the http server and waits until the shutdown is complete. + bool ShutdownAndWaitUntilComplete() WARN_UNUSED_RESULT; + + // Checks if the server is started. + bool Started() const { + return listen_socket_.get() != NULL; + } + + // Returns the base URL to the server, which looks like + // http://127.0.0.1:<port>/, where <port> is the actual port number used by + // the server. + const GURL& base_url() const { return base_url_; } + + // Returns a URL to the server based on the given relative URL, which + // should start with '/'. For example: GetURL("/path?query=foo") => + // http://127.0.0.1:<port>/path?query=foo. + GURL GetURL(const std::string& relative_url) const; + + // Returns the port number used by the server. + int port() const { return port_; } + + // The most general purpose method. Any request processing can be added using + // this method. Takes ownership of the object. The |callback| is called + // on UI thread. + void RegisterRequestHandler(const HandleRequestCallback& callback); + + private: + // Initializes and starts the server. If initialization succeeds, Starts() + // will return true. + void InitializeOnIOThread(); + + // Shuts down the server. + void ShutdownOnIOThread(); + + // Handles a request when it is parsed. It passes the request to registed + // request handlers and sends a http response. + void HandleRequest(HttpConnection* connection, + scoped_ptr<HttpRequest> request); + + // net::StreamListenSocket::Delegate overrides: + virtual void DidAccept(net::StreamListenSocket* server, + net::StreamListenSocket* connection) OVERRIDE; + virtual void DidRead(net::StreamListenSocket* connection, + const char* data, + int length) OVERRIDE; + virtual void DidClose(net::StreamListenSocket* connection) OVERRIDE; + + HttpConnection* FindConnection(net::StreamListenSocket* socket); + + scoped_refptr<base::SingleThreadTaskRunner> io_thread_; + + scoped_refptr<HttpListenSocket> listen_socket_; + int port_; + GURL base_url_; + + // Owns the HttpConnection objects. + std::map<net::StreamListenSocket*, HttpConnection*> connections_; + + // Vector of registered request handlers. + std::vector<HandleRequestCallback> request_handlers_; + + // Note: This should remain the last member so it'll be destroyed and + // invalidate its weak pointers before any other members are destroyed. + base::WeakPtrFactory<HttpServer> weak_factory_; + + base::ThreadChecker thread_checker_; + + DISALLOW_COPY_AND_ASSIGN(HttpServer); +}; + +} // namespace test_servers +} // namespace google_apis + +#endif // NET_TEST_EMBEDDED_TEST_SERVER_HTTP_SERVER_H_ diff --git a/net/test/embedded_test_server/http_server_unittest.cc b/net/test/embedded_test_server/http_server_unittest.cc new file mode 100644 index 0000000..d10622c --- /dev/null +++ b/net/test/embedded_test_server/http_server_unittest.cc @@ -0,0 +1,223 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "net/test/embedded_test_server/http_server.h" + +#include "base/stringprintf.h" +#include "base/threading/thread.h" +#include "net/http/http_response_headers.h" +#include "net/test/embedded_test_server/http_request.h" +#include "net/test/embedded_test_server/http_response.h" +#include "net/url_request/url_fetcher.h" +#include "net/url_request/url_fetcher_delegate.h" +#include "net/url_request/url_request_test_util.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace google_apis { +namespace test_server { + +namespace { + +// Gets the content from the given URLFetcher. +std::string GetContentFromFetcher(const net::URLFetcher& fetcher) { + std::string result; + const bool success = fetcher.GetResponseAsString(&result); + EXPECT_TRUE(success); + return result; +} + +// Gets the content type from the given URLFetcher. +std::string GetContentTypeFromFetcher(const net::URLFetcher& fetcher) { + const net::HttpResponseHeaders* headers = fetcher.GetResponseHeaders(); + if (headers) { + std::string content_type; + if (headers->GetMimeType(&content_type)) + return content_type; + } + return std::string(); +} + +} // namespace + +class HttpServerTest : public testing::Test, + public net::URLFetcherDelegate { + public: + HttpServerTest() + : num_responses_received_(0), + num_responses_expected_(0), + io_thread_("io_thread") { + } + + virtual void SetUp() OVERRIDE { + base::Thread::Options thread_options; + thread_options.message_loop_type = MessageLoop::TYPE_IO; + ASSERT_TRUE(io_thread_.StartWithOptions(thread_options)); + + request_context_getter_ = new net::TestURLRequestContextGetter( + io_thread_.message_loop_proxy()); + + server_.reset(new HttpServer(io_thread_.message_loop_proxy())); + ASSERT_TRUE(server_->InitializeAndWaitUntilReady()); + } + + virtual void TearDown() OVERRIDE { + ASSERT_TRUE(server_->ShutdownAndWaitUntilComplete()); + } + + // net::URLFetcherDelegate override. + virtual void OnURLFetchComplete(const net::URLFetcher* source) OVERRIDE { + ++num_responses_received_; + if (num_responses_received_ == num_responses_expected_) + MessageLoop::current()->Quit(); + } + + // Waits until the specified number of responses are received. + void WaitForResponses(int num_responses) { + num_responses_received_ = 0; + num_responses_expected_ = num_responses; + // Will be terminated in OnURLFetchComplete(). + MessageLoop::current()->Run(); + } + + // Handles |request| sent to |path| and returns the response per |content|, + // |content type|, and |code|. Saves the request URL for verification. + scoped_ptr<HttpResponse> HandleRequest(const std::string& path, + const std::string& content, + const std::string& content_type, + ResponseCode code, + const HttpRequest& request) { + request_relative_url_ = request.relative_url; + + GURL absolute_url = server_->GetURL(request.relative_url); + if (absolute_url.path() == path) { + scoped_ptr<HttpResponse> http_response(new HttpResponse); + http_response->set_code(code); + http_response->set_content(content); + http_response->set_content_type(content_type); + return http_response.Pass(); + } + + return scoped_ptr<HttpResponse>(); + } + + protected: + int num_responses_received_; + int num_responses_expected_; + std::string request_relative_url_; + base::Thread io_thread_; + scoped_refptr<net::TestURLRequestContextGetter> request_context_getter_; + scoped_ptr<HttpServer> server_; +}; + +TEST_F(HttpServerTest, GetBaseURL) { + EXPECT_EQ(base::StringPrintf("http://127.0.0.1:%d/", server_->port()), + server_->base_url().spec()); +} + +TEST_F(HttpServerTest, GetURL) { + EXPECT_EQ(base::StringPrintf("http://127.0.0.1:%d/path?query=foo", + server_->port()), + server_->GetURL("/path?query=foo").spec()); +} + +TEST_F(HttpServerTest, RegisterRequestHandler) { + server_->RegisterRequestHandler(base::Bind(&HttpServerTest::HandleRequest, + base::Unretained(this), + "/test", + "<b>Worked!</b>", + "text/html", + SUCCESS)); + + scoped_ptr<net::URLFetcher> fetcher( + net::URLFetcher::Create(server_->GetURL("/test?q=foo"), + net::URLFetcher::GET, + this)); + fetcher->SetRequestContext(request_context_getter_.get()); + fetcher->Start(); + WaitForResponses(1); + + EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher->GetStatus().status()); + EXPECT_EQ(SUCCESS, fetcher->GetResponseCode()); + EXPECT_EQ("<b>Worked!</b>", GetContentFromFetcher(*fetcher)); + EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher)); + + EXPECT_EQ("/test?q=foo", request_relative_url_); +} + +TEST_F(HttpServerTest, DefaultNotFoundResponse) { + scoped_ptr<net::URLFetcher> fetcher( + net::URLFetcher::Create(server_->GetURL("/non-existent"), + net::URLFetcher::GET, + this)); + fetcher->SetRequestContext(request_context_getter_.get()); + + fetcher->Start(); + WaitForResponses(1); + EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher->GetStatus().status()); + EXPECT_EQ(NOT_FOUND, fetcher->GetResponseCode()); +} + +TEST_F(HttpServerTest, ConcurrentFetches) { + server_->RegisterRequestHandler( + base::Bind(&HttpServerTest::HandleRequest, + base::Unretained(this), + "/test1", + "Raspberry chocolate", + "text/html", + SUCCESS)); + server_->RegisterRequestHandler( + base::Bind(&HttpServerTest::HandleRequest, + base::Unretained(this), + "/test2", + "Vanilla chocolate", + "text/html", + SUCCESS)); + server_->RegisterRequestHandler( + base::Bind(&HttpServerTest::HandleRequest, + base::Unretained(this), + "/test3", + "No chocolates", + "text/plain", + NOT_FOUND)); + + scoped_ptr<net::URLFetcher> fetcher1 = scoped_ptr<net::URLFetcher>( + net::URLFetcher::Create(server_->GetURL("/test1"), + net::URLFetcher::GET, + this)); + fetcher1->SetRequestContext(request_context_getter_.get()); + scoped_ptr<net::URLFetcher> fetcher2 = scoped_ptr<net::URLFetcher>( + net::URLFetcher::Create(server_->GetURL("/test2"), + net::URLFetcher::GET, + this)); + fetcher2->SetRequestContext(request_context_getter_.get()); + scoped_ptr<net::URLFetcher> fetcher3 = scoped_ptr<net::URLFetcher>( + net::URLFetcher::Create(server_->GetURL("/test3"), + net::URLFetcher::GET, + this)); + fetcher3->SetRequestContext(request_context_getter_.get()); + + // Fetch the three URLs concurrently. + fetcher1->Start(); + fetcher2->Start(); + fetcher3->Start(); + WaitForResponses(3); + + EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher1->GetStatus().status()); + EXPECT_EQ(SUCCESS, fetcher1->GetResponseCode()); + EXPECT_EQ("Raspberry chocolate", GetContentFromFetcher(*fetcher1)); + EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher1)); + + EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher2->GetStatus().status()); + EXPECT_EQ(SUCCESS, fetcher2->GetResponseCode()); + EXPECT_EQ("Vanilla chocolate", GetContentFromFetcher(*fetcher2)); + EXPECT_EQ("text/html", GetContentTypeFromFetcher(*fetcher2)); + + EXPECT_EQ(net::URLRequestStatus::SUCCESS, fetcher3->GetStatus().status()); + EXPECT_EQ(NOT_FOUND, fetcher3->GetResponseCode()); + EXPECT_EQ("No chocolates", GetContentFromFetcher(*fetcher3)); + EXPECT_EQ("text/plain", GetContentTypeFromFetcher(*fetcher3)); +} + +} // namespace test_server +} // namespace google_apis |