1 // Copyright 2019 The Chromium 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 "chrome/browser/ui/ash/assistant/test_support/fake_s3_server.h"
6 
7 #include <memory>
8 
9 #include "base/check.h"
10 #include "base/check_op.h"
11 #include "base/command_line.h"
12 #include "base/files/file_path.h"
13 #include "base/files/file_util.h"
14 #include "base/path_service.h"
15 #include "base/process/launch.h"
16 #include "base/strings/string_number_conversions.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/stringprintf.h"
19 #include "chromeos/assistant/internal/internal_constants.h"
20 #include "chromeos/services/assistant/service.h"
21 #include "testing/gtest/include/gtest/gtest.h"
22 
23 namespace chromeos {
24 namespace assistant {
25 
26 namespace {
27 
28 // Folder where the S3 communications are stored when running in replay mode.
29 constexpr char kTestDataFolder[] = "chromeos/assistant/internal/test_data/";
30 
31 // Fake device id passed to Libassistant. By fixing this we ensure it remains
32 // consistent between the current session and the value stored in the stored
33 // test data.
34 // This must be a 16 characters hex string or it will be rejected.
35 constexpr char kDeviceId[] = "11112222333344445555666677778888";
36 
GetExecutableDir()37 base::FilePath GetExecutableDir() {
38   base::FilePath result;
39   base::PathService::Get(base::DIR_EXE, &result);
40   return result;
41 }
42 
GetSourceDir()43 base::FilePath GetSourceDir() {
44   base::FilePath result;
45   base::PathService::Get(base::DIR_SOURCE_ROOT, &result);
46   return result;
47 }
48 
GetSanitizedTestName()49 std::string GetSanitizedTestName() {
50   std::string test_name = base::ToLowerASCII(base::StringPrintf(
51       "%s_%s",
52       testing::UnitTest::GetInstance()->current_test_info()->test_suite_name(),
53       testing::UnitTest::GetInstance()->current_test_info()->name()));
54   std::string new_test_name;
55   base::ReplaceChars(test_name, "/", "_", &new_test_name);
56   return new_test_name;
57 }
58 
GetAccessTokenFromEnvironmentOrDie()59 const std::string GetAccessTokenFromEnvironmentOrDie() {
60   const char* token = std::getenv("TOKEN");
61   CHECK(token && strlen(token))
62       << "No token found in the environmental variable $TOKEN.\n"
63       << kGenerateTokenInstructions;
64   return token;
65 }
66 
FakeS3ModeToString(FakeS3Mode mode)67 std::string FakeS3ModeToString(FakeS3Mode mode) {
68   switch (mode) {
69     case FakeS3Mode::kProxy:
70       return "PROXY";
71     case FakeS3Mode::kRecord:
72       return "RECORD";
73     case FakeS3Mode::kReplay:
74       return "REPLAY";
75   }
76   NOTREACHED();
77 }
78 
AppendArgument(base::CommandLine * command_line,const std::string & name,const std::string & value)79 void AppendArgument(base::CommandLine* command_line,
80                     const std::string& name,
81                     const std::string& value) {
82   // Note we can't use |AppendSwitchASCII| as that will add "<name>=<value>",
83   // and the fake s3 server binary does not support '='.
84   command_line->AppendArg(name);
85   command_line->AppendArg(value);
86 }
87 
88 }  // namespace
89 
90 // Selects a port for the fake S3 server to use.
91 // This will use a file-based lock because different test shards might be trying
92 // to run fake S3 servers at the same time, and we need to ensure they use
93 // different ports.
94 class PortSelector {
95  public:
PortSelector()96   PortSelector() { SelectPort(); }
97   PortSelector(PortSelector&) = delete;
98   PortSelector& operator=(PortSelector&) = delete;
~PortSelector()99   ~PortSelector() {
100     lock_file_.Close();
101     base::DeletePathRecursively(GetLockFilePath());
102   }
103 
port() const104   int port() const { return port_; }
105 
106  private:
107   // The first port we'll try to use. Randomly chosen to be outside of the range
108   // of known ports.
109   constexpr static int kStartPort = 23600;
110   // Maximum number of ports we'll try before we give up and conclude no ports
111   // are available (which really should not happen).
112   constexpr static int kMaxAttempts = 20000;
113 
SelectPort()114   void SelectPort() {
115     for (int offset = 0; offset < kMaxAttempts; offset++) {
116       port_ = kStartPort + offset;
117       lock_file_ = base::File(GetLockFilePath(), GetFileFlags());
118       if (lock_file_.IsValid())
119         return;
120     }
121     CHECK(false) << "Failed to find an available port.";
122   }
123 
GetLockFilePath() const124   base::FilePath GetLockFilePath() const {
125     std::string file_name = "port_" + base::NumberToString(port_) + "_lock";
126     return GetLockFileDirectory().Append(file_name);
127   }
GetLockFileDirectory()128   static base::FilePath GetLockFileDirectory() {
129     base::FilePath result;
130     bool success = base::GetTempDir(&result);
131     EXPECT_TRUE(success);
132     return result;
133   }
134 
GetFileFlags()135   static int GetFileFlags() {
136     return base::File::FLAG_CREATE | base::File::FLAG_EXCLUSIVE_WRITE |
137            base::File::FLAG_WRITE;
138   }
139 
140   // File exclusively opened on the file-system, to ensure no other fake S3
141   // server uses the same port.
142   base::File lock_file_;
143   int port_;
144 };
145 
FakeS3Server(int data_file_version)146 FakeS3Server::FakeS3Server(int data_file_version)
147     : data_file_version_(data_file_version),
148       port_selector_(std::make_unique<PortSelector>()) {
149   DCHECK_GT(data_file_version, 0);
150 }
151 
~FakeS3Server()152 FakeS3Server::~FakeS3Server() {
153   Teardown();
154 }
155 
Setup(FakeS3Mode mode)156 void FakeS3Server::Setup(FakeS3Mode mode) {
157   SetAccessTokenForMode(mode);
158   StartS3ServerProcess(mode);
159   SetFakeS3ServerURI();
160   SetDeviceId();
161 }
162 
Teardown()163 void FakeS3Server::Teardown() {
164   StopS3ServerProcess();
165   UnsetDeviceId();
166   UnsetFakeS3ServerURI();
167 }
168 
GetAccessToken() const169 std::string FakeS3Server::GetAccessToken() const {
170   return access_token_;
171 }
172 
SetAccessTokenForMode(FakeS3Mode mode)173 void FakeS3Server::SetAccessTokenForMode(FakeS3Mode mode) {
174   if (mode == FakeS3Mode::kProxy || mode == FakeS3Mode::kRecord) {
175     access_token_ = GetAccessTokenFromEnvironmentOrDie();
176   }
177 }
178 
SetFakeS3ServerURI()179 void FakeS3Server::SetFakeS3ServerURI() {
180   // Note this must be stored in a local variable, as
181   // |Service::OverrideS3ServerUriForTesting| does not take ownership of the
182   // |const char *|.
183   fake_s3_server_uri_ = "localhost:" + base::NumberToString(port());
184   Service::OverrideS3ServerUriForTesting(fake_s3_server_uri_.c_str());
185 }
186 
SetDeviceId()187 void FakeS3Server::SetDeviceId() {
188   Service::OverrideDeviceIdForTesting(kDeviceId);
189 }
190 
UnsetDeviceId()191 void FakeS3Server::UnsetDeviceId() {
192   Service::OverrideDeviceIdForTesting(nullptr);
193 }
194 
UnsetFakeS3ServerURI()195 void FakeS3Server::UnsetFakeS3ServerURI() {
196   Service::OverrideS3ServerUriForTesting(nullptr);
197   fake_s3_server_uri_ = "";
198 }
199 
StartS3ServerProcess(FakeS3Mode mode)200 void FakeS3Server::StartS3ServerProcess(FakeS3Mode mode) {
201   if (process_running_) {
202     LOG(WARNING)
203         << "Called FakeS3Server::StartS3ServerProcess when already running.";
204     return;
205   }
206 
207   base::FilePath fake_s3_server_main =
208       GetExecutableDir().Append(FILE_PATH_LITERAL(kFakeS3ServerBinary));
209 
210   base::CommandLine command_line(fake_s3_server_main);
211   AppendArgument(&command_line, "--port", base::NumberToString(port()));
212   AppendArgument(&command_line, "--mode", FakeS3ModeToString(mode));
213   AppendArgument(&command_line, "--auth_token", GetAccessToken());
214   AppendArgument(&command_line, "--test_data_file", GetTestDataFileName());
215 
216   fake_s3_server_ = base::LaunchProcess(command_line, base::LaunchOptions{});
217   process_running_ = true;
218 }
219 
StopS3ServerProcess()220 void FakeS3Server::StopS3ServerProcess() {
221   if (!process_running_) {
222     LOG(WARNING)
223         << "Called FakeS3Server::StopS3ServerProcess when already stopped.";
224     return;
225   }
226   fake_s3_server_.Terminate(/*exit_code=*/0, /*wait=*/true);
227   process_running_ = false;
228 }
229 
GetTestDataFileName()230 std::string FakeS3Server::GetTestDataFileName() {
231   auto create_file_path = [](const std::string& test_name, int version) {
232     return GetSourceDir()
233         .Append(FILE_PATH_LITERAL(kTestDataFolder))
234         .Append(FILE_PATH_LITERAL(test_name + ".v" +
235                                   base::NumberToString(version) +
236                                   ".fake_s3.proto"));
237   };
238   // Look for the latest version of the data file, if not found, look for older
239   // ones.
240   auto data_file = create_file_path(GetSanitizedTestName(), data_file_version_);
241   for (int version = data_file_version_ - 1;
242        !base::PathExists(data_file) && version > 0; --version) {
243     data_file = create_file_path(GetSanitizedTestName(), version);
244   }
245 
246   return data_file.MaybeAsASCII();
247 }
248 
port() const249 int FakeS3Server::port() const {
250   return port_selector_->port();
251 }
252 
253 }  // namespace assistant
254 }  // namespace chromeos
255