blob: 94bd8567aaba46013a8a87522f19a44f2ab70b3d [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>
Andreea Costinas08a5d182020-04-29 22:12:47 +020020#include <base/test/test_mock_time_task_runner.h>
Andreea Costinase45d54b2020-03-10 09:21:14 +010021#include <brillo/message_loops/base_message_loop.h>
Garrick Evanscd8c2972020-04-14 14:35:52 +090022#include <chromeos/patchpanel/socket.h>
23#include <chromeos/patchpanel/socket_forwarder.h>
Andreea Costinase45d54b2020-03-10 09:21:14 +010024
25#include "bindings/worker_common.pb.h"
26#include "system-proxy/protobuf_util.h"
Andreea Costinas054fbb52020-06-12 20:46:22 +020027#include "system-proxy/test_http_server.h"
Andreea Costinase45d54b2020-03-10 09:21:14 +010028
29namespace {
Andreea Costinas054fbb52020-06-12 20:46:22 +020030
31constexpr char kProxyServerUrl[] = "http://127.0.0.1:3128";
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
71 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 Costinasbb2aa022020-06-13 00:03:23 +020075 base::BindOnce(&ProxyConnectJobTest::FetchCredentialsFromCache,
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 }
Andreea Costinasbb2aa022020-06-13 00:03:23 +020080 void AddHttpAuthEntry(const std::string& origin,
81 const std::string& scheme,
82 const std::string& realm,
83 const std::string& credentials) {
84 http_auth_cache_.push_back(
85 HttpAuthEntry(origin, scheme, realm, credentials));
86 }
87
88 bool AuthRequested() { return auth_requested_; }
Andreea Costinase45d54b2020-03-10 09:21:14 +010089
90 protected:
91 void ResolveProxy(
92 const std::string& target_url,
93 base::OnceCallback<void(const std::list<std::string>&)> callback) {
Andreea Costinas054fbb52020-06-12 20:46:22 +020094 std::move(callback).Run({remote_proxy_url_});
Andreea Costinase45d54b2020-03-10 09:21:14 +010095 }
96
Andreea Costinasbb2aa022020-06-13 00:03:23 +020097 void FetchCredentialsFromCache(
98 const std::string& proxy_url,
99 const std::string& scheme,
100 const std::string& realm,
101 base::OnceCallback<void(const std::string&)> callback) {
102 ASSERT_FALSE(auth_requested_);
103 auth_requested_ = true;
104 for (const auto& auth_entry : http_auth_cache_) {
105 if (auth_entry.origin == proxy_url && auth_entry.realm == realm &&
106 auth_entry.scheme == scheme) {
107 std::move(callback).Run(auth_entry.credentials);
108 return;
109 }
110 }
111 std::move(callback).Run(/* credentials = */ "");
112 }
113
Andreea Costinase45d54b2020-03-10 09:21:14 +0100114 void OnConnectionSetupFinished(
Garrick Evans3388a032020-03-24 11:25:55 +0900115 std::unique_ptr<patchpanel::SocketForwarder> fwd,
Andreea Costinase45d54b2020-03-10 09:21:14 +0100116 ProxyConnectJob* connect_job) {
117 ASSERT_EQ(connect_job, connect_job_.get());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200118 if (fwd) {
119 forwarder_created_ = true;
120 }
Andreea Costinase45d54b2020-03-10 09:21:14 +0100121 }
122
Andreea Costinas054fbb52020-06-12 20:46:22 +0200123 std::string remote_proxy_url_ = kProxyServerUrl;
124 bool forwarder_created_ = false;
Andreea Costinase45d54b2020-03-10 09:21:14 +0100125 std::unique_ptr<ProxyConnectJob> connect_job_;
126 base::MessageLoopForIO loop_;
127 brillo::BaseMessageLoop brillo_loop_{&loop_};
Garrick Evans3388a032020-03-24 11:25:55 +0900128 std::unique_ptr<patchpanel::Socket> cros_client_socket_;
Andreea Costinas08a5d182020-04-29 22:12:47 +0200129
130 private:
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200131 std::vector<HttpAuthEntry> http_auth_cache_;
132 bool auth_requested_ = false;
Andreea Costinas08a5d182020-04-29 22:12:47 +0200133 FRIEND_TEST(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100134};
135
136TEST_F(ProxyConnectJobTest, SuccessfulConnection) {
Andreea Costinas054fbb52020-06-12 20:46:22 +0200137 HttpTestServer http_test_server;
138 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
139 http_test_server.Start();
140 remote_proxy_url_ = http_test_server.GetUrl();
141
Andreea Costinas08a5d182020-04-29 22:12:47 +0200142 connect_job_->Start();
Andreea Costinas054fbb52020-06-12 20:46:22 +0200143 cros_client_socket_->SendTo(kValidConnectRequest,
144 std::strlen(kValidConnectRequest));
Andreea Costinase45d54b2020-03-10 09:21:14 +0100145 brillo_loop_.RunOnce(false);
146
Andreea Costinasa2246592020-04-12 23:24:01 +0200147 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
Andreea Costinase45d54b2020-03-10 09:21:14 +0100148 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200149 EXPECT_EQ(http_test_server.GetUrl(), connect_job_->proxy_servers_.front());
150 EXPECT_TRUE(forwarder_created_);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200151 ASSERT_FALSE(AuthRequested());
Andreea Costinase45d54b2020-03-10 09:21:14 +0100152}
153
Andreea Costinasf90a4c02020-06-12 22:30:51 +0200154TEST_F(ProxyConnectJobTest, TunnelFailedBadGatewayFromRemote) {
155 HttpTestServer http_test_server;
156 http_test_server.AddHttpConnectReply(
157 HttpTestServer::HttpConnectReply::kBadGateway);
158 http_test_server.Start();
159 remote_proxy_url_ = http_test_server.GetUrl();
160
161 connect_job_->Start();
162 cros_client_socket_->SendTo(kValidConnectRequest,
163 std::strlen(kValidConnectRequest));
164 brillo_loop_.RunOnce(false);
165
166 EXPECT_FALSE(forwarder_created_);
167
168 std::string expected_server_reply =
169 "HTTP/1.1 502 Error creating tunnel - Origin: local proxy\r\n\r\n";
170 std::vector<char> buf(expected_server_reply.size());
171 ASSERT_TRUE(cros_client_socket_->RecvFrom(buf.data(), buf.size()));
172 std::string actual_server_reply(buf.data(), buf.size());
173
174 EXPECT_EQ(expected_server_reply, actual_server_reply);
175}
176
Andreea Costinased3f9782020-05-20 17:09:46 +0200177TEST_F(ProxyConnectJobTest, SuccessfulConnectionAltEnding) {
Andreea Costinas054fbb52020-06-12 20:46:22 +0200178 HttpTestServer http_test_server;
179 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
180 http_test_server.Start();
181 remote_proxy_url_ = http_test_server.GetUrl();
182
Andreea Costinased3f9782020-05-20 17:09:46 +0200183 connect_job_->Start();
Andreea Costinas054fbb52020-06-12 20:46:22 +0200184 char validConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n\n";
Andreea Costinased3f9782020-05-20 17:09:46 +0200185 cros_client_socket_->SendTo(validConnRequest, std::strlen(validConnRequest));
186 brillo_loop_.RunOnce(false);
187
188 EXPECT_EQ("www.example.server.com:443", connect_job_->target_url_);
189 EXPECT_EQ(1, connect_job_->proxy_servers_.size());
Andreea Costinas054fbb52020-06-12 20:46:22 +0200190 EXPECT_EQ(http_test_server.GetUrl(), connect_job_->proxy_servers_.front());
191 EXPECT_TRUE(forwarder_created_);
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200192 ASSERT_FALSE(AuthRequested());
Andreea Costinased3f9782020-05-20 17:09:46 +0200193}
194
Andreea Costinase45d54b2020-03-10 09:21:14 +0100195TEST_F(ProxyConnectJobTest, BadHttpRequestWrongMethod) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200196 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100197 char badConnRequest[] = "GET www.example.server.com:443 HTTP/1.1\r\n\r\n";
198 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
199 brillo_loop_.RunOnce(false);
200
201 EXPECT_EQ("", connect_job_->target_url_);
202 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
203 const std::string expected_http_response =
204 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
205 std::vector<char> buf(expected_http_response.size());
206 ASSERT_TRUE(
207 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
208 std::string actual_response(buf.data(), buf.size());
209 EXPECT_EQ(expected_http_response, actual_response);
210}
211
212TEST_F(ProxyConnectJobTest, BadHttpRequestNoEmptyLine) {
Andreea Costinas08a5d182020-04-29 22:12:47 +0200213 connect_job_->Start();
Andreea Costinase45d54b2020-03-10 09:21:14 +0100214 // No empty line after http message.
215 char badConnRequest[] = "CONNECT www.example.server.com:443 HTTP/1.1\r\n";
216 cros_client_socket_->SendTo(badConnRequest, std::strlen(badConnRequest));
217 brillo_loop_.RunOnce(false);
218
219 EXPECT_EQ("", connect_job_->target_url_);
220 EXPECT_EQ(0, connect_job_->proxy_servers_.size());
221 const std::string expected_http_response =
222 "HTTP/1.1 400 Bad Request - Origin: local proxy\r\n\r\n";
223 std::vector<char> buf(expected_http_response.size());
224 ASSERT_TRUE(
225 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
226 std::string actual_response(buf.data(), buf.size());
227 EXPECT_EQ(expected_http_response, actual_response);
228}
229
Andreea Costinas08a5d182020-04-29 22:12:47 +0200230TEST_F(ProxyConnectJobTest, WaitClientConnectTimeout) {
231 // Add a TaskRunner where we can control time.
232 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
233 new base::TestMockTimeTaskRunner()};
234 loop_.SetTaskRunner(task_runner);
235 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
236
237 connect_job_->Start();
238
239 EXPECT_EQ(1, task_runner->GetPendingTaskCount());
240 // Move the time ahead so that the client connection timeout callback is
241 // triggered.
242 task_runner->FastForwardBy(task_runner->NextPendingTaskDelay());
243
244 const std::string expected_http_response =
245 "HTTP/1.1 408 Request Timeout - Origin: local proxy\r\n\r\n";
246 std::vector<char> buf(expected_http_response.size());
247 ASSERT_TRUE(
248 base::ReadFromFD(cros_client_socket_->fd(), buf.data(), buf.size()));
249 std::string actual_response(buf.data(), buf.size());
250
251 EXPECT_EQ(expected_http_response, actual_response);
252}
253
254// Check that the client connect timeout callback is not fired if the owning
255// proxy connect job is destroyed.
256TEST_F(ProxyConnectJobTest, ClientConnectTimeoutJobCanceled) {
257 // Add a TaskRunner where we can control time.
258 scoped_refptr<base::TestMockTimeTaskRunner> task_runner{
259 new base::TestMockTimeTaskRunner()};
260 loop_.SetTaskRunner(task_runner);
261 base::TestMockTimeTaskRunner::ScopedContext scoped_context(task_runner.get());
262
263 // Create a proxy connect job and start the client connect timeout counter.
264 {
265 int fds[2];
266 ASSERT_NE(-1,
267 socketpair(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
268 0 /* protocol */, fds));
269 auto client_socket =
270 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[1]));
271
272 auto connect_job = std::make_unique<ProxyConnectJob>(
273 std::make_unique<patchpanel::Socket>(base::ScopedFD(fds[0])), "",
274 base::BindOnce(&ProxyConnectJobTest::ResolveProxy,
275 base::Unretained(this)),
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200276 base::BindOnce(&ProxyConnectJobTest::FetchCredentialsFromCache,
277 base::Unretained(this)),
Andreea Costinas08a5d182020-04-29 22:12:47 +0200278 base::BindOnce(&ProxyConnectJobTest::OnConnectionSetupFinished,
279 base::Unretained(this)));
280 // Post the timeout task.
281 connect_job->Start();
282 EXPECT_TRUE(task_runner->HasPendingTask());
283 }
284 // Check that the task was canceled.
285 EXPECT_FALSE(task_runner->HasPendingTask());
286}
287
Andreea Costinasbb2aa022020-06-13 00:03:23 +0200288// Test that the the CONNECT request is sent again after acquiring credentials.
289TEST_F(ProxyConnectJobTest, ResendWithCredentials) {
290 // Start the test server
291 HttpTestServer http_test_server;
292 http_test_server.AddHttpConnectReply(
293 HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
294 http_test_server.AddHttpConnectReply(HttpTestServer::HttpConnectReply::kOk);
295 http_test_server.Start();
296 remote_proxy_url_ = http_test_server.GetUrl();
297
298 AddHttpAuthEntry(remote_proxy_url_, "Basic", "\"My Proxy\"", kCredentials);
299 connect_job_->Start();
300
301 cros_client_socket_->SendTo(kValidConnectRequest,
302 std::strlen(kValidConnectRequest));
303 brillo_loop_.RunOnce(false);
304
305 ASSERT_TRUE(AuthRequested());
306 EXPECT_TRUE(forwarder_created_);
307 EXPECT_EQ(kCredentials, connect_job_->credentials_);
308 EXPECT_EQ(200, connect_job_->http_response_code_);
309}
310
311// Test that the proxy auth required status is forwarded to the client if
312// credentials are missing.
313TEST_F(ProxyConnectJobTest, NoCredentials) {
314 // Start the test server
315 HttpTestServer http_test_server;
316 http_test_server.AddHttpConnectReply(
317 HttpTestServer::HttpConnectReply::kAuthRequiredBasic);
318 http_test_server.Start();
319 remote_proxy_url_ = http_test_server.GetUrl();
320
321 connect_job_->Start();
322
323 cros_client_socket_->SendTo(kValidConnectRequest,
324 std::strlen(kValidConnectRequest));
325 brillo_loop_.RunOnce(false);
326
327 ASSERT_TRUE(AuthRequested());
328 EXPECT_EQ("", connect_job_->credentials_);
329 EXPECT_EQ(407, connect_job_->http_response_code_);
330}
331
332// Test that the proxy auth required status is forwarded to the client if the
333// server chose Kerberos as an authentication method.
334TEST_F(ProxyConnectJobTest, KerberosAuth) {
335 // Start the test server
336 HttpTestServer http_test_server;
337 http_test_server.AddHttpConnectReply(
338 HttpTestServer::HttpConnectReply::kAuthRequiredKerberos);
339 http_test_server.Start();
340 remote_proxy_url_ = http_test_server.GetUrl();
341
342 connect_job_->Start();
343
344 cros_client_socket_->SendTo(kValidConnectRequest,
345 std::strlen(kValidConnectRequest));
346 brillo_loop_.RunOnce(false);
347
348 ASSERT_FALSE(AuthRequested());
349 EXPECT_EQ("", connect_job_->credentials_);
350 EXPECT_EQ(407, connect_job_->http_response_code_);
351}
352
Andreea Costinase45d54b2020-03-10 09:21:14 +0100353} // namespace system_proxy