blob: fd96ca125de99c148b0d79afe9d8873f73d96dee [file] [log] [blame]
Andreea Costinase45d54b2020-03-10 09:21:14 +01001// Copyright 2020 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "system-proxy/proxy_connect_job.h"
6
7#include <netinet/in.h>
8#include <sys/socket.h>
9#include <sys/types.h>
10
11#include <gmock/gmock.h>
12#include <gtest/gtest.h>
13#include <utility>
14
Andreea Costinase45d54b2020-03-10 09:21:14 +010015#include <base/bind.h>
16#include <base/bind_helpers.h>
17#include <base/callback_helpers.h>
18#include <base/files/file_util.h>
19#include <base/files/scoped_file.h>
Qijiang Fan34014672020-07-20 16:05:38 +090020#include <base/task/single_thread_task_executor.h>
Andreea Costinas08a5d182020-04-29 22:12:47 +020021#include <base/test/test_mock_time_task_runner.h>
Andreea Costinase45d54b2020-03-10 09:21:14 +010022#include <brillo/message_loops/base_message_loop.h>
Garrick Evanscd8c2972020-04-14 14:35:52 +090023#include <chromeos/patchpanel/socket.h>
24#include <chromeos/patchpanel/socket_forwarder.h>
Andreea Costinase45d54b2020-03-10 09:21:14 +010025
26#include "bindings/worker_common.pb.h"
27#include "system-proxy/protobuf_util.h"
Andreea Costinas054fbb52020-06-12 20:46:22 +020028#include "system-proxy/test_http_server.h"
Andreea Costinase45d54b2020-03-10 09:21:14 +010029
30namespace {
Andreea Costinas054fbb52020-06-12 20:46:22 +020031
32constexpr char kProxyServerUrl[] = "http://127.0.0.1:3128";
Andreea Costinasbb2aa022020-06-13 00:03:23 +020033constexpr char kCredentials[] = "username:pwd";
Andreea Costinas054fbb52020-06-12 20:46:22 +020034constexpr char kValidConnectRequest[] =
35 "CONNECT www.example.server.com:443 HTTP/1.1\r\n\r\n";
Andreea Costinase45d54b2020-03-10 09:21:14 +010036} // namespace
37
38namespace system_proxy {
39
40using ::testing::_;
41using ::testing::Return;
42
43class ProxyConnectJobTest : public ::testing::Test {
44 public:
Andreea Costinasbb2aa022020-06-13 00:03:23 +020045 struct HttpAuthEntry {
46 HttpAuthEntry(const std::string& origin,
47 const std::string& scheme,
48 const std::string& realm,
49 const std::string& credentials)
50 : origin(origin),
51 scheme(scheme),
52 realm(realm),
53 credentials(credentials) {}
54 std::string origin;
55 std::string scheme;
56 std::string realm;
57 std::string credentials;
58 };
Andreea Costinase45d54b2020-03-10 09:21:14 +010059 ProxyConnectJobTest() = default;
60 ProxyConnectJobTest(const ProxyConnectJobTest&) = delete;
61 ProxyConnectJobTest& operator=(const ProxyConnectJobTest&) = delete;
62 ~ProxyConnectJobTest() = default;
63
64 void SetUp() override {
65 int fds[2];
66 ASSERT_NE(-1,
67 socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
68 0 /* protocol */, fds));
69 cros_client_socket_ =
Garrick Evans3388a032020-03-24 11:25:55 +090070 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[1]));
Andreea Costinase45d54b2020-03-10 09:21:14 +010071
72 connect_job_ = std::make_unique<ProxyConnectJob>(
Garrick Evans3388a032020-03-24 11:25:55 +090073 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[0])), "",
Andreea Costinase45d54b2020-03-10 09:21:14 +010074 base::BindOnce(&ProxyConnectJobTest::ResolveProxy,
75 base::Unretained(this)),
Andreea Costinasbb2aa022020-06-13 00:03:23 +020076 base::BindOnce(&ProxyConnectJobTest::FetchCredentialsFromCache,
77 base::Unretained(this)),
Andreea Costinase45d54b2020-03-10 09:21:14 +010078 base::BindOnce(&ProxyConnectJobTest::OnConnectionSetupFinished,
79 base::Unretained(this)));
Andreea Costinase45d54b2020-03-10 09:21:14 +010080 }
Andreea Costinasbb2aa022020-06-13 00:03:23 +020081 void AddHttpAuthEntry(const std::string& origin,
82 const std::string& scheme,
83 const std::string& realm,
84 const std::string& credentials) {
85 http_auth_cache_.push_back(
86 HttpAuthEntry(origin, scheme, realm, credentials));
87 }
88
89 bool AuthRequested() { return auth_requested_; }
Andreea Costinase45d54b2020-03-10 09:21:14 +010090
91 protected:
92 void ResolveProxy(
93 const std::string& target_url,
94 base::OnceCallback<void(const std::list<std::string>&)> callback) {
Andreea Costinas054fbb52020-06-12 20:46:22 +020095 std::move(callback).Run({remote_proxy_url_});
Andreea Costinase45d54b2020-03-10 09:21:14 +010096 }
97
Andreea Costinasbb2aa022020-06-13 00:03:23 +020098 void FetchCredentialsFromCache(
99 const std::string& proxy_url,
100 const std::string& scheme,
101 const std::string& realm,
102 base::OnceCallback<void(const std::string&)> callback) {
103 ASSERT_FALSE(auth_requested_);
104 auth_requested_ = true;
105 for (const auto& auth_entry : http_auth_cache_) {
106 if (auth_entry.origin == proxy_url && auth_entry.realm == realm &&
107 auth_entry.scheme == scheme) {
108 std::move(callback).Run(auth_entry.credentials);
109 return;
110 }
111 }
112 std::move(callback).Run(/* credentials = */ "");
113 }
114
Andreea Costinase45d54b2020-03-10 09:21:14 +0100115 void OnConnectionSetupFinished(
Garrick Evans3388a032020-03-24 11:25:55 +0900116 std::unique_ptr<patchpanel::SocketForwarder> fwd,
Andreea Costinase45d54b2020-03-10 09:21:14 +0100117 ProxyConnectJob* connect_job) {
118 ASSERT_EQ(connect_job, connect_job_.get());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200119 if (fwd) {
120 forwarder_created_ = true;
121 }
Andreea Costinase45d54b2020-03-10 09:21:14 +0100122 }
123
Andreea Costinas054fbb52020-06-12 20:46:22 +0200124 std::string remote_proxy_url_ = kProxyServerUrl;
125 bool forwarder_created_ = false;
Andreea Costinase45d54b2020-03-10 09:21:14 +0100126 std::unique_ptr<ProxyConnectJob> connect_job_;
Qijiang Fan34014672020-07-20 16:05:38 +0900127 base::SingleThreadTaskExecutor task_executor_{base::MessagePumpType::IO};
128 std::unique_ptr<brillo::BaseMessageLoop> brillo_loop_{
129 std::make_unique<brillo::BaseMessageLoop>(task_executor_.task_runner())};
Garrick Evans3388a032020-03-24 11:25:55 +0900130 std::unique_ptr<patchpanel::Socket> cros_client_socket_;
Andreea Costinas08a5d182020-04-29 22:12:47 +0200131
132 private:
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200133 std::vector<HttpAuthEntry> http_auth_cache_;
134 bool auth_requested_ = false;
Andreea Costinas08a5d182020-04-29 22:12:47 +0200135 FRIEND_TEST(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100136};
137
138TEST_F(ProxyConnectJobTest, SuccessfulConnection) {
Andreea Costinas054fbb52020-06-12 20:46:22 +0200139 HttpTestServer http_test_server;
140 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
141 http_test_server.Start();
142 remote_proxy_url_ = http_test_server.GetUrl();
143
Andreea Costinas08a5d182020-04-29 22:12:47 +0200144 connect_job_->Start();
Andreea Costinas054fbb52020-06-12 20:46:22 +0200145 cros_client_socket_->SendTo(kValidConnectRequest,
146 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900147 brillo_loop_->RunOnce(false);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100148
Andreea Costinasa2246592020-04-12 23:24:01 +0200149 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100150 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200151 EXPECT_EQ(http_test_server.GetUrl(), connect_job_->proxy_servers_.front());
152 EXPECT_TRUE(forwarder_created_);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200153 ASSERT_FALSE(AuthRequested());
Andreea Costinase45d54b2020-03-10 09:21:14 +0100154}
155
Andreea Costinasf90a4c02020-06-12 22:30:51 +0200156TEST_F(ProxyConnectJobTest, TunnelFailedBadGatewayFromRemote) {
157 HttpTestServer http_test_server;
158 http_test_server.AddHttpConnectReply(
159 HttpTestServer::HttpConnectReply::kBadGateway);
160 http_test_server.Start();
161 remote_proxy_url_ = http_test_server.GetUrl();
162
163 connect_job_->Start();
164 cros_client_socket_->SendTo(kValidConnectRequest,
165 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900166 brillo_loop_->RunOnce(false);
Andreea Costinasf90a4c02020-06-12 22:30:51 +0200167
168 EXPECT_FALSE(forwarder_created_);
169
170 std::string expected_server_reply =
171 "HTTP/1.1 502 Error creating tunnel - Origin: local proxy\r\n\r\n";
172 std::vector<char> buf(expected_server_reply.size());
173 ASSERT_TRUE(cros_client_socket_->RecvFrom(buf.data(), buf.size()));
174 std::string actual_server_reply(buf.data(), buf.size());
175
176 EXPECT_EQ(expected_server_reply, actual_server_reply);
177}
178
Andreea Costinased3f9782020-05-20 17:09:46 +0200179TEST_F(ProxyConnectJobTest, SuccessfulConnectionAltEnding) {
Andreea Costinas054fbb52020-06-12 20:46:22 +0200180 HttpTestServer http_test_server;
181 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
182 http_test_server.Start();
183 remote_proxy_url_ = http_test_server.GetUrl();
184
Andreea Costinased3f9782020-05-20 17:09:46 +0200185 connect_job_->Start();
Andreea Costinas054fbb52020-06-12 20:46:22 +0200186 char validConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n\n";
Andreea Costinased3f9782020-05-20 17:09:46 +0200187 cros_client_socket_->SendTo(validConnRequest, std::strlen(validConnRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900188 brillo_loop_->RunOnce(false);
Andreea Costinased3f9782020-05-20 17:09:46 +0200189
190 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
191 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200192 EXPECT_EQ(http_test_server.GetUrl(), connect_job_->proxy_servers_.front());
193 EXPECT_TRUE(forwarder_created_);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200194 ASSERT_FALSE(AuthRequested());
Andreea Costinased3f9782020-05-20 17:09:46 +0200195}
196
Andreea Costinase45d54b2020-03-10 09:21:14 +0100197TEST_F(ProxyConnectJobTest, BadHttpRequestWrongMethod) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200198 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100199 char badConnRequest[] = "GET www.example.server.com:443 HTTP/1.1\r\n\r\n";
200 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900201 brillo_loop_->RunOnce(false);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100202
203 EXPECT_EQ("", connect_job_->target_url_);
204 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
205 const std::string expected_http_response =
206 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
207 std::vector<char> buf(expected_http_response.size());
208 ASSERT_TRUE(
209 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
210 std::string actual_response(buf.data(), buf.size());
211 EXPECT_EQ(expected_http_response, actual_response);
212}
213
214TEST_F(ProxyConnectJobTest, BadHttpRequestNoEmptyLine) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200215 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100216 // No empty line after http message.
217 char badConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n";
218 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900219 brillo_loop_->RunOnce(false);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100220
221 EXPECT_EQ("", connect_job_->target_url_);
222 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
223 const std::string expected_http_response =
224 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
225 std::vector<char> buf(expected_http_response.size());
226 ASSERT_TRUE(
227 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
228 std::string actual_response(buf.data(), buf.size());
229 EXPECT_EQ(expected_http_response, actual_response);
230}
231
Andreea Costinas08a5d182020-04-29 22:12:47 +0200232TEST_F(ProxyConnectJobTest, WaitClientConnectTimeout) {
233 // Add a TaskRunner where we can control time.
234 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
235 new base::TestMockTimeTaskRunner()};
Qijiang Fan34014672020-07-20 16:05:38 +0900236 brillo_loop_ = nullptr;
237 brillo_loop_ = std::make_unique<brillo::BaseMessageLoop>(task_runner);
Andreea Costinas08a5d182020-04-29 22:12:47 +0200238 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
239
240 connect_job_->Start();
241
242 EXPECT_EQ(1, task_runner->GetPendingTaskCount());
243 // Move the time ahead so that the client connection timeout callback is
244 // triggered.
245 task_runner->FastForwardBy(task_runner->NextPendingTaskDelay());
246
247 const std::string expected_http_response =
248 "HTTP/1.1 408 Request Timeout - Origin: local proxy\r\n\r\n";
249 std::vector<char> buf(expected_http_response.size());
250 ASSERT_TRUE(
251 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
252 std::string actual_response(buf.data(), buf.size());
253
254 EXPECT_EQ(expected_http_response, actual_response);
255}
256
257// Check that the client connect timeout callback is not fired if the owning
258// proxy connect job is destroyed.
259TEST_F(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled) {
260 // Add a TaskRunner where we can control time.
261 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
262 new base::TestMockTimeTaskRunner()};
Qijiang Fan34014672020-07-20 16:05:38 +0900263 brillo_loop_ = nullptr;
264 brillo_loop_ = std::make_unique<brillo::BaseMessageLoop>(task_runner);
Andreea Costinas08a5d182020-04-29 22:12:47 +0200265 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
266
267 // Create a proxy connect job and start the client connect timeout counter.
268 {
269 int fds[2];
270 ASSERT_NE(-1,
271 socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
272 0 /* protocol */, fds));
273 auto client_socket =
274 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[1]));
275
276 auto connect_job = std::make_unique<ProxyConnectJob>(
277 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[0])), "",
278 base::BindOnce(&ProxyConnectJobTest::ResolveProxy,
279 base::Unretained(this)),
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200280 base::BindOnce(&ProxyConnectJobTest::FetchCredentialsFromCache,
281 base::Unretained(this)),
Andreea Costinas08a5d182020-04-29 22:12:47 +0200282 base::BindOnce(&ProxyConnectJobTest::OnConnectionSetupFinished,
283 base::Unretained(this)));
284 // Post the timeout task.
285 connect_job->Start();
286 EXPECT_TRUE(task_runner->HasPendingTask());
287 }
288 // Check that the task was canceled.
289 EXPECT_FALSE(task_runner->HasPendingTask());
290}
291
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200292// Test that the the CONNECT request is sent again after acquiring credentials.
293TEST_F(ProxyConnectJobTest, ResendWithCredentials) {
294 // Start the test server
295 HttpTestServer http_test_server;
296 http_test_server.AddHttpConnectReply(
297 HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
298 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
299 http_test_server.Start();
300 remote_proxy_url_ = http_test_server.GetUrl();
301
302 AddHttpAuthEntry(remote_proxy_url_, "Basic", "\"My Proxy\"", kCredentials);
303 connect_job_->Start();
304
305 cros_client_socket_->SendTo(kValidConnectRequest,
306 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900307 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200308
309 ASSERT_TRUE(AuthRequested());
310 EXPECT_TRUE(forwarder_created_);
311 EXPECT_EQ(kCredentials, connect_job_->credentials_);
312 EXPECT_EQ(200, connect_job_->http_response_code_);
313}
314
315// Test that the proxy auth required status is forwarded to the client if
316// credentials are missing.
317TEST_F(ProxyConnectJobTest, NoCredentials) {
318 // Start the test server
319 HttpTestServer http_test_server;
320 http_test_server.AddHttpConnectReply(
321 HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
322 http_test_server.Start();
323 remote_proxy_url_ = http_test_server.GetUrl();
324
325 connect_job_->Start();
326
327 cros_client_socket_->SendTo(kValidConnectRequest,
328 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900329 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200330
331 ASSERT_TRUE(AuthRequested());
332 EXPECT_EQ("", connect_job_->credentials_);
333 EXPECT_EQ(407, connect_job_->http_response_code_);
334}
335
336// Test that the proxy auth required status is forwarded to the client if the
337// server chose Kerberos as an authentication method.
338TEST_F(ProxyConnectJobTest, KerberosAuth) {
339 // Start the test server
340 HttpTestServer http_test_server;
341 http_test_server.AddHttpConnectReply(
342 HttpTestServer::HttpConnectReply::kAuthRequiredKerberos);
343 http_test_server.Start();
344 remote_proxy_url_ = http_test_server.GetUrl();
345
346 connect_job_->Start();
347
348 cros_client_socket_->SendTo(kValidConnectRequest,
349 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900350 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200351
352 ASSERT_FALSE(AuthRequested());
353 EXPECT_EQ("", connect_job_->credentials_);
354 EXPECT_EQ(407, connect_job_->http_response_code_);
355}
356
Andreea Costinase45d54b2020-03-10 09:21:14 +0100357} // namespace system_proxy