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

mendersoftware / mender / 1395024768

31 Jul 2024 08:57AM UTC coverage: 79.856%. Remained the same
1395024768

push

gitlab-ci

lluiscampos
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>
(cherry picked from commit 819d3d1b2)

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

89 existing lines in 2 files now uncovered.

7120 of 8916 relevant lines covered (79.86%)

12270.62 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 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 MaxRetryError:
55
                return "Tried maximum number of times";
2✔
56
        case DownloadResumerError:
57
                return "Resume download error";
5✔
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) {
104✔
68
        return error::Error(error_condition(code, HttpErrorCategory), msg);
852✔
69
}
70

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

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

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

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

117
        auto auth_index = address.host.rfind("@");
484✔
118
        if (auth_index != string::npos) {
484✔
119
                if (!with_auth) {
8✔
120
                        address = {};
1✔
121
                        return error::Error(
122
                                make_error_condition(errc::not_supported),
2✔
123
                                "URL Username and password is not supported");
2✔
124
                }
125
                auto user_password = address.host.substr(0, auth_index);
7✔
126
                address.host = address.host.substr(auth_index + 1);
7✔
127
                auto u_pw_sep_index = user_password.find(":");
7✔
128
                if (u_pw_sep_index == string::npos) {
7✔
129
                        // no password
130
                        address.username = std::move(user_password);
1✔
131
                } else {
132
                        address.username = user_password.substr(0, u_pw_sep_index);
6✔
133
                        address.password = user_password.substr(u_pw_sep_index + 1);
12✔
134
                }
135
        }
136

137
        split_index = address.host.find(":");
483✔
138
        if (split_index != string::npos) {
483✔
139
                tmp = std::move(address.host);
472✔
140
                address.host = tmp.substr(0, split_index);
472✔
141

142
                tmp = tmp.substr(split_index + 1);
472✔
143
                auto port = common::StringToLongLong(tmp);
472✔
144
                if (!port) {
472✔
145
                        address = {};
×
146
                        return error::Error(port.error().code, url + " contains invalid port number");
×
147
                }
148
                address.port = port.value();
472✔
149
        } else {
150
                if (address.protocol == "http") {
11✔
151
                        address.port = 80;
5✔
152
                } else if (address.protocol == "https") {
6✔
153
                        address.port = 443;
5✔
154
                } else {
155
                        address = {};
1✔
156
                        return error::Error(
157
                                make_error_condition(errc::protocol_not_supported),
2✔
158
                                "Cannot deduce port number from protocol " + address.protocol);
2✔
159
                }
160
        }
161

162
        log::Trace(
482✔
163
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
964✔
164
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + "),"
964✔
165
                + "(username: " + address.username
1,446✔
166
                + "), (password: " + (address.password == "" ? "" : "OMITTED") + ")");
1,446✔
167

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

171
string URLEncode(const string &value) {
15✔
172
        stringstream escaped;
30✔
173
        escaped << hex;
15✔
174

175
        for (auto c : value) {
288✔
176
                // Keep alphanumeric and other accepted characters intact
177
                if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
273✔
178
                        escaped << c;
251✔
179
                } else {
180
                        // Any other characters are percent-encoded
181
                        escaped << uppercase;
22✔
182
                        escaped << '%' << setw(2) << int((unsigned char) c);
22✔
183
                        escaped << nouppercase;
22✔
184
                }
185
        }
186

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

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

193
        auto len = value.length();
194
        for (size_t i = 0; i < len; i++) {
178✔
195
                if (value[i] != '%') {
169✔
196
                        unescaped << value[i];
161✔
197
                } else {
198
                        if ((i + 2 >= len) || !isxdigit(value[i + 1]) || !(isxdigit(value[i + 2]))) {
8✔
199
                                return expected::unexpected(
4✔
200
                                        MakeError(InvalidUrlError, "Incomplete % sequence in '" + value + "'"));
12✔
201
                        }
202
                        unsigned int num;
203
                        sscanf(value.substr(i + 1, 2).c_str(), "%x", &num);
4✔
204
                        unescaped << static_cast<char>(num);
4✔
205
                        i += 2;
206
                }
207
        }
208
        return unescaped.str();
18✔
209
}
210

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

455
        return current_interval;
456
}
457

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

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

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

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

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

498
expected::ExpectedString GetNoProxyStringFromEnvironment() {
119✔
499
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
238✔
500
}
501

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

519
        return false;
520
}
521

522
} // namespace http
523
} // 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