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, const std::string& type = PACKAGE_MANAGER_NONE) {
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  }).detach();
87 
88  std::thread([&token, &pause_promise]() {
89  std::unique_lock<std::mutex> lk(pause_m);
90  cv.wait(lk, [] { return do_pause; });
91  EXPECT_EQ(token.setPause(true), true);
92  EXPECT_EQ(token.setPause(true), false);
93  std::this_thread::sleep_for(std::chrono::seconds(pause_duration));
94  EXPECT_EQ(token.setPause(false), true);
95  EXPECT_EQ(token.setPause(false), false);
96  pause_promise.set_value();
97  }).detach();
98 
99  ASSERT_EQ(result.wait_for(std::chrono::seconds(download_timeout)), std::future_status::ready);
100  ASSERT_EQ(pause_res.wait_for(std::chrono::seconds(0)), std::future_status::ready);
101 
102  auto duration =
103  std::chrono::duration_cast<std::chrono::seconds>(std::chrono::high_resolution_clock::now() - start).count();
104  EXPECT_TRUE(result.get());
105  EXPECT_GE(duration, pause_duration);
106 }
107 
108 #ifdef BUILD_OSTREE
109 /*
110  * Download an OSTree package
111  * Verify an OSTree package
112  */
113 TEST(Fetcher, PauseOstree) {
114  Json::Value target_json;
115  target_json["hashes"]["sha256"] = "b9ac1e45f9227df8ee191b6e51e09417bd36c6ebbeff999431e3073ac50f0563";
116  target_json["custom"]["targetFormat"] = "OSTREE";
117  target_json["length"] = 0;
118  Uptane::Target target("pause", target_json);
119  test_pause(target, PACKAGE_MANAGER_OSTREE);
120 }
121 #endif // BUILD_OSTREE
122 
123 TEST(Fetcher, PauseBinary) {
124  Json::Value target_json;
125  target_json["hashes"]["sha256"] = "dd7bd1c37a3226e520b8d6939c30991b1c08772d5dab62b381c3a63541dc629a";
126  target_json["length"] = 100 * (1 << 20);
127 
128  Uptane::Target target("large_file", target_json);
129  test_pause(target);
130 }
131 
132 class HttpCustomUri : public HttpFake {
133  public:
134  HttpCustomUri(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
135  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
136  void* userp, curl_off_t from) override {
137  (void)write_cb;
138  (void)progress_cb;
139  (void)userp;
140  (void)from;
141  EXPECT_EQ(url, "test-uri");
142  return HttpResponse("0", 200, CURLE_OK, "");
143  }
144 };
145 
146 /* Download from URI specified in target metadata. */
147 TEST(Fetcher, DownloadCustomUri) {
148  TemporaryDirectory temp_dir;
149  config.storage.path = temp_dir.Path();
150  config.uptane.repo_server = server;
151 
152  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
153  auto http = std::make_shared<HttpCustomUri>(temp_dir.Path());
154 
155  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
156  KeyManager keys(storage, config.keymanagerConfig());
157  Uptane::Fetcher fetcher(config, http);
158 
159  // Make a fake target with the expected hash of "0".
160  Json::Value target_json;
161  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
162  target_json["custom"]["uri"] = "test-uri";
163  target_json["length"] = 1;
164  Uptane::Target target("fake_file", target_json);
165 
166  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
167 }
168 
169 class HttpDefaultUri : public HttpFake {
170  public:
171  HttpDefaultUri(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
172  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
173  void* userp, curl_off_t from) override {
174  (void)write_cb;
175  (void)progress_cb;
176  (void)userp;
177  (void)from;
178  EXPECT_EQ(url, server + "/targets/fake_file");
179  return HttpResponse("0", 200, CURLE_OK, "");
180  }
181 };
182 
183 /* Download from default file server URL. */
184 TEST(Fetcher, DownloadDefaultUri) {
185  TemporaryDirectory temp_dir;
186  config.storage.path = temp_dir.Path();
187  config.uptane.repo_server = server;
188 
189  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
190  auto http = std::make_shared<HttpDefaultUri>(temp_dir.Path());
191  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
192  KeyManager keys(storage, config.keymanagerConfig());
193  Uptane::Fetcher fetcher(config, http);
194 
195  {
196  // No custom uri.
197  Json::Value target_json;
198  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
199  target_json["length"] = 1;
200  Uptane::Target target("fake_file", target_json);
201 
202  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
203  }
204  {
205  // Empty custom uri.
206  Json::Value target_json;
207  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
208  target_json["custom"]["uri"] = "";
209  target_json["length"] = 1;
210  Uptane::Target target("fake_file", target_json);
211 
212  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
213  }
214  {
215  // example.com (default) custom uri.
216  Json::Value target_json;
217  target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
218  target_json["custom"]["uri"] = "https://example.com/";
219  target_json["length"] = 1;
220  Uptane::Target target("fake_file", target_json);
221 
222  EXPECT_TRUE(pacman->fetchTarget(target, fetcher, keys, progress_cb, nullptr));
223  }
224 }
225 
226 class HttpZeroLength : public HttpFake {
227  public:
228  HttpZeroLength(const boost::filesystem::path& test_dir_in) : HttpFake(test_dir_in) {}
229  HttpResponse download(const std::string& url, curl_write_callback write_cb, curl_xferinfo_callback progress_cb,
230  void* userp, curl_off_t from) override {
231  (void)progress_cb;
232  (void)from;
233 
234  EXPECT_EQ(url, server + "/targets/fake_file");
235  const std::string content = "0";
236  write_cb(const_cast<char*>(&content[0]), 1, 1, userp);
237  counter++;
238  return HttpResponse(content, 200, CURLE_OK, "");
239  }
240 
241  int counter = 0;
242 };
243 
244 /* Don't bother downloading a target with length 0, but make sure verification
245  * still succeeds so that installation is possible. */
246 TEST(Fetcher, DownloadLengthZero) {
247  TemporaryDirectory temp_dir;
248  config.storage.path = temp_dir.Path();
249  config.uptane.repo_server = server;
250 
251  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
252  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
253  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
254  KeyManager keys(storage, config.keymanagerConfig());
255  Uptane::Fetcher fetcher(config, http);
256 
257  // Empty target: download succeeds, but http module is never called.
258  Json::Value empty_target_json;
259  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
260  empty_target_json["length"] = 0;
261  // Make sure this isn't confused for an old-style OSTree target.
262  empty_target_json["custom"]["targetFormat"] = "binary";
263  Uptane::Target empty_target("empty_file", empty_target_json);
264  EXPECT_TRUE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
265  EXPECT_EQ(pacman->verifyTarget(empty_target), TargetStatus::kGood);
266  EXPECT_EQ(http->counter, 0);
267 
268  // Non-empty target: download succeeds, and http module is called. This is
269  // done purely to make sure the test is designed correctly.
270  Json::Value nonempty_target_json;
271  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
272  nonempty_target_json["length"] = 1;
273  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
274  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
275  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
276  EXPECT_EQ(http->counter, 1);
277 }
278 
279 /* Don't bother downloading a target that is larger than the available disk
280  * space. */
281 TEST(Fetcher, NotEnoughDiskSpace) {
282  TemporaryDirectory temp_dir;
283  config.storage.path = temp_dir.Path();
284  config.uptane.repo_server = server;
285 
286  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
287  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
288  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
289  KeyManager keys(storage, config.keymanagerConfig());
290  Uptane::Fetcher fetcher(config, http);
291 
292  // Find how much space is available on disk.
293  struct statvfs stvfsbuf {};
294  EXPECT_EQ(statvfs(temp_dir.Path().c_str(), &stvfsbuf), 0);
295  const uint64_t available_bytes = (stvfsbuf.f_bsize * stvfsbuf.f_bavail);
296 
297  // Try to fetch a target larger than the available disk space: an exception is
298  // thrown and the http module is never called. Note the hash is bogus.
299  Json::Value empty_target_json;
300  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
301  empty_target_json["length"] = available_bytes * 2;
302  Uptane::Target empty_target("empty_file", empty_target_json);
303  EXPECT_FALSE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
304  EXPECT_NE(pacman->verifyTarget(empty_target), TargetStatus::kGood);
305  EXPECT_EQ(http->counter, 0);
306 
307  // Try to fetch a 1-byte target: download succeeds, and http module is called.
308  // This is done purely to make sure the test is designed correctly.
309  Json::Value nonempty_target_json;
310  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
311  nonempty_target_json["length"] = 1;
312  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
313  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
314  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
315  EXPECT_EQ(http->counter, 1);
316 }
317 
318 /* Abort downloading an OSTree target with the fake/binary package manager. */
319 TEST(Fetcher, DownloadOstreeFail) {
320  TemporaryDirectory temp_dir;
321  config.storage.path = temp_dir.Path();
322  config.uptane.repo_server = server;
323 
324  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
325  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
326  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
327  KeyManager keys(storage, config.keymanagerConfig());
328  Uptane::Fetcher fetcher(config, http);
329 
330  // Empty target: download succeeds, but http module is never called.
331  Json::Value empty_target_json;
332  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
333  empty_target_json["length"] = 0;
334  empty_target_json["custom"]["targetFormat"] = "OSTREE";
335  Uptane::Target empty_target("empty_file", empty_target_json);
336  EXPECT_FALSE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
337  EXPECT_NE(pacman->verifyTarget(empty_target), TargetStatus::kGood);
338  EXPECT_EQ(http->counter, 0);
339 
340  // Non-empty target: download succeeds, and http module is called. This is
341  // done purely to make sure the test is designed correctly.
342  Json::Value nonempty_target_json;
343  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
344  nonempty_target_json["length"] = 1;
345  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
346  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
347  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
348  EXPECT_EQ(http->counter, 1);
349 }
350 
351 #ifndef __NO_MAIN__
352 int main(int argc, char** argv) {
353  ::testing::InitGoogleTest(&argc, argv);
354 
355  logger_init();
356  logger_set_threshold(boost::log::trivial::debug);
357 
358  std::string port = TestUtils::getFreePort();
359  server += port;
360  boost::process::child http_server_process("tests/fake_http_server/fake_test_server.py", port, "-f");
361  TestUtils::waitForServer(server + "/");
362 #ifdef BUILD_OSTREE
363  std::string treehub_port = TestUtils::getFreePort();
364  treehub_server += treehub_port;
365  TemporaryDirectory treehub_dir;
366  boost::process::child ostree_server_process("tests/sota_tools/treehub_server.py", std::string("-p"), treehub_port,
367  std::string("-d"), treehub_dir.PathString(), std::string("-s0.5"),
368  std::string("--create"));
369  TemporaryDirectory temp_dir;
370  int r = system((std::string("ostree admin init-fs ") + temp_dir.PathString()).c_str());
371  if (r != 0) {
372  return -1;
373  }
374  r = system((std::string("ostree config --repo=") + temp_dir.PathString() +
375  std::string("/ostree/repo set core.mode bare-user-only"))
376  .c_str());
377  if (r != 0) {
378  return -1;
379  }
380  sysroot = temp_dir.Path().string();
381  TestUtils::waitForServer(treehub_server + "/");
382 #endif // BUILD_OSTREE
383  return RUN_ALL_TESTS();
384 }
385 #endif // __NO_MAIN__
Provides a thread-safe way to pause and terminate task execution.
Definition: apiqueue.h:19
bool setPause(bool set_paused)
Called by the controlling thread to request the task to pause or resume.
Definition: apiqueue.cc:6
Configuration object for an aktualizr instance running on a Primary ECU.
Definition: config.h:74
Results of libaktualizr API calls.
Definition: results.h:13