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  })
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, PACKAGE_MANAGER_OSTREE);
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)progress_cb;
234  (void)from;
235 
236  EXPECT_EQ(url, server + "/targets/fake_file");
237  const std::string content = "0";
238  write_cb(const_cast<char*>(&content[0]), 1, 1, userp);
239  counter++;
240  return HttpResponse(content, 200, CURLE_OK, "");
241  }
242 
243  int counter = 0;
244 };
245 
246 /* Don't bother downloading a target with length 0, but make sure verification
247  * still succeeds so that installation is possible. */
248 TEST(Fetcher, DownloadLengthZero) {
249  TemporaryDirectory temp_dir;
250  config.storage.path = temp_dir.Path();
251  config.uptane.repo_server = server;
252 
253  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
254  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
255  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
256  KeyManager keys(storage, config.keymanagerConfig());
257  Uptane::Fetcher fetcher(config, http);
258 
259  // Empty target: download succeeds, but http module is never called.
260  Json::Value empty_target_json;
261  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
262  empty_target_json["length"] = 0;
263  // Make sure this isn't confused for an old-style OSTree target.
264  empty_target_json["custom"]["targetFormat"] = "binary";
265  Uptane::Target empty_target("empty_file", empty_target_json);
266  EXPECT_TRUE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
267  EXPECT_EQ(pacman->verifyTarget(empty_target), TargetStatus::kGood);
268  EXPECT_EQ(http->counter, 0);
269 
270  // Non-empty target: download succeeds, and http module is called. This is
271  // done purely to make sure the test is designed correctly.
272  Json::Value nonempty_target_json;
273  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
274  nonempty_target_json["length"] = 1;
275  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
276  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
277  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
278  EXPECT_EQ(http->counter, 1);
279 }
280 
281 /* Don't bother downloading a target that is larger than the available disk
282  * space. */
283 TEST(Fetcher, NotEnoughDiskSpace) {
284  TemporaryDirectory temp_dir;
285  config.storage.path = temp_dir.Path();
286  config.uptane.repo_server = server;
287 
288  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
289  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
290  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
291  KeyManager keys(storage, config.keymanagerConfig());
292  Uptane::Fetcher fetcher(config, http);
293 
294  // Find how much space is available on disk.
295  struct statvfs stvfsbuf {};
296  EXPECT_EQ(statvfs(temp_dir.Path().c_str(), &stvfsbuf), 0);
297  const uint64_t available_bytes = (stvfsbuf.f_bsize * stvfsbuf.f_bavail);
298 
299  // Try to fetch a target larger than the available disk space: an exception is
300  // thrown and the http module is never called. Note the hash is bogus.
301  Json::Value empty_target_json;
302  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
303  empty_target_json["length"] = available_bytes * 2;
304  Uptane::Target empty_target("empty_file", empty_target_json);
305  EXPECT_FALSE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
306  EXPECT_NE(pacman->verifyTarget(empty_target), TargetStatus::kGood);
307  EXPECT_EQ(http->counter, 0);
308 
309  // Try to fetch a 1-byte target: download succeeds, and http module is called.
310  // This is done purely to make sure the test is designed correctly.
311  Json::Value nonempty_target_json;
312  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
313  nonempty_target_json["length"] = 1;
314  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
315  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
316  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
317  EXPECT_EQ(http->counter, 1);
318 }
319 
320 /* Abort downloading an OSTree target with the fake/binary package manager. */
321 TEST(Fetcher, DownloadOstreeFail) {
322  TemporaryDirectory temp_dir;
323  config.storage.path = temp_dir.Path();
324  config.uptane.repo_server = server;
325 
326  std::shared_ptr<INvStorage> storage(new SQLStorage(config.storage, false));
327  auto http = std::make_shared<HttpZeroLength>(temp_dir.Path());
328  auto pacman = std::make_shared<PackageManagerFake>(config.pacman, config.bootloader, storage, http);
329  KeyManager keys(storage, config.keymanagerConfig());
330  Uptane::Fetcher fetcher(config, http);
331 
332  // Empty target: download succeeds, but http module is never called.
333  Json::Value empty_target_json;
334  empty_target_json["hashes"]["sha256"] = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
335  empty_target_json["length"] = 0;
336  empty_target_json["custom"]["targetFormat"] = "OSTREE";
337  Uptane::Target empty_target("empty_file", empty_target_json);
338  EXPECT_FALSE(pacman->fetchTarget(empty_target, fetcher, keys, progress_cb, nullptr));
339  EXPECT_NE(pacman->verifyTarget(empty_target), TargetStatus::kGood);
340  EXPECT_EQ(http->counter, 0);
341 
342  // Non-empty target: download succeeds, and http module is called. This is
343  // done purely to make sure the test is designed correctly.
344  Json::Value nonempty_target_json;
345  nonempty_target_json["hashes"]["sha256"] = "5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9";
346  nonempty_target_json["length"] = 1;
347  Uptane::Target nonempty_target("fake_file", nonempty_target_json);
348  EXPECT_TRUE(pacman->fetchTarget(nonempty_target, fetcher, keys, progress_cb, nullptr));
349  EXPECT_EQ(pacman->verifyTarget(nonempty_target), TargetStatus::kGood);
350  EXPECT_EQ(http->counter, 1);
351 }
352 
353 #ifndef __NO_MAIN__
354 int main(int argc, char** argv) {
355  ::testing::InitGoogleTest(&argc, argv);
356 
357  logger_init();
358  logger_set_threshold(boost::log::trivial::debug);
359 
360  std::string port = TestUtils::getFreePort();
361  server += port;
362  boost::process::child http_server_process("tests/fake_http_server/fake_test_server.py", port, "-f");
363  TestUtils::waitForServer(server + "/");
364 #ifdef BUILD_OSTREE
365  std::string treehub_port = TestUtils::getFreePort();
366  treehub_server += treehub_port;
367  TemporaryDirectory treehub_dir;
368  boost::process::child ostree_server_process("tests/sota_tools/treehub_server.py", std::string("-p"), treehub_port,
369  std::string("-d"), treehub_dir.PathString(), std::string("-s0.5"),
370  std::string("--create"));
371  TemporaryDirectory temp_dir;
372  int r = system((std::string("ostree admin init-fs ") + temp_dir.PathString()).c_str());
373  if (r != 0) {
374  return -1;
375  }
376  r = system((std::string("ostree config --repo=") + temp_dir.PathString() +
377  std::string("/ostree/repo set core.mode bare-user-only"))
378  .c_str());
379  if (r != 0) {
380  return -1;
381  }
382  sysroot = temp_dir.Path().string();
383  TestUtils::waitForServer(treehub_server + "/");
384 #endif // BUILD_OSTREE
385  return RUN_ALL_TESTS();
386 }
387 #endif // __NO_MAIN__
Uptane::Fetcher
Definition: fetcher.h:33
HttpFake
Definition: httpfake.h:20
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:74
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:210
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