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

mendersoftware / mender / 1057366047

01 Nov 2023 08:50AM UTC coverage: 80.121% (-0.07%) from 80.195%
1057366047

push

gitlab-ci

kacf
fix: Don't cache server URL and token locally in process.

The problem if we do is that is that we may then connect to a
forwarder address which doesn't exist anymore. This produces
"connection refused", not "Not authorized", and therefore it does not
trigger an authentication request by itself. So always ask.

This can be easily reproduced by bringing up both mender-auth and
mender-update, and then restarting mender-auth after the first auth
cycle.

This makes the whole `AuthenticatorExternalTokenUpdateTest` test
irrelevant, since we no longer need to test the caching capability of
the client side authenticator. So just remove it.

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

6884 of 8592 relevant lines covered (80.12%)

9363.06 hits per line

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

84.0
/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";
24✔
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";
×
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";
9✔
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) {
90✔
68
        return error::Error(error_condition(code, HttpErrorCategory), msg);
371✔
69
}
70

71
string MethodToString(Method method) {
185✔
72
        switch (method) {
185✔
73
        case Method::Invalid:
74
                return "Invalid";
×
75
        case Method::GET:
76
                return "GET";
164✔
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) {
463✔
95
        const string url_split {"://"};
463✔
96

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

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

107
        auto tmp = url.substr(split_index + url_split.size());
461✔
108
        split_index = tmp.find("/");
461✔
109
        if (split_index == string::npos) {
461✔
110
                address.host = tmp;
282✔
111
                address.path = "/";
282✔
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) {
461✔
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(":");
461✔
125
        if (split_index != string::npos) {
461✔
126
                tmp = std::move(address.host);
451✔
127
                address.host = tmp.substr(0, split_index);
451✔
128

129
                tmp = tmp.substr(split_index + 1);
451✔
130
                auto port = common::StringToLongLong(tmp);
451✔
131
                if (!port) {
451✔
132
                        address = {};
×
133
                        return error::Error(port.error().code, url + " contains invalid port number");
×
134
                }
135
                address.port = port.value();
451✔
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(
460✔
150
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
920✔
151
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")");
1,380✔
152

153
        return error::NoError;
460✔
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,497✔
190
        string lower_str(str.length(), ' ');
191
        transform(
192
                str.begin(), str.end(), lower_str.begin(), [](unsigned char c) { return std::tolower(c); });
30,069✔
193
        return hash<string>()(lower_str);
2,497✔
194
}
195

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

200
expected::ExpectedString Transaction::GetHeader(const string &name) const {
721✔
201
        if (headers_.find(name) == headers_.end()) {
721✔
202
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
741✔
203
        }
204
        return headers_.at(name);
474✔
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 {
89✔
220
        return method_;
89✔
221
}
222

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

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

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

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

239
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
454✔
240
        headers_[name] = value;
241
}
454✔
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) {
213✔
256
        orig_address_ = address;
213✔
257

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

261
IncomingRequest::~IncomingRequest() {
434✔
262
        if (!*cancelled_) {
217✔
263
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
264
        }
265
}
217✔
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, BodyWriterErrorMode mode) {
40✔
282
        auto exp_reader = MakeBodyAsyncReader();
40✔
283
        if (!exp_reader) {
40✔
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();
32✔
290

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

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

309
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
290✔
310
        client_ {client},
311
        cancelled_ {cancelled} {
580✔
312
}
290✔
313

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

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

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

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

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

356
OutgoingResponse::~OutgoingResponse() {
414✔
357
        if (!*cancelled_) {
207✔
358
                stream_.server_.RemoveStream(stream_.shared_from_this());
6✔
359
        }
360
}
207✔
361

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

369
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
204✔
370
        status_code_ = code;
204✔
371
        status_message_ = message;
204✔
372
}
204✔
373

374
void OutgoingResponse::SetHeader(const string &name, const string &value) {
222✔
375
        headers_[name] = value;
376
}
222✔
377

378
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
166✔
379
        async_body_reader_ = nullptr;
166✔
380
        body_reader_ = body_reader;
381
}
166✔
382

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

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

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

403
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
117✔
404
        iteration_++;
117✔
405

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

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

428
        return current_interval;
429
}
430

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

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

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

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

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

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

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

492
        return false;
493
}
494

495
} // namespace http
496
} // 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