1 // Part of Measurement Kit <https://measurement-kit.github.io/>.
2 // Measurement Kit is free software under the BSD license. See AUTHORS
3 // and LICENSE for more information on the copying conditions.
4 #ifndef MEASUREMENT_KIT_NETTESTS_HPP
5 #define MEASUREMENT_KIT_NETTESTS_HPP
6 
7 /*-
8  *  __      __  _____ __________  _______  .___ _______    ________
9  * /  \    /  \/  _  \\______   \ \      \ |   |\      \  /  _____/
10  * \   \/\/   /  /_\  \|       _/ /   |   \|   |/   |   \/   \  ___
11  *  \        /    |    \    |   \/    |    \   /    |    \    \_\  \
12  *   \__/\  /\____|__  /____|_  /\____|__  /___\____|__  /\______  /
13  *        \/         \/       \/         \/            \/        \/
14  *
15  * Autogenerated by `./script/autoapi/autoapi`. DO NOT EDIT!!!
16  */
17 
18 // Inline reimplementation of Measurement Kit's original API in terms
19 // of the new <measurement_kit/ffi.h> API.
20 //
21 // The most striking, major difference between this implementation and the
22 // previous implementation is the following. In the previous implementation
23 // tests were executed in FIFO order. In the new implementation, instead,
24 // they are still queued but the order of execution is random. This is fine
25 // since apps should actively discourage people from running parallel tests,
26 // using proper UX, as that is bad. The rough queuing mechanism that we
27 // have here is just the last line of defence against that behavior.
28 
29 #include <stdint.h>
30 
31 #include <functional>
32 #include <memory>
33 #include <string>
34 #include <thread>
35 #include <type_traits>
36 #include <vector>
37 
38 #include <measurement_kit/common/data_usage.hpp>
39 #include <measurement_kit/common/logger.hpp>
40 #include <measurement_kit/common/nlohmann/json.hpp>
41 #include <measurement_kit/common/shared_ptr.hpp>
42 
43 #include <measurement_kit/ffi.h>
44 
45 #if __cplusplus >= 201402L && !defined MK_NETTESTS_INTERNAL
46 #define MK_NETTESTS_DEPRECATED [[deprecated]]
47 #else
48 #define MK_NETTESTS_DEPRECATED /* Nothing */
49 #endif
50 
51 namespace mk {
52 namespace nettests {
53 
54 class MK_NETTESTS_DEPRECATED BaseTest {
55   public:
56     // Implementation notes
57     // --------------------
58     //
59     // 1. the code in here should work with both nlohmann/json v2 and v3 as
60     // long as we do not catch any exception. In fact, v2 used to throw standard
61     // exceptions (i.e. `std::exception`) while v3 has its own exceptions;
62     //
63     // 2. as a result, we're going to assume that the JSON objects passed to us
64     // by the FFI API of MK are always containing the fields we expect;
65     //
66     // 3. compared to the original implementation, this API allows to have
67     // multiple callbacks registered for any kind of event. In the original
68     // code, only _some_ events accepted multiple callbacks.
69 
70     class Details {
71       public:
72         nlohmann::json settings;
73         std::vector<std::function<void()>> logger_eof_cbs;
74         std::vector<std::function<void(uint32_t, const char *)>> log_cbs;
75         std::vector<std::function<void(const char *)>> event_cbs;
76         std::vector<std::function<void(double, const char *)>> progress_cbs;
77         uint32_t log_level = MK_LOG_WARNING;
78         std::vector<std::function<void(std::string)>> entry_cbs;
79         std::vector<std::function<void()>> begin_cbs;
80         std::vector<std::function<void()>> end_cbs;
81         std::vector<std::function<void()>> destroy_cbs;
82         std::vector<std::function<void(DataUsage)>> overall_data_usage_cbs;
83     };
84 
BaseTest()85     BaseTest() { impl_.reset(new Details); }
86 
87     // The original implementation had a virtual destructor but no other
88     // virtual members. Hence in the reimplementation I am removing the
89     // attribute `virtual` since it seems unnecessary.
~BaseTest()90     ~BaseTest() {}
91 
92     // Setters
93     // -------
94     //
95     // All the following methods are straightforward because they just
96     // configure the settings or register specific callbacks.
97 
add_input(std::string s)98     BaseTest &add_input(std::string s) {
99         impl_->settings["inputs"].push_back(std::move(s));
100         return *this;
101     }
102 
set_input_filepath(std::string s)103     BaseTest &set_input_filepath(std::string s) {
104         impl_->settings["input_filepaths"].clear();
105         return add_input_filepath(std::move(s));
106     }
107 
add_input_filepath(std::string s)108     BaseTest &add_input_filepath(std::string s) {
109         impl_->settings["input_filepaths"].push_back(std::move(s));
110         return *this;
111     }
112 
set_output_filepath(std::string s)113     BaseTest &set_output_filepath(std::string s) {
114         impl_->settings["output_filepath"] = std::move(s);
115         return *this;
116     }
117 
set_error_filepath(std::string s)118     BaseTest &set_error_filepath(std::string s) {
119         impl_->settings["log_filepath"] = std::move(s);
120         return *this;
121     }
122 
on_logger_eof(std::function<void ()> && fn)123     BaseTest &on_logger_eof(std::function<void()> &&fn) {
124         impl_->logger_eof_cbs.push_back(std::move(fn));
125         return *this;
126     }
127 
on_log(std::function<void (uint32_t,const char *)> && fn)128     BaseTest &on_log(std::function<void(uint32_t, const char *)> &&fn) {
129         impl_->log_cbs.push_back(std::move(fn));
130         return *this;
131     }
132 
on_event(std::function<void (const char *)> && fn)133     BaseTest &on_event(std::function<void(const char *)> &&fn) {
134         impl_->event_cbs.push_back(std::move(fn));
135         return *this;
136     }
137 
on_progress(std::function<void (double,const char *)> && fn)138     BaseTest &on_progress(std::function<void(double, const char *)> &&fn) {
139         impl_->progress_cbs.push_back(std::move(fn));
140         return *this;
141     }
142 
set_verbosity(uint32_t level)143     BaseTest &set_verbosity(uint32_t level) {
144         impl_->log_level = level;
145         return *this;
146     }
147 
increase_verbosity()148     BaseTest &increase_verbosity() {
149         if (impl_->log_level < MK_LOG_DEBUG2) {
150             ++impl_->log_level;
151         }
152         return *this;
153     }
154 
155     template <typename T, typename = typename std::enable_if<
156                                   std::is_arithmetic<T>::value>::type>
set_option(std::string key,T value)157     BaseTest &set_option(std::string key, T value) {
158         impl_->settings["options"][key] = value;
159         return *this;
160     }
161 
set_option(std::string key,std::string value)162     BaseTest &set_option(std::string key, std::string value) {
163         impl_->settings["options"][key] = value;
164         return *this;
165     }
166 
add_annotation(std::string key,std::string value)167     BaseTest &add_annotation(std::string key, std::string value) {
168         impl_->settings["annotations"][key] = value;
169         return *this;
170     }
171 
on_entry(std::function<void (std::string)> && fn)172     BaseTest &on_entry(std::function<void(std::string)> &&fn) {
173         impl_->entry_cbs.push_back(std::move(fn));
174         return *this;
175     }
176 
on_begin(std::function<void ()> && fn)177     BaseTest &on_begin(std::function<void()> &&fn) {
178         impl_->begin_cbs.push_back(std::move(fn));
179         return *this;
180     }
181 
on_end(std::function<void ()> && fn)182     BaseTest &on_end(std::function<void()> &&fn) {
183         impl_->end_cbs.push_back(std::move(fn));
184         return *this;
185     }
186 
on_destroy(std::function<void ()> && fn)187     BaseTest &on_destroy(std::function<void()> &&fn) {
188         impl_->destroy_cbs.push_back(std::move(fn));
189         return *this;
190     }
191 
on_overall_data_usage(std::function<void (DataUsage)> && fn)192     BaseTest &on_overall_data_usage(std::function<void(DataUsage)> &&fn) {
193         impl_->overall_data_usage_cbs.push_back(std::move(fn));
194         return *this;
195     }
196 
197     // run() & start()
198     // ---------------
199     //
200     // We should not be able to run more than a test with this class
201     // hence we immediately swap the context. Because we're using a
202     // SharedPtr, this means that attempting to run more than one test
203     // leads to an exception being thrown.
204 
run()205     void run() { run_static(std::move(impl_)); }
206 
start(std::function<void ()> && fn)207     void start(std::function<void()> &&fn) {
208         std::thread thread{[tip = std::move(impl_), fn = std::move(fn)]() {
209             run_static(std::move(tip));
210             fn();
211         }};
212         thread.detach();
213     }
214 
215   private:
216     // Task running
217     // ------------
218     //
219     // How we actually start a task and process its events.
220 
221     // Helper macro used to facilitate suppressing exceptions since the
222     // nettests.hpp API always suppresses exceptions in callbacks. This is
223     // consistent with the original implementation's behavior.
224     //
225     // This is not necessarily a very good idea, but the original code was
226     // doing that, hence we should do that here as well.
227 #define MK_NETTESTS_CALL_AND_SUPPRESS(func, args)                              \
228     do {                                                                       \
229         try {                                                                  \
230             func args;                                                         \
231         } catch (...) {                                                        \
232             /* SUPPRESS */                                                     \
233         }                                                                      \
234     } while (0)
235 
run_static(SharedPtr<Details> tip)236     static void run_static(SharedPtr<Details> tip) {
237         switch (tip->log_level) {
238         case MK_LOG_ERR:
239             tip->settings["log_level"] = "ERR";
240             break;
241         case MK_LOG_WARNING:
242             tip->settings["log_level"] = "WARNING";
243             break;
244         case MK_LOG_INFO:
245             tip->settings["log_level"] = "INFO";
246             break;
247         case MK_LOG_DEBUG:
248             tip->settings["log_level"] = "DEBUG";
249             break;
250         case MK_LOG_DEBUG2:
251             tip->settings["log_level"] = "DEBUG2";
252             break;
253         default:
254             assert(false); // Should not happen
255             break;
256         }
257         // Serializing the settings MAY throw if we provided strings
258         // that are non-JSON serializeable. For now, let the exception
259         // propagate if that unexpected condition occurs.
260         //
261         // TODO(bassosimone): since this error condition did not happen
262         // with the previous iteration of this API, it's an open question
263         // whether to handle this possible error condition or not.
264         std::string serialized_settings;
265         serialized_settings = tip->settings.dump();
266         mk_unique_task tup{mk_task_start(serialized_settings.c_str())};
267         if (!tup) {
268             throw std::runtime_error("mk_task_start() failed");
269         }
270         while (!mk_task_is_done(tup.get())) {
271             nlohmann::json ev;
272             {
273                 mk_unique_event eup{mk_task_wait_for_next_event(tup.get())};
274                 if (!eup) {
275                     throw std::runtime_error(
276                             "mk_task_wait_for_next_event() failed");
277                 }
278                 const char *s = mk_event_serialization(eup.get());
279                 assert(s != nullptr);
280 #ifdef MK_NETTESTS_TRACE_EVENTS
281                 std::clog << "mk::nettests: got event: " << s << std::endl;
282 #endif
283                 // The following statement MAY throw. Since we do not expect
284                 // MK to serialize a non-parseable JSON, just let the eventual
285                 // exception propagate and terminate the program.
286                 ev = nlohmann::json::parse(s);
287             }
288             process_event(tip, std::move(ev));
289         }
290         for (auto &cb : tip->logger_eof_cbs) {
291             MK_NETTESTS_CALL_AND_SUPPRESS(cb, ());
292         }
293         for (auto &cb : tip->destroy_cbs) {
294             MK_NETTESTS_CALL_AND_SUPPRESS(cb, ());
295         }
296     }
297 
298     // Events processing
299     // -----------------
300     //
301     // Map events emitted by the FFI API to nettests.hpp callbacks. This is the
302     // section where most of the complexity is.
303 
process_event(const SharedPtr<Details> & tip,nlohmann::json ev)304     static void process_event(
305             const SharedPtr<Details> &tip, nlohmann::json ev) {
306         // Implementation notes:
307         //
308         // 1) as mentioned above, in processing events we're quite strict in the
309         // sense that we _assume_ events to have a specific structure and fail
310         // with an unhandled exception otherwise;
311         //
312         // 2) the nettests API is less rich that the FFI API; as such, there
313         // are several FFI events that are going to be ignored.
314         std::string key = ev.at("key");
315         // TODO(bassosimone): make sure events names are OK.
316         if (key == "failure.measurement") {
317             // NOTHING
318         } else if (key == "failure.measurement_submission") {
319             // NOTHING
320         } else if (key == "failure.startup") {
321             // NOTHING
322         } else if (key == "log") {
323             std::string log_level = ev.at("value").at("log_level");
324             std::string message = ev.at("value").at("message");
325             uint32_t verbosity = MK_LOG_QUIET;
326             if (log_level == "ERR") {
327                 verbosity = MK_LOG_ERR;
328             } else if (log_level == "WARNING") {
329                 verbosity = MK_LOG_WARNING;
330             } else if (log_level == "INFO") {
331                 verbosity = MK_LOG_INFO;
332             } else if (log_level == "DEBUG") {
333                 verbosity = MK_LOG_DEBUG;
334             } else if (log_level == "DEBUG2") {
335                 verbosity = MK_LOG_DEBUG2;
336             } else {
337                 assert(false);
338                 return;
339             }
340             for (auto &cb : tip->log_cbs) {
341                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, (verbosity, message.c_str()));
342             }
343         } else if (key == "measurement") {
344             std::string json_str = ev.at("value").at("json_str");
345             for (auto &cb : tip->entry_cbs) {
346                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, (json_str));
347             }
348         } else if (key == "status.end") {
349             double downloaded_kb = ev.at("value").at("downloaded_kb");
350             double uploaded_kb = ev.at("value").at("uploaded_kb");
351             DataUsage du;
352             // There are cases where the following could overflow but, again, we
353             // do not want to break the existing API.
354             du.down = (uint64_t)(downloaded_kb * 1000.0);
355             du.up = (uint64_t)(uploaded_kb * 1000.0);
356             for (auto &cb : tip->overall_data_usage_cbs) {
357                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, (du));
358             }
359             for (auto &cb : tip->end_cbs) {
360                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, ());
361             }
362         } else if (key == "status.geoip_lookup") {
363             // NOTHING
364         } else if (key == "status.progress") {
365             double percentage = ev.at("value").at("percentage");
366             std::string message = ev.at("value").at("message");
367             for (auto &cb : tip->progress_cbs) {
368                 MK_NETTESTS_CALL_AND_SUPPRESS(
369                         cb, (percentage, message.c_str()));
370             }
371         } else if (key == "status.queued") {
372             // NOTHING
373         } else if (key == "status.measurement_started") {
374             // NOTHING
375         } else if (key == "status.measurement_uploaded") {
376             // NOTHING
377         } else if (key == "status.measurement_done") {
378             // NOTHING
379         } else if (key == "status.report_created") {
380             // NOTHING
381         } else if (key == "status.started") {
382             for (auto &cb : tip->begin_cbs) {
383                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, ());
384             }
385         } else if (key == "status.update.performance") {
386             std::string direction = ev.at("value").at("direction");
387             double elapsed = ev.at("value").at("elapsed");
388             int64_t num_streams = ev.at("value").at("num_streams");
389             double speed_kbps = ev.at("value").at("speed_kbps");
390             nlohmann::json doc;
391             doc["type"] = direction + "-speed";
392             doc["elapsed"] = {elapsed, "s"};
393             doc["num_streams"] = num_streams;
394             doc["speed"] = {speed_kbps, "kbit/s"};
395             // Serializing may throw but we expect MK to pass us a good
396             // JSON so don't consider this possible error condition.
397             auto s = doc.dump();
398             for (auto &cb : tip->event_cbs) {
399                 MK_NETTESTS_CALL_AND_SUPPRESS(cb, (s.c_str()));
400             }
401         } else if (key == "status.update.websites") {
402             // NOTHING
403         } else if (key == "task_terminated") {
404             // NOTHING
405         } else {
406 #ifdef MK_NETTESTS_TRACE_EVENTS
407             std::clog << "WARNING: mk::nettests: unhandled event: " << key
408                       << std::endl;
409 #endif
410         }
411     }
412 
413 #undef MK_NETTESTS_CALL_AND_SUPPRESS // Tidy up
414 
415   protected:
416     // Implementation note: using a SharedPtr<T> because it's easy to
417     // move around (especially into lambdas) and because it provides the
418     // guarantee of throwing on null, which was a trait of the previous
419     // implementation of the nettests API.
420     //
421     // Also: the pointer was public in the previous implementation but
422     // it was also opaque, so not very useful. For this reason, it's
423     // now protected in this implementation.
424     SharedPtr<Details> impl_;
425 };
426 
427 class CaptivePortalTest : public BaseTest {
428   public:
CaptivePortalTest()429     CaptivePortalTest() : BaseTest() {
430         impl_->settings["name"] = "CaptivePortal";
431     }
432 };
433 
434 class DashTest : public BaseTest {
435   public:
DashTest()436     DashTest() : BaseTest() {
437         impl_->settings["name"] = "Dash";
438     }
439 };
440 
441 class DnsInjectionTest : public BaseTest {
442   public:
DnsInjectionTest()443     DnsInjectionTest() : BaseTest() {
444         impl_->settings["name"] = "DnsInjection";
445     }
446 };
447 
448 class FacebookMessengerTest : public BaseTest {
449   public:
FacebookMessengerTest()450     FacebookMessengerTest() : BaseTest() {
451         impl_->settings["name"] = "FacebookMessenger";
452     }
453 };
454 
455 class HttpHeaderFieldManipulationTest : public BaseTest {
456   public:
HttpHeaderFieldManipulationTest()457     HttpHeaderFieldManipulationTest() : BaseTest() {
458         impl_->settings["name"] = "HttpHeaderFieldManipulation";
459     }
460 };
461 
462 class HttpInvalidRequestLineTest : public BaseTest {
463   public:
HttpInvalidRequestLineTest()464     HttpInvalidRequestLineTest() : BaseTest() {
465         impl_->settings["name"] = "HttpInvalidRequestLine";
466     }
467 };
468 
469 class MeekFrontedRequestsTest : public BaseTest {
470   public:
MeekFrontedRequestsTest()471     MeekFrontedRequestsTest() : BaseTest() {
472         impl_->settings["name"] = "MeekFrontedRequests";
473     }
474 };
475 
476 class NdtTest : public BaseTest {
477   public:
NdtTest()478     NdtTest() : BaseTest() {
479         impl_->settings["name"] = "Ndt";
480     }
481 };
482 
483 class TcpConnectTest : public BaseTest {
484   public:
TcpConnectTest()485     TcpConnectTest() : BaseTest() {
486         impl_->settings["name"] = "TcpConnect";
487     }
488 };
489 
490 class TelegramTest : public BaseTest {
491   public:
TelegramTest()492     TelegramTest() : BaseTest() {
493         impl_->settings["name"] = "Telegram";
494     }
495 };
496 
497 class WebConnectivityTest : public BaseTest {
498   public:
WebConnectivityTest()499     WebConnectivityTest() : BaseTest() {
500         impl_->settings["name"] = "WebConnectivity";
501     }
502 };
503 
504 class WhatsappTest : public BaseTest {
505   public:
WhatsappTest()506     WhatsappTest() : BaseTest() {
507         impl_->settings["name"] = "Whatsapp";
508     }
509 };
510 
511 } // namespace nettests
512 } // namespace mk
513 #endif
514