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();