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

mendersoftware / mender / 1057599036

01 Nov 2023 12:43PM UTC coverage: 80.276% (+0.07%) from 80.207%
1057599036

push

gitlab-ci

kacf
fix: Make sure Cancel is called if resumer body reader is destroyed.

This was already the case in most cases, since the inner reader would
call cancel, but not in case the reader was destroyed in between
retries, in which case there is no inner reader.

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%)

6911 of 8609 relevant lines covered (80.28%)

9347.78 hits per line

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

84.15
/src/common/http/http.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 <common/http.hpp>
16

17
#include <algorithm>
18
#include <cctype>
19
#include <cstdlib>
20
#include <iomanip>
21
#include <string>
22

23
#include <common/common.hpp>
24

25
namespace mender {
26
namespace http {
27

28
namespace common = mender::common;
29

30
const HttpErrorCategoryClass HttpErrorCategory;
31

32
const char *HttpErrorCategoryClass::name() const noexcept {
×
33
        return "HttpErrorCategory";
×
34
}
35

36
string HttpErrorCategoryClass::message(int code) const {
37✔
37
        switch (code) {
37✔
38
        case NoError:
39
                return "Success";
×
40
        case NoSuchHeaderError:
41
                return "No such header";
12✔
42
        case InvalidUrlError:
43
                return "Malformed URL";
×
44
        case BodyMissingError:
45
                return "Body is missing";
×
46
        case BodyIgnoredError:
47
                return "HTTP stream contains a body, but a reader has not been created for it";
16✔
48
        case UnsupportedMethodError:
49
                return "Unsupported HTTP method";
×
50
        case StreamCancelledError:
51
                return "Stream has been cancelled/destroyed";
×
52
        case UnsupportedBodyType:
53
                return "HTTP stream has a body type we don't understand";
×
54
        case MaxRetryError:
55
                return "Tried maximum number of times";
2✔
56
        case DownloadResumerError:
57
                return "Resume download error";
5✔
58
        case ProxyError:
59
                return "Proxy error";
2✔
60
        }
61
        // Don't use "default" case. This should generate a warning if we ever add any enums. But
62
        // still assert here for safety.
63
        assert(false);
64
        return "Unknown";
×
65
}
66

67
error::Error MakeError(ErrorCode code, const string &msg) {
106✔
68
        return error::Error(error_condition(code, HttpErrorCategory), msg);
390✔
69
}
70

71
string MethodToString(Method method) {
190✔
72
        switch (method) {
190✔
73
        case Method::Invalid:
74
                return "Invalid";
×
75
        case Method::GET:
76
                return "GET";
169✔
77
        case Method::HEAD:
78
                return "HEAD";
×
79
        case Method::POST:
80
                return "POST";
2✔
81
        case Method::PUT:
82
                return "PUT";
15✔
83
        case Method::PATCH:
84
                return "PATCH";
×
85
        case Method::CONNECT:
86
                return "CONNECT";
4✔
87
        }
88
        // Don't use "default" case. This should generate a warning if we ever add any methods. But
89
        // still assert here for safety.
90
        assert(false);
91
        return "INVALID_METHOD";
×
92
}
93

94
error::Error BreakDownUrl(const string &url, BrokenDownUrl &address) {
467✔
95
        const string url_split {"://"};
467✔
96

97
        auto split_index = url.find(url_split);
467✔
98
        if (split_index == string::npos) {
467✔
99
                return MakeError(InvalidUrlError, url + " is not a valid URL.");
4✔
100
        }
101
        if (split_index == 0) {
465✔
102
                return MakeError(InvalidUrlError, url + ": missing hostname");
×
103
        }
104

105
        address.protocol = url.substr(0, split_index);
930✔
106

107
        auto tmp = url.substr(split_index + url_split.size());
465✔
108
        split_index = tmp.find("/");
465✔
109
        if (split_index == string::npos) {
465✔
110
                address.host = tmp;
286✔
111
                address.path = "/";
286✔
112
        } else {
113
                address.host = tmp.substr(0, split_index);
179✔
114
                address.path = tmp.substr(split_index);
358✔
115
        }
116

117
        if (address.host.find("@") != string::npos) {
465✔
118
                address = {};
×
119
                return error::Error(
120
                        make_error_condition(errc::not_supported),
×
121
                        "URL Username and password is not supported");
×
122
        }
123

124
        split_index = address.host.find(":");
465✔
125
        if (split_index != string::npos) {
465✔
126
                tmp = std::move(address.host);
455✔
127
                address.host = tmp.substr(0, split_index);
455✔
128

129
                tmp = tmp.substr(split_index + 1);
455✔
130
                auto port = common::StringToLongLong(tmp);
455✔
131
                if (!port) {
455✔
132
                        address = {};
×
133
                        return error::Error(port.error().code, url + " contains invalid port number");
×
134
                }
135
                address.port = port.value();
455✔
136
        } else {
137
                if (address.protocol == "http") {
10✔
138
                        address.port = 80;
5✔
139
                } else if (address.protocol == "https") {
5✔
140
                        address.port = 443;
4✔
141
                } else {
142
                        address = {};
1✔
143
                        return error::Error(
144
                                make_error_condition(errc::protocol_not_supported),
2✔
145
                                "Cannot deduce port number from protocol " + address.protocol);
2✔
146
                }
147
        }
148

149
        log::Trace(
464✔
150
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
928✔
151
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")");
1,392✔
152

153
        return error::NoError;
464✔
154
}
155

156
string URLEncode(const string &value) {
15✔
157
        stringstream escaped;
30✔
158
        escaped << hex;
15✔
159

160
        for (auto c : value) {
288✔
161
                // Keep alphanumeric and other accepted characters intact
162
                if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
273✔
163
                        escaped << c;
251✔
164
                } else {
165
                        // Any other characters are percent-encoded
166
                        escaped << uppercase;
22✔
167
                        escaped << '%' << setw(2) << int((unsigned char) c);
22✔
168
                        escaped << nouppercase;
22✔
169
                }
170
        }
171

172
        return escaped.str();
15✔
173
}
174

175
string JoinOneUrl(const string &prefix, const string &suffix) {
166✔
176
        auto prefix_end = prefix.cend();
177
        while (prefix_end != prefix.cbegin() && prefix_end[-1] == '/') {
178✔
178
                prefix_end--;
179
        }
180

181
        auto suffix_start = suffix.cbegin();
182
        while (suffix_start != suffix.cend() && *suffix_start == '/') {
234✔
183
                suffix_start++;
184
        }
185

186
        return string(prefix.cbegin(), prefix_end) + "/" + string(suffix_start, suffix.cend());
332✔
187
}
188

189
size_t CaseInsensitiveHasher::operator()(const string &str) const {
2,528✔
190
        string lower_str(str.length(), ' ');
191
        transform(
192
                str.begin(), str.end(), lower_str.begin(), [](unsigned char c) { return std::tolower(c); });
30,449✔
193
        return hash<string>()(lower_str);
2,528✔
194
}
195

196
bool CaseInsensitiveComparator::operator()(const string &str1, const string &str2) const {
1,032✔
197
        return strcasecmp(str1.c_str(), str2.c_str()) == 0;
1,032✔
198
}
199

200
expected::ExpectedString Transaction::GetHeader(const string &name) const {
731✔
201
        if (headers_.find(name) == headers_.end()) {
731✔
202
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
750✔
203
        }
204
        return headers_.at(name);
481✔
205
}
206

207
string Request::GetHost() const {
×
208
        return address_.host;
×
209
}
210

211
string Request::GetProtocol() const {
×
212
        return address_.protocol;
×
213
}
214

215
int Request::GetPort() const {
×
216
        return address_.port;
×
217
}
218

219
Method Request::GetMethod() const {
90✔
220
        return method_;
90✔
221
}
222

223
string Request::GetPath() const {
150✔
224
        return address_.path;
150✔
225
}
226

227
unsigned Response::GetStatusCode() const {
540✔
228
        return status_code_;
540✔
229
}
230

231
string Response::GetStatusMessage() const {
329✔
232
        return status_message_;
329✔
233
}
234

235
void BaseOutgoingRequest::SetMethod(Method method) {
231✔
236
        method_ = method;
231✔
237
}
231✔
238

239
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
459✔
240
        headers_[name] = value;
241
}
459✔
242

243
void BaseOutgoingRequest::SetBodyGenerator(BodyGenerator body_gen) {
36✔
244
        async_body_gen_ = nullptr;
36✔
245
        async_body_reader_ = nullptr;
36✔
246
        body_gen_ = body_gen;
36✔
247
}
36✔
248

249
void BaseOutgoingRequest::SetAsyncBodyGenerator(AsyncBodyGenerator body_gen) {
6✔
250
        body_gen_ = nullptr;
6✔
251
        body_reader_ = nullptr;
6✔
252
        async_body_gen_ = body_gen;
6✔
253
}
6✔
254

255
error::Error OutgoingRequest::SetAddress(const string &address) {
215✔
256
        orig_address_ = address;
215✔
257

258
        return BreakDownUrl(address, address_);
215✔
259
}
260

261
IncomingRequest::~IncomingRequest() {
440✔
262
        if (!*cancelled_) {
220✔
263
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
264
        }
265
}
220✔
266

267
void IncomingRequest::Cancel() {
2✔
268
        if (!*cancelled_) {
2✔
269
                stream_.Cancel();
2✔
270
        }
271
}
2✔
272

273
io::ExpectedAsyncReaderPtr IncomingRequest::MakeBodyAsyncReader() {
53✔
274
        if (*cancelled_) {
53✔
275
                return expected::unexpected(MakeError(
×
276
                        StreamCancelledError, "Cannot make reader for a request that doesn't exist anymore"));
×
277
        }
278
        return stream_.server_.MakeBodyAsyncReader(shared_from_this());
106✔
279
}
280

281
void IncomingRequest::SetBodyWriter(io::WriterPtr writer) {
39✔
282
        auto exp_reader = MakeBodyAsyncReader();
39✔
283
        if (!exp_reader) {
39✔
284
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
16✔
285
                        log::Error(exp_reader.error().String());
×
286
                }
287
                return;
288
        }
289
        auto &reader = exp_reader.value();
31✔
290

291
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
1,951✔
292
                if (err != error::NoError) {
31✔
293
                        log::Error("Could not copy HTTP stream: " + err.String());
4✔
294
                }
295
        });
93✔
296
}
297

298
ExpectedOutgoingResponsePtr IncomingRequest::MakeResponse() {
210✔
299
        if (*cancelled_) {
210✔
300
                return expected::unexpected(MakeError(
×
301
                        StreamCancelledError, "Cannot make response for a request that doesn't exist anymore"));
×
302
        }
303
        return stream_.server_.MakeResponse(shared_from_this());
420✔
304
}
305

306
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
296✔
307
        client_ {client},
308
        cancelled_ {cancelled} {
592✔
309
}
296✔
310

311
void IncomingResponse::Cancel() {
1✔
312
        if (!*cancelled_) {
1✔
313
                client_.Cancel();
1✔
314
        }
315
}
1✔
316

317
io::ExpectedAsyncReaderPtr IncomingResponse::MakeBodyAsyncReader() {
136✔
318
        if (*cancelled_) {
136✔
319
                return expected::unexpected(MakeError(
×
320
                        StreamCancelledError, "Cannot make reader for a response that doesn't exist anymore"));
×
321
        }
322
        return client_.MakeBodyAsyncReader(shared_from_this());
272✔
323
}
324

325
void IncomingResponse::SetBodyWriter(io::WriterPtr writer) {
71✔
326
        auto exp_reader = MakeBodyAsyncReader();
71✔
327
        if (!exp_reader) {
71✔
328
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
24✔
329
                        log::Error(exp_reader.error().String());
×
330
                }
331
                return;
332
        }
333
        auto &reader = exp_reader.value();
59✔
334

335
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
3,210✔
336
                if (err != error::NoError) {
41✔
337
                        log::Error("Could not copy HTTP stream: " + err.String());
8✔
338
                }
339
        });
159✔
340
}
341

342
io::ExpectedAsyncReadWriterPtr IncomingResponse::SwitchProtocol() {
8✔
343
        if (*cancelled_) {
8✔
344
                return expected::unexpected(MakeError(
1✔
345
                        StreamCancelledError, "Cannot switch protocol when the stream doesn't exist anymore"));
3✔
346
        }
347
        return client_.GetHttpClient().SwitchProtocol(shared_from_this());
14✔
348
}
349

350
OutgoingResponse::~OutgoingResponse() {
420✔
351
        if (!*cancelled_) {
210✔
352
                stream_.server_.RemoveStream(stream_.shared_from_this());
6✔
353
        }
354
}
210✔
355

356
void OutgoingResponse::Cancel() {
1✔
357
        if (!*cancelled_) {
1✔
358
                stream_.Cancel();
1✔
359
                stream_.server_.RemoveStream(stream_.shared_from_this());
2✔
360
        }
361
}
1✔
362

363
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
207✔
364
        status_code_ = code;
207✔
365
        status_message_ = message;
207✔
366
}
207✔
367

368
void OutgoingResponse::SetHeader(const string &name, const string &value) {
225✔
369
        headers_[name] = value;
370
}
225✔
371

372
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
169✔
373
        async_body_reader_ = nullptr;
169✔
374
        body_reader_ = body_reader;
375
}
169✔
376

377
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
378
        body_reader_ = nullptr;
4✔
379
        async_body_reader_ = body_reader;
380
}
4✔
381

382
error::Error OutgoingResponse::AsyncReply(ReplyFinishedHandler reply_finished_handler) {
198✔
383
        if (*cancelled_) {
198✔
384
                return MakeError(StreamCancelledError, "Cannot reply when response doesn't exist anymore");
2✔
385
        }
386
        return stream_.server_.AsyncReply(shared_from_this(), reply_finished_handler);
591✔
387
}
388

389
error::Error OutgoingResponse::AsyncSwitchProtocol(SwitchProtocolHandler handler) {
9✔
390
        if (*cancelled_) {
9✔
391
                return MakeError(
392
                        StreamCancelledError, "Cannot switch protocol when response doesn't exist anymore");
×
393
        }
394
        return stream_.server_.AsyncSwitchProtocol(shared_from_this(), handler);
27✔
395
}
396

397
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
118✔
398
        iteration_++;
118✔
399

400
        if (try_count_ > 0 && iteration_ > try_count_) {
118✔
401
                return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
15✔
402
        }
403

404
        chrono::milliseconds current_interval = smallest_interval_;
113✔
405
        // Backoff algorithm: Each interval is returned three times, then it's doubled, and then
406
        // that is returned three times, and so on. But if interval is ever higher than the max
407
        // interval, then return the max interval instead, and once that is returned three times,
408
        // produce MaxRetryError. If try_count_ is set, then that controls the total number of
409
        // retries, but the rest is the same, so then it simply "gets stuck" at max interval for
410
        // many iterations.
411
        for (int count = 3; count < iteration_; count += 3) {
201✔
412
                auto new_interval = current_interval * 2;
413
                if (new_interval > max_interval_) {
414
                        new_interval = max_interval_;
415
                }
416
                if (try_count_ <= 0 && new_interval == current_interval) {
93✔
417
                        return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
15✔
418
                }
419
                current_interval = new_interval;
420
        }
421

422
        return current_interval;
423
}
424

425
static expected::ExpectedString GetProxyStringFromEnvironment(
330✔
426
        const string &primary, const string &secondary) {
427
        bool primary_set = false, secondary_set = false;
428

429
        if (getenv(primary.c_str()) != nullptr && getenv(primary.c_str())[0] != '\0') {
330✔
430
                primary_set = true;
431
        }
432
        if (getenv(secondary.c_str()) != nullptr && getenv(secondary.c_str())[0] != '\0') {
330✔
433
                secondary_set = true;
434
        }
435

436
        if (primary_set && secondary_set) {
330✔
437
                return expected::unexpected(error::Error(
3✔
438
                        make_error_condition(errc::invalid_argument),
6✔
439
                        primary + " and " + secondary
6✔
440
                                + " environment variables can't both be set at the same time"));
9✔
441
        } else if (primary_set) {
327✔
442
                return getenv(primary.c_str());
3✔
443
        } else if (secondary_set) {
324✔
444
                return getenv(secondary.c_str());
×
445
        } else {
446
                return "";
324✔
447
        }
448
}
449

450
// The proxy variables aren't standardized, but this page was useful for the common patterns:
451
// https://superuser.com/questions/944958/are-http-proxy-https-proxy-and-no-proxy-environment-variables-standard
452
expected::ExpectedString GetHttpProxyStringFromEnvironment() {
111✔
453
        if (getenv("REQUEST_METHOD") != nullptr && getenv("HTTP_PROXY") != nullptr) {
111✔
454
                return expected::unexpected(error::Error(
×
455
                        make_error_condition(errc::operation_not_permitted),
×
456
                        "Using REQUEST_METHOD (CGI) together with HTTP_PROXY is insecure. See https://github.com/golang/go/issues/16405"));
×
457
        }
458
        return GetProxyStringFromEnvironment("http_proxy", "HTTP_PROXY");
222✔
459
}
460

461
expected::ExpectedString GetHttpsProxyStringFromEnvironment() {
110✔
462
        return GetProxyStringFromEnvironment("https_proxy", "HTTPS_PROXY");
220✔
463
}
464

465
expected::ExpectedString GetNoProxyStringFromEnvironment() {
109✔
466
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
218✔
467
}
468

469
// The proxy variables aren't standardized, but this page was useful for the common patterns:
470
// https://superuser.com/questions/944958/are-http-proxy-https-proxy-and-no-proxy-environment-variables-standard
471
bool HostNameMatchesNoProxy(const string &host, const string &no_proxy) {
40✔
472
        auto entries = common::SplitString(no_proxy, " ");
120✔
473
        for (string &entry : entries) {
74✔
474
                if (entry[0] == '.') {
46✔
475
                        // Wildcard.
476
                        ssize_t wildcard_len = entry.size() - 1;
5✔
477
                        if (wildcard_len == 0
478
                                || entry.compare(0, wildcard_len, host, host.size() - wildcard_len)) {
5✔
479
                                return true;
5✔
480
                        }
481
                } else if (host == entry) {
41✔
482
                        return true;
483
                }
484
        }
485

486
        return false;
487
}
488

489
} // namespace http
490
} // 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