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

mendersoftware / mender / 2271300743

19 Jan 2026 11:42AM UTC coverage: 81.376% (+1.7%) from 79.701%
2271300743

push

gitlab-ci

web-flow
Merge pull request #1879 from lluiscampos/MEN-8687-ci-debian-updates

MEN-8687: Update Debian base images for CI jobs

8791 of 10803 relevant lines covered (81.38%)

20310.08 hits per line

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

88.41
/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 common {
27
namespace http {
28

29
namespace common = mender::common;
30

31
const HttpErrorCategoryClass HttpErrorCategory;
32

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

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

68
error::Error MakeError(ErrorCode code, const string &msg) {
107✔
69
        return error::Error(error_condition(code, HttpErrorCategory), msg);
1,039✔
70
}
71

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

95
error::Error BreakDownUrl(const string &url, BrokenDownUrl &address, bool with_auth) {
557✔
96
        const string url_split {"://"};
557✔
97

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

106
        address.protocol = url.substr(0, split_index);
1,110✔
107

108
        auto tmp = url.substr(split_index + url_split.size());
555✔
109
        split_index = tmp.find("/");
555✔
110
        if (split_index == string::npos) {
555✔
111
                address.host = tmp;
359✔
112
                address.path = "/";
359✔
113
        } else {
114
                address.host = tmp.substr(0, split_index);
196✔
115
                address.path = tmp.substr(split_index);
392✔
116
        }
117

118
        auto auth_index = address.host.rfind("@");
555✔
119
        if (auth_index != string::npos) {
555✔
120
                if (!with_auth) {
8✔
121
                        address = {};
1✔
122
                        return error::Error(
123
                                make_error_condition(errc::not_supported),
2✔
124
                                "URL Username and password is not supported");
2✔
125
                }
126
                auto user_password = address.host.substr(0, auth_index);
7✔
127
                address.host = address.host.substr(auth_index + 1);
7✔
128
                auto u_pw_sep_index = user_password.find(":");
7✔
129
                if (u_pw_sep_index == string::npos) {
7✔
130
                        // no password
131
                        address.username = std::move(user_password);
1✔
132
                } else {
133
                        address.username = user_password.substr(0, u_pw_sep_index);
6✔
134
                        address.password = user_password.substr(u_pw_sep_index + 1);
12✔
135
                }
136
        }
137

138
        split_index = address.host.find(":");
554✔
139
        if (split_index != string::npos) {
554✔
140
                tmp = std::move(address.host);
543✔
141
                address.host = tmp.substr(0, split_index);
543✔
142

143
                tmp = tmp.substr(split_index + 1);
543✔
144
                auto port = common::StringTo<decltype(address.port)>(tmp);
543✔
145
                if (!port) {
543✔
146
                        address = {};
×
147
                        return port.error().WithContext(url + " contains invalid port number");
×
148
                }
149
                address.port = port.value();
543✔
150
        } else {
151
                if (address.protocol == "http") {
11✔
152
                        address.port = 80;
5✔
153
                } else if (address.protocol == "https") {
6✔
154
                        address.port = 443;
5✔
155
                } else {
156
                        address = {};
1✔
157
                        return error::Error(
158
                                make_error_condition(errc::protocol_not_supported),
2✔
159
                                "Cannot deduce port number from protocol " + address.protocol);
2✔
160
                }
161
        }
162

163
        log::Trace(
553✔
164
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
553✔
165
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + "),"
1,106✔
166
                + "(username: " + address.username
1,106✔
167
                + "), (password: " + (address.password == "" ? "" : "OMITTED") + ")");
1,106✔
168

169
        return error::NoError;
553✔
170
}
2✔
171

172
string URLEncode(const string &value) {
15✔
173
        stringstream escaped;
15✔
174
        escaped << hex;
15✔
175

176
        for (auto c : value) {
288✔
177
                // Keep alphanumeric and other accepted characters intact
178
                if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
273✔
179
                        escaped << c;
251✔
180
                } else {
181
                        // Any other characters are percent-encoded
182
                        escaped << uppercase;
22✔
183
                        escaped << '%' << setw(2) << static_cast<int>(static_cast<unsigned char>(c));
22✔
184
                        escaped << nouppercase;
22✔
185
                }
186
        }
187

188
        return escaped.str();
15✔
189
}
15✔
190

191
expected::ExpectedString URLDecode(const string &value) {
14✔
192
        stringstream unescaped;
14✔
193

194
        auto len = value.length();
195
        for (size_t i = 0; i < len; i++) {
181✔
196
                if (value[i] != '%') {
172✔
197
                        unescaped << value[i];
163✔
198
                } else {
199
                        if ((i + 2 >= len) || !isxdigit(value[i + 1]) || !(isxdigit(value[i + 2]))) {
9✔
200
                                return expected::unexpected(
4✔
201
                                        MakeError(InvalidUrlError, "Incomplete % sequence in '" + value + "'"));
12✔
202
                        }
203
                        unsigned int num;
204
                        sscanf(value.substr(i + 1, 2).c_str(), "%x", &num);
5✔
205
                        if (num < 0x20) {
5✔
206
                                return expected::unexpected(
1✔
207
                                        MakeError(InvalidUrlError, "Invalid encoding in '" + value + "'"));
3✔
208
                        }
209
                        unescaped << static_cast<char>(num);
4✔
210
                        i += 2;
211
                }
212
        }
213
        return unescaped.str();
18✔
214
}
14✔
215

216
string JoinOneUrl(const string &prefix, const string &suffix) {
174✔
217
        auto prefix_end = prefix.cend();
218
        while (prefix_end != prefix.cbegin() && prefix_end[-1] == '/') {
186✔
219
                prefix_end--;
220
        }
221

222
        auto suffix_start = suffix.cbegin();
223
        while (suffix_start != suffix.cend() && *suffix_start == '/') {
248✔
224
                suffix_start++;
225
        }
226

227
        return string(prefix.cbegin(), prefix_end) + "/" + string(suffix_start, suffix.cend());
348✔
228
}
229

230
size_t CaseInsensitiveHasher::operator()(const string &str) const {
4,634✔
231
        return hash<string>()(common::StringToLower(str));
4,634✔
232
}
233

234
bool CaseInsensitiveComparator::operator()(const string &str1, const string &str2) const {
1,591✔
235
        return strcasecmp(str1.c_str(), str2.c_str()) == 0;
1,591✔
236
}
237

238
expected::ExpectedString Transaction::GetHeader(const string &name) const {
1,564✔
239
        if (headers_.find(name) == headers_.end()) {
1,564✔
240
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
2,664✔
241
        }
242
        return headers_.at(name);
676✔
243
}
244

245
string Request::GetHost() const {
330✔
246
        return address_.host;
330✔
247
}
248

249
string Request::GetProtocol() const {
×
250
        return address_.protocol;
×
251
}
252

253
int Request::GetPort() const {
985✔
254
        return address_.port;
985✔
255
}
256

257
Method Request::GetMethod() const {
92✔
258
        return method_;
92✔
259
}
260

261
string Request::GetPath() const {
155✔
262
        return address_.path;
155✔
263
}
264

265
unsigned Response::GetStatusCode() const {
734✔
266
        return status_code_;
734✔
267
}
268

269
string Response::GetStatusMessage() const {
453✔
270
        return status_message_;
453✔
271
}
272

273
void BaseOutgoingRequest::SetMethod(Method method) {
249✔
274
        method_ = method;
249✔
275
}
249✔
276

277
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
938✔
278
        headers_[name] = value;
279
}
938✔
280

281
void BaseOutgoingRequest::SetBodyGenerator(BodyGenerator body_gen) {
40✔
282
        async_body_gen_ = nullptr;
40✔
283
        async_body_reader_ = nullptr;
40✔
284
        body_gen_ = body_gen;
40✔
285
}
40✔
286

287
void BaseOutgoingRequest::SetAsyncBodyGenerator(AsyncBodyGenerator body_gen) {
6✔
288
        body_gen_ = nullptr;
6✔
289
        body_reader_ = nullptr;
6✔
290
        async_body_gen_ = body_gen;
6✔
291
}
6✔
292

293
error::Error OutgoingRequest::SetAddress(const string &address) {
231✔
294
        orig_address_ = address;
231✔
295

296
        return BreakDownUrl(address, address_);
231✔
297
}
298

299
IncomingRequest::~IncomingRequest() {
855✔
300
        if (!*cancelled_) {
285✔
301
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
302
        }
303
}
855✔
304

305
void IncomingRequest::Cancel() {
2✔
306
        if (!*cancelled_) {
2✔
307
                stream_.Cancel();
2✔
308
        }
309
}
2✔
310

311
io::ExpectedAsyncReaderPtr IncomingRequest::MakeBodyAsyncReader() {
60✔
312
        if (*cancelled_) {
60✔
313
                return expected::unexpected(MakeError(
×
314
                        StreamCancelledError, "Cannot make reader for a request that doesn't exist anymore"));
×
315
        }
316
        return stream_.server_.MakeBodyAsyncReader(shared_from_this());
120✔
317
}
318

319
void IncomingRequest::SetBodyWriter(io::WriterPtr writer) {
46✔
320
        auto exp_reader = MakeBodyAsyncReader();
46✔
321
        if (!exp_reader) {
46✔
322
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
30✔
323
                        log::Error(exp_reader.error().String());
×
324
                }
325
                return;
326
        }
327
        auto &reader = exp_reader.value();
36✔
328

329
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
2,706✔
330
                if (err != error::NoError) {
36✔
331
                        log::Error("Could not copy HTTP stream: " + err.String());
4✔
332
                }
333
        });
36✔
334
}
335

336
ExpectedOutgoingResponsePtr IncomingRequest::MakeResponse() {
276✔
337
        if (*cancelled_) {
276✔
338
                return expected::unexpected(MakeError(
×
339
                        StreamCancelledError, "Cannot make response for a request that doesn't exist anymore"));
×
340
        }
341
        return stream_.server_.MakeResponse(shared_from_this());
552✔
342
}
343

344
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
738✔
345
        client_ {client},
369✔
346
        cancelled_ {cancelled} {
369✔
347
}
738✔
348

349
void IncomingResponse::Cancel() {
1✔
350
        if (!*cancelled_) {
1✔
351
                client_.Cancel();
1✔
352
        }
353
}
1✔
354

355
io::ExpectedAsyncReaderPtr IncomingResponse::MakeBodyAsyncReader() {
153✔
356
        if (*cancelled_) {
153✔
357
                return expected::unexpected(MakeError(
×
358
                        StreamCancelledError, "Cannot make reader for a response that doesn't exist anymore"));
×
359
        }
360
        return client_.MakeBodyAsyncReader(shared_from_this());
306✔
361
}
362

363
void IncomingResponse::SetBodyWriter(io::WriterPtr writer) {
87✔
364
        auto exp_reader = MakeBodyAsyncReader();
87✔
365
        if (!exp_reader) {
87✔
366
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
39✔
367
                        log::Error(exp_reader.error().String());
×
368
                }
369
                return;
370
        }
371
        auto &reader = exp_reader.value();
74✔
372

373
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
6,127✔
374
                if (err != error::NoError) {
54✔
375
                        log::Error("Could not copy HTTP stream: " + err.String());
8✔
376
                }
377
        });
54✔
378
}
379

380
io::ExpectedAsyncReadWriterPtr IncomingResponse::SwitchProtocol() {
8✔
381
        if (*cancelled_) {
8✔
382
                return expected::unexpected(MakeError(
2✔
383
                        StreamCancelledError, "Cannot switch protocol when the stream doesn't exist anymore"));
1✔
384
        }
385
        return client_.GetHttpClient().SwitchProtocol(shared_from_this());
14✔
386
}
387

388
OutgoingResponse::~OutgoingResponse() {
828✔
389
        if (!*cancelled_) {
276✔
390
                stream_.server_.RemoveStream(stream_.shared_from_this());
8✔
391
        }
392
}
828✔
393

394
void OutgoingResponse::Cancel() {
1✔
395
        if (!*cancelled_) {
1✔
396
                stream_.Cancel();
1✔
397
                stream_.server_.RemoveStream(stream_.shared_from_this());
2✔
398
        }
399
}
1✔
400

401
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
272✔
402
        status_code_ = code;
272✔
403
        status_message_ = message;
272✔
404
}
272✔
405

406
void OutgoingResponse::SetHeader(const string &name, const string &value) {
337✔
407
        headers_[name] = value;
408
}
337✔
409

410
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
231✔
411
        async_body_reader_ = nullptr;
231✔
412
        body_reader_ = body_reader;
413
}
231✔
414

415
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
416
        body_reader_ = nullptr;
4✔
417
        async_body_reader_ = body_reader;
418
}
4✔
419

420
error::Error OutgoingResponse::AsyncReply(ReplyFinishedHandler reply_finished_handler) {
263✔
421
        if (*cancelled_) {
263✔
422
                return MakeError(StreamCancelledError, "Cannot reply when response doesn't exist anymore");
2✔
423
        }
424
        return stream_.server_.AsyncReply(shared_from_this(), reply_finished_handler);
786✔
425
}
426

427
error::Error OutgoingResponse::AsyncSwitchProtocol(SwitchProtocolHandler handler) {
9✔
428
        if (*cancelled_) {
9✔
429
                return MakeError(
×
430
                        StreamCancelledError, "Cannot switch protocol when response doesn't exist anymore");
431
        }
432
        return stream_.server_.AsyncSwitchProtocol(shared_from_this(), handler);
27✔
433
}
434

435
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
168✔
436
        iteration_++;
168✔
437

438
        if (try_count_ > 0 && iteration_ > try_count_) {
168✔
439
                return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
21✔
440
        }
441

442
        chrono::milliseconds current_interval = smallest_interval_;
161✔
443
        // Backoff algorithm: Each interval is returned three times, then it's doubled, and then
444
        // that is returned three times, and so on. But if interval is ever higher than the max
445
        // interval, then return the max interval instead, and once that is returned three times,
446
        // produce MaxRetryError. If try_count_ is set, then that controls the total number of
447
        // retries, but the rest is the same, so then it simply "gets stuck" at max interval for
448
        // many iterations.
449
        for (int count = 3; count < iteration_; count += 3) {
358✔
450
                auto new_interval = current_interval * 2;
451
                if (new_interval > max_interval_) {
202✔
452
                        new_interval = max_interval_;
453
                }
454
                if (try_count_ <= 0 && new_interval == current_interval) {
202✔
455
                        return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
15✔
456
                }
457
                current_interval = new_interval;
458
        }
459

460
        return current_interval;
461
}
462

463
static expected::ExpectedString GetProxyStringFromEnvironment(
456✔
464
        const string &primary, const string &secondary) {
465
        bool primary_set = false, secondary_set = false;
466

467
        if (getenv(primary.c_str()) != nullptr && getenv(primary.c_str())[0] != '\0') {
456✔
468
                primary_set = true;
469
        }
470
        if (getenv(secondary.c_str()) != nullptr && getenv(secondary.c_str())[0] != '\0') {
456✔
471
                secondary_set = true;
472
        }
473

474
        if (primary_set && secondary_set) {
3✔
475
                return expected::unexpected(error::Error(
3✔
476
                        make_error_condition(errc::invalid_argument),
6✔
477
                        primary + " and " + secondary
3✔
478
                                + " environment variables can't both be set at the same time"));
9✔
479
        } else if (primary_set) {
453✔
480
                return getenv(primary.c_str());
3✔
481
        } else if (secondary_set) {
450✔
482
                return getenv(secondary.c_str());
×
483
        } else {
484
                return "";
450✔
485
        }
486
}
487

488
// The proxy variables aren't standardized, but this page was useful for the common patterns:
489
// https://superuser.com/questions/944958/are-http-proxy-https-proxy-and-no-proxy-environment-variables-standard
490
expected::ExpectedString GetHttpProxyStringFromEnvironment() {
153✔
491
        if (getenv("REQUEST_METHOD") != nullptr && getenv("HTTP_PROXY") != nullptr) {
153✔
492
                return expected::unexpected(error::Error(
×
493
                        make_error_condition(errc::operation_not_permitted),
×
494
                        "Using REQUEST_METHOD (CGI) together with HTTP_PROXY is insecure. See https://github.com/golang/go/issues/16405"));
×
495
        }
496
        return GetProxyStringFromEnvironment("http_proxy", "HTTP_PROXY");
306✔
497
}
498

499
expected::ExpectedString GetHttpsProxyStringFromEnvironment() {
152✔
500
        return GetProxyStringFromEnvironment("https_proxy", "HTTPS_PROXY");
304✔
501
}
502

503
expected::ExpectedString GetNoProxyStringFromEnvironment() {
151✔
504
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
302✔
505
}
506

507
// The proxy variables aren't standardized, but this page was useful for the common patterns:
508
// https://superuser.com/questions/944958/are-http-proxy-https-proxy-and-no-proxy-environment-variables-standard
509
bool HostNameMatchesNoProxy(const string &host, const string &no_proxy) {
43✔
510
        auto entries = common::SplitString(no_proxy, " ");
86✔
511
        for (string &entry : entries) {
80✔
512
                if (entry[0] == '.') {
49✔
513
                        // Wildcard.
514
                        ssize_t wildcard_len = entry.size() - 1;
5✔
515
                        if (wildcard_len == 0
516
                                || entry.compare(0, wildcard_len, host, host.size() - wildcard_len)) {
5✔
517
                                return true;
5✔
518
                        }
519
                } else if (host == entry) {
44✔
520
                        return true;
521
                }
522
        }
523

524
        return false;
525
}
43✔
526

527
} // namespace http
528
} // namespace common
529
} // 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

© 2026 Coveralls, Inc