Aktualizr
C++ SOTA Client
All Classes Namespaces Files Functions Variables Enumerations Enumerator Pages
fetcher_test.cc
1 #include <gtest/gtest.h>
2 
3 #include <sys/statvfs.h>
4 #include <chrono>
5 #include <future>
6 #include <iostream>
7 #include <string>
8 #include <thread>
9 
10 #include <boost/process.hpp>
11 
12 #include "config/config.h"
13 #include "http/httpclient.h"
14 #include "httpfake.h"
15 #include "logging/logging.h"
16 #include "package_manager/packagemanagerfactory.h"
17 #include "package_manager/packagemanagerfake.h"
18 #include "storage/sqlstorage.h"
19 #include "test_utils.h"
20 #include "uptane/tuf.h"
21 #include "utilities/apiqueue.h"
22 
23 static const int pause_after = 50; // percent
24 static const int pause_duration = 1; // seconds
25 static const int download_timeout = 200; // seconds
26 
27 static std::string server = "http://127.0.0.1:";
28 static std::string treehub_server = "http://127.0.0.1:";
29 std::string sysroot;
30 
31 static std::mutex pause_m;
32 static std::condition_variable cv;
33 static bool do_pause = false;
34 
35 Config config;
36 
37 static void progress_cb(const Uptane::Target& target, const std::string& description, unsigned int progress) {
38  (void)description;
39  (void)target;
40  std::cout << "progress callback: " << progress << std::endl;
41  if (!do_pause) {
42  if (progress >= pause_after) {
43  std::lock_guard<std::mutex> lk(pause_m);
44  do_pause = true;
45  cv.notify_all();
46  }
47  }
48 }
49 
50 /* Pause downloading.
51  * Pausing while paused is ignored.
52  * Pausing while not downloading is ignored.
53  * Resume downloading.
54  * Resuming while not paused is ignored.
55  * Resuming while not downloading is ignored
56  */
57 void test_pause(const Uptane::Target& target, PackageManager type = PackageManager::kNone) {
58  TemporaryDirectory temp_dir;
59  config.storage.path = temp_dir.Path();
60  config.uptane.repo_server = server;
61  config.pacman.type = type;
62  config.pacman.sysroot = sysroot;
63  config.pacman.ostree_server = treehub_server;
64 
65  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
66  auto http = std::make_shared<HttpClient>();
67 
68  auto pacman = PackageManagerFactory::makePackageManager(config.pacman, config.bootloader, storage, http);
69  KeyManager keys(storage, config.keymanagerConfig());
70  Uptane::Fetcher fetcher(config, http);
71 
73  EXPECT_EQ(token.setPause(true), true);
74  EXPECT_EQ(token.setPause(false), true);
75 
76  std::promise<void> pause_promise;
77  std::promise<bool> download_promise;
78  auto result = download_promise.get_future();
79  auto pause_res = pause_promise.get_future();
80  auto start = std::chrono::high_resolution_clock::now();
81 
82  do_pause = false;
83  std::thread([&target, &fetcher, &download_promise, &token, pacman, &keys]() {
84  bool res = pacman->fetchTarget(target, fetcher, keys, progress_cb, &token);
85  download_promise.set_value(res);
86  })
87  .detach();
88 
89  std::thread([&token, &pause_promise]() {
90  std::unique_lock<std::mutex> lk(pause_m);
91  cv.wait(lk, [] { return do_pause; });
92  EXPECT_EQ(token.setPause(true), true);
93  EXPECT_EQ(token.setPause(true), false);
94  std::this_thread::sleep_for(std::chrono::seconds(pause_duration));
95  EXPECT_EQ(token.setPause(false), true);
96  EXPECT_EQ(token.setPause(false), false);
97  pause_promise.set_value();
98  })
99  .detach();
100 
101  ASSERT_EQ(result.wait_for(std::chrono::seconds(download_timeout)), std::future_status::ready);
102  ASSERT_EQ(pause_res.wait_for(std::chrono::seconds(0)), std::future_status::ready);
103 
104  auto duration =
105  std::chrono::duration_cast<std::chrono::seconds>(std::chrono::high_resolution_clock::now() - start).count();
106  EXPECT_TRUE(result.get());
107  EXPECT_GE(duration, pause_duration);
108 }
109 
110 #ifdef BUILD_OSTREE
111 /*
112  * Download an OSTree package
113  * Verify an OSTree package
114  */
115 TEST(Fetcher, PauseOstree) {
116  Json::Value target_json;
117  target_json["hashes"]["sha256"] = "b9ac1e45f9227df8ee191b6e51e09417bd36c6ebbeff999431e3073ac50f0563";
118  target_json["custom"]["targetFormat"] = "OSTREE";
119  target_json["length"] = 0;
120  Uptane::Target target("pause", target_json);
121  test_pause(target, PackageManager::kOstree);
122 }
123 #endif // BUILD_OSTREE
124 
125 TEST(Fetcher, PauseBinary) {
126  Json::Value target_json;
127  target_json["hashes"]["sha256"] = "dd7bd1c37a3226e520b8d6939c30991b1c08772d5dab62b381c3a63541dc629a";
128  target_json["length"] = 100 * (1 << 20);
129 
130  Uptane::Target target("large_file", target_json);
131  test_pause(target);
132 }
133 
134 class HttpCustomUri : public HttpFake {
135  public:
136  HttpCustomUri(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
137  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
138  void* userp, curl_off_t from) override {
139  (void)write_cb;
140  (void)progress_cb;
141  (void)userp;
142  (void)from;
143  EXPECT_EQ(url, "test-uri");
144  return HttpResponse("0", 200, CURLE_OK, "");
145  }
146 };
147 
148 /* Download from URI specified in target metadata. */
149 TEST(Fetcher, DownloadCustomUri) {
150  TemporaryDirectory temp_dir;
151  config.storage.path = temp_dir.Path();
152  config.uptane.repo_server = server;
153 
154  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
155  auto http = std::make_shared<HttpCustomUri>(temp_dir.Path());
156 
157  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
158  KeyManager keys(storage, config.keymanagerConfig());
159  Uptane::Fetcher fetcher(config, http);
160 
161  // Make a fake target with the expected hash of "0".
162  Json::Value target_json;
163  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
164  target_json["custom"]["uri"] = "test-uri";
165  target_json["length"] = 1;
166  Uptane::Target target("fake_file", target_json);
167 
168  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
169 }
170 
171 class HttpDefaultUri : public HttpFake {
172  public:
173  HttpDefaultUri(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
174  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
175  void* userp, curl_off_t from) override {
176  (void)write_cb;
177  (void)progress_cb;
178  (void)userp;
179  (void)from;
180  EXPECT_EQ(url, server + "/targets/fake_file");
181  return HttpResponse("0", 200, CURLE_OK, "");
182  }
183 };
184 
185 /* Download from default file server URL. */
186 TEST(Fetcher, DownloadDefaultUri) {
187  TemporaryDirectory temp_dir;
188  config.storage.path = temp_dir.Path();
189  config.uptane.repo_server = server;
190 
191  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
192  auto http = std::make_shared<HttpDefaultUri>(temp_dir.Path());
193  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
194  KeyManager keys(storage, config.keymanagerConfig());
195  Uptane::Fetcher fetcher(config, http);
196 
197  {
198  // No custom uri.
199  Json::Value target_json;
200  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
201  target_json["length"] = 1;
202  Uptane::Target target("fake_file", target_json);
203 
204  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
205  }
206  {
207  // Empty custom uri.
208  Json::Value target_json;
209  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
210  target_json["custom"]["uri"] = "";
211  target_json["length"] = 1;
212  Uptane::Target target("fake_file", target_json);
213 
214  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
215  }
216  {
217  // example.com (default) custom uri.
218  Json::Value target_json;
219  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
220  target_json["custom"]["uri"] = "https://example.com/";
221  target_json["length"] = 1;
222  Uptane::Target target("fake_file", target_json);
223 
224  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
225  }
226 }
227 
228 class HttpZeroLength : public HttpFake {
229  public:
230  HttpZeroLength(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
231  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
232  void* userp, curl_off_t from) override {
233  (void)write_cb;
234  (void)progress_cb;
235  (void)userp;
236  (void)from;
237 
238  EXPECT_EQ(url, server + "/targets/fake_file");
239  counter++;
240  return HttpResponse("0", 200, CURLE_OK, "");
241  }
242 
243  int counter = 0;
244 };
245 
246 /* Don't bother downloading a target with length 0. */
247 TEST(Fetcher, DownloadLengthZero) {
248  TemporaryDirectory temp_dir;
249  config.storage.path = temp_dir.Path();
250  config.uptane.repo_server = server;
251 
252  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
253  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
254  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
255  KeyManager keys(storage, config.keymanagerConfig());
256  Uptane::Fetcher fetcher(config, http);
257 
258  // Empty target: download succeeds, but http module is never called.
259  Json::Value empty_target_json;
260  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
261  empty_target_json["length"] = 0;
262  Uptane::Target empty_target("empty_file", empty_target_json);
263  EXPECT_TRUE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
264  EXPECT_EQ(http->counter, 0);
265 
266  // Non-empty target: download succeeds, and http module is called. This is
267  // done purely to make sure the test is designed correctly.
268  Json::Value nonempty_target_json;
269  nonempty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
270  nonempty_target_json["length"] = 1;
271  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
272  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
273  EXPECT_EQ(http->counter, 1);
274 }
275 
276 /* Don't bother downloading a target that is larger than the available disk
277  * space. */
278 TEST(Fetcher, NotEnoughDiskSpace) {
279  TemporaryDirectory temp_dir;
280  config.storage.path = temp_dir.Path();
281  config.uptane.repo_server = server;
282 
283  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
284  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
285  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
286  KeyManager keys(storage, config.keymanagerConfig());
287  Uptane::Fetcher fetcher(config, http);
288 
289  // Find how much space is available on disk.
290  struct statvfs stvfsbuf {};
291  EXPECT_EQ(statvfs(temp_dir.Path().c_str(), &stvfsbuf), 0);
292  const uint64_t available_bytes = (stvfsbuf.f_bsize * stvfsbuf.f_bavail);
293 
294  // Try to fetch a target larger than the available disk space: an exception is
295  // thrown and the http module is never called.
296  Json::Value empty_target_json;
297  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
298  empty_target_json["length"] = available_bytes * 2;
299  Uptane::Target empty_target("empty_file", empty_target_json);
300  EXPECT_THROW(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr), std::runtime_error);
301  EXPECT_EQ(http->counter, 0);
302 
303  // Try to fetch a 1-byte target: download succeeds, and http module is called.
304  // This is done purely to make sure the test is designed correctly.
305  Json::Value nonempty_target_json;
306  nonempty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
307  nonempty_target_json["length"] = 1;
308  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
309  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
310  EXPECT_EQ(http->counter, 1);
311 }
312 
313 #ifndef __NO_MAIN__
314 int main(int argc, char** argv) {
315  ::testing::InitGoogleTest(&argc, argv);
316 
317  logger_init();
318  logger_set_threshold(boost::log::trivial::debug);
319 
320  std::string port = TestUtils::getFreePort();
321  server += port;
322  boost::process::child http_server_process("tests/fake_http_server/fake_test_server.py", port, "-f");
323  TestUtils::waitForServer(server + "/");
324 #ifdef BUILD_OSTREE
325  std::string treehub_port = TestUtils::getFreePort();
326  treehub_server += treehub_port;
327  TemporaryDirectory treehub_dir;
328  boost::process::child ostree_server_process("tests/sota_tools/treehub_server.py", std::string("-p"), treehub_port,
329  std::string("-d"), treehub_dir.PathString(), std::string("-s0.5"),
330  std::string("--create"));
331  TemporaryDirectory temp_dir;
332  int r = system((std::string("ostree admin init-fs ") + temp_dir.PathString()).c_str());
333  if (r != 0) {
334  return -1;
335  }
336  r = system((std::string("ostree config --repo=") + temp_dir.PathString() +
337  std::string("/ostree/repo set core.mode bare-user-only"))
338  .c_str());
339  if (r != 0) {
340  return -1;
341  }
342  sysroot = temp_dir.Path().string();
343  TestUtils::waitForServer(treehub_server + "/");
344 #endif // BUILD_OSTREE
345  return RUN_ALL_TESTS();
346 }
347 #endif // __NO_MAIN__
Uptane::Fetcher
Definition: fetcher.h:33
HttpFake
Definition: httpfake.h:22
KeyManager
Definition: keymanager.h:13
HttpResponse
Definition: httpinterface.h:17
Config
Configuration object for an aktualizr instance running on a primary ECU.
Definition: config.h:73
HttpDefaultUri
Definition: fetcher_test.cc:171
api::FlowControlToken
Provides a thread-safe way to pause and terminate task execution.
Definition: apiqueue.h:19
TemporaryDirectory
Definition: utils.h:82
result
Results of libaktualizr API calls.
Definition: results.h:13
Uptane::Target
Definition: tuf.h:238
api::FlowControlToken::setPause
bool setPause(bool set_paused)
Called by the controlling thread to request the task to pause or resume.
Definition: apiqueue.cc:6
SQLStorage
Definition: sqlstorage.h:18
HttpCustomUri
Definition: fetcher_test.cc:134
HttpZeroLength
Definition: fetcher_test.cc:228