• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 1065424660

pending completion
1065424660

push

gitlab-ci

lluiscampos
fix(rootfs-image): Check for both config files

Not only the fallback one, which in some setup might not exist or not
have the RootfsPartA/B keys.

It is explicitly tested on an integration test.

Changelog: None
Ticket: MEN-6671

Signed-off-by: Lluis Campos <lluis.campos@northern.tech>
(cherry picked from commit b4e9864ed)

6958 of 8674 relevant lines covered (80.22%)

9278.98 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.78
/src/mender-update/http_resumer/http_resumer.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <mender-update/http_resumer.hpp>
16

17
#include <regex>
18

19
#include <common/common.hpp>
20
#include <common/expected.hpp>
21

22
namespace mender {
23
namespace update {
24
namespace http_resumer {
25

26
namespace common = mender::common;
27
namespace expected = mender::common::expected;
28
namespace http = mender::http;
29

30
// Represents the parts of a Content-Range HTTP header
31
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
32
struct RangeHeader {
33
        long long int range_start {0};
34
        long long int range_end {0};
35
        long long int size {0};
36
};
37
using ExpectedRangeHeader = expected::expected<RangeHeader, error::Error>;
38

39
// Parses the HTTP Content-Range header
40
// For an alternative implementation without regex dependency see:
41
// https://github.com/mendersoftware/mender/pull/1372/commits/ea711fc4dafa943266e9013fd6704da3d4518a27
42
ExpectedRangeHeader ParseRangeHeader(string header) {
41✔
43
        RangeHeader range_header {};
44

45
        std::regex content_range_regexp {R"(bytes\s+(\d+)\s?-\s?(\d+)\s?\/?\s?(\d+|\*)?)"};
82✔
46

47
        std::smatch range_matches;
48
        if (!regex_match(header, range_matches, content_range_regexp)) {
41✔
49
                return expected::unexpected(http::MakeError(
10✔
50
                        http::NoSuchHeaderError, "Invalid Content-Range returned from server: " + header));
30✔
51
        }
52

53
        auto exp_range_start = common::StringToLongLong(range_matches[1].str());
31✔
54
        auto exp_range_end = common::StringToLongLong(range_matches[2].str());
62✔
55
        if (!exp_range_start || !exp_range_end) {
31✔
56
                return expected::unexpected(http::MakeError(
×
57
                        http::NoSuchHeaderError, "Content-Range contains invalid number: " + header));
×
58
        }
59
        range_header.range_start = exp_range_start.value();
31✔
60
        range_header.range_end = exp_range_end.value();
31✔
61

62
        if (range_header.range_start > range_header.range_end) {
31✔
63
                return expected::unexpected(http::MakeError(
1✔
64
                        http::NoSuchHeaderError, "Invalid Content-Range returned from server: " + header));
3✔
65
        }
66

67
        if ((range_matches[3].matched) && (range_matches[3].str() != "*")) {
55✔
68
                auto exp_size = common::StringToLongLong(range_matches[3].str());
40✔
69
                if (!exp_size) {
20✔
70
                        return expected::unexpected(http::MakeError(
×
71
                                http::NoSuchHeaderError, "Content-Range contains invalid number: " + header));
×
72
                }
73
                range_header.size = exp_size.value();
20✔
74
        }
75

76
        return range_header;
77
}
78

79
class HeaderHandlerFunctor {
572✔
80
public:
81
        HeaderHandlerFunctor(weak_ptr<DownloadResumerClient> resumer) :
82
                resumer_client_ {resumer} {};
83

84
        void operator()(http::ExpectedIncomingResponsePtr exp_resp);
85

86
private:
87
        void HandleFirstResponse(
88
                const shared_ptr<DownloadResumerClient> &resumer_client,
89
                http::ExpectedIncomingResponsePtr exp_resp);
90
        void HandleNextResponse(
91
                const shared_ptr<DownloadResumerClient> &resumer_client,
92
                http::ExpectedIncomingResponsePtr exp_resp);
93

94
        weak_ptr<DownloadResumerClient> resumer_client_;
95
};
96

97
class BodyHandlerFunctor {
592✔
98
public:
99
        BodyHandlerFunctor(weak_ptr<DownloadResumerClient> resumer) :
100
                resumer_client_ {resumer} {};
101

102
        void operator()(http::ExpectedIncomingResponsePtr exp_resp);
103

104
private:
105
        weak_ptr<DownloadResumerClient> resumer_client_;
106
};
107

108
void HeaderHandlerFunctor::operator()(http::ExpectedIncomingResponsePtr exp_resp) {
140✔
109
        auto resumer_client = resumer_client_.lock();
140✔
110
        if (resumer_client) {
140✔
111
                // If an error has already occurred, schedule the next AsyncCall directly
112
                if (!exp_resp) {
138✔
113
                        resumer_client->logger_.Warning(exp_resp.error().String());
22✔
114
                        // resumer_client->CallUserHandler(exp_resp);
115
                        // return;
116
                        auto err = resumer_client->ScheduleNextResumeRequest();
11✔
117
                        if (err != error::NoError) {
11✔
118
                                resumer_client->logger_.Error(err.String());
2✔
119
                                resumer_client->CallUserHandler(expected::unexpected(err));
2✔
120
                        }
121
                        return;
122
                }
123

124
                if (resumer_client->resumer_state_->active_state == DownloadResumerActiveStatus::Resuming) {
127✔
125
                        HandleNextResponse(resumer_client, exp_resp);
84✔
126
                } else {
127
                        HandleFirstResponse(resumer_client, exp_resp);
170✔
128
                }
129
        }
130
}
131

132
void HeaderHandlerFunctor::HandleFirstResponse(
85✔
133
        const shared_ptr<DownloadResumerClient> &resumer_client,
134
        http::ExpectedIncomingResponsePtr exp_resp) {
135
        // The first response shall always call the user header callback. On resumable responses, we
136
        // create a our own incoming response and call the user header handler. On errors, we log a
137
        // warning and call the user handler with the original response
138

139
        auto resp = exp_resp.value();
85✔
140
        if (resp->GetStatusCode() != mender::http::StatusOK) {
85✔
141
                // Non-resumable response
142
                resumer_client->CallUserHandler(exp_resp);
2✔
143
                return;
2✔
144
        }
145

146
        auto exp_header = resp->GetHeader("Content-Length");
166✔
147
        if (!exp_header || exp_header.value() == "0") {
83✔
148
                resumer_client->logger_.Warning("Response does not contain Content-Length header");
2✔
149
                resumer_client->CallUserHandler(exp_resp);
1✔
150
                return;
1✔
151
        }
152

153
        auto exp_length = common::StringToLongLong(exp_header.value());
82✔
154
        if (!exp_length || exp_length.value() < 0) {
82✔
155
                resumer_client->logger_.Warning(
×
156
                        "Content-Length contains invalid number: " + exp_header.value());
×
157
                resumer_client->CallUserHandler(exp_resp);
×
158
                return;
159
        }
160

161
        // Resumable response
162
        resumer_client->resumer_state_->active_state = DownloadResumerActiveStatus::Resuming;
82✔
163
        resumer_client->resumer_state_->offset = 0;
82✔
164
        resumer_client->resumer_state_->content_length = exp_length.value();
82✔
165

166
        // Prepare a modified response and call user handler
167
        resumer_client->response_.reset(new http::IncomingResponse(*resumer_client, resp->cancelled_));
164✔
168
        resumer_client->response_->status_code_ = resp->GetStatusCode();
82✔
169
        resumer_client->response_->status_message_ = resp->GetStatusMessage();
164✔
170
        resumer_client->response_->headers_ = resp->GetHeaders();
171
        resumer_client->CallUserHandler(resumer_client->response_);
164✔
172
}
173

174
void HeaderHandlerFunctor::HandleNextResponse(
42✔
175
        const shared_ptr<DownloadResumerClient> &resumer_client,
176
        http::ExpectedIncomingResponsePtr exp_resp) {
177
        // If an error occurs during handling here, cancel the resuming and call the user handler.
178

179
        auto resp = exp_resp.value();
42✔
180
        auto resumer_reader = resumer_client->resumer_reader_.lock();
42✔
181
        if (!resumer_reader) {
42✔
182
                // Errors should already have been handled as part of the Cancel() inside the
183
                // destructor of the reader.
184
                return;
185
        }
186

187
        auto exp_content_range = resp->GetHeader("Content-Range").and_then(ParseRangeHeader);
84✔
188
        if (!exp_content_range) {
42✔
189
                resumer_client->logger_.Error(exp_content_range.error().String());
24✔
190
                resumer_client->CallUserHandler(expected::unexpected(exp_content_range.error()));
24✔
191
                return;
12✔
192
        }
193

194
        auto content_range = exp_content_range.value();
30✔
195
        if (content_range.size != 0
196
                && content_range.size != resumer_client->resumer_state_->content_length) {
30✔
197
                auto size_changed_err = http::MakeError(
198
                        http::DownloadResumerError,
199
                        "Size of artifact changed after download was resumed (expected "
200
                                + to_string(resumer_client->resumer_state_->content_length) + ", got "
4✔
201
                                + to_string(content_range.size) + ")");
8✔
202
                resumer_client->logger_.Error(size_changed_err.String());
4✔
203
                resumer_client->CallUserHandler(expected::unexpected(size_changed_err));
4✔
204
                return;
205
        }
206

207
        if ((content_range.range_end != resumer_client->resumer_state_->content_length - 1)
28✔
208
                || (content_range.range_start != resumer_client->resumer_state_->offset)) {
28✔
209
                auto bad_range_err = http::MakeError(
210
                        http::DownloadResumerError,
211
                        "HTTP server returned an different range than requested. Requested "
212
                                + to_string(resumer_client->resumer_state_->offset) + "-"
4✔
213
                                + to_string(resumer_client->resumer_state_->content_length - 1) + ", got "
8✔
214
                                + to_string(content_range.range_start) + "-" + to_string(content_range.range_end));
8✔
215
                resumer_client->logger_.Error(bad_range_err.String());
4✔
216
                resumer_client->CallUserHandler(expected::unexpected(bad_range_err));
4✔
217
                return;
218
        }
219

220
        // Get the reader for the new response
221
        auto exp_reader = resumer_client->client_.MakeBodyAsyncReader(resp);
52✔
222
        if (!exp_reader) {
26✔
223
                auto bad_range_err = exp_reader.error().WithContext("cannot get the reader after resume");
×
224
                resumer_client->logger_.Error(bad_range_err.String());
×
225
                resumer_client->CallUserHandler(expected::unexpected(bad_range_err));
×
226
                return;
227
        }
228
        // Update the inner reader of the user reader
229
        resumer_reader->inner_reader_ = exp_reader.value();
26✔
230

231
        // Resume reading reusing last user data (start, end, handler)
232
        auto err = resumer_reader->AsyncReadResume();
26✔
233
        if (err != error::NoError) {
26✔
234
                auto bad_read_err = err.WithContext("error reading after resume");
×
235
                resumer_client->logger_.Error(bad_read_err.String());
×
236
                resumer_client->CallUserHandler(expected::unexpected(bad_read_err));
×
237
                return;
238
        }
239
}
240

241
void BodyHandlerFunctor::operator()(http::ExpectedIncomingResponsePtr exp_resp) {
127✔
242
        auto resumer_client = resumer_client_.lock();
127✔
243
        if (!resumer_client) {
127✔
244
                return;
245
        }
246

247
        if (*resumer_client->cancelled_) {
127✔
248
                resumer_client->CallUserHandler(exp_resp);
72✔
249
                return;
72✔
250
        }
251

252
        if (resumer_client->resumer_state_->active_state == DownloadResumerActiveStatus::Inactive) {
55✔
253
                resumer_client->CallUserHandler(exp_resp);
2✔
254
                return;
2✔
255
        }
256

257
        // We resume the download if either:
258
        // * there is any error or
259
        // * successful read with status code Partial Content and there is still data missing
260
        const bool is_range_response =
261
                exp_resp && exp_resp.value()->GetStatusCode() == mender::http::StatusPartialContent;
53✔
262
        const bool is_data_missing =
263
                resumer_client->resumer_state_->offset < resumer_client->resumer_state_->content_length;
53✔
264
        if (!exp_resp || (is_range_response && is_data_missing)) {
53✔
265
                if (!exp_resp) {
44✔
266
                        auto resumer_reader = resumer_client->resumer_reader_.lock();
44✔
267
                        if (resumer_reader) {
44✔
268
                                resumer_reader->inner_reader_.reset();
44✔
269
                        }
270
                        if (exp_resp.error().code == make_error_condition(errc::operation_canceled)) {
44✔
271
                                // We don't want to resume cancelled requests, as these were
272
                                // cancelled for a reason.
273
                                resumer_client->CallUserHandler(exp_resp);
×
274
                                return;
275
                        }
276
                        resumer_client->logger_.Info(
88✔
277
                                "Will try to resume after error " + exp_resp.error().String());
88✔
278
                }
279

280
                auto err = resumer_client->ScheduleNextResumeRequest();
44✔
281
                if (err != error::NoError) {
44✔
282
                        resumer_client->logger_.Error(err.String());
×
283
                        resumer_client->CallUserHandler(expected::unexpected(err));
×
284
                        return;
285
                }
286
        } else {
287
                // Update headers with the last received server response. When resuming has taken place,
288
                // the user will get different headers on header and body handlers, representing (somehow)
289
                // what the resumer has been doing in its behalf.
290
                auto resp = exp_resp.value();
9✔
291
                resumer_client->response_->status_code_ = resp->GetStatusCode();
9✔
292
                resumer_client->response_->status_message_ = resp->GetStatusMessage();
18✔
293
                resumer_client->response_->headers_ = resp->GetHeaders();
294

295
                // Finished, call the user handler \o/
296
                resumer_client->logger_.Debug("Download resumed and completed successfully");
18✔
297
                resumer_client->CallUserHandler(resumer_client->response_);
18✔
298
        }
299
}
300

301
DownloadResumerAsyncReader::~DownloadResumerAsyncReader() {
243✔
302
        Cancel();
81✔
303
}
81✔
304

305
void DownloadResumerAsyncReader::Cancel() {
81✔
306
        auto resumer_client = resumer_client_.lock();
81✔
307
        if (!*cancelled_ && resumer_client) {
81✔
308
                resumer_client->Cancel();
54✔
309
        }
310
}
81✔
311

312
error::Error DownloadResumerAsyncReader::AsyncRead(
2,084✔
313
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end, io::AsyncIoHandler handler) {
314
        auto resumer_client = resumer_client_.lock();
2,084✔
315
        if (!resumer_client || *cancelled_) {
2,084✔
316
                return error::MakeError(
317
                        error::ProgrammingError,
318
                        "DownloadResumerAsyncReader::AsyncRead called after stream is destroyed");
×
319
        }
320
        // Save user parameters for further resumes of the body read
321
        resumer_client->last_read_ = {.start = start, .end = end, .handler = handler};
2,084✔
322
        return AsyncReadResume();
2,084✔
323
}
324

325
error::Error DownloadResumerAsyncReader::AsyncReadResume() {
2,110✔
326
        auto resumer_client = resumer_client_.lock();
2,110✔
327
        if (!resumer_client) {
2,110✔
328
                return error::MakeError(
329
                        error::ProgrammingError,
330
                        "DownloadResumerAsyncReader::AsyncReadResume called after client is destroyed");
×
331
        }
332
        return inner_reader_->AsyncRead(
333
                resumer_client->last_read_.start,
334
                resumer_client->last_read_.end,
335
                [this](io::ExpectedSize result) {
2,109✔
336
                        if (!result) {
2,109✔
337
                                logger_.Warning(
88✔
338
                                        "Reading error, a new request will be re-scheduled. "
339
                                        + result.error().String());
88✔
340
                        } else {
341
                                resumer_state_->offset += result.value();
2,065✔
342
                                logger_.Debug("read " + to_string(result.value()) + " bytes");
4,130✔
343
                                auto resumer_client = resumer_client_.lock();
2,065✔
344
                                if (resumer_client) {
2,065✔
345
                                        resumer_client->last_read_.handler(result);
4,130✔
346
                                } else {
347
                                        logger_.Error(
×
348
                                                "AsyncRead finish handler called after resumer client has been destroyed.");
×
349
                                }
350
                        }
351
                });
6,329✔
352
}
353

354
DownloadResumerClient::DownloadResumerClient(
124✔
355
        const http::ClientConfig &config, events::EventLoop &event_loop) :
356
        resumer_state_ {make_shared<DownloadResumerClientState>()},
357
        client_(config, event_loop, "http_resumer:client"),
358
        logger_ {"http_resumer:client"},
359
        cancelled_ {make_shared<bool>(true)},
124✔
360
        retry_ {
361
                .backoff = http::ExponentialBackoff(chrono::minutes(1), 10),
362
                .wait_timer = events::Timer(event_loop)} {
496✔
363
}
124✔
364

365
DownloadResumerClient::~DownloadResumerClient() {
248✔
366
        if (!*cancelled_) {
124✔
367
                logger_.Warning("DownloadResumerClient destroyed while request is still active!");
4✔
368
        }
369
        client_.Cancel();
124✔
370
}
124✔
371

372
error::Error DownloadResumerClient::AsyncCall(
87✔
373
        http::OutgoingRequestPtr req,
374
        http::ResponseHandler user_header_handler,
375
        http::ResponseHandler user_body_handler) {
376
        HeaderHandlerFunctor resumer_header_handler {shared_from_this()};
87✔
377
        BodyHandlerFunctor resumer_body_handler {shared_from_this()};
87✔
378

379
        user_request_ = req;
380
        user_header_handler_ = user_header_handler;
87✔
381
        user_body_handler_ = user_body_handler;
87✔
382

383
        if (!*cancelled_) {
87✔
384
                return error::Error(
385
                        make_error_condition(errc::operation_in_progress), "HTTP resumer call already ongoing");
×
386
        }
387

388
        *cancelled_ = false;
87✔
389
        retry_.backoff.Reset();
390
        resumer_state_->active_state = DownloadResumerActiveStatus::Inactive;
87✔
391
        resumer_state_->user_handlers_state = DownloadResumerUserHandlersStatus::None;
87✔
392
        return client_.AsyncCall(req, resumer_header_handler, resumer_body_handler);
348✔
393
}
394

395
io::ExpectedAsyncReaderPtr DownloadResumerClient::MakeBodyAsyncReader(
82✔
396
        http::IncomingResponsePtr resp) {
397
        auto exp_reader = client_.MakeBodyAsyncReader(resp);
164✔
398
        if (!exp_reader) {
82✔
399
                return exp_reader;
400
        }
401
        auto resumer_reader = make_shared<DownloadResumerAsyncReader>(
402
                exp_reader.value(), resumer_state_, cancelled_, shared_from_this());
162✔
403
        resumer_reader_ = resumer_reader;
404
        return resumer_reader;
81✔
405
}
406

407
http::OutgoingRequestPtr DownloadResumerClient::RemainingRangeRequest() const {
53✔
408
        auto range_req = make_shared<http::OutgoingRequest>(*user_request_);
53✔
409
        range_req->SetHeader(
53✔
410
                "Range",
411
                "bytes=" + to_string(resumer_state_->offset) + "-"
106✔
412
                        + to_string(resumer_state_->content_length - 1));
159✔
413
        return range_req;
53✔
414
};
415

416
error::Error DownloadResumerClient::ScheduleNextResumeRequest() {
55✔
417
        auto exp_interval = retry_.backoff.NextInterval();
55✔
418
        if (!exp_interval) {
55✔
419
                return http::MakeError(
420
                        http::DownloadResumerError,
421
                        "Giving up on resuming the download: " + exp_interval.error().String());
2✔
422
        }
423

424
        auto interval = exp_interval.value();
54✔
425
        logger_.Info(
108✔
426
                "Resuming download after "
427
                + to_string(chrono::duration_cast<chrono::seconds>(interval).count()) + " seconds");
108✔
428

429
        HeaderHandlerFunctor resumer_next_header_handler {shared_from_this()};
54✔
430
        BodyHandlerFunctor resumer_next_body_handler {shared_from_this()};
54✔
431

432
        retry_.wait_timer.AsyncWait(
54✔
433
                interval, [this, resumer_next_header_handler, resumer_next_body_handler](error::Error err) {
159✔
434
                        if (err != error::NoError) {
53✔
435
                                auto err_user = http::MakeError(
436
                                        http::DownloadResumerError, "Unexpected error in wait timer: " + err.String());
×
437
                                logger_.Error(err_user.String());
×
438
                                CallUserHandler(expected::unexpected(err_user));
×
439
                                return;
440
                        }
441

442
                        auto next_call_err = client_.AsyncCall(
443
                                RemainingRangeRequest(), resumer_next_header_handler, resumer_next_body_handler);
159✔
444
                        if (next_call_err != error::NoError) {
53✔
445
                                // Schedule once more
446
                                auto err = ScheduleNextResumeRequest();
×
447
                                if (err != error::NoError) {
×
448
                                        logger_.Error(err.String());
×
449
                                        CallUserHandler(expected::unexpected(err));
×
450
                                }
451
                        }
452
                });
162✔
453

454
        return error::NoError;
54✔
455
}
456

457
void DownloadResumerClient::CallUserHandler(http::ExpectedIncomingResponsePtr exp_resp) {
185✔
458
        if (!exp_resp) {
185✔
459
                DoCancel();
89✔
460
        }
461
        if (resumer_state_->user_handlers_state == DownloadResumerUserHandlersStatus::None) {
185✔
462
                resumer_state_->user_handlers_state =
85✔
463
                        DownloadResumerUserHandlersStatus::HeaderHandlerCalled;
464
                user_header_handler_(exp_resp);
170✔
465
        } else if (
100✔
466
                resumer_state_->user_handlers_state
467
                == DownloadResumerUserHandlersStatus::HeaderHandlerCalled) {
468
                resumer_state_->user_handlers_state = DownloadResumerUserHandlersStatus::BodyHandlerCalled;
84✔
469
                DoCancel();
84✔
470
                user_body_handler_(exp_resp);
168✔
471
        } else {
472
                string msg;
473
                if (!exp_resp) {
16✔
474
                        msg = "error: " + exp_resp.error().String();
32✔
475
                } else {
476
                        auto &resp = exp_resp.value();
×
477
                        msg = "response: " + to_string(resp->GetStatusCode()) + " " + resp->GetStatusMessage();
×
478
                }
479
                logger_.Warning("Cannot call any user handler with " + msg);
32✔
480
        }
481
}
185✔
482

483
void DownloadResumerClient::Cancel() {
57✔
484
        DoCancel();
57✔
485
        client_.Cancel();
57✔
486
};
57✔
487

488
void DownloadResumerClient::DoCancel() {
230✔
489
        // Set cancel state and then make a new one. Those who are interested should have their own
490
        // pointer to the old one.
491
        *cancelled_ = true;
230✔
492
        cancelled_ = make_shared<bool>(true);
230✔
493
};
230✔
494

495
} // namespace http_resumer
496
} // namespace update
497
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc