Aktualizr
C++ SOTA Client
All Classes Namespaces Files Functions Variables Enumerations Enumerator Pages
main.cc
1 #include <stdlib.h>
2 #include <iostream>
3 #include <random>
4 #include <sstream>
5 #include <string>
6 #include <utility>
7 
8 #include <boost/filesystem.hpp>
9 #include <boost/program_options.hpp>
10 
11 #include "json/json.h"
12 
13 #include "bootstrap/bootstrap.h"
14 #include "config/config.h"
15 #include "crypto/crypto.h"
16 #include "http/httpclient.h"
17 #include "logging/logging.h"
18 #include "utilities/aktualizr_version.h"
19 #include "utilities/utils.h"
20 
21 namespace bpo = boost::program_options;
22 
23 void checkInfoOptions(const bpo::options_description& description, const bpo::variables_map& vm) {
24  if (vm.count("help") != 0) {
25  std::cout << description << '\n';
26  exit(EXIT_SUCCESS);
27  }
28  if (vm.count("version") != 0) {
29  std::cout << "Current aktualizr-cert-provider version is: " << aktualizr_version() << "\n";
30  exit(EXIT_SUCCESS);
31  }
32 }
33 
34 bpo::variables_map parseOptions(int argc, char* argv[]) {
35  bpo::options_description description("aktualizr-cert-provider command line options");
36  // clang-format off
37  description.add_options()
38  ("help,h", "print usage")
39  ("version,v", "Current aktualizr-cert-provider version")
40  ("credentials,c", bpo::value<boost::filesystem::path>(), "zipped credentials file")
41  ("fleet-ca", bpo::value<boost::filesystem::path>(), "path to fleet certificate authority certificate (for signing device certificates)")
42  ("fleet-ca-key", bpo::value<boost::filesystem::path>(), "path to the private key of fleet certificate authority")
43  ("bits", bpo::value<int>(), "size of RSA keys in bits")
44  ("days", bpo::value<int>(), "validity term for the certificate in days")
45  ("certificate-c", bpo::value<std::string>(), "value for C field in certificate subject name")
46  ("certificate-st", bpo::value<std::string>(), "value for ST field in certificate subject name")
47  ("certificate-o", bpo::value<std::string>(), "value for O field in certificate subject name")
48  ("certificate-cn", bpo::value<std::string>(), "value for CN field in certificate subject name")
49  ("target,t", bpo::value<std::string>(), "target device to scp credentials to (or [user@]host)")
50  ("port,p", bpo::value<int>(), "target port")
51  ("directory,d", bpo::value<boost::filesystem::path>(), "directory on target to write credentials to (conflicts with -config)")
52  ("root-ca,r", "provide root CA")
53  ("server-url,u", "provide server url file")
54  ("local,l", bpo::value<boost::filesystem::path>(), "local directory to write credentials to")
55  ("config,g", bpo::value<std::vector<boost::filesystem::path> >()->composing(), "configuration file or directory from which to get file names")
56  ("skip-checks,s", "skip strict host key checking for ssh/scp commands");
57  // clang-format on
58 
59  bpo::variables_map vm;
60  std::vector<std::string> unregistered_options;
61  try {
62  bpo::basic_parsed_options<char> parsed_options =
63  bpo::command_line_parser(argc, argv).options(description).allow_unregistered().run();
64  bpo::store(parsed_options, vm);
65  checkInfoOptions(description, vm);
66  bpo::notify(vm);
67  unregistered_options = bpo::collect_unrecognized(parsed_options.options, bpo::include_positional);
68  if (vm.count("help") == 0 && !unregistered_options.empty()) {
69  std::cout << description << "\n";
70  exit(EXIT_FAILURE);
71  }
72  } catch (const bpo::required_option& ex) {
73  // print the error and append the default commandline option description
74  std::cout << ex.what() << std::endl << description;
75  exit(EXIT_FAILURE);
76  } catch (const bpo::error& ex) {
77  checkInfoOptions(description, vm);
78 
79  // print the error message to the standard output too, as the user provided
80  // a non-supported commandline option
81  std::cout << ex.what() << '\n';
82 
83  // set the returnValue, thereby ctest will recognize
84  // that something went wrong
85  exit(EXIT_FAILURE);
86  }
87 
88  return vm;
89 }
90 
91 // I miss Rust's ? operator
92 #define SSL_ERROR(description) \
93  { \
94  std::cerr << (description) << ERR_error_string(ERR_get_error(), nullptr) << std::endl; \
95  return false; \
96  }
97 bool generateAndSign(const std::string& cacert_path, const std::string& capkey_path, std::string* pkey,
98  std::string* cert, const bpo::variables_map& commandline_map) {
99  int rsa_bits = 2048;
100  if (commandline_map.count("bits") != 0) {
101  rsa_bits = (commandline_map["bits"].as<int>());
102  if (rsa_bits < 31) { // sic!
103  std::cerr << "RSA key size can't be smaller than 31 bits" << std::endl;
104  return false;
105  }
106  }
107 
108  int cert_days = 365;
109  if (commandline_map.count("days") != 0) {
110  cert_days = (commandline_map["days"].as<int>());
111  }
112 
113  std::string newcert_c;
114  if (commandline_map.count("certificate-c") != 0) {
115  newcert_c = (commandline_map["certificate-c"].as<std::string>());
116  if (newcert_c.length() != 2) {
117  std::cerr << "Country code (--certificate-c) should be 2 characters long" << std::endl;
118  return false;
119  }
120  };
121 
122  std::string newcert_st;
123  if (commandline_map.count("certificate-st") != 0) {
124  newcert_st = (commandline_map["certificate-st"].as<std::string>());
125  if (newcert_st.empty()) {
126  std::cerr << "State name (--certificate-st) can't be empty" << std::endl;
127  return false;
128  }
129  };
130 
131  std::string newcert_o;
132  if (commandline_map.count("certificate-o") != 0) {
133  newcert_o = (commandline_map["certificate-o"].as<std::string>());
134  if (newcert_o.empty()) {
135  std::cerr << "Organization name (--certificate-o) can't be empty" << std::endl;
136  return false;
137  }
138  };
139 
140  std::string newcert_cn;
141  if (commandline_map.count("certificate-cn") != 0) {
142  newcert_cn = (commandline_map["certificate-cn"].as<std::string>());
143  if (newcert_cn.empty()) {
144  std::cerr << "Common name (--certificate-cn) can't be empty" << std::endl;
145  return false;
146  }
147  } else {
148  newcert_cn = Utils::genPrettyName();
149  }
150 
151  // read CA certificate
152  std::string cacert_contents = Utils::readFile(cacert_path);
153  StructGuard<BIO> bio_in_cacert(BIO_new_mem_buf(cacert_contents.c_str(), static_cast<int>(cacert_contents.size())),
154  BIO_free_all);
155  StructGuard<X509> ca_certificate(PEM_read_bio_X509(bio_in_cacert.get(), nullptr, nullptr, nullptr), X509_free);
156  if (ca_certificate.get() == nullptr) {
157  std::cerr << "Reading CA certificate failed.\n";
158  return false;
159  }
160 
161  // read CA private key
162  std::string capkey_contents = Utils::readFile(capkey_path);
163  StructGuard<BIO> bio_in_capkey(BIO_new_mem_buf(capkey_contents.c_str(), static_cast<int>(capkey_contents.size())),
164  BIO_free_all);
165  StructGuard<EVP_PKEY> ca_privkey(PEM_read_bio_PrivateKey(bio_in_capkey.get(), nullptr, nullptr, nullptr),
166  EVP_PKEY_free);
167  if (ca_privkey.get() == nullptr) SSL_ERROR("PEM_read_bio_PrivateKey failed: ");
168 
169  // create certificate
170  StructGuard<X509> certificate(X509_new(), X509_free);
171  if (certificate.get() == nullptr) SSL_ERROR("X509_new failed: ");
172 
173  X509_set_version(certificate.get(), 2); // X509v3
174 
175  {
176  std::random_device urandom;
177  std::uniform_int_distribution<> serial_dist(0, (1UL << 20) - 1);
178  ASN1_INTEGER_set(X509_get_serialNumber(certificate.get()), serial_dist(urandom));
179  }
180 
181  // create and set certificate subject name
182  StructGuard<X509_NAME> subj(X509_NAME_new(), X509_NAME_free);
183  if (subj.get() == nullptr) SSL_ERROR("X509_NAME_new failed: ");
184 
185  if (!newcert_c.empty()) {
186  if (X509_NAME_add_entry_by_txt(subj.get(), "C", MBSTRING_ASC,
187  reinterpret_cast<const unsigned char*>(newcert_c.c_str()), -1, -1, 0) == 0)
188  SSL_ERROR("X509_NAME_add_entry_by_txt failed: ");
189  }
190 
191  if (!newcert_st.empty()) {
192  if (X509_NAME_add_entry_by_txt(subj.get(), "ST", MBSTRING_ASC,
193  reinterpret_cast<const unsigned char*>(newcert_st.c_str()), -1, -1, 0) == 0)
194  SSL_ERROR("X509_NAME_add_entry_by_txt failed: ");
195  }
196 
197  if (!newcert_o.empty()) {
198  if (X509_NAME_add_entry_by_txt(subj.get(), "O", MBSTRING_ASC,
199  reinterpret_cast<const unsigned char*>(newcert_o.c_str()), -1, -1, 0) == 0)
200  SSL_ERROR("X509_NAME_add_entry_by_txt failed: ");
201  }
202 
203  assert(!newcert_cn.empty());
204  if (X509_NAME_add_entry_by_txt(subj.get(), "CN", MBSTRING_ASC,
205  reinterpret_cast<const unsigned char*>(newcert_cn.c_str()), -1, -1, 0) == 0)
206  SSL_ERROR("X509_NAME_add_entry_by_txt failed: ");
207 
208  if (X509_set_subject_name(certificate.get(), subj.get()) == 0) SSL_ERROR("X509_set_subject_name failed: ");
209 
210  // set issuer name
211  X509_NAME* ca_subj = X509_get_subject_name(ca_certificate.get());
212  if (ca_subj == nullptr) SSL_ERROR("X509_get_subject_name failed: ");
213 
214  if (X509_set_issuer_name(certificate.get(), ca_subj) == 0) SSL_ERROR("X509_set_issuer_name failed: ");
215 
216  // create and set key
217 
218  // freed by owner EVP_PKEY
219  RSA* certificate_rsa = RSA_generate_key(rsa_bits, RSA_F4, nullptr, nullptr);
220  if (certificate_rsa == nullptr) SSL_ERROR("RSA_generate_key failed: ");
221 
222  StructGuard<EVP_PKEY> certificate_pkey(EVP_PKEY_new(), EVP_PKEY_free);
223  if (certificate_pkey.get() == nullptr) SSL_ERROR("EVP_PKEY_new failed: ");
224 
225  if (!EVP_PKEY_assign_RSA(certificate_pkey.get(), certificate_rsa)) // NOLINT
226  SSL_ERROR("EVP_PKEY_assign_RSA failed: ");
227 
228  if (X509_set_pubkey(certificate.get(), certificate_pkey.get()) == 0) SSL_ERROR("X509_set_pubkey failed: ");
229 
230  // set validity period
231  if (X509_gmtime_adj(X509_get_notBefore(certificate.get()), 0) == nullptr) SSL_ERROR("X509_gmtime_adj failed: ");
232 
233  if (X509_gmtime_adj(X509_get_notAfter(certificate.get()), 60L * 60L * 24L * cert_days) == nullptr)
234  SSL_ERROR("X509_gmtime_adj failed: ");
235 
236  // sign
237  const EVP_MD* cert_digest = EVP_sha256();
238  if (X509_sign(certificate.get(), ca_privkey.get(), cert_digest) == 0) SSL_ERROR("X509_sign failed: ");
239 
240  // serialize private key
241  char* privkey_buf;
242  StructGuard<BIO> privkey_file(BIO_new(BIO_s_mem()), BIO_vfree);
243  if (privkey_file == nullptr) {
244  std::cerr << "Error opening memstream" << std::endl;
245  return false;
246  }
247  int ret = PEM_write_bio_RSAPrivateKey(privkey_file.get(), certificate_rsa, nullptr, nullptr, 0, nullptr, nullptr);
248  if (ret == 0) {
249  std::cerr << "PEM_write_RSAPrivateKey" << std::endl;
250  return false;
251  }
252  auto privkey_len = BIO_get_mem_data(privkey_file.get(), &privkey_buf); // NOLINT
253  *pkey = std::string(privkey_buf, static_cast<size_t>(privkey_len));
254 
255  // serialize certificate
256  char* cert_buf;
257  StructGuard<BIO> cert_file(BIO_new(BIO_s_mem()), BIO_vfree);
258  if (cert_file == nullptr) {
259  std::cerr << "Error opening memstream" << std::endl;
260  return false;
261  }
262  ret = PEM_write_bio_X509(cert_file.get(), certificate.get());
263  if (ret == 0) {
264  std::cerr << "PEM_write_X509" << std::endl;
265  return false;
266  }
267  auto cert_len = BIO_get_mem_data(cert_file.get(), &cert_buf); // NOLINT
268  *cert = std::string(cert_buf, static_cast<size_t>(cert_len));
269 
270  return true;
271 }
272 
273 class SSHRunner {
274  public:
275  SSHRunner(std::string target, const bool skip_checks, const int port = 22)
276  : target_(std::move(target)), skip_checks_(skip_checks), port_(port) {}
277 
278  void runCmd(const std::string& cmd) const {
279  std::ostringstream prefix;
280 
281  prefix << "ssh ";
282  if (port_ != 22) {
283  prefix << "-p " << port_ << " ";
284  }
285  if (skip_checks_) {
286  prefix << "-o StrictHostKeyChecking=no ";
287  }
288  prefix << target_ << " ";
289 
290  std::string fullCmd = prefix.str() + cmd;
291  std::cout << "Running " << fullCmd << std::endl;
292 
293  int ret = system(fullCmd.c_str());
294  if (ret != 0) {
295  throw std::runtime_error("Error running command on " + target_ + ": " + std::to_string(ret));
296  }
297  }
298 
299  void transferFile(const boost::filesystem::path& inFile, const boost::filesystem::path& targetPath) {
300  // create parent directory
301  runCmd("mkdir -p " + targetPath.parent_path().string());
302 
303  std::ostringstream prefix;
304 
305  prefix << "scp ";
306  if (port_ != 22) {
307  prefix << "-P " << port_ << " ";
308  }
309  if (skip_checks_) {
310  prefix << "-o StrictHostKeyChecking=no ";
311  }
312 
313  std::string fullCmd = prefix.str() + inFile.string() + " " + target_ + ":" + targetPath.string();
314  std::cout << "Running " << fullCmd << std::endl;
315 
316  int ret = system(fullCmd.c_str());
317  if (ret != 0) {
318  throw std::runtime_error("Error copying file on " + target_ + ": " + std::to_string(ret));
319  }
320  }
321 
322  private:
323  std::string target_;
324  bool skip_checks_;
325  int port_;
326 };
327 
328 void copyLocal(const boost::filesystem::path& src, const boost::filesystem::path& dest) {
329  boost::filesystem::path dest_dir = dest.parent_path();
330  if (boost::filesystem::exists(dest_dir)) {
331  boost::filesystem::remove(dest);
332  } else {
333  boost::filesystem::create_directories(dest_dir);
334  }
335  boost::filesystem::copy_file(src, dest);
336 }
337 
338 int main(int argc, char* argv[]) {
339  int exit_code = EXIT_FAILURE;
340 
341  logger_init();
342  logger_set_threshold(static_cast<boost::log::trivial::severity_level>(2));
343 
344  try {
345  bpo::variables_map commandline_map = parseOptions(argc, argv);
346 
347  std::string target;
348  if (commandline_map.count("target") != 0) {
349  target = commandline_map["target"].as<std::string>();
350  }
351  int port = 22;
352  if (commandline_map.count("port") != 0) {
353  port = (commandline_map["port"].as<int>());
354  }
355  const bool provide_ca = commandline_map.count("root-ca") != 0;
356  const bool provide_url = commandline_map.count("server-url") != 0;
357  boost::filesystem::path local_dir;
358  if (commandline_map.count("local") != 0) {
359  local_dir = commandline_map["local"].as<boost::filesystem::path>();
360  }
361  std::vector<boost::filesystem::path> config_path;
362  if (commandline_map.count("config") != 0) {
363  config_path = commandline_map["config"].as<std::vector<boost::filesystem::path>>();
364  }
365  const bool skip_checks = commandline_map.count("skip-checks") != 0;
366 
367  boost::filesystem::path fleet_ca_path = "";
368  if (commandline_map.count("fleet-ca") != 0) {
369  fleet_ca_path = commandline_map["fleet-ca"].as<boost::filesystem::path>();
370  }
371 
372  boost::filesystem::path fleet_ca_key_path = "";
373  if (commandline_map.count("fleet-ca-key") != 0) {
374  fleet_ca_key_path = commandline_map["fleet-ca-key"].as<boost::filesystem::path>();
375  }
376 
377  if (fleet_ca_path.empty() != fleet_ca_key_path.empty()) {
378  std::cerr << "fleet-ca and fleet-ca-key options should be used together" << std::endl;
379  return 1;
380  }
381 
382  if (!commandline_map["directory"].empty() && !commandline_map["config"].empty()) {
383  std::cerr << "Directory (--directory) and config (--config) options cannot be used together" << std::endl;
384  return EXIT_FAILURE;
385  }
386 
387  boost::filesystem::path credentials_path = "";
388  if (commandline_map.count("credentials") != 0) {
389  credentials_path = commandline_map["credentials"].as<boost::filesystem::path>();
390  }
391 
392  if (local_dir.empty() && target.empty()) {
393  std::cerr << "Please provide a local directory and/or target to output the generated files to" << std::endl;
394  return EXIT_FAILURE;
395  }
396 
397  std::string serverUrl;
398  if ((fleet_ca_path.empty() || provide_ca || provide_url) && credentials_path.empty()) {
399  std::cerr
400  << "Error: missing -c/--credentials parameters which is mandatory if the fleet CA is not specified or an "
401  "output of the root CA or a gateway URL is requested";
402  return EXIT_FAILURE;
403  } else {
404  serverUrl = Bootstrap::readServerUrl(credentials_path);
405  }
406 
407  boost::filesystem::path directory = "/var/sota/import";
408  BasedPath pkey_file = BasedPath("pkey.pem");
409  BasedPath cert_file = BasedPath("client.pem");
410  BasedPath ca_file = BasedPath("root.crt");
411  BasedPath url_file = BasedPath("gateway.url");
412  if (!config_path.empty()) {
413  Config config(config_path);
414  // TODO: provide path to root directory in `--local` parameter
415 
416  // try first import base path and then storage path
417  if (!config.import.base_path.empty()) {
418  directory = config.import.base_path;
419  } else if (!config.storage.path.empty()) {
420  directory = config.storage.path;
421  }
422 
423  if (!config.import.tls_pkey_path.empty()) {
424  pkey_file = config.import.tls_pkey_path;
425  } else {
426  pkey_file = config.storage.tls_pkey_path;
427  }
428 
429  if (!config.import.tls_clientcert_path.empty()) {
430  cert_file = config.import.tls_clientcert_path;
431  } else {
432  cert_file = config.storage.tls_clientcert_path;
433  }
434  if (provide_ca) {
435  if (!config.import.tls_cacert_path.empty()) {
436  ca_file = config.import.tls_cacert_path;
437  } else {
438  ca_file = config.storage.tls_cacert_path;
439  }
440  }
441  if (provide_url) {
442  url_file = config.tls.server_url_path;
443  }
444  }
445 
446  if (!commandline_map["directory"].empty()) {
447  directory = commandline_map["directory"].as<boost::filesystem::path>();
448  }
449 
450  TemporaryFile tmp_pkey_file(pkey_file.get("").filename().string());
451  TemporaryFile tmp_cert_file(cert_file.get("").filename().string());
452  TemporaryFile tmp_ca_file(ca_file.get("").filename().string());
453  TemporaryFile tmp_url_file(url_file.get("").filename().string());
454 
455  std::string pkey;
456  std::string cert;
457  std::string ca;
458 
459  if (fleet_ca_path.empty()) { // no fleet CA => provision with shared credentials
460 
461  std::string device_id = Utils::genPrettyName();
462  std::cout << "Random device ID is " << device_id << "\n";
463 
464  Bootstrap boot(credentials_path, "");
465  HttpClient http;
466  Json::Value data;
467  data["deviceId"] = device_id;
468  data["ttl"] = 36000;
469 
470  std::cout << "Provisioning against server...\n";
471  http.setCerts(boot.getCa(), CryptoSource::kFile, boot.getCert(), CryptoSource::kFile, boot.getPkey(),
472  CryptoSource::kFile);
473  HttpResponse response = http.post(serverUrl + "/devices", data);
474  if (!response.isOk()) {
475  Json::Value resp_code = response.getJson()["code"];
476  if (resp_code.isString() && resp_code.asString() == "device_already_registered") {
477  std::cout << "Device ID" << device_id << "is occupied.\n";
478  return EXIT_FAILURE;
479  }
480  std::cout << "Provisioning failed, response: " << response.body << "\n";
481  return EXIT_FAILURE;
482  }
483  std::cout << "...success\n";
484 
485  StructGuard<BIO> device_p12(BIO_new_mem_buf(response.body.c_str(), static_cast<int>(response.body.size())),
486  BIO_vfree);
487  if (!Crypto::parseP12(device_p12.get(), "", &pkey, &cert, &ca)) {
488  std::cout << "Unable to parse p12 file received from server.\n";
489  return EXIT_FAILURE;
490  }
491  } else { // fleet CA set => generate and sign a new certificate
492  if (!generateAndSign(fleet_ca_path.native(), fleet_ca_key_path.native(), &pkey, &cert, commandline_map)) {
493  return EXIT_FAILURE;
494  }
495 
496  if (provide_ca) {
497  // Read server root CA from server_ca.pem in archive if found (to support
498  // community edition use case). Otherwise, default to the old version of
499  // expecting it to be in the p12.
500  ca = Bootstrap::readServerCa(credentials_path);
501  if (ca.empty()) {
502  Bootstrap boot(credentials_path, "");
503  ca = boot.getCa();
504  std::cout << "Server root CA read from autoprov_credentials.p12 in zipped archive.\n";
505  } else {
506  std::cout << "Server root CA read from server_ca.pem in zipped archive.\n";
507  }
508  }
509  }
510 
511  tmp_pkey_file.PutContents(pkey);
512  tmp_cert_file.PutContents(cert);
513  if (provide_ca) {
514  tmp_ca_file.PutContents(ca);
515  }
516  if (provide_url) {
517  tmp_url_file.PutContents(serverUrl);
518  }
519 
520  if (!local_dir.empty()) {
521  auto pkey_file_path = local_dir / pkey_file.get(directory);
522  std::cout << "Writing the generated client private key to " << pkey_file_path << " ...\n";
523  copyLocal(tmp_pkey_file.PathString(), pkey_file_path);
524 
525  auto cert_file_path = local_dir / cert_file.get(directory);
526  std::cout << "Writing the generated and signed client certificate to " << cert_file_path << " ...\n";
527  copyLocal(tmp_cert_file.PathString(), cert_file_path);
528 
529  if (provide_ca) {
530  auto root_ca_file = local_dir / ca_file.get(directory);
531  std::cout << "Writing the server root CA to " << root_ca_file << " ...\n";
532  copyLocal(tmp_ca_file.PathString(), root_ca_file);
533  }
534  if (provide_url) {
535  auto gtw_url_file = local_dir / url_file.get("");
536  std::cout << "Writing the gateway URL to " << gtw_url_file << " ...\n";
537  copyLocal(tmp_url_file.PathString(), gtw_url_file);
538  }
539  std::cout << "...success\n";
540  }
541 
542  if (!target.empty()) {
543  std::cout << "Copying client certificate and keys to " << target << ":" << directory;
544  if (port != 0) {
545  std::cout << " on port " << port;
546  }
547  std::cout << " ...\n";
548 
549  SSHRunner ssh{target, skip_checks, port};
550 
551  try {
552  ssh.transferFile(tmp_pkey_file.Path(), pkey_file.get(directory));
553 
554  ssh.transferFile(tmp_cert_file.Path(), cert_file.get(directory));
555 
556  if (provide_ca) {
557  ssh.transferFile(tmp_ca_file.Path(), ca_file.get(directory));
558  }
559  if (provide_url) {
560  ssh.transferFile(tmp_url_file.Path(), url_file.get(directory));
561  }
562 
563  std::cout << "...success\n";
564  } catch (const std::runtime_error& exc) {
565  std::cout << exc.what() << std::endl;
566  }
567  }
568 
569  exit_code = EXIT_SUCCESS;
570  } catch (const std::exception& exc) {
571  LOG_ERROR << "Error: " << exc.what();
572 
573  exit_code = EXIT_FAILURE;
574  }
575 
576  return exit_code;
577 }
SSHRunner
Definition: main.cc:273
BasedPath
Definition: utils.h:101
data
General data structures.
Definition: types.cc:44
HttpResponse
Definition: httpinterface.h:17
Config
Configuration object for an aktualizr instance running on a primary ECU.
Definition: config.h:73
HttpClient
Definition: httpclient.h:28
Bootstrap
Definition: bootstrap.h:7
TemporaryFile
RAII Temporary file creation.
Definition: utils.h:68