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

mendersoftware / mender / 1057637229

01 Nov 2023 01:17PM UTC coverage: 79.933% (-0.3%) from 80.207%
1057637229

push

gitlab-ci

oleorhagen
chore: Alias mender-authd.service -> mender-client.service

This aliases the systemd.service for `mender-authd` to `mender-client`, so that
other existing third-party services relying on it will still work transparently.

Ticket: MEN-6812

Signed-off-by: Ole Petter <ole.orhagen@northern.tech>

6899 of 8631 relevant lines covered (79.93%)

9322.97 hits per line

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

84.06
/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);
389✔
71
}
72

73
string MethodToString(Method method) {
188✔
74
        switch (method) {
188✔
75
        case Method::Invalid:
76
                return "Invalid";
×
77
        case Method::GET:
78
                return "GET";
167✔
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) {
465✔
97
        const string url_split {"://"};
465✔
98

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

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

109
        auto tmp = url.substr(split_index + url_split.size());
463✔
110
        split_index = tmp.find("/");
463✔
111
        if (split_index == string::npos) {
463✔
112
                address.host = tmp;
284✔
113
                address.path = "/";
284✔
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) {
463✔
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(":");
463✔
127
        if (split_index != string::npos) {
463✔
128
                tmp = std::move(address.host);
453✔
129
                address.host = tmp.substr(0, split_index);
453✔
130

131
                tmp = tmp.substr(split_index + 1);
453✔
132
                auto port = common::StringToLongLong(tmp);
453✔
133
                if (!port) {
453✔
134
                        address = {};
×
135
                        return error::Error(port.error().code, url + " contains invalid port number");
×
136
                }
137
                address.port = port.value();
453✔
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(
462✔
152
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
924✔
153
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")");
1,386✔
154

155
        return error::NoError;
462✔
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,516✔
192
        string lower_str(str.length(), ' ');
193
        transform(
194
                str.begin(), str.end(), lower_str.begin(), [](unsigned char c) { return std::tolower(c); });
30,304✔
195
        return hash<string>()(lower_str);
2,516✔
196
}
197

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

202
expected::ExpectedString Transaction::GetHeader(const string &name) const {
727✔
203
        if (headers_.find(name) == headers_.end()) {
727✔
204
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
747✔
205
        }
206
        return headers_.at(name);
478✔
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 {
536✔
230
        return status_code_;
536✔
231
}
232

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

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

241
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
457✔
242
        headers_[name] = value;
243
}
457✔
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) {
214✔
258
        orig_address_ = address;
214✔
259

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

263
IncomingRequest::~IncomingRequest() {
438✔
264
        if (!*cancelled_) {
219✔
265
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
266
        }
267
}
219✔
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, BodyWriterErrorMode mode) {
40✔
284
        auto exp_reader = MakeBodyAsyncReader();
40✔
285
        if (!exp_reader) {
40✔
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();
32✔
292

293
        io::AsyncCopy(writer, reader, [reader, mode](error::Error err) {
1,925✔
294
                if (err != error::NoError) {
32✔
295
                        log::Error("Could not copy HTTP stream: " + err.String());
6✔
296
                        if (mode == BodyWriterErrorMode::Cancel) {
3✔
297
                                reader->Cancel();
2✔
298
                        }
299
                }
300
        });
96✔
301
}
302

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

311
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
294✔
312
        client_ {client},
313
        cancelled_ {cancelled} {
588✔
314
}
294✔
315

316
void IncomingResponse::Cancel() {
1✔
317
        if (!*cancelled_) {
1✔
318
                client_.Cancel();
1✔
319
        }
320
}
1✔
321

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

330
void IncomingResponse::SetBodyWriter(io::WriterPtr writer, BodyWriterErrorMode mode) {
72✔
331
        auto exp_reader = MakeBodyAsyncReader();
72✔
332
        if (!exp_reader) {
72✔
333
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
24✔
334
                        log::Error(exp_reader.error().String());
×
335
                }
336
                return;
337
        }
338
        auto &reader = exp_reader.value();
60✔
339

340
        io::AsyncCopy(writer, reader, [reader, mode](error::Error err) {
3,394✔
341
                if (err != error::NoError) {
41✔
342
                        log::Error("Could not copy HTTP stream: " + err.String());
10✔
343
                        if (mode == BodyWriterErrorMode::Cancel) {
5✔
344
                                reader->Cancel();
4✔
345
                        }
346
                }
347
        });
161✔
348
}
349

350
io::ExpectedAsyncReadWriterPtr IncomingResponse::SwitchProtocol() {
8✔
351
        if (*cancelled_) {
8✔
352
                return expected::unexpected(MakeError(
1✔
353
                        StreamCancelledError, "Cannot switch protocol when the stream doesn't exist anymore"));
3✔
354
        }
355
        return client_.GetHttpClient().SwitchProtocol(shared_from_this());
14✔
356
}
357

358
OutgoingResponse::~OutgoingResponse() {
418✔
359
        if (!*cancelled_) {
209✔
360
                stream_.server_.RemoveStream(stream_.shared_from_this());
6✔
361
        }
362
}
209✔
363

364
void OutgoingResponse::Cancel() {
1✔
365
        if (!*cancelled_) {
1✔
366
                stream_.Cancel();
1✔
367
                stream_.server_.RemoveStream(stream_.shared_from_this());
2✔
368
        }
369
}
1✔
370

371
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
206✔
372
        status_code_ = code;
206✔
373
        status_message_ = message;
206✔
374
}
206✔
375

376
void OutgoingResponse::SetHeader(const string &name, const string &value) {
224✔
377
        headers_[name] = value;
378
}
224✔
379

380
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
168✔
381
        async_body_reader_ = nullptr;
168✔
382
        body_reader_ = body_reader;
383
}
168✔
384

385
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
386
        body_reader_ = nullptr;
4✔
387
        async_body_reader_ = body_reader;
388
}
4✔
389

390
error::Error OutgoingResponse::AsyncReply(ReplyFinishedHandler reply_finished_handler) {
197✔
391
        if (*cancelled_) {
197✔
392
                return MakeError(StreamCancelledError, "Cannot reply when response doesn't exist anymore");
2✔
393
        }
394
        return stream_.server_.AsyncReply(shared_from_this(), reply_finished_handler);
588✔
395
}
396

397
error::Error OutgoingResponse::AsyncSwitchProtocol(SwitchProtocolHandler handler) {
9✔
398
        if (*cancelled_) {
9✔
399
                return MakeError(
400
                        StreamCancelledError, "Cannot switch protocol when response doesn't exist anymore");
×
401
        }
402
        return stream_.server_.AsyncSwitchProtocol(shared_from_this(), handler);
27✔
403
}
404

405
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
117✔
406
        iteration_++;
117✔
407

408
        if (try_count_ > 0 && iteration_ > try_count_) {
117✔
409
                return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
15✔
410
        }
411

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

430
        return current_interval;
431
}
432

433
static expected::ExpectedString GetProxyStringFromEnvironment(
330✔
434
        const string &primary, const string &secondary) {
435
        bool primary_set = false, secondary_set = false;
436

437
        if (getenv(primary.c_str()) != nullptr && getenv(primary.c_str())[0] != '\0') {
330✔
438
                primary_set = true;
439
        }
440
        if (getenv(secondary.c_str()) != nullptr && getenv(secondary.c_str())[0] != '\0') {
330✔
441
                secondary_set = true;
442
        }
443

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

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

469
expected::ExpectedString GetHttpsProxyStringFromEnvironment() {
110✔
470
        return GetProxyStringFromEnvironment("https_proxy", "HTTPS_PROXY");
220✔
471
}
472

473
expected::ExpectedString GetNoProxyStringFromEnvironment() {
109✔
474
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
218✔
475
}
476

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

494
        return false;
495
}
496

497
} // namespace http
498
} // 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