shill: Support CONNECT HTTP method in proxy
This will allow HTTPS requests to be proxied.
BUG=chromium-os:24220
TEST=New unit tests +
manual "curl -v -x localhost:xxx https://www.google.com" test
Change-Id: Ibdec3536edb97018f6a7e7545025dd792998e7b0
Reviewed-on: https://gerrit.chromium.org/gerrit/13955
Reviewed-by: mukesh agrawal <quiche@chromium.org>
Reviewed-by: Thieu Le <thieule@chromium.org>
Commit-Ready: Paul Stewart <pstew@chromium.org>
Tested-by: Paul Stewart <pstew@chromium.org>
diff --git a/http_proxy.cc b/http_proxy.cc
index 1ba1d21..2075c65 100644
--- a/http_proxy.cc
+++ b/http_proxy.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -42,6 +42,8 @@
const size_t HTTPProxy::kMaxHeaderSize = 2048;
const int HTTPProxy::kTransactionTimeoutSeconds = 600;
+const char HTTPProxy::kHTTPMethodConnect[] = "connect";
+const char HTTPProxy::kHTTPMethodTerminator[] = " ";
const char HTTPProxy::kHTTPURLDelimiters[] = " /#?";
const char HTTPProxy::kHTTPURLPrefix[] = "http://";
const char HTTPProxy::kHTTPVersionPrefix[] = " HTTP/1";
@@ -207,6 +209,14 @@
}
server_socket_ = fd;
state_ = kStateTunnelData;
+
+ // If this was a "CONNECT" request, notify the client that the connection
+ // has been established by sending an "OK" response.
+ if (LowerCaseEqualsASCII(client_method_, kHTTPMethodConnect)) {
+ SetClientResponse(200, "OK", "", "");
+ StartReceive();
+ }
+
StartTransmit();
}
@@ -245,11 +255,13 @@
// Assemble the request as it will be sent to the server.
client_data_.Clear();
- for (vector<string>::iterator it = client_headers_.begin();
- it != client_headers_.end(); ++it) {
- client_data_.Append(ByteString(*it + "\r\n", false));
+ if (!LowerCaseEqualsASCII(client_method_, kHTTPMethodConnect)) {
+ for (vector<string>::iterator it = client_headers_.begin();
+ it != client_headers_.end(); ++it) {
+ client_data_.Append(ByteString(*it + "\r\n", false));
+ }
+ client_data_.Append(ByteString(string("\r\n"), false));
}
- client_data_.Append(ByteString(string("\r\n"), false));
TrimWhitespaceASCII(host, TRIM_ALL, &host);
if (host.empty()) {
@@ -314,7 +326,9 @@
// Is this is the first header line?
if (client_headers_.size() == 1) {
- if (!ReadClientHTTPVersion(header) || !ReadClientHostname(header)) {
+ if (!ReadClientHTTPMethod(header) ||
+ !ReadClientHTTPVersion(header) ||
+ !ReadClientHostname(header)) {
return false;
}
}
@@ -409,6 +423,17 @@
return true;
}
+bool HTTPProxy::ReadClientHTTPMethod(string *header) {
+ size_t method_end = header->find(kHTTPMethodTerminator);
+ if (method_end == string::npos || method_end == 0) {
+ LOG(ERROR) << "Could not parse HTTP method. Line was: " << *header;
+ SendClientError(501, "Server could not parse HTTP method");
+ return false;
+ }
+ client_method_ = header->substr(0, method_end);
+ return true;
+}
+
// Extract the HTTP version number from the first line of the client headers.
// Returns true if found.
bool HTTPProxy::ReadClientHTTPVersion(string *header) {
@@ -481,15 +506,27 @@
void HTTPProxy::SendClientError(int code, const string &error) {
VLOG(3) << "In " << __func__;
LOG(ERROR) << "Sending error " << error;
-
- string error_msg = StringPrintf("HTTP/1.1 %d ERROR\r\n"
- "Content-Type: text/plain\r\n\r\n"
- "%s", code, error.c_str());
- server_data_ = ByteString(error_msg, false);
+ SetClientResponse(code, "ERROR", "text/plain", error);
state_ = kStateFlushResponse;
StartTransmit();
}
+// Create an HTTP response message to be sent to the client.
+void HTTPProxy::SetClientResponse(int code, const string &type,
+ const string &content_type,
+ const string &message) {
+ string content_line;
+ if (!message.empty() && !content_type.empty()) {
+ content_line = StringPrintf("Content-Type: %s\r\n", content_type.c_str());
+ }
+ string response = StringPrintf("HTTP/1.1 %d %s\r\n"
+ "%s\r\n"
+ "%s", code, type.c_str(),
+ content_line.c_str(),
+ message.c_str());
+ server_data_ = ByteString(response, false);
+}
+
// Start a timeout for "the next event". This timeout augments the overall
// transaction timeout to make sure there is some activity occurring at
// reasonable intervals.
@@ -587,6 +624,7 @@
client_socket_ = -1;
}
client_headers_.clear();
+ client_method_.clear();
client_version_.clear();
server_port_ = kDefaultServerPort;
write_server_handler_.reset();
diff --git a/http_proxy.h b/http_proxy.h
index 3b9c334..1541917 100644
--- a/http_proxy.h
+++ b/http_proxy.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -83,6 +83,8 @@
// Timeout for whole transaction.
static const int kTransactionTimeoutSeconds;
+ static const char kHTTPMethodConnect[];
+ static const char kHTTPMethodTerminator[];
static const char kHTTPURLDelimiters[];
static const char kHTTPURLPrefix[];
static const char kHTTPVersionPrefix[];
@@ -97,9 +99,13 @@
bool ProcessLastHeaderLine();
bool ReadClientHeaders(InputData *data);
bool ReadClientHostname(std::string *header);
+ bool ReadClientHTTPMethod(std::string *header);
bool ReadClientHTTPVersion(std::string *header);
void ReadFromClient(InputData *data);
void ReadFromServer(InputData *data);
+ void SetClientResponse(int code, const std::string &type,
+ const std::string &content_type,
+ const std::string &message);
void SendClientError(int code, const std::string &error);
void StartIdleTimeout();
void StartReceive();
@@ -131,6 +137,7 @@
// State held while proxy is started and a transaction is active.
int client_socket_;
+ std::string client_method_;
std::string client_version_;
int server_port_;
int server_socket_;
diff --git a/http_proxy_unittest.cc b/http_proxy_unittest.cc
index d7f4ddb..ae4cd75 100644
--- a/http_proxy_unittest.cc
+++ b/http_proxy_unittest.cc
@@ -1,4 +1,4 @@
-// Copyright (c) 2011 The Chromium OS Authors. All rights reserved.
+// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
@@ -41,13 +41,17 @@
namespace shill {
namespace {
-const char kBadHeader[] = "BLAH\r\n";
+const char kBadHeaderMissingURL[] = "BLAH\r\n";
+const char kBadHeaderMissingVersion[] = "BLAH http://hostname\r\n";
const char kBadHostnameLine[] = "GET HTTP/1.1 http://hostname\r\n";
const char kBasicGetHeader[] = "GET / HTTP/1.1\r\n";
const char kBasicGetHeaderWithURL[] =
"GET http://www.chromium.org/ HTTP/1.1\r\n";
const char kBasicGetHeaderWithURLNoTrailingSlash[] =
"GET http://www.chromium.org HTTP/1.1\r\n";
+const char kConnectQuery[] =
+ "CONNECT 10.10.10.10:443 HTTP/1.1\r\n"
+ "Host: 10.10.10.10:443\r\n\r\n";
const char kQueryTemplate[] = "GET %s HTTP/%s\r\n%s"
"User-Agent: Mozilla/5.0 (X11; CrOS i686 1299.0.2011) "
"AppleWebKit/535.8 (KHTML, like Gecko) Chrome/17.0.936.0 Safari/535.8\r\n"
@@ -75,6 +79,7 @@
const int kServerFD = 10204;
const int kClientFD = 10205;
const int kServerPort = 40506;
+const int kConnectPort = 443;
} // namespace {}
MATCHER_P(IsIPAddress, address, "") {
@@ -243,13 +248,16 @@
void ExpectTransactionTimeout() {
ExpectTimeout(HTTPProxy::kTransactionTimeoutSeconds);
}
+ void ExpectInClientResponse(const string &response_data) {
+ string server_data(reinterpret_cast<char *>(proxy_.server_data_.GetData()),
+ proxy_.server_data_.GetLength());
+ EXPECT_NE(string::npos, server_data.find(response_data));
+ }
void ExpectClientError(int code, const string &error) {
EXPECT_EQ(HTTPProxy::kStateFlushResponse, GetProxyState());
string status_line = StringPrintf("HTTP/1.1 %d ERROR", code);
- string server_data(reinterpret_cast<char *>(proxy_.server_data_.GetData()),
- proxy_.server_data_.GetLength());
- EXPECT_NE(string::npos, server_data.find(status_line));
- EXPECT_NE(string::npos, server_data.find(error));
+ ExpectInClientResponse(status_line);
+ ExpectInClientResponse(error);
}
void ExpectClientInternalError() {
ExpectClientError(500, HTTPProxy::kInternalErrorMsg);
@@ -281,12 +289,15 @@
.WillOnce(DoAll(Invoke(this, &HTTPProxyTest::InvokeSyncConnect),
Return(true)));
}
- void ExpectClientResult() {
+ void ExpectClientData() {
EXPECT_CALL(dispatcher(),
CreateReadyHandler(kClientFD,
IOHandler::kModeOutput,
proxy_.write_client_callback_.get()))
.WillOnce(ReturnNew<IOHandler>());
+ }
+ void ExpectClientResult() {
+ ExpectClientData();
ExpectInputTimeout();
}
void ExpectServerInput() {
@@ -527,10 +538,17 @@
EXPECT_EQ(HTTPProxy::kStateWaitConnection, GetProxyState());
}
-TEST_F(HTTPProxyTest, ReadBadFirstLine) {
+TEST_F(HTTPProxyTest, ReadMissingURL) {
SetupClient();
ExpectClientResult();
- ReadFromClient(kBadHeader);
+ ReadFromClient(kBadHeaderMissingURL);
+ ExpectClientError(501, "Server could not parse HTTP method");
+}
+
+TEST_F(HTTPProxyTest, ReadMissingVersion) {
+ SetupClient();
+ ExpectClientResult();
+ ReadFromClient(kBadHeaderMissingVersion);
ExpectClientError(501, "Server only accepts HTTP/1.x requests");
}
@@ -680,6 +698,18 @@
SetupConnectComplete();
}
+TEST_F(HTTPProxyTest, HTTPConnectMethod) {
+ SetupClient();
+ ExpectAsyncConnect(kServerAddress, kConnectPort, true);
+ ExpectConnectTimeout();
+ ExpectRouteRequest();
+ ReadFromClient(kConnectQuery);
+ ExpectRepeatedInputTimeout();
+ ExpectClientData();
+ OnConnectCompletion(true, kServerFD);
+ ExpectInClientResponse("HTTP/1.1 200 OK\r\n\r\n");
+}
+
TEST_F(HTTPProxyTest, TunnelData) {
SetupConnectComplete();