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