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

mendersoftware / mender / 1385817157

24 Jul 2024 08:11AM UTC coverage: 75.647%. Remained the same
1385817157

push

gitlab-ci

vpodzime
fix: URL-decode proxy username and password

In cases where the proxy username or password contains characters that
cannot show up in URLs, they should be URL-encoded.

This is to ensure backwards compatibility with the Mender client
3.

Unfortunately, tinyproxy considers all special characters in the
BasicAuth configuration entry as syntax error so we have no way
to test this.

Ticket: MEN-7402
Changelog: none
Signed-off-by: Vratislav Podzimek <vratislav.podzimek@northern.tech>

7 of 9 new or added lines in 2 files covered. (77.78%)

16 existing lines in 1 file now uncovered.

7104 of 9391 relevant lines covered (75.65%)

11609.6 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::StringToLongLong(tmp);
472✔
145
                if (!port) {
472✔
146
                        address = {};
×
147
                        return error::Error(port.error().code, 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

UNCOV
245
string Request::GetProtocol() const {
×
UNCOV
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✔
UNCOV
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✔
UNCOV
309
                return expected::unexpected(MakeError(
×
UNCOV
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✔
UNCOV
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✔
UNCOV
334
                return expected::unexpected(MakeError(
×
UNCOV
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✔
UNCOV
353
                return expected::unexpected(MakeError(
×
UNCOV
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✔
UNCOV
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(
UNCOV
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(
360✔
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') {
360✔
464
                primary_set = true;
465
        }
466
        if (getenv(secondary.c_str()) != nullptr && getenv(secondary.c_str())[0] != '\0') {
360✔
467
                secondary_set = true;
468
        }
469

470
        if (primary_set && secondary_set) {
360✔
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) {
357✔
476
                return getenv(primary.c_str());
3✔
477
        } else if (secondary_set) {
354✔
UNCOV
478
                return getenv(secondary.c_str());
×
479
        } else {
480
                return "";
354✔
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() {
121✔
487
        if (getenv("REQUEST_METHOD") != nullptr && getenv("HTTP_PROXY") != nullptr) {
121✔
UNCOV
488
                return expected::unexpected(error::Error(
×
UNCOV
489
                        make_error_condition(errc::operation_not_permitted),
×
UNCOV
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");
242✔
493
}
494

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

499
expected::ExpectedString GetNoProxyStringFromEnvironment() {
119✔
500
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
238✔
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

© 2026 Coveralls, Inc