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

mendersoftware / mender / 1073264463

15 Nov 2023 10:33AM UTC coverage: 80.186% (+0.1%) from 80.062%
1073264463

push

gitlab-ci

kacf
docs: Restore Update Control XML file for documentation purposes.

This is a partial restore of ee5dc24db79fc57, just to get the XML file
back. The feature is still removed, this is just to be able to keep it
in the documentation with a notice that says it's removed (already
merged to master in 426caf729c3191b).

Signed-off-by: Kristian Amlie <kristian.amlie@northern.tech>

6969 of 8691 relevant lines covered (80.19%)

9260.9 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
                        auto err = resumer_client->ScheduleNextResumeRequest();
11✔
115
                        if (err != error::NoError) {
11✔
116
                                resumer_client->logger_.Error(err.String());
2✔
117
                                resumer_client->CallUserHandler(expected::unexpected(err));
2✔
118
                        }
119
                        return;
120
                }
121

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

299
DownloadResumerAsyncReader::~DownloadResumerAsyncReader() {
243✔
300
        Cancel();
81✔
301
}
81✔
302

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

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

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

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

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

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

377
        user_request_ = req;
378
        user_header_handler_ = user_header_handler;
87✔
379
        user_body_handler_ = user_body_handler;
87✔
380

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

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

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

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

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

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

427
        HeaderHandlerFunctor resumer_next_header_handler {shared_from_this()};
54✔
428
        BodyHandlerFunctor resumer_next_body_handler {shared_from_this()};
54✔
429

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

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

452
        return error::NoError;
54✔
453
}
454

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

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

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

493
} // namespace http_resumer
494
} // namespace update
495
} // 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