This commit is contained in:
fwastring 2025-10-13 10:38:50 +02:00
commit ab9a0bd4e2
183 changed files with 20701 additions and 0 deletions

237
src/measurement/defs.cpp Normal file
View file

@ -0,0 +1,237 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#ifdef __APPLE__
#include <sys/types.h>
#include <sys/sysctl.h>
#import <Foundation/Foundation.h>
#endif
#ifndef _WIN32
#include <unistd.h>
#else
#include <intrin.h>
#endif
#include <cstdio>
#include <memory>
#include <stdexcept>
#include <string>
#include <fstream>
#include "defs.h"
#include "../http/httpclientconnection.h"
namespace {
#ifdef __APPLE__
#elif defined(NO_EXTERNAL_CMD)
std::string get_shell_value(std::string filename, std::string key) {
std::ifstream file(filename);
if (file) {
key += "=";
std::string line;
while (std::getline(file, line)) {
if (line.find(key) == 0) {
std::string value = line.substr(key.size());
if (value.size() > 1 && value[0] == '"' &&
value[value.size() - 1] == '"') {
return value.substr(1, value.size() - 2);
}
return value;
}
}
}
return std::string();
}
std::string get_os_identifier() {
std::string os_name = get_shell_value("/etc/os-release", "NAME");
if (!os_name.empty()) {
std::string os_version = get_shell_value("/etc/os-release", "VERSION");
if (!os_version.empty()) {
os_name += " " + os_version;
}
}
return os_name;
}
std::string get_arch_identifier() {
#if defined(__aarch64__)
return "aarch64";
#elif defined(__arm__) || defined(_M_ARM)
return "arm";
#elif defined(__amd64__) || defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64)
return "x86_64";
#elif defined(__i386) || defined(_M_IX86) || defined(__X86__) || defined(_X86_)
return "i386";
#elif defined(mips) || defined(__mips__) || defined(__mips)
return "mips";
#elif defined(__PPC64__) || defined(__ppc64__) || defined(_ARCH_PPC64)
return "powerpc64"
#elif defined(__powerpc) || defined(__powerpc__) || defined(__powerpc64__) || defined(__POWERPC__) || defined(__ppc__) || defined(__PPC__) || defined(_ARCH_PPC)
return "powerpc";
#else
return std::string();
#endif
}
#else
std::string external_cmd(const char *cmd) {
char buffer[200];
std::string result;
#ifdef _WIN32
std::shared_ptr<FILE> pipe(_popen(cmd, "r"), _pclose);
#else
std::shared_ptr<FILE> pipe(popen(cmd, "r"), pclose);
#endif
if (pipe) {
while (!feof(pipe.get()))
if (fgets(buffer, sizeof buffer, pipe.get()) != nullptr)
result += buffer;
auto pos = result.find_last_not_of(" \t\r\n");
if (pos != std::string::npos)
result.resize(pos+1);
}
return result;
}
#endif
std::string app_name() {
std::string appName = "Bredbandskollen";
#ifdef _WIN32
appName += " Windows";
#elif defined(__ANDROID__)
appName += " Android";
#elif defined(__APPLE__)
appName += " Mac";
#elif defined(__FreeBSD__)
appName += " FreeBSD";
#elif defined(__NetBSD__)
appName += " NetBSD";
#elif defined(__OpenBSD__)
appName += " OpenBSD";
#elif defined(__bsdi__)
appName += " BSDi";
#elif defined(__DragonFly__)
appName += " DragonFly";
#else
appName += " Linux";
#endif
#ifdef __arm__
return appName + " ARM";
#elif defined(_M_ARM)
return appName + " ARM";
#elif defined(__aarch64__)
return appName + " ARM64";
#elif defined(__i386)
return appName + " i386";
#elif defined(_M_IX86)
return appName + " i386";
#elif defined(__X86__)
return appName + " i386";
#elif defined(_X86_)
return appName + " i386";
#elif defined(__amd64__)
return appName + " amd64";
#elif defined(__x86_64__)
return appName + " amd64";
#elif defined(_M_X64)
return appName + " amd64";
#elif defined(_M_AMD64)
return appName + " amd64";
#elif defined(__mips__)
return appName + " mips";
#elif defined(__mips)
return appName + " mips";
#else
return appName;
#endif
}
std::string getHWModel() {
#ifdef __APPLE__
size_t size;
char array[1000];
sysctlbyname("hw.model", NULL, &size, NULL, 0);
sysctlbyname("hw.model", &array, &size, NULL, 0);
return array;
#elif defined(_WIN32)
// Get extended ids.
int CPUInfo[4] = { -1 };
__cpuid(CPUInfo, 0x80000000);
unsigned int nExIds = CPUInfo[0];
// Get the information associated with each extended ID.
char CPUBrandString[0x40] = { 0 };
for (unsigned int i = 0x80000000; i <= nExIds; ++i) {
__cpuid(CPUInfo, i);
// Interpret CPU brand string and cache information.
if (i == 0x80000002)
memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo)); // "Intel(R) Core(TM)"
else if (i == 0x80000003)
memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo)); // "i7-4710HQ CPU"
else if (i == 0x80000004)
memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo)); // "@ 2.50GHz"
}
return CPUBrandString;
#elif defined(NO_EXTERNAL_CMD)
return get_arch_identifier();
#else
return external_cmd("uname -m").substr(0, 50);
#endif
}
std::string getOSInfo() {
std::string osInfo = "";
#ifdef _WIN32
osInfo = "Windows ";
NTSTATUS(WINAPI *RtlGetVersion)(LPOSVERSIONINFOEXW);
OSVERSIONINFOEXW osVerInfo;
*(FARPROC*)&RtlGetVersion = GetProcAddress(GetModuleHandleA("ntdll"), "RtlGetVersion");
if (NULL != RtlGetVersion) {
osVerInfo.dwOSVersionInfoSize = sizeof(osVerInfo);
RtlGetVersion(&osVerInfo);
osInfo += std::to_string(osVerInfo.dwMajorVersion) + "." + std::to_string(osVerInfo.dwMinorVersion);
if (osVerInfo.szCSDVersion[0] != 0) {
osInfo += " ";
int i = 0;
while (osVerInfo.szCSDVersion[i] != 0) {
osInfo += osVerInfo.szCSDVersion[i];
i++;
}
}
if (osVerInfo.wSuiteMask | VER_SUITE_PERSONAL) {
osInfo += " Home";
}
osInfo += " (Build " + std::to_string(osVerInfo.dwBuildNumber) + ")";
}
else {
osInfo += "unknown";
}
#elif defined(__APPLE__)
osInfo = [[[NSProcessInfo processInfo] operatingSystemVersionString] UTF8String];
#elif defined(NO_EXTERNAL_CMD)
osInfo = get_os_identifier();
#else
osInfo = external_cmd("uname -sr").substr(0, 50);
#endif
return osInfo;
}
}
const std::string measurement::appName = app_name();
#if defined(__ANDROID__)
const std::string measurement::hw_info = "Android Device";
const std::string measurement::os_version = "Android OS";
#else
const std::string measurement::hw_info = getHWModel();
const std::string measurement::os_version = getOSInfo();
#endif

12
src/measurement/defs.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <string>
namespace measurement {
extern const std::string appName;
const std::string appVersion = "1.2.2";
// buildVersion 15 released to App Store in 2016.
const unsigned int buildVersion = 16;
extern const std::string hw_info;
extern const std::string os_version;
}

View file

@ -0,0 +1,68 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "downloadtask.h"
DownloadTask::DownloadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn, unsigned int max_conn,
double duration, double max_time, double tick_s) :
ProgressTask("download", ticket, server, no_conn, max_conn, duration, max_time),
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
dynamic_conn_limit = 0.5 * duration + 0.5;
}
double DownloadTask::start() {
log() << "DownloadTask starting";
notifyStarted();
checkConnectionCount();
return tick_duration_s;
}
double DownloadTask::timerEvent() {
double time = elapsed();
if (time >= timeout_s()) {
log() << "DownloadTask timeout after " << time << " seconds";
setResult("-1");
return 0;
}
if (time > 0.05) {
// log() << "Check: " << byteCount() << " and " << threadRecvCount();
double speed = addOverheadMbps(threadRecvCount(), time);
if (time < dynamic_conn_limit && speed >= 50.0) {
unsigned int no_conn = (speed < 250.0) ?
((speed < 100) ? 12 : 24) :
((speed < 500) ? 32 : 48);
if (no_conn < currentNoConnections())
no_conn = currentNoConnections();
doTestProgress(speed, time, no_conn);
setNoConnections(no_conn);
} else {
doTestProgress(speed, time, currentNoConnections());
}
}
return tick_duration_s;
}
void DownloadTask::newRequest(HttpClientConnection *conn) {
if (size_t size = loadSize()) {
conn->get("/bigfile.bin?t=" + t() + "&len=" + std::to_string(size));
} else if (soonFinished()) {
// Delete the Connection object but let socket go to keep-alive cache.
} else {
// Keep the connection but don't make a new request at this point.
// Perhaps due to a speed limit.
conn->pass();
}
}
bool DownloadTask::headerComplete(HttpClientConnection *conn) {
conn->doStreamResponse();
notifyBytesLoaded(conn->rawHttpHeader().size());
return true;
}
void DownloadTask::payload(HttpClientConnection *, char *, size_t len) {
notifyBytesLoaded(len);
}

View file

@ -0,0 +1,22 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "progresstask.h"
class DownloadTask : public ProgressTask {
public:
DownloadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn = 10, unsigned int max_conn = 100,
double duration = 10.0, double max_time = 25.0,
double tick_s = 0.1);
double start() override;
double timerEvent() override;
void newRequest(HttpClientConnection *) override;
bool headerComplete(HttpClientConnection *) override;
void payload(HttpClientConnection *, char *, size_t len) override;
private:
double tick_duration_s;
double dynamic_conn_limit;
};

View file

@ -0,0 +1,45 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "infotask.h"
void InfoTask::newRequest(HttpClientConnection *conn) {
conn->ws_get("/minfo?t=" + t() + "&key=" + key);
}
bool InfoTask::wsTextMessage(HttpConnection *,
const std::string &msg) {
log() << "MEASUREMENT INFO: " << msg;
setMessage(msg);
return true;
}
double InfoTask::timerEvent() {
double time = elapsed();
log() << "IT TimerEvent " << elapsed();
if (time >= timeout_s()) {
log() << "InfoTask timeout after " << time << " seconds";
setResult("");
return 0;
}
if (upload_deadline > 0.0) {
log() << "waiting for server upload, t=" << time << ", max="
<< upload_deadline;
if (time >= upload_deadline) {
setMessage("server upload timeout");
upload_deadline = -1.0;
}
return 0.1;
} else if (info_deadline > 0.0) {
log() << "waiting for measurement info, t=" << time << ", max="
<< info_deadline;
if (time >= info_deadline) {
setResult("");
info_deadline = -1.0;
}
return 0.1;
}
return 1.0;
}

View file

@ -0,0 +1,46 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "measurementtask.h"
class InfoTask : public MeasurementTask {
public:
InfoTask(const std::string &label, const std::string &ticket_string,
const std::string &hash_key, const HttpHost &server,
unsigned int no_conn = 1, unsigned int max_conn = 3,
double duration = 300.0) :
MeasurementTask(label, ticket_string, server,
no_conn, max_conn, duration),
key(hash_key)
{
}
void newRequest(HttpClientConnection *) override;
bool requestComplete(HttpClientConnection *) override {
return false;
}
bool wsTextMessage(HttpConnection *,
const std::string &msg) override;
double timerEvent() override;
// Stop waiting for upload result after at most t seconds:
void setUploadDeadline(double t) {
if (t > 0.0)
upload_deadline = elapsed() + t;
else
upload_deadline = t;
}
// Stop waiting for measurement ID after at most t seconds:
void setInfoDeadline(double t) {
if (t > 0.0)
info_deadline = elapsed() + t;
else
info_deadline = t;
}
private:
std::string key;
double upload_deadline = -1.0;
double info_deadline = -1.0;
};

View file

@ -0,0 +1,34 @@
#include <algorithm>
#include "../json11/json11.hpp"
#include "latencytask.h"
LatencyTask::LatencyTask(const std::string &ticket, const HttpHost &server) :
MeasurementTask("httplatency", ticket, server, 1, 15) {
}
void LatencyTask::newRequest(HttpClientConnection *conn) {
std::string lbl = std::to_string(++serial_no);
current_request[lbl + " ok"] = timeNow();
conn->get("/pingpong/" + lbl + "?t=" + t());
}
bool LatencyTask::requestComplete(HttpClientConnection *conn) {
auto p = current_request.find(conn->contents());
if (p == current_request.end()) {
log() << "unexpected response: " << conn->contents();
} else {
double latency = secondsSince(p->second);
log() << "got " << conn->contents() << " after " << latency << " sec";
samples.push_back(latency);
}
if (samples.size() < 12)
return true;
if (!terminated()) {
log() << "Samples: " << json11::Json(samples).dump();
setResult(calculateLatency(samples));
}
return false;
}

View file

@ -0,0 +1,16 @@
#pragma once
#include <vector>
#include "measurementtask.h"
class LatencyTask : public MeasurementTask {
public:
LatencyTask(const std::string &ticket, const HttpHost &server);
void newRequest(HttpClientConnection *) override;
bool requestComplete(HttpClientConnection *conn) override;
private:
std::vector<double> samples;
// Maps expected response to start time of request:
std::map<std::string, TimePoint> current_request;
unsigned int serial_no = static_cast<unsigned int>(rand());
};

View file

@ -0,0 +1,604 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <clocale>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream>
#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<PingSweepTask *>(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<HttpClientTask *>(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<std::string, std::string> 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<std::string, std::string> 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<std::string, std::string> 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<bool>(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<uint16_t>(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<std::string, std::string> 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<BridgeTask *>(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<uint16_t>(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";
}
}

View file

@ -0,0 +1,88 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include <string>
#include <map>
#include <sstream>
#include "../framework/bridgetask.h"
#include "../json11/json11.hpp"
#include "../http/httphost.h"
#include "../http/cookiefile.h"
class SpeedTest;
class MeasurementAgent : public Task {
public:
MeasurementAgent(const TaskConfig &config, const HttpHost &webserver);
void taskMessage(Task *task) override;
void taskFinished(Task *task) override;
void handleExecution(Task *sender, const std::string &msg) override;
void sendToClient(const std::string &method,
const std::string &jsonobj = "{}") {
if (bridge)
bridge->sendMsgToClient(method, jsonobj);
}
void sendTaskComplete(const std::string &t, const std::string &res = "");
void sendTaskProgress(const std::string &taskname,
double speed, double progress);
void accumulateLog() {
setLogFile(accumulated_log);
}
void appendLog(const std::string &str) {
accumulated_log << "\nAppend " << str.size() << "\n" << str;
}
void sendLogToServer();
private:
std::string getDefaultConfig();
bool isValidHashkey(const std::string &key);
void pollBridge(const std::string &msg);
static bool isValidJson(const std::string &s) {
std::string err;
auto obj = json11::Json::parse(s, err);
return err.empty();
}
void handleMsgFromClient(const std::string &method,
const json11::Json &args);
void handleConfigurationOption(const std::string &name,
const std::string &value);
void uploadComplete();
void doSaveReport();
void resetCurrentTest();
BridgeTask *bridge = nullptr;
SpeedTest *current_test = nullptr;
std::string current_ticket;
std::ostringstream accumulated_log;
// Initial state is IDLE. When client says "startTest", state becomes
// STARTED. When test is done, we send "testComplete global" to client
// and set state to FINISHED. When client sends resetTest, state will
// be reset to IDLE.
// If client sends abortTest in state STARTED, state becomes ABORTED.
enum class MeasurementState { IDLE, STARTED, FINISHED, ABORTED };
MeasurementState state = MeasurementState::IDLE;
// If the client doesn't manage keys, we store them here:
CookieManager *key_store;
std::string force_key;
// The web server and the measurement server:
HttpHost wserv, mserv;
// Default value, might be modified by the client
std::string wserv_contentsurl = "/api/content";
std::string wserv_measurementsurl = "/api/measurements";
std::string wserv_settingsurl = "/api/servers";
std::string settings_result;
// Info to be included each time measurement result is sent:
std::map<std::string, std::string> report_template;
TaskConfig cfgOptions;
std::string options_filename;
};

View file

@ -0,0 +1,62 @@
#include <numeric>
#include "measurementtask.h"
#include "../json11/json11.hpp"
#ifdef _WIN32
typedef long ssize_t;
#endif
void MeasurementTask::checkConnectionCount() {
if (terminated())
return;
if (active_connections < no_connections)
dbg_log() << "checkConnectionCount: act=" << active_connections
<< " want=" << no_connections
<< " max=" << max_connections;
while (active_connections < no_connections && max_connections) {
--max_connections;
if (!createNewConnection())
log() << "couldn't add http connection, " << max_connections
<< " attempts remain";
}
if (no_connections && !active_connections)
connectionLost();
}
void MeasurementTask::connAdded(SocketConnection *c) {
++active_connections;
log() << "conn added: " << c << " now have " << active_connections;
}
void MeasurementTask::connRemoved(SocketConnection *c) {
--active_connections;
log() << "conn removed: " << c << " now have " << active_connections;
if (!terminated()) {
checkConnectionCount();
}
}
std::string MeasurementTask::json_obj(const std::string &attr,
const std::string &value) {
json11::Json obj = json11::Json::object {
{ attr, value },
};
return obj.dump();
}
std::string MeasurementTask::calculateLatency(std::vector<double> &samples) {
if (samples.size() < 5)
return std::string(); // Too few samples
// Keep the best 60%
std::sort(samples.begin(), samples.end());
ssize_t n = static_cast<ssize_t>(samples.size() * 3 / 5);
double latency_sum = std::accumulate(samples.cbegin(),
samples.cbegin()+n, 0.0);
return fValue(1000.0 * latency_sum / static_cast<double>(n));
}

View file

@ -0,0 +1,115 @@
#pragma once
#include "../http/httpclienttask.h"
#include "../http/httphost.h"
#include "defs.h"
#include <clocale>
#include <sstream>
class MeasurementTask : public HttpClientTask {
public:
MeasurementTask(const std::string &name, const std::string &ticket,
const HttpHost &httpserver,
unsigned int no_conn = 1, unsigned int max_conn = 3,
double timeout = 25.0) :
HttpClientTask(name, httpserver),
no_connections(no_conn),
active_connections(0),
max_connections(max_conn),
timeout_sec(timeout),
ticket_string(ticket) {
setUserAgentString(measurement::appName + " " + measurement::appVersion);
}
std::string t() {
return ticket_string;
}
// Only use keep-alive within the same measurement:
std::string cacheLabel() override {
return ticket_string;
}
// If you override this, you must call checkConnectionCount().
// Return value is number of seconds until timerEvent
// should be called, <= 0 for never.
double start() override {
checkConnectionCount();
return timeout_sec;
}
// If you override timerEvent(), you'll have to check for timeout.
/*
double timerEvent() override {
if (timeout())
setResult("");
return 0;
}
*/
// Will be called if checkConnectionCount fails.
// Default is to terminate task with an empty result.
virtual void connectionLost() {
log() << "connectionLost()";
setResult("");
}
// Returns a JSON object with a single attribute-value pair
static std::string json_obj(const std::string &attr,
const std::string &value);
// Return floating point value as string.
// We don't want to use to_string since it's locale dependent,
// which may break JSON.
static std::string fValue(double x) {
std::ostringstream s;
s.imbue(std::locale("C"));
s << x;
return s.str();
}
static std::string calculateLatency(std::vector<double> &samples);
protected:
// Tries to make sure we have at least no_connections simultaneous
// HTTP connections. Will not try more than max_connection times.
// Will call connectionLost on fatal failure.
void checkConnectionCount();
// Will not kill already created connections.
void setNoConnections(unsigned int no) {
no_connections = no;
checkConnectionCount();
}
unsigned int getNoConnections() const {
return no_connections;
}
unsigned int currentNoConnections() const {
return active_connections;
}
void noMoreConnections() {
max_connections = 0;
}
bool timeout() {
if (timeout_sec < 0)
return false;
if (elapsed() < timeout_sec)
return false;
log() << "Task timeout.";
return true;
}
double timeout_s() {
return timeout_sec;
}
private:
void connAdded(SocketConnection *) final;
void connRemoved(SocketConnection *) final;
unsigned int no_connections, active_connections, max_connections;
double timeout_sec;
std::string ticket_string;
};

18
src/measurement/mk.inc Normal file
View file

@ -0,0 +1,18 @@
SOURCES += \
$(DIRLEVEL)/measurement/defs.cpp \
$(DIRLEVEL)/measurement/speedtest.cpp \
$(DIRLEVEL)/measurement/measurementtask.cpp \
$(DIRLEVEL)/measurement/singlerequesttask.cpp \
$(DIRLEVEL)/measurement/rpingtask.cpp \
$(DIRLEVEL)/measurement/latencytask.cpp \
$(DIRLEVEL)/measurement/pingsweeptask.cpp \
$(DIRLEVEL)/measurement/warmuptask.cpp \
$(DIRLEVEL)/measurement/tickettask.cpp \
$(DIRLEVEL)/measurement/infotask.cpp \
$(DIRLEVEL)/measurement/progresstask.cpp \
$(DIRLEVEL)/measurement/downloadtask.cpp \
$(DIRLEVEL)/measurement/uploadtask.cpp \
$(DIRLEVEL)/measurement/measurementagent.cpp \
$(DIRLEVEL)/measurement/uploadinfotask.cpp
include $(DIRLEVEL)/http/mk.inc

View file

@ -0,0 +1,57 @@
#include "pingsweeptask.h"
PingSweepTask::PingSweepTask(const std::string &cfg,
const HttpHost &server) :
HttpClientTask("pingsweep", server),
config(json11::Json::parse(cfg, json_err)) {
}
double PingSweepTask::start() {
unsigned int no_calls = 0;
for (auto srv : config["servers"].array_items()) {
//log() << "Ping " << srv["url"].string_value();
setServer(srv["url"].string_value());
if (createNewConnection())
++no_calls;
}
if (no_calls)
return 5.0;
setResult("");
return 0.0;
}
double PingSweepTask::timerEvent() {
setResult(best_host);
return 0;
}
void PingSweepTask::newRequest(HttpClientConnection *conn) {
//log() << "newRequest " << conn->hostname();
auto p = requests.find(conn->hostname());
if (p != requests.end())
return; // Ignore host since we already have made a request to it.
requests[conn->hostname()] = timeNow();
conn->get("/pingsweep/req" + std::to_string(requests.size()));
}
bool PingSweepTask::requestComplete(HttpClientConnection *conn) {
//log() << "requestComplete " << conn->hostname() << " size " << requests.size();
auto p = requests.find(conn->hostname());
if (p != requests.end()) {
double t = 1000 * secondsSince(p->second);
std::string r = conn->hostname() + " " + std::to_string(t) + " ms";
log() << r;
if (results.empty())
setMessage("ping info");
results.push_back(r);
requests.erase(p);
if (t < best_time) {
best_time = t;
best_host = conn->hostname();
}
}
if (requests.empty())
setResult(best_host);
return false;
}

View file

@ -0,0 +1,32 @@
#pragma once
#include <vector>
#include <deque>
#include "../json11/json11.hpp"
#include "../http/httpclienttask.h"
class PingSweepTask : public HttpClientTask {
public:
PingSweepTask(const std::string &obj, const HttpHost &server);
double start() override;
double timerEvent() override;
void newRequest(HttpClientConnection *) override;
bool requestComplete(HttpClientConnection *conn) override;
std::string cacheLabel() override {
return std::string(); // Don't store in keep-alive cache
}
std::string getResult() {
if (results.empty())
return std::string();
std::string res = results.front();
results.pop_front();
return res;
}
private:
std::string json_err;
json11::Json config;
std::map<std::string, TimePoint> requests;
std::string best_host;
std::deque<std::string> results;
double best_time = 1e6;
};

View file

@ -0,0 +1,79 @@
#include "progresstask.h"
void ProgressTask::doTestProgress(double mbps, double duration,
unsigned int no_conn) {
if (duration <= current_duration || terminated())
return;
current_duration = duration;
// Don't let the speed decrease the last half second:
if (duration < tot_duration-0.5 || mbps > current_mbps)
current_mbps = mbps;
if (duration >= tot_duration) {
setResult(fValue(current_mbps));
return;
}
setMessage("progress");
log() << "task progress " << current_mbps << ' ' << duration/tot_duration;
if (duration > tot_duration-0.35) {
// Less than 0.35 seconds left, don't make any new requests
// (but let existing requests keep going).
noMoreConnections();
current_load_size = 0;
soon_finished = true;
return;
}
// Calculate appropriate load size for subsequent requests
//unsigned int n = getNoConnections();
if (!no_conn)
return;
double time_left = tot_duration - duration;
// We'll probably load this number of bytes during the time_left:
double exp_bytes;
if (speedlimit_mbps > 0.0)
exp_bytes = std::min(speedlimit_mbps, mbps) * std::min(time_left, 0.3) / 0.000008;
else
exp_bytes = mbps * time_left / 0.000008;
current_load_size = static_cast<size_t>(exp_bytes / 4.0 / no_conn);
if (current_load_size > 40000000)
current_load_size = 40000000;
else if (current_load_size < 6000)
current_load_size = 6000;
if (speedlimit_mbps > mbps) {
dbg_log() << "We're going too slow, wake up the passive connections";
wakeUp();
}
}
size_t ProgressTask::loadSize() {
// For very fast network, speed up even before first doTestProgress:
if (++no_started_loads == load_size_check && current_duration<=0) {
double time = elapsed();
double speed = addOverheadMbps(byteCount(), time);
doTestProgress(speed, time, currentNoConnections());
}
if (speedlimit_mbps > 0.0) {
double time = elapsed();
double speed = addOverheadMbps(byteCount(), time);
if (speed > speedlimit_mbps) {
log() << "going too fast, will pause";
return 0;
}
}
return current_load_size;
}
bool ProgressTask::requestComplete(HttpClientConnection *) {
return true;
}

View file

@ -0,0 +1,108 @@
#pragma once
#include "measurementtask.h"
class ProgressTask : public MeasurementTask {
public:
ProgressTask(const std::string &label, const std::string &ticket_string,
const HttpHost &server,
unsigned int no_conn = 4, unsigned int max_conn = 20,
double duration = 10.0, double timeout = 25.0) :
MeasurementTask(label, ticket_string, server, no_conn, max_conn, timeout),
byte_count(0),
current_load_size(50000),
load_size_check(no_conn + 2),
current_duration(0.0),
current_mbps(0.0) {
if (duration < 2.0)
tot_duration = 2.0;
else if (duration > 20.0)
tot_duration = 20.0;
else
tot_duration = duration;
}
bool requestComplete(HttpClientConnection *) override;
void notifyBytesAndDuration(uint64_t count, double duration) {
double mbps = addOverheadMbps(count, duration);
doTestProgress(mbps, duration, currentNoConnections());
}
void notifyBytesLoaded(size_t n) {
byte_count += n;
}
// If you want to use the threadSendCount/threadRecvCount methods, you
// should call the below method in your start() method:
void notifyStarted() {
thread_send_count = SocketConnection::totBytesSent();
thread_recv_count = SocketConnection::totBytesReceived();
}
void connectionLost() override {
// We're dead. If more than half the test was completed,
// we keep the result.
if (current_duration > tot_duration*0.5)
setResult(fValue(current_mbps));
else
setResult("-1");
}
void set_speedlimit(double limit_mbps) {
speedlimit_mbps = (limit_mbps < 0.5) ? 0.5 : limit_mbps;
if (speedlimit_mbps < 5.0)
current_load_size = 5000;
}
size_t loadSize();
double currentDuration() const {
return current_duration;
}
double currentProgress() const {
return current_duration/tot_duration;
}
double currentMbps() const {
return current_mbps;
}
protected:
uint64_t byteCount() {
return byte_count;
}
// Return total number of bytes sent through all sockets in the current
// thread since last call to notifyStarted():
uint64_t threadSendCount() {
return SocketConnection::totBytesSent() - thread_send_count;
}
// Return total number of bytes received from all sockets in the current
// thread since last call to notifyStarted():
uint64_t threadRecvCount() {
return SocketConnection::totBytesReceived() - thread_recv_count;
}
static double addOverheadMbps(uint64_t n, double s) {
if (s <= 0.0)
return 0.0;
double mbps = static_cast<double>(n) / s * 0.000008;
if (mbps > 8.0)
return (mbps * 1.02 + 0.16);
else
return mbps * 1.04;
}
void doTestProgress(double mbps, double duration, unsigned int no_conn);
bool soonFinished() const {
return soon_finished;
}
private:
uint64_t byte_count, thread_send_count = 0, thread_recv_count = 0;
size_t current_load_size;
unsigned int load_size_check, no_started_loads = 0;
double tot_duration, current_duration, current_mbps;
double speedlimit_mbps = 0.0;
bool soon_finished = false;
};

View file

@ -0,0 +1,61 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <sstream>
#include "rpingtask.h"
void RpingTask::newRequest(HttpClientConnection *conn) {
conn->ws_get("/ws?t=" + t());
}
bool RpingTask::wsTextMessage(HttpConnection *conn,
const std::string &msg) {
if (!max_roundtrips) {
setResult("");
return false;
}
--max_roundtrips;
std::istringstream line(msg);
std::string cmd, challenge;
double val;
line >> cmd;
if (sent_challenge) {
line >> val;
if (cmd == "latency_result") {
setResult(fValue(val));
return false;
} else if (line && val > 0.0) {
samples.push_back(val);
}
}
if (cmd != "challenge") {
err_log() << "unknown message: " << msg;
return false;
}
log() << "Received " << msg;
line >> challenge;
if (!line || challenge.size() > 10) {
err_log() << "Bad rping challenge";
return false;
}
if (samples.size() >= 12) {
conn->sendWsMessage("rping end");
log() << "Sending rping end";
return true;
}
conn->sendWsMessage("rping " + challenge);
log() << "Sending rping " << challenge;
sent_challenge = true;
return true;
}
bool RpingTask::websocketUpgrade(HttpClientConnection *conn) {
conn->sendWsMessage("rping start");
log() << "Sending rping start";
return true;
}

View file

@ -0,0 +1,30 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "measurementtask.h"
class RpingTask : public MeasurementTask {
public:
RpingTask(const std::string &label, const std::string &ticket_string,
const HttpHost &server,
unsigned int no_conn = 1, unsigned int max_conn = 20,
double duration = 25.0) :
MeasurementTask(label, ticket_string, server,
no_conn, max_conn, duration)
{
}
void newRequest(HttpClientConnection *) override;
bool requestComplete(HttpClientConnection *) override {
return false;
}
bool websocketUpgrade(HttpClientConnection *) override;
bool wsTextMessage(HttpConnection *conn,
const std::string &msg) override;
void calc_local_result();
private:
std::vector<double> samples;
unsigned int max_roundtrips = 100;
bool sent_challenge = false;
};

View file

@ -0,0 +1,19 @@
#include "singlerequesttask.h"
SingleRequestTask::SingleRequestTask(const std::string &url,
const std::string &name,
const std::string &ticket,
const HttpHost &server) :
MeasurementTask(name, ticket, server),
_url(url) {
}
void SingleRequestTask::newRequest(HttpClientConnection *conn) {
conn->get(_url);
}
bool SingleRequestTask::requestComplete(HttpClientConnection *conn) {
if (conn->httpStatus() == 200)
setResult(conn->contents());
return false;
}

View file

@ -0,0 +1,13 @@
#pragma once
#include "measurementtask.h"
class SingleRequestTask : public MeasurementTask {
public:
SingleRequestTask(const std::string &url, const std::string &name,
const std::string &ticket, const HttpHost &server);
void newRequest(HttpClientConnection *conn) override;
bool requestComplete(HttpClientConnection *conn) override;
private:
std::string _url;
};

View file

@ -0,0 +1,358 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include <iomanip>
#include "../http/sha1.h"
#include "speedtest.h"
#include "measurementagent.h"
#include "rpingtask.h"
// Alternative latency measurement:
#include "latencytask.h"
#include "warmuptask.h"
#include "tickettask.h"
#include "infotask.h"
#include "downloadtask.h"
// Alternative download measurement:
#include "wsdownloadtask.h"
#include "uploadtask.h"
// Alternative upload measurement: #include "wsuploadtask.h"
// Alternative to some parts of infotask:
#include "uploadinfotask.h"
SpeedTest::SpeedTest(MeasurementAgent *agent, const HttpHost &mserver,
const std::map<std::string, std::string> &report_data) :
Task("SpeedTest"),
the_agent(agent),
mserv(mserver),
report(report_data)
{
bytesSentAtStart = SocketConnection::totBytesSent();
bytesRecAtStart = SocketConnection::totBytesReceived();
killChildTaskWhenFinished();
upload_duration = std::stod(report["ulength"]);
download_duration = std::stod(report["dlength"]);
try {
auto p = report.find("speedlimit");
if (p != report.end()) {
log() << "speedlimit " << p->second;
speed_limit = std::stod(p->second);
if (speed_limit < 0.5)
speed_limit = 0.5;
if (speed_limit <= 10.0) {
initial_no_dconn = 2;
max_no_dconn = 10;
initial_no_uconn = 2;
max_no_uconn = 4;
}
} else {
log() << "no speedlimit";
}
} catch (...) {
the_agent->sendToClient("setInfo", MeasurementTask::
json_obj("warning", "cannot parse SpeedLimit value"));
}
}
double SpeedTest::start() {
startObserving(the_agent);
addNewTask(new TicketTask(mserv, report["key"], report["host"]), this);
return 0.0;
}
void SpeedTest::taskMessage(Task *task) {
if (!the_agent)
return;
std::string name = task->label(),
message = task->message();
if (task == info_task) {
log() << "GOT INFO: " << message;
if (message == "server upload timeout") {
uploadComplete();
return;
}
std::string err;
auto obj = json11::Json::parse(message, err);
if (!err.empty()) {
err_log() << "JSON error, ignoring message";
return;
}
std::string event = obj["event"].string_value();
const json11::Json &args = obj["args"];
// {"event": "measurementInfo",
// "args": {
// "ispname":"Telia","date":"Thu, 30 Nov 2017 15:25:31 +0000",
// "rating":"GOOD","MeasurementID":"109"
// }
// }
if (event == "measurementInfo") {
the_agent->sendToClient("measurementInfo", args.dump());
setResult("");
} else if (event == "uploadInfo") {
/* {
"event": "uploadInfo",
"args": {
"duration": 1.506616427,
"speed": 37348.559901815221
}
}
*/
double speed = args["speed"].number_value();
double duration = args["duration"].number_value();
log() << "uploadInfo Duration: " << duration
<< " Speed: " << speed;
if (duration >= upload_duration) {
server_upload_speed = MeasurementTask::fValue(speed);
uploadComplete();
} else if (duration > 0)
the_agent->sendTaskProgress("upload", speed,
duration/upload_duration);
} else {
log() << "unknown event: " << event;
}
} else if (ProgressTask *ptask = dynamic_cast<ProgressTask *>(task)) {
double speed = ptask->currentMbps();
double progress = ptask->currentProgress();
if (name != "upload")
the_agent->sendTaskProgress(name, speed, progress);
log() << name << " speed " << speed << " Mbit/s, progress " << progress;
} else {
log() << name << " message: " << message;
}
}
void SpeedTest::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 == the_agent) {
the_agent = nullptr;
log() << "Agent gone, will exit";
setResult("");
} else if (task == info_task) {
info_task = nullptr;
if (report_sent_to_server)
setResult("");
return;
}
if (!the_agent)
return;
if (name == "ticket") {
if (result.empty()) {
json11::Json obj = json11::Json::object {
{ "error", "no network access to server" },
{ "errno", "C01" }
};
the_agent->sendToClient("setInfo", obj.dump());
setResult("");
} else {
tstr = result;
if (TicketTask *ttask = dynamic_cast<TicketTask *>(task))
report["localip"] = ttask->localIp();
the_agent->sendToClient("setInfo", MeasurementTask::
json_obj("ticket", tstr));
the_agent->sendToClient("taskStart", MeasurementTask::
json_obj("task", "latency"));
info_task = new InfoTask("measurement info", tstr,
report["key"], mserv);
addNewTask(info_task, this);
RpingTask *t = new RpingTask("rping", tstr, mserv);
addNewTask(t, this);
// Send the ticket to the agent. The agent may use it to update
// subscription into or fetch logs after the measurement is done.
executeHandler(the_agent, tstr);
}
} else if (name == "measurementStart") {
std::string err;
auto obj = json11::Json::parse(result, err);
if (!result.empty() && err.empty()) {
std::string ip = obj["ip"].string_value();
if (!ip.empty())
log() << "Client external ip number: " << ip;
if (local_latency) {
double latency = obj["latency"].number_value();
if (latency > 0) {
local_latency = false;
report["latency"] = std::to_string(latency);
log() << "Adjusted latency: " << latency;
}
}
}
} else if (name == "rping" || name == "httplatency") {
if (name == "rping") {
if (result.size()) {
websocket_works = true;
} else {
addNewTask(new LatencyTask(tstr, mserv), this);
return;
}
} else {
local_latency = true;
if (!result.size())
result = "-1";
}
report["latency"] = result;
the_agent->sendTaskComplete("latency", result);
std::string url = "/measurementStarted?t=" + tstr;
//url += "&uptick=120";
HttpClientConnection::addUrlPars(url, report);
log() << "measurementStart: " << mserv.hostname << url;
addNewTask(new SingleRequestTask(url, "measurementStart",
tstr, mserv), this);
addNewTask(new WarmUpTask(tstr, mserv, initial_no_dconn, 3.0),
this);
} else if (name == "download") {
if (!result.size())
result = "-1";
the_agent->sendTaskComplete("download", result);
report["download"] = result;
the_agent->sendToClient("taskStart", MeasurementTask::
json_obj("task", "upload"));
// UploadInfoTask fetches upload speed/progress from the server.
// If the final speed sill hasn't arrived 3.0 seconds after the end of
// the upload measurement, we give up and use the local upload speed
// estimate which is lower since we can only count confirmed data.
if (!websocket_works)
addNewTask(new UploadInfoTask(tstr, mserv, upload_duration,
upload_duration+3.0), this);
UploadTask *t = new UploadTask(tstr, mserv, initial_no_uconn,
max_no_uconn, upload_duration);
if (speed_limit > 0)
t->set_speedlimit(speed_limit);
addNewTask(t, this);
} else if (name == "saveReport") {
uint64_t totBytesSent = SocketConnection::totBytesSent() - bytesSentAtStart;
uint64_t totBytesReceived = SocketConnection::totBytesReceived() - bytesRecAtStart;
json11::Json bytesInfo = json11::Json::object {
{ "totBytesReceived", std::to_string(totBytesReceived)},
{ "totBytesSent", std::to_string(totBytesSent) }
};
the_agent->sendToClient("setInfo", bytesInfo.dump());
std::string err;
auto obj = json11::Json::parse(result, err);
if (!result.empty() && err.empty()) {
the_agent->sendToClient("report", result);
if (report["host"] == "none") {
// Local measurement, no more info will arrive.
setResult("");
return;
}
log() << "INFO: " << result;
// Wait at most 3 seconds for final info from server.
if (info_task) {
info_task->setInfoDeadline(3.0);
info_task->resetTimer(0.1);
} else {
std::string url = "/getUpdate?t=" + tstr +
"&key=" + report["key"];
addNewTask(new SingleRequestTask(url, "getUpdate", tstr,
mserv), this);
}
} else {
the_agent->sendToClient("report", "{}");
log() << "invalid saveReport: " << result;
setResult("");
}
} else if (name == "warmup") {
the_agent->sendToClient("taskStart", MeasurementTask::
json_obj("task", "download"));
//WsDownloadTask *t = new WsDownloadTask(tstr, mserv, initial_no_dconn,
DownloadTask *t = new DownloadTask(tstr, mserv, initial_no_dconn,
max_no_dconn, download_duration);
log() << "t->set_speedlimit " << speed_limit;
if (speed_limit > 0)
t->set_speedlimit(speed_limit);
addNewTask(t, this);
} else if (name == "upload") {
local_upload_speed = result.size() ? result : "0";
if (server_upload_speed.empty()) {
if (info_task) {
// Wait for up to 3 seconds for server upload speed:
info_task->setUploadDeadline(3.0);
info_task->resetTimer(0.1);
} else {
// Pointless to wait for server upload info.
uploadComplete();
}
}
} else if (name == "getUpdate") {
std::string err;
auto obj = json11::Json::parse(result, err);
std::string event = obj["event"].string_value();
if (err.empty() && event == "measurementInfo") {
const json11::Json &args = obj["args"];
the_agent->sendToClient("measurementInfo", args.dump());
} else {
err_log() << "JSON error, ignoring message";
the_agent->sendToClient("measurementInfo", "{}");
}
setResult("");
} else {
log() << "unknown task, ignoring";
}
}
void SpeedTest::uploadComplete() {
if (report.find("upload") != report.end()) {
log() << "Upload already set: " << report["upload"];
return;
}
// Local and server speed calculations are complete.
// Use local calculation only if server calculation failed:
std::string result = server_upload_speed;
if (result.empty() || (result == "0" && !local_upload_speed.empty()))
result = local_upload_speed;
report["upload"] = result;
the_agent->sendTaskComplete("upload", result);
auto p = report.find("autosave");
if (p == report.end() || p->second != "false")
doSaveReport();
else
log() << "Wait for client to be ready for saving report";
if (info_task)
info_task->setUploadDeadline(-1.0);
}
void SpeedTest::addToReport(const std::string &attr, const std::string &val) {
report[attr] = val;
}
void SpeedTest::doSaveReport(const json11::Json &args) {
if (report_sent_to_server)
return;
report_sent_to_server = true;
std::string url("/saveReport?t=" + tstr);
for (auto p : args.object_items()) {
const std::string &attr = p.first;
if (report.find(attr) == report.end())
report[attr] = p.second.string_value();
}
HttpClientConnection::addUrlPars(url, report);
log() << "saveReport " << url;
addNewTask(new SingleRequestTask(url, "saveReport",
tstr, mserv), this);
}

View file

@ -0,0 +1,48 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
// Perform a standard speed test, measuring latency, download speed, and upload
// speed.
#include "../json11/json11.hpp"
#include "../framework/task.h"
#include "../http/httphost.h"
class MeasurementAgent;
class InfoTask;
class SpeedTest : public Task {
public:
SpeedTest(MeasurementAgent *agent, const HttpHost &mserver,
const std::map<std::string, std::string> &report_data);
double start() override;
void taskMessage(Task *task) override;
void taskFinished(Task *task) override;
void uploadComplete();
void doSaveReport(const json11::Json &args = json11::Json::object());
void addToReport(const std::string &attr, const std::string &val);
private:
MeasurementAgent *the_agent = nullptr;
InfoTask *info_task = nullptr;
HttpHost mserv;
std::map<std::string, std::string> report;
std::string tstr;
double upload_duration, download_duration;
std::string server_upload_speed, local_upload_speed;
bool report_sent_to_server = false;
bool websocket_works = false;
// Normally, the server should calculate the latency result.
// If that failed and we have a locally calculated latency result,
// this will be set to true:
bool local_latency = false;
double speed_limit = 0.0;
unsigned int initial_no_dconn = 10, initial_no_uconn = 4;
unsigned int max_no_dconn = 100, max_no_uconn = 100;
uint64_t bytesSentAtStart;
uint64_t bytesRecAtStart;
};

View file

@ -0,0 +1,28 @@
#include "../json11/json11.hpp"
#include "tickettask.h"
TicketTask::TicketTask(const HttpHost &server, const std::string &key,
const std::string &host) :
SingleRequestTask("/ticket?key=" + key + "&host=" + host, "ticket", "",
server) {
}
bool TicketTask::requestComplete(HttpClientConnection *conn) {
if (conn->httpStatus() == 200) {
std::string err;
json11::Json ticket_obj = json11::Json::parse(conn->contents(), err);
if (err.empty()) {
_ticket = ticket_obj["ticket"].string_value();
if (!_ticket.empty()) {
_localIp = conn->localIp();
setResult(_ticket);
} else {
err_log() << "no ticket found";
}
} else {
err_log() << "cannot parse ticket: " << err;
}
}
return false;
}

View file

@ -0,0 +1,20 @@
#pragma once
#include "singlerequesttask.h"
class TicketTask : public SingleRequestTask {
public:
TicketTask(const HttpHost &server, const std::string &key,
const std::string &host);
std::string cacheLabel() override {
return _ticket;
}
bool requestComplete(HttpClientConnection *conn) override;
const std::string &localIp() const {
return _localIp;
}
private:
std::string _ticket, _localIp;
};

View file

@ -0,0 +1,41 @@
#include "uploadinfotask.h"
UploadInfoTask::
UploadInfoTask(const std::string &ticket, const HttpHost &server,
double duration, double max_time) :
ProgressTask("uploadinfo", ticket, server, 1, 3, duration, max_time) {
}
double UploadInfoTask::start() {
checkConnectionCount();
return timeout_s();
}
void UploadInfoTask::newRequest(HttpClientConnection *conn) {
conn->get("/ulinfo/1.txt?t=" + t());
}
bool UploadInfoTask::headerComplete(HttpClientConnection *conn) {
conn->doStreamResponse();
return true;
}
void UploadInfoTask::payload(HttpClientConnection *, char *buf, size_t len) {
buffer.append(buf, len);
while (true) {
std::string::size_type pos = buffer.find("\r\n");
if (pos == std::string::npos)
return;
std::istringstream line(buffer.substr(0, pos));
uint64_t byte_count;
double duration;
if (line >> byte_count >> duration) {
log() << "server upload info " << byte_count << ' ' << duration;
notifyBytesAndDuration(byte_count, duration);
} else if (pos) {
err_log() << "bad server info line: " << buffer.substr(0, pos);
}
buffer.erase(0, pos+2);
}
}

View file

@ -0,0 +1,15 @@
#pragma once
#include "progresstask.h"
class UploadInfoTask : public ProgressTask {
public:
UploadInfoTask(const std::string &ticket, const HttpHost &server,
double duration = 10.0, double max_time = 25.0);
double start() override;
void newRequest(HttpClientConnection *) override;
bool headerComplete(HttpClientConnection *) override;
void payload(HttpClientConnection *, char *, size_t ) override;
private:
std::string buffer;
};

View file

@ -0,0 +1,83 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "uploadtask.h"
#include <vector>
UploadTask::UploadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn, unsigned int max_conn,
double duration, double max_time, double tick_s) :
ProgressTask("upload", ticket, server, no_conn, max_conn, duration, max_time),
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
}
double UploadTask::start() {
log() << "UploadTask starting";
notifyStarted();
checkConnectionCount();
return tick_duration_s;
}
void UploadTask::newRequest(HttpClientConnection *conn) {
if (size_t to_write = loadSize()) {
conn->post("/cgi/upload.cgi?t=" + t() + "&id=1", to_write);
post_size[conn] = to_write;
} else {
conn->pass();
}
}
bool UploadTask::requestComplete(HttpClientConnection *conn) {
if (conn->httpStatus() == 200) {
// Now we know for sure that all posted data has arrived at the server
notifyBytesLoaded(post_size[conn]);
}
return true;
}
double UploadTask::timerEvent() {
double time = elapsed();
if (time >= timeout_s()) {
log() << "UploadTask timeout after " << time << " seconds";
setResult("-1");
return 0;
}
if (time > 0.05) {
// log() << "Check: " << byteCount() << " and " << threadSendCount();
// Note: byteCount() is the number of bytes we know for sure have
// arrived at the server, threadSendCount() is the number of bytes
// we've put into socket buffers. We have to be careful and use the
// lower value, i.e. byteCount(), which is too low, instead of the
// higher value, i.e. threadSendCount(), which is too large.
// However, the server knows the correct value and will send it to
// us regularly through the InfoTask.
double mbps = addOverheadMbps(byteCount(), time);
doTestProgress(mbps, time, currentNoConnections());
}
return tick_duration_s;
}
namespace {
std::vector<char> getUploadBuffer(size_t len) {
std::vector<char> buf;
// Fill the buffer with dummy data that cannot cause data compression
while (len) {
buf.push_back(static_cast<char>(rand()));
--len;
}
return buf;
}
}
size_t UploadTask::doPost(HttpClientConnection *conn, size_t len) {
static std::vector<char> buffer = getUploadBuffer(post_buffer_len);
size_t to_write = std::min(len, post_buffer_len);
return conn->sendData(buffer.data(), to_write);
}
const size_t UploadTask::post_buffer_len;

View file

@ -0,0 +1,20 @@
#pragma once
#include "progresstask.h"
class UploadTask : public ProgressTask {
public:
UploadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn = 4, unsigned int max_conn = 20,
double duration = 10.0, double max_time = 25.0,
double tick_s = 0.1);
double start() override;
double timerEvent() override;
void newRequest(HttpClientConnection *conn) override;
bool requestComplete(HttpClientConnection *conn) override;
size_t doPost(HttpClientConnection *conn, size_t len) override;
private:
double tick_duration_s;
static const size_t post_buffer_len = 131072;
std::map<HttpClientConnection *, size_t> post_size;
};

View file

@ -0,0 +1,9 @@
#include "warmuptask.h"
void WarmUpTask::newRequest(HttpClientConnection *conn) {
conn->get(url);
}
bool WarmUpTask::requestComplete(HttpClientConnection *) {
return false;
}

View file

@ -0,0 +1,16 @@
#pragma once
#include "measurementtask.h"
class WarmUpTask : public MeasurementTask {
public:
WarmUpTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn, double timeout) :
MeasurementTask("warmup", ticket, server, no_conn, no_conn, timeout),
url("/pingpong/warmup?t=" + ticket) {
}
void newRequest(HttpClientConnection *conn) override;
bool requestComplete(HttpClientConnection *conn) override;
private:
std::string url;
};

View file

@ -0,0 +1,97 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "wsdownloadtask.h"
WsDownloadTask::
WsDownloadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn, unsigned int max_conn,
double duration, double max_time,
double tick_s) :
ProgressTask("download", ticket, server, no_conn, max_conn, duration, max_time),
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
dynamic_conn_limit = 0.5 * duration + 0.5;
}
double WsDownloadTask::start() {
log() << "WsDownloadTask starting";
checkConnectionCount();
return tick_duration_s;
}
double WsDownloadTask::timerEvent() {
double time = elapsed();
if (time >= timeout_s()) {
log() << "WsDownloadTask timeout after " << time << " seconds";
setResult("-1");
return 0;
}
if (time > 0.05) {
double speed = addOverheadMbps(byteCount(), time);
if (time < dynamic_conn_limit && speed >= 50.0) {
unsigned int no_conn = (speed < 250.0) ?
((speed < 100) ? 12 : 24) :
((speed < 500) ? 32 : 48);
if (no_conn < currentNoConnections())
no_conn = currentNoConnections();
doTestProgress(speed, time, no_conn);
setNoConnections(no_conn);
} else {
doTestProgress(speed, time, currentNoConnections());
}
}
return tick_duration_s;
}
void WsDownloadTask::newRequest(HttpClientConnection *conn) {
conn->ws_get("/ws?t=" + t());
//conn->dbgOn();
}
#include <unistd.h>
bool WsDownloadTask::nextRequest(HttpConnection *conn) {
if (size_t size = loadSize()) {
conn->sendWsMessage("download " + std::to_string(size));
//log() << "nextRequest " << size;
return true;
} else if (soonFinished()) {
log() << "nextRequest no more";
return false;
} else {
log() << "nextRequest idle";
// Keep the connection but don't make a new request at this point.
// Perhaps due to a speed limit.
//conn->pass();
return true;
}
}
bool WsDownloadTask::requestComplete(HttpClientConnection *) {
return false;
}
bool WsDownloadTask::websocketUpgrade(HttpClientConnection *conn) {
return nextRequest(conn);
}
bool WsDownloadTask::wsBinMessage(HttpConnection *conn,
const std::string &msg) {
log() << "Got BIN: " << msg.size();
notifyBytesLoaded(msg.size());
return nextRequest(conn);
}
bool WsDownloadTask::wsBinData(HttpConnection *conn, const char *, size_t count) {
notifyBytesLoaded(count);
if (conn->wsIncomingBytesLeft())
return true;
return nextRequest(conn);
}
bool WsDownloadTask::wsTextMessage(HttpConnection *,
const std::string &msg) {
log() << "Unexpected ws text message: " << msg;
return false;
}

View file

@ -0,0 +1,32 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "progresstask.h"
class WsDownloadTask : public ProgressTask {
public:
WsDownloadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn = 10, unsigned int max_conn = 100,
double duration = 10.0, double max_time = 25.0,
double tick_s = 0.1);
double start() override;
double timerEvent() override;
void newRequest(HttpClientConnection *) override;
bool requestComplete(HttpClientConnection *) override;
bool websocketUpgrade(HttpClientConnection *) override;
bool wsBinMessage(HttpConnection *conn,
const std::string &msg) override;
bool wsTextMessage(HttpConnection *conn,
const std::string &msg) override;
bool wsBinHeader(HttpConnection *conn, size_t ) override {
conn->streamWsResponse();
return true;
}
bool wsBinData(HttpConnection *conn, const char *, size_t count) override;
private:
bool nextRequest(HttpConnection *conn);
double tick_duration_s;
double dynamic_conn_limit;
};

View file

@ -0,0 +1,93 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#include "wsuploadtask.h"
#include <vector>
WsUploadTask::WsUploadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn, unsigned int max_conn,
double duration, double max_time, double tick_s) :
ProgressTask("upload", ticket, server, no_conn, max_conn, duration, max_time),
tick_duration_s(tick_s>0 ? tick_s : 0.1) {
}
double WsUploadTask::start() {
log() << "WsUploadTask starting";
checkConnectionCount();
return tick_duration_s;
}
void WsUploadTask::newRequest(HttpClientConnection *conn) {
// conn->dbgOn();
conn->ws_get("/ws?t=" + t());
}
namespace {
std::vector<char> getUploadBuffer(size_t len) {
std::vector<char> buf;
// Fill the buffer with dummy data that cannot cause data compression
while (len) {
buf.push_back(static_cast<char>(rand()));
--len;
}
return buf;
}
}
bool WsUploadTask::nextRequest(HttpConnection *conn) {
size_t len = loadSize();
if (!len)
return false;
conn->startWsBinStream(len);
return true;
}
size_t WsUploadTask::sendWsData(HttpConnection *conn) {
static std::vector<char> buffer = getUploadBuffer(post_buffer_len);
size_t to_write = std::min(conn->wsOutgoingBytesLeft(), post_buffer_len);
//log() << "sendWsData " << to_write;
return conn->sendData(buffer.data(), to_write);
}
double WsUploadTask::timerEvent() {
double time = elapsed();
if (time >= timeout_s()) {
log() << "UploadTask timeout after " << time << " seconds";
setResult("-1");
return 0;
}
if (time > 0.05) {
double mbps = addOverheadMbps(byteCount(), time);
doTestProgress(mbps, time, currentNoConnections());
}
return tick_duration_s;
}
bool WsUploadTask::websocketUpgrade(HttpClientConnection *conn) {
return nextRequest(conn);
}
bool WsUploadTask::wsBinMessage(HttpConnection *,
const std::string &msg) {
log() << "Unexpected ws bin message, size=" << msg.size();
return false;
}
bool WsUploadTask::wsTextMessage(HttpConnection *conn,
const std::string &msg) {
//log() << "WsUploadTask got ws text message: " << msg;
std::istringstream ss(msg);
std::string op;
size_t count;
if (ss >> op >> count && op == "upload") {
notifyBytesLoaded(count);
return nextRequest(conn);
}
return false;
}
const size_t WsUploadTask::post_buffer_len;

View file

@ -0,0 +1,28 @@
// Copyright (c) 2018 The Swedish Internet Foundation
// Written by Göran Andersson <initgoran@gmail.com>
#pragma once
#include "progresstask.h"
class WsUploadTask : public ProgressTask {
public:
WsUploadTask(const std::string &ticket, const HttpHost &server,
unsigned int no_conn = 4, unsigned int max_conn = 20,
double duration = 10.0, double max_time = 25.0,
double tick_s = 0.1);
double start() override;
double timerEvent() override;
void newRequest(HttpClientConnection *conn) override;
bool websocketUpgrade(HttpClientConnection *conn) override;
bool wsBinMessage(HttpConnection *conn,
const std::string &msg) override;
bool wsTextMessage(HttpConnection *conn,
const std::string &msg) override;
size_t sendWsData(HttpConnection *conn) override;
private:
bool nextRequest(HttpConnection *conn);
double tick_duration_s;
static const size_t post_buffer_len = 131072;
std::map<HttpClientConnection *, size_t> bytes_left_to_post, post_size;
};