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

mendersoftware / mender / 1018307341

27 Sep 2023 12:04PM UTC coverage: 77.682% (-0.4%) from 78.076%
1018307341

push

gitlab-ci

vpodzime
fix: Wait repeatedly for I/O events on DBus file descriptors

ASIO's `stream_descriptor::async_read/wait/error()` all register
one-off handlers. We actually need to watch the given file
descriptors for all events until we cancel the watch.

We can do that by using functors re-registering themselves
whenever called.

Also, add a tip and commented code to DBus tests helping debug
DBus issues.

Ticket: MEN-6655
Changelog: none
Signed-off-by: Vratislav Podzimek <v.podzimek@mykolab.com>

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

6474 of 8334 relevant lines covered (77.68%)

11035.56 hits per line

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

73.8
/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 {
1✔
37
        switch (code) {
1✔
38
        case NoError:
×
39
                return "Success";
×
40
        case NoSuchHeaderError:
×
41
                return "No such header";
×
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:
1✔
55
                return "Tried maximum number of times";
1✔
56
        }
57
        // Don't use "default" case. This should generate a warning if we ever add any enums. But
58
        // still assert here for safety.
59
        assert(false);
×
60
        return "Unknown";
61
}
62

63
error::Error MakeError(ErrorCode code, const string &msg) {
207✔
64
        return error::Error(error_condition(code, HttpErrorCategory), msg);
414✔
65
}
66

67
string MethodToString(Method method) {
94✔
68
        switch (method) {
94✔
69
        case Method::Invalid:
×
70
                return "Invalid";
×
71
        case Method::GET:
78✔
72
                return "GET";
78✔
73
        case Method::HEAD:
×
74
                return "HEAD";
×
75
        case Method::POST:
×
76
                return "POST";
×
77
        case Method::PUT:
16✔
78
                return "PUT";
16✔
79
        case Method::PATCH:
×
80
                return "PATCH";
×
81
        case Method::CONNECT:
×
82
                return "CONNECT";
×
83
        }
84
        // Don't use "default" case. This should generate a warning if we ever add any methods. But
85
        // still assert here for safety.
86
        assert(false);
×
87
        return "INVALID_METHOD";
88
}
89

90
error::Error BreakDownUrl(const string &url, BrokenDownUrl &address) {
318✔
91
        const string url_split {"://"};
636✔
92

93
        auto split_index = url.find(url_split);
318✔
94
        if (split_index == string::npos) {
318✔
95
                return MakeError(InvalidUrlError, url + " is not a valid URL.");
2✔
96
        }
97
        if (split_index == 0) {
317✔
98
                return MakeError(InvalidUrlError, url + ": missing hostname");
×
99
        }
100

101
        address.protocol = url.substr(0, split_index);
317✔
102

103
        auto tmp = url.substr(split_index + url_split.size());
634✔
104
        split_index = tmp.find("/");
317✔
105
        if (split_index == string::npos) {
317✔
106
                address.host = tmp;
177✔
107
                address.path = "/";
177✔
108
        } else {
109
                address.host = tmp.substr(0, split_index);
140✔
110
                address.path = tmp.substr(split_index);
140✔
111
        }
112

113
        split_index = address.host.find(":");
317✔
114
        if (split_index != string::npos) {
317✔
115
                tmp = std::move(address.host);
314✔
116
                address.host = tmp.substr(0, split_index);
314✔
117

118
                tmp = tmp.substr(split_index + 1);
314✔
119
                auto port = common::StringToLongLong(tmp);
314✔
120
                if (!port) {
314✔
121
                        return error::Error(port.error().code, url + " contains invalid port number");
×
122
                }
123
                address.port = port.value();
314✔
124
        } else {
125
                if (address.protocol == "http") {
3✔
126
                        address.port = 80;
1✔
127
                } else if (address.protocol == "https") {
2✔
128
                        address.port = 443;
1✔
129
                } else {
130
                        return error::Error(
131
                                make_error_condition(errc::protocol_not_supported),
1✔
132
                                "Cannot deduce port number from protocol " + address.protocol);
3✔
133
                }
134
        }
135

136
        log::Trace(
316✔
137
                "URL broken down into (protocol: " + address.protocol + "), (host: " + address.host
632✔
138
                + "), (port: " + to_string(address.port) + "), (path: " + address.path + ")");
1,264✔
139

140
        return error::NoError;
316✔
141
}
142

143
string URLEncode(const string &value) {
15✔
144
        stringstream escaped;
30✔
145
        escaped << hex;
15✔
146

147
        for (auto c : value) {
288✔
148
                // Keep alphanumeric and other accepted characters intact
149
                if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
273✔
150
                        escaped << c;
251✔
151
                } else {
152
                        // Any other characters are percent-encoded
153
                        escaped << uppercase;
22✔
154
                        escaped << '%' << setw(2) << int((unsigned char) c);
22✔
155
                        escaped << nouppercase;
22✔
156
                }
157
        }
158

159
        return escaped.str();
30✔
160
}
161

162
string JoinOneUrl(const string &prefix, const string &suffix) {
151✔
163
        auto prefix_end = prefix.cend();
151✔
164
        while (prefix_end != prefix.cbegin() && prefix_end[-1] == '/') {
158✔
165
                prefix_end--;
7✔
166
        }
167

168
        auto suffix_start = suffix.cbegin();
151✔
169
        while (suffix_start != suffix.cend() && *suffix_start == '/') {
207✔
170
                suffix_start++;
56✔
171
        }
172

173
        return string(prefix.cbegin(), prefix_end) + "/" + string(suffix_start, suffix.cend());
453✔
174
}
175

176
size_t CaseInsensitiveHasher::operator()(const string &str) const {
1,390✔
177
        string lower_str(str.length(), ' ');
1,390✔
178
        transform(
179
                str.begin(), str.end(), lower_str.begin(), [](unsigned char c) { return std::tolower(c); });
17,403✔
180
        return hash<string>()(lower_str);
2,779✔
181
}
182

183
bool CaseInsensitiveComparator::operator()(const string &str1, const string &str2) const {
449✔
184
        return strcasecmp(str1.c_str(), str2.c_str()) == 0;
449✔
185
}
186

187
expected::ExpectedString Transaction::GetHeader(const string &name) const {
349✔
188
        if (headers_.find(name) == headers_.end()) {
349✔
189
                return expected::unexpected(MakeError(NoSuchHeaderError, "No such header: " + name));
256✔
190
        }
191
        return headers_.at(name);
221✔
192
}
193

194
Method Request::GetMethod() const {
83✔
195
        return method_;
83✔
196
}
197

198
string Request::GetPath() const {
154✔
199
        return address_.path;
154✔
200
}
201

202
unsigned Response::GetStatusCode() const {
247✔
203
        return status_code_;
247✔
204
}
205

206
string Response::GetStatusMessage() const {
153✔
207
        return status_message_;
153✔
208
}
209

210
void OutgoingRequest::SetMethod(Method method) {
152✔
211
        method_ = method;
152✔
212
}
152✔
213

214
void OutgoingRequest::SetHeader(const string &name, const string &value) {
301✔
215
        headers_[name] = value;
301✔
216
}
301✔
217

218
error::Error OutgoingRequest::SetAddress(const string &address) {
152✔
219
        orig_address_ = address;
152✔
220

221
        return BreakDownUrl(address, address_);
152✔
222
}
223

224
void OutgoingRequest::SetBodyGenerator(BodyGenerator body_gen) {
40✔
225
        async_body_gen_ = nullptr;
40✔
226
        async_body_reader_ = nullptr;
40✔
227
        body_gen_ = body_gen;
40✔
228
}
40✔
229

230
void OutgoingRequest::SetAsyncBodyGenerator(AsyncBodyGenerator body_gen) {
6✔
231
        body_gen_ = nullptr;
6✔
232
        body_reader_ = nullptr;
6✔
233
        async_body_gen_ = body_gen;
6✔
234
}
6✔
235

236
IncomingRequest::~IncomingRequest() {
×
237
        if (!*cancelled_) {
×
238
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
239
        }
240
}
×
241

242
void IncomingRequest::Cancel() {
2✔
243
        if (!*cancelled_) {
2✔
244
                stream_.Cancel();
2✔
245
                stream_.server_.RemoveStream(stream_.shared_from_this());
2✔
246
        }
247
}
2✔
248

249
io::ExpectedAsyncReaderPtr IncomingRequest::MakeBodyAsyncReader() {
52✔
250
        if (*cancelled_) {
52✔
251
                return expected::unexpected(MakeError(
×
252
                        StreamCancelledError, "Cannot make reader for a request that doesn't exist anymore"));
×
253
        }
254
        return stream_.server_.MakeBodyAsyncReader(shared_from_this());
104✔
255
}
256

257
void IncomingRequest::SetBodyWriter(io::WriterPtr writer, BodyWriterErrorMode mode) {
45✔
258
        auto exp_reader = MakeBodyAsyncReader();
45✔
259
        if (!exp_reader) {
45✔
260
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
8✔
261
                        log::Error(exp_reader.error().String());
×
262
                }
263
                return;
8✔
264
        }
265
        auto &reader = exp_reader.value();
37✔
266

267
        io::AsyncCopy(writer, reader, [reader, mode](error::Error err) {
37✔
268
                if (err != error::NoError) {
37✔
269
                        log::Error("Could not copy HTTP stream: " + err.String());
3✔
270
                        if (mode == BodyWriterErrorMode::Cancel) {
3✔
271
                                reader->Cancel();
2✔
272
                        }
273
                }
274
        });
111✔
275
}
276

277
ExpectedOutgoingResponsePtr IncomingRequest::MakeResponse() {
128✔
278
        if (*cancelled_) {
128✔
279
                return expected::unexpected(MakeError(
×
280
                        StreamCancelledError, "Cannot make response for a request that doesn't exist anymore"));
×
281
        }
282
        return stream_.server_.MakeResponse(shared_from_this());
256✔
283
}
284

285
IncomingResponse::IncomingResponse(Client &client, shared_ptr<bool> cancelled) :
×
286
        client_ {client},
287
        cancelled_ {cancelled} {
×
288
}
×
289

290
void IncomingResponse::Cancel() {
×
291
        if (!*cancelled_) {
×
292
                client_.Cancel();
×
293
        }
294
}
×
295

296
io::ExpectedAsyncReaderPtr IncomingResponse::MakeBodyAsyncReader() {
102✔
297
        if (*cancelled_) {
102✔
298
                return expected::unexpected(MakeError(
×
299
                        StreamCancelledError, "Cannot make reader for a response that doesn't exist anymore"));
×
300
        }
301
        return client_.MakeBodyAsyncReader(shared_from_this());
204✔
302
}
303

304
void IncomingResponse::SetBodyWriter(io::WriterPtr writer, BodyWriterErrorMode mode) {
45✔
305
        auto exp_reader = MakeBodyAsyncReader();
45✔
306
        if (!exp_reader) {
45✔
307
                if (exp_reader.error().code != MakeError(BodyMissingError, "").code) {
11✔
308
                        log::Error(exp_reader.error().String());
×
309
                }
310
                return;
11✔
311
        }
312
        auto &reader = exp_reader.value();
34✔
313

314
        io::AsyncCopy(writer, reader, [reader, mode](error::Error err) {
34✔
315
                if (err != error::NoError) {
34✔
316
                        log::Error("Could not copy HTTP stream: " + err.String());
5✔
317
                        if (mode == BodyWriterErrorMode::Cancel) {
5✔
318
                                reader->Cancel();
4✔
319
                        }
320
                }
321
        });
102✔
322
}
323

324
OutgoingResponse::~OutgoingResponse() {
×
325
        if (!*cancelled_) {
×
326
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
327
        }
328
}
×
329

330
void OutgoingResponse::Cancel() {
×
331
        if (!*cancelled_) {
×
332
                stream_.Cancel();
×
333
                stream_.server_.RemoveStream(stream_.shared_from_this());
×
334
        }
335
}
×
336

337
void OutgoingResponse::SetStatusCodeAndMessage(unsigned code, const string &message) {
125✔
338
        status_code_ = code;
125✔
339
        status_message_ = message;
125✔
340
}
125✔
341

342
void OutgoingResponse::SetHeader(const string &name, const string &value) {
112✔
343
        headers_[name] = value;
112✔
344
}
112✔
345

346
void OutgoingResponse::SetBodyReader(io::ReaderPtr body_reader) {
100✔
347
        async_body_reader_ = nullptr;
100✔
348
        body_reader_ = body_reader;
100✔
349
}
100✔
350

351
void OutgoingResponse::SetAsyncBodyReader(io::AsyncReaderPtr body_reader) {
4✔
352
        body_reader_ = nullptr;
4✔
353
        async_body_reader_ = body_reader;
4✔
354
}
4✔
355

356
error::Error OutgoingResponse::AsyncReply(ReplyFinishedHandler reply_finished_handler) {
126✔
357
        if (*cancelled_) {
126✔
358
                return MakeError(StreamCancelledError, "Cannot reply when response doesn't exist anymore");
1✔
359
        }
360
        return stream_.server_.AsyncReply(shared_from_this(), reply_finished_handler);
250✔
361
}
362

363
ExponentialBackoff::ExpectedInterval ExponentialBackoff::NextInterval() {
63✔
364
        iteration_++;
63✔
365

366
        if (try_count_ > 0 && iteration_ > try_count_) {
63✔
367
                return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
8✔
368
        }
369

370
        chrono::milliseconds current_interval = smallest_interval_;
59✔
371
        // Backoff algorithm: Each interval is returned three times, then it's doubled, and then
372
        // that is returned three times, and so on. But if interval is ever higher than the max
373
        // interval, then return the max interval instead, and once that is returned three times,
374
        // produce MaxRetryError. If try_count_ is set, then that controls the total number of
375
        // retries, but the rest is the same, so then it simply "gets stuck" at max interval for
376
        // many iterations.
377
        for (int count = 3; count < iteration_; count += 3) {
124✔
378
                auto new_interval = current_interval * 2;
70✔
379
                if (new_interval > max_interval_) {
70✔
380
                        new_interval = max_interval_;
17✔
381
                }
382
                if (try_count_ <= 0 && new_interval == current_interval) {
70✔
383
                        return expected::unexpected(MakeError(MaxRetryError, "Exponential backoff"));
10✔
384
                }
385
                current_interval = new_interval;
65✔
386
        }
387

388
        return current_interval;
54✔
389
}
390

391
} // namespace http
392
} // 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