blob: 296891588809c6098417bedcbe06cc9956fbd176 [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
Andreea Costinasbb2aa022020-06-13 00:03:23 +020032constexpr char kCredentials[] = "username:pwd";
Andreea Costinas054fbb52020-06-12 20:46:22 +020033constexpr char kValidConnectRequest[] =
34 "CONNECT www.example.server.com:443 HTTP/1.1\r\n\r\n";
Andreea Costinase45d54b2020-03-10 09:21:14 +010035} // namespace
36
37namespace system_proxy {
38
39using ::testing::_;
40using ::testing::Return;
41
42class ProxyConnectJobTest : public ::testing::Test {
43 public:
Andreea Costinasbb2aa022020-06-13 00:03:23 +020044 struct HttpAuthEntry {
45 HttpAuthEntry(const std::string& origin,
46 const std::string& scheme,
47 const std::string& realm,
48 const std::string& credentials)
49 : origin(origin),
50 scheme(scheme),
51 realm(realm),
52 credentials(credentials) {}
53 std::string origin;
54 std::string scheme;
55 std::string realm;
56 std::string credentials;
57 };
Andreea Costinase45d54b2020-03-10 09:21:14 +010058 ProxyConnectJobTest() = default;
59 ProxyConnectJobTest(const ProxyConnectJobTest&) = delete;
60 ProxyConnectJobTest& operator=(const ProxyConnectJobTest&) = delete;
61 ~ProxyConnectJobTest() = default;
62
63 void SetUp() override {
64 int fds[2];
65 ASSERT_NE(-1,
66 socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
67 0 /* protocol */, fds));
68 cros_client_socket_ =
Garrick Evans3388a032020-03-24 11:25:55 +090069 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[1]));
Andreea Costinase45d54b2020-03-10 09:21:14 +010070
Andreea-Elena Costinasfae5c152020-09-28 18:18:31 +000071 connect_job_ = std::make_unique<ProxyConnectJob>(
Garrick Evans3388a032020-03-24 11:25:55 +090072 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[0])), "",
Andreea Costinase45d54b2020-03-10 09:21:14 +010073 base::BindOnce(&ProxyConnectJobTest::ResolveProxy,
74 base::Unretained(this)),
Andreea Costinased9e6122020-08-12 12:06:19 +020075 base::BindRepeating(&ProxyConnectJobTest::OnAuthCredentialsRequired,
76 base::Unretained(this)),
Andreea Costinase45d54b2020-03-10 09:21:14 +010077 base::BindOnce(&ProxyConnectJobTest::OnConnectionSetupFinished,
78 base::Unretained(this)));
Andreea Costinase45d54b2020-03-10 09:21:14 +010079 }
80
81 protected:
Andreea Costinasa8bc59c2020-09-18 13:49:13 +020082 virtual void OnConnectionSetupFinished(
83 std::unique_ptr<patchpanel::SocketForwarder> fwd,
84 ProxyConnectJob* connect_job) {}
85 virtual void ResolveProxy(
Andreea Costinase45d54b2020-03-10 09:21:14 +010086 const std::string& target_url,
87 base::OnceCallback<void(const std::list<std::string>&)> callback) {
Andreea Costinasa8bc59c2020-09-18 13:49:13 +020088 std::move(callback).Run({});
Andreea Costinase45d54b2020-03-10 09:21:14 +010089 }
Andreea Costinasa8bc59c2020-09-18 13:49:13 +020090 virtual void OnAuthCredentialsRequired(
Andreea Costinasbb2aa022020-06-13 00:03:23 +020091 const std::string& proxy_url,
92 const std::string& scheme,
93 const std::string& realm,
Andreea Costinased9e6122020-08-12 12:06:19 +020094 const std::string& bad_credentials,
95 base::RepeatingCallback<void(const std::string&)> callback) {
Andreea Costinasa8bc59c2020-09-18 13:49:13 +020096 std::move(callback).Run(/* credentials = */ "");
Andreea Costinasbb2aa022020-06-13 00:03:23 +020097 }
98
Andreea Costinase45d54b2020-03-10 09:21:14 +010099 std::unique_ptr<ProxyConnectJob> connect_job_;
Qijiang Fan34014672020-07-20 16:05:38 +0900100 base::SingleThreadTaskExecutor task_executor_{base::MessagePumpType::IO};
101 std::unique_ptr<brillo::BaseMessageLoop> brillo_loop_{
102 std::make_unique<brillo::BaseMessageLoop>(task_executor_.task_runner())};
Garrick Evans3388a032020-03-24 11:25:55 +0900103 std::unique_ptr<patchpanel::Socket> cros_client_socket_;
Andreea Costinas08a5d182020-04-29 22:12:47 +0200104
Andreea Costinas08a5d182020-04-29 22:12:47 +0200105 FRIEND_TEST(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100106};
107
Andreea Costinase45d54b2020-03-10 09:21:14 +0100108TEST_F(ProxyConnectJobTest, BadHttpRequestWrongMethod) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200109 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100110 char badConnRequest[] = "GET www.example.server.com:443 HTTP/1.1\r\n\r\n";
111 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900112 brillo_loop_->RunOnce(false);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100113
114 EXPECT_EQ("", connect_job_->target_url_);
115 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
116 const std::string expected_http_response =
117 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
118 std::vector<char> buf(expected_http_response.size());
119 ASSERT_TRUE(
120 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
121 std::string actual_response(buf.data(), buf.size());
122 EXPECT_EQ(expected_http_response, actual_response);
123}
124
125TEST_F(ProxyConnectJobTest, BadHttpRequestNoEmptyLine) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200126 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100127 // No empty line after http message.
128 char badConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n";
129 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900130 brillo_loop_->RunOnce(false);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100131
132 EXPECT_EQ("", connect_job_->target_url_);
133 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
134 const std::string expected_http_response =
135 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
136 std::vector<char> buf(expected_http_response.size());
137 ASSERT_TRUE(
138 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
139 std::string actual_response(buf.data(), buf.size());
140 EXPECT_EQ(expected_http_response, actual_response);
141}
142
Andreea Costinas08a5d182020-04-29 22:12:47 +0200143TEST_F(ProxyConnectJobTest, WaitClientConnectTimeout) {
144 // Add a TaskRunner where we can control time.
145 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
146 new base::TestMockTimeTaskRunner()};
Qijiang Fan34014672020-07-20 16:05:38 +0900147 brillo_loop_ = nullptr;
148 brillo_loop_ = std::make_unique<brillo::BaseMessageLoop>(task_runner);
Andreea Costinas08a5d182020-04-29 22:12:47 +0200149 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
150
151 connect_job_->Start();
152
153 EXPECT_EQ(1, task_runner->GetPendingTaskCount());
154 // Move the time ahead so that the client connection timeout callback is
155 // triggered.
156 task_runner->FastForwardBy(task_runner->NextPendingTaskDelay());
157
158 const std::string expected_http_response =
159 "HTTP/1.1 408 Request Timeout - Origin: local proxy\r\n\r\n";
160 std::vector<char> buf(expected_http_response.size());
161 ASSERT_TRUE(
162 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
163 std::string actual_response(buf.data(), buf.size());
164
165 EXPECT_EQ(expected_http_response, actual_response);
166}
167
168// Check that the client connect timeout callback is not fired if the owning
169// proxy connect job is destroyed.
170TEST_F(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled) {
171 // Add a TaskRunner where we can control time.
172 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
173 new base::TestMockTimeTaskRunner()};
Qijiang Fan34014672020-07-20 16:05:38 +0900174 brillo_loop_ = nullptr;
175 brillo_loop_ = std::make_unique<brillo::BaseMessageLoop>(task_runner);
Andreea Costinas08a5d182020-04-29 22:12:47 +0200176 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
177
178 // Create a proxy connect job and start the client connect timeout counter.
179 {
180 int fds[2];
181 ASSERT_NE(-1,
182 socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
183 0 /* protocol */, fds));
184 auto client_socket =
185 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[1]));
186
187 auto connect_job = std::make_unique<ProxyConnectJob>(
188 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[0])), "",
189 base::BindOnce(&ProxyConnectJobTest::ResolveProxy,
190 base::Unretained(this)),
Andreea Costinased9e6122020-08-12 12:06:19 +0200191 base::BindRepeating(&ProxyConnectJobTest::OnAuthCredentialsRequired,
192 base::Unretained(this)),
Andreea Costinas08a5d182020-04-29 22:12:47 +0200193 base::BindOnce(&ProxyConnectJobTest::OnConnectionSetupFinished,
194 base::Unretained(this)));
195 // Post the timeout task.
196 connect_job->Start();
197 EXPECT_TRUE(task_runner->HasPendingTask());
198 }
199 // Check that the task was canceled.
200 EXPECT_FALSE(task_runner->HasPendingTask());
201}
202
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200203class HttpServerProxyConnectJobTest : public ProxyConnectJobTest {
204 public:
205 HttpServerProxyConnectJobTest() = default;
206 HttpServerProxyConnectJobTest(const HttpServerProxyConnectJobTest&) = delete;
207 HttpServerProxyConnectJobTest& operator=(
208 const HttpServerProxyConnectJobTest&) = delete;
209 ~HttpServerProxyConnectJobTest() = default;
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200210
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200211 protected:
212 HttpTestServer http_test_server_;
213
214 std::vector<HttpAuthEntry> http_auth_cache_;
215 bool auth_requested_ = false;
216
217 void AddHttpAuthEntry(const std::string& origin,
218 const std::string& scheme,
219 const std::string& realm,
220 const std::string& credentials) {
221 http_auth_cache_.push_back(
222 HttpAuthEntry(origin, scheme, realm, credentials));
223 }
224
225 bool AuthRequested() { return auth_requested_; }
226
227 void AddServerReply(HttpTestServer::HttpConnectReply reply) {
228 http_test_server_.AddHttpConnectReply(reply);
229 }
230
231 void ResolveProxy(const std::string& target_url,
232 base::OnceCallback<void(const std::list<std::string>&)>
233 callback) override {
234 // Return the URL of the test proxy.
235 std::move(callback).Run({http_test_server_.GetUrl()});
236 }
237
238 void OnAuthCredentialsRequired(
239 const std::string& proxy_url,
240 const std::string& scheme,
241 const std::string& realm,
242 const std::string& bad_credentials,
243 base::RepeatingCallback<void(const std::string&)> callback) override {
244 auth_requested_ = true;
245 for (const auto& auth_entry : http_auth_cache_) {
246 if (auth_entry.origin == proxy_url && auth_entry.realm == realm &&
247 auth_entry.scheme == scheme) {
248 std::move(callback).Run(auth_entry.credentials);
249 return;
250 }
251 }
252 if (invoke_authentication_callback_) {
253 std::move(callback).Run(/* credentials = */ "");
254 }
255 }
256 void OnConnectionSetupFinished(
257 std::unique_ptr<patchpanel::SocketForwarder> fwd,
258 ProxyConnectJob* connect_job) override {
259 ASSERT_EQ(connect_job, connect_job_.get());
260 if (fwd) {
261 forwarder_created_ = true;
262
263 brillo_loop_->RunOnce(false);
264
265 fwd.reset();
266 }
267 }
268
269 bool forwarder_created_ = false;
270 // Used to simulate time-outs while waiting for credentials from the Browser.
271 bool invoke_authentication_callback_ = true;
272};
273
274TEST_F(HttpServerProxyConnectJobTest, SuccessfulConnection) {
275 AddServerReply(HttpTestServer::HttpConnectReply::kOk);
276 http_test_server_.Start();
277
278 connect_job_->Start();
279 cros_client_socket_->SendTo(kValidConnectRequest,
280 std::strlen(kValidConnectRequest));
281 brillo_loop_->RunOnce(false);
282 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
283 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
284 EXPECT_EQ(http_test_server_.GetUrl(), connect_job_->proxy_servers_.front());
285 EXPECT_TRUE(forwarder_created_);
286}
287
288TEST_F(HttpServerProxyConnectJobTest, TunnelFailedBadGatewayFromRemote) {
289 AddServerReply(HttpTestServer::HttpConnectReply::kBadGateway);
290 http_test_server_.Start();
291
292 connect_job_->Start();
293 cros_client_socket_->SendTo(kValidConnectRequest,
294 std::strlen(kValidConnectRequest));
295 brillo_loop_->RunOnce(false);
296 EXPECT_FALSE(forwarder_created_);
297
298 std::string expected_server_reply =
299 "HTTP/1.1 502 Error creating tunnel - Origin: local proxy\r\n\r\n";
300 std::vector<char> buf(expected_server_reply.size());
301
302 ASSERT_TRUE(cros_client_socket_->RecvFrom(buf.data(), buf.size()));
303 std::string actual_server_reply(buf.data(), buf.size());
304 EXPECT_EQ(expected_server_reply, actual_server_reply);
305}
306
307TEST_F(HttpServerProxyConnectJobTest, SuccessfulConnectionAltEnding) {
308 AddServerReply(HttpTestServer::HttpConnectReply::kOk);
309 http_test_server_.Start();
310
311 connect_job_->Start();
312 char validConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n\n";
313 cros_client_socket_->SendTo(validConnRequest, std::strlen(validConnRequest));
314 brillo_loop_->RunOnce(false);
315
316 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
317 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
318 EXPECT_EQ(http_test_server_.GetUrl(), connect_job_->proxy_servers_.front());
319 EXPECT_TRUE(forwarder_created_);
320 ASSERT_FALSE(AuthRequested());
321}
322
323// Test that the the CONNECT request is sent again after acquiring credentials.
324TEST_F(HttpServerProxyConnectJobTest, ResendWithCredentials) {
325 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
326 AddServerReply(HttpTestServer::HttpConnectReply::kOk);
327 http_test_server_.Start();
328
329 AddHttpAuthEntry(http_test_server_.GetUrl(), "Basic", "\"My Proxy\"",
330 kCredentials);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200331 connect_job_->Start();
332
333 cros_client_socket_->SendTo(kValidConnectRequest,
334 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900335 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200336
337 ASSERT_TRUE(AuthRequested());
338 EXPECT_TRUE(forwarder_created_);
339 EXPECT_EQ(kCredentials, connect_job_->credentials_);
340 EXPECT_EQ(200, connect_job_->http_response_code_);
341}
342
343// Test that the proxy auth required status is forwarded to the client if
344// credentials are missing.
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200345TEST_F(HttpServerProxyConnectJobTest, NoCredentials) {
346 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
347 http_test_server_.Start();
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200348 connect_job_->Start();
349
350 cros_client_socket_->SendTo(kValidConnectRequest,
351 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900352 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200353
354 ASSERT_TRUE(AuthRequested());
355 EXPECT_EQ("", connect_job_->credentials_);
356 EXPECT_EQ(407, connect_job_->http_response_code_);
357}
358
359// Test that the proxy auth required status is forwarded to the client if the
360// server chose Kerberos as an authentication method.
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200361TEST_F(HttpServerProxyConnectJobTest, KerberosAuth) {
362 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredKerberos);
363 http_test_server_.Start();
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200364
365 connect_job_->Start();
366
367 cros_client_socket_->SendTo(kValidConnectRequest,
368 std::strlen(kValidConnectRequest));
Qijiang Fan34014672020-07-20 16:05:38 +0900369 brillo_loop_->RunOnce(false);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200370
371 ASSERT_FALSE(AuthRequested());
372 EXPECT_EQ("", connect_job_->credentials_);
373 EXPECT_EQ(407, connect_job_->http_response_code_);
374}
375
Andreea Costinased9e6122020-08-12 12:06:19 +0200376// Test that the connection times out while waiting for credentials from the
377// Browser.
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200378TEST_F(HttpServerProxyConnectJobTest, AuthenticationTimeout) {
Andreea Costinased9e6122020-08-12 12:06:19 +0200379 // Add a TaskRunner where we can control time.
380 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
381 new base::TestMockTimeTaskRunner()};
382 brillo_loop_ = nullptr;
383 brillo_loop_ = std::make_unique<brillo::BaseMessageLoop>(task_runner);
384 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
385
386 invoke_authentication_callback_ = false;
387
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200388 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
389 http_test_server_.Start();
Andreea Costinased9e6122020-08-12 12:06:19 +0200390
391 connect_job_->Start();
392
393 cros_client_socket_->SendTo(kValidConnectRequest,
394 std::strlen(kValidConnectRequest));
395 task_runner->RunUntilIdle();
396 // We need to manually invoke the method which reads from the client socket
397 // because |task_runner| will not execute the FileDescriptorWatcher's tasks.
398 connect_job_->OnClientReadReady();
399
400 // Check that an authentication request was sent.
401 ASSERT_TRUE(AuthRequested());
402 EXPECT_EQ(1, task_runner->GetPendingTaskCount());
403 // Move the time ahead so that the client connection timeout callback is
404 // triggered.
405 task_runner->FastForwardBy(task_runner->NextPendingTaskDelay());
406
407 const std::string expected_http_response =
408 "HTTP/1.1 407 Credentials required - Origin: local proxy\r\n\r\n";
409 std::vector<char> buf(expected_http_response.size());
410 ASSERT_TRUE(
411 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
412 std::string actual_response(buf.data(), buf.size());
413
414 // Check that the auth failure was forwarded to the client.
415 EXPECT_EQ(expected_http_response, actual_response);
416}
417
418// Verifies that the receiving the same bad credentials twice will send an auth
419// failure to the Chrome OS local client.
Andreea Costinasa8bc59c2020-09-18 13:49:13 +0200420TEST_F(HttpServerProxyConnectJobTest, CancelIfBadCredentials) {
421 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
422 AddServerReply(HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
423 http_test_server_.Start();
424
425 AddHttpAuthEntry(http_test_server_.GetUrl(), "Basic", "\"My Proxy\"",
426 kCredentials);
Andreea Costinased9e6122020-08-12 12:06:19 +0200427
428 connect_job_->Start();
429
430 cros_client_socket_->SendTo(kValidConnectRequest,
431 std::strlen(kValidConnectRequest));
432 brillo_loop_->RunOnce(false);
433
434 ASSERT_TRUE(AuthRequested());
435
436 const std::string expected_http_response =
437 "HTTP/1.1 407 Credentials required - Origin: local proxy\r\n\r\n";
438 std::vector<char> buf(expected_http_response.size());
439 ASSERT_TRUE(
440 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
441 std::string actual_response(buf.data(), buf.size());
442
443 EXPECT_EQ(expected_http_response, actual_response);
444}
445
Andreea Costinase45d54b2020-03-10 09:21:14 +0100446} // namespace system_proxy