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

mendersoftware / mender / 1500056092

17 Oct 2024 10:12AM UTC coverage: 76.273% (+0.01%) from 76.262%
1500056092

push

gitlab-ci

kacf
chore: Fix typo, probably a copy/paste error from the state code.

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

7310 of 9584 relevant lines covered (76.27%)

11287.15 hits per line

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

88.01
/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 {
37✔
38
        switch (code) {
37✔
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";
2✔
57
        case DownloadResumerError:
58
                return "Resume download error";
5✔
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) {
104✔
69
        return error::Error(error_condition(code, HttpErrorCategory), msg);
852✔
70
}
71

72
string MethodToString(Method method) {
142✔
73
        switch (method) {
142✔
74
        case Method::Invalid:
75
                return "Invalid";
×
76
        case Method::GET:
77
                return "GET";
121✔
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) {
486✔
96
        const string url_split {"://"};
486✔
97

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

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

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

118
        auto auth_index = address.host.rfind("@");
484✔
119
        if (auth_index != string::npos) {
484✔
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(":");
483✔
139
        if (split_index != string::npos) {
483✔
140
                tmp = std::move(address.host);
472✔
141
                address.host = tmp.substr(0, split_index);
472✔
142

143
                tmp = tmp.substr(split_index + 1);
472✔
144
                auto port = common::StringTo<decltype(address.port)>(tmp);
472✔
145
                if (!port) {
472✔
146
                        address = {};
×
147
                        return port.error().WithContext(url + " contains invalid port number");
×
148
                }
149
                address.port = port.value();
472✔
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(
482✔
164
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
964✔
165
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + "),"
964✔
166
                + "(username: " + address.username
1,446✔
167
                + "), (password: " + (address.password == "" ? "" : "OMITTED") + ")");
1,446✔
168

169
        return error::NoError;
482✔
170
}
171

172
string URLEncode(const string &value) {
15✔
173
        stringstream escaped;
30✔
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) << int((unsigned char) c);
22✔
184
                        escaped << nouppercase;
22✔
185
                }
186
        }
187

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

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

194
        auto len = value.length();
195
        for (size_t i = 0; i < len; i++) {
178✔
196
                if (value[i] != '%') {
169✔
197
                        unescaped << value[i];
161✔
198
                } else {
199
                        if ((i + 2 >= len) || !isxdigit(value[i + 1]) || !(isxdigit(value[i + 2]))) {
8✔
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);
4✔
205
                        unescaped << static_cast<char>(num);
4✔
206
                        i += 2;
207
                }
208
        }
209
        return unescaped.str();
18✔
210
}
211

212
string JoinOneUrl(const string &prefix, const string &suffix) {
168✔
213
        auto prefix_end = prefix.cend();
214
        while (prefix_end != prefix.cbegin() && prefix_end[-1] == '/') {
180✔
215
                prefix_end--;
216
        }
217

218
        auto suffix_start = suffix.cbegin();
219
        while (suffix_start != suffix.cend() && *suffix_start == '/') {
238✔
220
                suffix_start++;
221
        }
222

223
        return string(prefix.cbegin(), prefix_end) + "/" + string(suffix_start, suffix.cend());
336✔
224
}
225

226
size_t CaseInsensitiveHasher::operator()(const string &str) const {
3,039✔
227
        return hash<string>()(common::StringToLower(str));
3,039✔
228
}
229

230
bool CaseInsensitiveComparator::operator()(const string &str1, const string &str2) const {
1,058✔
231
        return strcasecmp(str1.c_str(), str2.c_str()) == 0;
1,058✔
232
}
233

234
expected::ExpectedString Transaction::GetHeader(const string &name) const {
1,201✔
235
        if (headers_.find(name) == headers_.end()) {
1,201✔
236
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
2,124✔
237
        }
238
        return headers_.at(name);
493✔
239
}
240

241
string Request::GetHost() const {
272✔
242
        return address_.host;
272✔
243
}
244

245
string Request::GetProtocol() const {
×
246
        return address_.protocol;
×
247
}
248

249
int Request::GetPort() const {
811✔
250
        return address_.port;
811✔
251
}
252

253
Method Request::GetMethod() const {
90✔
254
        return method_;
90✔
255
}
256

257
string Request::GetPath() const {
153✔
258
        return address_.path;
153✔
259
}
260

261
unsigned Response::GetStatusCode() const {
650✔
262
        return status_code_;
650✔
263
}
264

265
string Response::GetStatusMessage() const {
382✔
266
        return status_message_;
382✔
267
}
268

269
void BaseOutgoingRequest::SetMethod(Method method) {
239✔
270
        method_ = method;
239✔
271
}
239✔
272

273
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
472✔
274
        headers_[name] = value;
275
}
472✔
276

277
void BaseOutgoingRequest::SetBodyGenerator(BodyGenerator body_gen) {
36✔
278
        async_body_gen_ = nullptr;
36✔
279
        async_body_reader_ = nullptr;
36✔
280
        body_gen_ = body_gen;
36✔
281
}
36✔
282

283
void BaseOutgoingRequest::SetAsyncBodyGenerator(AsyncBodyGenerator body_gen) {
6✔
284
        body_gen_ = nullptr;
6✔
285
        body_reader_ = nullptr;
6✔
286
        async_body_gen_ = body_gen;
6✔
287
}
6✔
288

289
error::Error OutgoingRequest::SetAddress(const string &address) {
221✔
290
        orig_address_ = address;
221✔
291

292
        return BreakDownUrl(address, address_);
221✔
293
}
294

295
IncomingRequest::~IncomingRequest() {
450✔
296
        if (!*cancelled_) {
225✔
297
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
298
        }
299
}
225✔
300

301
void IncomingRequest::Cancel() {
2✔
302
        if (!*cancelled_) {
2✔
303
                stream_.Cancel();
2✔
304
        }
305
}
2✔
306

307
io::ExpectedAsyncReaderPtr IncomingRequest::MakeBodyAsyncReader() {
56✔
308
        if (*cancelled_) {
56✔
309
                return expected::unexpected(MakeError(
×
310
                        StreamCancelledError, "Cannot make reader for a request that doesn't exist anymore"));
×
311
        }
312
        return stream_.server_.MakeBodyAsyncReader(shared_from_this());
112✔
313
}
314

315
void IncomingRequest::SetBodyWriter(io::WriterPtr writer) {
42✔
316
        auto exp_reader = MakeBodyAsyncReader();
42✔
317
        if (!exp_reader) {
42✔
318
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
20✔
319
                        log::Error(exp_reader.error().String());
×
320
                }
321
                return;
322
        }
323
        auto &reader = exp_reader.value();
32✔
324

325
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
2,566✔
326
                if (err != error::NoError) {
32✔
327
                        log::Error("Could not copy HTTP stream: " + err.String());
4✔
328
                }
329
        });
96✔
330
}
331

332
ExpectedOutgoingResponsePtr IncomingRequest::MakeResponse() {
216✔
333
        if (*cancelled_) {
216✔
334
                return expected::unexpected(MakeError(
×
335
                        StreamCancelledError, "Cannot make response for a request that doesn't exist anymore"));
×
336
        }
337
        return stream_.server_.MakeResponse(shared_from_this());
432✔
338
}
339

340
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
304✔
341
        client_ {client},
342
        cancelled_ {cancelled} {
608✔
343
}
304✔
344

345
void IncomingResponse::Cancel() {
1✔
346
        if (!*cancelled_) {
1✔
347
                client_.Cancel();
1✔
348
        }
349
}
1✔
350

351
io::ExpectedAsyncReaderPtr IncomingResponse::MakeBodyAsyncReader() {
142✔
352
        if (*cancelled_) {
142✔
353
                return expected::unexpected(MakeError(
×
354
                        StreamCancelledError, "Cannot make reader for a response that doesn't exist anymore"));
×
355
        }
356
        return client_.MakeBodyAsyncReader(shared_from_this());
284✔
357
}
358

359
void IncomingResponse::SetBodyWriter(io::WriterPtr writer) {
77✔
360
        auto exp_reader = MakeBodyAsyncReader();
77✔
361
        if (!exp_reader) {
77✔
362
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
24✔
363
                        log::Error(exp_reader.error().String());
×
364
                }
365
                return;
366
        }
367
        auto &reader = exp_reader.value();
65✔
368

369
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
4,008✔
370
                if (err != error::NoError) {
46✔
371
                        log::Error("Could not copy HTTP stream: " + err.String());
8✔
372
                }
373
        });
176✔
374
}
375

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

384
OutgoingResponse::~OutgoingResponse() {
432✔
385
        if (!*cancelled_) {
216✔
386
                stream_.server_.RemoveStream(stream_.shared_from_this());
8✔
387
        }
388
}
216✔
389

390
void OutgoingResponse::Cancel() {
1✔
391
        if (!*cancelled_) {
1✔
392
                stream_.Cancel();
1✔
393
                stream_.server_.RemoveStream(stream_.shared_from_this());
2✔
394
        }
395
}
1✔
396

397
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
212✔
398
        status_code_ = code;
212✔
399
        status_message_ = message;
212✔
400
}
212✔
401

402
void OutgoingResponse::SetHeader(const string &name, const string &value) {
228✔
403
        headers_[name] = value;
404
}
228✔
405

406
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
172✔
407
        async_body_reader_ = nullptr;
172✔
408
        body_reader_ = body_reader;
409
}
172✔
410

411
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
412
        body_reader_ = nullptr;
4✔
413
        async_body_reader_ = body_reader;
414
}
4✔
415

416
error::Error OutgoingResponse::AsyncReply(ReplyFinishedHandler reply_finished_handler) {
203✔
417
        if (*cancelled_) {
203✔
418
                return MakeError(StreamCancelledError, "Cannot reply when response doesn't exist anymore");
2✔
419
        }
420
        return stream_.server_.AsyncReply(shared_from_this(), reply_finished_handler);
606✔
421
}
422

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

431
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
119✔
432
        iteration_++;
119✔
433

434
        if (try_count_ > 0 && iteration_ > try_count_) {
119✔
435
                return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
15✔
436
        }
437

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

456
        return current_interval;
457
}
458

459
static expected::ExpectedString GetProxyStringFromEnvironment(
456✔
460
        const string &primary, const string &secondary) {
461
        bool primary_set = false, secondary_set = false;
462

463
        if (getenv(primary.c_str()) != nullptr && getenv(primary.c_str())[0] != '\0') {
456✔
464
                primary_set = true;
465
        }
466
        if (getenv(secondary.c_str()) != nullptr && getenv(secondary.c_str())[0] != '\0') {
456✔
467
                secondary_set = true;
468
        }
469

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

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

495
expected::ExpectedString GetHttpsProxyStringFromEnvironment() {
152✔
496
        return GetProxyStringFromEnvironment("https_proxy", "HTTPS_PROXY");
304✔
497
}
498

499
expected::ExpectedString GetNoProxyStringFromEnvironment() {
151✔
500
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
302✔
501
}
502

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

520
        return false;
521
}
522

523
} // namespace http
524
} // namespace common
525
} // 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