// Copyright (c) 2018 The Swedish Internet Foundation // Written by Göran Andersson #include #include #include #include #include #include #include #include "speedtest.h" #include "defs.h" #include "singlerequesttask.h" #include "../http/singlerequest.h" #include "pingsweeptask.h" #include "measurementagent.h" MeasurementAgent::MeasurementAgent(const TaskConfig &config, const HttpHost &webserver) : Task("MeasurementAgent"), wserv(webserver) { killChildTaskWhenFinished(); key_store = wserv.cmgr ? wserv.cmgr : new CookieManager(); // Default values; could be changed by the client app (gui): report_template["appname"] = measurement::appName; report_template["appver"] = measurement::appVersion; report_template["dlength"] = "10"; report_template["ulength"] = "10"; options_filename = config.value("options_file"); if (!options_filename.empty()) { // Load preexisting configuration options cfgOptions = cfgOptions.loadJsonFromFile(options_filename); //if (cfgOptions.hasKey("Client.hashkey")) // handleConfigurationOption("Client.hashkey", // cfgOptions.value("Client.hashkey")); } for (auto p : config.cfg()) handleConfigurationOption(p.first, p.second); } void MeasurementAgent::taskMessage(Task *task) { // std::string name = task->label(), message = task->message(); if (PingSweepTask *ptask = dynamic_cast(task)) { while (true) { std::string res = ptask->getResult(); if (res.empty()) break; sendToClient("setInfo", MeasurementTask:: json_obj("approxLatency", res)); } } } void MeasurementAgent::taskFinished(Task *task) { std::string name = task->label(), result = task->result(); if (task->wasKilled()) log() << "Task " << name << " killed"; else log() << "Task " << name << " finished, result: " << result; if (task == bridge) { bridge = nullptr; state = MeasurementState::ABORTED; setResult(""); return; } if (task == current_test) { current_test = nullptr; state = MeasurementState::FINISHED; sendTaskComplete("global"); return; } if (task->wasKilled() || !bridge || state == MeasurementState::ABORTED) return; if (name == "msettings") { // Check that the result is valid JSON: std::string err; auto obj = json11::Json::parse(result, err); if (result.empty() || !err.empty()) { settings_result = getDefaultConfig(); } else { if (result[result.size()-1] == '}') { std::string newkey = obj["hashkey"].string_value(); std::string hashkey = !force_key.empty() ? force_key : key_store->getCookieVal("hash_key", wserv.hostname); if (newkey == hashkey) { // Nothing changed. } else if (isValidHashkey(hashkey)) { // We already had a key. Try to insert it into settings. if (newkey.empty()) { result.resize(result.size()-1); ((result += ",\"hashkey\":\"") += hashkey) += "\"}"; } else { auto pos = result.find('"' + newkey + '"'); if (pos != std::string::npos) result.replace(pos+1, newkey.size(), hashkey); } } else if (isValidHashkey(newkey)) { // We had no valid key. Save the one we got. std::string domain = wserv.hostname; if (domain.size() >= 18 && domain.substr(domain.size() - 18) == "bredbandskollen.se") domain = ".bredbandskollen.se"; std::string line = "hash_key=" + newkey + "; max-age=999999999; domain=" + domain; key_store->setCookie(line, wserv.hostname, "/"); log() << "SET COOKIE " << line; } } settings_result = result; if (!obj["x_bbk"].string_value().empty()) report_template["x_bbk"] = obj["x_bbk"].string_value(); } sendToClient("configuration", settings_result); key_store->save(); } else if (name == "contents") { sendToClient("setInfo", "{\"contents\": " + result + "}"); } else if (name == "pingsweep") { log() << "Best server: " << result; sendToClient("setInfo", "{\"bestServer\": \"" + result + "\"}"); } else if (name == "list_measurements") { sendToClient("measurementList", result); } else if (name == "checkHost") { if (HttpClientTask *t = dynamic_cast(task)) { sendToClient("hostCheck", MeasurementTask:: json_obj(t->serverHost(), result.empty() ? "" : "1")); log() << "checkHost: " << result; } } else if (name == "measurementStart") { std::string err; auto obj = json11::Json::parse(result, err); if (err.empty()) { sendToClient("setInfo", result); log() << "measurementStart: " << result; } else { sendToClient("setInfo", "{}"); warn_log() << "invalid measurementStart: " << result; } } else if (name == "setSubscription") { log() << "setSubscription done"; } else if (name == "sendLogToServer") { std::string err; auto obj = json11::Json::parse(result, err); std::string res = "NOK"; if (err.empty() && obj["status"].number_value() != 0) res = "OK"; sendToClient("setInfo", "{\"logSent\": \"" + res + "\"}"); } else { log() << "unknown task, ignoring"; } } void MeasurementAgent::pollBridge(const std::string &msg) { if (msg.empty()) return; std::string err; auto obj = json11::Json::parse(msg, err); if (!err.empty()) { err_log() << "JSON error, ignoring message: " << msg; return; } std::string method = obj["method"].string_value(); const json11::Json &args = obj["args"]; handleMsgFromClient(method, args); } void MeasurementAgent::sendTaskComplete(const std::string &t, const std::string &res) { if (!bridge) return; std::string args = "{\"task\": \"" + t + "\""; if (!res.empty()) args += ", \"result\": " + res; sendToClient("taskComplete", args + "}"); } bool MeasurementAgent::isValidHashkey(const std::string &key) { if (key.size() >= 12 && key.size() <= 40 && std::string::npos == key.find_first_not_of("0123456789ABCDEFabcdef-")) return true; return false; } void MeasurementAgent::sendLogToServer() { std::string url = "/api/devlog"; addNewTask(new SingleRequest("sendLogToServer", "frontend-beta.bredbandskollen.se", url, accumulated_log.str(), 10.0), this); setLogLimit(); log() << "Sent " << accumulated_log.str().size() << " bytes."; accumulated_log.str(""); } std::string MeasurementAgent::getDefaultConfig() { // Either the client doesn't want settings to be fetched // (wserv.hostname is "none"), or we failed to fetch settings. // If the client has supplied a measurement server, or if the domain is // bredbandskollen.se, we can create a valid config. std::string mserver4 = mserv.hostname, mserver6 = mserv.hostname, domain = wserv.hostname; if (domain == "none") { domain = mserv.hostname; } else if (domain.size() >= 18 && domain.substr(domain.size() - 18) == "bredbandskollen.se" && mserver4.empty()) { // Perhaps a client dns failure. mserver4 = "192.36.30.2"; mserver6 = "2001:67c:2ff4::2"; } std::string hashkey = !force_key.empty() ? force_key : key_store->getCookieVal("hash_key", domain); if (hashkey.empty()) { hashkey = createHashKey(); key_store->setCookie("hash_key="+hashkey+"; max-age=999999999", domain, "/"); } if (mserver4.empty()) { json11::Json settings = json11::Json::object { { "ispname", "" }, { "hashkey", hashkey } }; return settings.dump(); } json11::Json settings = json11::Json::object { { "servers", json11::Json::array { json11::Json::object { { "url", mserver4 }, { "name", mserver4 }, { "type", "ipv4" } }, json11::Json::object { { "url", mserver6 }, { "name", mserver6 }, { "type", "ipv6" } } } }, { "ispname", "" }, { "hashkey", hashkey } }; return settings.dump(); } void MeasurementAgent::handleMsgFromClient(const std::string &method, const json11::Json &args) { log() << "handleMsgFromClient " << method; if (method == "clientReady") { std::string savedConfig; savedConfig = json11::Json(cfgOptions).dump(); sendToClient("agentReady", savedConfig); } else if (method == "getConfiguration") { dbg_log() << "Webserver: " << wserv.hostname; if (wserv.hostname == "none") { settings_result = getDefaultConfig(); sendToClient("configuration", settings_result); } else { std::map attrs; std::string hashkey = !force_key.empty() ? force_key : key_store->getCookieVal("hash_key", wserv.hostname); #ifdef USE_GNUTLS if (mserv.is_tls) attrs["ssl"] = "1"; #endif if (isValidHashkey(hashkey)) attrs["key"] = hashkey; // attrs["nokey"] = ""; if (!args["consent"].string_value().empty()) attrs["consent"] = args["consent"].string_value(); std::string url = wserv_settingsurl; HttpClientConnection::addUrlPars(url, attrs); log() << wserv.hostname << ':' << wserv.port << " " << url; addNewTask(new SingleRequestTask(url, "msettings", "", wserv), this); } } else if (method == "getContent") { std::map attrs; for (auto p : args.object_items()) if (!p.second.string_value().empty()) attrs[p.first] = p.second.string_value(); std::string url = wserv_contentsurl; HttpClientConnection::addUrlPars(url, attrs); addNewTask(new SingleRequestTask(url, "contents", "", wserv), this); } else if (method == "setConfigurationOption") { for (auto p : args.object_items()) handleConfigurationOption(p.first, p.second.string_value()); } else if (method =="saveConfigurationOption") { for (auto p : args.object_items()) { auto key = p.first, value = p.second.string_value(); if (key == "Client.hashkey") { std::string url = wserv_settingsurl; if (value.empty()) { key_store->eraseCookies(wserv.hostname); key_store->eraseCookies("bredbandskollen.se"); key_store->save(); addNewTask(new SingleRequestTask(url, "msettings", "", wserv), this); //value = createHashKey(); } else if (isValidHashkey(value)) { key_store->setCookie("hash_key="+value+"; max-age=999999999" "; path=/; domain=.bredbandskollen.se", "bredbandskollen.se", "/"); if (key_store->save()) err_log() << "cannot save cookies"; else cfgOptions.erase(key); std::map attrs; attrs["key"] = value; HttpClientConnection::addUrlPars(url, attrs); addNewTask(new SingleRequestTask(url, "msettings", "", wserv), this); } else { err_log() << "invalid Client.hashkey: " << value; sendToClient("setInfo", MeasurementTask:: json_obj("keyRejected", value)); } //handleConfigurationOption(key, value); } if (value.empty()) { cfgOptions.erase(key); } else { cfgOptions.set(key, value); } } if (!options_filename.empty()) { if (!cfgOptions.saveJsonToFile(options_filename)) sendToClient("setInfo", MeasurementTask::json_obj("error", "cannot save configuration options")); } } else if (method == "saveReport") { if (current_test) current_test->doSaveReport(args); } else if (method == "checkHost") { std::string hostname = args["server"].string_value(); HttpHost tmpServer(wserv); tmpServer.hostname = hostname; addNewTask(new SingleRequestTask("/pingsweep/check", "checkHost", "", tmpServer), this); } else if (method == "startTest") { log() << "Args: " << args.dump(); if (state == MeasurementState::IDLE) { std::string mserver = args["serverUrl"].string_value(); std::string key = args["userKey"].string_value(); #ifdef USE_GNUTLS mserv.is_tls = static_cast(args["tls"].int_value()); mserv.port = mserv.is_tls ? 443 : 80; #else mserv.port = 80; #endif if (args.object_items().find("serverPort") != args.object_items().end()) { int p = args["serverPort"].int_value(); if (p && p > 0 && p < 65536) { mserv.port = static_cast(p); log() << "measurement server port number " << p; } else err_log() << "invalid port number, will use default"; } if (mserver.empty() || !isValidHashkey(key)) { json11::Json obj = json11::Json::object { { "error", "startTest requires parameters serverUrl and userKey" }, { "errno", "X01" } }; sendToClient("setInfo", obj.dump()); state = MeasurementState::FINISHED; sendTaskComplete("global"); return; } log() << "got measurement server " << mserver; mserv.hostname = mserver; report_template["host"] = wserv.hostname; report_template["key"] = key; current_test = new SpeedTest(this, mserv, report_template); state = MeasurementState::STARTED; addNewTask(current_test, this); } else { err_log() << "Must do resetTest before starting a new test"; } } else if (method == "abortTest") { if (state == MeasurementState::STARTED) { if (current_test) { state = MeasurementState::ABORTED; abortTask(current_test); } else { state = MeasurementState::FINISHED; } } else { err_log("got abortTest when not in measurement"); } } else if (method == "resetTest") { switch (state) { case MeasurementState::FINISHED: state = MeasurementState::IDLE; sendToClient("agentReady"); mserv.hostname.clear(); current_ticket.clear(); break; case MeasurementState::IDLE: break; case MeasurementState::ABORTED: case MeasurementState::STARTED: err_log("got resetTest during measurement"); json11::Json obj = json11::Json::object { { "error", "got resetTest during measurement" }, { "errno", "X02" } }; sendToClient("setInfo", obj.dump()); break; } } else if (method == "setSubscription") { if (current_ticket.empty()) { err_log() << "cannot set subscription: latest measurement missing"; return; } std::string id = args["speedId"].string_value(); if (id.empty()) { long n = args["speedId"].int_value(); if (n <= 0) { err_log() << "cannot set subscription: no speedId"; return; } id = std::to_string(n); } std::string url = "/setSubscription?t=" + current_ticket + "&key=" + report_template["key"] + "&speed=" + id; addNewTask(new SingleRequestTask(url, "setSubscription", current_ticket, mserv), this); } else if (method == "listMeasurements") { std::map uargs; uargs["key"] = !force_key.empty() ? force_key : key_store->getCookieVal("hash_key", wserv.hostname); for (auto p : args.object_items()) uargs[p.first] = p.second.string_value(); if (isValidHashkey(uargs["key"])) { std::string url = wserv_measurementsurl; HttpClientConnection::addUrlPars(url, uargs); log() << "Get URL: " << url; addNewTask(new SingleRequestTask(url, "list_measurements", "lmtask", wserv), this); } else { sendToClient("measurementList", "{\"measurements\":[]}"); } } else if (method == "pingSweep") { addNewTask(new PingSweepTask(settings_result, wserv), this); } else if (method == "accumulateLog") { accumulateLog(); } else if (method == "appendLog") { std::string l = args["logText"].string_value(); if (!l.empty()) appendLog(l); } else if (method == "sendLogToServer") { sendLogToServer(); } else if (method == "terminate") { bridge = nullptr; setResult(""); } else if (method == "quit") { bridge = nullptr; setResult(""); } else { log() << "unknown command"; } } void MeasurementAgent::handleExecution(Task *sender, const std::string &msg) { if (sender == bridge) { pollBridge(msg); } else if (sender == current_test) { current_ticket = msg; dbg_log() << "current ticket: " << current_ticket; } else if (!bridge) { if (BridgeTask *bt = dynamic_cast(sender)) { log() << "Bridge connected"; bridge = bt; pollBridge(msg); } } } void MeasurementAgent::sendTaskProgress(const std::string &taskname, double speed, double progress) { // We can't use locale dependent number formating in JSON! std::ostringstream json; json.imbue(std::locale("C")); json << "{\"task\": \"" << taskname << "\", \"result\": " << speed << ", \"progress\": " << progress << "}"; sendToClient("taskProgress", json.str()); } void MeasurementAgent::handleConfigurationOption(const std::string &name, const std::string &value) { log() << "option: " << name << " value: " << value; if (name == "Logging.LogToConsole") { if (!value.empty()) { Logger::setLogFile(std::cerr); } } else if (name.substr(0, 7) == "Client.") { std::string attr = name.substr(7); if (!attr.empty() && attr.size() <= 20 && std::string::npos == attr.find_first_not_of("abcdefghijklmnopqrstuvwxyz")) { // TODO: check not reserved! if (attr == "hashkey") force_key = value; else report_template[attr] = value; } else { log() << "will ignore option " << name; } } else if (name.substr(0, 7) == "Report.") { std::string attr = name.substr(7); if (!attr.empty() && attr.size() <= 20 && std::string::npos == attr.find_first_not_of("abcdefghijklmnopqrstuvwxyz") && current_test) // TODO: check not reserved! current_test->addToReport(attr, value); else log() << "will ignore option " << name; } else if (name == "Measure.IpType") { wserv.iptype = (value == "ipv6") ? 6 : 4; std::string s = MeasurementTask::getLocalAddress(); if (!s.empty()) HttpClientTask::setLocalAddress(s, wserv.iptype); } else if (name == "Measure.Webserver") { wserv.hostname = value; } else if (name == "Measure.Server") { mserv.hostname = value; } else if (name == "Measure.LocalAddress") { if (!HttpClientTask::setLocalAddress(value, wserv.iptype)) setError("cannot use local address"); #ifdef USE_GNUTLS } else if (name == "Measure.TLS") { if (value == "1") { mserv.is_tls = true; wserv.is_tls = true; wserv.port = 443; } else { mserv.is_tls = false; wserv.is_tls = false; wserv.port = 80; } #endif } else if (name == "Measure.ProxyServerUrl") { wserv.proxyHost = value; mserv.proxyHost = value; } else if (name == "Measure.ProxyServerPort") { try { wserv.proxyPort = static_cast(stoi(value)); } catch (...) { wserv.proxyPort = 80; } mserv.proxyPort = wserv.proxyPort; } else if (name == "Measure.LoadDuration") { double duration = 10.0; try { duration = stod(value); } catch (...) { } if (duration < 2.0) duration = 2.0; else if (duration > 10.0) duration = 10.0; report_template["dlength"] = std::to_string(duration); report_template["ulength"] = std::to_string(duration); } else if (name == "Measure.SpeedLimit") { if (value.empty()) report_template.erase("speedlimit"); else report_template["speedlimit"] = value; } else if (name == "Measure.AutoSaveReport") { report_template["autosave"] = value; } else if (name == "Measure.SettingsUrl") { wserv_settingsurl = value; } else if (name == "Measure.ContentsUrl") { wserv_contentsurl = value; } else if (name == "Measure.MeasurementsUrl") { wserv_measurementsurl = value; } else if (name == "options_file") { // Ignore. } else { log() << name << ": unknown option"; } }