Aktualizr
C++ SOTA Client
packagemanagerinterface.cc
1 #include <sys/statvfs.h>
2 #include <chrono>
3 
4 #include "libaktualizr/packagemanagerinterface.h"
5 
6 #include "bootloader/bootloader.h"
7 #include "crypto/keymanager.h"
8 #include "http/httpclient.h"
9 #include "logging/logging.h"
10 #include "storage/invstorage.h"
11 #include "uptane/fetcher.h"
12 #include "utilities/apiqueue.h"
13 
15  public:
16  DownloadMetaStruct(Uptane::Target target_in, FetcherProgressCb progress_cb_in, const api::FlowControlToken* token_in)
17  : hash_type{target_in.hashes()[0].type()},
18  target{std::move(target_in)},
19  token{token_in},
20  progress_cb{std::move(progress_cb_in)},
21  time_lastreport{std::chrono::steady_clock::now()} {}
22  uintmax_t downloaded_length{0};
23  unsigned int last_progress{0};
24  std::ofstream fhandle;
25  const Hash::Type hash_type;
26  MultiPartHasher& hasher() {
27  switch (hash_type) {
28  case Hash::Type::kSha256:
29  return sha256_hasher;
30  case Hash::Type::kSha512:
31  return sha512_hasher;
32  default:
33  throw std::runtime_error("Unknown hash algorithm");
34  }
35  }
36  Uptane::Target target;
37  const api::FlowControlToken* token;
38  FetcherProgressCb progress_cb;
39  // each LogProgressInterval msec log dowload progress for big files
40  std::chrono::time_point<std::chrono::steady_clock> time_lastreport;
41 
42  private:
43  MultiPartSHA256Hasher sha256_hasher;
44  MultiPartSHA512Hasher sha512_hasher;
45 };
46 
47 static size_t DownloadHandler(char* contents, size_t size, size_t nmemb, void* userp) {
48  assert(userp);
49  auto* ds = static_cast<DownloadMetaStruct*>(userp);
50  size_t downloaded = size * nmemb;
51  uint64_t expected = ds->target.length();
52  if ((ds->downloaded_length + downloaded) > expected) {
53  return downloaded + 1; // curl will abort if return unexpected size;
54  }
55 
56  ds->fhandle.write(contents, static_cast<std::streamsize>(downloaded));
57  ds->hasher().update(reinterpret_cast<const unsigned char*>(contents), downloaded);
58  ds->downloaded_length += downloaded;
59  return downloaded;
60 }
61 
62 static constexpr int64_t LogProgressInterval = 15000;
63 
64 static int ProgressHandler(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) {
65  (void)dltotal;
66  (void)dlnow;
67  (void)ultotal;
68  (void)ulnow;
69  auto* ds = static_cast<DownloadMetaStruct*>(clientp);
70 
71  uint64_t expected = ds->target.length();
72  auto progress = static_cast<unsigned int>((ds->downloaded_length * 100) / expected);
73  if (ds->progress_cb && progress > ds->last_progress) {
74  ds->last_progress = progress;
75  ds->progress_cb(ds->target, "Downloading", progress);
76  // OTA-4864:Improve binary file download progress logging. Report each XX sec report event that notify user
77  auto now = std::chrono::steady_clock::now();
78  auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now - ds->time_lastreport);
79  if (milliseconds.count() > LogProgressInterval) {
80  LOG_INFO << "Download progress for file " << ds->target.filename() << ": " << progress << "%";
81  ds->time_lastreport = now;
82  }
83  }
84  if (ds->token != nullptr && !ds->token->canContinue(false)) {
85  return 1;
86  }
87  return 0;
88 }
89 
90 static void restoreHasherState(MultiPartHasher& hasher, std::ifstream data) {
91  static constexpr size_t buf_len = 1024;
92  std::array<uint8_t, buf_len> buf{};
93  do {
94  data.read(reinterpret_cast<char*>(buf.data()), buf.size());
95  hasher.update(buf.data(), static_cast<uint64_t>(data.gcount()));
96  } while (data.gcount() != 0);
97 }
98 
99 bool PackageManagerInterface::fetchTarget(const Uptane::Target& target, Uptane::Fetcher& fetcher,
100  const KeyManager& keys, const FetcherProgressCb& progress_cb,
101  const api::FlowControlToken* token) {
102  (void)keys;
103  bool result = false;
104  try {
105  if (target.hashes().empty()) {
106  throw Uptane::Exception("image", "No hash defined for the target");
107  }
108  TargetStatus exists = PackageManagerInterface::verifyTarget(target);
109  if (exists == TargetStatus::kGood) {
110  LOG_INFO << "Image already downloaded; skipping download";
111  return true;
112  }
113  std::unique_ptr<DownloadMetaStruct> ds = std_::make_unique<DownloadMetaStruct>(target, progress_cb, token);
114  if (target.length() == 0) {
115  LOG_INFO << "Skipping download of target with length 0";
116  ds->fhandle = createTargetFile(target);
117  return true;
118  }
119  if (exists == TargetStatus::kIncomplete) {
120  LOG_INFO << "Continuing incomplete download of file " << target.filename();
121  auto target_check = checkTargetFile(target);
122  ds->downloaded_length = target_check->first;
123  ::restoreHasherState(ds->hasher(), openTargetFile(target));
124  ds->fhandle = appendTargetFile(target);
125  } else {
126  // If the target was found, but is oversized or the hash doesn't match,
127  // just start over.
128  LOG_DEBUG << "Initiating download of file " << target.filename();
129  ds->fhandle = createTargetFile(target);
130  }
131 
132  const uint64_t required_bytes = target.length() - ds->downloaded_length;
133  if (!checkAvailableDiskSpace(required_bytes)) {
134  throw std::runtime_error("Insufficient disk space available to download target");
135  }
136 
137  std::string target_url = target.uri();
138  if (target_url.empty()) {
139  target_url = fetcher.getRepoServer() + "/targets/" + Utils::urlEncode(target.filename());
140  }
141 
142  HttpResponse response;
143  for (;;) {
144  response = http_->download(target_url, DownloadHandler, ProgressHandler, ds.get(),
145  static_cast<curl_off_t>(ds->downloaded_length));
146 
147  if (response.curl_code == CURLE_RANGE_ERROR) {
148  LOG_WARNING << "The image server doesn't support byte range requests,"
149  " try to download the image from the beginning: "
150  << target_url;
151  ds = std_::make_unique<DownloadMetaStruct>(target, progress_cb, token);
152  ds->fhandle = createTargetFile(target);
153  continue;
154  }
155 
156  if (!response.wasInterrupted()) {
157  break;
158  }
159  ds->fhandle.close();
160  // sleep if paused or abort the download
161  if (!token->canContinue()) {
162  throw Uptane::Exception("image", "Download of a target was aborted");
163  }
164  ds->fhandle = appendTargetFile(target);
165  }
166  LOG_TRACE << "Download status: " << response.getStatusStr() << std::endl;
167  if (!response.isOk()) {
168  if (response.curl_code == CURLE_WRITE_ERROR) {
169  throw Uptane::OversizedTarget(target.filename());
170  }
171  throw Uptane::Exception("image", "Could not download file, error: " + response.error_message);
172  }
173  if (!target.MatchHash(Hash(ds->hash_type, ds->hasher().getHexDigest()))) {
174  ds->fhandle.close();
175  removeTargetFile(target);
176  throw Uptane::TargetHashMismatch(target.filename());
177  }
178  ds->fhandle.close();
179  result = true;
180  } catch (const std::exception& e) {
181  LOG_WARNING << "Error while downloading a target: " << e.what();
182  }
183  return result;
184 }
185 
186 TargetStatus PackageManagerInterface::verifyTarget(const Uptane::Target& target) const {
187  auto target_exists = checkTargetFile(target);
188  if (!target_exists) {
189  LOG_DEBUG << "File " << target.filename() << " with expected hash not found in the database.";
190  return TargetStatus::kNotFound;
191  } else if (target_exists->first < target.length()) {
192  LOG_DEBUG << "File " << target.filename() << " was found in the database, but is incomplete.";
193  return TargetStatus::kIncomplete;
194  } else if (target_exists->first > target.length()) {
195  LOG_DEBUG << "File " << target.filename() << " was found in the database, but is oversized.";
196  return TargetStatus::kOversized;
197  }
198 
199  // Even if the file exists and the length matches, recheck the hash.
200  DownloadMetaStruct ds(target, nullptr, nullptr);
201  ds.downloaded_length = target_exists->first;
202  ::restoreHasherState(ds.hasher(), openTargetFile(target));
203  if (!target.MatchHash(Hash(ds.hash_type, ds.hasher().getHexDigest()))) {
204  LOG_ERROR << "Target exists with expected length, but hash does not match metadata! " << target;
205  return TargetStatus::kHashMismatch;
206  }
207 
208  return TargetStatus::kGood;
209 }
210 
211 bool PackageManagerInterface::checkAvailableDiskSpace(const uint64_t required_bytes) const {
212  struct statvfs stvfsbuf {};
213  const int stat_res = statvfs(config.images_path.c_str(), &stvfsbuf);
214  if (stat_res < 0) {
215  LOG_WARNING << "Unable to read filesystem statistics: error code " << stat_res;
216  return true;
217  }
218  const uint64_t available_bytes = (static_cast<uint64_t>(stvfsbuf.f_bsize) * stvfsbuf.f_bavail);
219  const uint64_t reserved_bytes = 1 << 20;
220 
221  if (required_bytes + reserved_bytes < available_bytes) {
222  return true;
223  } else {
224  LOG_ERROR << "Insufficient disk space available to download target! Required: " << required_bytes
225  << ", available: " << available_bytes << ", reserved: " << reserved_bytes;
226  return false;
227  }
228 }
229 
230 boost::optional<std::pair<uintmax_t, std::string>> PackageManagerInterface::checkTargetFile(
231  const Uptane::Target& target) const {
232  std::string filename = storage_->getTargetFilename(target.filename());
233  if (!filename.empty()) {
234  auto path = config.images_path / filename;
235  if (boost::filesystem::exists(path)) {
236  return {{boost::filesystem::file_size(path), path.string()}};
237  }
238  }
239  return boost::none;
240 }
241 
242 std::ifstream PackageManagerInterface::openTargetFile(const Uptane::Target& target) const {
243  auto file = checkTargetFile(target);
244  if (!file) {
245  throw std::runtime_error("File doesn't exist for target " + target.filename());
246  }
247  std::ifstream stream(file->second, std::ios::binary);
248  if (!stream.good()) {
249  throw std::runtime_error("Can't open file " + file->second);
250  }
251  return stream;
252 }
253 
254 std::ofstream PackageManagerInterface::createTargetFile(const Uptane::Target& target) {
255  std::string filename = target.hashes()[0].HashString();
256  std::string filepath = (config.images_path / filename).string();
257  boost::filesystem::create_directories(config.images_path);
258  std::ofstream stream(filepath, std::ios::binary | std::ios::ate);
259  if (!stream.good()) {
260  throw std::runtime_error("Can't write to file " + filepath);
261  }
262  storage_->storeTargetFilename(target.filename(), filename);
263  return stream;
264 }
265 
266 std::ofstream PackageManagerInterface::appendTargetFile(const Uptane::Target& target) {
267  auto file = checkTargetFile(target);
268  if (!file) {
269  throw std::runtime_error("File doesn't exist for target " + target.filename());
270  }
271  std::ofstream stream(file->second, std::ios::binary | std::ios::app);
272  if (!stream.good()) {
273  throw std::runtime_error("Can't open file " + file->second);
274  }
275  return stream;
276 }
277 
278 void PackageManagerInterface::removeTargetFile(const Uptane::Target& target) {
279  auto file = checkTargetFile(target);
280  if (!file) {
281  throw std::runtime_error("File doesn't exist for target " + target.filename());
282  }
283  boost::filesystem::remove(file->second);
284  storage_->deleteTargetInfo(target.filename());
285 }
286 
287 std::vector<Uptane::Target> PackageManagerInterface::getTargetFiles() {
288  std::vector<Uptane::Target> v;
289  auto names = storage_->getAllTargetNames();
290  v.reserve(names.size());
291  for (const auto& name : names) {
292  v.emplace_back(name, Uptane::EcuMap{}, std::vector<Hash>{}, 0);
293  }
294  return v;
295 }
Uptane::Fetcher
Definition: fetcher.h:33
Hash
The Hash class The hash of a file or Uptane metadata.
Definition: types.h:159
KeyManager
Definition: keymanager.h:13
MultiPartSHA256Hasher
Definition: crypto.h:56
data
General data structures.
Definition: types.h:217
HttpResponse
Definition: httpinterface.h:17
MultiPartSHA512Hasher
Definition: crypto.h:38
Uptane::OversizedTarget
Definition: exceptions.h:48
DownloadMetaStruct
Definition: packagemanagerinterface.cc:14
api::FlowControlToken::canContinue
bool canContinue(bool blocking=true) const
Called by the controlled thread to query the currently requested state.
Definition: apiqueue.cc:33
Uptane::Exception
Definition: exceptions.h:10
api::FlowControlToken
Provides a thread-safe way to pause and terminate task execution.
Definition: apiqueue.h:19
Uptane::TargetHashMismatch
Definition: exceptions.h:41
result
Results of libaktualizr API calls.
Definition: results.h:12
Uptane::Target
Definition: types.h:379
MultiPartHasher
Definition: crypto.h:26