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