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