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

mendersoftware / mender / 1384575330

23 Jul 2024 01:03PM UTC coverage: 75.647% (+0.02%) from 75.624%
1384575330

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>

1 of 1 new or added line in 1 file covered. (100.0%)

16 existing lines in 1 file now uncovered.

7098 of 9383 relevant lines covered (75.65%)

11619.5 hits per line

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

87.92
/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);
848✔
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
string URLDecode(const string &value) {
9✔
192
        stringstream unescaped;
18✔
193

194
        auto len = value.length();
195
        for (size_t i = 0; i < len; i++) {
132✔
196
                if (value[i] != '%') {
123✔
197
                        unescaped << value[i];
119✔
198
                } else {
199
                        unsigned int num;
200
                        sscanf(value.substr(i + 1, 2).c_str(), "%x", &num);
4✔
201
                        unescaped << static_cast<char>(num);
4✔
202
                        i += 2;
4✔
203
                }
204
        }
205
        return unescaped.str();
9✔
206
}
207

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

214
        auto suffix_start = suffix.cbegin();
215
        while (suffix_start != suffix.cend() && *suffix_start == '/') {
238✔
216
                suffix_start++;
217
        }
218

219
        return string(prefix.cbegin(), prefix_end) + "/" + string(suffix_start, suffix.cend());
336✔
220
}
221

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

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

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

237
string Request::GetHost() const {
272✔
238
        return address_.host;
272✔
239
}
240

UNCOV
241
string Request::GetProtocol() const {
×
UNCOV
242
        return address_.protocol;
×
243
}
244

245
int Request::GetPort() const {
811✔
246
        return address_.port;
811✔
247
}
248

249
Method Request::GetMethod() const {
90✔
250
        return method_;
90✔
251
}
252

253
string Request::GetPath() const {
153✔
254
        return address_.path;
153✔
255
}
256

257
unsigned Response::GetStatusCode() const {
650✔
258
        return status_code_;
650✔
259
}
260

261
string Response::GetStatusMessage() const {
382✔
262
        return status_message_;
382✔
263
}
264

265
void BaseOutgoingRequest::SetMethod(Method method) {
239✔
266
        method_ = method;
239✔
267
}
239✔
268

269
void BaseOutgoingRequest::SetHeader(const string &name, const string &value) {
472✔
270
        headers_[name] = value;
271
}
472✔
272

273
void BaseOutgoingRequest::SetBodyGenerator(BodyGenerator body_gen) {
36✔
274
        async_body_gen_ = nullptr;
36✔
275
        async_body_reader_ = nullptr;
36✔
276
        body_gen_ = body_gen;
36✔
277
}
36✔
278

279
void BaseOutgoingRequest::SetAsyncBodyGenerator(AsyncBodyGenerator body_gen) {
6✔
280
        body_gen_ = nullptr;
6✔
281
        body_reader_ = nullptr;
6✔
282
        async_body_gen_ = body_gen;
6✔
283
}
6✔
284

285
error::Error OutgoingRequest::SetAddress(const string &address) {
221✔
286
        orig_address_ = address;
221✔
287

288
        return BreakDownUrl(address, address_);
221✔
289
}
290

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

297
void IncomingRequest::Cancel() {
2✔
298
        if (!*cancelled_) {
2✔
299
                stream_.Cancel();
2✔
300
        }
301
}
2✔
302

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

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

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

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

336
IncomingResponse::IncomingResponse(ClientInterface &client, shared_ptr<bool> cancelled) :
304✔
337
        client_ {client},
338
        cancelled_ {cancelled} {
608✔
339
}
304✔
340

341
void IncomingResponse::Cancel() {
1✔
342
        if (!*cancelled_) {
1✔
343
                client_.Cancel();
1✔
344
        }
345
}
1✔
346

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

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

365
        io::AsyncCopy(writer, reader, [reader](error::Error err) {
4,020✔
366
                if (err != error::NoError) {
47✔
367
                        log::Error("Could not copy HTTP stream: " + err.String());
8✔
368
                }
369
        });
177✔
370
}
371

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

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

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

393
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
212✔
394
        status_code_ = code;
212✔
395
        status_message_ = message;
212✔
396
}
212✔
397

398
void OutgoingResponse::SetHeader(const string &name, const string &value) {
228✔
399
        headers_[name] = value;
400
}
228✔
401

402
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
172✔
403
        async_body_reader_ = nullptr;
172✔
404
        body_reader_ = body_reader;
405
}
172✔
406

407
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
408
        body_reader_ = nullptr;
4✔
409
        async_body_reader_ = body_reader;
410
}
4✔
411

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

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

427
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
119✔
428
        iteration_++;
119✔
429

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

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

452
        return current_interval;
453
}
454

455
static expected::ExpectedString GetProxyStringFromEnvironment(
360✔
456
        const string &primary, const string &secondary) {
457
        bool primary_set = false, secondary_set = false;
458

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

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

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

491
expected::ExpectedString GetHttpsProxyStringFromEnvironment() {
120✔
492
        return GetProxyStringFromEnvironment("https_proxy", "HTTPS_PROXY");
240✔
493
}
494

495
expected::ExpectedString GetNoProxyStringFromEnvironment() {
119✔
496
        return GetProxyStringFromEnvironment("no_proxy", "NO_PROXY");
238✔
497
}
498

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

516
        return false;
517
}
518

519
} // namespace http
520
} // namespace common
521
} // 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