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

mendersoftware / mender / 1057366047

01 Nov 2023 08:50AM UTC coverage: 80.121% (-0.07%) from 80.195%
1057366047

push

gitlab-ci

kacf
fix: Don't cache server URL and token locally in process.

The problem if we do is that is that we may then connect to a
forwarder address which doesn't exist anymore. This produces
"connection refused", not "Not authorized", and therefore it does not
trigger an authentication request by itself. So always ask.

This can be easily reproduced by bringing up both mender-auth and
mender-update, and then restarting mender-auth after the first auth
cycle.

This makes the whole `AuthenticatorExternalTokenUpdateTest` test
irrelevant, since we no longer need to test the caching capability of
the client side authenticator. So just remove it.

Changelog: None
Ticket: None

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

6 of 6 new or added lines in 1 file covered. (100.0%)

6884 of 8592 relevant lines covered (80.12%)

9363.06 hits per line

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

82.61
/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 {
552✔
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 {
530✔
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) {
135✔
109
        auto resumer_client = resumer_client_.lock();
135✔
110
        if (resumer_client) {
135✔
111
                if (resumer_client->resumer_state_->active_state == DownloadResumerActiveStatus::Resuming) {
135✔
112
                        HandleNextResponse(resumer_client, exp_resp);
106✔
113
                } else {
114
                        HandleFirstResponse(resumer_client, exp_resp);
164✔
115
                }
116
        }
117
}
135✔
118

119
void HeaderHandlerFunctor::HandleFirstResponse(
82✔
120
        const shared_ptr<DownloadResumerClient> &resumer_client,
121
        http::ExpectedIncomingResponsePtr exp_resp) {
122
        // The first response shall always call the user header callback. On resumable responses, we
123
        // create a our own incoming response and call the user header handler. On errors, we log a
124
        // warning and call the user handler with the original response
125

126
        if (!exp_resp) {
82✔
127
                resumer_client->logger_.Warning(exp_resp.error().String());
×
128
                resumer_client->CallUserHandler(exp_resp);
×
129
                return;
3✔
130
        }
131
        auto resp = exp_resp.value();
82✔
132

133
        if (resp->GetStatusCode() != mender::http::StatusOK) {
82✔
134
                // Non-resumable response
135
                resumer_client->CallUserHandler(exp_resp);
2✔
136
                return;
2✔
137
        }
138

139
        auto exp_header = resp->GetHeader("Content-Length");
160✔
140
        if (!exp_header || exp_header.value() == "0") {
80✔
141
                resumer_client->logger_.Warning("Response does not contain Content-Length header");
2✔
142
                resumer_client->CallUserHandler(exp_resp);
1✔
143
                return;
1✔
144
        }
145

146
        auto exp_length = common::StringToLongLong(exp_header.value());
79✔
147
        if (!exp_length || exp_length.value() < 0) {
79✔
148
                resumer_client->logger_.Warning(
×
149
                        "Content-Length contains invalid number: " + exp_header.value());
×
150
                resumer_client->CallUserHandler(exp_resp);
×
151
                return;
152
        }
153

154
        // Resumable response
155
        resumer_client->resumer_state_->active_state = DownloadResumerActiveStatus::Resuming;
79✔
156
        resumer_client->resumer_state_->offset = 0;
79✔
157
        resumer_client->resumer_state_->content_length = exp_length.value();
79✔
158

159
        // Prepare a modified response and call user handler
160
        resumer_client->response_.reset(new http::IncomingResponse(*resumer_client, resp->cancelled_));
158✔
161
        resumer_client->response_->status_code_ = resp->GetStatusCode();
79✔
162
        resumer_client->response_->status_message_ = resp->GetStatusMessage();
158✔
163
        resumer_client->response_->headers_ = resp->GetHeaders();
164
        resumer_client->CallUserHandler(resumer_client->response_);
158✔
165
}
166

167
void HeaderHandlerFunctor::HandleNextResponse(
53✔
168
        const shared_ptr<DownloadResumerClient> &resumer_client,
169
        http::ExpectedIncomingResponsePtr exp_resp) {
170
        // If an error occurs has already occurred, schedule the next AsyncCall directly
171
        // If an error occurs during handling here, cancel the resuming and call the user handler.
172
        if (!exp_resp) {
53✔
173
                resumer_client->logger_.Warning(exp_resp.error().String());
22✔
174

175
                auto err = resumer_client->ScheduleNextResumeRequest();
11✔
176
                if (err != error::NoError) {
11✔
177
                        resumer_client->logger_.Error(err.String());
2✔
178
                        resumer_client->Cancel();
1✔
179
                        resumer_client->CallUserHandler(expected::unexpected(err));
2✔
180
                }
181
                return;
182
        }
183
        auto resp = exp_resp.value();
42✔
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->Cancel();
12✔
189
                resumer_client->CallUserHandler(expected::unexpected(exp_content_range.error()));
24✔
190
                return;
12✔
191
        }
192

193
        auto content_range = exp_content_range.value();
30✔
194
        if (content_range.size != 0
195
                && content_range.size != resumer_client->resumer_state_->content_length) {
30✔
196
                auto size_changed_err = http::MakeError(
197
                        http::DownloadResumerError,
198
                        "Size of artifact changed after download was resumed (expected "
199
                                + to_string(resumer_client->resumer_state_->content_length) + ", got "
4✔
200
                                + to_string(content_range.size) + ")");
8✔
201
                resumer_client->logger_.Error(size_changed_err.String());
4✔
202
                resumer_client->Cancel();
2✔
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->Cancel();
2✔
217
                resumer_client->CallUserHandler(expected::unexpected(bad_range_err));
4✔
218
                return;
219
        }
220

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

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

244
void BodyHandlerFunctor::operator()(http::ExpectedIncomingResponsePtr exp_resp) {
124✔
245
        auto resumer_client = resumer_client_.lock();
124✔
246
        if (!resumer_client) {
124✔
247
                return;
248
        }
249

250
        if (*resumer_client->cancelled_) {
73✔
251
                resumer_client->CallUserHandler(exp_resp);
19✔
252
                return;
19✔
253
        }
254

255
        if (resumer_client->resumer_state_->active_state == DownloadResumerActiveStatus::Inactive) {
54✔
256
                resumer_client->CallUserHandler(exp_resp);
2✔
257
                return;
2✔
258
        }
259

260
        // We resume the download if either:
261
        // * there is any error or
262
        // * successful read with status code Partial Content and there is still data missing
263
        const bool is_range_response =
264
                exp_resp && exp_resp.value()->GetStatusCode() == mender::http::StatusPartialContent;
52✔
265
        const bool is_data_missing =
266
                resumer_client->resumer_state_->offset < resumer_client->resumer_state_->content_length;
52✔
267
        if (!exp_resp || (is_range_response && is_data_missing)) {
52✔
268
                if (!exp_resp) {
43✔
269
                        resumer_client->logger_.Info(
86✔
270
                                "Will try to resume after error " + exp_resp.error().String());
86✔
271
                }
272

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

289
                // Finished, call the user handler \o/
290
                resumer_client->logger_.Debug("Download resumed and completed successfully");
18✔
291
                resumer_client->DoCancel();
9✔
292
                resumer_client->CallUserHandler(resumer_client->response_);
18✔
293
        }
294
}
295

296
void DownloadResumerAsyncReader::Cancel() {
×
297
        if (!*cancelled_) {
×
298
                inner_reader_->Cancel();
×
299
        }
300
}
×
301

302
error::Error DownloadResumerAsyncReader::AsyncRead(
2,041✔
303
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end, io::AsyncIoHandler handler) {
304
        auto resumer_client = resumer_client_.lock();
2,041✔
305
        if (!resumer_client || *cancelled_) {
2,041✔
306
                return error::MakeError(
307
                        error::ProgrammingError,
308
                        "DownloadResumerAsyncReader::AsyncRead called after stream is destroyed");
×
309
        }
310
        // Save user parameters for further resumes of the body read
311
        resumer_client->last_read_ = {.start = start, .end = end, .handler = handler};
2,041✔
312
        return AsyncReadResume();
2,041✔
313
}
314

315
error::Error DownloadResumerAsyncReader::AsyncReadResume() {
2,067✔
316
        auto resumer_client = resumer_client_.lock();
2,067✔
317
        if (!resumer_client) {
2,067✔
318
                return error::MakeError(
319
                        error::ProgrammingError,
320
                        "DownloadResumerAsyncReader::AsyncReadResume called after client is destroyed");
×
321
        }
322
        return inner_reader_->AsyncRead(
323
                resumer_client->last_read_.start,
324
                resumer_client->last_read_.end,
325
                [this](io::ExpectedSize result) {
2,066✔
326
                        if (!result) {
2,066✔
327
                                inner_reader_.reset();
43✔
328

329
                                logger_.Warning(
86✔
330
                                        "Reading error, a new request will be re-scheduled. "
331
                                        + result.error().String());
86✔
332
                        } else {
333
                                resumer_state_->offset += result.value();
2,023✔
334
                                logger_.Debug("read " + to_string(result.value()) + " bytes");
4,046✔
335
                                auto resumer_client = resumer_client_.lock();
2,023✔
336
                                if (resumer_client) {
2,023✔
337
                                        resumer_client->last_read_.handler(result);
4,046✔
338
                                } else {
339
                                        logger_.Error(
×
340
                                                "AsyncRead finish handler called after resumer client has been destroyed.");
×
341
                                }
342
                        }
343
                });
6,200✔
344
}
345

346
DownloadResumerClient::DownloadResumerClient(
122✔
347
        const http::ClientConfig &config, events::EventLoop &event_loop) :
348
        resumer_state_ {make_shared<DownloadResumerClientState>()},
349
        client_(config, event_loop, "http_resumer:client"),
350
        logger_ {"http_resumer:client"},
351
        cancelled_ {make_shared<bool>(true)},
122✔
352
        retry_ {
353
                .backoff = http::ExponentialBackoff(chrono::minutes(1), 10),
354
                .wait_timer = events::Timer(event_loop)} {
488✔
355
}
122✔
356

357
DownloadResumerClient::~DownloadResumerClient() {
244✔
358
        if (!*cancelled_) {
122✔
359
                logger_.Warning("DownloadResumerClient destroyed while request is still active!");
106✔
360
        }
361
        client_.Cancel();
122✔
362
}
122✔
363

364
error::Error DownloadResumerClient::AsyncCall(
83✔
365
        http::OutgoingRequestPtr req,
366
        http::ResponseHandler user_header_handler,
367
        http::ResponseHandler user_body_handler) {
368
        HeaderHandlerFunctor resumer_header_handler {shared_from_this()};
83✔
369
        BodyHandlerFunctor resumer_body_handler {shared_from_this()};
83✔
370

371
        user_request_ = req;
372
        user_header_handler_ = user_header_handler;
83✔
373
        user_body_handler_ = user_body_handler;
83✔
374

375
        if (!*cancelled_) {
83✔
376
                return error::Error(
377
                        make_error_condition(errc::operation_in_progress), "HTTP call already ongoing");
2✔
378
        }
379

380
        *cancelled_ = false;
82✔
381
        retry_.backoff.Reset();
382
        resumer_state_->active_state = DownloadResumerActiveStatus::Inactive;
82✔
383
        resumer_state_->user_handlers_state = DownloadResumerUserHandlersStatus::None;
82✔
384
        return client_.AsyncCall(req, resumer_header_handler, resumer_body_handler);
328✔
385
}
386

387
io::ExpectedAsyncReaderPtr DownloadResumerClient::MakeBodyAsyncReader(
79✔
388
        http::IncomingResponsePtr resp) {
389
        auto exp_reader = client_.MakeBodyAsyncReader(resp);
158✔
390
        if (!exp_reader) {
79✔
391
                return exp_reader;
392
        }
393
        resumer_reader_ = make_shared<DownloadResumerAsyncReader>(
78✔
394
                exp_reader.value(), resumer_state_, cancelled_, shared_from_this());
78✔
395
        return resumer_reader_;
78✔
396
}
397

398
http::OutgoingRequestPtr DownloadResumerClient::RemainingRangeRequest() const {
53✔
399
        auto range_req = make_shared<http::OutgoingRequest>(*user_request_);
53✔
400
        range_req->SetHeader(
53✔
401
                "Range",
402
                "bytes=" + to_string(resumer_state_->offset) + "-"
106✔
403
                        + to_string(resumer_state_->content_length - 1));
159✔
404
        return range_req;
53✔
405
};
406

407
error::Error DownloadResumerClient::ScheduleNextResumeRequest() {
54✔
408
        auto exp_interval = retry_.backoff.NextInterval();
54✔
409
        if (!exp_interval) {
54✔
410
                return http::MakeError(
411
                        http::DownloadResumerError,
412
                        "Giving up on resuming the download: " + exp_interval.error().String());
2✔
413
        }
414

415
        auto interval = exp_interval.value();
53✔
416
        logger_.Info(
106✔
417
                "Resuming download after "
418
                + to_string(chrono::duration_cast<chrono::seconds>(interval).count()) + " seconds");
106✔
419

420
        HeaderHandlerFunctor resumer_next_header_handler {shared_from_this()};
53✔
421
        BodyHandlerFunctor resumer_next_body_handler {shared_from_this()};
53✔
422

423
        retry_.wait_timer.AsyncWait(
53✔
424
                interval, [this, resumer_next_header_handler, resumer_next_body_handler](error::Error err) {
×
425
                        if (err != error::NoError) {
53✔
426
                                auto err_user = http::MakeError(
427
                                        http::DownloadResumerError, "Unexpected error in wait timer: " + err.String());
×
428
                                logger_.Error(err_user.String());
×
429
                                Cancel();
×
430
                                CallUserHandler(expected::unexpected(err_user));
×
431
                                return;
432
                        }
433

434
                        auto next_call_err = client_.AsyncCall(
435
                                RemainingRangeRequest(), resumer_next_header_handler, resumer_next_body_handler);
159✔
436
                        if (next_call_err != error::NoError) {
53✔
437
                                // Schedule once more
438
                                auto err = ScheduleNextResumeRequest();
×
439
                                if (err != error::NoError) {
×
440
                                        logger_.Error(err.String());
×
441
                                        Cancel();
×
442
                                        CallUserHandler(expected::unexpected(err));
×
443
                                }
444
                        }
445
                });
159✔
446

447
        return error::NoError;
53✔
448
}
449

450
void DownloadResumerClient::CallUserHandler(http::ExpectedIncomingResponsePtr exp_resp) {
129✔
451
        if (resumer_state_->user_handlers_state == DownloadResumerUserHandlersStatus::None) {
129✔
452
                resumer_state_->user_handlers_state =
82✔
453
                        DownloadResumerUserHandlersStatus::HeaderHandlerCalled;
454
                user_header_handler_(exp_resp);
164✔
455
        } else if (
47✔
456
                resumer_state_->user_handlers_state
457
                == DownloadResumerUserHandlersStatus::HeaderHandlerCalled) {
458
                resumer_state_->user_handlers_state = DownloadResumerUserHandlersStatus::BodyHandlerCalled;
31✔
459
                user_body_handler_(exp_resp);
62✔
460
        } else {
461
                string msg;
462
                if (!exp_resp) {
16✔
463
                        msg = "error: " + exp_resp.error().String();
32✔
464
                } else {
465
                        auto &resp = exp_resp.value();
×
466
                        msg = "response: " + to_string(resp->GetStatusCode()) + " " + resp->GetStatusMessage();
×
467
                }
468
                logger_.Warning("Cannot call any user handler with " + msg);
32✔
469
        }
470
}
129✔
471

472
void DownloadResumerClient::Cancel() {
20✔
473
        DoCancel();
20✔
474
        client_.Cancel();
20✔
475
};
20✔
476

477
void DownloadResumerClient::DoCancel() {
29✔
478
        // Set cancel state and then make a new one. Those who are interested should have their own
479
        // pointer to the old one.
480
        *cancelled_ = true;
29✔
481
        cancelled_ = make_shared<bool>(true);
29✔
482
};
29✔
483

484
} // namespace http_resumer
485
} // namespace update
486
} // 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