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

mendersoftware / mender / 1058883419

02 Nov 2023 12:38PM UTC coverage: 80.182% (+0.2%) from 79.944%
1058883419

push

gitlab-ci

kacf
fix: Make sure RebootAction::Automatic is handled in RollbackReboot.

Changelog: None
Ticket: None

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

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

6947 of 8664 relevant lines covered (80.18%)

9289.26 hits per line

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

83.81
/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 HTTPInitError:
49
                return "Failed to initialize the client";
×
50
        case UnsupportedMethodError:
51
                return "Unsupported HTTP method";
×
52
        case StreamCancelledError:
53
                return "Stream has been cancelled/destroyed";
×
54
        case UnsupportedBodyType:
55
                return "HTTP stream has a body type we don't understand";
×
56
        case MaxRetryError:
57
                return "Tried maximum number of times";
2✔
58
        case DownloadResumerError:
59
                return "Resume download error";
5✔
60
        case ProxyError:
61
                return "Proxy error";
2✔
62
        }
63
        // Don't use "default" case. This should generate a warning if we ever add any enums. But
64
        // still assert here for safety.
65
        assert(false);
66
        return "Unknown";
×
67
}
68

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

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

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

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

107
        address.protocol = url.substr(0, split_index);
930✔
108

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

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

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

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

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

155
        return error::NoError;
464✔
156
}
157

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

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

174
        return escaped.str();
15✔
175
}
176

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

260
        return BreakDownUrl(address, address_);
215✔
261
}
262

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

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

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

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

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

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

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

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

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

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

337
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
3,198✔
338
                if (err != error::NoError) {
40✔
339
                        log::Error("Could not copy HTTP stream: " + err.String());
8✔
340
                }
341
        });
158✔
342
}
343

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

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

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

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

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

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

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

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

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

399
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
118✔
400
        iteration_++;
118✔
401

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

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

424
        return current_interval;
425
}
426

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

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

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

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

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

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

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

488
        return false;
489
}
490

491
} // namespace http
492
} // 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