-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathupdater.cpp
More file actions
1106 lines (871 loc) · 45 KB
/
Copy pathupdater.cpp
File metadata and controls
1106 lines (871 loc) · 45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// system headers+
#include <algorithm>
#include <chrono>
#include <deque>
#include <fnmatch.h>
#include <fstream>
#include <iostream>
#include <libgen.h>
#include <mutex>
#include <sstream>
#include <thread>
#include <unistd.h>
// library headers
#include <zsclient.h>
#include <hashlib/sha256.h>
#include <cpr/cpr.h>
#include <ftw.h>
// local headers
#include "appimage/update.h"
#include "util.h"
// convenience declaration
typedef std::lock_guard<std::mutex> lock_guard;
namespace appimage {
namespace update {
class Updater::Private {
public:
Private() : state(INITIALIZED),
pathToAppImage(),
zSyncClient(nullptr),
thread(nullptr),
mutex(),
overwrite(false)
{};
~Private() {
delete zSyncClient;
}
public:
// data
std::string pathToAppImage;
// state
State state;
// ZSync client -- will be instantiated only if necessary
zsync2::ZSyncClient* zSyncClient;
// threading
std::thread* thread;
std::mutex mutex;
// status messages
std::deque<std::string> statusMessages;
// defines whether to overwrite original file
bool overwrite;
public:
enum UpdateInformationType {
INVALID = -1,
ZSYNC_GENERIC = 0,
ZSYNC_GITHUB_RELEASES,
ZSYNC_BINTRAY,
};
struct AppImage {
std::string filename;
int appImageVersion;
std::string rawUpdateInformation;
UpdateInformationType updateInformationType;
std::string zsyncUrl;
std::string signature;
AppImage() : appImageVersion(-1), updateInformationType(INVALID) {};
};
typedef struct AppImage AppImage;
public:
void issueStatusMessage(const std::string& message) {
statusMessages.push_back(message);
}
static const std::string readAppImageSignature(const std::string& pathToAppImage) {
return readElfSection(pathToAppImage, ".sha256_sig");
}
static const std::string hashAppImage(const std::string& pathToAppImage) {
// read offset and length of signature section to skip it later
unsigned long sigOffset = 0, sigLength = 0;
unsigned long keyOffset = 0, keyLength = 0;
if (!appimage_get_elf_section_offset_and_length(pathToAppImage.c_str(), ".sha256_sig", &sigOffset, &sigLength))
return "";
if (!appimage_get_elf_section_offset_and_length(pathToAppImage.c_str(), ".sig_key", &keyOffset, &keyLength))
return "";
std::ifstream ifs(pathToAppImage);
if (!ifs)
return "";
SHA256 digest;
// validate.c uses "offset" as chunk size, but that value might be quite high, and therefore uses
// a lot of memory
// TODO: use a smaller value (maybe use a prime factorization and use the biggest prime factor?)
const ssize_t chunkSize = 4096;
std::vector<char> buffer(chunkSize, 0);
ssize_t totalBytesRead = 0;
// bytes that should be skipped when reading the next chunk
// when e.g., a section that must be ignored spans over more than one chunk, this amount of bytes is
// being nulled & skipped before reading data from the file again
std::streamsize bytesToSkip = 0;
ifs.seekg(0, ifs.end);
const ssize_t fileSize = ifs.tellg();
ifs.seekg(0, ifs.beg);
while (ifs) {
ssize_t bytesRead = 0;
auto bytesLeftInChunk = std::min(chunkSize, (fileSize - totalBytesRead));
if (bytesLeftInChunk <= 0)
break;
auto skipBytes = [&bytesRead, &bytesLeftInChunk, &buffer, &ifs, &totalBytesRead](ssize_t count) {
if (count <= 0)
return;
const auto start = buffer.begin() + bytesRead;
std::fill_n(start, count, '\0');
bytesRead += count;
totalBytesRead += count;
bytesLeftInChunk -= count;
ifs.seekg(count, std::ios::cur);
};
auto readBytes = [&bytesRead, &bytesLeftInChunk, &buffer, &ifs, &totalBytesRead](ssize_t count) {
if (count <= 0)
return;
ifs.read(buffer.data() + bytesRead, count);
bytesRead += ifs.gcount();
totalBytesRead += count;
bytesLeftInChunk -= bytesRead;
};
auto checkSkipSection = [&](const ssize_t sectionOffset, const ssize_t sectionLength) {
// check whether signature starts in current chunk
const ssize_t sectionOffsetDelta = sectionOffset - totalBytesRead;
if (sectionOffsetDelta >= 0 && sectionOffsetDelta < bytesLeftInChunk) {
// read until section begins
readBytes(sectionOffsetDelta);
// calculate how many bytes must be nulled in this chunk
// the rest will be nulled in the following chunk(s)
auto bytesLeft = sectionLength;
const auto bytesToNullInCurrentChunk = std::min(bytesLeftInChunk, bytesLeft);
// null these bytes
skipBytes(bytesToNullInCurrentChunk);
// calculate how many bytes must be nulled in future chunks
bytesLeft -= bytesToNullInCurrentChunk;
bytesToSkip = bytesLeft;
}
};
// check whether bytes must be skipped from previous sections
if (bytesToSkip > 0) {
auto bytesToSkipInCurrentChunk = std::min(chunkSize, bytesToSkip);
skipBytes(bytesToSkipInCurrentChunk);
bytesToSkip -= bytesToSkipInCurrentChunk;
}
// check whether one of the sections that must be skipped are in the current chunk, and if they
// are, skip those sections in the current and future sections
checkSkipSection(sigOffset, sigLength);
checkSkipSection(keyOffset, keyLength);
// read remaining bytes in chunk, given the file has still data to be read
if (ifs && bytesLeftInChunk > 0) {
readBytes(bytesLeftInChunk);
}
// update hash with data from buffer
digest.add(buffer.data(), static_cast<size_t>(bytesRead));
}
return digest.getHash();
}
const AppImage* readAppImage(const std::string& pathToAppImage) {
// error state: empty AppImage path
if (pathToAppImage.empty()) {
issueStatusMessage("Path to AppImage must not be empty.");
return nullptr;
}
// check whether file exists
std::ifstream ifs(pathToAppImage);
// if file can't be opened, it's an error
if (!ifs || !ifs.good()) {
issueStatusMessage("Failed to access AppImage file: " + pathToAppImage);
return nullptr;
}
// read magic number
ifs.seekg(8, std::ios::beg);
std::vector<char> magicByte(4, '\0');
ifs.read(magicByte.data(), 3);
// validate first two bytes are A and I
if (magicByte[0] != 'A' && magicByte[1] != 'I') {
std::ostringstream oss;
oss << "Invalid magic bytes: " << (int) magicByte[0] << (int) magicByte[1];
issueStatusMessage(oss.str());
}
int appImageType = -1;
// the third byte contains the version
switch (magicByte[2]) {
case '\x01':
appImageType = 1;
break;
case '\x02':
appImageType = 2;
break;
default:
// see fallback in the final else block in the next if construct
break;
}
// read update information in the file
std::string updateInformation;
// also read signature in the file
std::string signature;
if (appImageType == 1) {
// update information is always at the same position, and has a fixed length
static constexpr auto position = 0x8373;
static constexpr auto length = 512;
ifs.seekg(position);
std::vector<char> rawUpdateInformation(length, 0);
ifs.read(rawUpdateInformation.data(), length);
updateInformation = rawUpdateInformation.data();
} else if (appImageType == 2) {
// try to read ELF section .upd_info
updateInformation = readElfSection(pathToAppImage, ".upd_info");
// type 2 supports signatures, so we can extract it here, too
signature = readAppImageSignature(pathToAppImage);
} else {
// final try: type 1 AppImages do not have to set the magic bytes, although they should
// if the file is both an ELF and an ISO9660 file, we'll suspect it to be a type 1 AppImage, and
// proceed with a warning
static constexpr int elfMagicPos = 1;
static const std::string elfMagicValue = "ELF";
static constexpr int isoMagicPos = 32769;
static const std::string isoMagicValue = "CD001";
ifs.seekg(elfMagicPos);
std::vector<char> elfMagicPosData(elfMagicValue.size() + 1, '\0');
ifs.read(elfMagicPosData.data(), elfMagicValue.size());
auto elfMagicAvailable = (elfMagicPosData.data() == elfMagicValue);
ifs.seekg(isoMagicPos);
std::vector<char> isoMagicPosData(isoMagicValue.size() + 1, '\0');
ifs.read(isoMagicPosData.data(), isoMagicValue.size());
auto isoMagicAvailable = (isoMagicPosData.data() == isoMagicValue);
if (isoMagicAvailable) {
issueStatusMessage("Guessing AppImage type 1 or ISO");
appImageType = 1;
} else {
// all possible methods attempted, ultimately fail here
issueStatusMessage("No such AppImage type: " + std::to_string(magicByte[2]));
return nullptr;
}
}
UpdateInformationType uiType = INVALID;
std::string zsyncUrl;
// parse update information
auto uiParts = split(updateInformation, '|');
// make sure uiParts isn't empty
if (!uiParts.empty()) {
// TODO: GitHub releases type should consider pre-releases when there's no other types of releases
if (uiParts[0] == "gh-releases-zsync") {
// validate update information
if (uiParts.size() != 5) {
std::ostringstream oss;
oss << "Update information has invalid parameter count. Please contact the author of "
<< "the AppImage and ask them to revise the update information. They should consult "
<< "the AppImage specification, there might have been changes to the update"
<< "information.";
issueStatusMessage(oss.str());
} else {
uiType = ZSYNC_GITHUB_RELEASES;
auto username = uiParts[1];
auto repository = uiParts[2];
auto tag = uiParts[3];
auto filename = uiParts[4];
std::stringstream url;
url << "https://api.github.com/repos/" << username << "/" << repository << "/releases/";
if (tag.find("latest") != std::string::npos) {
issueStatusMessage("Fetching latest release information from GitHub API");
url << "latest";
} else {
std::ostringstream oss;
oss << "Fetching release information for tag \"" << tag << "\" from GitHub API.";
issueStatusMessage(oss.str());
url << "tags/" << tag;
}
auto response = cpr::Get(url.str());
// counter that will be evaluated later to give some meaningful feedback why parsing API
// response might have failed
int downloadUrlLines = 0;
int matchingUrls = 0;
// continue only if HTTP status is good
if (response.status_code >= 200 && response.status_code < 300) {
// in contrary to the original implementation, instead of converting wildcards into
// all-matching regular expressions, we have the power of fnmatch() available, a real wildcard
// implementation
// unfortunately, this is still hoping for GitHub's JSON API to return a pretty printed
// response which can be parsed like this
std::stringstream responseText(response.text);
std::string currentLine;
// not ideal, but allows for returning a match for the entire line
auto pattern = "*" + filename + "*";
// iterate through all lines to find a possible download URL and compare it to the pattern
while (std::getline(responseText, currentLine)) {
if (currentLine.find("browser_download_url") != std::string::npos) {
downloadUrlLines++;
if (fnmatch(pattern.c_str(), currentLine.c_str(), 0) == 0) {
matchingUrls++;
auto parts = split(currentLine, '"');
zsyncUrl = parts.back();
break;
}
}
}
} else {
issueStatusMessage("GitHub API request failed!");
}
if (downloadUrlLines <= 0) {
std::ostringstream oss;
oss << "Could not find any artifacts in release data. "
<< "Please contact the author of the AppImage and tell them the files are missing "
<< "on the releases page.";
issueStatusMessage(oss.str());
} else if (matchingUrls <= 0) {
std::ostringstream oss;
oss << "None of the artifacts matched the pattern in the update information. "
<< "The pattern is most likely invalid, e.g., due to changes in the filenames of "
<< "the AppImages. Please contact the author of the AppImage and ask them to "
<< "revise the update information.";
issueStatusMessage(oss.str());
} else if (zsyncUrl.empty()) {
// unlike that this code will ever be reached, the other two messages should cover all
// cases in which a ZSync URL is missing
// if it does, however, it's most likely that GitHub's API didn't return a URL
issueStatusMessage("Failed to parse GitHub's response.");
}
}
} else if (uiParts[0] == "bintray-zsync") {
// TODO: better error handling
if (uiParts.size() == 5) {
uiType = ZSYNC_BINTRAY;
auto username = uiParts[1];
auto repository = uiParts[2];
auto packageName = uiParts[3];
auto filename = uiParts[4];
std::stringstream downloadUrl;
downloadUrl << "https://dl.bintray.com/" << username << "/" << repository << "/"
<< filename;
if (downloadUrl.str().find("_latestVersion") == std::string::npos) {
zsyncUrl = downloadUrl.str();
} else {
std::stringstream redirectorUrl;
redirectorUrl << "https://bintray.com/" << username << "/" << repository << "/"
<< packageName << "/_latestVersion";
auto versionResponse = cpr::Head(redirectorUrl.str());
// this request is supposed to be redirected
// due to how cpr works, we can't check for a redirection status, as we get the response for
// the redirected request
// therefore, we check for a 2xx response, and then can inspect and compare the redirected URL
if (versionResponse.status_code >= 200 && versionResponse.status_code < 400) {
auto redirectedUrl = versionResponse.url;
// if they're different, it's probably been successful
if (redirectorUrl.str() != redirectedUrl) {
// the last part will contain the current version
auto packageVersion = static_cast<std::string>(split(redirectedUrl, '/').back());
auto urlTemplate = downloadUrl.str();
// split by _latestVersion, insert correct value, compose final value
auto pos = urlTemplate.find("_latestVersion");
auto firstPart = urlTemplate.substr(0, pos);
auto secondPart = urlTemplate.substr(pos + std::string("_latestVersion").length());
zsyncUrl = firstPart + packageVersion + secondPart;
}
}
}
}
} else if (uiParts[0] == "zsync") {
// validate update information
if (uiParts.size() == 2) {
uiType = ZSYNC_GENERIC;
zsyncUrl = uiParts.back();
} else {
std::ostringstream oss;
oss << "Update information has invalid parameter count. Please contact the author of "
<< "the AppImage and ask them to revise the update information. They should consult "
<< "the AppImage specification, there might have been changes to the update"
<< "information.";
issueStatusMessage(oss.str());
}
} else {
// unknown type
}
}
auto* appImage = new AppImage();
appImage->filename = pathToAppImage;
appImage->appImageVersion = appImageType;
appImage->rawUpdateInformation = updateInformation;
appImage->updateInformationType = uiType;
appImage->zsyncUrl = zsyncUrl;
appImage->signature = signature;
return appImage;
}
bool validateAppImage(AppImage const* appImage) {
// a null pointer is clearly a sign
if (appImage == nullptr) {
std::ostringstream oss;
oss << "Parsing AppImage failed. See previous message for details. "
<< "Are you sure the file is an AppImage?";
issueStatusMessage(oss.str());
return false;
}
// first check whether there's update information at all
if (appImage->rawUpdateInformation.empty()) {
std::ostringstream oss;
oss << "Could not find update information in the AppImage. "
<< "Please contact the author of the AppImage and ask them to embed update information.";
issueStatusMessage(oss.str());
return false;
}
// now check whether a ZSync URL could be composed by readAppImage
// this is the only supported update type at the moment
if (appImage->zsyncUrl.empty()) {
std::ostringstream oss;
oss << "ZSync URL not available. See previous messages for details.";
issueStatusMessage(oss.str());
return false;
}
// check whether update information is available
if (appImage->updateInformationType == INVALID) {
std::stringstream oss;
oss << "Could not detect update information type."
<< "Please contact the author of the AppImage and ask them whether the update information "
<< "is correct.";
issueStatusMessage(oss.str());
return false;
}
return true;
}
// thread runner
void runUpdate() {
// initialization
{
lock_guard guard(mutex);
// make sure it runs only once at a time
// should never occur, but you never know
if (state != INITIALIZED)
return;
// if there is a ZSync client (e.g., because an update check has been run), clean it up
// this ensures that a fresh instance will be used for the update run
if (zSyncClient != nullptr) {
delete zSyncClient;
zSyncClient = nullptr;
}
// WARNING: if you don't want to shoot yourself in the foot, make sure to read in the AppImage
// while locking the mutex and/or before the RUNNING state to make sure readAppImage() finishes
// before progress() and such can be called! Otherwise, progress() etc. will return an error state,
// causing e.g., main(), to interrupt the thread and finish.
auto* appImage = readAppImage(pathToAppImage);
if (!validateAppImage(appImage)) {
// cleanup
delete appImage;
state = ERROR;
return;
}
if (appImage->updateInformationType == ZSYNC_BINTRAY) {
issueStatusMessage("Updating from Bintray via ZSync");
} else if (appImage->updateInformationType == ZSYNC_GITHUB_RELEASES) {
issueStatusMessage("Updating from GitHub Releases via ZSync");
} else if (appImage->updateInformationType == ZSYNC_GENERIC) {
issueStatusMessage("Updating from generic server via ZSync");
}
if (appImage->updateInformationType == ZSYNC_GITHUB_RELEASES ||
appImage->updateInformationType == ZSYNC_BINTRAY ||
appImage->updateInformationType == ZSYNC_GENERIC) {
// doesn't matter which type it is exactly, they all work like the same
zSyncClient = new zsync2::ZSyncClient(appImage->zsyncUrl, pathToAppImage, overwrite);
// enable ranges optimizations
zSyncClient->setRangesOptimizationThreshold(64 * 4096);
// make sure the new AppImage goes into the same directory as the old one
// unfortunately, to be able to use dirname(), one has to copy the C string first
auto* path = strdup(appImage->filename.c_str());
std::string dirPath = dirname(path);
free(path);
zSyncClient->setCwd(dirPath);
} else {
// error unsupported type
issueStatusMessage("Error: update method not implemented");
// cleanup
delete appImage;
state = ERROR;
return;
}
// cleanup
delete appImage;
state = RUNNING;
}
// keep state -- by default, an error (false) is assumed
bool result = false;
// run phase
{
// check whether it's a zsync operation
if (zSyncClient != nullptr) {
result = zSyncClient->run();
}
}
// end phase
{
lock_guard guard(mutex);
if (result) {
state = SUCCESS;
} else {
state = ERROR;
}
}
}
bool checkForChanges(bool& updateAvailable, const unsigned int method = 0) {
lock_guard guard(mutex);
if (state != INITIALIZED)
return false;
// TODO: this code is somewhat duplicated in run()
// should probably be extracted to separate function
auto* appImage = readAppImage(pathToAppImage);
// validate AppImage
if(!validateAppImage(appImage))
return false;
if (appImage->updateInformationType == ZSYNC_GITHUB_RELEASES ||
appImage->updateInformationType == ZSYNC_BINTRAY ||
appImage->updateInformationType == ZSYNC_GENERIC) {
zSyncClient = new zsync2::ZSyncClient(appImage->zsyncUrl, pathToAppImage);
return zSyncClient->checkForChanges(updateAvailable, method);
}
zSyncClient = nullptr;
// return error in case of unknown update information
issueStatusMessage("Unknown update information type, aborting.");
return false;
}
};
Updater::Updater(const std::string& pathToAppImage, bool overwrite) {
// initialize data class
d = new Updater::Private();
// workaround for AppImageLauncher filesystem
d->pathToAppImage = ailfsRealpath(pathToAppImage);
d->overwrite = overwrite;
// check whether file exists, otherwise throw exception
std::ifstream f(d->pathToAppImage);
if(!f || !f.good()) {
auto errorMessage = std::strerror(errno);
throw std::invalid_argument(errorMessage + std::string(": ") + d->pathToAppImage);
}
}
Updater::~Updater() {
delete d;
}
void Updater::runUpdate() {
// alias for private function
return d->runUpdate();
}
bool Updater::start() {
// lock mutex
lock_guard guard(d->mutex);
// prevent multiple start calls
if(d->state != INITIALIZED)
return false;
// if there's a thread managed by this class already, should not start another one and lose access to
// this one
if(d->thread)
return false;
// create thread
d->thread = new std::thread(&Updater::runUpdate, this);
return true;
}
bool Updater::isDone() {
lock_guard guard(d->mutex);
return d->state != INITIALIZED && d->state != RUNNING && d->state != STOPPING;
}
bool Updater::hasError() {
lock_guard guard(d->mutex);
return d->state == ERROR;
}
bool Updater::progress(double& progress) {
lock_guard guard(d->mutex);
if (d->state == INITIALIZED) {
// this protects update checks from returning progress, which would only occur when using method 0
progress = 0;
return true;
} else if (d->state == SUCCESS || d->state == ERROR) {
progress = 1;
return true;
}
if (d->zSyncClient != nullptr) {
progress = d->zSyncClient->progress();
return true;
}
return false;
}
bool Updater::stop() {
throw std::runtime_error("not implemented");
}
bool Updater::nextStatusMessage(std::string& message) {
// first, check own message queue
if (!d->statusMessages.empty()) {
message = d->statusMessages.front();
d->statusMessages.pop_front();
return true;
}
// next, check zsync client for a message
if (d->zSyncClient != nullptr) {
std::string zsyncMessage;
if (!d->zSyncClient->nextStatusMessage(zsyncMessage))
return false;
// show that the message is coming from zsync2
message = "zsync2: " + zsyncMessage;
return true;
}
return false;
}
Updater::State Updater::state() {
return d->state;
}
bool Updater::checkForChanges(bool &updateAvailable, const unsigned int method) {
return d->checkForChanges(updateAvailable, method);
}
bool Updater::describeAppImage(std::string& description) const {
std::ostringstream oss;
auto* appImage = d->readAppImage(d->pathToAppImage);
if (appImage == nullptr)
return false;
oss << "Parsing file: " << appImage->filename << std::endl;
oss << "AppImage type: " << appImage->appImageVersion << std::endl;
oss << "Raw update information: ";
if (appImage->rawUpdateInformation.empty())
oss << "<empty>";
else
oss << appImage->rawUpdateInformation;
oss << std::endl;
oss << "Update information type: ";
if (appImage->updateInformationType == d->ZSYNC_GENERIC)
oss << "Generic ZSync URL";
else if (appImage->updateInformationType == d->ZSYNC_BINTRAY)
oss << "ZSync via Bintray";
else if (appImage->updateInformationType == d->ZSYNC_GITHUB_RELEASES)
oss << "ZSync via GitHub Releases";
else if (appImage->updateInformationType == d->INVALID)
oss << "Invalid (parsing failed/no update information available)";
else
oss << "Unknown error";
oss << std::endl;
if (!appImage->zsyncUrl.empty())
oss << "Assembled ZSync URL: " << appImage->zsyncUrl << std::endl;
else
oss << "Failed to assemble ZSync URL. AppImageUpdate can not be used with this AppImage.";
description = oss.str();
return true;
}
bool Updater::pathToNewFile(std::string& path) const {
// only available update method is via ZSync
if (d->zSyncClient)
return d->zSyncClient->pathToNewFile(path);
return false;
}
bool Updater::remoteFileSize(off_t& fileSize) const {
// only available update method is via ZSync
if (d->zSyncClient != nullptr)
return d->zSyncClient->remoteFileSize(fileSize);
return false;
}
Updater::ValidationState Updater::validateSignature() {
std::string pathToNewAppImage;
if (!this->pathToNewFile(pathToNewAppImage)) {
// return generic error
return VALIDATION_FAILED;
}
auto pathToOldAppImage = abspath(d->pathToAppImage);
if (pathToOldAppImage == pathToNewAppImage) {
pathToOldAppImage = pathToNewAppImage + ".zs-old";
}
auto oldSignature = d->readAppImageSignature(pathToOldAppImage);
auto newSignature = d->readAppImageSignature(pathToNewAppImage);
// remove any spaces and/or newline characters there might be on the left or right of the payload
zsync2::trim(oldSignature, '\n');
zsync2::trim(newSignature, '\n');
zsync2::trim(oldSignature);
zsync2::trim(newSignature);
auto oldSigned = !oldSignature.empty();
auto newSigned = !newSignature.empty();
auto oldDigest = d->hashAppImage(pathToOldAppImage);
auto newDigest = d->hashAppImage(pathToNewAppImage);
auto oldDigestOrig = readElfSection(pathToOldAppImage, ".sha256_sig");
auto newDigestOrig = readElfSection(pathToNewAppImage, ".sha256_sig");
std::string tempDir;
{
char* buffer;
char* pattern = strdup("/tmp/AppImageUpdate-XXXXXX");
if ((buffer = mkdtemp(pattern)) == nullptr) {
d->issueStatusMessage("Failed to create temporary directory");
return VALIDATION_TEMPDIR_CREATION_FAILED;
}
tempDir = buffer;
free(pattern);
}
auto tempFile = [&tempDir](const std::string& filename, const std::string& contents) {
std::stringstream path;
path << tempDir << "/" << filename;
auto x = path.str();
std::ofstream ofs(path.str());
ofs.write(contents.c_str(), contents.size());
return path.str();
};
if (!oldSigned && !newSigned)
return VALIDATION_NOT_SIGNED;
else if (oldSigned && !newSigned)
return VALIDATION_NO_LONGER_SIGNED;
// store digests and signatures in files so they can be passed to gpg2
auto oldDigestFilename = tempFile("old-digest", oldDigest);
auto newDigestFilename = tempFile("new-digest", newDigest);
auto oldSignatureFilename = tempFile("old-signature", oldSignature);
auto newSignatureFilename = tempFile("new-signature", newSignature);
auto cleanup = [&tempDir]() {
// cleanup
auto unlinkCb = [](const char *fpath, const struct stat *sb, int typeflag, struct FTW *ftwbuf) {
int rv = remove(fpath);
if (rv)
perror(fpath);
return rv;
};
nftw(tempDir.c_str(), unlinkCb, 64, FTW_DEPTH | FTW_PHYS);
rmdir(tempDir.c_str());
};
// find gpg2 binary
auto gpg2Path = findInPATH("gpg2");
if (gpg2Path.empty()) {
cleanup();
return VALIDATION_GPG2_MISSING;
}
const auto tempKeyRingPath = tempDir + "/keyring";
// create keyring file, otherwise GPG will complain
std::ofstream ofs(tempKeyRingPath);
ofs.close();
auto importKeyFromAppImage = [&tempKeyRingPath, &gpg2Path](const std::string& path) {
auto key = readElfSection(path, ".sig_key");
if (key.empty())
return false;
std::ostringstream oss;
oss << "'" << gpg2Path << "' "
<< "--no-default-keyring --keyring '" << tempKeyRingPath << "' --import";
auto command = oss.str();
auto proc = popen(oss.str().c_str(), "w");
fwrite(key.c_str(), key.size(), sizeof(char), proc);
auto retval = pclose(proc);
return retval == 0;
};
importKeyFromAppImage(pathToOldAppImage);
importKeyFromAppImage(pathToNewAppImage);
auto verifySignature = [this, &gpg2Path, &tempKeyRingPath](
const std::string& signatureFile, const std::string& digestFile,
bool& keyFound, bool& goodSignature,
std::string& keyID, std::string& keyOwner
) {
std::ostringstream oss;
oss << "'" << gpg2Path << "'"
<< " --keyring '" << tempKeyRingPath << "'"
<< " --verify '" << signatureFile << "' '" << digestFile << "' 2>&1";
auto command = oss.str();
// make sure output is in English
setenv("LANGUAGE", "C", 1);
setenv("LANG", "C", 1);
setenv("LC_ALL", "C", 1);
auto* proc = popen(command.c_str(), "r");
if (proc == nullptr)
return false;
char* currentLine = nullptr;
size_t lineSize = 0;
keyFound = true;
goodSignature = false;
while (getline(¤tLine, &lineSize, proc) != -1) {
std::string line = currentLine;
trim(line, '\n');
trim(line);
d->issueStatusMessage(std::string("gpg2: ") + line);
auto splitOwner = [&line]() {
auto parts = split(line, '"');
if (parts.size() < 2)
return std::string();
auto skip = parts[0].length();
return line.substr(skip + 1, line.length() - skip - 2);
};
if (stringStartsWith(line, "gpg: Signature made")) {
// extract key
auto parts = split(line);
if (*(parts.end() - 3) == "key" && *(parts.end() - 2) == "ID")
keyID = parts.back();
} else if (stringStartsWith(line, "gpg: Good signature from")) {
goodSignature = true;
keyOwner = splitOwner();
} else if (stringStartsWith(line, "gpg: BAD signature from")) {
goodSignature = false;
keyOwner = splitOwner();
} else if (stringStartsWith(line, "gpg: Can't check signature: No public key")) {
keyFound = false;
}
}
auto rv = pclose(proc);
return true;
};
bool oldKeyFound = false, newKeyFound = false, oldSignatureGood = false, newSignatureGood = false;
std::string oldKeyID, newKeyID, oldKeyOwner, newKeyOwner;
if (oldSigned) {