initial
This commit is contained in:
commit
ab9a0bd4e2
183 changed files with 20701 additions and 0 deletions
2
src/.gitignore
vendored
Normal file
2
src/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
*.o
|
||||
*.d
|
||||
1
src/cli/.gitignore
vendored
Normal file
1
src/cli/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
cli
|
||||
31
src/cli/Makefile
Normal file
31
src/cli/Makefile
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
TARGET = cli
|
||||
DIRLEVEL = ..
|
||||
|
||||
# Possible LOGLEVEL values: dbg, info, warn, err, none
|
||||
LOGLEVEL=info
|
||||
|
||||
# Uncomment if GnuTLS version 3.5 or later is available
|
||||
# GNUTLS=1
|
||||
|
||||
# Uncomment this to avoid creating new processes
|
||||
# NO_EXTERNAL_CMD=1
|
||||
|
||||
SOURCES=../http/cookiefile.cpp \
|
||||
main.cpp \
|
||||
../measurement/wsdownloadtask.cpp \
|
||||
utils.cpp \
|
||||
cliclient.cpp
|
||||
|
||||
ifeq ($(SERVER),1)
|
||||
SOURCES += ../server/measurementserver.cpp \
|
||||
../server/ticketclient.cpp
|
||||
CXXFLAGS += -DRUN_SERVER
|
||||
else
|
||||
CLEAN += ../server/measurementserver.o ../server/ticketclient.o
|
||||
endif
|
||||
|
||||
ifeq ($(NO_EXTERNAL_CMD),1)
|
||||
CXXFLAGS += -DNO_EXTERNAL_CMD
|
||||
endif
|
||||
|
||||
include $(DIRLEVEL)/measurement/mk.inc
|
||||
455
src/cli/cliclient.cpp
Normal file
455
src/cli/cliclient.cpp
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <winsock2.h>
|
||||
#include <windows.h>
|
||||
#include <iso646.h>
|
||||
#include <stdio.h>
|
||||
#include <io.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <iomanip>
|
||||
#include <fstream>
|
||||
|
||||
#include "../json11/json11.hpp"
|
||||
#include "cliclient.h"
|
||||
|
||||
CliClient::CliClient(const TaskConfig &config) :
|
||||
Logger("CLI"),
|
||||
out(&std::cout),
|
||||
the_config(config) {
|
||||
|
||||
//setlocale(LC_ALL, "");
|
||||
|
||||
#ifdef _WIN32
|
||||
out_is_tty = _isatty(_fileno(stdout));
|
||||
#else
|
||||
out_is_tty = isatty(fileno(stdout));
|
||||
#endif
|
||||
out_quiet = (the_config.value("quiet") == "1" ||
|
||||
the_config.value("logfile") == "-");
|
||||
if (!the_config.value("out").empty()) {
|
||||
out = new std::ofstream(the_config.value("out"), std::ofstream::app);
|
||||
out_is_tty = false;
|
||||
}
|
||||
report.measurement_server = the_config.value("server");
|
||||
if (the_config.value("pingsweep") != "1" &&
|
||||
!the_config.hasKey("list_measurements")) {
|
||||
if (out_quiet) {
|
||||
// Block all output:
|
||||
out->clear(std::istream::eofbit);
|
||||
} else {
|
||||
*out << "Start: ";
|
||||
sayTime(*out);
|
||||
*out << ((the_config.value("mtype") == "ipv6") ? " [ipv6]\n" : "\n");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
try {
|
||||
current_line.imbue(std::locale(""));
|
||||
} catch (std::exception &e) {
|
||||
log() << "cannot set locale: " << e.what();
|
||||
}
|
||||
*/
|
||||
|
||||
if (the_config.value("logfile") == "-")
|
||||
out_is_tty = false; // Otherwise output might be garbled by the log.
|
||||
}
|
||||
|
||||
void CliClient::initialMsgToAgent(std::deque<std::string> &return_msgs) {
|
||||
return_msgs.push_back("{\"method\": \"clientReady\", \"args\": {}}");
|
||||
}
|
||||
|
||||
void CliClient::newEventFromAgent(std::deque<std::string> &return_msgs,
|
||||
const std::string &msg) {
|
||||
std::string jsonerr;
|
||||
auto obj = json11::Json::parse(msg, jsonerr);
|
||||
if (!jsonerr.empty()) {
|
||||
if (BridgeTask::isAgentTerminatedMessage(msg))
|
||||
std::cerr << msg << std::endl;
|
||||
else
|
||||
err_log() << "JSON error: got " << msg;
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
return;
|
||||
}
|
||||
|
||||
std::string event = obj["event"].string_value();
|
||||
auto arg_obj = obj["args"];
|
||||
|
||||
if (event == "setInfo" && !arg_obj["logText"].string_value().empty()) {
|
||||
log() << "EVENT: setInfo logText";
|
||||
} else
|
||||
log() << "EVENT: " << event << ' ' << msg;
|
||||
|
||||
if (event == "configuration") {
|
||||
if (!arg_obj["require_consent"].string_value().empty()) {
|
||||
// We need user consent to process personal data.
|
||||
// First fetch the legalese:
|
||||
json11::Json args = json11::Json::object {
|
||||
{ "lang", "en" },
|
||||
{ "format", "text" },
|
||||
{ "consent", arg_obj["require_consent"] },
|
||||
};
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("getContent", args.dump()));
|
||||
// When the content arrives, we'll show it to the user and
|
||||
// ask for consent.
|
||||
return;
|
||||
}
|
||||
report.isp = arg_obj["ispname"].string_value();
|
||||
if (!report.isp.empty())
|
||||
*out << "Network operator: " << report.isp << std::endl;
|
||||
|
||||
if (the_config.value("ssl") == "1")
|
||||
report.tls = 1;
|
||||
int server_port = std::stoi(the_config.value("port"));
|
||||
if (report.measurement_server.empty()) {
|
||||
std::vector<json11::Json> vec = arg_obj["servers"].array_items();
|
||||
for (auto srv : vec) {
|
||||
if (srv["type"].string_value() == the_config.value("mtype")) {
|
||||
std::string hostname = srv["url"].string_value();
|
||||
auto pos = hostname.find(':');
|
||||
if (the_config.value("mtype") != "ipv6" &&
|
||||
pos != std::string::npos) {
|
||||
server_port = std::stoi(hostname.substr(pos+1));
|
||||
hostname.resize(pos);
|
||||
}
|
||||
if (report.tls) {
|
||||
if (int tlsport = srv["tlsport"].int_value()) {
|
||||
report.measurement_server = hostname;
|
||||
server_port = tlsport;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
report.measurement_server = hostname;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.measurement_server.empty()) {
|
||||
show_message("Error: no measurement server");
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
//*out << "Server: " << report.measurement_server << std::endl;
|
||||
std::string key = arg_obj["hashkey"].string_value();
|
||||
json11::Json out_args = json11::Json::object {
|
||||
{ "serverUrl", report.measurement_server },
|
||||
{ "serverPort", server_port },
|
||||
{ "userKey", key },
|
||||
{ "tls", report.tls },
|
||||
};
|
||||
if (the_config.value("pingsweep") == "1")
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("pingSweep"));
|
||||
else
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("startTest", out_args.dump()));
|
||||
} else if (event == "taskProgress") {
|
||||
std::string tst = arg_obj["task"].string_value();
|
||||
if (tst == "download" || tst == "upload" || tst == "uploadinfo") {
|
||||
double val = arg_obj["result"].number_value();
|
||||
do_output(val, " Mbit/s", false);
|
||||
}
|
||||
} else if (event == "taskStart") {
|
||||
std::string tst = arg_obj["task"].string_value();
|
||||
if (tst == "download") {
|
||||
setHeader("Download: ");
|
||||
} else if (tst == "upload") {
|
||||
setHeader("Upload: ");
|
||||
}
|
||||
} else if (event == "taskComplete") {
|
||||
std::string tst = arg_obj["task"].string_value();
|
||||
if (tst == "global") {
|
||||
if (out_quiet) {
|
||||
// Enable output:
|
||||
out->clear(std::istream::goodbit);
|
||||
|
||||
if (the_config.value("quiet") != "1")
|
||||
*out << "\n\nRESULT: ";
|
||||
|
||||
std::string limiter = the_config.value("limiter", " ");
|
||||
|
||||
*out << report.download << limiter
|
||||
<< report.upload << limiter
|
||||
<< report.latency << limiter
|
||||
<< report.measurement_server << limiter
|
||||
<< report.isp << limiter
|
||||
<< report.ticket << limiter
|
||||
<< measurement_id;
|
||||
if (report.rating.empty())
|
||||
*out << std::endl;
|
||||
else
|
||||
*out << limiter << report.rating << std::endl;
|
||||
}
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("quit"));
|
||||
} else if (tst == "latency") {
|
||||
auto res = arg_obj["result"].number_value();
|
||||
report.latency = res;
|
||||
if (in_progress_task) {
|
||||
deferred_latency = true;
|
||||
} else {
|
||||
setHeader("Latency: ");
|
||||
do_output(report.latency, " ms", true);
|
||||
}
|
||||
} else if (tst == "download") {
|
||||
in_progress_task = false;
|
||||
auto res = arg_obj["result"].number_value();
|
||||
report.download = res;
|
||||
do_output(res, " Mbit/s", true);
|
||||
} else if (tst == "upload") {
|
||||
in_progress_task = false;
|
||||
auto res = arg_obj["result"].number_value();
|
||||
report.upload = res;
|
||||
do_output(res, " Mbit/s", true);
|
||||
if (deferred_latency) {
|
||||
*out << "Latency: " << std::flush;
|
||||
do_output(report.latency, " ms", true);
|
||||
}
|
||||
// Since Measure.AutoSaveReport is false, we tell the agent we're done.
|
||||
// Any information can be sent in the args object.
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("saveReport", "{}"));
|
||||
}
|
||||
} else if (event == "agentReady") {
|
||||
// Translate from "user-friendly" option name to agent's name:
|
||||
static const std::map<std::string, std::string> configMap {
|
||||
{ "speedlimit", "Measure.SpeedLimit" },
|
||||
{ "duration", "Measure.LoadDuration" },
|
||||
{ "iptype", "Measure.IpType" },
|
||||
{ "hashkey", "Client.hashkey" }
|
||||
};
|
||||
|
||||
std::map<std::string, std::string> newConfig;
|
||||
if (!savedOptions) {
|
||||
savedOptions = true;
|
||||
auto range = the_config.range("configure");
|
||||
for (auto newp = range.first; newp != range.second; ++newp) {
|
||||
std::string opt = newp->second;
|
||||
auto pos = opt.find('=');
|
||||
std::string attr = opt.substr(0, pos);
|
||||
auto pp = configMap.find(attr);
|
||||
if (pp != configMap.end()) {
|
||||
std::string value = (pos == std::string::npos) ? "1" :
|
||||
opt.substr(pos+1);
|
||||
newConfig[pp->second] = value;
|
||||
}
|
||||
}
|
||||
if (!newConfig.empty()) {
|
||||
json11::Json args = newConfig;
|
||||
auto mesg = BridgeTask::msgToAgent("saveConfigurationOption",
|
||||
args.dump());
|
||||
return_msgs.push_back(mesg);
|
||||
// Let the agent send agentReady again, with updated options:
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("clientReady"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Loop over saved configuration options:
|
||||
for (auto &p : arg_obj.object_items()) {
|
||||
std::string attr = p.first;
|
||||
std::string value = p.second.string_value();
|
||||
if (the_config.value(attr) != "override")
|
||||
newConfig[attr] = value;
|
||||
}
|
||||
if (!newConfig.empty()) {
|
||||
json11::Json args = newConfig;
|
||||
auto mesg = BridgeTask::msgToAgent("setConfigurationOption",
|
||||
args.dump());
|
||||
return_msgs.push_back(mesg);
|
||||
}
|
||||
|
||||
if (the_config.hasKey("list_measurements")) {
|
||||
std::map<std::string, std::string> pars;
|
||||
pars["max"] = the_config.value("list_measurements");
|
||||
if (the_config.hasKey("list_from"))
|
||||
pars["from"] = the_config.value("list_from");
|
||||
// pars["key"] = ...
|
||||
json11::Json args = json11::Json(pars);
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("listMeasurements",
|
||||
args.dump()));
|
||||
} else {
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("getConfiguration"));
|
||||
}
|
||||
} else if (event == "measurementList") {
|
||||
std::vector<json11::Json> vec = arg_obj["measurements"].array_items();
|
||||
if (out_quiet) {
|
||||
*out << arg_obj.string_value() << std::endl;
|
||||
} else if (vec.empty()) {
|
||||
*out << "No measurements found." << std::endl;
|
||||
} else {
|
||||
// {"id":83310,"down":113.994,"up":58.6238,"latency":6.28596,"server":"Stockholm","isp":"Stiftelsen for Internetinfrastruktur","ts":1519736433}
|
||||
*out << "ID\tDownload\tUpload\tLatency\tServer\tISP\tDate\n";
|
||||
for (auto &m : vec) {
|
||||
time_t ts = static_cast<time_t>(m["ts"].number_value());
|
||||
unsigned long id = static_cast<unsigned long>(m["id"].number_value());
|
||||
if (id && ts)
|
||||
*out << id << "\t"
|
||||
<< m["down"].number_value() << "\t"
|
||||
<< m["up"].number_value() << "\t"
|
||||
<< m["latency"].number_value() << "\t"
|
||||
<< m["server"].string_value() << "\t"
|
||||
<< m["isp"].string_value() << "\t"
|
||||
<< dateString(ts) << "\n";
|
||||
}
|
||||
long n = static_cast<long>(arg_obj["remaining"].number_value());
|
||||
if (n > 0)
|
||||
*out << n << " older measurements\n";
|
||||
}
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
} else if (event == "report") {
|
||||
auto res = arg_obj["subscription"];
|
||||
if (res["status"].int_value() != 1)
|
||||
return;
|
||||
std::string isp = res["ispOperator"].string_value();
|
||||
if (!isp.empty() && isp != report.isp)
|
||||
*out << "Service provider: " << isp << std::endl;
|
||||
report.msg = res["ispInfoMessage"].string_value();
|
||||
if (!report.msg.empty())
|
||||
*out << "Message from service provider: " << report.msg << std::endl;
|
||||
std::string subs = res["ispSpeedName"].string_value();
|
||||
if (subs.empty())
|
||||
return;
|
||||
*out << "Subscription: " << subs << std::endl;
|
||||
auto sinfo = arg_obj["subscription_info"];
|
||||
for (auto &p1 : sinfo.array_items()) {
|
||||
for (auto &p2 : p1["categories"].array_items()) {
|
||||
if (p2["description"].string_value() == subs) {
|
||||
try {
|
||||
std::string good = p2["good"].string_value(),
|
||||
acceptable = p2["acceptable"].string_value();
|
||||
if (good.empty() || acceptable.empty())
|
||||
return;
|
||||
if (report.download*1000 >= std::stod(good))
|
||||
report.rating = "GOOD";
|
||||
else if (report.download*1000 >= std::stod(acceptable))
|
||||
report.rating = "ACCEPTABLE";
|
||||
else
|
||||
report.rating = "BAD";
|
||||
*out << "The download result is "
|
||||
<< report.rating << std::endl;
|
||||
break;
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (report.rating == "BAD") {
|
||||
std::string bmsg = res["ispBadInfoMessage"].string_value();
|
||||
if (!bmsg.empty() && bmsg != report.msg)
|
||||
*out << "Message from service provider regarding the download "
|
||||
<< "result: " << bmsg << std::endl;
|
||||
}
|
||||
} else if (event == "measurementInfo") {
|
||||
measurement_id = arg_obj["MeasurementID"].string_value();
|
||||
if (!measurement_id.empty())
|
||||
*out << "Measurement ID: " << measurement_id << std::endl;
|
||||
std::string imsg = arg_obj["ispInfoMessage"].string_value();
|
||||
if (!imsg.empty() && imsg != report.msg)
|
||||
*out << "Message from service provider: " << imsg << std::endl;
|
||||
} else if (event == "setInfo") {
|
||||
for (auto &p : arg_obj.object_items()) {
|
||||
std::string attr = p.first;
|
||||
std::string value = p.second.string_value();
|
||||
if (attr == "error") {
|
||||
if (!value.empty()) {
|
||||
std::string ecode = arg_obj["errno"].string_value();
|
||||
if (ecode.empty())
|
||||
*out << "fatal error: " << value << std::endl;
|
||||
else
|
||||
*out << "fatal error: " << value << " (error code "
|
||||
<< ecode << ")" << std::endl;
|
||||
break;
|
||||
}
|
||||
} else if (attr == "ticket") {
|
||||
report.ticket = value;
|
||||
*out << "Support ID: " << value << std::endl;
|
||||
} else if (attr == "contents") {
|
||||
json11::Json cobj = p.second;
|
||||
if (!cobj["consent"].string_value().empty() &&
|
||||
!cobj["body"].string_value().empty()) {
|
||||
show_message(cobj["body"].string_value());
|
||||
show_message("Type Y to accept, N to decline: ", false);
|
||||
std::string reply;
|
||||
std::getline(std::cin, reply);
|
||||
if (reply.find_first_of("Yy") != std::string::npos) {
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("getConfiguration",
|
||||
cobj.dump()));
|
||||
} else {
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
}
|
||||
} else {
|
||||
show_message("Server error.");
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
}
|
||||
} else if (attr == "logText") {
|
||||
} else if (attr == "approxLatency") {
|
||||
show_message("Response time: " + value);
|
||||
} else if (attr == "bestServer") {
|
||||
if (value.empty())
|
||||
show_message("error: no server available");
|
||||
else
|
||||
show_message("Closest server: " + value);
|
||||
return_msgs.push_back(BridgeTask::msgToAgent("terminate"));
|
||||
} else if (attr == "msgToUser") {
|
||||
show_message(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CliClient::show_message(const std::string &msg, bool linefeed) {
|
||||
if (out_quiet)
|
||||
out->clear(std::istream::goodbit);
|
||||
*out << msg;
|
||||
if (linefeed)
|
||||
*out << std::endl;
|
||||
else
|
||||
*out << std::flush;
|
||||
if (out_quiet)
|
||||
out->clear(std::istream::eofbit);
|
||||
}
|
||||
|
||||
void CliClient::do_output(double value, const char *msg, bool final) {
|
||||
if (out_is_tty) // Erase current line:
|
||||
*out << '\r';
|
||||
if (out_is_tty || final) {
|
||||
auto to_delete = current_line.str().size();
|
||||
current_line.str("");
|
||||
current_line << current_header << std::setw(10) << std::setprecision(3)
|
||||
<< std::fixed << value << msg;
|
||||
*out << current_line.str();
|
||||
if (to_delete > current_line.str().size())
|
||||
*out << std::string(to_delete - current_line.str().size(), ' ');
|
||||
*out << std::flush;
|
||||
}
|
||||
if (final) {
|
||||
current_line.str("");
|
||||
if (value <= 0)
|
||||
*out << " test failed";
|
||||
*out << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void CliClient::do_output(const char *msg, bool final) {
|
||||
if (out_is_tty) // Erase current line:
|
||||
*out << '\r';
|
||||
if (out_is_tty || final) {
|
||||
auto to_delete = current_line.str().size();
|
||||
current_line.str("");
|
||||
current_line << msg;
|
||||
*out << current_line.str();
|
||||
if (to_delete > current_line.str().size())
|
||||
*out << std::string(to_delete - current_line.str().size(), ' ');
|
||||
*out << std::flush;
|
||||
}
|
||||
if (final) {
|
||||
current_line.str("");
|
||||
*out << std::endl;
|
||||
}
|
||||
}
|
||||
54
src/cli/cliclient.h
Normal file
54
src/cli/cliclient.h
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <sstream>
|
||||
|
||||
#include "../framework/logger.h"
|
||||
#include "../framework/synchronousbridge.h"
|
||||
|
||||
class CliClient : public Logger, public SynchronousClient {
|
||||
public:
|
||||
CliClient(const TaskConfig &config);
|
||||
|
||||
void initialMsgToAgent(std::deque<std::string> &return_msgs) override;
|
||||
|
||||
// msg is a new message from the agent.
|
||||
// push any return messages onto return_msgs.
|
||||
virtual void newEventFromAgent(std::deque<std::string> &return_msgs,
|
||||
const std::string &msg) override;
|
||||
void setHeader(const std::string &hdr) {
|
||||
in_progress_task = true;
|
||||
*out << hdr << std::flush;
|
||||
current_header = hdr;
|
||||
}
|
||||
private:
|
||||
// To store the measurement details:
|
||||
struct {
|
||||
double latency, download, upload;
|
||||
std::string ticket, measurement_server, rating, isp, msg;
|
||||
int tls;
|
||||
} report = { -1.0, -1.0, -1.0, "no_support_ID", "", "", "", "", 0 };
|
||||
|
||||
// Stuff to handle output:
|
||||
void show_message(const std::string &msg, bool linefeed = true);
|
||||
void do_output(const char *msg, bool final = false);
|
||||
void do_output(double value, const char *msg, bool final = false);
|
||||
std::ostream *out;
|
||||
bool out_is_tty, out_quiet;
|
||||
std::ostringstream current_line;
|
||||
std::string current_header;
|
||||
std::string measurement_id;
|
||||
// Set to true during upload and download:
|
||||
bool in_progress_task = false;
|
||||
// It latency result arrives asynchronously during
|
||||
// upload or download, we must wait until the end to show it:
|
||||
bool deferred_latency = false;
|
||||
|
||||
// If user wants to set persistent options, we shoud save them only once.
|
||||
bool savedOptions = false;
|
||||
|
||||
const TaskConfig the_config;
|
||||
};
|
||||
56
src/cli/main.cpp
Normal file
56
src/cli/main.cpp
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#if defined(RUN_SERVER)
|
||||
#include "../server/measurementserver.h"
|
||||
#endif
|
||||
|
||||
#include "../http/httphost.h"
|
||||
#include "../http/websocketbridge.h"
|
||||
#include "../measurement/measurementagent.h"
|
||||
#include "utils.h"
|
||||
#include "cliclient.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
// Options for the measurement agent and the client (user interface):
|
||||
TaskConfig agent_cfg, config;
|
||||
|
||||
if (!parseArgs(argc, argv, config, agent_cfg))
|
||||
return 1;
|
||||
|
||||
std::ofstream log_file;
|
||||
config.openlog(log_file);
|
||||
if (!log_file) {
|
||||
std::cerr << "cannot write to log file" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
EventLoop loop;
|
||||
|
||||
#if defined(RUN_SERVER)
|
||||
if (config.value("run_server") == "1") {
|
||||
std::string srv_cfg = "listen " + config.value("listen");
|
||||
if (!config.value("Measure.LocalAddress").empty())
|
||||
(srv_cfg += ' ') += config.value("Measure.LocalAddress");
|
||||
loop.addTask(new MeasurementServer(srv_cfg));
|
||||
loop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
CookieFile cf(config.value("config_file"));
|
||||
HttpHost webserver(agent_cfg.value("Measure.Webserver"), 80, "", 0, &cf);
|
||||
MeasurementAgent *agent = new MeasurementAgent(agent_cfg, webserver);
|
||||
CliClient client(config);
|
||||
if (config.value("listen").empty()) {
|
||||
loop.addTask(new SynchronousBridge(agent, &client));
|
||||
} else {
|
||||
loop.addTask(new WebsocketBridge(agent, config));
|
||||
}
|
||||
|
||||
loop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
325
src/cli/utils.cpp
Normal file
325
src/cli/utils.cpp
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <direct.h>
|
||||
#define mkdir(x) _mkdir(x)
|
||||
#else
|
||||
#include <csignal>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
#include <cstdlib>
|
||||
#include <cerrno>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include "utils.h"
|
||||
#include "../framework/taskconfig.h"
|
||||
#include "../measurement/defs.h"
|
||||
#include "../http/sha1.h"
|
||||
#include "../framework/logger.h"
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
#ifdef _WIN32
|
||||
std::string pathSep = "\\";
|
||||
#else
|
||||
std::string pathSep = "/";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string createAndGetAppDir(std::string dir) {
|
||||
std::string home;
|
||||
#ifdef _WIN32
|
||||
if (dir.empty()) {
|
||||
if (!std::getenv("HOMEDRIVE") || !std::getenv("HOMEPATH"))
|
||||
return "";
|
||||
home = std::string(std::getenv("HOMEDRIVE")) + std::getenv("HOMEPATH");
|
||||
dir = home + "\\.bredbandskollen";
|
||||
}
|
||||
_mkdir(dir.c_str());
|
||||
#else
|
||||
if (dir.empty()) {
|
||||
if (!std::getenv("HOME"))
|
||||
return "";
|
||||
home = std::getenv("HOME");
|
||||
dir = home + "/.bredbandskollen";
|
||||
}
|
||||
int status = mkdir(dir.c_str(), 0755);
|
||||
if (status && errno != EEXIST) {
|
||||
return "";
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef IS_SANDBOXED
|
||||
if (!home.empty())
|
||||
chdir(home.c_str());
|
||||
#endif
|
||||
|
||||
return dir + pathSep;
|
||||
}
|
||||
|
||||
enum class CliMode { NONE, LIVE, TEST, LOCAL,
|
||||
#if defined(RUN_SERVER)
|
||||
SERVER,
|
||||
#endif
|
||||
IN_ERROR } ;
|
||||
|
||||
bool parseArgs(int argc, char *argv[],
|
||||
TaskConfig &client_cfg, TaskConfig &agent_cfg) {
|
||||
|
||||
CliMode mode = CliMode::NONE;
|
||||
|
||||
client_cfg.set("port", "80");
|
||||
client_cfg.set("mtype", "ipv4");
|
||||
client_cfg.set("listen_addr", "127.0.0.1");
|
||||
|
||||
agent_cfg.add("Measure.Webserver", "frontend.bredbandskollen.se");
|
||||
agent_cfg.add("Measure.SettingsUrl", "/api/servers");
|
||||
agent_cfg.add("Measure.ContentsUrl", "/api/content");
|
||||
agent_cfg.add("Measure.MeasurementsUrl", "/api/measurements");
|
||||
|
||||
for (int i=1; i<argc; ++i) {
|
||||
std::string arg(argv[i]);
|
||||
if (arg == "--v6") {
|
||||
client_cfg.set("mtype", "ipv6");
|
||||
client_cfg.set("Measure.IpType", "override");
|
||||
} else if (arg == "--v4") {
|
||||
client_cfg.set("mtype", "ipv4");
|
||||
client_cfg.set("Measure.IpType", "override");
|
||||
} else if (arg == "--test") {
|
||||
mode = (mode == CliMode::NONE) ? CliMode::TEST : CliMode::IN_ERROR;
|
||||
} else if (arg == "--live") {
|
||||
mode = (mode == CliMode::NONE) ? CliMode::LIVE : CliMode::IN_ERROR;
|
||||
} else if (arg == "--version") {
|
||||
std::cout << measurement::appName << ' '
|
||||
<< measurement::appVersion << '\n';
|
||||
return false;
|
||||
} else if (arg == "--quiet") {
|
||||
client_cfg.set("quiet", "1");
|
||||
} else if (arg == "--csv") {
|
||||
client_cfg.set("quiet", "1");
|
||||
client_cfg.set("limiter", ",");
|
||||
} else if (arg == "--local") {
|
||||
mode = (mode == CliMode::NONE) ? CliMode::LOCAL : CliMode::IN_ERROR;
|
||||
#if defined(RUN_SERVER)
|
||||
} else if (arg == "--run-server") {
|
||||
mode = (mode == CliMode::NONE) ? CliMode::SERVER : CliMode::IN_ERROR;
|
||||
#endif
|
||||
} else if (arg.substr(0, 11) == "--duration=") {
|
||||
agent_cfg.set("Measure.LoadDuration", argv[i]+11);
|
||||
client_cfg.set("Measure.LoadDuration", "override");
|
||||
} else if (arg.substr(0, 13) == "--speedlimit=") {
|
||||
agent_cfg.set("Measure.SpeedLimit", argv[i]+13);
|
||||
client_cfg.set("Measure.SpeedLimit", "override");
|
||||
} else if (arg.substr(0, 6) == "--out=")
|
||||
client_cfg.set("out", argv[i]+6);
|
||||
else if (arg.substr(0, 6) == "--dir=")
|
||||
client_cfg.set("app_dir", (argv[i]+6) + pathSep);
|
||||
else if (arg.substr(0, 6) == "--log=")
|
||||
client_cfg.set("logfile", argv[i]+6);
|
||||
else if (arg.substr(0, 11) == "--local-ip=")
|
||||
agent_cfg.set("Measure.LocalAddress", argv[i]+11);
|
||||
else if (arg.substr(0, 9) == "--server=")
|
||||
client_cfg.set("server", argv[i]+9);
|
||||
else if (arg.substr(0, 7) == "--port=")
|
||||
client_cfg.set("port", argv[i]+7);
|
||||
else if (arg.substr(0, 12) == "--configure=")
|
||||
client_cfg.add("configure", argv[i]+12);
|
||||
else if (arg.substr(0, 9) == "--listen=")
|
||||
client_cfg.set("listen", argv[i]+9);
|
||||
else if (arg.substr(0, 14) == "--listen-addr=")
|
||||
client_cfg.set("listen_addr", argv[i]+14);
|
||||
else if (arg.substr(0, 12) == "--listen-pw=") {
|
||||
client_cfg.set("listen_pw", argv[i]+12);
|
||||
#ifdef USE_GNUTLS
|
||||
} else if (arg == "--ssl") {
|
||||
agent_cfg.set("Measure.TLS", "1");
|
||||
client_cfg.set("ssl", "1");
|
||||
if (client_cfg.value("port") == "80")
|
||||
client_cfg.set("port", "443");
|
||||
#endif
|
||||
} else if (arg.substr(0, 9) == "--fakeip=")
|
||||
agent_cfg.set("Client.fakeip", argv[i]+9);
|
||||
else if (arg == "--check-servers")
|
||||
client_cfg.set("pingsweep", "1");
|
||||
else if (arg.substr(0, 14) == "--measurements")
|
||||
client_cfg.set("list_measurements",
|
||||
(arg.size() > 15 && arg[14] == '=') ? argv[i]+15 : "10");
|
||||
else if (arg.substr(0, 10) == "--from-id=") {
|
||||
client_cfg.set("list_from", argv[i]+10);
|
||||
if (client_cfg.value("list_measurements").empty())
|
||||
client_cfg.set("list_measurements", "10");
|
||||
} else if (arg == "--browser") {
|
||||
client_cfg.set("browser", "1");
|
||||
if (client_cfg.value("listen").empty())
|
||||
client_cfg.set("listen", "0"); // Use any avaliable port
|
||||
} else if (arg.substr(0, 13) == "--proxy-host=")
|
||||
agent_cfg.set("Measure.ProxyServerUrl", argv[i]+13);
|
||||
else if (arg.substr(0, 13) == "--proxy-port=")
|
||||
agent_cfg.set("Measure.ProxyServerPort", argv[i]+13);
|
||||
else {
|
||||
int status = 0;
|
||||
if (arg != "--help") {
|
||||
status = 1;
|
||||
std::cerr << argv[0] << ": invalid argument -- " << arg << std::endl;
|
||||
}
|
||||
std::ostream &fh = status ? std::cerr : std::cout;
|
||||
fh << "Usage: " << argv[0] << " [OPTION]...\n\nOptions:\n\n"
|
||||
" --help Show this help text\n"
|
||||
" --version Print version number and exit\n"
|
||||
<< "\nNetwork related options:\n"
|
||||
#ifndef BBK_WEBVIEW
|
||||
<< " --v6 Prefer IPv6 (default is IPv4)\n"
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#else
|
||||
<< " --local-ip=IP Measure using existing local ip address IP\n"
|
||||
<< " Note: this will not work on all platforms\n"
|
||||
#endif
|
||||
<< " --proxy-host=HOST Use HTTP proxy server HOST\n"
|
||||
<< " --proxy-port=PORT Use port PORT on proxy server (default 80)\n"
|
||||
<< "\nMeasurement configuration:\n"
|
||||
#ifndef BBK_WEBVIEW
|
||||
<< " --server=HOST Use HOST as measurement server\n"
|
||||
<< " --port=N Port number for measurement server, default 80\n"
|
||||
#endif
|
||||
#ifdef USE_GNUTLS
|
||||
<< " --ssl Measure using transport layer security (default port 443)\n"
|
||||
#endif
|
||||
<< " --duration=N Measure upload/download for N seconds (2-10, default 10)\n"
|
||||
<< " --speedlimit=N Keep upload/download speed below N mbps on average\n"
|
||||
<< "\nMeasurement type:\n"
|
||||
<< " --live Measure using Bredbandskollen's live servers (default)\n"
|
||||
<< " --test Measure using Bredbandskollen's development servers\n"
|
||||
#ifndef BBK_WEBVIEW
|
||||
<< " --local Don't fetch configuration (server list) from bredbandskollen.se,\n"
|
||||
<< " communicate only with server given by the --server option.\n"
|
||||
#endif
|
||||
#if defined(RUN_SERVER)
|
||||
<< " --run-server Run as a measurement server (requires option --listen=PORT)\n"
|
||||
#endif
|
||||
<< "\nLogging:\n"
|
||||
<< " --log=FILENAME Write debug log to FILENAME\n"
|
||||
<< " (log to stderr if FILENAME is -)\n"
|
||||
#ifndef BBK_WEBVIEW
|
||||
<< "\nFinding measurement servers:\n"
|
||||
<< " --check-servers Find closest measurement server\n"
|
||||
<< "\nList previous measurements:\n"
|
||||
<< " --measurements List 10 last measurements\n"
|
||||
<< " --measurements=N List N last measurements\n"
|
||||
<< " If --quiet, output will be JSON. Otherwise\n"
|
||||
<< " output will be lines with tab separated fields.\n"
|
||||
<< " --from-id=N List only measurements before ID N\n"
|
||||
<< "\nBrowser interface:\n"
|
||||
<< " --browser Use a web browser as interface\n"
|
||||
<< " --listen=PORT Use web browser as interface;\n"
|
||||
<< " the browser must connect to the given PORT\n"
|
||||
<< " --listen-addr=IP When listening, bind socket to ip address IP\n"
|
||||
<< " (default is 127.0.0.1) to use a web browser on\n"
|
||||
<< " a remote host as interface\n"
|
||||
<< " Note: this may not work due to e.g. firewalls.\n"
|
||||
<< " Don't use it unless you know what you are doing.\n"
|
||||
<< " --listen-pw=PW Use PW as a one-time password when connecting from browser\n"
|
||||
<< " Note: DO NOT reuse a sensitive password here!\n"
|
||||
<< " It is better to omit this option because by default\n"
|
||||
<< " a secure one-time password will be generated.\n"
|
||||
<< "\nCommand line interface:\n"
|
||||
<< " --quiet Write a single line of output\n"
|
||||
<< " --csv Write a single line of output, comma separated\n"
|
||||
<< " --out=FILENAME Append output to FILENAME instead of stdout\n"
|
||||
#endif
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
client_cfg.set("app_dir", createAndGetAppDir(client_cfg.value("app_dir")));
|
||||
|
||||
if (client_cfg.value("local") == "1" && client_cfg.value("server").empty()) {
|
||||
std::cerr << "missing --server option" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> pdir = { "listen", "port" };
|
||||
for (auto &str : pdir)
|
||||
if (!client_cfg.value(str).empty()) {
|
||||
auto port = client_cfg.value(str);
|
||||
if (port.find_first_not_of("0123456789") != std::string::npos ||
|
||||
port.size() > 5 || std::stod(port) > 65535) {
|
||||
std::cerr << "invalid port number" << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case CliMode::NONE:
|
||||
case CliMode::LIVE:
|
||||
agent_cfg.set("Measure.Webserver", "frontend.bredbandskollen.se");
|
||||
break;
|
||||
case CliMode::TEST:
|
||||
agent_cfg.set("Measure.Webserver", "frontend-beta.bredbandskollen.se");
|
||||
break;
|
||||
case CliMode::LOCAL:
|
||||
client_cfg.set("local", "1");
|
||||
agent_cfg.set("Measure.Webserver", "none");
|
||||
break;
|
||||
#if defined(RUN_SERVER)
|
||||
case CliMode::SERVER:
|
||||
client_cfg.set("local", "1");
|
||||
client_cfg.set("run_server", "1");
|
||||
if (client_cfg.value("listen").empty()) {
|
||||
std::cerr << "option --listen is required with --run-server"
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case CliMode::IN_ERROR:
|
||||
std::cerr << "can have only one of options --live, --test,";
|
||||
#if defined(RUN_SERVER)
|
||||
std::cerr << " --run-server,";
|
||||
#endif
|
||||
std::cerr << " and --local";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!client_cfg.value("listen").empty() &&
|
||||
client_cfg.value("listen_pw").empty()) {
|
||||
client_cfg.add("listen_pw", Logger::createHashKey(12));
|
||||
}
|
||||
client_cfg.add("url", "http://" + agent_cfg.value("Measure.Webserver") +
|
||||
"/standalone/dev/index.php");
|
||||
|
||||
if (client_cfg.value("logfile").empty()) {
|
||||
#if defined(RUN_SERVER)
|
||||
if (mode == CliMode::SERVER)
|
||||
client_cfg.add("logfile", client_cfg.value("app_dir") + "server_log");
|
||||
else
|
||||
#endif
|
||||
client_cfg.add("logfile", client_cfg.value("app_dir") + "last_log");
|
||||
}
|
||||
client_cfg.set("config_file", client_cfg.value("app_dir") + "config");
|
||||
agent_cfg.set("options_file", client_cfg.value("app_dir") + "ConfigOptions.txt");
|
||||
|
||||
// Default to ipv6 if user wants to use a local ipv6 address
|
||||
if (agent_cfg.value("Measure.LocalAddress").find(':') !=
|
||||
std::string::npos) {
|
||||
client_cfg.set("mtype", "ipv6");
|
||||
client_cfg.set("Measure.IpType", "override");
|
||||
}
|
||||
|
||||
agent_cfg.add("Measure.AutoSaveReport",
|
||||
client_cfg.value("listen").empty() ? "false" : "true");
|
||||
|
||||
agent_cfg.add("Measure.IpType", client_cfg.value("mtype"));
|
||||
|
||||
agent_cfg.add("Client.appname", measurement::appName);
|
||||
agent_cfg.add("Client.appver", measurement::appVersion);
|
||||
agent_cfg.add("Client.machine", measurement::hw_info);
|
||||
agent_cfg.add("Client.system", measurement::os_version);
|
||||
agent_cfg.add("Client.language", "en");
|
||||
|
||||
return true;
|
||||
}
|
||||
11
src/cli/utils.h
Normal file
11
src/cli/utils.h
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
class TaskConfig;
|
||||
|
||||
std::string createAndGetAppDir(std::string dir = "");
|
||||
|
||||
bool parseArgs(int argc, char *argv[],
|
||||
TaskConfig &client_cfg, TaskConfig &agent_cfg);
|
||||
10
src/examples/.gitignore
vendored
Normal file
10
src/examples/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
boss
|
||||
cliclient
|
||||
client
|
||||
echoclient
|
||||
echoserver
|
||||
threadserver
|
||||
timers
|
||||
webserver
|
||||
winner
|
||||
winnerclient
|
||||
25
src/examples/001_single_request/Makefile
Normal file
25
src/examples/001_single_request/Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Name of the executable program to be built:
|
||||
TARGET = client
|
||||
|
||||
# Relative path to where the framework directory is located:
|
||||
DIRLEVEL = ../..
|
||||
|
||||
# Possible LOGLEVEL values: dbg, info, warn, err, none
|
||||
LOGLEVEL = dbg
|
||||
|
||||
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
|
||||
GNUTLS = 0
|
||||
|
||||
# Set to 1 to be able to run tasks in different threads.
|
||||
THREADS = 0
|
||||
|
||||
# All C++ source files used by the target program
|
||||
SOURCES = main.cpp
|
||||
|
||||
# To be able to debug the target program
|
||||
CXXFLAGS += -g
|
||||
|
||||
# Include the HTTP support files. It will also include the base framework,
|
||||
# i.e. $(DIRLEVEL)/framework/mk.inc
|
||||
# The below line should be the last one in the Makefile.
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
18
src/examples/001_single_request/main.cpp
Normal file
18
src/examples/001_single_request/main.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#include <http/singlerequest.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 001
|
||||
|
||||
Fetch http://frontend.bredbandskollen.se/api/servers, then exit.
|
||||
Write debug log to stderr.
|
||||
|
||||
*/
|
||||
|
||||
int main(int , char *[]) {
|
||||
Task *t = new SingleRequest("MyRequest",
|
||||
"frontend.bredbandskollen.se",
|
||||
"/api/servers");
|
||||
EventLoop::runTask(t);
|
||||
// Note: the object t will be deleted by the event loop.
|
||||
return 0;
|
||||
}
|
||||
25
src/examples/002_simple_task/Makefile
Normal file
25
src/examples/002_simple_task/Makefile
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# Name of the executable program to be built:
|
||||
TARGET = client
|
||||
|
||||
# Relative path to where the framework directory is located:
|
||||
DIRLEVEL = ../..
|
||||
|
||||
# Possible LOGLEVEL values: dbg, info, warn, err, none
|
||||
LOGLEVEL = dbg
|
||||
|
||||
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
|
||||
GNUTLS = 0
|
||||
|
||||
# All C++ source files used by the target program
|
||||
SOURCES = main.cpp
|
||||
|
||||
# To be able to debug the target program
|
||||
CXXFLAGS += -g
|
||||
|
||||
# Extra files to be removed by "make clean":
|
||||
CLEAN += log.txt
|
||||
|
||||
# Include the HTTP support files. It will also include the base framework,
|
||||
# i.e. $(DIRLEVEL)/framework/mk.inc
|
||||
# The below line should be the last one in the Makefile.
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
77
src/examples/002_simple_task/main.cpp
Normal file
77
src/examples/002_simple_task/main.cpp
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#include <http/singlerequest.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 002
|
||||
|
||||
Fetch http://frontend.bredbandskollen.se/api/servers.
|
||||
Write the result to stdout.
|
||||
Write debug log to log.txt.
|
||||
|
||||
*/
|
||||
|
||||
class MainTask : public Task {
|
||||
public:
|
||||
|
||||
// Each task object has a name (or "label"), which is used (for example)
|
||||
// by the logger.
|
||||
MainTask(const std::string &name) : Task(name) {
|
||||
// Note: the constructor will be executed before the task has been added
|
||||
// to the eventloop.
|
||||
// DO NOT perform any actions that concern the eventloop here!
|
||||
// Instead, most of the initialisation should be performed from within
|
||||
// the start() method, which will be called by the eventloop when the
|
||||
// execution of this task starts.
|
||||
}
|
||||
|
||||
double start() override {
|
||||
dbg_log() << "starting";
|
||||
|
||||
// If child tasks still exist when this task is done,
|
||||
// we want them to be removed:
|
||||
killChildTaskWhenFinished();
|
||||
|
||||
// The second parameter to addNewTask sets this task as
|
||||
// parent of the new task.
|
||||
addNewTask(new SingleRequest("MyRequest",
|
||||
"frontend.bredbandskollen.se",
|
||||
"/api/servers"), this);
|
||||
|
||||
// First timer is to be called after 10.0 seconds.
|
||||
// If we return <= 0, no timer will be added.
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
double timerEvent() override {
|
||||
std::cerr << "Timeout after " << elapsed() << " seconds";
|
||||
|
||||
// Tell the eventloop that this task has given up:
|
||||
setTimeout();
|
||||
|
||||
// Return number of seconds until this method should be
|
||||
// called again, or <= 0 if you don't want it to be called again.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Will be called when a child task is finished:
|
||||
void taskFinished(Task *task) override {
|
||||
log() << task->label() << " finished, ok=" << task->finishedOK();
|
||||
|
||||
// Our only child task is a SingleRequest task, so the below cast will
|
||||
// succeed. We need the cast to be able to call httpStatus().
|
||||
if (SingleRequest *req = dynamic_cast<SingleRequest *>(task)) {
|
||||
log() << "Status: " << req->httpStatus();
|
||||
std::cout << "Result: " << req->result() << std::endl;
|
||||
// Calling setResult tells the eventloop that this task is done.
|
||||
setResult("OK");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main(int , char *[]) {
|
||||
std::ofstream log("log.txt");
|
||||
// Note: the log file object must not be destroyed until either the
|
||||
// eventloop is finished, or until setLogFile is called again.
|
||||
Logger::setLogFile(log);
|
||||
EventLoop::runTask(new MainTask("Main Task"));
|
||||
return 0;
|
||||
}
|
||||
7
src/examples/003_webserver/Makefile
Normal file
7
src/examples/003_webserver/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TARGET = webserver
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = info
|
||||
GNUTLS = 0
|
||||
SOURCES = main.cpp
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
55
src/examples/003_webserver/main.cpp
Normal file
55
src/examples/003_webserver/main.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#include <http/webservertask.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 003
|
||||
|
||||
Start a webserver on port 8080. Write log to stderr.
|
||||
From a web browser, you can retrieve the URLs
|
||||
|
||||
http://127.0.0.1:8080/getTime
|
||||
http://127.0.0.1:8080/getStats
|
||||
http://127.0.0.1:8080/shutdown
|
||||
|
||||
*/
|
||||
|
||||
class WebServer : public WebServerTask {
|
||||
public:
|
||||
WebServer(const std::string &cfg) :
|
||||
WebServerTask("WebServer", cfg) {
|
||||
}
|
||||
|
||||
HttpState newGetRequest(HttpServerConnection *,
|
||||
const std::string &uri) override;
|
||||
private:
|
||||
unsigned long tot_no_requests = 0;
|
||||
};
|
||||
|
||||
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
|
||||
const std::string &uri) {
|
||||
++tot_no_requests;
|
||||
log() << "URI: " << uri << " #" << tot_no_requests;
|
||||
if (uri == "/getTime")
|
||||
// Send current time
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
|
||||
else if (uri == "/getStats")
|
||||
// Send number of requests since server was started.
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain",
|
||||
std::to_string(tot_no_requests));
|
||||
else if (uri == "/shutdown") {
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain",
|
||||
"server shutdown");
|
||||
// This will terminate the task, i.e. shut the server down. Normally, of
|
||||
// course, clients wouldn't be able to do this on a production server.
|
||||
setResult("");
|
||||
} else
|
||||
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
|
||||
"unknown service");
|
||||
return HttpState::WAITING_FOR_REQUEST;
|
||||
}
|
||||
|
||||
int main(int , char *[]) {
|
||||
// Listen on port 8080. To listen on the standard port 80, we'd have to
|
||||
// run as a privileged user.
|
||||
EventLoop::runTask(new WebServer("listen 8080"));
|
||||
return 0;
|
||||
}
|
||||
22
src/examples/004_timers/Makefile
Normal file
22
src/examples/004_timers/Makefile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Name of the executable program to be built:
|
||||
TARGET = timers
|
||||
|
||||
# Relative path to where the framework directory is located:
|
||||
DIRLEVEL = ../..
|
||||
|
||||
# Possible LOGLEVEL values: dbg, info, warn, err, none
|
||||
LOGLEVEL = dbg
|
||||
|
||||
# Set to 1 to link with GnuTLS, i.e. to enable SSL support.
|
||||
GNUTLS = 0
|
||||
|
||||
# All C++ source files used by the target program
|
||||
SOURCES = main.cpp
|
||||
|
||||
# To be able to debug the target program
|
||||
CXXFLAGS += -g
|
||||
|
||||
# Include the HTTP support files. It will also include the base framework,
|
||||
# i.e. $(DIRLEVEL)/framework/mk.inc
|
||||
# The below line should be the last one in the Makefile.
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
45
src/examples/004_timers/main.cpp
Normal file
45
src/examples/004_timers/main.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#include <framework/task.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 004
|
||||
|
||||
Simple demonstration of tasks and timers.
|
||||
Write log to stderr.
|
||||
|
||||
*/
|
||||
|
||||
class PointlessTask : public Task {
|
||||
public:
|
||||
|
||||
PointlessTask(const std::string &name,
|
||||
double tick_length, unsigned int no_ticks) :
|
||||
Task(name),
|
||||
tick_duration(tick_length),
|
||||
ticks(no_ticks), curr_tick(0) {
|
||||
}
|
||||
|
||||
double start() override {
|
||||
dbg_log() << "starting, " << ticks << " ticks";
|
||||
return tick_duration;
|
||||
}
|
||||
|
||||
double timerEvent() override {
|
||||
log() << "timerEvent " << ++curr_tick << " of " << ticks;
|
||||
if (curr_tick < ticks)
|
||||
return tick_duration;
|
||||
setResult("Done after " + std::to_string(elapsed()) + " seconds");
|
||||
return 0.0;
|
||||
}
|
||||
private:
|
||||
double tick_duration;
|
||||
unsigned int ticks, curr_tick;
|
||||
};
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop loop;
|
||||
loop.addTask(new PointlessTask("Pointless 1", 1.0, 3));
|
||||
loop.addTask(new PointlessTask("Pointless 2", 0.7, 5));
|
||||
loop.addTask(new PointlessTask("Pointless 3", 0.5, 7));
|
||||
loop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
7
src/examples/005_websocket_server/Makefile
Normal file
7
src/examples/005_websocket_server/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TARGET = winner
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = info
|
||||
GNUTLS = 0
|
||||
SOURCES = main.cpp winner.cpp
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
36
src/examples/005_websocket_server/main.cpp
Normal file
36
src/examples/005_websocket_server/main.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include <framework/eventloop.h>
|
||||
#include "winner.h"
|
||||
|
||||
/* Example 005
|
||||
|
||||
Start a webserver on port 8080. Write log to stderr.
|
||||
The client can open a websocket connection on
|
||||
|
||||
ws://127.0.0.1:8080/winner
|
||||
|
||||
The client can then send text messages of the format
|
||||
|
||||
name score
|
||||
|
||||
and the server will remember the names and scores.
|
||||
If the client sends the text message "winner", the
|
||||
server will respond with the name with the highest score.
|
||||
|
||||
E.g. the client sends the five messages
|
||||
|
||||
Bill 12
|
||||
Steve 19
|
||||
Linus 33
|
||||
Ken 27
|
||||
winner
|
||||
|
||||
and the server responds
|
||||
|
||||
Linus
|
||||
|
||||
*/
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop::runTask(new Winner("listen 8080"));
|
||||
return 0;
|
||||
}
|
||||
43
src/examples/005_websocket_server/winner.cpp
Normal file
43
src/examples/005_websocket_server/winner.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include "winner.h"
|
||||
|
||||
bool Winner::newWsRequest(HttpServerConnection *,
|
||||
const std::string &uri) {
|
||||
log() << "websocket request to " << uri;
|
||||
|
||||
if (uri == "/winner")
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Winner::wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) {
|
||||
log() << "Got: " << msg;
|
||||
auto p = leader.find(conn);
|
||||
if (msg == "winner") {
|
||||
if (p != leader.end()) {
|
||||
log() << "Sending " << p->second.name;
|
||||
conn->sendWsMessage(p->second.name);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
auto pos = msg.find_last_of(" ");
|
||||
if (pos == std::string::npos || !pos || pos+1 == msg.size())
|
||||
return true;
|
||||
if (msg.find_last_not_of("0123456789") != pos)
|
||||
return true;
|
||||
auto score = std::stoul(msg.substr(pos+1));
|
||||
if (p == leader.end()) {
|
||||
leader[conn] = { msg.substr(0, pos), score };
|
||||
} else if (score > p->second.score) {
|
||||
p->second.score = score;
|
||||
p->second.name = msg.substr(0, pos);
|
||||
} else if (score == p->second.score) {
|
||||
(p->second.name += ", ") += msg.substr(0, pos);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Winner::connRemoved(SocketConnection *conn) {
|
||||
leader.erase(dynamic_cast<HttpConnection *>(conn));
|
||||
}
|
||||
30
src/examples/005_websocket_server/winner.h
Normal file
30
src/examples/005_websocket_server/winner.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <http/webservertask.h>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
struct Info {
|
||||
std::string name;
|
||||
unsigned long score;
|
||||
};
|
||||
|
||||
class Winner : public WebServerTask {
|
||||
public:
|
||||
Winner(const std::string &cfg) :
|
||||
WebServerTask("Winner", cfg) {
|
||||
}
|
||||
|
||||
// Client want's to open a websocket connection on uri.
|
||||
// Return true to accept, false to close the connection.
|
||||
bool newWsRequest(HttpServerConnection *conn,
|
||||
const std::string &uri) override;
|
||||
|
||||
// When a text message is sent from the client, we'll be notified here:
|
||||
bool wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
|
||||
void connRemoved(SocketConnection *conn) override;
|
||||
private:
|
||||
std::map<HttpConnection *, Info> leader;
|
||||
};
|
||||
7
src/examples/006_websocket_client/Makefile
Normal file
7
src/examples/006_websocket_client/Makefile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
TARGET = winnerclient
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = info
|
||||
GNUTLS = 0
|
||||
SOURCES = main.cpp winnerclient.cpp
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
13
src/examples/006_websocket_client/main.cpp
Normal file
13
src/examples/006_websocket_client/main.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#include <framework/eventloop.h>
|
||||
#include "winnerclient.h"
|
||||
|
||||
/* Example 006
|
||||
|
||||
A websocket client talking to the server of example 005.
|
||||
|
||||
*/
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop::runTask(new WinnerClient("127.0.0.1", 8080, "/winner"));
|
||||
return 0;
|
||||
}
|
||||
37
src/examples/006_websocket_client/main2.cpp
Normal file
37
src/examples/006_websocket_client/main2.cpp
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#include <framework/eventloop.h>
|
||||
#include "winnerclient.h"
|
||||
|
||||
/* Example 006
|
||||
|
||||
A websocket client talking to the server of example 005.
|
||||
This is an alternative version of the main function, where you can start the
|
||||
client with the command line parameters
|
||||
|
||||
--logfile=
|
||||
--host=
|
||||
--port=
|
||||
--url=
|
||||
|
||||
To use this version, update the Makefile to use main2.cpp instead of main.cpp.
|
||||
|
||||
*/
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
TaskConfig cfg;
|
||||
|
||||
cfg.set("host", "127.0.0.1");
|
||||
cfg.set("port", "8080");
|
||||
cfg.set("url", "/winner");
|
||||
cfg.parseArgs(argc, argv);
|
||||
|
||||
// Use value of parameter --logfile as filename to save the log if it exists
|
||||
// and isn't equal to "-".
|
||||
std::ofstream log;
|
||||
cfg.openlog(log);
|
||||
|
||||
auto task = new WinnerClient(cfg.value("host"),
|
||||
std::stoi(cfg.value("port")),
|
||||
cfg.value("url"));
|
||||
EventLoop::runTask(task);
|
||||
return 0;
|
||||
}
|
||||
49
src/examples/006_websocket_client/winnerclient.cpp
Normal file
49
src/examples/006_websocket_client/winnerclient.cpp
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#include "winnerclient.h"
|
||||
|
||||
WinnerClient::WinnerClient(const std::string &host, uint16_t port,
|
||||
const std::string &url) :
|
||||
HttpClientTask("WinnerClient", HttpHost(host, port)),
|
||||
_url(url) {
|
||||
}
|
||||
|
||||
double WinnerClient::start() {
|
||||
log() << "starting";
|
||||
if (!createNewConnection()) {
|
||||
log() << "Cannot connect to server\n";
|
||||
setResult("error");
|
||||
}
|
||||
return 10;
|
||||
}
|
||||
|
||||
void WinnerClient::connRemoved(SocketConnection *) {
|
||||
if (!terminated()) {
|
||||
log() << "Lost connection";
|
||||
setResult("error");
|
||||
}
|
||||
}
|
||||
|
||||
void WinnerClient::newRequest(HttpClientConnection *conn) {
|
||||
conn->ws_get(_url);
|
||||
}
|
||||
|
||||
bool WinnerClient::requestComplete(HttpClientConnection *) {
|
||||
// Server ignored websocket upgrade request
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WinnerClient::websocketUpgrade(HttpClientConnection *conn) {
|
||||
log() << "connected";
|
||||
conn->sendWsMessage("Bill 12");
|
||||
conn->sendWsMessage("Steve 19");
|
||||
conn->sendWsMessage("Linus 33");
|
||||
conn->sendWsMessage("Ken 27");
|
||||
conn->sendWsMessage("winner");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WinnerClient::wsTextMessage(HttpConnection *,
|
||||
const std::string &msg) {
|
||||
log() << "Got " << msg;
|
||||
setResult(msg);
|
||||
return false;
|
||||
}
|
||||
18
src/examples/006_websocket_client/winnerclient.h
Normal file
18
src/examples/006_websocket_client/winnerclient.h
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
#pragma once
|
||||
|
||||
#include <http/httpclienttask.h>
|
||||
|
||||
class WinnerClient : public HttpClientTask {
|
||||
public:
|
||||
WinnerClient(const std::string &host, uint16_t port,
|
||||
const std::string &url);
|
||||
double start() override;
|
||||
void connRemoved(SocketConnection *conn) override;
|
||||
void newRequest(HttpClientConnection *conn) override;
|
||||
bool requestComplete(HttpClientConnection *) override;
|
||||
bool websocketUpgrade(HttpClientConnection *) override;
|
||||
bool wsTextMessage(HttpConnection *conn,
|
||||
const std::string &msg) override;
|
||||
private:
|
||||
std::string _url;
|
||||
};
|
||||
10
src/examples/007_events/Makefile
Normal file
10
src/examples/007_events/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
TARGET = client
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
|
||||
SOURCES = main.cpp
|
||||
|
||||
CXXFLAGS += -g
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
78
src/examples/007_events/main.cpp
Normal file
78
src/examples/007_events/main.cpp
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
#include <framework/task.h>
|
||||
#include <http/singlerequest.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 007
|
||||
|
||||
Simple demonstration of sending events (direct messages) between tasks.
|
||||
The SenderTask will send a message to ReceiverTask each second.
|
||||
The ReceiverTask will terminate after the third message.
|
||||
The SenderTask will terminate after the ReceiverTask has terminated.
|
||||
|
||||
*/
|
||||
|
||||
class ReceiverTask;
|
||||
|
||||
class SenderTask : public Task {
|
||||
public:
|
||||
SenderTask(ReceiverTask *task) : Task("SenderTask"), peer(task) {
|
||||
}
|
||||
double start() override;
|
||||
double timerEvent() override;
|
||||
void taskFinished(Task *task) override;
|
||||
|
||||
private:
|
||||
ReceiverTask *peer;
|
||||
};
|
||||
|
||||
class ReceiverTask : public Task {
|
||||
public:
|
||||
ReceiverTask() : Task("ReceiverTask") {
|
||||
}
|
||||
|
||||
void handleExecution(Task *, const std::string &msg) override;
|
||||
private:
|
||||
unsigned int msgCount = 0;
|
||||
};
|
||||
|
||||
void ReceiverTask::handleExecution(Task *, const std::string &msg) {
|
||||
log() << "Event: " << msg;
|
||||
if (++msgCount == 3)
|
||||
setResult("Got Event");
|
||||
}
|
||||
|
||||
double SenderTask::start() {
|
||||
// We have to register an "interest" in peer by calling the startObserving
|
||||
// method, otherwise the messages we try to send to it will be ignored.
|
||||
// Also, by doing this we will be notified (through taskFinished) if
|
||||
// the peer task dies; that is important since we must not keep the pointer
|
||||
// to the peer after it has been deleted.
|
||||
if (!startObserving(peer)) {
|
||||
log() << "Peer task does not exist.";
|
||||
setResult("Fail");
|
||||
return 0.0;
|
||||
}
|
||||
return 0.1;
|
||||
}
|
||||
|
||||
double SenderTask::timerEvent() {
|
||||
executeHandler(peer, "Hi there!");
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
void SenderTask::taskFinished(Task *task) {
|
||||
if (task == peer) {
|
||||
peer = nullptr;
|
||||
log() << "Peer task dead, will exit.";
|
||||
setResult("Done.");
|
||||
}
|
||||
}
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop eventloop("EventLoop");
|
||||
ReceiverTask *it = new ReceiverTask();
|
||||
eventloop.addTask(it);
|
||||
eventloop.addTask(new SenderTask(it));
|
||||
eventloop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
11
src/examples/008_workers/Makefile
Normal file
11
src/examples/008_workers/Makefile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
TARGET = boss
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
|
||||
SOURCES = main.cpp
|
||||
|
||||
CLEAN += w0.log w1.log w2.log
|
||||
CXXFLAGS += -g
|
||||
|
||||
include $(DIRLEVEL)/framework/mk.inc
|
||||
103
src/examples/008_workers/main.cpp
Normal file
103
src/examples/008_workers/main.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
|
||||
#include <framework/task.h>
|
||||
#include <framework/socketreceiver.h>
|
||||
|
||||
/* Example 008
|
||||
|
||||
Demonstration of a task that creates workers (subprocesses).
|
||||
The Boss task creates three subprocesses, each running a Worker task.
|
||||
Each Worker will do some processing and then exit.
|
||||
The Boss will exit when all worker processes are done.
|
||||
|
||||
This example does not work on Windows.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
class Worker : public Task {
|
||||
public:
|
||||
|
||||
Worker(const std::string label) : Task(label) {
|
||||
}
|
||||
|
||||
~Worker() override {
|
||||
log() << "Goodbye from " << label();
|
||||
}
|
||||
|
||||
double start() override;
|
||||
|
||||
private:
|
||||
};
|
||||
|
||||
double Worker::start() {
|
||||
log() << "Worker PID " << getpid() << " started.";
|
||||
srand(static_cast<unsigned int>(getpid()));
|
||||
unsigned long count = 0;
|
||||
while (true) {
|
||||
int n = rand();
|
||||
++count;
|
||||
if (n % 10000000 == 3333333) {
|
||||
setResult(std::to_string(count) + " tries, result " +
|
||||
std::to_string(n));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
class Boss : public Task {
|
||||
public:
|
||||
|
||||
Boss(unsigned int no_workers) :
|
||||
Task("Boss"),
|
||||
tot_no_workers(no_workers) {
|
||||
}
|
||||
|
||||
~Boss() override {
|
||||
}
|
||||
|
||||
// After creating a child process, the eventloop will call our
|
||||
// createWorkerTask method to get a task to run in the new process.
|
||||
Task *createWorkerTask(unsigned int wno) override {
|
||||
std::string name = "w" + std::to_string(wno);
|
||||
log() << "Will create worker " << name;
|
||||
return new Worker(name);
|
||||
}
|
||||
|
||||
double start() override;
|
||||
|
||||
void processFinished(int pid, int wstatus) override {
|
||||
log() << "OK, end of " << pid << " status " << wstatus;
|
||||
if (++wdone == tot_no_workers)
|
||||
setResult("all done");
|
||||
}
|
||||
private:
|
||||
const unsigned int tot_no_workers;
|
||||
unsigned int wdone = 0;
|
||||
};
|
||||
|
||||
double Boss::start() {
|
||||
// Messages and sockets can be passed between parent and worker processes
|
||||
// through "channels". We don't use channels in this example.
|
||||
|
||||
// The createWorker call instructs the eventloop to create a child process
|
||||
// and run a new eventloop in that process. Our createWorkerTask method
|
||||
// will be called in the child process to provide a task for the eventloop
|
||||
// running in the child process.
|
||||
// First parameter to createWorker is a file name for the worker's log.
|
||||
// The second parameter is the number of channels between parent and worker.
|
||||
for (unsigned int n=0; n < tot_no_workers; ++n)
|
||||
createWorker("w" + std::to_string(n) + ".log", 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop eventloop("EventLoop");
|
||||
eventloop.addTask(new Boss(3));
|
||||
eventloop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
12
src/examples/009_threads/Makefile
Normal file
12
src/examples/009_threads/Makefile
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
TARGET = threadserver
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
THREADS = 1
|
||||
GNUTLS = 0
|
||||
|
||||
SOURCES = main.cpp
|
||||
|
||||
CLEAN += main.log M8000.log M8080.log
|
||||
CXXFLAGS += -g
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
63
src/examples/009_threads/main.cpp
Normal file
63
src/examples/009_threads/main.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fstream>
|
||||
#include <algorithm>
|
||||
|
||||
#include <http/webservertask.h>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
/* Example 009
|
||||
|
||||
Create two threads, each running a WebServer in its own eventloop.
|
||||
One will listen on port 8000, the other on 8080. To test, open URLs
|
||||
|
||||
http://127.0.0.1:8000/getTime
|
||||
http://127.0.0.1:8080/getTime
|
||||
http://127.0.0.1:8000/getStats
|
||||
http://127.0.0.1:8080/getStats
|
||||
http://127.0.0.1:8000/stop
|
||||
http://127.0.0.1:8080/stop
|
||||
|
||||
in a web browser. The /stop URL will tell each server to exit. The test program
|
||||
will exit when both servers have been stopped.
|
||||
|
||||
*/
|
||||
|
||||
class WebServer : public WebServerTask {
|
||||
public:
|
||||
WebServer(const std::string &cfg, const std::string &name) :
|
||||
WebServerTask(name, cfg) {
|
||||
}
|
||||
|
||||
HttpState newGetRequest(HttpServerConnection *,
|
||||
const std::string &uri) override;
|
||||
private:
|
||||
unsigned long tot_no_requests = 0;
|
||||
};
|
||||
|
||||
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
|
||||
const std::string &uri) {
|
||||
++tot_no_requests;
|
||||
log() << "URI: " << uri << " #" << tot_no_requests;
|
||||
if (uri == "/getTime")
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
|
||||
else if (uri == "/getStats")
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain",
|
||||
std::to_string(tot_no_requests));
|
||||
else if (uri == "/stop")
|
||||
setResult("DONE");
|
||||
else
|
||||
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
|
||||
"unknown service");
|
||||
return HttpState::WAITING_FOR_REQUEST;
|
||||
}
|
||||
|
||||
int main(int , char *[]) {
|
||||
std::ofstream my_log("main.log"), log1("M8000.log"), log2("M8080.log");
|
||||
Logger::setLogFile(my_log);
|
||||
EventLoop loop;
|
||||
loop.spawnThread(new WebServer("listen 8000", "W8000"), "M8000", &log1);
|
||||
loop.spawnThread(new WebServer("listen 8080", "W8080"), "M8080", &log2);
|
||||
loop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
10
src/examples/010_echoserver/Makefile
Normal file
10
src/examples/010_echoserver/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
TARGET = echoserver
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
|
||||
SOURCES = echoserverconnection.cpp \
|
||||
echoservertask.cpp \
|
||||
echoserver.cpp
|
||||
|
||||
include $(DIRLEVEL)/framework/mk.inc
|
||||
23
src/examples/010_echoserver/echoserver.cpp
Normal file
23
src/examples/010_echoserver/echoserver.cpp
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#include <framework/eventloop.h>
|
||||
#include "echoservertask.h"
|
||||
|
||||
/* Example 010
|
||||
|
||||
Implement a server for the echo network protocol, i.e.
|
||||
just echo all received data back to the client.
|
||||
Write log to stderr.
|
||||
|
||||
The class EchoServerTask takes a port number as a parameter.
|
||||
Will accept clients on that port number.
|
||||
|
||||
To test it, start the server and open one or more other terminals.
|
||||
In each of the terminals, run the command
|
||||
telnet 127.0.0.1 1400
|
||||
and type a few lines of text. The server should echo back the text.
|
||||
|
||||
*/
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop::runTask(new EchoServerTask(1400));
|
||||
return 0;
|
||||
}
|
||||
39
src/examples/010_echoserver/echoserverconnection.cpp
Normal file
39
src/examples/010_echoserver/echoserverconnection.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "echoserverconnection.h"
|
||||
|
||||
EchoServerConnection::EchoServerConnection(Task *task, int fd,
|
||||
const char *ip, uint16_t port) :
|
||||
SocketConnection("Echo Client Handler", task, fd, ip, port) {
|
||||
}
|
||||
|
||||
PollState EchoServerConnection::connected() {
|
||||
log() << "New client " << id() << ", waiting for data";
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
PollState EchoServerConnection::readData(char *buf, size_t len) {
|
||||
// Just send the data back to the client
|
||||
asyncSendData(buf, len);
|
||||
|
||||
if (len > 20) {
|
||||
dbg_log() << "Client " << id() << " said "
|
||||
<< std::string(buf, 20) << "... (" << len << " bytes)";
|
||||
} else {
|
||||
dbg_log() << "Client " << id() << " said " << std::string(buf, len);
|
||||
}
|
||||
|
||||
if (asyncBufferSize() > 1000000) {
|
||||
// The client does not read data as fast as we receive it.
|
||||
// We have to stop reading until the outgoing buffer
|
||||
// has shrunk to a manageable size.
|
||||
dbg_log() << "Send buffer too large: " << asyncBufferSize();
|
||||
return PollState::READ_BLOCKED;
|
||||
}
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
PollState EchoServerConnection::checkReadBlock() {
|
||||
dbg_log() << "Check send buffer: " << asyncBufferSize();
|
||||
if (asyncBufferSize() > 100000)
|
||||
return PollState::READ_BLOCKED;
|
||||
return PollState::READ;
|
||||
}
|
||||
19
src/examples/010_echoserver/echoserverconnection.h
Normal file
19
src/examples/010_echoserver/echoserverconnection.h
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/socketconnection.h>
|
||||
|
||||
class EchoServerConnection : public SocketConnection {
|
||||
public:
|
||||
EchoServerConnection(Task *task, int fd, const char *ip, uint16_t port);
|
||||
|
||||
// Called by the eventloop when a client connection has been established.
|
||||
PollState connected() override;
|
||||
|
||||
// Called by the eventloop when data has arrived from the client.
|
||||
PollState readData(char *buf, size_t len) override;
|
||||
|
||||
// Called regularly by the eventloop if we have blocked incoming data,
|
||||
// i.e. if we have returned PollState::READ_BLOCKED from the above method.
|
||||
PollState checkReadBlock() override;
|
||||
private:
|
||||
};
|
||||
52
src/examples/010_echoserver/echoservertask.cpp
Normal file
52
src/examples/010_echoserver/echoservertask.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include "echoservertask.h"
|
||||
#include "echoserverconnection.h"
|
||||
#include <framework/serversocket.h>
|
||||
|
||||
EchoServerTask::EchoServerTask(uint16_t port) :
|
||||
Task("Echo Server"),
|
||||
port_number(port) {
|
||||
}
|
||||
|
||||
double EchoServerTask::start() {
|
||||
// The ServerSocket object is owned by the eventloop and will be deleted
|
||||
// by the eventloop when it's no longer needed.
|
||||
if (addServer(new ServerSocket("Echo Listen Socket", this, port_number)))
|
||||
return 2.0;
|
||||
setResult("Failed, cannot start server");
|
||||
return 0;
|
||||
}
|
||||
|
||||
double EchoServerTask::timerEvent() {
|
||||
log() << "Current number of clients: " << no_clients;
|
||||
return 5.0;
|
||||
}
|
||||
|
||||
SocketConnection *EchoServerTask::newClient(int fd, const char *ip,
|
||||
uint16_t port, ServerSocket *) {
|
||||
// The EchoServerConnection object is owned by the eventloop and will be
|
||||
// deleted by the eventloop when it's no longer needed.
|
||||
// Just before deleting it, the eventloop will notify us by calling the
|
||||
// below connRemoved method.
|
||||
// We really shoudn't keep local copies of the (pointers to the) connection
|
||||
// objects, but if we still do so, we _must_ implement connRemoved to delete
|
||||
// our local copies so we don't use them after the objects are deleted.
|
||||
return new EchoServerConnection(this, fd, ip, port);
|
||||
}
|
||||
|
||||
void EchoServerTask::connAdded(SocketConnection *) {
|
||||
log() << "added client";
|
||||
++no_clients;
|
||||
}
|
||||
|
||||
void EchoServerTask::connRemoved(SocketConnection *) {
|
||||
log() << "lost client";
|
||||
--no_clients;
|
||||
}
|
||||
|
||||
void EchoServerTask::serverAdded(ServerSocket *) {
|
||||
log() << "listen socket added";
|
||||
}
|
||||
|
||||
void EchoServerTask::serverRemoved(ServerSocket *) {
|
||||
setResult("Server hangup!");
|
||||
}
|
||||
39
src/examples/010_echoserver/echoservertask.h
Normal file
39
src/examples/010_echoserver/echoservertask.h
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/task.h>
|
||||
|
||||
class EchoServerTask : public Task {
|
||||
public:
|
||||
EchoServerTask(uint16_t port);
|
||||
|
||||
double start() override;
|
||||
|
||||
double timerEvent() override;
|
||||
|
||||
// Whenever a new client connects to our server socket, the eventloop
|
||||
// will call the below method. To reject the connection, we could return
|
||||
// nullptr. If we accept the connection we must create and return an object
|
||||
// belonging to a subclass of SocketConnection.
|
||||
SocketConnection *newClient(int fd, const char *ip,
|
||||
uint16_t port, ServerSocket *) override;
|
||||
|
||||
// The eventloop will call this method to notify us each time a new
|
||||
// SocketConnection has been added, i.e. after the newClient call.
|
||||
void connAdded(SocketConnection *conn) override;
|
||||
|
||||
// The eventloop will call this method to notify us each time an existing
|
||||
// SocketConnection has been removed.
|
||||
void connRemoved(SocketConnection *conn) override;
|
||||
|
||||
// The eventloop will call this method to notify us each time a new
|
||||
// server (listening) socket has been added.
|
||||
void serverAdded(ServerSocket *conn) override;
|
||||
|
||||
// The eventloop will call this method to notify us each time an existing
|
||||
// server (listening) socket has been removed.
|
||||
void serverRemoved(ServerSocket *conn) override;
|
||||
|
||||
private:
|
||||
uint16_t port_number;
|
||||
unsigned int no_clients = 0;
|
||||
};
|
||||
10
src/examples/011_echoclient/Makefile
Normal file
10
src/examples/011_echoclient/Makefile
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
TARGET = echoclient
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
|
||||
SOURCES = echoclientconnection.cpp \
|
||||
echoclienttask.cpp \
|
||||
echoclient.cpp
|
||||
|
||||
include $(DIRLEVEL)/framework/mk.inc
|
||||
14
src/examples/011_echoclient/echoclient.cpp
Normal file
14
src/examples/011_echoclient/echoclient.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include <framework/eventloop.h>
|
||||
#include "echoclienttask.h"
|
||||
|
||||
/* Example 011
|
||||
|
||||
Write a few messages to an echo server, check the result.
|
||||
Make sure the server from example 010 is running while you run this client.
|
||||
|
||||
*/
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop::runTask(new EchoClientTask("127.0.0.1", 1400));
|
||||
return 0;
|
||||
}
|
||||
36
src/examples/011_echoclient/echoclientconnection.cpp
Normal file
36
src/examples/011_echoclient/echoclientconnection.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include "echoclientconnection.h"
|
||||
#include "echoclienttask.h"
|
||||
|
||||
EchoClientConnection::
|
||||
EchoClientConnection(EchoClientTask *task, const std::string &hostname,
|
||||
unsigned int port) :
|
||||
SocketConnection("EchoClientConnection", task, hostname, port) {
|
||||
}
|
||||
|
||||
PollState EchoClientConnection::connected() {
|
||||
msg_out = "Hi ho, hi ho";
|
||||
log() << "Socket connected, sending " << msg_out;
|
||||
asyncSendData(msg_out);
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
PollState EchoClientConnection::readData(char *buf, size_t len) {
|
||||
log() << "Got data: " << std::string(buf, len);
|
||||
msg_in.append(buf, len);
|
||||
if (msg_in.size() == msg_out.size()) {
|
||||
if (msg_in == msg_out) {
|
||||
log() << "Full response read. Will take a short nap.";
|
||||
if (EchoClientTask *t = dynamic_cast<EchoClientTask *>(owner()))
|
||||
t->test_succeeded();
|
||||
} else {
|
||||
log() << "Unexpected response. Nevermind. Will try again soon.";
|
||||
}
|
||||
return PollState::KEEPALIVE;
|
||||
} else if (msg_in.size() < msg_out.size()) {
|
||||
log() << "More data needed";
|
||||
return PollState::READ;
|
||||
} else {
|
||||
log() << "Got unexpected data: sent " << msg_out << ", got " << msg_in;
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
}
|
||||
17
src/examples/011_echoclient/echoclientconnection.h
Normal file
17
src/examples/011_echoclient/echoclientconnection.h
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/socketconnection.h>
|
||||
|
||||
class EchoClientTask;
|
||||
|
||||
class EchoClientConnection : public SocketConnection {
|
||||
public:
|
||||
EchoClientConnection(EchoClientTask *task, const std::string &hostname,
|
||||
unsigned int port);
|
||||
PollState connected() override;
|
||||
|
||||
PollState readData(char *buf, size_t len) override;
|
||||
private:
|
||||
size_t sent = 0;
|
||||
std::string msg_in, msg_out;
|
||||
};
|
||||
20
src/examples/011_echoclient/echoclienttask.cpp
Normal file
20
src/examples/011_echoclient/echoclienttask.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#include "echoclienttask.h"
|
||||
#include "echoclientconnection.h"
|
||||
|
||||
double EchoClientTask::start() {
|
||||
log() << "Will create EchoClientConnection";
|
||||
if (!addConnection(new EchoClientConnection(this, _hostname, _port)))
|
||||
setError("Cannot find server");
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
double EchoClientTask::timerEvent() {
|
||||
if (elapsed() > 15.0) {
|
||||
setResult("Timeout!");
|
||||
return 0;
|
||||
}
|
||||
log() << "Create another EchoClientConnection";
|
||||
if (!addConnection(new EchoClientConnection(this, _hostname, _port)))
|
||||
setError("Cannot find server");
|
||||
return 1.0;
|
||||
}
|
||||
35
src/examples/011_echoclient/echoclienttask.h
Normal file
35
src/examples/011_echoclient/echoclienttask.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include <framework/task.h>
|
||||
|
||||
class EchoClientTask : public Task {
|
||||
public:
|
||||
EchoClientTask(const std::string &host, unsigned int port) :
|
||||
Task("EchoClientTask"),
|
||||
_port(port),
|
||||
_hostname(host) {
|
||||
}
|
||||
|
||||
void test_succeeded() {
|
||||
++no_succeeded_requests;
|
||||
if (no_succeeded_requests == 10)
|
||||
setResult("All done!");
|
||||
}
|
||||
|
||||
double start() override;
|
||||
|
||||
void connAdded(SocketConnection *) override {
|
||||
log() << "connection added";
|
||||
}
|
||||
|
||||
void connRemoved(SocketConnection *) override {
|
||||
log() << "connection removed";
|
||||
}
|
||||
|
||||
double timerEvent() override;
|
||||
|
||||
private:
|
||||
uint16_t _port;
|
||||
std::string _hostname;
|
||||
unsigned int no_succeeded_requests = 0;
|
||||
};
|
||||
9
src/examples/020_cliclient/Makefile
Normal file
9
src/examples/020_cliclient/Makefile
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
TARGET = cliclient
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
SOURCES = cliclient.cpp webserver.cpp
|
||||
|
||||
CLEAN += log.txt
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
98
src/examples/020_cliclient/cliclient.cpp
Normal file
98
src/examples/020_cliclient/cliclient.cpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#include <fstream>
|
||||
|
||||
#include <framework/eventloop.h>
|
||||
#include <framework/synchronousbridge.h>
|
||||
|
||||
#include "webserver.h"
|
||||
|
||||
/* Example 020: Demonstration of the agent/bridge/client concept.
|
||||
|
||||
This program contains a CLI (command line interface) to a WebServer task.
|
||||
It must be run in a terminal. The CLI is rather pointless, it just writes
|
||||
the latest URL fetched from the WebServer in a single line in the terminal.
|
||||
However, the same technique can be used to implement real user interfaces,
|
||||
e.g. a GUI or a web interface.
|
||||
|
||||
A log is written to log.txt.
|
||||
|
||||
The WebServer task is an _agent_, i.e. it can communicate with a client that
|
||||
does not run in the eventloop. The client is the CLI which is implemented by
|
||||
the MyClient class below.
|
||||
|
||||
The communication between the agent and the client is facilitated by a _bridge_
|
||||
which is a task running in the eventloop. The bridge can pass messages from
|
||||
the agent to the client and from the client to the agent. Each message is a
|
||||
single string.
|
||||
|
||||
The bridge class is a subclass of the BridgeTask class, specifying the API from
|
||||
the agent's point of view. Since the client is not a Task (it could be anything
|
||||
really), different BridgeTask sublasses have different means of communicating
|
||||
with the client. However, the agent code does not depend on what client is used.
|
||||
|
||||
In this example, the agent and the client run in the same process. The most
|
||||
common case though is that the client runs in another process or at least not
|
||||
in the same thread as the eventloop.
|
||||
|
||||
To test the CLI client, run it in a terminal. Then retrieve a few URLs from the
|
||||
agent, which runs on port 8080, e.g.
|
||||
|
||||
http://127.0.0.1:8080/getTime
|
||||
http://127.0.0.1:8080/getStats
|
||||
http://127.0.0.1:8080/hi
|
||||
|
||||
The program will (as a demonstration) terminate after a few requests.
|
||||
|
||||
*/
|
||||
|
||||
class MyClient : public SynchronousClient {
|
||||
public:
|
||||
// initial messages to the agent shall be pushed onto return_msgs.
|
||||
void initialMsgToAgent(std::deque<std::string> &return_msgs) override;
|
||||
|
||||
// msg is a new message from the agent.
|
||||
// push any return messages onto return_msgs.
|
||||
void newEventFromAgent(std::deque<std::string> &return_msgs,
|
||||
const std::string &msg) override;
|
||||
private:
|
||||
std::string last_msg;
|
||||
unsigned int msgCount = 0;
|
||||
};
|
||||
|
||||
void MyClient::initialMsgToAgent(std::deque<std::string> &return_msgs) {
|
||||
return_msgs.push_back("client ready");
|
||||
}
|
||||
|
||||
void MyClient::newEventFromAgent(std::deque<std::string> &return_msgs,
|
||||
const std::string &msg) {
|
||||
++msgCount;
|
||||
|
||||
// Move back to leftmost position in the terminal
|
||||
std::cerr << std::string(last_msg.size(), '\010') << msg;
|
||||
if (last_msg.size() > msg.size()) {
|
||||
// New message is shorter, erase last part of old message in terminal
|
||||
size_t n = static_cast<size_t>(last_msg.size()-msg.size());
|
||||
std::cerr << std::string(n, ' ') << std::string(n, '\010');
|
||||
}
|
||||
last_msg = msg;
|
||||
|
||||
// Just as a demonstration, we will send a termination request to the agent
|
||||
// after having received a few messages.
|
||||
if (msgCount == 4)
|
||||
return_msgs.push_back("quit");
|
||||
|
||||
if (BridgeTask::isAgentTerminatedMessage(msg)) {
|
||||
// Agent gone.
|
||||
std::cerr << "\nBye." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int , char *[]) {
|
||||
std::ofstream log("log.txt");
|
||||
Logger::setLogFile(log);
|
||||
EventLoop eventloop("EventLoop");
|
||||
WebServer *agent = new WebServer("listen 8080");
|
||||
MyClient *client = new MyClient();
|
||||
eventloop.addTask(new SynchronousBridge(agent, client));
|
||||
eventloop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
57
src/examples/020_cliclient/webserver.cpp
Normal file
57
src/examples/020_cliclient/webserver.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#include "webserver.h"
|
||||
#include <framework/bridgetask.h>
|
||||
|
||||
|
||||
// Messages from the client arrive here:
|
||||
void WebServer::handleExecution(Task *sender, const std::string &message) {
|
||||
dbg_log() << "Event " << message << " from " << sender->label();
|
||||
if (BridgeTask *bridge = dynamic_cast<BridgeTask *>(sender)) {
|
||||
if (the_bridge && bridge != the_bridge) {
|
||||
// We already had a connected bridge and a new one tries to connect.
|
||||
// We could allow several simultaneous clients, but normally one is
|
||||
// enough.
|
||||
log() << "Won't allow more than one client";
|
||||
return;
|
||||
}
|
||||
|
||||
// Here we can handle all kinds of messages from the client.
|
||||
if (message == "client ready") {
|
||||
// Client is ready to receive messages from us.
|
||||
the_bridge=bridge;
|
||||
} else if (message == "quit" ) {
|
||||
setResult("Terminate by request from client");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WebServer::taskFinished(Task *task) {
|
||||
if (task == the_bridge) {
|
||||
log() << "Client connection broken";
|
||||
the_bridge = nullptr;
|
||||
// We could call setResult("") to terminate
|
||||
// but will wait for a new client instead.
|
||||
}
|
||||
}
|
||||
|
||||
HttpState WebServer::newGetRequest(HttpServerConnection *conn,
|
||||
const std::string &uri) {
|
||||
++tot_no_requests;
|
||||
log() << "URI: " << uri << " #" << tot_no_requests;
|
||||
if (the_bridge) {
|
||||
std::string msg = "Last request: " + uri + " (request #" +
|
||||
std::to_string(tot_no_requests) + ")";
|
||||
the_bridge->sendMsgToClient(msg);
|
||||
}
|
||||
|
||||
if (uri == "/getTime")
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain", dateString());
|
||||
else if (uri == "/getStats")
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain",
|
||||
std::to_string(tot_no_requests));
|
||||
else if (uri == "/hi")
|
||||
conn->sendHttpResponse(headers("200 OK"), "text/plain", "hello");
|
||||
else
|
||||
conn->sendHttpResponse(headers("404 Not Found"), "text/plain",
|
||||
"unknown service");
|
||||
return HttpState::WAITING_FOR_REQUEST;
|
||||
}
|
||||
23
src/examples/020_cliclient/webserver.h
Normal file
23
src/examples/020_cliclient/webserver.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#pragma once
|
||||
|
||||
#include <http/webservertask.h>
|
||||
|
||||
class BridgeTask;
|
||||
|
||||
class WebServer : public WebServerTask {
|
||||
public:
|
||||
WebServer(const std::string &cfg) :
|
||||
WebServerTask("WebServer", cfg) {
|
||||
}
|
||||
|
||||
HttpState newGetRequest(HttpServerConnection *,
|
||||
const std::string &uri) override;
|
||||
|
||||
void handleExecution(Task *sender, const std::string &message) override;
|
||||
void taskFinished(Task *task) override;
|
||||
private:
|
||||
unsigned long tot_no_requests = 0;
|
||||
|
||||
// We will only talk to one client.
|
||||
BridgeTask *the_bridge = nullptr;
|
||||
};
|
||||
11
src/examples/030_httpclient/Makefile
Normal file
11
src/examples/030_httpclient/Makefile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
TARGET = client
|
||||
DIRLEVEL = ../..
|
||||
LOGLEVEL = dbg
|
||||
GNUTLS = 0
|
||||
THREADS = 0
|
||||
|
||||
SOURCES = main.cpp
|
||||
|
||||
CXXFLAGS += -g
|
||||
|
||||
include $(DIRLEVEL)/http/mk.inc
|
||||
76
src/examples/030_httpclient/main.cpp
Normal file
76
src/examples/030_httpclient/main.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#include <http/httprequestengine.h>
|
||||
#include <json11/json11.hpp>
|
||||
#include <framework/eventloop.h>
|
||||
|
||||
class MainTask : public Task {
|
||||
public:
|
||||
MainTask() : Task("Main") {
|
||||
bot = new HttpRequestEngine("MyBot", HttpHost("127.0.0.1", 8080),
|
||||
0, 3);
|
||||
//bot = new HttpRequestEngine("MyBot", "lab04.bredbandskollen.se");
|
||||
}
|
||||
|
||||
~MainTask() {
|
||||
}
|
||||
|
||||
double start() override {
|
||||
dbg_log() << "starting";
|
||||
|
||||
addNewTask(bot, this);
|
||||
|
||||
bot->startObserving(this);
|
||||
bot->getJob(this, "Ticks " + std::to_string(++request_no),
|
||||
"/chunk?ticks=10");
|
||||
bot->getJob(this, "Count " + std::to_string(++request_no),
|
||||
"/getStats");
|
||||
killChildTaskWhenFinished();
|
||||
return 3.0;
|
||||
}
|
||||
|
||||
double timerEvent() override {
|
||||
dbg_log() << "timerEvent";
|
||||
if (request_no < 10)
|
||||
bot->getJob(this, "Ticks " + std::to_string(++request_no),
|
||||
"/chunk?ticks=10");
|
||||
bot->getJob(this, "Count " + std::to_string(++request_no),
|
||||
"/getStats");
|
||||
if (request_no < 20)
|
||||
return 3.0;
|
||||
else
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void taskFinished(Task *task) override {
|
||||
log() << "Oops, task " << task->label() << " died.";
|
||||
if (task == bot) {
|
||||
bot = nullptr;
|
||||
setResult("Failed");
|
||||
}
|
||||
}
|
||||
|
||||
void handleExecution(Task *task, const std::string &name) override {
|
||||
log() << task->label() << " event: " << name;
|
||||
if (task != bot)
|
||||
return;
|
||||
if (bot->httpStatus()) {
|
||||
log() << "Exit job " << name << " result: " << bot->contents();
|
||||
++ok_jobs;
|
||||
if (ok_jobs == 20)
|
||||
setResult("All done.");
|
||||
} else {
|
||||
log() << "Exit job " << name << " failed, will retry";
|
||||
bot->redoJob();
|
||||
}
|
||||
}
|
||||
private:
|
||||
unsigned int request_no = 0, ok_jobs = 0;
|
||||
HttpRequestEngine *bot = nullptr;
|
||||
std::string _ticket;
|
||||
};
|
||||
|
||||
int main(int , char *[]) {
|
||||
EventLoop eventloop("EventLoop");
|
||||
eventloop.addTask(new MainTask());
|
||||
eventloop.runUntilComplete();
|
||||
return 0;
|
||||
}
|
||||
15
src/examples/Makefile
Normal file
15
src/examples/Makefile
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# These can't be built in parallel since there's a race when creating
|
||||
# the dependency files, so "make" / "make -j1" is needed.
|
||||
|
||||
SUBDIRS := $(wildcard [0-9][0-9][0-9]_*)
|
||||
|
||||
all: $(SUBDIRS)
|
||||
|
||||
$(SUBDIRS):
|
||||
$(MAKE) -C $@ clean # needed because of conflicting obj instrumentations
|
||||
$(MAKE) -C $@ -j # internal parallel build is ok
|
||||
|
||||
clean:
|
||||
@$(foreach dir,$(SUBDIRS),$(MAKE) -C$(dir) clean;)
|
||||
|
||||
.PHONY: $(SUBDIRS) clean all
|
||||
20
src/framework/bridgetask.cpp
Normal file
20
src/framework/bridgetask.cpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "../json11/json11.hpp"
|
||||
|
||||
#include "bridgetask.h"
|
||||
|
||||
BridgeTask::~BridgeTask() {
|
||||
}
|
||||
|
||||
double BridgeTask::start() {
|
||||
if (the_agent) {
|
||||
addNewTask(the_agent, this);
|
||||
the_agent->startObserving(this);
|
||||
} else {
|
||||
err_log() << "You must call setAgent before starting bridge";
|
||||
setError("no agent");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
142
src/framework/bridgetask.h
Normal file
142
src/framework/bridgetask.h
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "task.h"
|
||||
|
||||
/// \brief
|
||||
/// Tasks may use a bridge to communicate with an application running
|
||||
/// outside the event loop.
|
||||
///
|
||||
/// This class provides an abstract interface for a _bridge_ which facilitates
|
||||
/// communication between a task and code running outside the event loop.
|
||||
/// The code might run in another process or thread. The actual
|
||||
/// means of communication might be sockets or pipes, or shared variables in
|
||||
/// the case of threads, or something else entirely.
|
||||
///
|
||||
/// The task running in the event loop is called the _agent_, and the outside
|
||||
/// aplication is called the _client_.
|
||||
///
|
||||
/// To use this class, you must
|
||||
/// - derive from this class
|
||||
/// - implement the sendMessageToClient method
|
||||
/// - implement a way for the client to send messages back, and pass those
|
||||
/// messages through the sendMsgToAgent method
|
||||
/// - if you override the start() method, call BridgeTask::start() first
|
||||
/// - create an agent object and a bridge object
|
||||
/// - add the bridge object to the event loop
|
||||
/// The agent task will be aborted when the bridge task is finished.
|
||||
/// If you need timers (e.g. for polling), you must override the start() method
|
||||
/// below and your start() method must call BridgeTask::start().
|
||||
///
|
||||
/// When there is a message from the client to the agent, the agent's
|
||||
/// Task::handleExecution method will be called with two parameters:
|
||||
/// the bridge object and a message.
|
||||
///
|
||||
/// Typically, the agent's Task::handleExecution method checks if the
|
||||
/// first parameter can be cast to a BridgeTask pointer.
|
||||
/// If so, the second parameter is a message from the client.
|
||||
/// When the first message from the client arrives, the agent stores
|
||||
/// the pointer to the bridge in case it needs to send messages back.
|
||||
class BridgeTask : public Task {
|
||||
|
||||
public:
|
||||
/// \brief
|
||||
/// Create a bridge to the given agent task.
|
||||
///
|
||||
/// If the agent task isn't available yet, it must be added using
|
||||
/// BridgeTask::setAgent before the bridge is added to the EventLoop.
|
||||
BridgeTask(Task *agent = nullptr) : Task("Bridge"), the_agent(agent) {
|
||||
killChildTaskWhenFinished();
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Will add the agent task to the EventLoop.
|
||||
///
|
||||
/// *Note!* If you override this method, it should be called explicitly by
|
||||
/// the overriding method!
|
||||
double start() override;
|
||||
|
||||
/// If the agent dies, a special message will be sent to notify the client.
|
||||
void taskFinished(Task *task) override {
|
||||
if (task == the_agent) {
|
||||
if (!task->result().empty()) {
|
||||
log() << "Agent terminated";
|
||||
sendMsgToClient(agentTerminatedMessage(task->result()));
|
||||
the_agent = nullptr;
|
||||
}
|
||||
die();
|
||||
}
|
||||
}
|
||||
|
||||
/// If the agent task was created after the bridge, it must be added using
|
||||
/// this method _before_ the bridge is added to the EventLoop.
|
||||
void setAgent(Task *agent) {
|
||||
if (!the_agent && !hasStarted())
|
||||
the_agent = agent;
|
||||
else
|
||||
err_log() << "cannot set agent";
|
||||
}
|
||||
|
||||
/// Terminate the bridge task.
|
||||
void die() {
|
||||
setResult("");
|
||||
}
|
||||
|
||||
/// The agent will call this to pass messages to the client.
|
||||
virtual void sendMsgToClient(const std::string &msg) = 0;
|
||||
|
||||
/// \brief Format a message to the agent.
|
||||
///
|
||||
/// Recommended way to format messages to the agent: a method name,
|
||||
/// and "arguments" stored in a JSON object.
|
||||
///
|
||||
/// *Note!* The method name must not contain any special chararcters.
|
||||
static std::string msgToAgent(const std::string &method,
|
||||
const std::string &jsonobj = "{}") {
|
||||
return "{\"method\": \"" + method + "\", \"args\": " + jsonobj + "}";
|
||||
}
|
||||
|
||||
/// Return true if msg is formatted as a note that the agent has terminated
|
||||
/// and that there will be no more messages.
|
||||
static bool isAgentTerminatedMessage(const std::string &msg) {
|
||||
return (msg.substr(0, 12) == "AGENT EXIT: ");
|
||||
}
|
||||
|
||||
/// Format a message to signal that the Agent is gone.
|
||||
static std::string agentTerminatedMessage(const std::string &err_msg) {
|
||||
return "AGENT EXIT: " + err_msg;
|
||||
}
|
||||
|
||||
/// \brief Format a message to the client.
|
||||
///
|
||||
/// Recommended way to format messages to the client: an event (or method)
|
||||
/// name, and "arguments" stored in a JSON object.
|
||||
///
|
||||
/// *Note!* The method name must not contain any special chararcters.
|
||||
void sendMsgToClient(const std::string &method, const std::string &jsonobj) {
|
||||
sendMsgToClient("{\"event\": \"" + method +
|
||||
"\", \"args\": " + jsonobj + "}");
|
||||
}
|
||||
|
||||
virtual ~BridgeTask() override;
|
||||
|
||||
protected:
|
||||
/// Send message to the agent.
|
||||
void sendMsgToAgent(const std::string &msg) {
|
||||
executeHandler(the_agent, msg);
|
||||
if (msg.substr(0, terminate_msg.size()) == terminate_msg)
|
||||
die();
|
||||
}
|
||||
|
||||
/// Format and send message to the agent.
|
||||
void sendMsgToAgent(const std::string &method, const std::string &jsonobj) {
|
||||
sendMsgToAgent(msgToAgent(method, jsonobj));
|
||||
}
|
||||
private:
|
||||
Task *the_agent;
|
||||
//const std::string quit_msg = "quit";
|
||||
const std::string terminate_msg = "{\"method\": \"terminate\"";
|
||||
};
|
||||
644
src/framework/engine.cpp
Normal file
644
src/framework/engine.cpp
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <string.h>
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#else
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <sys/select.h>
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "engine.h"
|
||||
#include "socketconnection.h"
|
||||
#include "serversocket.h"
|
||||
#ifndef _WIN32
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
|
||||
Engine::Engine(std::string label) :
|
||||
Logger(label) {
|
||||
#ifdef USE_GNUTLS
|
||||
x509_cred.resize(1);
|
||||
if (gnutls_global_init() < 0 ||
|
||||
gnutls_certificate_allocate_credentials(&x509_cred[0]) < 0 ||
|
||||
gnutls_priority_init(&priority_cache,
|
||||
"PERFORMANCE:%SERVER_PRECEDENCE",
|
||||
nullptr) < 0)
|
||||
throw std::runtime_error("cannot enable TLS");
|
||||
#if GNUTLS_VERSION_NUMBER >= 0x030506
|
||||
/* only available since GnuTLS 3.5.6, on previous versions see
|
||||
* gnutls_certificate_set_dh_params(). */
|
||||
gnutls_certificate_set_known_dh_params(x509_cred[0], GNUTLS_SEC_PARAM_MEDIUM);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
Engine::~Engine() {
|
||||
|
||||
terminate(0);
|
||||
Socket::clearCache();
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
for (auto &cred : x509_cred)
|
||||
gnutls_certificate_free_credentials(cred);
|
||||
gnutls_priority_deinit(priority_cache);
|
||||
gnutls_global_deinit();
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
|
||||
bool Engine::setCABundle(const std::string &path) {
|
||||
if (path.empty())
|
||||
return true;
|
||||
ca_bundle = path;
|
||||
if (gnutls_certificate_set_x509_trust_file(x509_cred[0], ca_bundle.c_str(),
|
||||
GNUTLS_X509_FMT_PEM) < 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Engine::tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path,
|
||||
const std::string &password) {
|
||||
if (key_path.empty() && crt_path.empty())
|
||||
return 0;
|
||||
|
||||
unsigned int index;
|
||||
auto it = tls_crt_map.find(crt_path);
|
||||
if (it != tls_crt_map.end()) {
|
||||
index = it->second;
|
||||
} else {
|
||||
index = static_cast<unsigned int>(x509_cred.size());
|
||||
x509_cred.resize(index+1);
|
||||
if (gnutls_certificate_allocate_credentials(&x509_cred[index]) < 0) {
|
||||
x509_cred.resize(index);
|
||||
return false;
|
||||
}
|
||||
int ret = gnutls_certificate_set_x509_key_file2(x509_cred[index],
|
||||
crt_path.c_str(),
|
||||
key_path.c_str(),
|
||||
GNUTLS_X509_FMT_PEM,
|
||||
password.c_str(), 0);
|
||||
if (ret < 0) {
|
||||
err_log() << "Cannot add TLS server certificate: "
|
||||
<< gnutls_strerror(ret);
|
||||
return false;
|
||||
}
|
||||
tls_crt_map[crt_path] = index;
|
||||
}
|
||||
conn->tlsSetKey(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// Hand the SocketConnection object over to us.
|
||||
// Will delete the object and return false on immediate failure.
|
||||
// If the object already has a socket, i.e. conn->socket()>=0,
|
||||
// it will be used.
|
||||
// Otherwise we will listen on the given port number and
|
||||
// local ip number (port 0 means any available port,
|
||||
// leave ip_number empty to listen on all local ip numbers).
|
||||
bool Engine::addServer(ServerSocket *conn) {
|
||||
if (!conn)
|
||||
return false;
|
||||
if (conn->socket()<0 && !conn->createServerSocket()) {
|
||||
err_log() << "cannot add ServerSocket "
|
||||
<< conn->hostname() << ':' << conn->port();
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
dbg_log() << "addServer fd " << conn->socket();
|
||||
connectionStore[conn->socket()] = conn;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hand the SocketConnection object over to us.
|
||||
// Will delete the object and return false on immediate failure.
|
||||
// Otherwise return true; then either connected or connectionFailed
|
||||
// will be called on conn.
|
||||
bool Engine::addClient(SocketConnection *conn) {
|
||||
if (!conn)
|
||||
return false;
|
||||
|
||||
#ifndef _WIN32
|
||||
if (conn->socket() >= 0) {
|
||||
if (connectionStore.find(conn->socket()) != connectionStore.end()) {
|
||||
err_log() << "Socket " << conn->socket() << " host "
|
||||
<< conn->hostname() << " already exists";
|
||||
return false;
|
||||
}
|
||||
if (conn->hostname() != "UnixDomain" || !conn->socket()) {
|
||||
err_log() << "Socket " << conn->socket() << " host "
|
||||
<< conn->hostname() << " not accepted";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unix Domain socket, socket already created and connected
|
||||
addConnected(conn);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
std::string label = conn->cacheLabel();
|
||||
auto p = keepaliveCache.find(label);
|
||||
if (!label.empty() &&
|
||||
p != keepaliveCache.end()) {
|
||||
log() << "Using cached socket " << p->second << " label " << label;
|
||||
conn->setSocket(p->second);
|
||||
#ifdef USE_GNUTLS
|
||||
auto it = tls_session_cache.find(p->second);
|
||||
if (it != tls_session_cache.end()) {
|
||||
conn->insert_cached_session(it->second);
|
||||
tls_session_cache.erase(it);
|
||||
}
|
||||
#endif
|
||||
keepaliveCache.erase(p);
|
||||
//log() << "Cache size is " << keepaliveCache.size();
|
||||
conn->setState(conn->connected());
|
||||
} else if (!conn->asyncConnect()) {
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
|
||||
connectionStore[conn->socket()] = conn;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Engine::childProcessCloseSockets() {
|
||||
for (auto p : connectionStore)
|
||||
Socket::closeSocket(p.first);
|
||||
for (auto p : keepaliveCache)
|
||||
Socket::closeSocket(p.second);
|
||||
}
|
||||
|
||||
void Engine::terminate(unsigned int ) {
|
||||
while (!connectionStore.empty())
|
||||
killConnection(connectionStore.cbegin()->first);
|
||||
for (auto p : keepaliveCache)
|
||||
Socket::closeSocket(p.second);
|
||||
#ifdef USE_GNUTLS
|
||||
for (auto p : tls_session_cache)
|
||||
gnutls_deinit(p.second);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Engine::reclaimConnections() {
|
||||
std::set<int> fds_to_remove;
|
||||
for (auto p : connectionStore)
|
||||
switch (p.second->state()) {
|
||||
case PollState::CLOSE:
|
||||
case PollState::KILL:
|
||||
case PollState::KEEPALIVE:
|
||||
fds_to_remove.insert(p.first);
|
||||
break;
|
||||
case PollState::NONE:
|
||||
case PollState::READ_BLOCKED:
|
||||
#ifdef USE_GNUTLS
|
||||
case PollState::TLS_HANDSHAKE:
|
||||
#endif
|
||||
case PollState::CONNECTING:
|
||||
case PollState::WRITE:
|
||||
case PollState::READ_WRITE:
|
||||
case PollState::READ:
|
||||
break;
|
||||
}
|
||||
if (fds_to_remove.empty())
|
||||
return false;
|
||||
for (auto fd : fds_to_remove)
|
||||
killConnection(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
int Engine::setFds(fd_set &r, fd_set &w, fd_set &e) {
|
||||
int max = -1;
|
||||
FD_ZERO(&r);
|
||||
FD_ZERO(&w);
|
||||
FD_ZERO(&e);
|
||||
|
||||
// Check what to do with each connection.
|
||||
// Note! We cannot do anything non-trivial while looping
|
||||
// over connectionStore since elements might be added or
|
||||
// removed, invalidating iterators.
|
||||
for (auto p : connectionStore) {
|
||||
int fd = p.first;
|
||||
Socket *c = p.second;
|
||||
switch (c->state()) {
|
||||
case PollState::CLOSE:
|
||||
case PollState::KILL:
|
||||
case PollState::KEEPALIVE:
|
||||
continue;
|
||||
case PollState::CONNECTING:
|
||||
case PollState::WRITE:
|
||||
case PollState::READ_WRITE:
|
||||
FD_SET(fd, &w);
|
||||
break;
|
||||
case PollState::READ:
|
||||
if (c->wantToSend())
|
||||
FD_SET(fd, &w);
|
||||
break;
|
||||
#ifdef USE_GNUTLS
|
||||
case PollState::TLS_HANDSHAKE:
|
||||
#endif
|
||||
case PollState::NONE:
|
||||
break;
|
||||
case PollState::READ_BLOCKED:
|
||||
c->setState(c->checkReadBlock());
|
||||
if (c->wantToSend())
|
||||
FD_SET(fd, &w);
|
||||
FD_SET(fd, &e);
|
||||
if (fd > max)
|
||||
max = fd;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always check for readability (if closed by peer) and error:
|
||||
FD_SET(fd, &r);
|
||||
FD_SET(fd, &e);
|
||||
|
||||
if (fd > max)
|
||||
max = fd;
|
||||
}
|
||||
|
||||
// Check keep-alive connection since they may be closed by peer.
|
||||
for (auto p : keepaliveCache) {
|
||||
FD_SET(p.second, &r);
|
||||
FD_SET(p.second, &e);
|
||||
if (p.second > max)
|
||||
max = p.second;
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
volatile bool Engine::yield_called = false;
|
||||
bool Engine::max_open_fd_reached = false;
|
||||
|
||||
void Engine::handleMaxOpenFdReached() {
|
||||
// TODO thread safe
|
||||
if (!keepaliveCache.empty()) {
|
||||
auto it = keepaliveCache.begin();
|
||||
Socket::closeSocket(it->second);
|
||||
#ifdef USE_GNUTLS
|
||||
auto p = tls_session_cache.find(it->second);
|
||||
if (p != tls_session_cache.end())
|
||||
tls_session_cache.erase(p);
|
||||
#endif
|
||||
keepaliveCache.erase(it);
|
||||
max_open_fd_reached = false;
|
||||
return;
|
||||
}
|
||||
if (connectionStore.size() > 100) {
|
||||
// Try to find a client socket to remove
|
||||
for (auto &p : connectionStore)
|
||||
if (auto s = dynamic_cast<SocketConnection *>(p.second)) {
|
||||
warn_log() << "Out of file descriptors, will remove "
|
||||
<< s->id();
|
||||
s->closeMe();
|
||||
max_open_fd_reached = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Close some random file descriptor
|
||||
warn_log() << "Out of file descriptors, possible fd leak";
|
||||
static int fd_to_remove = 10;
|
||||
while (++fd_to_remove < 65536) {
|
||||
if (connectionStore.find(fd_to_remove) == connectionStore.end()) {
|
||||
if (!Socket::closeSocket(fd_to_remove)) {
|
||||
warn_log() << "Possible fd leak, closed fd " << fd_to_remove;
|
||||
max_open_fd_reached = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
warn_log() << "Out of file descriptors, cannot recover";
|
||||
fd_to_remove = 10;
|
||||
}
|
||||
|
||||
// Handle network events for max_time seconds or until yield is called.
|
||||
bool Engine::run(double max_time) {
|
||||
deadline = timeAfter(max_time);
|
||||
yield_called = false;
|
||||
|
||||
{
|
||||
// Find sockets that will expire and delete them.
|
||||
// Can't delete them while looping over connectionStore since
|
||||
// connectionStore gets modified when Socket objects are deleted.
|
||||
std::vector<int> expired;
|
||||
for (auto p : connectionStore)
|
||||
if (p.second->hasExpired(deadline))
|
||||
expired.push_back(p.first);
|
||||
|
||||
for (auto fd : expired)
|
||||
killConnection(fd);
|
||||
}
|
||||
|
||||
while (!yield_called) {
|
||||
|
||||
if (reclaimConnections())
|
||||
continue; // Recheck after callbacks
|
||||
|
||||
fd_set readFds, writeFds, errFds;
|
||||
int max_fd = setFds(readFds, writeFds, errFds);
|
||||
|
||||
struct timeval timeout;
|
||||
{
|
||||
double time_left = secondsTo(deadline);
|
||||
long us = static_cast<long>(1e6*time_left);
|
||||
if (us <= 0)
|
||||
return true;
|
||||
timeout.tv_sec = us/1000000;
|
||||
timeout.tv_usec = us%1000000;
|
||||
}
|
||||
|
||||
int res = select(max_fd + 1, &readFds, &writeFds, &errFds, &timeout);
|
||||
if (res < 0) {
|
||||
if (fatalSelectError())
|
||||
return false;
|
||||
} else {
|
||||
doFds(readFds, writeFds, errFds, max_fd);
|
||||
if (max_open_fd_reached)
|
||||
handleMaxOpenFdReached();
|
||||
}
|
||||
}
|
||||
reclaimConnections();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Engine::doFds(const fd_set &r, const fd_set &w, const fd_set &e, int max) {
|
||||
|
||||
for (auto it=keepaliveCache.begin(); it != keepaliveCache.end(); ) {
|
||||
int fd = it->second;
|
||||
if (FD_ISSET(fd, &r) || FD_ISSET(fd, &e)) {
|
||||
log() << "close keepalive socket " << fd;
|
||||
#ifdef USE_GNUTLS
|
||||
auto p = tls_session_cache.find(fd);
|
||||
if (p != tls_session_cache.end()) {
|
||||
gnutls_deinit(p->second);
|
||||
tls_session_cache.erase(p);
|
||||
}
|
||||
#endif
|
||||
Socket::closeSocket(fd);
|
||||
it = keepaliveCache.erase(it);
|
||||
} else
|
||||
++it;
|
||||
}
|
||||
|
||||
for (int fd=0; fd<=max; ++fd) {
|
||||
// We cannot loop over connectionStore since it may
|
||||
// be modified in all sorts of ways within the loop.
|
||||
auto p = connectionStore.find(fd);
|
||||
if (p == connectionStore.end())
|
||||
continue;
|
||||
|
||||
if (FD_ISSET(fd, &e)) {
|
||||
err_log() << "socket error " << fd;
|
||||
killConnection(fd);
|
||||
continue;
|
||||
}
|
||||
Socket *conn = p->second;
|
||||
|
||||
bool readable = FD_ISSET(fd, &r);
|
||||
bool writable = FD_ISSET(fd, &w);
|
||||
|
||||
if (!readable && !writable)
|
||||
continue;
|
||||
|
||||
SocketConnection *c = dynamic_cast<SocketConnection *>(conn);
|
||||
|
||||
if (!c) {
|
||||
// Is it a server connection?
|
||||
if (ServerSocket *srv =
|
||||
dynamic_cast<ServerSocket *>(conn)) {
|
||||
// New connection on listen socket
|
||||
handleIncoming(srv);
|
||||
continue;
|
||||
}
|
||||
err_log() << "event on non-client connection";
|
||||
killConnection(fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (readable && (c->state() == PollState::READ ||
|
||||
c->state() == PollState::READ_WRITE)) {
|
||||
// React to reading before considering writing:
|
||||
c->setState(c->doRead(fd));
|
||||
// If it switches to sending, we'll probably be able to send some data
|
||||
// immediately, saving us a select roundtrip.
|
||||
if (c->state() == PollState::WRITE) {
|
||||
writable = true;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (writable) {
|
||||
PollState s;
|
||||
if (c->state() == PollState::CONNECTING) {
|
||||
if (c->inError()) {
|
||||
err_log() << "async connect failed";
|
||||
killConnection(fd);
|
||||
continue;
|
||||
}
|
||||
#ifdef USE_GNUTLS
|
||||
if (c->use_tls) {
|
||||
if (!c->init_tls_client(x509_cred[0], !ca_bundle.empty())) {
|
||||
log() << "cannot enable TLS" << fd;
|
||||
killConnection(fd);
|
||||
continue;
|
||||
}
|
||||
int ret = c->try_tls_handshake();
|
||||
if (ret >= 0) {
|
||||
s = c->connected();
|
||||
} else if (gnutls_error_is_fatal(ret)) {
|
||||
err_log() << "TLS handshake failure: " << gnutls_strerror(ret);
|
||||
killConnection(fd);
|
||||
continue;
|
||||
} else {
|
||||
s = PollState::TLS_HANDSHAKE;
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
s = c->connected();
|
||||
} else {
|
||||
s = c->doWrite();
|
||||
}
|
||||
c->setState(s);
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
if (c->state() == PollState::TLS_HANDSHAKE) {
|
||||
int ret = c->try_tls_handshake();
|
||||
if (ret >= 0) {
|
||||
c->setState(c->connected());
|
||||
} else if (gnutls_error_is_fatal(ret)) {
|
||||
err_log() << "TLS handshake failure: " << gnutls_strerror(ret);
|
||||
killConnection(fd);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
// The socket is readable, but we don't want to read it
|
||||
// since state is neither READ nor READ_WRITE.
|
||||
// The connection may have been closed by peer.
|
||||
PollState s = c->doRead(fd);
|
||||
c->setState(s);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool Engine::fatalSelectError() {
|
||||
if (Socket::isTempError()) {
|
||||
errno_log() << "select interrupt";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Oops, we've done something really wrong: an invalid
|
||||
// filedescriptor may have been added to the poll set.
|
||||
// Find it and remove it, then try again.
|
||||
errno_log() << "select error";
|
||||
|
||||
for (auto p : connectionStore)
|
||||
if (p.second->inError()) {
|
||||
p.second->setState(PollState::CLOSE);
|
||||
killConnection(p.first);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (auto p=keepaliveCache.begin(); p != keepaliveCache.end(); ++p)
|
||||
if (Socket::socketInError(p->second)) {
|
||||
#ifdef USE_GNUTLS
|
||||
auto it = tls_session_cache.find(p->second);
|
||||
if (it != tls_session_cache.end()) {
|
||||
gnutls_deinit(it->second);
|
||||
tls_session_cache.erase(it);
|
||||
}
|
||||
#endif
|
||||
Socket::closeSocket(p->second);
|
||||
keepaliveCache.erase(p);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't know what's wrong, cannot recover.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Engine::killConnection(int fd) {
|
||||
auto p = connectionStore.find(fd);
|
||||
if (p != connectionStore.end()) {
|
||||
Socket *conn = p->second;
|
||||
SocketConnection *c = dynamic_cast<SocketConnection *>(conn);
|
||||
if (conn->state() == PollState::KEEPALIVE) {
|
||||
std::string label = conn->cacheLabel();
|
||||
if (!label.empty()) {
|
||||
log() << "keep-alive socket " << fd << " to " << label;
|
||||
keepaliveCache.insert(std::make_pair(label, fd));
|
||||
#ifdef USE_GNUTLS
|
||||
if (c && c->is_tls())
|
||||
tls_session_cache[fd] = c->cache_session();
|
||||
#endif
|
||||
// Otherwise the socket will get closed when conn is deleted:
|
||||
conn->setSocket(-1);
|
||||
}
|
||||
}
|
||||
if (Task *task = conn->owner()) {
|
||||
if (c)
|
||||
task->connRemoved(c);
|
||||
else if (ServerSocket *srv = dynamic_cast<ServerSocket *>(conn))
|
||||
task->serverRemoved(srv);
|
||||
}
|
||||
delete conn;
|
||||
connectionStore.erase(p);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<Socket *> Engine::findSockByTask(const Task *t) const {
|
||||
std::set<Socket *> cset;
|
||||
for (auto p : connectionStore)
|
||||
if (p.second->owner() == t)
|
||||
cset.insert(p.second);
|
||||
return cset;
|
||||
}
|
||||
|
||||
void Engine::wakeUpByTask(Task *t) {
|
||||
for (auto p : connectionStore)
|
||||
if (SocketConnection *c = dynamic_cast<SocketConnection *>(p.second))
|
||||
if (c->owner() == t && c->state() == PollState::NONE)
|
||||
c->setState(c->connected());
|
||||
}
|
||||
|
||||
bool Engine::wakeUpConnection(SocketConnection *s) {
|
||||
if (s->state() != PollState::NONE)
|
||||
return false;
|
||||
s->setState(s->connected());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Engine::deleteConnByTask(const Task *task) {
|
||||
std::set<int> toRemove;
|
||||
for (auto p : connectionStore)
|
||||
if (p.second->owner() == task)
|
||||
toRemove.insert(p.first);
|
||||
for (auto fd : toRemove)
|
||||
killConnection(fd);
|
||||
}
|
||||
|
||||
void Engine::cancelConnection(SocketConnection *s) {
|
||||
killConnection(s->socket());
|
||||
}
|
||||
|
||||
void Engine::handleIncoming(ServerSocket *server) {
|
||||
SocketConnection *client = server->incoming();
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
int newfd = client->socket();
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
log() << "Socket " << newfd << " TLS=" << server->tlsKey();
|
||||
if (server->tlsKey()) {
|
||||
if (!client->init_tls_server(x509_cred[server->tlsKey()], priority_cache)) {
|
||||
log() << "cannot enable TLS" << newfd;
|
||||
delete client;
|
||||
return;
|
||||
}
|
||||
client->enableTLS();
|
||||
int ret = client->try_tls_handshake();
|
||||
if (ret >= 0) {
|
||||
client->setState(client->connected());
|
||||
} else if (gnutls_error_is_fatal(ret)) {
|
||||
err_log() << "TLS handshake has failed: " << gnutls_strerror(ret);
|
||||
delete client;
|
||||
return;
|
||||
} else {
|
||||
client->setState(PollState::TLS_HANDSHAKE);
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
client->setState(client->connected());
|
||||
connectionStore[newfd] = client;
|
||||
server->owner()->connAdded(client);
|
||||
}
|
||||
|
||||
void Engine::addConnected(SocketConnection *conn) {
|
||||
dbg_log() << "addConnected " << conn->socket();
|
||||
connectionStore[conn->socket()] = conn;
|
||||
conn->owner()->connAdded(conn);
|
||||
conn->setState(conn->connected());
|
||||
}
|
||||
151
src/framework/engine.h
Normal file
151
src/framework/engine.h
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
// This class implements a network engine, managing sockets.
|
||||
// It is only used by the EventLoop class.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "logger.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
class Socket;
|
||||
class SocketConnection;
|
||||
class ServerSocket;
|
||||
class Task;
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
#include <gnutls/gnutls.h>
|
||||
#endif
|
||||
|
||||
/// \brief
|
||||
/// The network engine.
|
||||
///
|
||||
/// Cannot be used directly. Will be managed by the EventLoop class.
|
||||
class Engine : public Logger {
|
||||
public:
|
||||
bool addClient(SocketConnection *conn);
|
||||
void addConnected(SocketConnection *conn);
|
||||
bool addServer(ServerSocket *conn);
|
||||
|
||||
Engine(std::string label);
|
||||
|
||||
/// Will kill all remaining connections.
|
||||
~Engine();
|
||||
|
||||
/// Close all connections, giving them at most
|
||||
/// max_time_ms milliseconds to finish what's
|
||||
/// already written to them.
|
||||
void terminate(unsigned int max_time_ms);
|
||||
|
||||
/// Call this in child process to close all redundant sockets after fork.
|
||||
void childProcessCloseSockets();
|
||||
|
||||
/// \brief
|
||||
/// Run the "event loop" for at most `max_time` seconds.
|
||||
///
|
||||
/// Will return when all connections have been closed,
|
||||
/// when a fatal error has occurred, or when max_time has passed.
|
||||
/// Return value is false on fatal error, otherwise true.
|
||||
/// You should call this function repetedly until you're done.
|
||||
///
|
||||
/// Don't set max_time > 2000 on 32-bit platforms.
|
||||
bool run(double max_time);
|
||||
|
||||
/// Remove all connections owned by the task.
|
||||
void deleteConnByTask(const Task *task);
|
||||
|
||||
/// Call this to make the Engine::run method return prematurely.
|
||||
static void yield() {
|
||||
yield_called = true;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Call this to enter a recovery mode if no more file descriptors
|
||||
/// could be created.
|
||||
static void notifyOutOfFds() {
|
||||
// TODO: thread safe
|
||||
max_open_fd_reached = true;
|
||||
}
|
||||
std::set<Socket *> findSockByTask(const Task *t) const;
|
||||
|
||||
/// Wake up all idle connections belonging to t:
|
||||
void wakeUpByTask(Task *t);
|
||||
|
||||
/// Wake up connection s if it is idle, return false otherwise.
|
||||
bool wakeUpConnection(SocketConnection *s);
|
||||
void cancelConnection(SocketConnection *s);
|
||||
|
||||
/// Return true if connection still exists.
|
||||
/// *Note!* We cannot _use_ conn if it has been deleted!
|
||||
bool connActive(const Socket *conn) const {
|
||||
for (auto it : connectionStore)
|
||||
if (it.second == conn)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Call this to make the Engine::run method return earlier.
|
||||
void resetDeadline(const TimePoint &t) {
|
||||
if (deadline > t)
|
||||
deadline = t;
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
/// Set path to file containing chain of trust for SSL certificate.
|
||||
bool setCABundle(const std::string &path);
|
||||
|
||||
/// Use SSL certificate for a listening socket.
|
||||
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path, const std::string &password);
|
||||
#endif
|
||||
|
||||
private:
|
||||
|
||||
TimePoint deadline;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static volatile bool yield_called;
|
||||
static bool max_open_fd_reached;
|
||||
void handleMaxOpenFdReached();
|
||||
void killConnection(int fd);
|
||||
void handleIncoming(ServerSocket *server);
|
||||
// Check all connections, remove closed, reclaim keepalive,
|
||||
// return false if none was removed:
|
||||
bool reclaimConnections();
|
||||
|
||||
// Prepare for select, return largest fd or -1:
|
||||
int setFds(fd_set &r, fd_set &w, fd_set &e);
|
||||
// Check error from select, return false if fatal:
|
||||
bool fatalSelectError();
|
||||
// Take care of all network events:
|
||||
void doFds(const fd_set &r, const fd_set &w, const fd_set &e, int max);
|
||||
|
||||
// Map socket number to Socket object:
|
||||
std::map<int, Socket *> connectionStore;
|
||||
|
||||
// Cache of open connections that may be reused.
|
||||
// Value is socket number. Key is some kind of label,
|
||||
// chosen by the tasks owning the connections,
|
||||
// that should identify the type of connection.
|
||||
std::multimap<std::string, int> keepaliveCache;
|
||||
#ifdef USE_GNUTLS
|
||||
std::map<int, gnutls_session_t> tls_session_cache;
|
||||
std::string ca_bundle;
|
||||
std::map<std::string, unsigned int> tls_crt_map;
|
||||
// Index 0 is used for outgoing connection, containing trust store.
|
||||
// At index > 0 certificates for server sockets are stored.
|
||||
std::vector<gnutls_certificate_credentials_t> x509_cred;
|
||||
gnutls_priority_t priority_cache;
|
||||
#endif
|
||||
};
|
||||
726
src/framework/eventloop.cpp
Normal file
726
src/framework/eventloop.cpp
Normal file
|
|
@ -0,0 +1,726 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <csignal>
|
||||
|
||||
#include "eventloop.h"
|
||||
#include "engine.h"
|
||||
#include "task.h"
|
||||
#include "socketconnection.h"
|
||||
#include "serversocket.h"
|
||||
|
||||
#ifdef USE_THREADS
|
||||
//thread_local
|
||||
#endif
|
||||
volatile int EventLoop::got_signal = 0;
|
||||
|
||||
#ifdef _WIN32
|
||||
void EventLoop::signalHandler(int signum) {
|
||||
EventLoop::interrupt();
|
||||
got_signal = signum;
|
||||
}
|
||||
#else
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
#include "socketreceiver.h"
|
||||
#include "workerprocess.h"
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
std::map<int, int> EventLoop::terminatedPIDs;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
volatile int EventLoop::terminatedPIDtmp[100] = { 0 };
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
std::string EventLoop::openFileOnSIGHUP;
|
||||
|
||||
void EventLoop::signalHandler(int signum) {
|
||||
EventLoop::interrupt();
|
||||
if (signum != SIGCHLD) {
|
||||
got_signal = signum;
|
||||
return;
|
||||
}
|
||||
while (true) {
|
||||
int wstatus;
|
||||
pid_t pid = waitpid(-1, &wstatus, WNOHANG);
|
||||
if (pid <= 0)
|
||||
break;
|
||||
//Logger::log("signalHandler") << "Child PID " << pid
|
||||
// << " terminated, status " << wstatus;
|
||||
for (size_t i=0; i<sizeof(terminatedPIDtmp)/sizeof(int); i+=2)
|
||||
if (!terminatedPIDtmp[i]) {
|
||||
terminatedPIDtmp[i] = pid;
|
||||
terminatedPIDtmp[++i] = wstatus;
|
||||
return;
|
||||
}
|
||||
// Bad, shouldn't use terminatedPIDs asynchronously. Won't happen if
|
||||
// terminatedPIDtmp is large enough, i.e. twice number of chldren.
|
||||
EventLoop::terminatedPIDs[pid] = wstatus;
|
||||
}
|
||||
}
|
||||
|
||||
int EventLoop::externalCommand(Task *owner, const char *const argv[]) {
|
||||
if (!argv[0])
|
||||
return false;
|
||||
pid_t chld = fork();
|
||||
if (chld < 0) {
|
||||
errno_log() << "cannot fork";
|
||||
return -1;
|
||||
}
|
||||
if (!chld) {
|
||||
// TODO: Close all file descriptors in child!
|
||||
int ret = execvp( argv[0], const_cast<char **>(argv) );
|
||||
exit(ret);
|
||||
}
|
||||
pidOwner[chld] = owner;
|
||||
return chld;
|
||||
}
|
||||
|
||||
void EventLoop::daemonize() {
|
||||
if (fork() != 0)
|
||||
exit(0); // Close original process
|
||||
|
||||
setsid(); // Detach from shell
|
||||
|
||||
if (int fd = open("/dev/null", O_RDWR, 0) != -1) {
|
||||
dup2(fd, STDIN_FILENO);
|
||||
dup2(fd, STDOUT_FILENO);
|
||||
dup2(fd, STDERR_FILENO);
|
||||
if (fd > STDERR_FILENO)
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
|
||||
WorkerProcess *EventLoop::createWorker(Task *parent,
|
||||
const std::string &log_file_name,
|
||||
unsigned int channels,
|
||||
unsigned int wno) {
|
||||
auto old = openFileOnSIGHUP;
|
||||
setLogFilename(log_file_name);
|
||||
auto logfile = new std::ofstream(log_file_name, std::ios::app);
|
||||
auto ret = createWorker(parent, logfile, channels, wno);
|
||||
openFileOnSIGHUP = old;
|
||||
return ret;
|
||||
}
|
||||
|
||||
WorkerProcess *EventLoop::createWorker(Task *parent, std::ostream *log_file,
|
||||
unsigned int channels,
|
||||
unsigned int wno) {
|
||||
if (aborted())
|
||||
return nullptr;
|
||||
std::vector<int> array(2 * channels);
|
||||
int *pair_sd = array.data();
|
||||
for (unsigned int i = 0; i<channels; ++i)
|
||||
if (socketpair(AF_UNIX, SOCK_DGRAM, 0, pair_sd+2*i) < 0) {
|
||||
if (errno == EMFILE)
|
||||
Engine::notifyOutOfFds();
|
||||
errno_log() << "cannot create socketpair";
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// TODO: second parameter: unsigned int noSocketReceivers, default 1
|
||||
// different class for communication channels
|
||||
int ppid = getpid();
|
||||
|
||||
pid_t wpid = fork();
|
||||
if (wpid < 0)
|
||||
return nullptr;
|
||||
|
||||
if (wpid > 0) {
|
||||
// In parent (original) process
|
||||
log() << "started worker " << wpid;
|
||||
if (log_file)
|
||||
delete log_file;
|
||||
std::vector<SocketReceiver *> receivers;
|
||||
for (unsigned int i = 0; i<2*channels; i+=2) {
|
||||
close(pair_sd[i+1]);
|
||||
fcntl(pair_sd[i], F_SETFL, O_NONBLOCK);
|
||||
SocketReceiver *conn = new SocketReceiver(parent, pair_sd[i], wpid);
|
||||
if (!addServer(conn)) {
|
||||
err_log() << "Cannot create worker channel";
|
||||
while (i < 2*channels)
|
||||
close(pair_sd[i++]);
|
||||
return nullptr;
|
||||
}
|
||||
log() << "Worker channel fd " << pair_sd[i];
|
||||
receivers.push_back(conn);
|
||||
}
|
||||
pidOwner[wpid] = parent;
|
||||
return new WorkerProcess(wpid, receivers);
|
||||
}
|
||||
|
||||
// In worker (new child process)
|
||||
|
||||
engine.childProcessCloseSockets();
|
||||
Task::supervisor = nullptr;
|
||||
|
||||
// Set new log file for child process
|
||||
if (!log_file) {
|
||||
log_file = new std::ostringstream();
|
||||
log_file->clear(std::istream::eofbit);
|
||||
}
|
||||
setLogFile(*log_file);
|
||||
|
||||
EventLoop eventloop("Worker");
|
||||
Task *worker = parent->createWorkerTask(wno);
|
||||
if (!worker) {
|
||||
err_log() << "Cannot create worker " << wno << ", will exit";
|
||||
exit(1);
|
||||
}
|
||||
eventloop.addTask(worker);
|
||||
for (unsigned int i = 0; i<2*channels; i+=2) {
|
||||
close(pair_sd[i]);
|
||||
fcntl(pair_sd[i+1], F_SETFL, O_NONBLOCK);
|
||||
SocketReceiver *chan = new SocketReceiver(worker, pair_sd[i+1], ppid);
|
||||
worker->newWorkerChannel(chan, i/2);
|
||||
worker->addServer(chan);
|
||||
}
|
||||
eventloop.runUntilComplete();
|
||||
parent->finishWorkerTask(wno);
|
||||
log() << "Exit" << std::endl;
|
||||
delete log_file;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void EventLoop::killChildProcesses(int signum) {
|
||||
for (auto p : pidOwner)
|
||||
kill(p.first, signum);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
EventLoop::~EventLoop() {
|
||||
removeAllTasks();
|
||||
Task::supervisor = nullptr;
|
||||
log() << "EventLoop finished.\n";
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
void EventLoop::do_init(EventLoop *parent) {
|
||||
parent_loop = parent;
|
||||
#else
|
||||
void EventLoop::do_init() {
|
||||
#endif
|
||||
if (Task::supervisor) {
|
||||
throw std::runtime_error("eventloop already exists");
|
||||
}
|
||||
Task::supervisor = this;
|
||||
got_signal = 0;
|
||||
#ifdef USE_THREADS
|
||||
if (parent_loop)
|
||||
return;
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
// Windows specific sockets initialization
|
||||
WSADATA wsaData;
|
||||
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (result != 0)
|
||||
throw std::runtime_error("cannot initialize network");
|
||||
signal(SIGTERM, signalHandler);
|
||||
signal(SIGABRT, signalHandler);
|
||||
#else
|
||||
terminatedPIDs.clear();
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = signalHandler;
|
||||
sigfillset(&sa.sa_mask);
|
||||
if (sigaction(SIGCHLD, &sa, nullptr) ||
|
||||
sigaction(SIGTERM, &sa, nullptr) ||
|
||||
sigaction(SIGINT, &sa, nullptr) ||
|
||||
sigaction(SIGHUP, &sa, nullptr) ||
|
||||
sigaction(SIGQUIT, &sa, nullptr) ||
|
||||
sigaction(SIGABRT, &sa, nullptr) ||
|
||||
sigaction(SIGPIPE, &sa, nullptr))
|
||||
errno_log() << "cannot add signal handler";
|
||||
#endif
|
||||
}
|
||||
|
||||
void EventLoop::addSignalHandler(int signum, void (*handler)(int, EventLoop &)) {
|
||||
#ifdef _WIN32
|
||||
if (signal(signum, signalHandler) == SIG_ERR) {
|
||||
errno_log() << "cannot add handler for signal " << signum;
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (signum == SIGCHLD) {
|
||||
err_log() << "must not set custom handler for SIGCHLD";
|
||||
return;
|
||||
}
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_handler = signalHandler;
|
||||
if (sigaction(signum, &sa, nullptr)) {
|
||||
errno_log() << "cannot add handler for signal " << signum ;
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
userSignalHandler.insert(std::make_pair(signum, handler));
|
||||
}
|
||||
|
||||
void EventLoop::addTask(Task *task, Task *parent) {
|
||||
if (task && tasks.find(task) == tasks.end()) {
|
||||
tasks[task] = parent;
|
||||
if (parent)
|
||||
startObserving(parent, task);
|
||||
if (task->terminated()) {
|
||||
// Task finished when executing its constructor
|
||||
notifyTaskFinished(task);
|
||||
return;
|
||||
}
|
||||
double ts = task->begin();
|
||||
log() << "Task " << task->label() << " timeout " << ts;
|
||||
if (ts > 0) {
|
||||
TimePoint t = timeAfter(ts);
|
||||
timer_queue.insert(std::make_pair(t, task));
|
||||
// Make sure we get back here when the timer expires:
|
||||
engine.resetDeadline(t);
|
||||
}
|
||||
} else {
|
||||
err_log() << "cannot add task";
|
||||
}
|
||||
}
|
||||
|
||||
bool EventLoop::addConnection(SocketConnection *conn) {
|
||||
if (!conn)
|
||||
return false;
|
||||
|
||||
if (tasks.find(conn->owner()) != tasks.end() &&
|
||||
engine.addClient(conn)) {
|
||||
conn->owner()->connAdded(conn);
|
||||
return true;
|
||||
}
|
||||
|
||||
err_log() << "cannot add connection";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EventLoop::addConnected(SocketConnection *conn) {
|
||||
if (!conn)
|
||||
return false;
|
||||
|
||||
if (tasks.find(conn->owner()) != tasks.end()) {
|
||||
engine.addConnected(conn);
|
||||
return true;
|
||||
}
|
||||
|
||||
err_log() << "cannot add connection";
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool EventLoop::addServer(ServerSocket *conn) {
|
||||
if (!conn)
|
||||
return false;
|
||||
|
||||
if (tasks.find(conn->owner()) != tasks.end() &&
|
||||
engine.addServer(conn)) {
|
||||
conn->owner()->serverAdded(conn);
|
||||
return true;
|
||||
}
|
||||
|
||||
err_log() << "cannot add server connection";
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is just a safeguard against buggy clients.
|
||||
// Will be called _after_ object pointed to by task
|
||||
// has been deleted, so don't dereference the pointer.
|
||||
void EventLoop::taskDeleted(Task *task) {
|
||||
auto p = tasks.find(task);
|
||||
if (p != tasks.end()) {
|
||||
err_log() << "task owned by me deleted by client!";
|
||||
tasks.erase(p);
|
||||
engine.deleteConnByTask(task);
|
||||
}
|
||||
}
|
||||
|
||||
bool EventLoop::startObserving(Task *from, Task *to) {
|
||||
if (tasks.find(from) == tasks.end() ||
|
||||
tasks.find(to) == tasks.end()) {
|
||||
err_log() << "startObserving: no such task";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
auto p = observing.find(to);
|
||||
if (p == observing.end()) {
|
||||
std::set<Task *> tset { from };
|
||||
observing.insert(std::make_pair(to, tset));
|
||||
} else {
|
||||
p->second.insert(from);
|
||||
}
|
||||
}
|
||||
{
|
||||
auto p = observed_by.find(from);
|
||||
if (p == observed_by.end()) {
|
||||
std::set<Task *> tset { to };
|
||||
observed_by.insert(std::make_pair(from, tset));
|
||||
} else {
|
||||
p->second.insert(to);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EventLoop::getChildTasks(std::set<Task *> &tset, Task *parent) const {
|
||||
// parent is observing its children.
|
||||
auto p = observed_by.find(parent);
|
||||
if (p == observed_by.end())
|
||||
return;
|
||||
|
||||
for (auto task : p->second)
|
||||
// task is observed by parent but isn't necessarily a child task.
|
||||
if (tasks.at(task) == parent)
|
||||
tset.insert(task);
|
||||
}
|
||||
|
||||
void EventLoop::abortChildTasks(Task *parent) {
|
||||
std::set<Task *> tset;
|
||||
getChildTasks(tset, parent);
|
||||
for (auto task : tset) {
|
||||
task->was_killed = true;
|
||||
finishedTasks.insert(task);
|
||||
}
|
||||
engine.yield();
|
||||
}
|
||||
|
||||
void EventLoop::abortTask(Task *task) {
|
||||
task->was_killed = true;
|
||||
finishedTasks.insert(task);
|
||||
engine.yield();
|
||||
}
|
||||
|
||||
void EventLoop::wakeUpTask(Task *t) {
|
||||
engine.wakeUpByTask(t);
|
||||
}
|
||||
|
||||
std::set<Socket *> EventLoop::findConnByTask(const Task *t) const {
|
||||
return engine.findSockByTask(t);
|
||||
}
|
||||
|
||||
bool EventLoop::running(Task *task) {
|
||||
return tasks.find(task) != tasks.end();
|
||||
}
|
||||
|
||||
void EventLoop::resetTimer(Task *task, double s) {
|
||||
_removeTimer(task);
|
||||
if (s < 0)
|
||||
return;
|
||||
if (s == 0)
|
||||
s = task->timerEvent();
|
||||
if (s > 0) {
|
||||
auto t = toUs(s);
|
||||
timer_queue.insert(std::make_pair(timeNow()+t, task));
|
||||
}
|
||||
}
|
||||
|
||||
void EventLoop::_removeTimer(Task *task) {
|
||||
for (auto it : timer_queue)
|
||||
if (it.second == task) {
|
||||
timer_queue.erase(it.first);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EventLoop::_removeTask(Task *task, bool killed) {
|
||||
{
|
||||
auto p = tasks.find(task);
|
||||
if (p == tasks.end())
|
||||
return; // Task already removed.
|
||||
tasks.erase(p);
|
||||
}
|
||||
|
||||
task->setTerminated();
|
||||
task->was_killed = killed;
|
||||
log() << "remove task " << task->label();
|
||||
|
||||
// Remove the task's connections
|
||||
engine.deleteConnByTask(task);
|
||||
|
||||
// Remove the task's timer
|
||||
for (auto it : timer_queue)
|
||||
if (it.second == task) {
|
||||
timer_queue.erase(it.first);
|
||||
break;
|
||||
}
|
||||
|
||||
// The dying task may be an observer:
|
||||
auto p1 = observed_by.find(task);
|
||||
if (p1 != observed_by.end()) {
|
||||
for (auto t : p1->second) {
|
||||
// task must no longer be observing t
|
||||
observing[t].erase(task);
|
||||
auto p = tasks.find(t);
|
||||
if (p->second == task) {
|
||||
// t is a child of the dying task. Mark it as an orphan:
|
||||
p->second = nullptr;
|
||||
if (task->kill_children)
|
||||
abortTask(t);
|
||||
}
|
||||
}
|
||||
observed_by.erase(p1);
|
||||
}
|
||||
|
||||
// Other tasks may be observing the dying task:
|
||||
auto p2 = observing.find(task);
|
||||
if (p2 != observing.end()) {
|
||||
for (auto t : p2->second) {
|
||||
// t must no longer be observing task
|
||||
observed_by[t].erase(task);
|
||||
// Notify t that an observed task has died
|
||||
t->taskFinished(task);
|
||||
}
|
||||
observing.erase(p2);
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
if (task->is_child_thread)
|
||||
finished_threads.push(task);
|
||||
else
|
||||
#endif
|
||||
delete task;
|
||||
}
|
||||
|
||||
Task *EventLoop::nextTimerToExecute() {
|
||||
if (!timer_queue.empty() &&
|
||||
timer_queue.cbegin()->first <= timeNow()) {
|
||||
Task *task = timer_queue.cbegin()->second;
|
||||
timer_queue.erase(timer_queue.begin());
|
||||
return task;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool EventLoop::run(double timeout_s) {
|
||||
TimePoint deadline = timeAfter(timeout_s);
|
||||
while (true) {
|
||||
// First run timers, regardless of deadline:
|
||||
while (Task *task = nextTimerToExecute()) {
|
||||
double ts = task->timerEvent();
|
||||
if (ts > 0) {
|
||||
auto t = toUs(ts);
|
||||
timer_queue.insert(std::make_pair(timeNow()+t, task));
|
||||
}
|
||||
}
|
||||
check_finished();
|
||||
|
||||
#ifdef USE_THREADS
|
||||
if ( (tasks.empty() && threads.empty()) ||
|
||||
timeNow() > deadline || aborted())
|
||||
#else
|
||||
if (tasks.empty() || timeNow() > deadline || aborted())
|
||||
#endif
|
||||
break;
|
||||
|
||||
double time_left;
|
||||
if (timer_queue.empty()) {
|
||||
time_left = secondsTo(deadline);
|
||||
} else {
|
||||
auto next_timer = timer_queue.cbegin()->first;
|
||||
time_left = secondsTo(std::min(next_timer, deadline));
|
||||
}
|
||||
|
||||
if (time_left <= 0) {
|
||||
log() << "Timeout passed, will not poll connections";
|
||||
break;
|
||||
}
|
||||
if (!engine.run(time_left)) {
|
||||
err_log() << "fatal engine failure";
|
||||
do_abort = true;
|
||||
break;
|
||||
}
|
||||
check_finished();
|
||||
}
|
||||
if (do_abort)
|
||||
removeAllTasks();
|
||||
#ifdef USE_THREADS
|
||||
return (!tasks.empty() || !threads.empty());
|
||||
#else
|
||||
return (!tasks.empty());
|
||||
#endif
|
||||
}
|
||||
|
||||
void EventLoop::runUntilComplete() {
|
||||
while (run(1.5)) {
|
||||
// for (auto &p : tasks)
|
||||
// log() << "REM: " << p.first->label();
|
||||
}
|
||||
#ifdef USE_THREADS
|
||||
dbg_log() << "End Loop; Threads left: " << threads.size();
|
||||
waitForThreadsToFinish();
|
||||
#endif
|
||||
flushLogFile();
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
void EventLoop::runTask(Task *task, const std::string &name,
|
||||
std::ostream *log_file, EventLoop *parent) {
|
||||
#else
|
||||
void EventLoop::runTask(Task *task, const std::string &name,
|
||||
std::ostream *log_file) {
|
||||
#endif
|
||||
if (log_file)
|
||||
setLogFile(*log_file);
|
||||
#ifdef USE_THREADS
|
||||
if (task->is_child_thread)
|
||||
parent = nullptr;
|
||||
EventLoop loop(name, parent);
|
||||
#else
|
||||
EventLoop loop(name);
|
||||
#endif
|
||||
loop.addTask(task);
|
||||
loop.runUntilComplete();
|
||||
flushLogFile();
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
|
||||
void EventLoop::spawnThread(Task *task, const std::string &name,
|
||||
std::ostream *log_file, Task *parent) {
|
||||
if (parent_loop)
|
||||
throw std::runtime_error("not in main loop");
|
||||
if (parent)
|
||||
threadTaskObserver.insert(std::make_pair(task, parent));
|
||||
task->is_child_thread = true;
|
||||
|
||||
threads.insert(std::make_pair(task, std::thread(runTask, task, name,
|
||||
log_file, this)));
|
||||
dbg_log() << "Added thread, now have " << threads.size();
|
||||
}
|
||||
|
||||
MsgQueue<Task *> EventLoop::finished_threads;
|
||||
|
||||
void EventLoop::collect_thread(Task *t) {
|
||||
if (parent_loop)
|
||||
return;
|
||||
log() << "Thread task " << t->label() << " finished";
|
||||
auto p = threads.find(t);
|
||||
if (p != threads.end()) {
|
||||
p->second.join();
|
||||
threads.erase(p);
|
||||
}
|
||||
|
||||
// Notify parent?
|
||||
auto it = threadTaskObserver.find(t);
|
||||
|
||||
// TODO: make sure to erase (task, parent) from
|
||||
// threadTaskObserver if parent dies first
|
||||
if (it != threadTaskObserver.end()) {
|
||||
Task *parent = it->second;
|
||||
if (tasks.find(parent) != tasks.end())
|
||||
parent->taskFinished(t);
|
||||
threadTaskObserver.erase(it);
|
||||
}
|
||||
|
||||
delete t;
|
||||
}
|
||||
|
||||
void EventLoop::waitForThreadsToFinish() {
|
||||
while (!threads.empty()) {
|
||||
dbg_log() << "Threads left: " << threads.size();
|
||||
Task *t = finished_threads.pop_blocking();
|
||||
collect_thread(t);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void EventLoop::removeAllTasks() {
|
||||
while (!tasks.empty())
|
||||
_removeTask(tasks.begin()->first, true);
|
||||
}
|
||||
|
||||
void EventLoop::check_finished() {
|
||||
if (got_signal) {
|
||||
int signum = got_signal;
|
||||
auto p = userSignalHandler.lower_bound(signum);
|
||||
auto to = userSignalHandler.upper_bound(signum);
|
||||
if (p == to) {
|
||||
// No user defined handler for signal, will abort
|
||||
#ifdef _WIN32
|
||||
err_log() << "got signal " << signum << ", will exit";
|
||||
abort();
|
||||
#else
|
||||
if (signum == SIGPIPE) {
|
||||
warn_log() << "got SIGPIPE";
|
||||
} else if (signum == SIGHUP) {
|
||||
Logger::reopenLogFile(openFileOnSIGHUP);
|
||||
Logger::setLogLimit();
|
||||
log() << "got SIGHUP";
|
||||
killChildProcesses(signum);
|
||||
} else if (signum == SIGTERM) {
|
||||
err_log() << "got SIGTERM, exit immediately" << std::endl;
|
||||
killChildProcesses(signum);
|
||||
exit(0);
|
||||
} else {
|
||||
err_log() << "got signal " << signum << ", will exit";
|
||||
killChildProcesses(signum);
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
for (; p != to; ++p) {
|
||||
void (*handler)(int, EventLoop &) = p->second;
|
||||
(*handler)(got_signal, *this);
|
||||
}
|
||||
}
|
||||
got_signal = 0;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
size_t i = 0;
|
||||
while (terminatedPIDtmp[i] && i<sizeof(terminatedPIDtmp)/sizeof(int)) {
|
||||
int pid = terminatedPIDtmp[i], wstatus = terminatedPIDtmp[i+1];
|
||||
log() << "PID " << pid << " finished, status=" << wstatus;
|
||||
terminatedPIDs[pid] = wstatus;
|
||||
terminatedPIDtmp[i] = 0;
|
||||
i += 2;
|
||||
}
|
||||
while (!terminatedPIDs.empty()) {
|
||||
auto it = terminatedPIDs.begin();
|
||||
int pid = it->first;
|
||||
int wstatus = it->second;
|
||||
log() << "Terminated PID " << pid << " status " << wstatus;
|
||||
terminatedPIDs.erase(it);
|
||||
auto it2 = pidOwner.find(pid);
|
||||
if (it2 != pidOwner.end()) {
|
||||
Task *task = it2->second;
|
||||
if (tasks.find(task) != tasks.end()) {
|
||||
task->processFinished(pid, wstatus);
|
||||
}
|
||||
pidOwner.erase(it2);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_THREADS
|
||||
Task *t;
|
||||
while (!threads.empty() && finished_threads.fetch(t))
|
||||
collect_thread(t);
|
||||
#endif
|
||||
while (!messageTasks.empty() || !finishedTasks.empty()) {
|
||||
if (!messageTasks.empty()) {
|
||||
auto p = tasks.find(*messageTasks.begin());
|
||||
messageTasks.erase(messageTasks.begin());
|
||||
if (p != tasks.end()) {
|
||||
if (Task *parent = p->second)
|
||||
parent->taskMessage(p->first);
|
||||
}
|
||||
}
|
||||
if (!finishedTasks.empty()) {
|
||||
auto p = tasks.find(*finishedTasks.begin());
|
||||
finishedTasks.erase(finishedTasks.begin());
|
||||
if (p != tasks.end())
|
||||
_removeTask(p->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
308
src/framework/eventloop.h
Normal file
308
src/framework/eventloop.h
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
|
||||
#include "logger.h"
|
||||
#include "engine.h"
|
||||
#ifdef USE_THREADS
|
||||
#include "msgqueue.h"
|
||||
#endif
|
||||
|
||||
class Task;
|
||||
class SocketConnection;
|
||||
class ServerSocket;
|
||||
class WorkerProcess;
|
||||
|
||||
/// \brief Manage timers and tasks.
|
||||
///
|
||||
/// This class manages Task objects and their timers. It also manages
|
||||
/// all network connections through an Engine object. The Engine is an
|
||||
/// "inner event loop" used to manage low-level network events.
|
||||
///
|
||||
/// Your code must create an EventLoop object, add one or more Task objects
|
||||
/// to it, and then run the event loop, either "forever" using the method
|
||||
/// EventLoop::runUntilComplete()
|
||||
/// or by regularly calling the method
|
||||
/// EventLoop::run(double timeout_s)
|
||||
class EventLoop : public Logger {
|
||||
public:
|
||||
/// Create a new EventLoop. Normally, the application should have at most
|
||||
/// one EventLoop in each thread.
|
||||
EventLoop(std::string log_label = "MainLoop") :
|
||||
Logger(log_label),
|
||||
engine("NetworkEngine"),
|
||||
name(log_label) {
|
||||
#ifdef USE_THREADS
|
||||
do_init(nullptr);
|
||||
#else
|
||||
do_init();
|
||||
#endif
|
||||
}
|
||||
|
||||
~EventLoop();
|
||||
|
||||
/// Add a task to be managed by the EventLoop. The value of the task
|
||||
/// parameter must be an object of a subclass of Task. You _must_ create
|
||||
/// the object with new; it cannot be an object on the stack.
|
||||
/// The EventLoop will take ownership of the object and will delete it
|
||||
/// when the task has finished. Before it is deleted, the parent's
|
||||
/// taskFinished method will be called unless the parent is nullptr.
|
||||
void addTask(Task *task, Task *parent = nullptr);
|
||||
|
||||
/// Run for at most timeout_s seconds.
|
||||
/// Returns false if all done, otherwise true:
|
||||
bool run(double timeout_s);
|
||||
|
||||
/// Run until all task are done.
|
||||
void runUntilComplete();
|
||||
|
||||
#ifdef USE_THREADS
|
||||
/// Create an EventLoop object that runs the task until it's finished.
|
||||
/// You cannot use this if you have created your own EventLoop object.
|
||||
///
|
||||
/// The "parent" parameter is used if the main thread (the parent) should
|
||||
/// be notified when the thread running the task is finished.
|
||||
static void runTask(Task *task, const std::string &name = "MainLoop",
|
||||
std::ostream *log_file = nullptr,
|
||||
EventLoop *parent = nullptr);
|
||||
#else
|
||||
static void runTask(Task *task, const std::string &name = "MainLoop",
|
||||
std::ostream *log_file = nullptr);
|
||||
#endif
|
||||
|
||||
/// Block current thread until all spawned threads have finished.
|
||||
void waitForThreadsToFinish();
|
||||
|
||||
/// Remove the given task.
|
||||
void abortTask(Task *task);
|
||||
|
||||
/// Remove all tasks.
|
||||
void abort() {
|
||||
interrupt();
|
||||
do_abort = true;
|
||||
}
|
||||
|
||||
/// Get all tasks with the given parent.
|
||||
void getChildTasks(std::set<Task *> &tset, Task *parent) const;
|
||||
|
||||
/// Remove all tasks with the given parent.
|
||||
void abortChildTasks(Task *parent);
|
||||
|
||||
/// Restart idle connections owned by the given task.
|
||||
void wakeUpTask(Task *t);
|
||||
|
||||
/// Restart an idle connection.
|
||||
bool wakeUpConnection(SocketConnection *s) {
|
||||
return engine.wakeUpConnection(s);
|
||||
}
|
||||
|
||||
/// Remove a connection.
|
||||
void cancelConnection(SocketConnection *s) {
|
||||
engine.cancelConnection(s);
|
||||
}
|
||||
|
||||
/// Return all connetcions owned by the given task.
|
||||
std::set<Socket *> findConnByTask(const Task *t) const;
|
||||
|
||||
/// Return true if conn still exists.
|
||||
bool isActive(const Socket *conn) const {
|
||||
return engine.connActive(conn);
|
||||
}
|
||||
|
||||
/// Remove previous timer, run after s seconds instead.
|
||||
/// If s = 0, run timer immediately. If s < 0, remove timer.
|
||||
void resetTimer(Task *task, double s);
|
||||
|
||||
/// Create a new socket connection, and add it to the loop.
|
||||
/// Returns false (and deletes conn) on failure.
|
||||
/// On success, returns true and calls connAdded on owner task.
|
||||
/// A connection to the server will be initiated. When connected, the
|
||||
/// connected() method will be called on conn to get initial state.
|
||||
bool addConnection(SocketConnection *conn);
|
||||
|
||||
/// Use this if conn contains a socket that has already been connected.
|
||||
/// Returns false (and deletes conn) on failure.
|
||||
/// On success, returns true and calls connAdded on owner task,
|
||||
/// then calls connected() on conn to get initial state.
|
||||
bool addConnected(SocketConnection *conn);
|
||||
|
||||
/// Returns false (and deletes conn) on failure.
|
||||
/// On success, returns true and calls serverAdded on owner task.
|
||||
bool addServer(ServerSocket *conn);
|
||||
#ifdef USE_GNUTLS
|
||||
/// Use SSL certificate for a listening socket.
|
||||
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path, const std::string &password) {
|
||||
return engine.tlsSetKey(conn, crt_path, key_path, password);
|
||||
}
|
||||
|
||||
/// Set path to file containing chain of trust for SSL certificate.
|
||||
bool setCABundle(const std::string &path) {
|
||||
return engine.setCABundle(path);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Return true if task is running.
|
||||
bool running(Task *task);
|
||||
|
||||
/// \brief
|
||||
/// Notify EventLoop that a task object it ows has been deleted.
|
||||
///
|
||||
/// This is just a safeguard against buggy clients.
|
||||
/// Only the EventLoop is allowed to delete tasks is owns.
|
||||
void taskDeleted(Task *task);
|
||||
|
||||
/// Call this to make the network engine yield control
|
||||
/// to me (the task supervisor).
|
||||
static void interrupt() {
|
||||
Engine::yield();
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
/// Create a new thread and run task in its own loop in that thread.
|
||||
void spawnThread(Task *task, const std::string &name="ThreadLoop",
|
||||
std::ostream *log_file = nullptr,
|
||||
Task *parent = nullptr);
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
int externalCommand(Task *owner, const char *const argv[]) {
|
||||
// Not implemented
|
||||
exit(1);
|
||||
}
|
||||
#else
|
||||
/// Asynchronously execute an external command.
|
||||
/// Return false on immediate failure.
|
||||
int externalCommand(Task *owner, const char *const argv[]);
|
||||
|
||||
/// Fork into background, detach from shell.
|
||||
static void daemonize();
|
||||
|
||||
/// Create a child process. Return child's PID. Channels can be
|
||||
/// used to pass sockets and messages between parent and child.
|
||||
WorkerProcess *createWorker(Task *parent, std::ostream *log_file,
|
||||
unsigned int channels, unsigned int wno);
|
||||
|
||||
/// Create a child process. Return child's PID. Channels can be
|
||||
/// used to pass sockets and messages between parent and child.
|
||||
WorkerProcess *createWorker(Task *parent, const std::string &log_file_name,
|
||||
unsigned int channels, unsigned int wno);
|
||||
|
||||
/// Send signal to all child processes
|
||||
void killChildProcesses(int signum);
|
||||
|
||||
/// \brief
|
||||
/// Set path to log file.
|
||||
///
|
||||
/// The new log file will be activated upon receiving
|
||||
/// the SIGHUP signal.
|
||||
static void setLogFilename(const std::string &filename) {
|
||||
openFileOnSIGHUP = filename;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// \brief
|
||||
/// Mark the given task as finished.
|
||||
///
|
||||
/// Don't call this method directly. Use Task::setResult instead.
|
||||
void notifyTaskFinished(Task *task) {
|
||||
auto ret = finishedTasks.insert(task);
|
||||
if (ret.second)
|
||||
engine.yield();
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Notify the EventLoop that the given task has a message to deliver.
|
||||
///
|
||||
/// Don't call this method directly. Use Task::setMessage instead.
|
||||
void notifyTaskMessage(Task *task) {
|
||||
auto ret = messageTasks.insert(task);
|
||||
if (ret.second)
|
||||
engine.yield();
|
||||
}
|
||||
|
||||
/// Return true if the EventLoop is about to be terminated.
|
||||
bool aborted() const {
|
||||
return do_abort;
|
||||
}
|
||||
|
||||
/// Add handler for the given OS signal.
|
||||
void addSignalHandler(int signum, void (*handler)(int, EventLoop &));
|
||||
|
||||
/// Enable events from task "from" to task "to". I.e. "from" will be able to
|
||||
/// call executeHandler with "to" as a parameter. If "to" dies before
|
||||
/// "from", "from" will be notified through a call to taskFinished.
|
||||
/// Will return false unless both tasks still exist.
|
||||
bool startObserving(Task *from, Task *to);
|
||||
|
||||
/// Return true if observer is observing task.
|
||||
bool isObserving(Task *observer, Task *task) const {
|
||||
auto p = observed_by.find(observer);
|
||||
return p != observed_by.end() &&
|
||||
p->second.find(task) != p->second.end();
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef _WIN32
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static std::map<int, int> terminatedPIDs;
|
||||
std::map<int, Task *> pidOwner;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static std::string openFileOnSIGHUP;
|
||||
#endif
|
||||
#ifdef USE_THREADS
|
||||
void do_init(EventLoop *parent);
|
||||
|
||||
EventLoop(std::string log_label, EventLoop *parent) :
|
||||
Logger(log_label),
|
||||
engine("NetworkEngine"),
|
||||
name(log_label) {
|
||||
do_init(parent);
|
||||
}
|
||||
std::map<Task *, std::thread> threads;
|
||||
static MsgQueue<Task *> finished_threads;
|
||||
std::map<Task *, Task *> threadTaskObserver;
|
||||
void collect_thread(Task *t);
|
||||
EventLoop *parent_loop;
|
||||
#else
|
||||
void do_init();
|
||||
#endif
|
||||
|
||||
#ifdef USE_THREADS
|
||||
//thread_local
|
||||
#endif
|
||||
static volatile int got_signal;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static volatile int terminatedPIDtmp[100];
|
||||
|
||||
static void signalHandler(int signum);
|
||||
void removeAllTasks();
|
||||
void check_finished();
|
||||
Task *nextTimerToExecute();
|
||||
void _removeTimer(Task *task);
|
||||
void _removeTask(Task *task, bool killed = false);
|
||||
Engine engine;
|
||||
|
||||
// Map each task to its parent (or, if it has no parent, to nullptr)
|
||||
std::map<Task *, Task *> tasks;
|
||||
|
||||
// These are used to keep track of "observation" between tasks.
|
||||
// E.g. a parent task is observing its children.
|
||||
std::map<Task *, std::set<Task *> > observed_by;
|
||||
std::map<Task *, std::set<Task *> > observing;
|
||||
|
||||
std::set<Task *> finishedTasks, messageTasks;
|
||||
std::multimap<int, void (*)(int, EventLoop &)> userSignalHandler;
|
||||
std::multimap<TimePoint, Task *> timer_queue;
|
||||
std::string name;
|
||||
bool do_abort = false;
|
||||
};
|
||||
161
src/framework/loadbalancer.cpp
Normal file
161
src/framework/loadbalancer.cpp
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <signal.h>
|
||||
#include "loadbalancer.h"
|
||||
#include "socketreceiver.h"
|
||||
#include "workerprocess.h"
|
||||
|
||||
LoadBalancer::LoadBalancer(const TaskConfig &tc) :
|
||||
Task("LoadBalancer"), my_config(tc) {
|
||||
try {
|
||||
tot_no_workers = std::stoul(my_config.value("workers"));
|
||||
} catch (...) {
|
||||
tot_no_workers = 0;
|
||||
}
|
||||
auto range = tc.cfg().equal_range("workercfg");
|
||||
for (auto p=range.first; p!=range.second; ++p)
|
||||
(worker_config += p->second) += "\n";
|
||||
}
|
||||
|
||||
LoadBalancer::~LoadBalancer() {
|
||||
for (WorkerProcess *wp : worker_proc)
|
||||
if (wp)
|
||||
delete wp;
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
bool LoadBalancer::tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path,
|
||||
const std::string &password) {
|
||||
portMap[conn] = no_channels;
|
||||
worker_config += "channel" + std::to_string(no_channels) + " tls " +
|
||||
crt_path + " " + key_path + " " + password + "\n";
|
||||
++no_channels;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
double LoadBalancer::start() {
|
||||
if (!tot_no_workers) {
|
||||
setError("Internal error, no workers");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
worker_proc.resize(tot_no_workers);
|
||||
worker_proc_health.resize(tot_no_workers);
|
||||
if (parseListen(my_config, "LoadBalancerSocket")) {
|
||||
for (size_t i=0; i<tot_no_workers; ++i)
|
||||
new_worker(i);
|
||||
} else {
|
||||
setResult("Failed, cannot start server");
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
SocketConnection *LoadBalancer::newClient(int fd, const char *, uint16_t,
|
||||
ServerSocket *srv) {
|
||||
doPass(fd, nextWorker(), srv);
|
||||
rotateNextWorker();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LoadBalancer::serverRemoved(ServerSocket *s) {
|
||||
log() << "Server removed socket " << s;
|
||||
if (SocketReceiver *conn = dynamic_cast<SocketReceiver *>(s)) {
|
||||
if (pid_t peer = conn->peerPid()) {
|
||||
removeWorker(peer);
|
||||
log() << "Kill process " << peer;
|
||||
kill(peer, SIGKILL);
|
||||
}
|
||||
} else {
|
||||
log() << "Not a receiver";
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
void LoadBalancer::doPass(int fd, size_t wid, ServerSocket *srv) {
|
||||
#else
|
||||
void LoadBalancer::doPass(int fd, size_t wid, ServerSocket *) {
|
||||
#endif
|
||||
if (wid >= worker_proc.size() || !worker_proc[wid]) {
|
||||
err_log() << "Worker " << wid << " dead, dropping socket " << fd;
|
||||
return;
|
||||
}
|
||||
#ifdef USE_GNUTLS
|
||||
unsigned int ch = portMap.find(srv) != portMap.end() ? portMap[srv] : 0;
|
||||
#else
|
||||
unsigned int ch = 0;
|
||||
#endif
|
||||
SocketReceiver *channel = worker_proc[wid]->channel(ch);
|
||||
dbg_log() << "New connection fd=" << fd << " pass to worker " << wid;
|
||||
int ret = channel->passSocketToPeer(fd);
|
||||
if (!ret) {
|
||||
worker_proc_health[wid] = TimePoint();
|
||||
} else if (ret == EAGAIN) {
|
||||
// The peer might be broken. Or we have a burst of new connections.
|
||||
// If the peer stays broken for more than 5 seconds, we'll kill it.
|
||||
if (worker_proc_health[wid] == TimePoint()) {
|
||||
warn_log() << "Worker " << wid << " not responding, give it 5s to recover.";
|
||||
worker_proc_health[wid] = timeNow();
|
||||
} else if (secondsSince(worker_proc_health[wid]) > 5) {
|
||||
err_log() << "job queue full, worker " << wid << " will be restarted";
|
||||
removeWorker(worker_proc[wid]->pid());
|
||||
worker_proc_health[wid] = TimePoint();
|
||||
} else {
|
||||
err_log() << "worker " << wid << " busy, cannot pass socket";
|
||||
}
|
||||
} else {
|
||||
err_log() << "cannot pass socket to worker " << wid << " " << strerror(ret);
|
||||
}
|
||||
}
|
||||
|
||||
void LoadBalancer::processFinished(int pid, int wstatus) {
|
||||
log() << "End of PID " << pid << ", status " << wstatus;
|
||||
removeWorker(pid);
|
||||
}
|
||||
|
||||
void LoadBalancer::new_worker(size_t i) {
|
||||
if (worker_proc[i]) {
|
||||
delete worker_proc[i];
|
||||
worker_proc[i] = nullptr;
|
||||
}
|
||||
|
||||
if (terminated())
|
||||
return;
|
||||
|
||||
std::string logfilename = my_config.value("workerlog");
|
||||
if (logfilename.empty()) {
|
||||
worker_proc[i] = createWorker(nullptr, no_channels,
|
||||
static_cast<unsigned int>(i));
|
||||
} else {
|
||||
// In logfilename, replace all %d with worker number (i.e. i),
|
||||
// adjusting to fixed width by filling with zeroes.
|
||||
auto len = std::to_string(tot_no_workers).size();
|
||||
auto wno = std::to_string(i);
|
||||
wno = std::string(len-wno.size(), '0') + wno;
|
||||
while (true) {
|
||||
auto pos = logfilename.find("%d");
|
||||
if (pos == std::string::npos)
|
||||
break;
|
||||
logfilename.replace(pos, 2, wno);
|
||||
}
|
||||
worker_proc[i] = createWorker(logfilename, no_channels,
|
||||
static_cast<unsigned int>(i));
|
||||
}
|
||||
if (worker_proc[i])
|
||||
log() << "Created worker " << i << ", pid: " << worker_proc[i]->pid();
|
||||
}
|
||||
|
||||
void LoadBalancer::removeWorker(pid_t pid) {
|
||||
for (size_t i=0; i<worker_proc.size(); ++i)
|
||||
if (worker_proc[i] && worker_proc[i]->pid() == pid) {
|
||||
if (max_retries) {
|
||||
--max_retries;
|
||||
} else {
|
||||
setResult("Too many failures");
|
||||
}
|
||||
new_worker(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
78
src/framework/loadbalancer.h
Normal file
78
src/framework/loadbalancer.h
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "task.h"
|
||||
#include "socketreceiver.h"
|
||||
#include "workerprocess.h"
|
||||
|
||||
/// \brief
|
||||
/// Create worker (child) processes, and pass new connections
|
||||
/// evenly among them.
|
||||
///
|
||||
/// New clients are passed to the worker processes using a round-robin
|
||||
/// algorithm. Override the Task::newClient method to use a more
|
||||
/// sophisticated algorithm.
|
||||
class LoadBalancer : public Task {
|
||||
public:
|
||||
|
||||
LoadBalancer(const TaskConfig &tc);
|
||||
|
||||
~LoadBalancer() override;
|
||||
|
||||
SocketConnection *newClient(int fd, const char *, uint16_t,
|
||||
ServerSocket *) override;
|
||||
|
||||
double start() override;
|
||||
#ifdef USE_GNUTLS
|
||||
bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path,
|
||||
const std::string &password) override;
|
||||
#endif
|
||||
void serverRemoved(ServerSocket *s) override;
|
||||
void processFinished(int pid, int wstatus) override;
|
||||
protected:
|
||||
/// Return a worker number.
|
||||
size_t nextWorker() const {
|
||||
return next_worker;
|
||||
}
|
||||
|
||||
/// Move on to next worker (in a round-robin fashion) for
|
||||
/// LoadBalancer::nextWorker.
|
||||
void rotateNextWorker() {
|
||||
if (++next_worker >= worker_proc.size())
|
||||
next_worker = 0;
|
||||
}
|
||||
|
||||
/// Pass a connection to a worker process.
|
||||
void doPass(int fd, size_t wid, ServerSocket *srv);
|
||||
|
||||
/// Create a new worker process.
|
||||
void new_worker(size_t i);
|
||||
|
||||
/// Remove a worker process.
|
||||
void removeWorker(pid_t pid);
|
||||
|
||||
/// Return configuration of a worker process.
|
||||
std::string workerConfig(unsigned int i=0) const {
|
||||
return worker_config + "\nworker_number " + std::to_string(i);
|
||||
}
|
||||
|
||||
/// Max number of times to restart failed worker processes.
|
||||
void setMaxRetries(unsigned int n) {
|
||||
max_retries = n;
|
||||
}
|
||||
private:
|
||||
#ifdef USE_GNUTLS
|
||||
std::map<ServerSocket *, unsigned int> portMap;
|
||||
#endif
|
||||
unsigned int max_retries = 100;
|
||||
unsigned int no_channels = 1;
|
||||
std::vector<WorkerProcess *> worker_proc;
|
||||
std::vector<TimePoint> worker_proc_health;
|
||||
size_t next_worker = 0;
|
||||
TaskConfig my_config;
|
||||
std::string worker_config;
|
||||
size_t tot_no_workers;
|
||||
};
|
||||
179
src/framework/logger.cpp
Normal file
179
src/framework/logger.cpp
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
#include "logger.h"
|
||||
#include <iomanip>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
double Logger::secondsSince(const TimePoint &t) {
|
||||
auto now = timeNow();
|
||||
std::chrono::duration<double> d = now - t;
|
||||
return d.count();
|
||||
}
|
||||
|
||||
double Logger::secondsTo(const TimePoint &t) {
|
||||
auto now = timeNow();
|
||||
std::chrono::duration<double> d = t - now;
|
||||
return d.count();
|
||||
}
|
||||
|
||||
int64_t Logger::msSince(const TimePoint &t) {
|
||||
auto now = timeNow();
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>
|
||||
(now - t).count();
|
||||
}
|
||||
|
||||
int64_t Logger::msTo(const TimePoint &t) {
|
||||
auto now = timeNow();
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>
|
||||
(t - now).count();
|
||||
}
|
||||
|
||||
std::string Logger::dateString(time_t t) {
|
||||
// put_time might not be available in older libs.
|
||||
char dstr[200];
|
||||
if (!t)
|
||||
t = time(nullptr);
|
||||
struct tm *dtm = localtime(&t);
|
||||
size_t len = strftime(dstr, sizeof(dstr), "%FT%T%z", dtm);
|
||||
return std::string(dstr, len);
|
||||
}
|
||||
|
||||
std::string Logger::dateString2(time_t t) {
|
||||
// put_time might not be available in older libs.
|
||||
char dstr[200];
|
||||
if (!t)
|
||||
t = time(nullptr);
|
||||
struct tm *dtm = localtime(&t);
|
||||
size_t len = strftime(dstr, sizeof(dstr), "%a, %d %b %Y %H:%M:%S", dtm);
|
||||
return std::string(dstr, len);
|
||||
}
|
||||
|
||||
std::string Logger::createHashKey(unsigned int length) {
|
||||
std::random_device rng;
|
||||
std::ostringstream s;
|
||||
std::uniform_int_distribution<unsigned short> dist;
|
||||
while (s.str().size() < length)
|
||||
s << std::setw(sizeof(unsigned short)*2) << std::uppercase
|
||||
<< std::hex << std::setfill('0') << dist(rng);
|
||||
return s.str();
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
bool Logger::in_error = false;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
TimePoint Logger::global_start_time(timeNow());
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
std::ostream *Logger::_logFile = &std::cerr;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::log_count = 100000;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::warn_count = 10000;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::err_count = 10000;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::log_count_saved = 100000;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::warn_count_saved = 10000;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
unsigned int Logger::err_count_saved = 10000;
|
||||
|
||||
DummyStream Logger::_dummyLog;
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
std::ostringstream Logger::_blackHole;
|
||||
|
||||
DummyStream::~DummyStream() {
|
||||
}
|
||||
|
||||
void Logger::setLogLimit(unsigned int loglines, unsigned int warnlines,
|
||||
unsigned int errlines) {
|
||||
if (loglines)
|
||||
log_count_saved = loglines;
|
||||
if (warnlines)
|
||||
warn_count_saved = warnlines;
|
||||
if (errlines)
|
||||
err_count_saved = errlines;
|
||||
|
||||
log_count = log_count_saved;
|
||||
warn_count = warn_count_saved;
|
||||
err_count = err_count_saved;
|
||||
}
|
||||
|
||||
void Logger::sayTime(std::ostream &stream) {
|
||||
std::time_t t = std::time(nullptr);
|
||||
std::tm *tm = std::localtime(&t);
|
||||
stream << tm->tm_year+1900 << '-'
|
||||
<< std::setfill('0') << std::setw(2) << tm->tm_mon+1 << '-'
|
||||
<< std::setfill('0') << std::setw(2) << tm->tm_mday << ' '
|
||||
<< std::setfill('0') << std::setw(2) << tm->tm_hour << ':'
|
||||
<< std::setfill('0') << std::setw(2) << tm->tm_min << ':'
|
||||
<< std::setfill('0') << std::setw(2) << tm->tm_sec;
|
||||
}
|
||||
|
||||
#ifdef TASKRUNNER_LOGERR
|
||||
std::ostream &Logger::errno_log() const {
|
||||
if (err_count) {
|
||||
in_error = true;
|
||||
--err_count;
|
||||
*_logFile << "\n" << global_elapsed_ms() << ' ' << _label << "*** "
|
||||
<< (err_count ? "ERROR ***: " : "LAST ERR ***: ")
|
||||
#ifdef _WIN32
|
||||
<< std::to_string(WSAGetLastError())
|
||||
#else
|
||||
<< strerror(errno)
|
||||
#endif
|
||||
<< ": ";
|
||||
return *_logFile;
|
||||
} else {
|
||||
return _blackHole;
|
||||
}
|
||||
}
|
||||
#else
|
||||
DummyStream &Logger::errno_log() const {
|
||||
return _dummyLog;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::setLogFile(std::ostream &stream) {
|
||||
_logFile = &stream;
|
||||
in_error = false;
|
||||
global_start_time = timeNow();
|
||||
sayTime(stream);
|
||||
}
|
||||
|
||||
void Logger::reopenLogFile(const std::string &filename) {
|
||||
if (std::ofstream *the_log = dynamic_cast<std::ofstream *>(_logFile)) {
|
||||
*the_log << "\n";
|
||||
the_log->close();
|
||||
the_log->open(filename, std::ios::app);
|
||||
}
|
||||
}
|
||||
382
src/framework/logger.h
Normal file
382
src/framework/logger.h
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifdef max
|
||||
#undef max
|
||||
#endif
|
||||
#else
|
||||
#include <string.h>
|
||||
#endif
|
||||
|
||||
#ifndef DEBUG
|
||||
#define DEBUG 0
|
||||
#endif
|
||||
#ifndef TARGET_OS_IPHONE
|
||||
#define TARGET_OS_IPHONE 0
|
||||
#endif
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#ifdef __ANDROID_API__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
|
||||
/// This class is used to optionally disable logging operations at compile time.
|
||||
class DummyStream {
|
||||
public:
|
||||
/// Do nothing, emulating the << stream operator.
|
||||
template<class T>
|
||||
DummyStream &operator<<(T ) { return *this; }
|
||||
#ifdef __ANDROID_API__
|
||||
DummyStream &operator<<(const char *s) {
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%s", s);
|
||||
return *this; }
|
||||
DummyStream &operator<<(std::string s) {
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%s", s.c_str());
|
||||
return *this; }
|
||||
DummyStream &operator<<(int i) {
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%d", i);
|
||||
return *this; }
|
||||
DummyStream &operator<<(double x) {
|
||||
__android_log_print(ANDROID_LOG_VERBOSE, "BBK", "%f", x);
|
||||
return *this; }
|
||||
#endif
|
||||
/// Do nothing, emulating the << stream operator.
|
||||
DummyStream& operator<<(std::ostream &(*)(std::ostream &) ) {
|
||||
return *this;
|
||||
}
|
||||
virtual ~DummyStream();
|
||||
private:
|
||||
};
|
||||
|
||||
/// \class TimePoint
|
||||
/// \brief
|
||||
/// Measure elapsed time during execution,
|
||||
/// for example by timer events.
|
||||
///
|
||||
/// It is simply a typedef to std::chrono::steady_clock::time_point.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// TimePoint start = Logger::timeNow();
|
||||
/// // Do stuff...
|
||||
/// std::cout << Logger::secondsSince(start) << " seconds have elapsed.";
|
||||
/// // 2.00042 seconds have elapsed.
|
||||
typedef std::chrono::steady_clock::time_point TimePoint;
|
||||
|
||||
/// \brief
|
||||
/// A simple logger. All classes that want to write to the global log
|
||||
/// file should inherit from this class.
|
||||
///
|
||||
/// By default, logs will be written to cerr. To log elsewhere, you must
|
||||
/// call the static function Logger::setLogFile with a stream object (e.g. an
|
||||
/// std::ofstream or an std::ostringstream) which the logs will be written to.
|
||||
/// The stream will be used globally. You must make sure the global stream
|
||||
/// is never destroyed, at least not before Logger::setLogFile has been called
|
||||
/// with another stream.
|
||||
class Logger {
|
||||
public:
|
||||
/// Each object of the Logger class (or its subclasses) have a log label,
|
||||
/// which will often be thought of as the name of the object.
|
||||
Logger(std::string label) :
|
||||
_label(label) {
|
||||
// TODO: single initialisation
|
||||
_blackHole.clear(std::istream::eofbit);
|
||||
}
|
||||
|
||||
/// \brief Set global log destination.
|
||||
///
|
||||
/// The given stream will be the destination of all subsequent log calls
|
||||
/// globally. You must make sure the global stream never is destroyed, at
|
||||
/// least not until Logger::setLogFile is called with another stream.
|
||||
static void setLogFile(std::ostream &stream);
|
||||
|
||||
/// If current log is a file (ofstream), reopen it with new filename:
|
||||
static void reopenLogFile(const std::string &filename);
|
||||
|
||||
/// \brief Set max number of lines of info/warn/err log.
|
||||
///
|
||||
/// If 0, reset to previous (non-zero) max number of lines.
|
||||
/// After the limit has been reached, no more lines for that log level
|
||||
/// will be printed until the limit has been reset.
|
||||
static void setLogLimit(unsigned int loglines = 0,
|
||||
unsigned int warnlines = 0,
|
||||
unsigned int errlines = 0);
|
||||
|
||||
/// Write current local time to the given stream
|
||||
static void sayTime(std::ostream &stream);
|
||||
|
||||
/// Return true if any error has been logged (globally since start)
|
||||
static bool inError() {
|
||||
return in_error;
|
||||
}
|
||||
|
||||
/// \brief Write a line of error log.
|
||||
///
|
||||
/// Access the current global error log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// In non-static members of subclasses to Logger, the method
|
||||
/// Logger::err_log() should be used instead of this function.
|
||||
static std::ostream &err_log(const std::string &label) {
|
||||
if (err_count) {
|
||||
in_error = true;
|
||||
--err_count;
|
||||
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << " *** "
|
||||
<< (err_count ? "ERROR ***: " : "LAST ERR ***: ");
|
||||
return *_logFile;
|
||||
} else {
|
||||
return _blackHole;
|
||||
}
|
||||
}
|
||||
|
||||
/// \brief Write a line of warning log.
|
||||
///
|
||||
/// Access the current global warning log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// In non-static members of subclasses to Logger, the method
|
||||
/// Logger::warn_log() should be used instead of this function.
|
||||
static std::ostream &warn_log(const std::string &label) {
|
||||
if (warn_count) {
|
||||
--warn_count;
|
||||
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << " *** "
|
||||
<< (warn_count ? "WARNING ***: " : "LAST WARN ***: ");
|
||||
return *_logFile;
|
||||
} else {
|
||||
return _blackHole;
|
||||
}
|
||||
}
|
||||
|
||||
/// \brief Write a line of info log.
|
||||
///
|
||||
/// Access the current global info log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// In non-static members of subclasses to Logger, the method
|
||||
/// Logger::warn_log() should be used instead of this function.
|
||||
static std::ostream &log(const std::string &label) {
|
||||
if (log_count) {
|
||||
--log_count;
|
||||
*_logFile << "\n" << global_elapsed_ms() << ' ' << label << ": ";
|
||||
if (!log_count)
|
||||
*_logFile << "LAST LOG: ";
|
||||
return *_logFile;
|
||||
} else {
|
||||
return _blackHole;
|
||||
}
|
||||
}
|
||||
|
||||
/// Anything written to the global log may be buffered for quite some time,
|
||||
/// and thus not visible in the destination file. This method will flush
|
||||
/// the buffer and write an extra empty line.
|
||||
///
|
||||
/// Calling this often may be bad for performance.
|
||||
static void flushLogFile() {
|
||||
*_logFile << std::endl;
|
||||
}
|
||||
|
||||
/// Disable all log output until next call to Logger::setLogFile.
|
||||
static void pauseLogging() {
|
||||
_logFile = &_blackHole;
|
||||
}
|
||||
|
||||
/// Return number of seconds since the given TimePoint.
|
||||
/// The returned value might be negative.
|
||||
static double secondsSince(const TimePoint &t);
|
||||
|
||||
/// Return number of seconds until the given TimePoint.
|
||||
/// The returned value might be negative.
|
||||
static double secondsTo(const TimePoint &t);
|
||||
|
||||
/// Return number of milliseconds since the given TimePoint.
|
||||
/// The returned value might be negative.
|
||||
static int64_t msSince(const TimePoint &t);
|
||||
|
||||
/// Return number of milliseconds until the given TimePoint.
|
||||
/// The returned value might be negative.
|
||||
static int64_t msTo(const TimePoint &t);
|
||||
|
||||
/// Return true if current time is after the given TimePoint.
|
||||
static bool hasExpired(const TimePoint &t) {
|
||||
return secondsSince(t) >= 0;
|
||||
}
|
||||
|
||||
/// Return current time.
|
||||
static TimePoint timeNow() {
|
||||
return std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
/// Return current time plus s seconds.
|
||||
static TimePoint timeAfter(double s) {
|
||||
return timeNow() + std::chrono::microseconds(toUs(s));
|
||||
}
|
||||
|
||||
/// Return a very distant time.
|
||||
static TimePoint timeMax() {
|
||||
return TimePoint::max();
|
||||
}
|
||||
|
||||
/// Convert s (seconds) to std::chrono::microseconds
|
||||
static std::chrono::microseconds toUs(double t) {
|
||||
auto us = static_cast<std::chrono::microseconds::rep>(1e6*t);
|
||||
return std::chrono::microseconds(us);
|
||||
}
|
||||
|
||||
/// Return local time, formatted as 2023-10-14T09:38:47+0200
|
||||
static std::string dateString(time_t t = 0);
|
||||
|
||||
/// Return local time, formatted as Sat, 14 Oct 2023 09:38:47
|
||||
static std::string dateString2(time_t t = 0);
|
||||
|
||||
/// \brief Return a random string.
|
||||
///
|
||||
/// Create string of length random hex chars from system's random number
|
||||
/// generator. The length should be a multiple of 4.
|
||||
static std::string createHashKey(unsigned int length = 20);
|
||||
|
||||
/// Return the object's log label.
|
||||
std::string label() const {
|
||||
return _label;
|
||||
}
|
||||
|
||||
/// Modify the object's log label
|
||||
void resetLabel(const std::string &new_label) {
|
||||
_label = new_label;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
#if DEBUG
|
||||
#define TASKRUNNER_LOGERR
|
||||
#define TASKRUNNER_LOGWARN
|
||||
#define TASKRUNNER_LOGINFO
|
||||
#define TASKRUNNER_LOGBDG
|
||||
#endif
|
||||
|
||||
#ifdef TASKRUNNER_LOGERR
|
||||
/// \brief Write a line of error log after a failed system call
|
||||
/// has set the global errno to a non-zero value.
|
||||
///
|
||||
/// Access the current global error log stream. A line feed and a preamble,
|
||||
/// including the latest OS error, will be written to the stream.
|
||||
///
|
||||
/// *Note!* The global error stream will be "disabled" (i.e. set to a dummy stream)
|
||||
/// unless compiler macro TASKRUNNER_LOGERR is defined.
|
||||
std::ostream &errno_log() const;
|
||||
|
||||
/// \brief Write a line of error log.
|
||||
///
|
||||
/// Access the current global error log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// *Note!* The global error log stream will be "disabled" (i.e. set to a
|
||||
/// dummy stream) unless compiler macro TASKRUNNER_LOGERR is defined.
|
||||
///
|
||||
/// May be used in any non-static member of any subclass. Example:
|
||||
///
|
||||
/// err_log() << "Child task " << t->label() << " failed.";
|
||||
std::ostream &err_log() const {
|
||||
return err_log(_label);
|
||||
}
|
||||
#else
|
||||
DummyStream &errno_log() const;
|
||||
static DummyStream &err_log() {
|
||||
return _dummyLog;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef TASKRUNNER_LOGWARN
|
||||
/// \brief Write a line of warning log.
|
||||
///
|
||||
/// Access the current global warning log stream. A line feed and a
|
||||
/// preamble will be written to the stream. Then send whetever you want to
|
||||
/// the log stream using the standard std::ostream API.
|
||||
///
|
||||
/// *Note!* The global warning log stream will be "disabled" (i.e. set to a
|
||||
/// dummy stream) unless compiler macro TASKRUNNER_LOGWARN is defined.
|
||||
std::ostream &warn_log() const {
|
||||
return warn_log(_label);
|
||||
}
|
||||
#else
|
||||
static DummyStream &warn_log() {
|
||||
return _dummyLog;
|
||||
}
|
||||
#endif
|
||||
#ifdef TASKRUNNER_LOGINFO
|
||||
/// \brief Write a line of info log.
|
||||
///
|
||||
/// Access the current global info log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// *Note!* The global info log stream will be "disabled" (i.e. set to a
|
||||
/// dummy stream) unless compiler macro TASKRUNNER_LOGINFO is defined.
|
||||
std::ostream &log() const {
|
||||
return log(_label);
|
||||
}
|
||||
#else
|
||||
static DummyStream &log() {
|
||||
return _dummyLog;
|
||||
}
|
||||
#endif
|
||||
#ifdef TASKRUNNER_LOGDBG
|
||||
/// \brief Write a line of debug log.
|
||||
///
|
||||
/// Access the current global debug log stream. A line feed and a preamble
|
||||
/// will be written to the stream. Then send whetever you want to the log
|
||||
/// stream using the standard std::ostream API.
|
||||
///
|
||||
/// *Note!* The global debug log stream will be "disabled" (i.e. set to a
|
||||
/// dummy stream) unless compiler macro TASKRUNNER_LOGDBG is defined.
|
||||
std::ostream &dbg_log() const {
|
||||
*_logFile << "\n" << global_elapsed_ms() << ' ' << _label << ": ";
|
||||
return *_logFile;
|
||||
}
|
||||
#else
|
||||
static DummyStream &dbg_log() {
|
||||
return _dummyLog;
|
||||
}
|
||||
#endif
|
||||
private:
|
||||
static int64_t global_elapsed_ms() {
|
||||
return msSince(global_start_time);
|
||||
}
|
||||
std::string _label;
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static bool in_error;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static TimePoint global_start_time;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static std::ostream *_logFile;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static std::ostringstream _blackHole;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static unsigned int log_count, warn_count, err_count,
|
||||
log_count_saved, warn_count_saved, err_count_saved;
|
||||
static DummyStream _dummyLog;
|
||||
};
|
||||
140
src/framework/mk.inc
Normal file
140
src/framework/mk.inc
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
ALL += $(TARGET) $(PROGRAMS)
|
||||
|
||||
all: $(ALL)
|
||||
|
||||
OS:=$(shell uname)
|
||||
|
||||
CXXFLAGS += -O2 -W -Wall -I$(DIRLEVEL)
|
||||
|
||||
GLIB_COMPILE_RESOURCES = glib-compile-resources
|
||||
|
||||
ifeq ($(OS),Linux)
|
||||
CXX = g++
|
||||
endif
|
||||
|
||||
ifeq ($(OS),Darwin)
|
||||
CXX = c++
|
||||
CXXFLAGS += -x objective-c++
|
||||
LIBS += -framework Foundation -framework ApplicationServices
|
||||
endif
|
||||
|
||||
ifeq ($(OS),OpenBSD)
|
||||
#CXX = ec++
|
||||
CXX = clang++
|
||||
endif
|
||||
|
||||
ifeq ($(OS),FreeBSD)
|
||||
CXX = c++
|
||||
endif
|
||||
|
||||
ifeq ($(OS),NetBSD)
|
||||
CXX = clang++
|
||||
CXXFLAGS += -I/usr/pkg/include
|
||||
LIBS += -Wl,-R/usr/pkg/lib -L/usr/pkg/lib
|
||||
endif
|
||||
|
||||
CXXFLAGS += -std=c++14
|
||||
|
||||
LINK ?= $(CXX)
|
||||
|
||||
ifeq ($(STATIC),1)
|
||||
LDFLAGS += -static-libstdc++
|
||||
endif
|
||||
|
||||
ifeq ($(STATIC),2)
|
||||
LDFLAGS += -static -static-libstdc++
|
||||
endif
|
||||
|
||||
ifneq ($(findstring clang,$(CXX)),)
|
||||
CXXFLAGS += -Weverything -Wno-c++98-compat -Wno-exit-time-destructors \
|
||||
-Wno-global-constructors -Wno-padded -Wno-disabled-macro-expansion \
|
||||
-Wno-float-equal
|
||||
endif
|
||||
|
||||
ifeq ($(CXX),g++)
|
||||
CXXFLAGS += --pedantic -Wextra
|
||||
endif
|
||||
|
||||
ifeq ($(GNUTLS),1)
|
||||
CXXFLAGS += -DUSE_GNUTLS -I/usr/local/include
|
||||
LIBS += -L/usr/local/lib -lgnutls
|
||||
endif
|
||||
ifeq ($(THREADS),1)
|
||||
CXXFLAGS += -DUSE_THREADS
|
||||
LIBS += -pthread
|
||||
endif
|
||||
ifeq ($(LOGLEVEL),)
|
||||
LOGLEVEL=info
|
||||
endif
|
||||
ifeq ($(LOGLEVEL),err)
|
||||
CXXFLAGS += -DTASKRUNNER_LOGERR
|
||||
endif
|
||||
ifeq ($(LOGLEVEL),warn)
|
||||
CXXFLAGS += -DTASKRUNNER_LOGWARN -DTASKRUNNER_LOGERR
|
||||
endif
|
||||
ifeq ($(LOGLEVEL),info)
|
||||
CXXFLAGS += -DTASKRUNNER_LOGERR -DTASKRUNNER_LOGWARN -DTASKRUNNER_LOGINFO
|
||||
endif
|
||||
ifeq ($(LOGLEVEL),dbg)
|
||||
CXXFLAGS += -DTASKRUNNER_LOGERR -DTASKRUNNER_LOGWARN
|
||||
CXXFLAGS += -DTASKRUNNER_LOGINFO -DTASKRUNNER_LOGDBG
|
||||
endif
|
||||
ifeq ($(SANDBOXED),1)
|
||||
CXXFLAGS += -DIS_SANDBOXED
|
||||
endif
|
||||
|
||||
SOURCES += \
|
||||
$(DIRLEVEL)/framework/task.cpp \
|
||||
$(DIRLEVEL)/framework/taskconfig.cpp \
|
||||
$(DIRLEVEL)/framework/engine.cpp \
|
||||
$(DIRLEVEL)/framework/eventloop.cpp \
|
||||
$(DIRLEVEL)/framework/socket.cpp \
|
||||
$(DIRLEVEL)/framework/socketconnection.cpp \
|
||||
$(DIRLEVEL)/framework/serversocket.cpp \
|
||||
$(DIRLEVEL)/framework/socketreceiver.cpp \
|
||||
$(DIRLEVEL)/framework/logger.cpp \
|
||||
$(DIRLEVEL)/framework/bridgetask.cpp \
|
||||
$(DIRLEVEL)/framework/synchronousbridge.cpp \
|
||||
$(DIRLEVEL)/json11/json11.cpp
|
||||
|
||||
OPT_SOURCES += \
|
||||
$(DIRLEVEL)/framework/shortmessageconnection.cpp \
|
||||
$(DIRLEVEL)/framework/threadbridge.cpp \
|
||||
$(DIRLEVEL)/framework/unixdomainbridge.cpp \
|
||||
$(DIRLEVEL)/framework/unixdomainclient.cpp \
|
||||
$(DIRLEVEL)/framework/loadbalancer.cpp
|
||||
|
||||
OBJ=$(SOURCES:.cpp=.o)
|
||||
EXTRA_OBJ=$(EXTRA_SOURCES:.cpp=.o)
|
||||
OPT_OBJ=$(OPT_SOURCES:.cpp=.o)
|
||||
RC_OBJ=$(RC_SOURCES:.rc.xml=.rc.o)
|
||||
RC_DEPS=$(RC_SOURCES:.rc.xml=.rc_d)
|
||||
RC_DEPS+=$(RC_SOURCES:.rc.xml=.rc.d)
|
||||
RC_INTERMEDIATE+=$(RC_SOURCES:.rc.xml=.rc.cpp)
|
||||
|
||||
%.d: %.cpp
|
||||
$(CXX) -MM $(CXXFLAGS) -MT $(@:.d=.o) $< > $@
|
||||
|
||||
%.o: %.cpp
|
||||
$(CXX) $(CXXFLAGS) -c $< -o $@
|
||||
|
||||
%.rc_d: %.rc.xml
|
||||
$(GLIB_COMPILE_RESOURCES) --generate-dependencies --dependency-file=$@ $<
|
||||
|
||||
%.rc.cpp: %.rc.xml
|
||||
$(GLIB_COMPILE_RESOURCES) --generate-source $< --target=$@
|
||||
|
||||
$(TARGET): $(OBJ) $(RC_OBJ)
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(LIBS)
|
||||
|
||||
$(PROGRAMS):
|
||||
$(CXX) $(LDFLAGS) -o $@ $^ $(LIBS)
|
||||
|
||||
ifneq ($(MAKECMDGOALS),clean)
|
||||
include $(SOURCES:.cpp=.d) $(RC_DEPS) $(EXTRA_SOURCES:.cpp=.d)
|
||||
endif
|
||||
|
||||
clean:
|
||||
$(RM) $(CLEAN) $(TARGET) $(OBJ) $(OPT_OBJ) $(EXTRA_OBJ) *~ \
|
||||
$(SOURCES:.cpp=.d) $(EXTRA_SOURCES:.cpp=.d) \
|
||||
$(RC_OBJ) $(RC_DEPS) $(RC_INTERMEDIATE) $(PROGRAMS)
|
||||
72
src/framework/msgqueue.h
Normal file
72
src/framework/msgqueue.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <queue>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
|
||||
/// \brief
|
||||
/// Thread safe queue.
|
||||
///
|
||||
/// By design, there is no method named `pop`.
|
||||
/// To retrieve an object from the queue, you must
|
||||
/// use either the non-blocking MsgQueue::fetch
|
||||
/// or the blocking MsgQueue::pop_blocking method.
|
||||
template <class T>
|
||||
class MsgQueue {
|
||||
public:
|
||||
MsgQueue() :
|
||||
queue(),
|
||||
mutex(),
|
||||
cond() {
|
||||
}
|
||||
|
||||
~MsgQueue() {
|
||||
}
|
||||
|
||||
/// Add object at the end of the queue.
|
||||
void push(T t) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
queue.push(t);
|
||||
cond.notify_one();
|
||||
}
|
||||
|
||||
/// Return true if the queue is empty.
|
||||
bool empty() {
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
return queue.empty();
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Wait until there is an object in the queue, then
|
||||
/// remove and return the first object.
|
||||
T pop_blocking() {
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
while (queue.empty()) {
|
||||
cond.wait(lock);
|
||||
}
|
||||
T val = queue.front();
|
||||
queue.pop();
|
||||
return val;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// A non-blocking pop.
|
||||
///
|
||||
/// If the queue is empty, return false.
|
||||
/// Otherwise remove the first object from the queue,
|
||||
/// assign it to `val`, and return true.
|
||||
bool fetch(T &val)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
if (queue.empty())
|
||||
return false;
|
||||
val = queue.front();
|
||||
queue.pop();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::queue<T> queue;
|
||||
mutable std::mutex mutex;
|
||||
std::condition_variable cond;
|
||||
};
|
||||
25
src/framework/pollstate.h
Normal file
25
src/framework/pollstate.h
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
/*! \file */
|
||||
|
||||
/// \enum PollState
|
||||
/// After doing an operation on a socket, a PollState must be returned to the
|
||||
/// network engine to describe what you want it to do next with the socket.
|
||||
enum class PollState {
|
||||
NONE, /**< Do nothing right now, but keep socket open for later. */
|
||||
READ_BLOCKED, /**< Don't check for incoming data/close, but check for
|
||||
writability if wantToSend(). */
|
||||
CONNECTING, /**< Wait for asynchronous connect to complete. */
|
||||
#ifdef USE_GNUTLS
|
||||
TLS_HANDSHAKE, /**< Wait for TLS handshake to complete. */
|
||||
#endif
|
||||
CLOSE, /**< Close the socket gracefully. */
|
||||
KEEPALIVE, /**< Put the connected socket in keep-alive cache. */
|
||||
KILL, /**< Terminate connection immediately, discarding buffers. */
|
||||
READ, /**< Check for incoming data/close. */
|
||||
WRITE, /**< Check for close, and for writability. */
|
||||
READ_WRITE /**< Check for incoming data/close, and for writability. */
|
||||
};
|
||||
51
src/framework/serversocket.cpp
Normal file
51
src/framework/serversocket.cpp
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <WS2tcpip.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
#include "serversocket.h"
|
||||
class Task;
|
||||
|
||||
ServerSocket::ServerSocket(const std::string &label, Task *task,
|
||||
uint16_t port, const std::string &ip) :
|
||||
Socket(label, task, ip, port) {
|
||||
}
|
||||
|
||||
ServerSocket::~ServerSocket() {
|
||||
}
|
||||
|
||||
SocketConnection *ServerSocket::incoming() {
|
||||
struct sockaddr_storage remoteaddr; // client address
|
||||
socklen_t addrlen = sizeof(remoteaddr);
|
||||
int fd = accept(socket(),
|
||||
reinterpret_cast<sockaddr *>(&remoteaddr), &addrlen);
|
||||
if (fd < 0) {
|
||||
if (errno == EMFILE)
|
||||
Engine::notifyOutOfFds();
|
||||
errno_log() << "accept failure on socket " << socket();
|
||||
return nullptr;
|
||||
}
|
||||
if (!setNonBlocking(fd)) {
|
||||
log() << "cannot set non-blocking " << fd;
|
||||
closeSocket(fd);
|
||||
return nullptr;
|
||||
}
|
||||
uint16_t port;
|
||||
const char *ip = getIp(reinterpret_cast<sockaddr *>(&remoteaddr), &port);
|
||||
log() << "Incoming socket " << fd << " from " << ip << " port " << port;
|
||||
|
||||
SocketConnection *conn = owner()->newClient(fd, ip, port, this);
|
||||
if (!conn)
|
||||
closeSocket(fd);
|
||||
return conn;
|
||||
}
|
||||
58
src/framework/serversocket.h
Normal file
58
src/framework/serversocket.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "socket.h"
|
||||
#include "task.h"
|
||||
|
||||
class SocketConnection;
|
||||
|
||||
/// \brief
|
||||
/// Listen on a single socket for incoming connections.
|
||||
///
|
||||
/// The owner task decides what to do when someone tries to connect.
|
||||
class ServerSocket : public Socket {
|
||||
public:
|
||||
/// \brief
|
||||
/// Create a new server socket.
|
||||
///
|
||||
/// Will listen on the given ip address and port number.
|
||||
///
|
||||
/// *Note!* If ip is an empty string, the socket will listen
|
||||
/// on all local IPv4 and IPv6 addresses.
|
||||
ServerSocket(const std::string &label, Task *task,
|
||||
uint16_t port, const std::string &ip = "127.0.0.1");
|
||||
|
||||
/// \brief
|
||||
/// Create a new server to listen on an existing file descriptor.
|
||||
ServerSocket(int fd, const std::string &label, Task *owner) :
|
||||
Socket(label, owner, fd) {
|
||||
}
|
||||
|
||||
virtual ~ServerSocket() override;
|
||||
|
||||
/// Server sockets shall not be cached.
|
||||
std::string cacheLabel() override {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
/// Schedule listen socket for removal.
|
||||
void stopListening() {
|
||||
closeMe();
|
||||
}
|
||||
|
||||
/// Return Connection object if new client available, else return nullptr.
|
||||
virtual SocketConnection *incoming();
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
void tlsSetKey(unsigned int i) {
|
||||
tlsKeyIndex = i;
|
||||
}
|
||||
unsigned int tlsKey() const {
|
||||
return tlsKeyIndex;
|
||||
}
|
||||
private:
|
||||
unsigned int tlsKeyIndex = 0;
|
||||
#endif
|
||||
};
|
||||
68
src/framework/shortmessageconnection.cpp
Normal file
68
src/framework/shortmessageconnection.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "task.h"
|
||||
#include "shortmessageconnection.h"
|
||||
|
||||
ShortMessageConnection::
|
||||
ShortMessageConnection(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port) :
|
||||
SocketConnection(label, owner, hostname, port) {
|
||||
}
|
||||
|
||||
|
||||
ShortMessageConnection::
|
||||
ShortMessageConnection(const std::string &label, Task *owner, int fd,
|
||||
const char *ip, uint16_t port) :
|
||||
SocketConnection(label, owner, fd, ip, port) {
|
||||
}
|
||||
|
||||
PollState ShortMessageConnection::connected() {
|
||||
return owner()->connectionReady(this);
|
||||
}
|
||||
|
||||
PollState ShortMessageConnection::readData(char *buf, size_t len) {
|
||||
|
||||
if (reading_header) {
|
||||
while (len && *buf >= '0' && *buf <= '9') {
|
||||
(bytes_left *= 10) += (*buf++ - '0');
|
||||
--len;
|
||||
}
|
||||
if (!len)
|
||||
return PollState::READ;
|
||||
if (!bytes_left || *buf != '\n') {
|
||||
err_log() << "Got unexpected data";
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
reading_header = false;
|
||||
++buf;
|
||||
--len;
|
||||
}
|
||||
|
||||
if (len < bytes_left) {
|
||||
msg.append(std::string(buf, len));
|
||||
bytes_left -= len;
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
msg.append(std::string(buf, bytes_left));
|
||||
len -= bytes_left;
|
||||
buf += bytes_left;
|
||||
bytes_left = 0;
|
||||
|
||||
if (tellOwner(msg) == PollState::CLOSE)
|
||||
return PollState::CLOSE;
|
||||
|
||||
msg.clear();
|
||||
reading_header = true;
|
||||
|
||||
if (len)
|
||||
return readData(buf, len);
|
||||
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
void ShortMessageConnection::sendMessage(const std::string &msg) {
|
||||
std::string s = std::to_string(msg.size()) + '\n';
|
||||
asyncSendData(s + msg);
|
||||
}
|
||||
47
src/framework/shortmessageconnection.h
Normal file
47
src/framework/shortmessageconnection.h
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "socketconnection.h"
|
||||
|
||||
/// \brief
|
||||
/// Simple protocol for exchanging messages.
|
||||
///
|
||||
/// Use sendMessage to send a message to peer.
|
||||
///
|
||||
/// When a message has been received from peer, it will be passed to
|
||||
/// the owner task's msgFromConnection method.
|
||||
/// When the connection is ready (connected), the owner task's connectionReady
|
||||
/// method will be called.
|
||||
/// The connectionReady and msgFromConnection methods must return
|
||||
/// PollState::READ to keep the connection, or PollState::CLOSE to close it.
|
||||
class ShortMessageConnection : public SocketConnection {
|
||||
public:
|
||||
/// For client sockets, connecting to a server.
|
||||
ShortMessageConnection(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port);
|
||||
|
||||
/// For already connected sockets, i.e. in a server.
|
||||
ShortMessageConnection(const std::string &label, Task *owner, int fd,
|
||||
const char *ip = "unknown", uint16_t port = 0);
|
||||
|
||||
/// Will notify owner task that a new connection is available.
|
||||
PollState connected() override;
|
||||
|
||||
/// Will notify owner when a complete message has been retrieved.
|
||||
PollState readData(char *buf, size_t len) override;
|
||||
|
||||
/// Send message to peer.
|
||||
void sendMessage(const std::string &msg);
|
||||
|
||||
private:
|
||||
// Message we're currently receiving, or empty.
|
||||
std::string msg;
|
||||
|
||||
// Bytes left for the above message to be complete,
|
||||
// or 0 if we're not currently receiving a message
|
||||
size_t bytes_left = 0;
|
||||
|
||||
bool reading_header = true;
|
||||
};
|
||||
370
src/framework/socket.cpp
Normal file
370
src/framework/socket.cpp
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <ws2tcpip.h>
|
||||
#include <time.h>
|
||||
#define NOMINMAX
|
||||
#include <windows.h>
|
||||
#include <winsock2.h>
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/ip.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <netdb.h>
|
||||
#include <net/if.h>
|
||||
#endif
|
||||
#ifdef __linux
|
||||
#include <netinet/tcp.h>
|
||||
#endif
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <map>
|
||||
|
||||
#include "socket.h"
|
||||
#include "task.h"
|
||||
|
||||
Socket::Socket(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port) :
|
||||
Logger(label),
|
||||
_owner(owner),
|
||||
_hostname(hostname),
|
||||
_port(port),
|
||||
_state(PollState::NONE)
|
||||
{
|
||||
#ifndef _WIN32
|
||||
if (!port && hostname == "UnixDomain") {
|
||||
int pair_sd[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, pair_sd) < 0) {
|
||||
errno_log() << "cannot create socket pair";
|
||||
_socket = 0;
|
||||
} else {
|
||||
_socket = pair_sd[0];
|
||||
unix_domain_peer = pair_sd[1];
|
||||
fcntl(pair_sd[0], F_SETFL, O_NONBLOCK|O_CLOEXEC);
|
||||
fcntl(pair_sd[1], F_SETFL, O_NONBLOCK);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
_socket = -1;
|
||||
_peer_label = _hostname + ":" + std::to_string(_port);
|
||||
}
|
||||
|
||||
// TODO: take initial state as a parameter, default PollState::READ.
|
||||
Socket::Socket(const std::string &label, Task *owner, int fd) :
|
||||
Logger(label),
|
||||
_owner(owner),
|
||||
_socket(fd),
|
||||
_hostname(""),
|
||||
_port(0),
|
||||
_state(PollState::READ)
|
||||
{
|
||||
}
|
||||
|
||||
Socket::~Socket() {
|
||||
if (_socket >= 0) {
|
||||
closeSocket(_socket);
|
||||
log() << "closed socket " << _socket;
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
std::map<std::string, struct addrinfo *> dns_cache;
|
||||
}
|
||||
|
||||
void Socket::clearCache() {
|
||||
for (auto p : dns_cache)
|
||||
freeaddrinfo(p.second);
|
||||
dns_cache.clear();
|
||||
}
|
||||
|
||||
struct addrinfo *Socket::getAddressInfo(uint16_t iptype) {
|
||||
auto it = dns_cache.find(_peer_label);
|
||||
if (it == dns_cache.end()) {
|
||||
struct addrinfo hints, *addressInfo;
|
||||
memset(&hints, 0, sizeof hints);
|
||||
hints.ai_family = AF_UNSPEC;
|
||||
hints.ai_socktype = SOCK_STREAM;
|
||||
hints.ai_flags = AI_ADDRCONFIG;
|
||||
|
||||
const char *hostaddr;
|
||||
if (_hostname.empty()) {
|
||||
hints.ai_family = AF_INET6;
|
||||
hints.ai_flags |= AI_PASSIVE;
|
||||
log() << "wildcard address *:" << _port;
|
||||
hostaddr = nullptr;
|
||||
} else if (_hostname.find_first_not_of("1234567890.:") ==
|
||||
std::string::npos) {
|
||||
hints.ai_flags |= AI_NUMERICHOST;
|
||||
log() << "numeric address " << _hostname;
|
||||
hostaddr = _hostname.c_str();
|
||||
} else {
|
||||
log() << "dns lookup " << _hostname;
|
||||
hostaddr = _hostname.c_str();
|
||||
}
|
||||
|
||||
int res = getaddrinfo(hostaddr, std::to_string(_port).c_str(),
|
||||
&hints, &addressInfo);
|
||||
if (res != 0) {
|
||||
err_log() << "lookup failed: " << gai_strerror(res);
|
||||
return nullptr;
|
||||
} else if (!addressInfo) {
|
||||
err_log() << "no valid address found";
|
||||
return nullptr;
|
||||
}
|
||||
if (!(hints.ai_flags & AI_NUMERICHOST)) {
|
||||
char ip[INET6_ADDRSTRLEN];
|
||||
struct sockaddr *addr = addressInfo->ai_addr;
|
||||
if (addressInfo->ai_family == AF_INET) {
|
||||
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(addr);
|
||||
inet_ntop(AF_INET, &s->sin_addr, ip, sizeof ip);
|
||||
} else {
|
||||
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(addr);
|
||||
inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof ip);
|
||||
}
|
||||
log() << "lookup done: " << ip;
|
||||
}
|
||||
|
||||
auto p2 = dns_cache.insert(std::make_pair(_peer_label, addressInfo));
|
||||
it = p2.first;
|
||||
}
|
||||
if (iptype) {
|
||||
int fam = (iptype == 6) ? AF_INET6 : AF_INET;
|
||||
struct addrinfo *ai = it->second;
|
||||
while (ai) {
|
||||
if (ai->ai_family == fam)
|
||||
return ai;
|
||||
ai = ai->ai_next;
|
||||
}
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void Socket::createNonBlockingSocket(struct addrinfo *addressEntry,
|
||||
struct addrinfo *localAddr) {
|
||||
if (_socket >= 0) {
|
||||
err_log() << "socket already exists";
|
||||
return;
|
||||
}
|
||||
int fd = ::socket(addressEntry->ai_family, addressEntry->ai_socktype,
|
||||
addressEntry->ai_protocol);
|
||||
if (fd == -1) {
|
||||
errno_log() << "cannot create socket";
|
||||
return;
|
||||
}
|
||||
if (!setNonBlocking(fd)) {
|
||||
closeSocket(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
if (localAddr && bind(fd, localAddr->ai_addr, localAddr->ai_addrlen) != 0) {
|
||||
errno_log() << "cannot bind to local address";
|
||||
return;
|
||||
}
|
||||
|
||||
int res = connect(fd, addressEntry->ai_addr, addressEntry->ai_addrlen);
|
||||
if (res == -1 && !isTempError()) {
|
||||
errno_log() << "connect error";
|
||||
closeSocket(fd);
|
||||
return;
|
||||
}
|
||||
|
||||
// All good, let's keep the socket:
|
||||
_socket = fd;
|
||||
_state = PollState::CONNECTING;
|
||||
}
|
||||
|
||||
int Socket::closeSocket(int fd) {
|
||||
#ifdef _WIN32
|
||||
return closesocket(fd);
|
||||
#else
|
||||
return close(fd);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Socket::socketInError(int fd) {
|
||||
int res;
|
||||
socklen_t res_len = sizeof(res);
|
||||
#ifdef _WIN32
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *)&res, &res_len) < 0)
|
||||
#else
|
||||
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &res, &res_len) < 0)
|
||||
#endif
|
||||
return true;
|
||||
if (!res)
|
||||
return false;
|
||||
errno = res;
|
||||
return !isTempError();
|
||||
}
|
||||
|
||||
bool Socket::setNonBlocking(int fd) {
|
||||
#ifdef __APPLE__
|
||||
// SO_NOSIGPIPE only for OS X
|
||||
int value = 1;
|
||||
int status = setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE,
|
||||
&value, sizeof(value));
|
||||
if (status != 0) {
|
||||
errno_log() << "cannot set SO_NOSIGPIPE";
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
u_long enabledParameter = 1;
|
||||
int nonBlockingResult = ioctlsocket(fd, FIONBIO, &enabledParameter);
|
||||
|
||||
if (nonBlockingResult == -1) {
|
||||
errno_log() << "cannot set socket non-blocking";
|
||||
closesocket(fd);
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
int nonBlockingResult = fcntl(fd, F_SETFL, O_NONBLOCK);
|
||||
if (nonBlockingResult == -1) {
|
||||
errno_log() << "cannot set socket non-blocking";
|
||||
close(fd);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#ifdef __linux
|
||||
int flag = 1;
|
||||
int result = setsockopt(fd, IPPROTO_TCP, TCP_NODELAY,
|
||||
reinterpret_cast<char *>(&flag), sizeof(int));
|
||||
if (result < 0)
|
||||
errno_log() << "cannot set TCP_NODELAY";
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
const char *Socket::getIp(struct sockaddr *address, uint16_t *port) {
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static char client_ip[INET6_ADDRSTRLEN];
|
||||
if (address->sa_family == AF_INET) {
|
||||
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(address);
|
||||
inet_ntop(AF_INET, &s->sin_addr, client_ip, INET6_ADDRSTRLEN);
|
||||
if (port)
|
||||
*port = ntohs(s->sin_port);
|
||||
} else {
|
||||
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(address);
|
||||
inet_ntop(AF_INET6, &s->sin6_addr, client_ip, INET6_ADDRSTRLEN);
|
||||
if (port)
|
||||
*port = ntohs(s->sin6_port);
|
||||
}
|
||||
if (strncmp(client_ip, "::ffff:", 7) == 0)
|
||||
return client_ip+7;
|
||||
else
|
||||
return client_ip;
|
||||
}
|
||||
|
||||
const char *Socket::getIp(struct addrinfo *address, uint16_t *port) {
|
||||
return getIp(address->ai_addr, port);
|
||||
}
|
||||
|
||||
const char *Socket::getIp(int fd, uint16_t *port, bool peer) {
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static char client_ip[INET6_ADDRSTRLEN];
|
||||
static const char *no_ip = "unknown IP";
|
||||
|
||||
struct sockaddr_storage address;
|
||||
memset(&address, 0, sizeof address);
|
||||
socklen_t addrlen = sizeof(address);
|
||||
|
||||
int ret = peer ?
|
||||
getpeername(fd, reinterpret_cast<sockaddr *>(&address), &addrlen) :
|
||||
getsockname(fd, reinterpret_cast<sockaddr *>(&address), &addrlen);
|
||||
|
||||
if (ret < 0) {
|
||||
return no_ip;
|
||||
} else {
|
||||
if (address.ss_family == AF_INET) {
|
||||
struct sockaddr_in *s = reinterpret_cast<sockaddr_in *>(&address);
|
||||
inet_ntop(AF_INET, &s->sin_addr, client_ip, INET6_ADDRSTRLEN);
|
||||
if (port)
|
||||
*port = ntohs(s->sin_port);
|
||||
} else {
|
||||
struct sockaddr_in6 *s = reinterpret_cast<sockaddr_in6 *>(&address);
|
||||
inet_ntop(AF_INET6, &s->sin6_addr, client_ip, INET6_ADDRSTRLEN);
|
||||
if (port)
|
||||
*port = ntohs(s->sin6_port);
|
||||
}
|
||||
if (strncmp(client_ip, "::ffff:", 7) == 0)
|
||||
return client_ip+7;
|
||||
else
|
||||
return client_ip;
|
||||
}
|
||||
}
|
||||
|
||||
bool Socket::createServerSocket() {
|
||||
std::string ip = _hostname;
|
||||
|
||||
if (_socket >= 0)
|
||||
return false; // Already in use!!
|
||||
|
||||
log() << "Listen on " << port() << " ip " << ip;
|
||||
|
||||
struct addrinfo *addr = getAddressInfo();
|
||||
if (!addr)
|
||||
return false;
|
||||
|
||||
int fd = ::socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol);
|
||||
if (fd < 0) {
|
||||
errno_log() << "cannot create listen socket";
|
||||
return false;
|
||||
}
|
||||
#ifndef _WIN32
|
||||
int reuse = 1;
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse,
|
||||
sizeof(reuse)) < 0) {
|
||||
errno_log() << "cannot reuse listen socket";
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (bind(fd, addr->ai_addr, addr->ai_addrlen) != 0) {
|
||||
errno_log() << "cannot bind listen socket";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(fd, 20) != 0) {
|
||||
errno_log() << "cannot listen";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Socket will be -1, and state will be UNDEFINED, unless we get here:
|
||||
_socket = fd;
|
||||
_state = PollState::READ;
|
||||
|
||||
// Check port number
|
||||
struct sockaddr_in6 address;
|
||||
socklen_t len = sizeof(address);
|
||||
if (getsockname(fd, reinterpret_cast<sockaddr *>(&address), &len) == -1)
|
||||
errno_log() << "getsockname failed";
|
||||
else {
|
||||
_port = ntohs(address.sin6_port);
|
||||
log() << "server socket " << fd << " listening on port " << _port;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
237
src/framework/socket.h
Normal file
237
src/framework/socket.h
Normal file
|
|
@ -0,0 +1,237 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#endif
|
||||
|
||||
#include "pollstate.h"
|
||||
#include "logger.h"
|
||||
|
||||
class Task;
|
||||
|
||||
/// \brief
|
||||
/// This is a slave to the Engine class. You can't use it directly,
|
||||
/// only through its subclasses, SocketConnection or ServerSocket.
|
||||
class Socket : public Logger {
|
||||
friend class Engine;
|
||||
public:
|
||||
Socket(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port);
|
||||
|
||||
Socket(const std::string &label, Task *owner, int fd);
|
||||
|
||||
/// Return task owning the socket.
|
||||
Task *owner() const {
|
||||
return _owner;
|
||||
}
|
||||
|
||||
/// Return name of the host to which the socket is supposed to connect.
|
||||
std::string hostname() const {
|
||||
return _hostname;
|
||||
}
|
||||
|
||||
/// Return port number to which the socket is supposed to connect.
|
||||
uint16_t port() const {
|
||||
return _port;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return current socket state.
|
||||
PollState state() const {
|
||||
return _state;
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
/// \brief
|
||||
/// Return the peer socket descriptor.
|
||||
///
|
||||
/// If this is a Unix Domain socket, return the peer socket descriptor.
|
||||
/// If not, return 0.
|
||||
///
|
||||
/// May be used in another thread or process.
|
||||
int getUnixDomainPeer() const {
|
||||
return unix_domain_peer;
|
||||
}
|
||||
#endif
|
||||
|
||||
virtual ~Socket();
|
||||
|
||||
/// \brief
|
||||
/// Return the socket's cache group, or an empty string.
|
||||
///
|
||||
/// By default, if we have a keepalive (cached) connection to the same
|
||||
/// host and port, it will be used instead of creating a
|
||||
/// new connection. Override this method to disable keepalive
|
||||
/// (returning empty string) or to use only a special type of
|
||||
/// cached connection (returning a label for that special type).
|
||||
virtual std::string cacheLabel() {
|
||||
return _hostname + std::to_string(_port);
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return unique connection ID if connected.
|
||||
///
|
||||
/// Return a positive number that is unique to this connection if it is
|
||||
/// active, otherwise -1.
|
||||
int id() const {
|
||||
return _socket;
|
||||
}
|
||||
|
||||
/// Set the given task as owner of the socket.
|
||||
virtual void setOwner(Task *t) {
|
||||
_owner = t;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Set a time to live for the socket.
|
||||
///
|
||||
/// Call this to have the socket removed automatically before a given number
|
||||
/// of seconds. Note: the network engine might remove the socket 1-2 seconds
|
||||
/// before the timeout, so adjust the timeout value accordingly!
|
||||
void setExpiry(double s) {
|
||||
expiry = timeAfter(s);
|
||||
}
|
||||
|
||||
/// Return true if the given TimePoint is after the socket's expiry.
|
||||
bool hasExpired(const TimePoint &when) const {
|
||||
return (expiry < when);
|
||||
}
|
||||
|
||||
/// Return local IP address in static buffer.
|
||||
const char *localIp() const {
|
||||
const char *ip = getIp(socket(), nullptr, false);
|
||||
return ip;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return IP address of connected socket in static buffer.
|
||||
///
|
||||
/// Return the local IP address if peer==false, otherwise the peer IP.
|
||||
static const char *getIp(int fd, uint16_t *port = nullptr,
|
||||
bool peer = true);
|
||||
|
||||
/// Return IP address in static buffer.
|
||||
static const char *getIp(struct sockaddr *address, uint16_t *port=nullptr);
|
||||
|
||||
/// Return IP address in static buffer.
|
||||
static const char *getIp(struct addrinfo *address, uint16_t *port=nullptr);
|
||||
|
||||
/// Perform DNS lookup of remote host.
|
||||
struct addrinfo *getAddressInfo(uint16_t iptype = 0);
|
||||
protected:
|
||||
/// Return true unless last syscall encountered a fatal error.
|
||||
static bool isTempError() {
|
||||
#ifdef _WIN32
|
||||
if (WSAGetLastError() == WSAEWOULDBLOCK ||
|
||||
WSAGetLastError() == WSAEINPROGRESS ||
|
||||
WSAGetLastError() == WSAENOTCONN ||
|
||||
!WSAGetLastError())
|
||||
#else
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINPROGRESS
|
||||
|| errno == EINTR || !errno)
|
||||
#endif
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Return true if socket is watched for writeability.
|
||||
virtual bool wantToSend() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// This will be called regularly on READ_BLOCKED sockets to check if the
|
||||
/// block can be lifted.
|
||||
///
|
||||
/// If your subclass ever returns READ_BLOCKED,
|
||||
/// it should override this method to return the new state when the block
|
||||
/// should be removed.
|
||||
virtual PollState checkReadBlock() {
|
||||
return PollState::READ_BLOCKED;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Notify intention of sending large amounts of data.
|
||||
///
|
||||
/// Normally, this is done simply by returning PollState::READ_WRITE
|
||||
/// from a scoket callback.
|
||||
/// This method is useful if you're not inside such a callback when you
|
||||
/// find out you need to send (large amounts of) data.
|
||||
void setWantToSend() {
|
||||
if (_state != PollState::CLOSE)
|
||||
_state = PollState::READ_WRITE;
|
||||
}
|
||||
|
||||
/// Return file descriptor.
|
||||
int socket() const {
|
||||
return _socket;
|
||||
}
|
||||
|
||||
/// Close a file descriptor.
|
||||
static int closeSocket(int fd);
|
||||
|
||||
/// Tell the network engine that the connection should be closed.
|
||||
void closeMe() {
|
||||
_state = PollState::CLOSE;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Create socket and initiate the connection.
|
||||
///
|
||||
/// Will do no nothing if socket has already been created.
|
||||
/// On error, socket() will return -1.
|
||||
void createNonBlockingSocket(struct addrinfo *addressEntry,
|
||||
struct addrinfo *localAddr=nullptr);
|
||||
|
||||
/// Set socket as non-blocking.
|
||||
bool setNonBlocking(int fd);
|
||||
|
||||
/// Return true if the file descriptor has encountered a fatal error.
|
||||
static bool socketInError(int fd);
|
||||
|
||||
/// Return true if the socket has encountered a fatal error.
|
||||
bool inError() const {
|
||||
if (!socketInError(_socket))
|
||||
return false;
|
||||
errno_log() << "failed socket " << _socket;
|
||||
return true;
|
||||
}
|
||||
private:
|
||||
void setState(PollState state) {
|
||||
if (_state != PollState::CLOSE)
|
||||
_state = state;
|
||||
}
|
||||
|
||||
// Return false on failure:
|
||||
bool createServerSocket();
|
||||
|
||||
static void clearCache();
|
||||
|
||||
void killMe() {
|
||||
_state = PollState::KILL;
|
||||
}
|
||||
|
||||
// Internal identifier used as key in dns_cache:
|
||||
std::string _peer_label;
|
||||
|
||||
void setSocket(int fd) {
|
||||
_socket = fd;
|
||||
}
|
||||
|
||||
Socket(const Socket &);
|
||||
Task *_owner;
|
||||
int _socket;
|
||||
#ifndef _WIN32
|
||||
// If this is a Unix Domain socket, the peer socket descriptor will be
|
||||
// stored here:
|
||||
int unix_domain_peer = 0;
|
||||
#endif
|
||||
std::string _hostname;
|
||||
uint16_t _port;
|
||||
PollState _state;
|
||||
TimePoint expiry = timeMax();
|
||||
};
|
||||
327
src/framework/socketconnection.cpp
Normal file
327
src/framework/socketconnection.cpp
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#include <WS2tcpip.h>
|
||||
#include <Windows.h>
|
||||
#include <winsock2.h>
|
||||
#pragma comment(lib, "ws2_32.lib")
|
||||
typedef long ssize_t;
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
#endif
|
||||
|
||||
#include "socketconnection.h"
|
||||
#include "task.h"
|
||||
#include <sys/types.h>
|
||||
#ifdef USE_GNUTLS
|
||||
#include <gnutls/x509.h>
|
||||
#endif
|
||||
|
||||
SocketConnection::
|
||||
SocketConnection(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port,
|
||||
uint16_t iptype, struct addrinfo *local_addr) :
|
||||
Socket(label, owner, hostname, port) {
|
||||
peer_ip = hostname;
|
||||
peer_port = port;
|
||||
local_ip = local_addr;
|
||||
if (local_ip)
|
||||
prefer_ip_type = (local_ip->ai_family == AF_INET6 ? 6 : 4);
|
||||
else
|
||||
prefer_ip_type = iptype;
|
||||
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
uint64_t SocketConnection::tot_bytes_sent = 0;
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
uint64_t SocketConnection::tot_bytes_received = 0;
|
||||
|
||||
SocketConnection::SocketConnection(const std::string &label, Task *owner,
|
||||
int fd, const char *ip, uint16_t port) :
|
||||
Socket(label, owner, fd) {
|
||||
peer_ip = ip;
|
||||
peer_port = port;
|
||||
}
|
||||
|
||||
void SocketConnection::closedByPeer() {
|
||||
log() << "connection closed by peer";
|
||||
}
|
||||
|
||||
PollState SocketConnection::doRead(int fd) {
|
||||
ssize_t n;
|
||||
#ifdef USE_GNUTLS
|
||||
if (is_tls()) {
|
||||
n = gnutls_record_recv(session, socket_buffer, sizeof socket_buffer);
|
||||
if (n < 0) {
|
||||
if (gnutls_error_is_fatal(static_cast<int>(n))) {
|
||||
// Probably client terminated the connection abruptly
|
||||
dbg_log() << "TLS error on socket " << fd;
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
//warn_log() << "TLS recv interrupt on socket " << fd;
|
||||
return state();
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
n = recv(fd, socket_buffer, sizeof socket_buffer, 0);
|
||||
if (debugging)
|
||||
log() << "socket " << socket() << " doRead " << n;
|
||||
if (n == 0) {
|
||||
log() << "socket closed by peer " << fd;
|
||||
return PollState::CLOSE;
|
||||
} else if (n < 0) {
|
||||
if (isTempError()) {
|
||||
warn_log() << "recv interrupt on socket " << fd;
|
||||
return state();
|
||||
} else {
|
||||
errno_log() << "recv error on socket " << fd;
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
}
|
||||
if (debugging && n < 1000)
|
||||
log() << "socket " << socket() << "doRead <"
|
||||
<< std::string(socket_buffer, static_cast<size_t>(n));
|
||||
#ifdef USE_GNUTLS
|
||||
// For SSL sockets, it's the tls_pull method that counts the actual number
|
||||
// of bytes fetched from the network
|
||||
if (!is_tls())
|
||||
#endif
|
||||
{
|
||||
tot_bytes_received += static_cast<uint64_t>(n);
|
||||
owner()->notifyBytesReceived(static_cast<uint64_t>(n));
|
||||
}
|
||||
|
||||
// Now let the subclass take care of what we read:
|
||||
if (state() == PollState::READ || state() == PollState::READ_WRITE) {
|
||||
PollState ret = readData(socket_buffer, static_cast<size_t>(n));
|
||||
// If there's more to read, do it immediately instead of after
|
||||
// next select call:
|
||||
if (ret == PollState::READ &&
|
||||
static_cast<size_t>(n) == sizeof socket_buffer)
|
||||
return doRead(fd);
|
||||
return ret;
|
||||
}
|
||||
return unexpectedData(socket_buffer, static_cast<size_t>(n));
|
||||
}
|
||||
|
||||
// Must not be called more than once on the same object!
|
||||
bool SocketConnection::asyncConnect() {
|
||||
if (socket() >= 0) {
|
||||
log() << "internal error, connect called twice on socket" << socket();
|
||||
return false;
|
||||
}
|
||||
|
||||
struct addrinfo *addr = getAddressInfo(prefer_ip_type);
|
||||
if (!addr)
|
||||
return false;
|
||||
|
||||
createNonBlockingSocket(addr, local_ip);
|
||||
if (socket() < 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PollState SocketConnection::unexpectedData(char *buf, size_t len) {
|
||||
err_log() << "unexpected data arrived; will close connection: "
|
||||
<< std::string(buf, std::min(len, static_cast<size_t>(30)));
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
size_t SocketConnection::sendData(const char *buf, size_t len) {
|
||||
ssize_t n;
|
||||
#ifdef USE_GNUTLS
|
||||
if (is_tls()) {
|
||||
n = gnutls_record_send(session, buf, len);
|
||||
if (n < 0) {
|
||||
if (gnutls_error_is_fatal(static_cast<int>(n))) {
|
||||
errno_log() << "TLS write error";
|
||||
closeMe();
|
||||
} else {
|
||||
if (!tls_send_pending) {
|
||||
warn_log() << "Socket " << socket()
|
||||
<< " TLS write failure: "
|
||||
<< gnutls_strerror(static_cast<int>(n));
|
||||
tls_send_pending = true;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// To reenable the GNUTLS_E_AGAIN warning, do this:
|
||||
// tls_send_pending = false;
|
||||
if (debugging && n < 1000)
|
||||
log() << "socket " << socket() << "sendData <"
|
||||
<< std::string(buf, static_cast<size_t>(n)) << ">";
|
||||
// For SSL sockets, it's the tls_push method that counts the actual
|
||||
// number of bytes sent over the network
|
||||
return static_cast<size_t>(n);
|
||||
}
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
n = send(socket(), buf, len, 0);
|
||||
#elif defined(_WIN32)
|
||||
n = send(socket(), buf, len, 0);
|
||||
#else
|
||||
n = send(socket(), buf, len, MSG_NOSIGNAL);
|
||||
#endif
|
||||
if (debugging)
|
||||
log() << "socket " << socket() << "sendData " << n << " of " << len;
|
||||
if (n < 0) {
|
||||
if (!isTempError()) {
|
||||
errno_log() << "cannot write";
|
||||
closeMe();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (debugging && n < 1000)
|
||||
log() << "socket " << socket() << "sendData <"
|
||||
<< std::string(buf, static_cast<size_t>(n)) << ">";
|
||||
// Global count
|
||||
tot_bytes_sent += static_cast<uint64_t>(n);
|
||||
// Per task count
|
||||
owner()->notifyBytesSent(static_cast<uint64_t>(n));
|
||||
return static_cast<size_t>(n);
|
||||
}
|
||||
|
||||
#ifdef USE_WEBROOT
|
||||
size_t SocketConnection::sendFileData(int fd, size_t len) {
|
||||
static char buf[50000];
|
||||
if (len > sizeof buf)
|
||||
len = sizeof buf;
|
||||
ssize_t n = read(fd, buf, len);
|
||||
log() << "Read " << n << " bytes from file";
|
||||
if (n <= 0) {
|
||||
errno_log() << "cannot read from file";
|
||||
closeMe();
|
||||
return 0;
|
||||
}
|
||||
return sendData(buf, static_cast<size_t>(n));
|
||||
}
|
||||
#endif
|
||||
|
||||
void SocketConnection::asyncSendData(const char *buf, size_t len) {
|
||||
if (to_send.empty()) {
|
||||
size_t sent = sendData(buf, len);
|
||||
if (sent == len)
|
||||
return;
|
||||
to_send = std::string(buf+sent, len-sent);
|
||||
} else {
|
||||
to_send.append(buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
PollState SocketConnection::tellOwner(const std::string &msg) {
|
||||
return owner()->msgFromConnection(this, msg);
|
||||
}
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
|
||||
bool SocketConnection::
|
||||
init_tls_client(gnutls_certificate_credentials_t &x509_cred, bool verify_cert) {
|
||||
dbg_log() << "Enable TLS on socket " << socket();
|
||||
|
||||
if (gnutls_init(&session, GNUTLS_CLIENT | GNUTLS_NONBLOCK) < 0)
|
||||
return false;
|
||||
setSessionInitialized();
|
||||
if (gnutls_set_default_priority(session) < 0)
|
||||
return false;
|
||||
if (gnutls_server_name_set(session, GNUTLS_NAME_DNS, peer_ip.c_str(),
|
||||
peer_ip.size()) < 0)
|
||||
return false;
|
||||
|
||||
if (gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
|
||||
x509_cred) < 0)
|
||||
return false;
|
||||
|
||||
if (verify_cert) {
|
||||
log() << "verify cert";
|
||||
gnutls_session_set_verify_cert(session, peer_ip.c_str(), 0);
|
||||
}
|
||||
|
||||
gnutls_certificate_set_verify_flags (x509_cred,
|
||||
GNUTLS_VERIFY_ALLOW_BROKEN);
|
||||
|
||||
gnutls_handshake_set_timeout(session,
|
||||
GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
|
||||
|
||||
// gnutls_transport_set_int(session, socket());
|
||||
// We use custom send/recv functions in order to be able to count the
|
||||
// number of bytes sent and received:
|
||||
{
|
||||
gnutls_transport_set_ptr(session,
|
||||
static_cast<gnutls_transport_ptr_t>(this));
|
||||
gnutls_transport_set_push_function(session, tls_push_static);
|
||||
gnutls_transport_set_pull_function(session, tls_pull_static);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
ssize_t SocketConnection::tls_pull(void *buf, size_t len) {
|
||||
ssize_t n = recv(socket(), buf, len, 0);
|
||||
//dbg_log() << "tls_pull " << n << " bytes of " << len;
|
||||
if (n > 0) {
|
||||
owner()->notifyBytesReceived(static_cast<uint64_t>(n));
|
||||
tot_bytes_received += static_cast<uint64_t>(n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
ssize_t SocketConnection::tls_push(const void *buf, size_t len) {
|
||||
#ifdef __APPLE__
|
||||
ssize_t n = send(socket(), buf, len, 0);
|
||||
#elif defined(_WIN32)
|
||||
ssize_t n = send(socket(), buf, len, 0);
|
||||
#else
|
||||
ssize_t n = send(socket(), buf, len, MSG_NOSIGNAL);
|
||||
#endif
|
||||
//dbg_log() << "tls_push " << n << " bytes of " << len;
|
||||
if (n > 0) {
|
||||
owner()->notifyBytesSent(static_cast<uint64_t>(n));
|
||||
tot_bytes_sent += static_cast<uint64_t>(n);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool SocketConnection::
|
||||
init_tls_server(gnutls_certificate_credentials_t &x509_cred,
|
||||
gnutls_priority_t &priority_cache) {
|
||||
dbg_log() << "Enable TLS on incoming socket " << socket();
|
||||
|
||||
if (gnutls_init(&session, GNUTLS_SERVER | GNUTLS_NONBLOCK) < 0) {
|
||||
err_log() << "Cannot initialise TLS session";
|
||||
return false;
|
||||
}
|
||||
setSessionInitialized();
|
||||
|
||||
if (gnutls_priority_set(session, priority_cache) < 0 ||
|
||||
gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE,
|
||||
x509_cred) < 0)
|
||||
return false;
|
||||
|
||||
gnutls_certificate_server_set_request(session,
|
||||
GNUTLS_CERT_IGNORE);
|
||||
gnutls_handshake_set_timeout(session,
|
||||
GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);
|
||||
// gnutls_transport_set_int(session, socket());
|
||||
// We use custom send/recv functions in order to be able to count the
|
||||
// number of bytes sent and received:
|
||||
{
|
||||
gnutls_transport_set_ptr(session,
|
||||
static_cast<gnutls_transport_ptr_t>(this));
|
||||
gnutls_transport_set_push_function(session, tls_push_static);
|
||||
gnutls_transport_set_pull_function(session, tls_pull_static);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
327
src/framework/socketconnection.h
Normal file
327
src/framework/socketconnection.h
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include "socket.h"
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
#include <gnutls/gnutls.h>
|
||||
#endif
|
||||
|
||||
/// \brief
|
||||
/// This class implements low-level socket connection operations.
|
||||
/// Inherit from it to implement protocols like HTTP.
|
||||
///
|
||||
/// A SocketConnection object represents a socket connection. Inherit from this
|
||||
/// class to implement a "protocol" for the connection, i.e. when/what to
|
||||
/// read/write through the connection.
|
||||
///
|
||||
/// Each SocketConnection is owned by a Task object. SocketConnection objects
|
||||
/// will be added to the network Engine (a part of the EventLoop) through the
|
||||
/// task's addConnection method.
|
||||
///
|
||||
/// The subclass will be notified through callback functions when
|
||||
/// anything happens on the socket, i.e. if the socket has been closed, if data
|
||||
/// has arrived, or if the socket is writable.
|
||||
/// You define the callback functions by overloading the below virtual functions.
|
||||
/// All operations will be performed asynchronously (non-blocking) except for
|
||||
/// dns lookups, which are performed synchronously (blocking).
|
||||
///
|
||||
/// Note! You _must_ create the SocketConnection objects with new. The ownership
|
||||
/// will then be passed to the network engine.
|
||||
/// You are not allowed to delete a SocketConnection object.
|
||||
/// We will delete it when the connection has been closed, which will be
|
||||
///
|
||||
/// 1) If you order us to close it by returning CLOSE, KEEPALIVE or KILL from
|
||||
/// a callback, or
|
||||
///
|
||||
/// 2) After the closedByPeer callback, if the connection has been
|
||||
/// closed by peer.
|
||||
///
|
||||
/// 3) After the connectionFailed callback, if the connection couldn't
|
||||
/// be established in the first place.
|
||||
///
|
||||
/// The owner task will be notified before the object is deleted.
|
||||
class SocketConnection : public Socket {
|
||||
friend class Engine;
|
||||
public:
|
||||
/// Create a SocketConnection owned by the given Task. The network Engine
|
||||
/// will connect to the given hostname/port, and notify through the below
|
||||
/// method when the connection is ready.
|
||||
/// If iptype is 4, prefer ipv4. If iptype is 6, prefer ipv6.
|
||||
SocketConnection(const std::string &label, Task *owner,
|
||||
const std::string &hostname, uint16_t port,
|
||||
uint16_t iptype = 0, struct addrinfo *local_addr=nullptr);
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
virtual ~SocketConnection() override {
|
||||
if (tlsInitialized()) {
|
||||
gnutls_deinit(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify that the connection will be encrypted (SSL).
|
||||
void enableTLS() {
|
||||
use_tls = true;
|
||||
}
|
||||
|
||||
/// Return true if the connection will be encrypted (SSL).
|
||||
bool is_tls() const {
|
||||
return use_tls;
|
||||
}
|
||||
|
||||
/// Store SSL session in cache.
|
||||
gnutls_session_t cache_session() {
|
||||
session_initialized = false;
|
||||
return session;
|
||||
}
|
||||
|
||||
/// Reuse cached SSL session.
|
||||
void insert_cached_session(gnutls_session_t &old_session) {
|
||||
session_initialized = true;
|
||||
use_tls = true;
|
||||
session = old_session;
|
||||
gnutls_transport_set_ptr(session,
|
||||
static_cast<gnutls_transport_ptr_t>(this));
|
||||
}
|
||||
#endif
|
||||
|
||||
/// \brief
|
||||
/// Will be called when the connection is established.
|
||||
///
|
||||
/// Override it to start sending data when the connection is ready.
|
||||
///
|
||||
/// If a connection couldn't be established,
|
||||
/// SocketConnection::connectionFailed will be called instead.
|
||||
/// You must not return PollState::CONNECTING.
|
||||
virtual PollState connected() {
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
/// Will be called if the connection couldn't be established.
|
||||
virtual void connectionFailed(const std::string &err_msg) {
|
||||
log() << "connection failed: " << err_msg;
|
||||
}
|
||||
|
||||
/// Called when the socket has been closed by peer:
|
||||
/// May be called in states READ, WRITE, READ_WRITE
|
||||
virtual void closedByPeer();
|
||||
|
||||
/// \brief
|
||||
/// Callback, called when data has arrived; len > 0.
|
||||
///
|
||||
/// May be called in states READ, READ_WRITE. Override this method to
|
||||
/// handle the data. Return value should be the new state.
|
||||
///
|
||||
/// The buffer is owned by the implementation. However, you are allowed
|
||||
/// to modify the contents of the buffer and you may also use
|
||||
/// it in the asyncSendData call.
|
||||
virtual PollState readData(char *, size_t ) {
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Peer has sent data when it wasn't supposed to.
|
||||
///
|
||||
/// If peer sends data when we're not in state READ/READ_WRITE, this
|
||||
/// function will be called. Default is for the socket to be closed.
|
||||
/// If you return READ, any remaining async_send data will be discarded.
|
||||
virtual PollState unexpectedData(char *buf, size_t len);
|
||||
|
||||
/// \brief
|
||||
/// Callback, called when socket is writable.
|
||||
///
|
||||
/// May be called in states PollState::WRITE and PollState::READ_WRITE.
|
||||
/// Override it to write data.
|
||||
/// Return value should be the new state. Do not return PollState::WRITE or
|
||||
/// PollState::READ_WRITE except after a write operation where
|
||||
/// all data couldn't be sent immediately.
|
||||
virtual PollState writeData() {
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Try to send len bytes from the given buffer. Return the amount sent.
|
||||
///
|
||||
/// More accurately, this method will return the number of bytes that
|
||||
/// could be copied into the socket's send buffer.
|
||||
///
|
||||
/// Please understand that the return value might be < len.
|
||||
/// To safely get all data sent, you should use the
|
||||
/// SocketConnection::asyncSendData
|
||||
/// function instead. However, if you need to send large
|
||||
/// amounts of data ("large" as in "no upper limit") as fast
|
||||
/// as possible, this is the function to use.
|
||||
size_t sendData(const char *buf, size_t len);
|
||||
|
||||
#ifdef USE_WEBROOT
|
||||
size_t sendFileData(int fd, size_t len);
|
||||
#endif
|
||||
|
||||
/// \brief
|
||||
/// Send data to peer as soon as possible.
|
||||
///
|
||||
/// Helper function which you may call only during the
|
||||
/// execution of the callbacks SocketConnection::connected,
|
||||
/// SocketConnection::readData, and SocketConnection::writeData.
|
||||
///
|
||||
/// Send len bytes from the given buffer.
|
||||
/// The callback SocketConnection::closedByPeer
|
||||
/// might be executed before all data was sent.
|
||||
///
|
||||
/// If you need to send "unlimited" amounts of data, you cant use
|
||||
/// this method; instead you must use SocketConnection::sendData.
|
||||
void asyncSendData(const char *buf, size_t len);
|
||||
|
||||
/// \brief
|
||||
/// Send data to peer as soon as possible.
|
||||
///
|
||||
/// Helper function which you may call only during the
|
||||
/// execution of the callbacks SocketConnection::connected,
|
||||
/// SocketConnection::readData, and SocketConnection::writeData.
|
||||
///
|
||||
/// Send len bytes from the given buffer.
|
||||
/// The callback SocketConnection::closedByPeer
|
||||
/// might be executed before all data was sent.
|
||||
///
|
||||
/// If you need to send "unlimited" amounts of data, you cant use
|
||||
/// this method; instead you must use SocketConnection::sendData.
|
||||
void asyncSendData(const std::string data) {
|
||||
asyncSendData(data.c_str(), data.size());
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return number of bytes left to send after calling
|
||||
/// SocketConnection::asyncSendData.
|
||||
size_t asyncBufferSize() const {
|
||||
return to_send.size();
|
||||
}
|
||||
|
||||
/// Number of bytes sent by current thread
|
||||
static uint64_t totBytesSent() {
|
||||
return tot_bytes_sent;
|
||||
}
|
||||
|
||||
/// Number of bytes recieved by current thread
|
||||
static uint64_t totBytesReceived() {
|
||||
return tot_bytes_received;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Reset counter for SocketConnection::totBytesSent
|
||||
/// and SocketConnection::totBytesReceived.
|
||||
static void resetByteCounter() {
|
||||
tot_bytes_sent = 0;
|
||||
tot_bytes_received = 0;
|
||||
}
|
||||
|
||||
/// Return peer's IP address.
|
||||
const std::string &peerIp() const {
|
||||
return peer_ip;
|
||||
}
|
||||
|
||||
/// Return peer's port number.
|
||||
uint16_t peerPort() const {
|
||||
return peer_port;
|
||||
}
|
||||
|
||||
/// Enable debug output of data sent and received.
|
||||
void dbgOn(bool b = true) {
|
||||
debugging = b;
|
||||
}
|
||||
|
||||
/// Return true if socket debugging is enabled.
|
||||
bool dbgIsOn() {
|
||||
return debugging;
|
||||
}
|
||||
protected:
|
||||
|
||||
/// If fd is the socket descriptor of an already established connection,
|
||||
/// you may let us manage the connection by calling this constructor.
|
||||
/// fd will probably be a client socket connected through a ServerSocket.
|
||||
SocketConnection(const std::string &label, Task *owner, int fd,
|
||||
const char *ip, uint16_t port);
|
||||
|
||||
/// Send a "message" to owner task. It will be executed as
|
||||
/// Task::msgFromConnection(this, msg) in the owner task.
|
||||
/// This is useful if you want to create SocketConnection
|
||||
/// subclasseses that work with any Task.
|
||||
PollState tellOwner(const std::string &msg);
|
||||
|
||||
private:
|
||||
// Create a connection to the given host. Will be executed asynchronously,
|
||||
// then one of the callbacks connected / connectionFailed will be called.
|
||||
SocketConnection(Task *owner, const std::string &hostname,
|
||||
unsigned int port);
|
||||
|
||||
SocketConnection(const SocketConnection &);
|
||||
|
||||
// Will return false if connection fails immediately, e.g. if DNS
|
||||
// lookup fails. Otherwise callback "connected" or "connectionFailed"
|
||||
// will be called - perhaps even before this call returns:
|
||||
bool asyncConnect();
|
||||
|
||||
PollState doRead(int fd);
|
||||
|
||||
// In states READ and READ_BLOCKED, this will be called to check if
|
||||
// we the connection should be checked for writability too.
|
||||
// If you override this, make sure to return true if asyncBufferSize() > 0.
|
||||
bool wantToSend() override {
|
||||
return !to_send.empty();
|
||||
}
|
||||
|
||||
PollState doWrite() {
|
||||
if (to_send.empty())
|
||||
return this->writeData();
|
||||
if (size_t written = sendData(to_send.c_str(), to_send.size()))
|
||||
to_send.erase(0, written);
|
||||
return state();
|
||||
}
|
||||
|
||||
std::string to_send;
|
||||
char socket_buffer[100000];
|
||||
|
||||
std::string peer_ip;
|
||||
struct addrinfo *local_ip;
|
||||
uint16_t peer_port;
|
||||
uint16_t prefer_ip_type;
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
// Per thread byte counters.
|
||||
static uint64_t tot_bytes_sent, tot_bytes_received;
|
||||
bool debugging = false;
|
||||
#ifdef USE_GNUTLS
|
||||
bool use_tls = false;
|
||||
bool session_initialized = false;
|
||||
bool tls_send_pending = false;
|
||||
void setSessionInitialized() {
|
||||
session_initialized = true;
|
||||
}
|
||||
bool tlsInitialized() {
|
||||
return session_initialized;
|
||||
}
|
||||
bool init_tls_server(gnutls_certificate_credentials_t &x509_cred,
|
||||
gnutls_priority_t &priority_cache);
|
||||
bool init_tls_client(gnutls_certificate_credentials_t &x509_cred,
|
||||
bool verify_cert);
|
||||
int try_tls_handshake() {
|
||||
dbg_log() << "TLS handshake socket " << socket();
|
||||
return gnutls_handshake(session);
|
||||
}
|
||||
static ssize_t tls_pull_static(gnutls_transport_ptr_t self,
|
||||
void *buf, size_t len) {
|
||||
return static_cast<SocketConnection *>(self)->tls_pull(buf, len);
|
||||
}
|
||||
static ssize_t tls_push_static(gnutls_transport_ptr_t self,
|
||||
const void *buf, size_t len) {
|
||||
return static_cast<SocketConnection *>(self)->tls_push(buf, len);
|
||||
}
|
||||
ssize_t tls_pull(void *buf, size_t len);
|
||||
ssize_t tls_push(const void *buf, size_t len);
|
||||
gnutls_session_t session;
|
||||
#endif
|
||||
};
|
||||
103
src/framework/socketreceiver.cpp
Normal file
103
src/framework/socketreceiver.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
#include "socketreceiver.h"
|
||||
|
||||
SocketReceiver::SocketReceiver(Task *task, int sock, pid_t peer_pid) :
|
||||
ServerSocket(sock, "SocketReceiver", task),
|
||||
peer(peer_pid) {
|
||||
|
||||
empty_data.iov_base = nullptr;
|
||||
empty_data.iov_len = 0;
|
||||
|
||||
memset(&fdpass_msg, 0, sizeof(fdpass_msg));
|
||||
fdpass_msg.msg_iov = &empty_data;
|
||||
fdpass_msg.msg_iovlen = 1;
|
||||
|
||||
fdpass_msg.msg_control = cmsgbuf;
|
||||
fdpass_msg.msg_controllen = sizeof(cmsgbuf);
|
||||
cmsg = CMSG_FIRSTHDR(&fdpass_msg);
|
||||
cmsg->cmsg_level = SOL_SOCKET;
|
||||
cmsg->cmsg_type = SCM_RIGHTS;
|
||||
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
|
||||
fdpass_msg.msg_controllen = cmsg->cmsg_len;
|
||||
|
||||
memset(&parent_msg, 0, sizeof(parent_msg));
|
||||
parent_msg.msg_iov = &msg_data;
|
||||
parent_msg.msg_iovlen = 1;
|
||||
}
|
||||
|
||||
SocketConnection *SocketReceiver::incoming() {
|
||||
//log() << "SocketReceiver::new_client_socket()";
|
||||
struct msghdr child_msg;
|
||||
|
||||
memset(&child_msg, 0, sizeof(child_msg));
|
||||
child_msg.msg_control = cmsgbuf;
|
||||
child_msg.msg_controllen = sizeof(cmsgbuf);
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static char buf[10000];
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static struct iovec data = { buf, sizeof(buf) };
|
||||
|
||||
child_msg.msg_iov = &data;
|
||||
child_msg.msg_iovlen = 1;
|
||||
|
||||
ssize_t len = recvmsg(socket(), &child_msg, MSG_DONTWAIT);
|
||||
if (len < 0) {
|
||||
log() << "recvmsg() failed: " << strerror(errno);
|
||||
return nullptr;
|
||||
}
|
||||
cmsg = CMSG_FIRSTHDR(&child_msg);
|
||||
if (cmsg == nullptr || cmsg->cmsg_type != SCM_RIGHTS) {
|
||||
//log() << "Error: not a file descriptor";
|
||||
if (child_msg.msg_iovlen && owner()) {
|
||||
struct iovec data1 = child_msg.msg_iov[0];
|
||||
auto addr = reinterpret_cast<const char *>(data1.iov_base);
|
||||
owner()->workerMessage(this, addr, static_cast<size_t>(len));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int newfd;
|
||||
memcpy(&newfd, CMSG_DATA(cmsg), sizeof(newfd));
|
||||
|
||||
uint16_t port;
|
||||
const char *ip = Socket::getIp(newfd, &port);
|
||||
dbg_log() << "Received socket " << newfd << " from " << ip
|
||||
<< " port " << port;
|
||||
|
||||
SocketConnection *conn = owner()->newClient(newfd, ip, port, this);
|
||||
if (!conn)
|
||||
closeSocket(newfd);
|
||||
return conn;
|
||||
}
|
||||
|
||||
int SocketReceiver::passSocketToPeer(int fd) {
|
||||
memcpy(CMSG_DATA(cmsg), &fd, sizeof(fd));
|
||||
|
||||
log() << "passing fd " << fd << " to peer";
|
||||
int ret;
|
||||
if (sendmsg(socket(), &fdpass_msg, MSG_DONTWAIT) < 0)
|
||||
ret = errno;
|
||||
else
|
||||
ret = 0;
|
||||
return ret;
|
||||
}
|
||||
|
||||
ssize_t SocketReceiver::passMessageToPeer(const char *buf, size_t len) {
|
||||
msg_data.iov_base = const_cast<char *>(buf);
|
||||
msg_data.iov_len = len;
|
||||
ssize_t sent = sendmsg(socket(), &parent_msg, 0);
|
||||
if (sent != static_cast<ssize_t>(len))
|
||||
log() << "only sent " << sent << " of total " << len;
|
||||
return sent;
|
||||
}
|
||||
|
||||
#endif
|
||||
59
src/framework/socketreceiver.h
Normal file
59
src/framework/socketreceiver.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include "serversocket.h"
|
||||
class SocketConnection;
|
||||
|
||||
/// \brief
|
||||
/// Pass sockets and messages between processes.
|
||||
///
|
||||
/// Typical use is to have a master process listen on a port,
|
||||
/// passing any new connections to child processes.
|
||||
///
|
||||
/// Both sockets and messages (data) may be passsed between processes.
|
||||
///
|
||||
/// A pair of Unix domain sockets must be created in the parent process,
|
||||
/// to be used by one SocketReceiver in the parent and one SocketReceiver
|
||||
/// in the child process.
|
||||
class SocketReceiver : public ServerSocket {
|
||||
public:
|
||||
/// Add a SocketReceiver to the given Task.
|
||||
/// The `sock` parameter shall be one of a pair of Unix domain sockets.
|
||||
/// The `peer_pid` parameter is used only for logging.
|
||||
SocketReceiver(Task *task, int sock, pid_t peer_pid);
|
||||
|
||||
/// Return connection object if new client available, else return nullptr.
|
||||
virtual SocketConnection *incoming() override;
|
||||
|
||||
/// Return 0 for success, errno on failure
|
||||
int passSocketToPeer(int fd);
|
||||
|
||||
/// Send data to peer. Return amount sent, or < 0 for error.
|
||||
ssize_t passMessageToPeer(const char *buf, size_t len);
|
||||
|
||||
/// Return PID of the peer process.
|
||||
pid_t peerPid() const {
|
||||
return peer;
|
||||
}
|
||||
|
||||
/// Stop communication with peer process and terminate as soon as possible.
|
||||
void peerDead() {
|
||||
peer = 0;
|
||||
closeMe();
|
||||
}
|
||||
private:
|
||||
pid_t peer;
|
||||
|
||||
// Used when passing file descriptors to peer:
|
||||
struct msghdr fdpass_msg;
|
||||
struct iovec empty_data;
|
||||
char cmsgbuf[CMSG_SPACE(sizeof(int))];
|
||||
struct cmsghdr *cmsg;
|
||||
|
||||
// Used when sending data to peer:
|
||||
struct msghdr parent_msg;
|
||||
struct iovec msg_data;
|
||||
};
|
||||
27
src/framework/synchronousbridge.cpp
Normal file
27
src/framework/synchronousbridge.cpp
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <queue>
|
||||
|
||||
#include "synchronousbridge.h"
|
||||
|
||||
void SynchronousClient::initialMsgToAgent(std::deque<std::string> &) {
|
||||
// Override this to push messages onto the queue.
|
||||
}
|
||||
|
||||
double SynchronousBridge::start() {
|
||||
dbg_log() << "starting";
|
||||
BridgeTask::start();
|
||||
the_client->initialMsgToAgent(incoming_messages);
|
||||
clear_queue();
|
||||
return 0;
|
||||
}
|
||||
|
||||
SynchronousBridge::~SynchronousBridge() {
|
||||
}
|
||||
|
||||
void SynchronousBridge::sendMsgToClient(const std::string &msg) {
|
||||
// The client executes code only from within the below call.
|
||||
the_client->newEventFromAgent(incoming_messages, msg);
|
||||
clear_queue();
|
||||
}
|
||||
69
src/framework/synchronousbridge.h
Normal file
69
src/framework/synchronousbridge.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <map>
|
||||
|
||||
#include "../framework/bridgetask.h"
|
||||
|
||||
/// \brief
|
||||
/// Client that only exists (or, rather, executes code) from within the bridge.
|
||||
///
|
||||
/// This way, it's trivial to create a non-interactive interface to the agent.
|
||||
///
|
||||
/// Shall be used with a SynchronousBridge.
|
||||
class SynchronousClient {
|
||||
public:
|
||||
/// \brief
|
||||
/// Send initial messages to the agent.
|
||||
///
|
||||
/// Override this to push messages onto the queue.
|
||||
virtual void initialMsgToAgent(std::deque<std::string> &return_msgs);
|
||||
|
||||
/// \brief
|
||||
/// Retrieve a new message from the agent.
|
||||
///
|
||||
/// The client will only execute code from within its implementation
|
||||
/// of this method.
|
||||
///
|
||||
/// The client must push any return messages onto return_msgs.
|
||||
virtual void newEventFromAgent(std::deque<std::string> &return_msgs,
|
||||
const std::string &msg) = 0;
|
||||
|
||||
virtual ~SynchronousClient() { }
|
||||
};
|
||||
|
||||
/// \brief
|
||||
/// A bridge that "owns" the client.
|
||||
///
|
||||
/// The client only exists (or, rather, executes code) from within the bridge.
|
||||
///
|
||||
/// Shall be used with a SynchronousClient.
|
||||
class SynchronousBridge : public BridgeTask {
|
||||
public:
|
||||
SynchronousBridge(Task *agent, SynchronousClient *client) :
|
||||
BridgeTask(agent),
|
||||
the_client(client) {
|
||||
}
|
||||
|
||||
/// See Task::start.
|
||||
double start() override;
|
||||
|
||||
/// Will call the client's SynchronousClient::newEventFromAgent method.
|
||||
void sendMsgToClient(const std::string &msg) override;
|
||||
|
||||
virtual ~SynchronousBridge() override;
|
||||
private:
|
||||
void clear_queue() {
|
||||
while (!incoming_messages.empty()) {
|
||||
std::string msg = incoming_messages.front();
|
||||
incoming_messages.pop_front();
|
||||
log() << "sendMsgToAgent " << msg;
|
||||
sendMsgToAgent(msg);
|
||||
}
|
||||
}
|
||||
SynchronousClient *the_client;
|
||||
std::deque<std::string> incoming_messages;
|
||||
};
|
||||
146
src/framework/task.cpp
Normal file
146
src/framework/task.cpp
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "task.h"
|
||||
#include "socketconnection.h"
|
||||
#include "serversocket.h"
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
EventLoop *Task::supervisor = nullptr;
|
||||
|
||||
Task::Task(const std::string &task_name) :
|
||||
Logger(task_name) {
|
||||
log() << "Task " << task_name << " created";
|
||||
}
|
||||
|
||||
void Task::setResult(const std::string &res) {
|
||||
if (is_finished) {
|
||||
dbg_log() << "result already set, ignoring " << res;
|
||||
} else {
|
||||
dbg_log() << "setResult " << res;
|
||||
is_finished = true;
|
||||
the_result = res;
|
||||
if (has_started)
|
||||
supervisor->notifyTaskFinished(this);
|
||||
}
|
||||
}
|
||||
|
||||
void Task::setMessage(const std::string &msg) {
|
||||
the_message = msg;
|
||||
supervisor->notifyTaskMessage(this);
|
||||
}
|
||||
|
||||
PollState Task::connectionReady(SocketConnection * /* conn */) {
|
||||
log() << "connectionReady not implemented";
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
PollState Task::msgFromConnection(SocketConnection * /* conn */,
|
||||
const std::string & /* msg */) {
|
||||
log() << "msgFromConnection not implemented";
|
||||
return PollState::CLOSE;
|
||||
}
|
||||
|
||||
bool Task::addConnection(SocketConnection *conn) {
|
||||
if (terminated()) {
|
||||
if (conn)
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
return supervisor->addConnection(conn);
|
||||
}
|
||||
|
||||
bool Task::addConnected(SocketConnection *conn) {
|
||||
if (terminated()) {
|
||||
if (conn)
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
return supervisor->addConnected(conn);
|
||||
}
|
||||
|
||||
bool Task::adoptConnection(Socket *conn) {
|
||||
conn->setOwner(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::set<Socket *> Task::getMyConnections() const {
|
||||
return supervisor->findConnByTask(this);
|
||||
}
|
||||
|
||||
void Task::wakeUp() {
|
||||
supervisor->wakeUpTask(this);
|
||||
}
|
||||
|
||||
Task::~Task() {
|
||||
supervisor->taskDeleted(this);
|
||||
}
|
||||
|
||||
bool Task::addServer(ServerSocket *conn) {
|
||||
if (terminated()) {
|
||||
if (conn)
|
||||
delete conn;
|
||||
return false;
|
||||
}
|
||||
return supervisor->addServer(conn);
|
||||
}
|
||||
|
||||
int Task::runProcess(const char *const argv[]) {
|
||||
return supervisor->externalCommand(this, argv);
|
||||
}
|
||||
|
||||
void Task::processFinished(int pid, int wstatus) {
|
||||
log() << "Process " << pid << " finished, status " << wstatus;
|
||||
}
|
||||
|
||||
bool Task::parseListen(const TaskConfig &tc, const std::string &log_label) {
|
||||
auto to = tc.cfg().upper_bound("listen");
|
||||
for (auto p=tc.cfg().lower_bound("listen"); p!=to; ++p) {
|
||||
std::istringstream s(p->second);
|
||||
uint16_t port;
|
||||
std::string ip;
|
||||
s >> port;
|
||||
if (!s) {
|
||||
err_log() << "Bad configuration directive: listen " << p->second;
|
||||
setError("bad configuration directive");
|
||||
return false;
|
||||
}
|
||||
s >> ip;
|
||||
bool tls;
|
||||
if (ip.empty()) {
|
||||
tls = false;
|
||||
} else if (ip == "tls") {
|
||||
ip.clear();
|
||||
tls = true;
|
||||
} else {
|
||||
std::string tmp;
|
||||
s >> tmp;
|
||||
tls = (tmp == "tls");
|
||||
}
|
||||
auto sock = new ServerSocket(log_label, this, port, ip);
|
||||
#ifdef USE_GNUTLS
|
||||
if (tls) {
|
||||
std::string cert, key, password;
|
||||
s >> cert >> key >> password;
|
||||
if (key.empty() || !tlsSetKey(sock, cert, key, password)) {
|
||||
err_log() << "Bad configuration: " << p->second;
|
||||
setError("cannot use TLS certificate");
|
||||
return false;
|
||||
}
|
||||
log() << "Port " << port << " enable TLS";
|
||||
}
|
||||
#else
|
||||
if (tls) {
|
||||
err_log() << "cannot enable TLS, will not listen on port " << port;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
if (!addServer(sock)) {
|
||||
setError("cannot listen");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
519
src/framework/task.h
Normal file
519
src/framework/task.h
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include <fstream>
|
||||
#include "logger.h"
|
||||
#include "taskconfig.h"
|
||||
#include "eventloop.h"
|
||||
|
||||
class Socket;
|
||||
class ServerSocket;
|
||||
class SocketConnection;
|
||||
enum class PollState;
|
||||
class SocketReceiver;
|
||||
class WorkerProcess;
|
||||
|
||||
/// \brief
|
||||
/// The purpose of a task is to manage socket connections, and/or to execute
|
||||
/// timers.
|
||||
///
|
||||
/// This is essentially an abstract base class. You must create subclasses and
|
||||
/// pass subclass objects to the EventLoop's addTask method.
|
||||
///
|
||||
/// Timers are very simple; the start method must return the number of seconds
|
||||
/// until the timerEvent method should be called. The overridden timerEvent can
|
||||
/// do whatever it needs and then return the number of seconds until it should
|
||||
/// be called again. A value <= 0 means it will never be called again.
|
||||
///
|
||||
/// Note that the application will be single threaded. Thus, timers (and other
|
||||
/// callbacks) must avoid blocking and generally try do be as quick as possible,
|
||||
/// otherwise all subsequent timers will be delayed.
|
||||
///
|
||||
/// The Task may create SocketConnection objects and add them using the
|
||||
/// addConnection method. It may also create ServerSocket objects and
|
||||
/// add them with addServer; whenever a client connects to the server,
|
||||
/// you will be notified through the newClient method.
|
||||
class Task : public Logger {
|
||||
public:
|
||||
/// \brief Create a task with the given name.
|
||||
///
|
||||
/// The name will be used as a log label
|
||||
/// and may be retrieved using the inherited Logger::label method.
|
||||
///
|
||||
/// The EventLoop might not be available when the constructor is run, so
|
||||
/// do not use it in the constructor of any subclass. Any asynchronous
|
||||
/// initialization should be performed in the Task::start() method.
|
||||
Task(const std::string &task_name);
|
||||
|
||||
/// A Task object will be deleted by the EventLoop after it has finished
|
||||
/// and its parent task has been notified.
|
||||
virtual ~Task();
|
||||
|
||||
/// When the EventLoop starts executing a task, it will call its start
|
||||
/// method. All non-trivial initialization, e.g. creating new socket
|
||||
/// connections, should be performed in the start method.
|
||||
///
|
||||
/// If the task needs a timer, the start method must return the number of
|
||||
/// seconds until timerEvent should
|
||||
/// be called, or <= 0 if you don't want it to be called.
|
||||
virtual double start() {
|
||||
dbg_log() << "Task starting. No timer.";
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Return number of seconds until this method should be
|
||||
/// called again, or <= 0 if you don't want it to be called again.
|
||||
virtual double timerEvent() {
|
||||
dbg_log() << "Default timerEvent: will kill task.";
|
||||
setTimeout();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Run timerEvent after s seconds instead of previous value.
|
||||
void resetTimer(double s) {
|
||||
supervisor->resetTimer(this, s);
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return true if the task has finished normally.
|
||||
///
|
||||
/// This method is meant to be used in the taskFinished callback.
|
||||
bool finishedOK() const {
|
||||
return (has_started && is_finished && !was_killed &&
|
||||
!was_error && !was_timeout);
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return true if the task is finished and was aborted by another task.
|
||||
///
|
||||
/// This method is meant to be used in the taskFinished callback.
|
||||
bool wasKilled() const {
|
||||
return was_killed;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return true if the task terminated with an error.
|
||||
///
|
||||
/// This method is meant to be used in the taskFinished callback.
|
||||
bool wasError() const {
|
||||
return was_error;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return true if the task terminated with a timeout.
|
||||
///
|
||||
/// This method is meant to be used in the taskFinished callback.
|
||||
bool wasTimeout() const {
|
||||
return was_timeout;
|
||||
}
|
||||
|
||||
/// Return true if the task has been added to the EventLoop and its
|
||||
/// Task::start() method has been executed.
|
||||
bool hasStarted() const {
|
||||
return has_started;
|
||||
}
|
||||
|
||||
/// Call this in the start() callback if you want all child tasks to be
|
||||
/// killed when this task is finished.
|
||||
void killChildTaskWhenFinished() {
|
||||
kill_children = true;
|
||||
}
|
||||
|
||||
/// Ignore this unless the task is a server task.
|
||||
/// If a new remote connection is made through any ServerSocket object
|
||||
/// owned by us, the client socket, ip address and port number will
|
||||
/// be passed to the below method. Override it to create and return
|
||||
/// an object of a subclass to SocketConnection, otherwise the client
|
||||
/// socket will be closed. The object will be owned by the implementation
|
||||
/// and will be deleted when the connection has been closed. You _must_
|
||||
/// create the object with new.
|
||||
virtual SocketConnection *newClient(int, const char *, uint16_t,
|
||||
ServerSocket *) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// Request for me to adopt a socket owned by some other task.
|
||||
/// Return false to reject. Otherwise set me as owner and return true.
|
||||
virtual bool adoptConnection(Socket *conn);
|
||||
|
||||
/// This will be called to notify us when a new client socket object
|
||||
/// has been successfully added to this task.
|
||||
/// If you need to know that, override this method.
|
||||
virtual void connAdded(SocketConnection *) {
|
||||
}
|
||||
|
||||
/// This will be called when a client socket object has been removed from
|
||||
/// this task, just before it is deleted.
|
||||
/// If you need to know that, override this method.
|
||||
virtual void connRemoved(SocketConnection *) {
|
||||
}
|
||||
|
||||
/// This will be called to notify us when a new server (listening) socket
|
||||
/// object has been successfully added to this task.
|
||||
/// If you need to know that, override this method.
|
||||
virtual void serverAdded(ServerSocket *) {
|
||||
}
|
||||
|
||||
/// This will be called when a server socket object has been removed from
|
||||
/// this task, just before it is deleted.
|
||||
/// If you need to know that, override this method.
|
||||
virtual void serverRemoved(ServerSocket *) {
|
||||
}
|
||||
|
||||
/// To get the "result" of the task after it has finished.
|
||||
std::string result() const {
|
||||
return the_result;
|
||||
}
|
||||
|
||||
/// Return all current connections.
|
||||
std::set<Socket *> getMyConnections() const;
|
||||
|
||||
/// Return true if the connection still exists.
|
||||
bool isActive(Socket *conn) const {
|
||||
return supervisor->isActive(conn);
|
||||
}
|
||||
|
||||
/// Restart all idle connections
|
||||
void wakeUp();
|
||||
|
||||
/// If s is idle, restart it and return true. Otherwise return false.
|
||||
bool wakeUpConnection(SocketConnection *s) {
|
||||
return supervisor->wakeUpConnection(s);
|
||||
}
|
||||
|
||||
/// Terminate and remove a connection.
|
||||
void cancelConnection(SocketConnection *s) {
|
||||
supervisor->cancelConnection(s);
|
||||
}
|
||||
|
||||
/// Return the current (outgoing) message.
|
||||
std::string message() const {
|
||||
return the_message;
|
||||
}
|
||||
|
||||
/// Notify that this task will be observing task "to". This task will be
|
||||
/// notified through a call to taskFinished if "to" terminates before me.
|
||||
/// (A parent task is observing its child tasks by default.)
|
||||
bool startObserving(Task *to) {
|
||||
return supervisor->startObserving(this, to);
|
||||
}
|
||||
|
||||
/// Execute receiver's Task::handleExecution method immediately. The call
|
||||
/// will be ignored unless this task is observing the receiver.
|
||||
/// Note that you could also call `receiver->handleExecution()` (or any
|
||||
/// other method in receiver) directly. However, that might be dangerous
|
||||
/// since your pointer to the receiver is a _weak reference_ and you
|
||||
/// must somehow make sure that the receiver still exists.
|
||||
/// The advantages of using this API instead of directly calling
|
||||
/// methods in the receiver task are:
|
||||
/// 1. Safer; the call will be ignored if the receiver task doesn't exist.
|
||||
/// 2. The sender and receiver classes do not have to know each other.
|
||||
/// 3. Since you are observing the receiver, you will be notified
|
||||
/// when the receiver task terminates.
|
||||
///
|
||||
/// *Note*:
|
||||
/// Essentially, the receiver's Task::handleExecution method executes from
|
||||
/// within the sender's method (event handler). If the receiver in any
|
||||
/// way modifies the sender from within its Task::handleExecution method,
|
||||
/// bad things may happen. Code the Task::handleExecution methods carefully.
|
||||
void executeHandler(Task *receiver, const std::string &message) {
|
||||
if (supervisor->isObserving(this, receiver) && !receiver->terminated())
|
||||
receiver->handleExecution(this, message);
|
||||
else
|
||||
log() << "Will not call handleExecution since task isn't observed.";
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return number of seconds since the task was started.
|
||||
///
|
||||
/// The start time of a task is when the Task::start method is executed.
|
||||
/// Do not call this method until the task has been added to the EventLoop.
|
||||
double elapsed() const {
|
||||
return secondsSince(start_time);
|
||||
}
|
||||
#ifndef _WIN32
|
||||
/// Override this if you intend to start child processes with
|
||||
/// createWorker(). It will be executed in the child process.
|
||||
/// wno is the argument last parameter you supplied to createWorker().
|
||||
/// You must create a Task with new and return its address.
|
||||
virtual Task *createWorkerTask(unsigned int wno) {
|
||||
log() << "missing createWorkerTask, cannot create worker " << wno;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/// This will be called in child process directly before exit.
|
||||
/// Override if you need to clean up after createWorkerTask.
|
||||
virtual void finishWorkerTask(unsigned int ) {
|
||||
}
|
||||
|
||||
/// Called during startup of worker process, once for each SocketReceiver
|
||||
/// object. Will be called before serverAdded with the same SocketReceiver.
|
||||
/// Overload this if your worker process has two or more SocketReceiver
|
||||
/// objects with different confguration (i.e. different SSL keys).
|
||||
virtual void newWorkerChannel(SocketReceiver *, unsigned int ) {
|
||||
}
|
||||
|
||||
/// Called if parent/worker sends a message through a SocketReceiver:
|
||||
virtual void workerMessage(SocketReceiver *, const char *buf, size_t len) {
|
||||
log() << "Worker message: " << std::string(buf, len);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Normally, SocketConnection objects are designed for a specific type of
|
||||
/// Task, i.e. a HttpServerConnection might be designed for a WebServerTask.
|
||||
/// However, some SocketConnection subclasses are "generic" and meant to
|
||||
/// work with any Task object as owner. When such SocketConnection objects
|
||||
/// want to contact the owner, they may use this method to signal that the
|
||||
/// socket has been connetced.
|
||||
///
|
||||
/// Return PollState::READ to keep the connection,
|
||||
/// or PollState::CLOSE to close it.
|
||||
virtual PollState connectionReady(SocketConnection * /* conn */);
|
||||
|
||||
|
||||
/// Normally, SocketConnection objects are designed for a specific type of
|
||||
/// Task, i.e. a HttpServerConnection might be designed for a WebServerTask.
|
||||
/// However, some SocketConnection subclasses are "generic" and meant to
|
||||
/// work with any Task object as owner. When such SocketConnection objects
|
||||
/// have a message for their owner, they may use this method.
|
||||
///
|
||||
/// Return PollState::READ to keep the connection,
|
||||
/// or PollState::CLOSE to close it.
|
||||
virtual PollState msgFromConnection(SocketConnection * /* conn */,
|
||||
const std::string & /* msg */);
|
||||
|
||||
/// Number of bytes sent through SocketConnection objects owned by me.
|
||||
uint64_t bytesSent() const {
|
||||
return tot_bytes_sent;
|
||||
}
|
||||
|
||||
/// Number of bytes received through SocketConnection objects owned by me.
|
||||
uint64_t bytesReceived() const {
|
||||
return tot_bytes_received;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Reset the values for the methods Task::bytesSent
|
||||
/// and Task::bytesReceived.
|
||||
void resetByteCount() {
|
||||
tot_bytes_sent = 0;
|
||||
tot_bytes_received = 0;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Notify the task that data has been sent on its behalf.
|
||||
///
|
||||
/// For use *only* by custom SocketConnection subclasses after calling send
|
||||
/// directly on a socket.
|
||||
/// Don't call the this methods unless you know what you're doing.
|
||||
void notifyBytesSent(uint64_t n) {
|
||||
tot_bytes_sent += n;
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Notify the task that data has been received on its behalf.
|
||||
///
|
||||
/// For use *only* by custom SocketConnection subclasses after calling recv
|
||||
/// directly on a socket.
|
||||
/// Don't call the this methods unless you know what you're doing.
|
||||
void notifyBytesReceived(uint64_t n) {
|
||||
tot_bytes_received += n;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
/// To add a new client connection. Will delete conn and return false
|
||||
/// on immediate failure. connRemoved will not be called in that case.
|
||||
///
|
||||
/// The connection must belong to a _running_ task (probably this one).
|
||||
/// I.e. the task must have been added to the EventLoop and the start()
|
||||
/// method must have been called. You can't do this in the constructor!
|
||||
///
|
||||
/// If successfully added, we will call connAdded and return true.
|
||||
/// The EventLoop takes ownership of the SocketConnection object and
|
||||
/// will call connRemoved and then delete it if the connection fails or
|
||||
/// is closed or when the task is finished.
|
||||
bool addConnection(SocketConnection *conn);
|
||||
|
||||
/// Use this if conn contains a socket that has already been connected.
|
||||
/// Returns false (and deletes conn) on failure.
|
||||
/// On success, returns true and calls connAdded on owner task,
|
||||
/// then calls connected() on conn to get initial state.
|
||||
bool addConnected(SocketConnection *conn);
|
||||
|
||||
/// As Task::addConnected, but with a server connection.
|
||||
bool addServer(ServerSocket *conn);
|
||||
|
||||
/// Check config file for listening (server) sockets. The sockets
|
||||
/// are added to the task, with the given log_label. E.g.
|
||||
///
|
||||
/// listen 80 192.36.30.2
|
||||
/// listen 8080
|
||||
/// listen 443 tls /etc/ssl/fd.crt /etc/ssl/fd.key 4lEGyLax
|
||||
///
|
||||
/// The value of the listen parameter is either a port number,
|
||||
/// e.g. "8080", or a port number followed by a space and an ip address,
|
||||
/// e.g. "8080 192.168.0.1". The address is either ipv4 or ipv6.
|
||||
/// If connections are to be encrypted, add "tls" followed by
|
||||
/// paths to your SSL certificate and private key, optionally followed
|
||||
/// by the password (if the key is protected by a password).
|
||||
bool parseListen(const TaskConfig &tc, const std::string &log_label);
|
||||
|
||||
#ifdef USE_GNUTLS
|
||||
/// Use SSL certificate for a listening socket.
|
||||
virtual bool tlsSetKey(ServerSocket *conn, const std::string &crt_path,
|
||||
const std::string &key_path, const std::string &password) {
|
||||
return supervisor->tlsSetKey(conn, crt_path, key_path, password);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// When the task is done, it should notify the EventLoop by calling the
|
||||
/// Task::setResult method. Then the task's parent will be notified and the
|
||||
/// task will be deleted. The "result" of the task should be a non-empty
|
||||
/// string on success, and an empty string on timeout or error.
|
||||
/// Of course, subclasses can calculate more complex custom "results" in
|
||||
/// addition to this simple string.
|
||||
void setResult(const std::string &res);
|
||||
|
||||
/// Called to signal fatal error. May be overridden to "catch" errors.
|
||||
/// It should always call Task::setResult with an empty string.
|
||||
virtual void setError(const std::string &msg) {
|
||||
log() << "Task failure: " << msg;
|
||||
was_error = true;
|
||||
setResult("");
|
||||
}
|
||||
|
||||
/// Called to signal timeout. May be overridden to "catch" timeouts.
|
||||
/// It should always call Task::setResult with an empty string.
|
||||
virtual void setTimeout() {
|
||||
was_timeout = true;
|
||||
setResult("");
|
||||
}
|
||||
|
||||
/// Call to signal that the task has a "message" to deliver. The parent
|
||||
/// will be notified using the taskMessage method. Only the last message
|
||||
/// will be stored, and it may be retrieved using the message method.
|
||||
void setMessage(const std::string &msg);
|
||||
|
||||
/// Called when an observed task, e.g. a child task, terminates.
|
||||
/// Override this method to handle such events.
|
||||
virtual void taskFinished(Task *task) {
|
||||
dbg_log() << "Task " << task->label() << " died, no handler defined.";
|
||||
}
|
||||
|
||||
/// Called when an a child task has (set/sent) a message.
|
||||
/// Override this method to handle such events.
|
||||
virtual void taskMessage(Task *task) {
|
||||
dbg_log() << "No taskMessage handler implemented for " << task->label();
|
||||
}
|
||||
|
||||
/// Callback to execute code on behalf of another Task.
|
||||
virtual void handleExecution(Task *sender, const std::string &message) {
|
||||
dbg_log() << "Event " << message << " from "
|
||||
<< (sender ? sender->label() : "unknown")
|
||||
<< ", no handler implemented";
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return true if task is finished.
|
||||
///
|
||||
/// By default, this will return true if the task has called
|
||||
/// the Task::setResult method.
|
||||
///
|
||||
/// The task will be deleted very soon unless return value is false. So
|
||||
/// this method is mostly useful within the Task subclass itself, checking
|
||||
/// if it is about to be removed.
|
||||
bool terminated() const {
|
||||
return is_finished;
|
||||
}
|
||||
|
||||
/// Insert another Task for execution by the EventLoop.
|
||||
void addNewTask(Task *task, Task *parent = nullptr) {
|
||||
supervisor->addTask(task, parent);
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
/// Run task in a new thread.
|
||||
void addNewThread(Task *task, const std::string &name="ThreadLoop",
|
||||
std::ostream *log_file = nullptr,
|
||||
Task *parent = nullptr) {
|
||||
supervisor->spawnThread(task, name, log_file, parent);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Add all my child tasks to the given set.
|
||||
void getMyTasks(std::set<Task *> &tset) {
|
||||
supervisor->getChildTasks(tset, this);
|
||||
}
|
||||
|
||||
/// Terminate all my child tasks.
|
||||
void abortMyTasks() {
|
||||
supervisor->abortChildTasks(this);
|
||||
}
|
||||
|
||||
/// Terminate a task.
|
||||
void abortTask(Task *task) {
|
||||
supervisor->abortTask(task);
|
||||
}
|
||||
|
||||
/// Terminate all tasks and exit the EventLoop.
|
||||
void abortAllTasks() {
|
||||
supervisor->abort();
|
||||
}
|
||||
|
||||
/// Start execution of external command, return an ID.
|
||||
/// On immediate failure, return value is -1.
|
||||
int runProcess(const char *const argv[]);
|
||||
|
||||
/// Will be called to notify when an external process has terminated.
|
||||
virtual void processFinished(int pid, int wstatus);
|
||||
|
||||
#ifndef _WIN32
|
||||
/// \brief
|
||||
/// Run task returned by this->createWorkerTask in a child process.
|
||||
/// Return nullptr on failure.
|
||||
WorkerProcess *createWorker(std::ostream *log_file = nullptr,
|
||||
unsigned int channels = 1,
|
||||
unsigned int wno = 0) {
|
||||
return supervisor->createWorker(this, log_file, channels, wno);
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Run task returned by this->createWorkerTask in a child process.
|
||||
/// Return nullptr on failure.
|
||||
WorkerProcess *createWorker(const std::string &log_file_name,
|
||||
unsigned int channels = 1,
|
||||
unsigned int wno = 0) {
|
||||
return supervisor->createWorker(this, log_file_name, channels, wno);
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
friend class EventLoop;
|
||||
void setTerminated() {
|
||||
is_finished = true;
|
||||
}
|
||||
// Called by EventLoop when task is scheduled for execution:
|
||||
double begin() {
|
||||
has_started = true;
|
||||
start_time = timeNow();
|
||||
return this->start();
|
||||
}
|
||||
|
||||
#ifdef USE_THREADS
|
||||
thread_local
|
||||
#endif
|
||||
static EventLoop *supervisor;
|
||||
TimePoint start_time;
|
||||
std::string the_result;
|
||||
std::string the_message;
|
||||
uint64_t tot_bytes_sent = 0, tot_bytes_received = 0;
|
||||
bool is_finished = false;
|
||||
bool has_started = false;
|
||||
bool was_killed = false, was_error = false, was_timeout = false;
|
||||
bool kill_children = false;
|
||||
bool is_child_thread = false;
|
||||
};
|
||||
196
src/framework/taskconfig.cpp
Normal file
196
src/framework/taskconfig.cpp
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include "taskconfig.h"
|
||||
#include "eventloop.h"
|
||||
#include "../json11/json11.hpp"
|
||||
|
||||
TaskConfig::TaskConfig(std::istream &cfg_stream) {
|
||||
_load(cfg_stream);
|
||||
}
|
||||
|
||||
TaskConfig::TaskConfig(const std::string &cfg_text) {
|
||||
std::istringstream cfg_stream(cfg_text);
|
||||
_load(cfg_stream);
|
||||
}
|
||||
const char *BadTaskConfig::what() const noexcept {
|
||||
return err_msg.c_str();
|
||||
}
|
||||
|
||||
BadTaskConfig::~BadTaskConfig() noexcept {
|
||||
}
|
||||
|
||||
TaskConfig TaskConfig::load(const std::string &filename) {
|
||||
std::ifstream cfg_stream(filename);
|
||||
return TaskConfig(cfg_stream);
|
||||
}
|
||||
|
||||
std::string TaskConfig::value(const std::string &key, const std::string &default_value) const {
|
||||
auto range = the_config.equal_range(key);
|
||||
if (range.first == range.second)
|
||||
return default_value;
|
||||
--range.second;
|
||||
return range.second->second;
|
||||
}
|
||||
|
||||
void TaskConfig::openlog(std::ofstream &logger, bool append) const {
|
||||
auto p = the_config.find("logfile");
|
||||
if (p != the_config.end() && p->second != "-") {
|
||||
logger.open(p->second, append ? std::ios::app : std::ios::trunc);
|
||||
if (logger) {
|
||||
Logger::setLogFile(logger);
|
||||
#ifndef _WIN32
|
||||
EventLoop::setLogFilename(p->second);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TaskConfig::add(const std::string &key, const std::string &val) {
|
||||
the_config.insert(std::make_pair(key, val));
|
||||
}
|
||||
|
||||
void TaskConfig::addLine(const std::string &line) {
|
||||
size_t pos = line.find('#');
|
||||
if (pos != std::string::npos) {
|
||||
addLine(line.substr(0, pos));
|
||||
return;
|
||||
}
|
||||
if (line.empty())
|
||||
return;
|
||||
pos = line.find(" ");
|
||||
if (pos == std::string::npos)
|
||||
the_config.insert(std::make_pair(line, std::string()));
|
||||
else
|
||||
the_config.insert(std::make_pair(line.substr(0, pos),
|
||||
line.substr(pos+1)));
|
||||
}
|
||||
|
||||
void TaskConfig::workerAttributes(const std::set<std::string> &attrs) {
|
||||
std::vector<std::string> to_add;
|
||||
for (auto &p : the_config)
|
||||
if (attrs.find(p.first) != attrs.end())
|
||||
to_add.push_back(p.first + " " + p.second);
|
||||
for (auto &line : to_add)
|
||||
add("workercfg", line);
|
||||
}
|
||||
|
||||
void TaskConfig::_load(std::istream &cfg_stream) {
|
||||
std::string line;
|
||||
while (getline(cfg_stream, line)) {
|
||||
addLine(line);
|
||||
}
|
||||
if (!cfg_stream.eof())
|
||||
throw BadTaskConfig();
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &out, const TaskConfig &tc) {
|
||||
out << "[ ";
|
||||
for (auto &p : tc)
|
||||
out << p.first << " --> " << p.second << " ";
|
||||
out << ']';
|
||||
return out;
|
||||
}
|
||||
|
||||
std::set<std::string>
|
||||
TaskConfig::parseList(const std::string &category) const {
|
||||
std::string val;
|
||||
std::set<std::string> res;
|
||||
auto to = the_config.upper_bound(category);
|
||||
for (auto p=the_config.lower_bound(category); p!=to; ++p) {
|
||||
std::istringstream s(p->second);
|
||||
while (s >> val)
|
||||
res.insert(val);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void TaskConfig::parseArgs(int &argc, char **&argv) {
|
||||
int apos = 0;
|
||||
while (++apos < argc) {
|
||||
std::string arg = argv[apos];
|
||||
if (arg.substr(0, 2) != "--")
|
||||
break;
|
||||
arg.erase(0, 2);
|
||||
if (arg.empty()) {
|
||||
++apos;
|
||||
break;
|
||||
}
|
||||
auto pos = arg.find('=');
|
||||
if (pos == std::string::npos)
|
||||
the_config.insert(std::make_pair(arg, std::string()));
|
||||
else if (pos)
|
||||
the_config.insert(std::make_pair(arg.substr(0, pos),
|
||||
arg.substr(pos+1)));
|
||||
// else ignore
|
||||
}
|
||||
if (--apos) {
|
||||
argc -= apos;
|
||||
argv[apos] = argv[0];
|
||||
argv += apos;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::string, std::string>
|
||||
TaskConfig::parseKeyVal(const std::string &category) const {
|
||||
std::string key, val;
|
||||
std::map<std::string, std::string> res;
|
||||
auto to = the_config.upper_bound(category);
|
||||
for (auto p=the_config.lower_bound(category); p!=to; ++p) {
|
||||
std::istringstream s(p->second);
|
||||
if (s >> key >> val)
|
||||
res.insert(std::make_pair(key, val));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/*Preconditions: New configs set with method "saveConfigurationOption"
|
||||
Old config options loaded into the_config in MeasurementAgent() (loadJsonFromFile).
|
||||
Postconditions: Configuration options transferred onto file */
|
||||
bool TaskConfig::saveJsonToFile(const std::string &filename) {
|
||||
std::fstream cfgOptionsFile;
|
||||
|
||||
//Create file if not existing. Write the updated config in json-format to file.
|
||||
cfgOptionsFile.open(filename, std::fstream::out);
|
||||
if (cfgOptionsFile.is_open()) {
|
||||
cfgOptionsFile << json11::Json(the_config).dump();
|
||||
} else {
|
||||
Logger::log("TaskConfig") << "Unable to open config file";
|
||||
}
|
||||
|
||||
cfgOptionsFile.close();
|
||||
return bool(cfgOptionsFile);
|
||||
}
|
||||
|
||||
/*
|
||||
Used to load preexisting configuration options from file into json-object for further use.
|
||||
*/
|
||||
TaskConfig TaskConfig::loadJsonFromFile(const std::string &filename){
|
||||
std::string fileContent;
|
||||
std::ifstream inFile;
|
||||
std::string err;
|
||||
TaskConfig cfg;
|
||||
|
||||
inFile.open(filename);
|
||||
|
||||
if (inFile.is_open() && inFile) {
|
||||
//consume entire inFile, from beginning to end.
|
||||
fileContent.assign( (std::istreambuf_iterator<char>(inFile)),
|
||||
(std::istreambuf_iterator<char>()) );
|
||||
} else {
|
||||
Logger::log("TaskConfig") << "Unable to open config file";
|
||||
}
|
||||
|
||||
json11::Json JsonObj = json11::Json::parse(fileContent, err);
|
||||
|
||||
inFile.close();
|
||||
|
||||
for (auto p : JsonObj.object_items()) {
|
||||
cfg.add(p.first, p.second.string_value());
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
161
src/framework/taskconfig.h
Normal file
161
src/framework/taskconfig.h
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
|
||||
/// Exception thrown on syntax errors in task config
|
||||
class BadTaskConfig : public std::exception {
|
||||
public:
|
||||
BadTaskConfig(const std::string &msg = "cannot read config file") :
|
||||
err_msg(msg) {
|
||||
}
|
||||
const char *what() const noexcept override;
|
||||
BadTaskConfig(const BadTaskConfig &old) : err_msg(old.err_msg) {
|
||||
}
|
||||
~BadTaskConfig() noexcept override;
|
||||
std::string err_msg;
|
||||
};
|
||||
|
||||
/// \brief
|
||||
/// Read configuration from file or string
|
||||
///
|
||||
/// Empty lines are ignored.
|
||||
///
|
||||
/// A # character means the rest of the line is a comment.
|
||||
///
|
||||
/// All other lines must contain a configuration directive.
|
||||
/// If the line contains a space, the directive ends at the first space,
|
||||
/// and the rest of the line is the value of the directive.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// logfile /var/log/my_service.log
|
||||
/// name My Service # Will be printed in greeting message
|
||||
///
|
||||
/// listen 80
|
||||
/// listen 443 tls /etc/pki/tls/certs/mycert.pem /etc/pki/tls/private/mycert.pem
|
||||
class TaskConfig {
|
||||
public:
|
||||
/// Empty configuration.
|
||||
TaskConfig() {}
|
||||
|
||||
/// Load configuration from file.
|
||||
TaskConfig(std::istream &cfg_stream);
|
||||
|
||||
/// Load configuration from string.
|
||||
TaskConfig(const std::string &cfg_text);
|
||||
|
||||
/// Add a directive to the config.
|
||||
void add(const std::string &key, const std::string &val);
|
||||
|
||||
/// Replace value(s) of a directive with a new one.
|
||||
void set(const std::string &key, const std::string &val) {
|
||||
the_config.erase(key);
|
||||
add(key, val);
|
||||
}
|
||||
|
||||
/// Remove value(s) of a directive.
|
||||
void erase(const std::string &key) {
|
||||
the_config.erase(key);
|
||||
}
|
||||
|
||||
/// Set value of a directive unless already set.
|
||||
void setDefault(const std::string &key, const std::string &val) {
|
||||
if (the_config.find(key) == the_config.end())
|
||||
add(key, val);
|
||||
}
|
||||
|
||||
/// Incrementally add to the config.
|
||||
void addLine(const std::string &line);
|
||||
|
||||
/// Start iterator to loop over the config.
|
||||
std::multimap<std::string, std::string>::iterator begin() {
|
||||
return the_config.begin();
|
||||
}
|
||||
|
||||
/// End iterator to loop over the config.
|
||||
std::multimap<std::string, std::string>::iterator end() {
|
||||
return the_config.end();
|
||||
}
|
||||
|
||||
/// Start const iterator to loop over the config.
|
||||
std::multimap<std::string, std::string>::const_iterator begin() const {
|
||||
return the_config.begin();
|
||||
}
|
||||
|
||||
/// End const iterator to loop over the config.
|
||||
std::multimap<std::string, std::string>::const_iterator end() const {
|
||||
return the_config.end();
|
||||
}
|
||||
|
||||
/// Make a set of directives available to worker processes.
|
||||
void workerAttributes(const std::set<std::string> &attrs);
|
||||
|
||||
/// Read config from file.
|
||||
static TaskConfig load(const std::string &filename);
|
||||
|
||||
/// Return the parsed configuration.
|
||||
const std::multimap<std::string, std::string> &cfg() const {
|
||||
return the_config;
|
||||
}
|
||||
|
||||
/// Return value of last occurence of key. Return default_value if key does not exist.
|
||||
std::string value(const std::string &key, const std::string &default_value = "") const;
|
||||
|
||||
/// Return true if key exists, otherwise false:
|
||||
bool hasKey(const std::string &key) const {
|
||||
return the_config.find(key) != the_config.end();
|
||||
}
|
||||
|
||||
/// Return a range of the key/value paris for the given key.
|
||||
std::pair<std::multimap<std::string, std::string>::const_iterator,
|
||||
std::multimap<std::string, std::string>::const_iterator>
|
||||
range(const std::string &key) const {
|
||||
return the_config.equal_range(key);
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Log to the file specified by the `logfile` directive.
|
||||
///
|
||||
/// If the `logfile` key exists, and its value is not "-", try to use the
|
||||
/// value as a file name for the log.
|
||||
void openlog(std::ofstream &logger, bool append = false) const;
|
||||
|
||||
/// \brief
|
||||
/// Split config value into non-blank strings.
|
||||
///
|
||||
/// Return set of all non-whitespace strings listed after the configuration
|
||||
/// directive given by second parameter.
|
||||
std::set<std::string>
|
||||
parseList(const std::string &category = "whitelist") const;
|
||||
|
||||
/// Parse command line arguments starting with "--":
|
||||
void parseArgs(int &argc, char **&argv);
|
||||
|
||||
/// Return map of all key-value pairs of strings listed after the
|
||||
/// configuration directive given by second parameter.
|
||||
std::map<std::string, std::string>
|
||||
parseKeyVal(const std::string &category = "user") const;
|
||||
|
||||
/// Store contents as a JSON object. Return false on failure.
|
||||
bool saveJsonToFile(const std::string &filename);
|
||||
|
||||
/// \brief Load key/value pairs from JSON object.
|
||||
///
|
||||
/// Values that are not strings will
|
||||
/// be ignored. Return empty object on failure.
|
||||
static TaskConfig loadJsonFromFile(const std::string &filename);
|
||||
|
||||
private:
|
||||
void _load(std::istream &cfg_stream);
|
||||
std::multimap<std::string, std::string> the_config;
|
||||
};
|
||||
|
||||
std::ostream &operator<<(std::ostream &out, const TaskConfig &tc);
|
||||
42
src/framework/threadbridge.cpp
Normal file
42
src/framework/threadbridge.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "threadbridge.h"
|
||||
#include <iostream>
|
||||
|
||||
ThreadBridge::ThreadBridge(Task *agent, double tick) :
|
||||
BridgeTask(agent),
|
||||
tick_len(tick) {
|
||||
}
|
||||
|
||||
double ThreadBridge::start() {
|
||||
BridgeTask::start();
|
||||
return tick_len;
|
||||
}
|
||||
|
||||
double ThreadBridge::timerEvent() {
|
||||
std::string msg;
|
||||
while (true) {
|
||||
method_queue.fetch(msg);
|
||||
if (msg.empty())
|
||||
return tick_len;
|
||||
sendMsgToAgent(msg);
|
||||
msg.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ThreadBridge::pushToAgent(const std::string &msg) {
|
||||
dbg_log() << "To agent: " << msg;
|
||||
method_queue.push(msg);
|
||||
}
|
||||
|
||||
std::string ThreadBridge::popFromAgent() {
|
||||
std::string msg;
|
||||
event_queue.fetch(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
void ThreadBridge::sendMsgToClient(const std::string &msg) {
|
||||
dbg_log() << "To client: " << msg;
|
||||
event_queue.push(msg);
|
||||
}
|
||||
37
src/framework/threadbridge.h
Normal file
37
src/framework/threadbridge.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "bridgetask.h"
|
||||
#include "msgqueue.h"
|
||||
|
||||
/// \brief
|
||||
/// Bridge using a thread safe queue to enable communication
|
||||
/// between agent and client.
|
||||
class ThreadBridge : public BridgeTask {
|
||||
public:
|
||||
ThreadBridge(Task *agent = nullptr, double tick=0.05);
|
||||
|
||||
/// API for client to send message to the agent.
|
||||
void pushToAgent(const std::string &msg)
|
||||
|
||||
/// \brief
|
||||
/// API for client to retrieve next message from agent.
|
||||
///
|
||||
/// If no messages are available, an empty string will be returned.
|
||||
/// Client shoud call this regularly.
|
||||
std::string popFromAgent();
|
||||
|
||||
/// Pass message to the client.
|
||||
void sendMsgToClient(const std::string &msg) override;
|
||||
|
||||
/// Initiate timer to be called after `tick` seconds.
|
||||
double start() override;
|
||||
|
||||
/// Push queued messages for the agent. Called every `tick` seconds.
|
||||
double timerEvent() override;
|
||||
private:
|
||||
MsgQueue<std::string> method_queue, event_queue;
|
||||
double tick_len;
|
||||
};
|
||||
42
src/framework/unixdomainbridge.cpp
Normal file
42
src/framework/unixdomainbridge.cpp
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "unixdomainbridge.h"
|
||||
#include "shortmessageconnection.h"
|
||||
|
||||
UnixDomainBridge::UnixDomainBridge(Task *agent) :
|
||||
BridgeTask(agent),
|
||||
msg_conn(new ShortMessageConnection("UDBridgeConn", this, "UnixDomain", 0)) {
|
||||
}
|
||||
|
||||
double UnixDomainBridge::start() {
|
||||
BridgeTask::start();
|
||||
addConnected(msg_conn);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void UnixDomainBridge::sendMsgToClient(const std::string &msg) {
|
||||
msg_conn->sendMessage(msg);
|
||||
}
|
||||
|
||||
int UnixDomainBridge::getClientSocket() const {
|
||||
if (!msg_conn)
|
||||
return 0;
|
||||
return msg_conn->getUnixDomainPeer();
|
||||
}
|
||||
|
||||
int UnixDomainBridge::getAgentSocket() const {
|
||||
if (!msg_conn || msg_conn->id() < 0)
|
||||
return 0;
|
||||
return msg_conn->id();
|
||||
}
|
||||
|
||||
PollState UnixDomainBridge::connectionReady(SocketConnection * /* conn */) {
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
PollState UnixDomainBridge::msgFromConnection(SocketConnection *,
|
||||
const std::string &msg) {
|
||||
sendMsgToAgent(msg);
|
||||
return PollState::READ;
|
||||
}
|
||||
49
src/framework/unixdomainbridge.h
Normal file
49
src/framework/unixdomainbridge.h
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "bridgetask.h"
|
||||
class ShortMessageConnection;
|
||||
|
||||
/// \brief
|
||||
/// Bridge using a pair of Unix domain sockets to enable communication
|
||||
/// between agent and client.
|
||||
class UnixDomainBridge : public BridgeTask {
|
||||
public:
|
||||
|
||||
/// \brief
|
||||
/// Create a bridge to the given agent task.
|
||||
///
|
||||
/// A pair of Unix domain sockets will be used to enable communication
|
||||
/// between agent and client.
|
||||
UnixDomainBridge(Task *agent = nullptr);
|
||||
|
||||
/// Pass message to the client.
|
||||
void sendMsgToClient(const std::string &msg) override;
|
||||
|
||||
/// \brief
|
||||
/// Get client's socket descriptor.
|
||||
///
|
||||
/// Return 0 on failure.
|
||||
///
|
||||
/// *Note:* client may run in another thread or process.
|
||||
/// Use in child, close in parent after fork.
|
||||
int getClientSocket() const;
|
||||
|
||||
/// Close in child after fork.
|
||||
int getAgentSocket() const;
|
||||
|
||||
/// See Task::connectionReady.
|
||||
PollState connectionReady(SocketConnection * /* conn */) override;
|
||||
|
||||
/// Will be called when client has sent a message.
|
||||
PollState msgFromConnection(SocketConnection * /* conn */,
|
||||
const std::string &msg) override;
|
||||
|
||||
/// See Task::start.
|
||||
double start() override;
|
||||
|
||||
private:
|
||||
ShortMessageConnection *msg_conn;
|
||||
};
|
||||
87
src/framework/unixdomainclient.cpp
Normal file
87
src/framework/unixdomainclient.cpp
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "unixdomainclient.h"
|
||||
#include "bridgetask.h"
|
||||
|
||||
void UnixDomainClient::pushToAgent(const std::string &msg) {
|
||||
to_agent += std::to_string(msg.size());
|
||||
to_agent += '\n';
|
||||
to_agent += msg;
|
||||
auto n = send(client_socket, to_agent.c_str(), to_agent.size(), 0);
|
||||
if (n > 0)
|
||||
to_agent.erase(0, n);
|
||||
}
|
||||
|
||||
std::string UnixDomainClient::pollAgent() {
|
||||
ssize_t n;
|
||||
while (true) {
|
||||
char buffer[50000];
|
||||
n = recv(client_socket, buffer, sizeof buffer, 0);
|
||||
if (n <= 0)
|
||||
break;
|
||||
to_client.append(buffer, n);
|
||||
// Maybe we should have a max size for a single message.
|
||||
}
|
||||
auto err = errno;
|
||||
|
||||
auto pos = to_client.find('\n');
|
||||
if (pos != std::string::npos) {
|
||||
// Check if a new (complete) message has arrived.
|
||||
try {
|
||||
auto msg_len = std::stoul(to_client.substr(0, pos));
|
||||
if (to_client.size() > pos + msg_len) {
|
||||
++pos;
|
||||
std::string result = to_client.substr(pos, msg_len);
|
||||
to_client.erase(0, pos+msg_len);
|
||||
return result;
|
||||
}
|
||||
} catch (...) {
|
||||
return BridgeTask::agentTerminatedMessage("bad data");
|
||||
}
|
||||
}
|
||||
|
||||
if (n == 0)
|
||||
return BridgeTask::agentTerminatedMessage("lost connection");
|
||||
if (err && err != EAGAIN && err != EWOULDBLOCK &&
|
||||
err != EINPROGRESS && err != EINTR) {
|
||||
return BridgeTask::agentTerminatedMessage("connection error");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool UnixDomainClient::flushToAgent() {
|
||||
if (to_agent.empty())
|
||||
return true;
|
||||
auto n = send(client_socket, to_agent.c_str(), to_agent.size(), 0);
|
||||
if (n > 0) {
|
||||
to_agent.erase(0, n);
|
||||
return to_agent.empty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string UnixDomainClient::waitForMsgFromAgent(unsigned long timeout_us) {
|
||||
|
||||
std::string msg = pollAgent();
|
||||
if (!msg.empty())
|
||||
return msg;
|
||||
|
||||
struct timeval timeout;
|
||||
timeout.tv_sec = timeout_us/1000000;
|
||||
timeout.tv_usec = timeout_us%1000000;
|
||||
|
||||
fd_set readFds, errFds;
|
||||
FD_ZERO(&readFds);
|
||||
FD_ZERO(&errFds);
|
||||
FD_SET(client_socket, &readFds);
|
||||
FD_SET(client_socket, &errFds);
|
||||
select(client_socket + 1, &readFds, nullptr, &errFds,
|
||||
timeout_us ? &timeout : nullptr);
|
||||
|
||||
return pollAgent();
|
||||
}
|
||||
62
src/framework/unixdomainclient.h
Normal file
62
src/framework/unixdomainclient.h
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/// \brief
|
||||
/// Client communicating with an agent task using a UnixDomainBridge.
|
||||
///
|
||||
/// This class is meant to be used only _outside_ the event loop, i.e. from some
|
||||
/// other thread or process. It is used together with a UnixDomainBridge, which
|
||||
/// runs in the event loop. After creating the UnixDomainBridge object, call
|
||||
/// getClientSocket() on it to get a file descriptor which should be used as
|
||||
/// an argument to the constructor of this class. Then make sure the
|
||||
/// UnixDomainBridge object runs in an event loop, but not in the same thread or
|
||||
/// process as this class.
|
||||
class UnixDomainClient {
|
||||
public:
|
||||
/// Create client using one of a pair of Unix domain sockets.
|
||||
UnixDomainClient(int peer_fd) :
|
||||
client_socket(peer_fd) {
|
||||
}
|
||||
|
||||
/// \brief
|
||||
/// Return a line of data from the agent.
|
||||
///
|
||||
/// Will return an empty string if no message is available.
|
||||
///
|
||||
/// Poll regularly or monitor the socket descriptor.
|
||||
std::string pollAgent();
|
||||
|
||||
/// As UnixDomainClient::pollAgent, but block until a message is available
|
||||
/// or until timeout_us
|
||||
/// microseconds have passed (or forever, if timeout_us is 0).
|
||||
std::string waitForMsgFromAgent(unsigned long timeout_us = 0);
|
||||
|
||||
/// \brief
|
||||
/// Send message to agent.
|
||||
///
|
||||
/// *Note:* If the message is large (perhaps >200KB),
|
||||
/// it's likely that the complete message can't be delivered to the agent
|
||||
/// immediately. In that case, it will be buffered and you may have to
|
||||
/// call pushToAgent (with new messages) or flushToAgent() repeatedly until
|
||||
/// the buffer is drained. (Of course, unless the agent actively reads the
|
||||
/// messages, the buffer can't be drained.)
|
||||
void pushToAgent(const std::string &msg);
|
||||
|
||||
/// \brief If there is unsent data, retry sending it to the agent.
|
||||
///
|
||||
/// Return true if agent has received all messages we sent.
|
||||
///
|
||||
/// If the return value is false,
|
||||
/// you must call flushToAgent again at a later time (e.g.
|
||||
/// after 50ms, or preferably when the socket is found to be writable.)
|
||||
bool flushToAgent();
|
||||
|
||||
private:
|
||||
std::string to_agent;
|
||||
std::string to_client;
|
||||
int client_socket;
|
||||
};
|
||||
52
src/framework/workerprocess.h
Normal file
52
src/framework/workerprocess.h
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
class SocketReceiver;
|
||||
|
||||
/// \brief
|
||||
/// Used by LoadBalancer to manage child processes.
|
||||
///
|
||||
/// One or more SocketReceiver objects may be used to pass sockets
|
||||
/// (and/or messages) between master and child processes.
|
||||
///
|
||||
/// Each SocketReceiver will be called _a channel_, and they will be
|
||||
/// referenced using integers starting with 0.
|
||||
///
|
||||
/// The point of using channels is to pass different types of connections
|
||||
/// (e.g. with or without SSL encryption) on different channels.
|
||||
class WorkerProcess {
|
||||
public:
|
||||
/// Create worker to run in newly forked process `pid`.
|
||||
WorkerProcess(pid_t pid, std::vector<SocketReceiver *> &receivers) :
|
||||
worker_pid(pid),
|
||||
channels(receivers) {
|
||||
}
|
||||
|
||||
~WorkerProcess() {
|
||||
for (auto &conn : channels)
|
||||
conn->peerDead();
|
||||
}
|
||||
|
||||
/// Return the PID of the worker process.
|
||||
pid_t pid() const {
|
||||
return worker_pid;
|
||||
}
|
||||
|
||||
/// Return a channel.
|
||||
SocketReceiver *channel(unsigned int n=0) const {
|
||||
return channels.at(n);
|
||||
}
|
||||
|
||||
/// Return number of channels.
|
||||
size_t noChannels() const {
|
||||
return channels.size();
|
||||
}
|
||||
|
||||
private:
|
||||
pid_t worker_pid;
|
||||
std::vector<SocketReceiver *> channels;
|
||||
};
|
||||
1
src/gtkgui/.gitignore
vendored
Normal file
1
src/gtkgui/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
bredbandskollen
|
||||
26
src/gtkgui/Makefile
Normal file
26
src/gtkgui/Makefile
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
TARGET = bredbandskollen
|
||||
DIRLEVEL = ..
|
||||
|
||||
# Possible LOGLEVEL values: dbg, info, warn, err, none
|
||||
LOGLEVEL=info
|
||||
|
||||
# We will use Logger in more than one thread:
|
||||
THREADS=1
|
||||
|
||||
# Uncomment if GnuTLS version 3.5 or later is available
|
||||
# GNUTLS=1
|
||||
|
||||
SOURCES=../http/cookiefile.cpp \
|
||||
../framework/unixdomainbridge.cpp \
|
||||
../framework/unixdomainclient.cpp \
|
||||
../framework/shortmessageconnection.cpp \
|
||||
main.cpp \
|
||||
gtkclient.cpp \
|
||||
../cli/utils.cpp
|
||||
|
||||
CXXFLAGS=$(shell pkg-config --cflags gtk+-3.0)
|
||||
LIBS=$(shell pkg-config --libs gtk+-3.0)
|
||||
|
||||
#CXXFLAGS += -g
|
||||
|
||||
include $(DIRLEVEL)/measurement/mk.inc
|
||||
407
src/gtkgui/gtkclient.cpp
Normal file
407
src/gtkgui/gtkclient.cpp
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include "gtkclient.h"
|
||||
#include <glib-unix.h>
|
||||
|
||||
void GtkClient::run() {
|
||||
app = gtk_application_new("bbk.iis.se", G_APPLICATION_DEFAULT_FLAGS);
|
||||
g_signal_connect(app, "activate",
|
||||
G_CALLBACK(GtkClient::activate), this);
|
||||
g_application_run(G_APPLICATION(app), 0, nullptr);
|
||||
g_object_unref(app);
|
||||
}
|
||||
|
||||
void GtkClient::newEventFromAgent(const std::string &msg) {
|
||||
log() << "Got: " << msg;
|
||||
|
||||
std::string jsonerr;
|
||||
auto obj = json11::Json::parse(msg, jsonerr);
|
||||
if (!jsonerr.empty()) {
|
||||
if (BridgeTask::isAgentTerminatedMessage(msg))
|
||||
setLabel(label_message, msg);
|
||||
else {
|
||||
setLabel(label_message, "JSON error: got " + msg);
|
||||
err_log() << "JSON error";
|
||||
}
|
||||
pushToAgent("terminate");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string event = obj["event"].string_value();
|
||||
auto arg_obj = obj["args"];
|
||||
|
||||
while (state == MState::MEASURING) {
|
||||
if (event == "taskProgress") {
|
||||
std::string tst = arg_obj["task"].string_value();
|
||||
double val = arg_obj["result"].number_value();
|
||||
std::string p = myStrFormat(val) + " Mbit/s";
|
||||
if (tst == "download")
|
||||
setLabel(label_download, p);
|
||||
else if (tst == "upload" || tst == "uploadinfo")
|
||||
setLabel(label_upload, p);
|
||||
} else if (event == "taskStart") {
|
||||
} else if (event == "taskComplete") {
|
||||
gotTaskComplete(arg_obj);
|
||||
} else if (event == "report") {
|
||||
gotReport(arg_obj);
|
||||
} else if (event == "measurementInfo") {
|
||||
std::string id = arg_obj["MeasurementID"].string_value();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (event == "configuration") {
|
||||
gotSettings(arg_obj);
|
||||
} else if (event == "agentReady") {
|
||||
if (state == MState::IDLE)
|
||||
pushToAgent("getConfiguration");
|
||||
else if (state == MState::RESTARTING)
|
||||
doStartMeasurement();
|
||||
} else if (event == "measurementList") {
|
||||
} else if (event == "setInfo") {
|
||||
gotInfo(arg_obj);
|
||||
}
|
||||
}
|
||||
|
||||
void GtkClient::reset() {
|
||||
pushToAgent("resetTest");
|
||||
setLabel(label_message, "");
|
||||
got_ticket = false;
|
||||
user_abort = false;
|
||||
setLabel(label_ticket, "");
|
||||
setLabel(label_latency, "");
|
||||
setLabel(label_download, "");
|
||||
setLabel(label_upload, "");
|
||||
setLabel(label_evaluation, "");
|
||||
}
|
||||
|
||||
std::string GtkClient::myStrFormat(double x) {
|
||||
char strBuf[80];
|
||||
snprintf(strBuf, sizeof(strBuf), "%7.2f", x);
|
||||
return std::string(strBuf);
|
||||
}
|
||||
|
||||
void GtkClient::gotReport(const json11::Json &obj) {
|
||||
setLabel(label_pip, obj["localip"].string_value());
|
||||
}
|
||||
|
||||
void GtkClient::gotTaskComplete(const json11::Json &obj) {
|
||||
std::string tst = obj["task"].string_value();
|
||||
auto res = obj["result"].number_value();
|
||||
if (tst == "global") {
|
||||
if (got_ticket || user_abort) {
|
||||
setState(MState::FINISHED);
|
||||
} else {
|
||||
setState(MState::ERROR);
|
||||
pushToAgent("resetTest");
|
||||
}
|
||||
} else if (tst == "latency")
|
||||
setLabel(label_latency, myStrFormat(res) + " ms");
|
||||
else if (tst == "download")
|
||||
setLabel(label_download, myStrFormat(res) + " Mbit/s");
|
||||
else if (tst == "upload")
|
||||
setLabel(label_upload, myStrFormat(res) + " Mbit/s");
|
||||
}
|
||||
|
||||
void GtkClient::gotInfo(const json11::Json &obj) {
|
||||
for (auto &p : obj.object_items()) {
|
||||
std::string attr = p.first;
|
||||
std::string value = p.second.string_value();
|
||||
if (attr == "error") {
|
||||
if (!value.empty())
|
||||
setLabel(label_message, "Error: " + value);
|
||||
} else if (attr == "ticket") {
|
||||
setLabel(label_ticket, value);
|
||||
got_ticket = true;
|
||||
} else if (attr == "logText") {
|
||||
} else if (attr == "msgToUser") {
|
||||
setLabel(label_message, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GtkClient::gotSettings(const json11::Json &obj) {
|
||||
if (state == MState::ERROR) {
|
||||
setState(MState::IDLE);
|
||||
}
|
||||
settings = obj;
|
||||
setLabel(label_date, dateString());
|
||||
setLabel(label_isp, settings["ispname"].string_value());
|
||||
setLabel(label_ip, settings["ip"].string_value());
|
||||
updateServerBox();
|
||||
}
|
||||
|
||||
void GtkClient::setLabel(GtkWidget *widget, const std::string &label) {
|
||||
gtk_label_set_text(GTK_LABEL(widget), label.c_str());
|
||||
}
|
||||
|
||||
void GtkClient::updateServerBox() {
|
||||
|
||||
gtk_combo_box_text_remove_all(GTK_COMBO_BOX_TEXT(server_box));
|
||||
|
||||
std::set<std::string> srvName;
|
||||
std::string mtype(gtk_combo_box_text_get_active_text
|
||||
(GTK_COMBO_BOX_TEXT(iptype_box)));
|
||||
bool tls = false;
|
||||
for (auto srv : settings["servers"].array_items()) {
|
||||
if (srv["type"].string_value() != mtype)
|
||||
continue;
|
||||
if (tls && srv["tlsport"].int_value() == 0)
|
||||
continue;
|
||||
std::string name = srv["name"].string_value();
|
||||
if (srvName.find(name) != srvName.end())
|
||||
continue;
|
||||
srvName.insert(name);
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_box),
|
||||
name.c_str());
|
||||
}
|
||||
|
||||
if (srvName.empty())
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(server_box),
|
||||
"No Server");
|
||||
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(server_box), 0);
|
||||
}
|
||||
|
||||
gboolean GtkClient::poll_agent(gint , GIOCondition c, gpointer data) {
|
||||
auto self = reinterpret_cast<GtkClient *>(data);
|
||||
if (c & G_IO_HUP) {
|
||||
// Agent gone
|
||||
self->doQuit();
|
||||
return FALSE;
|
||||
}
|
||||
while (true) {
|
||||
auto msg = self->pollAgent();
|
||||
if (msg.empty())
|
||||
break;
|
||||
self->newEventFromAgent(msg);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GtkClient::GtkClient(const TaskConfig &config, int unix_domain_peer) :
|
||||
Logger("GUI"),
|
||||
peer_fd(unix_domain_peer),
|
||||
ud_client(peer_fd),
|
||||
the_config(config) {
|
||||
pushToAgent("clientReady");
|
||||
}
|
||||
|
||||
GtkClient::~GtkClient() {
|
||||
pushToAgent("terminate");
|
||||
}
|
||||
|
||||
void GtkClient::start_measurement(GtkWidget *, gpointer data) {
|
||||
auto self = reinterpret_cast<GtkClient *>(data);
|
||||
self->doStartMeasurement();
|
||||
}
|
||||
|
||||
void GtkClient::update_serverlist(GtkWidget *, gpointer data) {
|
||||
auto self = reinterpret_cast<GtkClient *>(data);
|
||||
self->updateServerBox();
|
||||
}
|
||||
|
||||
void GtkClient::doStartMeasurement() {
|
||||
if (state == MState::MEASURING) {
|
||||
pushToAgent("abortTest");
|
||||
user_abort = true;
|
||||
return;
|
||||
} else if (state == MState::FINISHED) {
|
||||
setState(MState::RESTARTING);
|
||||
return;
|
||||
} else if (state == MState::ERROR) {
|
||||
pushToAgent("getConfiguration");
|
||||
return;
|
||||
}
|
||||
setLabel(label_date, dateString());
|
||||
|
||||
setState(MState::MEASURING);
|
||||
std::string mtype(gtk_combo_box_text_get_active_text
|
||||
(GTK_COMBO_BOX_TEXT(iptype_box)));
|
||||
bool tls = false;
|
||||
|
||||
std::string server_name(gtk_combo_box_text_get_active_text
|
||||
(GTK_COMBO_BOX_TEXT(server_box)));
|
||||
|
||||
int server_port = 80;
|
||||
std::string hostname;
|
||||
std::set<std::string> srvName;
|
||||
for (auto srv : settings["servers"].array_items()) {
|
||||
if (srv["type"].string_value() != mtype)
|
||||
continue;
|
||||
if ( srv["name"].string_value() != server_name)
|
||||
continue;
|
||||
if (tls && srv["tlsport"].int_value() == 0)
|
||||
continue;
|
||||
hostname = srv["url"].string_value();
|
||||
auto pos = hostname.find(':');
|
||||
if (pos != std::string::npos) {
|
||||
server_port = std::stoi(hostname.substr(pos+1));
|
||||
hostname.resize(pos);
|
||||
}
|
||||
if (tls)
|
||||
server_port = srv["tlsport"].int_value();
|
||||
break;
|
||||
}
|
||||
|
||||
std::string key = settings["hashkey"].string_value();
|
||||
json11::Json out_args = json11::Json::object {
|
||||
{ "serverUrl", hostname },
|
||||
{ "serverPort", server_port },
|
||||
{ "userKey", key },
|
||||
{ "tls", tls },
|
||||
};
|
||||
pushToAgent("startTest", out_args.dump());
|
||||
}
|
||||
|
||||
void GtkClient::setState(MState newState) {
|
||||
|
||||
if (state == MState::FINISHED) {
|
||||
reset();
|
||||
}
|
||||
|
||||
switch (newState) {
|
||||
case MState::IDLE:
|
||||
setLabel(label_message, "");
|
||||
gtk_button_set_label(GTK_BUTTON(start_button), "Start Measurement");
|
||||
break;
|
||||
case MState::RESTARTING:
|
||||
case MState::MEASURING:
|
||||
gtk_button_set_label(GTK_BUTTON(start_button), "ABORT");
|
||||
break;
|
||||
case MState::FINISHED:
|
||||
gtk_button_set_label(GTK_BUTTON(start_button), "New Measurement");
|
||||
break;
|
||||
case MState::ERROR:
|
||||
gtk_button_set_label(GTK_BUTTON(start_button), "RETRY");
|
||||
break;
|
||||
}
|
||||
|
||||
state = newState;
|
||||
}
|
||||
|
||||
void GtkClient::activate(GtkApplication *app,
|
||||
gpointer user_data) {
|
||||
auto self = reinterpret_cast<GtkClient *>(user_data);
|
||||
self->doActivate(app);
|
||||
}
|
||||
|
||||
GtkWidget *GtkClient::staticLabel(const char *text, GtkAlign align) {
|
||||
GtkWidget *label = gtk_label_new(text);
|
||||
gtk_widget_set_halign(label, align);
|
||||
gtk_widget_set_name(label, "static-label");
|
||||
return label;
|
||||
}
|
||||
|
||||
GtkWidget *GtkClient::dynamicLabel(GtkAlign align) {
|
||||
GtkWidget *label = gtk_label_new("");
|
||||
gtk_widget_set_halign(label, align);
|
||||
gtk_widget_set_name(label, "dynamic-label");
|
||||
return label;
|
||||
}
|
||||
|
||||
void GtkClient::doActivate(GtkApplication *app) {
|
||||
main_window = gtk_application_window_new(app);
|
||||
gtk_window_set_title(GTK_WINDOW(main_window), "Bandwidth Measurement");
|
||||
gtk_window_set_default_size(GTK_WINDOW(main_window), 700, 300);
|
||||
|
||||
GdkDisplay *display = gdk_display_get_default();
|
||||
GdkScreen *screen = gdk_display_get_default_screen(display);
|
||||
GtkCssProvider *cssProvider = gtk_css_provider_new();
|
||||
if (gtk_css_provider_load_from_path(cssProvider, "gtkclient.css", nullptr))
|
||||
gtk_style_context_add_provider_for_screen(screen,
|
||||
GTK_STYLE_PROVIDER(cssProvider),
|
||||
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
||||
|
||||
GtkWidget *mw = gtk_grid_new();
|
||||
gtk_widget_set_hexpand(mw, TRUE);
|
||||
gtk_grid_set_column_homogeneous(GTK_GRID(mw), TRUE);
|
||||
gtk_container_add(GTK_CONTAINER(main_window), mw);
|
||||
|
||||
gtk_grid_set_row_spacing(GTK_GRID(mw), 10);
|
||||
gtk_grid_set_column_spacing(GTK_GRID(mw), 2);
|
||||
|
||||
auto label_ticket1 = staticLabel("Suport ID: ");
|
||||
gtk_widget_set_halign(label_ticket1, GTK_ALIGN_END);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_ticket1, 0, 0, 1, 1);
|
||||
label_ticket = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_ticket, 1, 0, 1, 1);
|
||||
label_date = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_date, 2, 0, 2, 1);
|
||||
|
||||
auto label_isp1 = staticLabel("ISP: ");
|
||||
gtk_grid_attach(GTK_GRID(mw), label_isp1, 0, 1, 1, 1);
|
||||
label_isp = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_isp, 1, 1, 3, 1);
|
||||
|
||||
GtkWidget *label_ip1 = staticLabel("IP: ");
|
||||
gtk_grid_attach(GTK_GRID(mw), label_ip1, 0, 2, 1, 1);
|
||||
label_ip = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_ip, 1, 2, 1, 1);
|
||||
|
||||
GtkWidget *label_ip2 = staticLabel("Local IP: ");
|
||||
gtk_grid_attach(GTK_GRID(mw), label_ip2, 2, 2, 1, 1);
|
||||
label_pip = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_pip, 3, 2, 1, 1);
|
||||
|
||||
int res_line = 3;
|
||||
|
||||
label_message = dynamicLabel(GTK_ALIGN_CENTER);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_message, 0, res_line, 4, 1);
|
||||
|
||||
++res_line;
|
||||
|
||||
GtkWidget *label_x2 = staticLabel("Latency: ");
|
||||
label_latency = dynamicLabel();
|
||||
GtkWidget *label_x3 = staticLabel("Evaluation: ");
|
||||
label_evaluation = dynamicLabel();
|
||||
|
||||
gtk_grid_attach(GTK_GRID(mw), label_x2, 0, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_latency, 1, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_x3, 2, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_evaluation, 3, res_line, 1, 1);
|
||||
|
||||
++res_line;
|
||||
|
||||
GtkWidget *label_x0 = staticLabel("Download: ");
|
||||
label_download = dynamicLabel();
|
||||
GtkWidget *label_x1 = staticLabel("Upload: ");
|
||||
label_upload = dynamicLabel();
|
||||
gtk_grid_attach(GTK_GRID(mw), label_x0, 0, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_download, 1, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_x1, 2, res_line, 1, 1);
|
||||
gtk_grid_attach(GTK_GRID(mw), label_upload, 3, res_line, 1, 1);
|
||||
|
||||
++res_line;
|
||||
|
||||
start_button = gtk_button_new_with_label("Start Measurement");
|
||||
gtk_grid_attach(GTK_GRID(mw), start_button, 1, res_line+4, 2, 1);
|
||||
g_signal_connect(start_button, "clicked",
|
||||
G_CALLBACK(start_measurement), this);
|
||||
|
||||
GtkWidget *label_server1 = staticLabel("Measurement server: ");
|
||||
gtk_grid_attach(GTK_GRID(mw), label_server1, 0, res_line+5, 1, 1);
|
||||
server_box = gtk_combo_box_text_new();
|
||||
gtk_grid_attach(GTK_GRID(mw), server_box, 1, res_line+5, 1, 1);
|
||||
|
||||
iptype_box = gtk_combo_box_text_new();
|
||||
gtk_grid_attach(GTK_GRID(mw), iptype_box, 2, res_line+5, 1, 1);
|
||||
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(iptype_box),
|
||||
"ipv4");
|
||||
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(iptype_box),
|
||||
"ipv6");
|
||||
gtk_combo_box_set_active(GTK_COMBO_BOX(iptype_box), 0);
|
||||
g_signal_connect(iptype_box, "changed",
|
||||
G_CALLBACK(update_serverlist), this);
|
||||
|
||||
auto cond = static_cast<GIOCondition>(G_IO_IN | G_IO_HUP | G_IO_ERR);
|
||||
g_unix_fd_add(peer_fd, cond, poll_agent, this);
|
||||
|
||||
gtk_widget_show_all(main_window);
|
||||
}
|
||||
11
src/gtkgui/gtkclient.css
Normal file
11
src/gtkgui/gtkclient.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
window {
|
||||
}
|
||||
|
||||
#dynamic-label {
|
||||
background-color: #ffffff;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#static-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
73
src/gtkgui/gtkclient.h
Normal file
73
src/gtkgui/gtkclient.h
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include "../json11/json11.hpp"
|
||||
#include "../framework/unixdomainclient.h"
|
||||
#include "../framework/bridgetask.h"
|
||||
#include "../framework/logger.h"
|
||||
|
||||
class GtkClient : public Logger {
|
||||
public:
|
||||
GtkClient(const TaskConfig &config, int unix_domain_peer);
|
||||
~GtkClient();
|
||||
|
||||
void run();
|
||||
void doQuit() {
|
||||
pushToAgent("terminate");
|
||||
g_application_quit(G_APPLICATION(app));
|
||||
}
|
||||
static void activate(GtkApplication *app, gpointer user_data);
|
||||
static gboolean poll_agent(gint fd, GIOCondition c, gpointer data);
|
||||
static void start_measurement(GtkWidget *widget, gpointer data);
|
||||
static void update_serverlist(GtkWidget *widget, gpointer data);
|
||||
|
||||
void doActivate(GtkApplication *app);
|
||||
void doStartMeasurement();
|
||||
|
||||
GtkWidget *staticLabel(const char *label, GtkAlign align = GTK_ALIGN_END);
|
||||
GtkWidget *dynamicLabel(GtkAlign align = GTK_ALIGN_START);
|
||||
void setLabel(GtkWidget *widget, const std::string &label);
|
||||
void pushToAgent(const std::string &method, const std::string &arg = "{}") {
|
||||
if (arg.size() > 2)
|
||||
log() << "Send: " << method << " " << arg;
|
||||
else
|
||||
log() << "Send: " << method;
|
||||
ud_client.pushToAgent(BridgeTask::msgToAgent(method, arg));
|
||||
}
|
||||
|
||||
std::string myStrFormat(double x);
|
||||
void gotSettings(const json11::Json &obj);
|
||||
void gotInfo(const json11::Json &obj);
|
||||
void gotReport(const json11::Json &obj);
|
||||
void gotTaskComplete(const json11::Json &obj);
|
||||
void newEventFromAgent(const std::string &msg);
|
||||
std::string pollAgent() {
|
||||
return ud_client.pollAgent();
|
||||
}
|
||||
private:
|
||||
enum class MState {
|
||||
IDLE, RESTARTING, MEASURING, FINISHED, ERROR
|
||||
};
|
||||
void setState(MState newState);
|
||||
void updateServerBox();
|
||||
void reset();
|
||||
MState state = MState::IDLE;
|
||||
bool got_ticket = false;
|
||||
bool user_abort = false;
|
||||
GtkApplication *app;
|
||||
GtkWidget *main_window;
|
||||
GtkWidget *start_button;
|
||||
GtkWidget *label_isp, *label_ticket, *label_date,
|
||||
*label_ip, *label_pip, *label_id,
|
||||
*label_download, *label_upload, *label_latency,
|
||||
*label_ipsmsg, *label_evaluation, *label_message;
|
||||
GtkWidget *server_box, *iptype_box;
|
||||
int peer_fd;
|
||||
UnixDomainClient ud_client;
|
||||
json11::Json settings;
|
||||
const TaskConfig the_config;
|
||||
};
|
||||
57
src/gtkgui/main.cpp
Normal file
57
src/gtkgui/main.cpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "gtkclient.h"
|
||||
|
||||
#include "../measurement/measurementagent.h"
|
||||
#include "../cli/utils.h"
|
||||
#include "../framework/unixdomainbridge.h"
|
||||
|
||||
namespace {
|
||||
void runAgent(BridgeTask *bridge, const TaskConfig &cfg,
|
||||
const std::string &cfgfile, const std::string &logfile) {
|
||||
std::ofstream log_file;
|
||||
if (logfile != "-") {
|
||||
log_file.open(logfile);
|
||||
Logger::setLogFile(log_file);
|
||||
}
|
||||
CookieFile cf(cfgfile);
|
||||
HttpHost webserver(cfg.value("Measure.Webserver"), 80, "", 0, &cf);
|
||||
EventLoop loop;
|
||||
bridge->setAgent(new MeasurementAgent(cfg, webserver));
|
||||
loop.addTask(bridge);
|
||||
loop.runUntilComplete();
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
|
||||
TaskConfig agent_cfg, config;
|
||||
if (!parseArgs(argc, argv, config, agent_cfg))
|
||||
return 1;
|
||||
agent_cfg.set("Measure.AutoSaveReport", "true");
|
||||
|
||||
std::ofstream log_file;
|
||||
if (config.value("logfile") != "-") {
|
||||
log_file.open(config.value("app_dir") + "gtk_last_log");
|
||||
Logger::setLogFile(log_file);
|
||||
}
|
||||
|
||||
UnixDomainBridge *bridge = new UnixDomainBridge();
|
||||
int client_socket = bridge->getClientSocket();
|
||||
if (!client_socket)
|
||||
exit(1);
|
||||
|
||||
std::thread agent_thread(runAgent, bridge, agent_cfg,
|
||||
config.value("config_file"), config.value("logfile"));
|
||||
|
||||
{
|
||||
GtkClient client(config, client_socket);
|
||||
client.run();
|
||||
}
|
||||
|
||||
close(client_socket);
|
||||
agent_thread.join();
|
||||
|
||||
return 0;
|
||||
}
|
||||
98
src/http/cookiefile.cpp
Normal file
98
src/http/cookiefile.cpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
#include "cookiefile.h"
|
||||
#include "../http/http_common.h"
|
||||
|
||||
CookieFile::~CookieFile() {
|
||||
if (isDirty())
|
||||
writeCookiesFile();
|
||||
}
|
||||
|
||||
void CookieFile::readCookiesFile() {
|
||||
if (filename.empty())
|
||||
return;
|
||||
std::ifstream cookieFile(filename);
|
||||
if (!cookieFile) {
|
||||
// Failed, but who cares?
|
||||
log() << "cannot read cookie file " << filename;
|
||||
return;
|
||||
}
|
||||
|
||||
log() << "reading cookie file " << filename;
|
||||
std::string domain;
|
||||
std::string line;
|
||||
auto it = cookies().end();
|
||||
while (std::getline(cookieFile, line)) {
|
||||
if (line.size() < 10) {
|
||||
continue;
|
||||
} else if (line[0] == '*') {
|
||||
if (line.substr(0, 5) == "**** " &&
|
||||
line.substr(line.size()-5) == " ****") {
|
||||
domain = line.substr(5, line.size()-10);
|
||||
it = cookies().find(domain);
|
||||
} else {
|
||||
err_log() << "bad cookie file";
|
||||
domain.clear();
|
||||
}
|
||||
continue;
|
||||
} else if (domain.empty()) {
|
||||
continue;
|
||||
} else {
|
||||
// name, value, domain, expires, path, secure, httponly
|
||||
std::vector<std::string> vec = HttpCommon::split(line, ";");
|
||||
if (vec.size() != Field::field_len)
|
||||
continue;
|
||||
if (it == cookies().end()) {
|
||||
std::vector<std::string> v(Cache::cache_len);
|
||||
auto p = cookies().insert(std::make_pair(domain, v));
|
||||
it = p.first;
|
||||
}
|
||||
auto &v = it->second;
|
||||
std::move(vec.begin(), vec.end(), back_inserter(v));
|
||||
}
|
||||
}
|
||||
|
||||
clearDirty();
|
||||
}
|
||||
|
||||
void CookieFile::writeCookiesFile() {
|
||||
if (filename.empty()) {
|
||||
log() << "missing name of cookie file";
|
||||
return;
|
||||
}
|
||||
|
||||
std::ofstream cookieFile(filename, std::ofstream::trunc);
|
||||
for (auto p : cookies()) {
|
||||
const std::string &domain = p.first;
|
||||
const std::vector<std::string> &v = p.second;
|
||||
bool said_domain = false;
|
||||
std::size_t i=Cache::cache_len;
|
||||
while (i+Field::field_len <= v.size()) {
|
||||
if (v[i+Field::expires].empty() ||
|
||||
isExpired(v[i+Field::expires])) {
|
||||
// Session cookie or expired, don't persist.
|
||||
i += Field::field_len;
|
||||
continue;
|
||||
} else if (!said_domain) {
|
||||
cookieFile << "**** " << domain << " ****\n";
|
||||
said_domain = true;
|
||||
}
|
||||
cookieFile << v[i++];
|
||||
for (std::size_t j=1; j<Field::field_len; ++j)
|
||||
cookieFile << ';' << v[i++];
|
||||
cookieFile << '\n';
|
||||
}
|
||||
}
|
||||
cookieFile.close();
|
||||
|
||||
if (cookieFile) {
|
||||
clearDirty();
|
||||
} else {
|
||||
// Failed, but who cares?
|
||||
log() << "cannot write cookie file";
|
||||
}
|
||||
}
|
||||
37
src/http/cookiefile.h
Normal file
37
src/http/cookiefile.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "../http/cookiemanager.h"
|
||||
|
||||
class CookieFile : public CookieManager {
|
||||
public:
|
||||
CookieFile(const std::string &cookie_filename = "") :
|
||||
CookieManager("CookieFile"),
|
||||
filename(cookie_filename) {
|
||||
readCookiesFile();
|
||||
}
|
||||
|
||||
// By default, we try to save cookies in the destructor. However, if
|
||||
// the save operation fails, all updates are lost. If you can't afford to
|
||||
// lose cookies, call save() and check the return value.
|
||||
~CookieFile() override;
|
||||
|
||||
// Write to disk, return false on failure.
|
||||
bool save() override {
|
||||
if (isDirty())
|
||||
writeCookiesFile();
|
||||
return isDirty();
|
||||
}
|
||||
|
||||
// Default move constructor is OK despite us having a destructor:
|
||||
CookieFile(CookieFile &&) = default;
|
||||
|
||||
private:
|
||||
void readCookiesFile();
|
||||
void writeCookiesFile();
|
||||
std::string filename;
|
||||
};
|
||||
288
src/http/cookiemanager.cpp
Normal file
288
src/http/cookiemanager.cpp
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "cookiemanager.h"
|
||||
#include "http_common.h"
|
||||
#include <algorithm>
|
||||
#include <stdexcept>
|
||||
#include <iterator>
|
||||
|
||||
CookieManager::~CookieManager() {
|
||||
}
|
||||
|
||||
bool CookieManager::save() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// timeval contains only digits 0-9, doesn't start with 0.
|
||||
bool CookieManager::isExpired(const std::string &timeval) {
|
||||
if (timeval.empty())
|
||||
return false; // No expiry - a session cookie.
|
||||
|
||||
if (time_t d = time(nullptr) - t_now) {
|
||||
t_now += d;
|
||||
s_now = std::to_string(t_now);
|
||||
}
|
||||
|
||||
return !less(s_now, timeval);
|
||||
}
|
||||
|
||||
// line is the remainder of a header line starting with "Set-Cookie: ", e.g.
|
||||
// previous_isp=23; expires=Wed, 07-Jun-2017 11:34:59 GMT; path=/;
|
||||
// domain=.bredbandskollen.se
|
||||
void CookieManager::setCookie(const std::string &line,
|
||||
std::string domain, std::string uri) {
|
||||
std::vector<std::string> cookie_av = cookieSplit(line);
|
||||
if (cookie_av.empty())
|
||||
return;
|
||||
|
||||
const std::string &cdomain = cookie_av[Field::domain];
|
||||
// Disallow third-party cookies and cookies for top-level domains.
|
||||
// TODO: should disallow all public suffixes, e.g. ".co.uk".
|
||||
if (cdomain.find('.') != std::string::npos &&
|
||||
HttpCommon::isSubdomain(domain, cdomain))
|
||||
domain = cdomain;
|
||||
auto it = store.find(domain);
|
||||
if (it == store.end()) {
|
||||
std::vector<std::string> v(Cache::cache_len);
|
||||
auto p = store.insert(std::make_pair(domain, v));
|
||||
it = p.first;
|
||||
} else {
|
||||
// Invalidate cache, also for subdomains.
|
||||
it->second[Cache::cookie_header].clear();
|
||||
for (auto p : store)
|
||||
if (HttpCommon::isSubdomain(p.first, domain))
|
||||
p.second[Cache::cookie_header].clear();
|
||||
}
|
||||
|
||||
auto &v = it->second;
|
||||
bool expired = isExpired(cookie_av[Field::expires]);
|
||||
if (!expired)
|
||||
if (cookie_av[Field::path].empty() || cookie_av[Field::path][0] != '/')
|
||||
cookie_av[Field::path] = HttpCommon::uriPath(uri); // default-path
|
||||
|
||||
// Check if the new cookie replaces an existing
|
||||
for (size_t i=Cache::cache_len; i<v.size(); i+=Field::field_len) {
|
||||
if (v[i+Field::name] == cookie_av[Field::name] &&
|
||||
v[i+Field::path] == cookie_av[Field::path]) {
|
||||
if (expired) {
|
||||
v.erase(v.begin() + static_cast<long>(i),
|
||||
v.begin() + static_cast<long>(i+Field::field_len));
|
||||
} else {
|
||||
std::move(cookie_av.begin(), cookie_av.end(),
|
||||
v.begin() + static_cast<long>(i));
|
||||
dirty = true;
|
||||
cookie_av.clear();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!expired) {
|
||||
std::move(cookie_av.begin(), cookie_av.end(),
|
||||
back_inserter(v));
|
||||
dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
// line is the remainder of a header line starting with "Set-Cookie: ", e.g.
|
||||
// previous_isp=23; expires=Wed, 07-Jun-2017 11:34:59 GMT; path=/;
|
||||
// domain=.bredbandskollen.se
|
||||
|
||||
// Return vector with Field::field_len elements if OK:
|
||||
// name, value, domain, expires, path, secure, httponly
|
||||
// Return empty vector if bad.
|
||||
std::vector<std::string> CookieManager::cookieSplit(const std::string &line) {
|
||||
std::vector<std::string> v = HttpCommon::split(line, "; "), cookie_av;
|
||||
for (auto av : v) {
|
||||
std::string lcname, value, avlc;
|
||||
auto eqpos = av.find('=');
|
||||
if (!cookie_av.empty()) {
|
||||
// Make a lower-case copy:
|
||||
std::transform(av.begin(), av.end(), back_inserter(avlc), tolower);
|
||||
HttpCommon::trimWSP(lcname = avlc.substr(0, eqpos));
|
||||
if (eqpos != std::string::npos)
|
||||
HttpCommon::trimWSP(value = av.substr(eqpos+1));
|
||||
} else {
|
||||
if (eqpos == std::string::npos)
|
||||
break; // Bad line
|
||||
}
|
||||
|
||||
if (cookie_av.empty()) {
|
||||
cookie_av.resize(Field::field_len);
|
||||
HttpCommon::trimWSP(cookie_av[Field::name] = av.substr(0, eqpos));
|
||||
HttpCommon::trimWSP(cookie_av[Field::value] = av.substr(eqpos+1));
|
||||
} else if (eqpos == 6 && lcname == "domain") {
|
||||
++eqpos;
|
||||
if (av[eqpos] == '.')
|
||||
++eqpos; // Ignore leading dot.
|
||||
cookie_av[Field::domain] = avlc.substr(eqpos);
|
||||
} else if (eqpos == 8 && lcname == "_expires") {
|
||||
// Our internal format, unix timestamp value.
|
||||
++eqpos;
|
||||
if (av.find_first_not_of("0123456789", eqpos) == std::string::npos)
|
||||
cookie_av[Field::expires] = av.substr(eqpos);
|
||||
else // Corrupt cookie file?
|
||||
cookie_av[Field::expires] = "0";
|
||||
} else if (eqpos == 7 && lcname == "expires") {
|
||||
// max-age has precedence, so ignore if already set.
|
||||
if (cookie_av[Field::expires].empty()) {
|
||||
time_t val = HttpCommon::parseDateRfc1123(av.substr(eqpos+1));
|
||||
if (val >= 0) {
|
||||
cookie_av[Field::expires] = std::to_string(val);
|
||||
} else if (val == -1) {
|
||||
// Ignore bad date
|
||||
} else {
|
||||
// Expired.
|
||||
cookie_av[Field::expires] = "0";
|
||||
}
|
||||
}
|
||||
} else if (eqpos == 4 && lcname == "path") {
|
||||
cookie_av[Field::path] = av.substr(eqpos+1);
|
||||
} else if (eqpos == 7 && lcname == "max-age") {
|
||||
++eqpos;
|
||||
if (av.size() == eqpos) {
|
||||
// Ignore?
|
||||
} else if (av[eqpos] == '-') {
|
||||
if (av.find_first_not_of("0123456789", ++eqpos)
|
||||
== std::string::npos)
|
||||
cookie_av[Field::expires] = "0"; // Mark as expired
|
||||
// Else ignore!
|
||||
} else if (av.find_first_not_of("0123456789", eqpos) ==
|
||||
std::string::npos) {
|
||||
time_t res, maxv = std::numeric_limits<time_t>::max();
|
||||
try {
|
||||
long secs = std::stol(av.substr(eqpos));
|
||||
res = time(nullptr);
|
||||
if (secs >= maxv-res)
|
||||
res = maxv;
|
||||
else
|
||||
res += secs;
|
||||
} catch (std::out_of_range &) {
|
||||
// Too large
|
||||
res = maxv;
|
||||
}
|
||||
cookie_av[Field::expires] = std::to_string(res);
|
||||
}
|
||||
} else if (avlc == "secure") {
|
||||
cookie_av[Field::secure] = avlc;
|
||||
} else if (avlc == "httponly") {
|
||||
cookie_av[Field::secure] = avlc;
|
||||
} else {
|
||||
// Unknown attribute; ignore.
|
||||
}
|
||||
}
|
||||
return cookie_av;
|
||||
}
|
||||
|
||||
std::string CookieManager::httpHeaderLine(const std::string &domain,
|
||||
const std::string &uri) {
|
||||
auto cit = store.find(domain);
|
||||
if (cit == store.end()) {
|
||||
// Make room for cache!
|
||||
std::vector<std::string> v(Cache::cache_len);
|
||||
auto p = store.insert(std::make_pair(domain, v));
|
||||
cit = p.first;
|
||||
} else {
|
||||
// Check if cached value exists and hasn't expired:
|
||||
const std::vector<std::string> &v = cit->second;
|
||||
if (!v.empty() && !v[Cache::cookie_header].empty() &&
|
||||
!isExpired(v[Cache::expiry])) {
|
||||
// Cached value exists, no part of it has expired.
|
||||
if (v[Cache::cookie_header].size() == 1)
|
||||
return "";
|
||||
else
|
||||
return v[Cache::cookie_header];
|
||||
}
|
||||
}
|
||||
|
||||
// We'll set this to true if we can't cache the result, e.g.
|
||||
// if it depends on path:
|
||||
bool dont_cache = false;
|
||||
|
||||
// Find all subdomains for which we have cookies:
|
||||
std::vector<std::map<std::string, std::vector<std::string>>::iterator>
|
||||
subdomains;
|
||||
std::string subdomain = domain;
|
||||
auto it = cit;
|
||||
while (true) {
|
||||
if (it != store.end())
|
||||
subdomains.push_back(it);
|
||||
auto pos = subdomain.find('.');
|
||||
if (pos == std::string::npos)
|
||||
break; // We don't accept top level domains.
|
||||
subdomain.erase(0, pos+1);
|
||||
it = store.find(subdomain);
|
||||
}
|
||||
|
||||
// Get all cookie values, starting from least significant subdomain:
|
||||
std::string cache_expiry;
|
||||
std::map<std::string, std::string> cookieVal;
|
||||
while (!subdomains.empty()) {
|
||||
// Reevaluate:
|
||||
auto &v = subdomains.back()->second;
|
||||
for (size_t i=Cache::cache_len; i<v.size(); ) {
|
||||
const std::string &expiry = v[i+Field::expires];
|
||||
if (!expiry.empty() &&
|
||||
(cache_expiry.empty() || less(expiry, cache_expiry))) {
|
||||
if (isExpired(expiry)) {
|
||||
v.erase(v.begin()+static_cast<long>(i),
|
||||
v.begin()+static_cast<long>(i+Field::field_len));
|
||||
continue;
|
||||
} else
|
||||
cache_expiry = expiry;
|
||||
}
|
||||
if (v[i+Field::path] != "/") {
|
||||
dont_cache = true;
|
||||
if (HttpCommon::isWithinPath(uri, v[i+Field::path]))
|
||||
cookieVal[v[i+Field::name]] = v[i+Field::value];
|
||||
} else
|
||||
cookieVal[v[i+Field::name]] = v[i+Field::value];
|
||||
i += Field::field_len;
|
||||
}
|
||||
subdomains.pop_back();
|
||||
}
|
||||
|
||||
auto p = cookieVal.begin();
|
||||
if (p == cookieVal.end()) {
|
||||
cit->second[Cache::cookie_header] = "-";
|
||||
cit->second[Cache::expiry].clear();
|
||||
return "";
|
||||
}
|
||||
std::string cval = "Cookie: " + p->first + "=" + p->second;
|
||||
while (++p != cookieVal.end()) {
|
||||
cval += ("; " + p->first + "=" + p->second);
|
||||
}
|
||||
|
||||
cval += "\r\n";
|
||||
if (!dont_cache) {
|
||||
cit->second[Cache::cookie_header] = cval;
|
||||
cit->second[Cache::expiry] = cache_expiry;
|
||||
}
|
||||
return cval;
|
||||
}
|
||||
|
||||
void CookieManager::eraseCookies(const std::string &domain) {
|
||||
auto cit = store.find(domain);
|
||||
if (cit == store.end())
|
||||
return;
|
||||
store.erase(cit);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
std::string CookieManager::getCookieVal(const std::string &name,
|
||||
std::string domain) {
|
||||
while (true) {
|
||||
auto cit = store.find(domain);
|
||||
if (cit != store.end()) {
|
||||
const auto &v = cit->second;
|
||||
for (unsigned i=Cache::cache_len; i<v.size(); i+=Field::field_len)
|
||||
if (v[i+Field::name] == name && v[i+Field::path] == "/")
|
||||
return v[i+Field::value];
|
||||
}
|
||||
auto pos = domain.find('.');
|
||||
if (pos == std::string::npos)
|
||||
return std::string();
|
||||
domain.erase(0, pos+1);
|
||||
}
|
||||
}
|
||||
95
src/http/cookiemanager.h
Normal file
95
src/http/cookiemanager.h
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright (c) 2019 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <goran@init.se>
|
||||
|
||||
// This class stores cookies in memory only.
|
||||
// Create a subclass if you want persistent cookies.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
#include "../framework/logger.h"
|
||||
|
||||
class CookieManager : public Logger {
|
||||
public:
|
||||
CookieManager(const std::string &name = "CookieManager") :
|
||||
Logger(name) {
|
||||
}
|
||||
|
||||
virtual ~CookieManager();
|
||||
|
||||
void setCookie(const std::string &line, std::string domain,
|
||||
std::string uri);
|
||||
|
||||
// Return empty string if no cookies exist, or a HTTP header line
|
||||
// "Cookie: name1=val1; name2=val2\r\n"
|
||||
std::string httpHeaderLine(const std::string &domain,
|
||||
const std::string &uri);
|
||||
|
||||
// Return cookie value for domain and name; return empty if not found.
|
||||
std::string getCookieVal(const std::string &name, std::string domain);
|
||||
|
||||
// Return true if no cookies have been set.
|
||||
bool empty() const {
|
||||
return store.empty();
|
||||
}
|
||||
|
||||
void eraseCookies(const std::string &domain);
|
||||
|
||||
// Store persistently, return true on success
|
||||
virtual bool save();
|
||||
|
||||
protected:
|
||||
bool isDirty() const {
|
||||
return dirty;
|
||||
}
|
||||
void clearDirty() {
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
// timeval contains only digits 0-9, doesn't start with 0.
|
||||
bool isExpired(const std::string &timeval);
|
||||
|
||||
// both must be non-empty, only digits, no leading zeroes.
|
||||
static bool less(const std::string &tv1, const std::string &tv2) {
|
||||
if (tv1.size() < tv2.size())
|
||||
return true;
|
||||
if (tv1.size() > tv2.size())
|
||||
return false;
|
||||
return tv1 < tv2;
|
||||
}
|
||||
|
||||
// Access to all cookies, to serialise/deserialise.
|
||||
// The key is the domain for which the cookie is valid (also subdomains).
|
||||
// The value is a vector with elements as follows:
|
||||
// The first Cache::cache_len elements are used to cache values and must be
|
||||
// ignored. The remaining elements are 0 or more groups of Field::field_len
|
||||
// elements; each group describes the properties of a single cookie.
|
||||
std::map<std::string, std::vector<std::string> > &cookies() {
|
||||
return store;
|
||||
}
|
||||
enum Cache {
|
||||
cookie_header, expiry, cache_len
|
||||
};
|
||||
enum Field {
|
||||
name, value, domain, expires, path, secure, httponly, field_len
|
||||
};
|
||||
private:
|
||||
// Map domain to cookie strings:
|
||||
// First element of each store vector is a cache for httpHeaderLine():
|
||||
// - empty string means nothing is cached
|
||||
// - string of length 1 means there are no cookies for the domain
|
||||
// - string of length > 1 is the cookie header line
|
||||
// - NOTE! We only cache for path=/
|
||||
// Second element is cache expiry time, empty for no expiry.
|
||||
std::map<std::string, std::vector<std::string> > store;
|
||||
// TODO: Store time of creation / last use?
|
||||
// last use may ignore cache.
|
||||
|
||||
static std::vector<std::string> cookieSplit(const std::string &line);
|
||||
TimePoint expire_date;
|
||||
bool dirty = false;
|
||||
time_t t_now = 0;
|
||||
std::string s_now = "0";
|
||||
};
|
||||
173
src/http/http_common.cpp
Normal file
173
src/http/http_common.cpp
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#include "http_common.h"
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
bool HttpCommon::parseHeaders(const std::string &header, std::string &r,
|
||||
std::multimap<std::string, std::string> &res) {
|
||||
std::string::size_type start, end=0;
|
||||
while (true) {
|
||||
start = end;
|
||||
end = header.find("\r\n", start);
|
||||
if (end == std::string::npos)
|
||||
return false;
|
||||
std::string line = header.substr(start, end-start);
|
||||
if (line.empty()) {
|
||||
break;
|
||||
} else {
|
||||
// Remove trailing spaces
|
||||
std::size_t n = line.find_last_not_of(' ');
|
||||
if (n != std::string::npos)
|
||||
line.resize(n+1);
|
||||
}
|
||||
end += std::strlen("\r\n");
|
||||
|
||||
if (!start) {
|
||||
r = line;
|
||||
continue;
|
||||
}
|
||||
std::string::size_type separator = line.find(": ");
|
||||
if (separator == std::string::npos)
|
||||
return false;
|
||||
std::string attribute = line.substr(0, separator);
|
||||
for (std::string::size_type i=0; i<attribute.size(); ++i)
|
||||
if (attribute[i] >= 'A' && attribute[i] <= 'Z')
|
||||
attribute[i] += ('a' - 'A');
|
||||
res.insert(std::make_pair(attribute, line.substr(separator+2)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::map<std::string, const char *> mime_db = {
|
||||
{ "txt", "text/plain; charset=utf-8" },
|
||||
{ "html", "text/html; charset=utf-8" },
|
||||
{ "css", "text/css" },
|
||||
{ "js", "text/javascript" },
|
||||
{ "pdf", "application/pdf" },
|
||||
{ "xml", "application/xml" },
|
||||
{ "json", "application/json" },
|
||||
};
|
||||
}
|
||||
|
||||
const char *HttpCommon::mime_type(const std::string &file_name) {
|
||||
auto pos = file_name.find_last_of('.');
|
||||
if (pos == std::string::npos)
|
||||
pos = 0;
|
||||
else
|
||||
++pos;
|
||||
auto p = mime_db.find(file_name.substr(pos));
|
||||
if (p != mime_db.end())
|
||||
return p->second;
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
namespace {
|
||||
inline int toInt(char p1, char p2) {
|
||||
if (!isdigit(p1) || !isdigit(p2))
|
||||
return -1;
|
||||
return 10*(p1-'0') + (p2-'0');
|
||||
}
|
||||
}
|
||||
|
||||
// Return -1 on failure.
|
||||
time_t HttpCommon::parseDateRfc1123(const std::string &date) {
|
||||
time_t ret = -1;
|
||||
|
||||
// Date format:
|
||||
// Wed, 07-Jun-2017 11:34:59 GMT
|
||||
// 012345678901234567890123456789
|
||||
|
||||
if (date.size() != 29)
|
||||
return ret;
|
||||
static const std::string wdays("Sun, Mon, Tue, Wed, Thu, Fri, Sat, ");
|
||||
auto wday_pos = wdays.find(date.substr(0, 5));
|
||||
if (wday_pos == std::string::npos || wday_pos%5)
|
||||
return ret;
|
||||
|
||||
static const std::string months("-Jan-Feb-Mar-Apr-May-Jun-Jul-Aug-Sep-Oct-Nov-Dec-");
|
||||
auto mon_pos = months.find(date.substr(7, 5));
|
||||
if (mon_pos == std::string::npos || mon_pos%4)
|
||||
return ret;
|
||||
|
||||
if (date[16] != ' ' || date[19] != ':' || date[22] != ':' ||
|
||||
date.substr(25) != " GMT")
|
||||
return ret;
|
||||
|
||||
struct tm timespec;
|
||||
timespec.tm_sec = toInt(date[23], date[24]);
|
||||
timespec.tm_min = toInt(date[20], date[21]);
|
||||
timespec.tm_hour = toInt(date[17], date[18]);
|
||||
int mday = toInt(date[5], date[6]);
|
||||
timespec.tm_mday = mday;
|
||||
if (timespec.tm_sec < 0 || timespec.tm_sec > 59 ||
|
||||
timespec.tm_min < 0 || timespec.tm_min > 59 ||
|
||||
timespec.tm_hour < 0 || timespec.tm_hour > 23 || mday < 0)
|
||||
return ret;
|
||||
timespec.tm_mon = static_cast<int>(mon_pos / 4);
|
||||
timespec.tm_year = toInt(date[14], date[15]);
|
||||
int yy = toInt(date[12], date[13]) - 19;
|
||||
if (timespec.tm_year < 0 || yy < 0)
|
||||
return ret;
|
||||
timespec.tm_year += yy*100;
|
||||
//timespec.tm_wday = 0;
|
||||
//timespec.tm_yday = 0;
|
||||
timespec.tm_isdst = -1;
|
||||
|
||||
// mktime depends on current timezone, so we must
|
||||
// temporarily reset timezone.
|
||||
char *tz = getenv("TZ");
|
||||
#ifdef _WIN32
|
||||
_putenv("TZ=UTC");
|
||||
_tzset();
|
||||
ret = mktime(×pec);
|
||||
if (tz) {
|
||||
char buf[255];
|
||||
snprintf(buf, sizeof(buf), "TZ=%s", tz);
|
||||
_putenv(buf);
|
||||
} else {
|
||||
_putenv("TZ=");
|
||||
}
|
||||
_tzset();
|
||||
#else
|
||||
setenv("TZ", "", 1);
|
||||
tzset();
|
||||
ret = mktime(×pec);
|
||||
if (tz)
|
||||
setenv("TZ", tz, 1);
|
||||
else
|
||||
unsetenv("TZ");
|
||||
tzset();
|
||||
if (timespec.tm_wday*5 != static_cast<int>(wday_pos)
|
||||
|| timespec.tm_mday != mday)
|
||||
return -1;
|
||||
#endif
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void HttpCommon::trimWSP(std::string &s) {
|
||||
auto pos = s.find_first_not_of(" \t\r\n\v");
|
||||
if (pos && pos != std::string::npos)
|
||||
s.erase(0, pos);
|
||||
|
||||
pos = s.find_last_not_of(" \t\r\n\v");
|
||||
if (pos != std::string::npos)
|
||||
s.erase(++pos);
|
||||
}
|
||||
|
||||
std::vector<std::string> HttpCommon::split(const std::string &s,
|
||||
const std::string &sep) {
|
||||
std::vector<std::string> v;
|
||||
std::string::size_type pos = 0;
|
||||
while (true) {
|
||||
auto epos = s.find(sep, pos);
|
||||
v.push_back(s.substr(pos, epos-pos));
|
||||
if (epos == std::string::npos)
|
||||
break;
|
||||
pos = epos + sep.size();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
62
src/http/http_common.h
Normal file
62
src/http/http_common.h
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
class HttpCommon {
|
||||
public:
|
||||
// Return true on success.
|
||||
static bool parseHeaders(const std::string &header, std::string &r,
|
||||
std::multimap<std::string, std::string> &res);
|
||||
|
||||
// Guess mime type, e.g. "text/html", from url/filename
|
||||
static const char *mime_type(const std::string &file_name);
|
||||
|
||||
// Return -1 on failure.
|
||||
static time_t parseDateRfc1123(const std::string &date);
|
||||
|
||||
// Remove leading and trailing whitespace:
|
||||
static void trimWSP(std::string &s);
|
||||
|
||||
// Split s into fields sepatared by sep:
|
||||
static std::vector<std::string> split(const std::string &s,
|
||||
const std::string &sep);
|
||||
static bool isSubdomain(const std::string &subdomain,
|
||||
const std::string &domain) {
|
||||
auto s = domain.size();
|
||||
return (subdomain.size() > s &&
|
||||
subdomain[subdomain.size()-s-1] == '.' &&
|
||||
subdomain.substr(subdomain.size()-s) == domain);
|
||||
}
|
||||
static bool isWithinPath(const std::string &uri,
|
||||
const std::string &path) {
|
||||
if (path.empty())
|
||||
return (!uri.empty() && uri[0] == '/');
|
||||
if (path != uri.substr(0, path.size()))
|
||||
return false;
|
||||
if (path.size() < uri.size()) {
|
||||
return path[path.size()-1] == '/' ||
|
||||
uri[path.size()] == '/';
|
||||
} else if (path.size() == uri.size()) {
|
||||
return true; // Identical.
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Return the path part of an uri.
|
||||
static std::string uriPath(std::string uri) {
|
||||
auto qpos = uri.find('?');
|
||||
if (qpos != std::string::npos)
|
||||
uri.erase(qpos);
|
||||
qpos = uri.rfind('/');
|
||||
if (qpos != std::string::npos)
|
||||
uri.erase(qpos+1);
|
||||
return uri;
|
||||
}
|
||||
};
|
||||
397
src/http/httpclientconnection.cpp
Normal file
397
src/http/httpclientconnection.cpp
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
// Copyright (c) 2018 The Swedish Internet Foundation
|
||||
// Written by Göran Andersson <initgoran@gmail.com>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include <iomanip>
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "httpclientconnection.h"
|
||||
#include "httpclienttask.h"
|
||||
#include "http_common.h"
|
||||
#include "sha1.h"
|
||||
|
||||
HttpClientConnection::HttpClientConnection(const std::string &label,
|
||||
HttpClientTask *task,
|
||||
const std::string &hostname,
|
||||
uint16_t port,
|
||||
const std::string &http_hostname,
|
||||
uint16_t iptype,
|
||||
struct addrinfo *local_addr) :
|
||||
HttpConnection(label, task, hostname, port, iptype, local_addr),
|
||||
real_hostname(http_hostname.empty() ? hostname : http_hostname),
|
||||
owner_task(task) {
|
||||
if (!http_hostname.empty())
|
||||
proto_hostname = "http://" + http_hostname;
|
||||
}
|
||||
|
||||
PollState HttpClientConnection::connected() {
|
||||
owner_task->newRequest(this);
|
||||
if (current_request == "pass")
|
||||
return PollState::NONE;
|
||||
if (current_request.empty())
|
||||
return PollState::KEEPALIVE;
|
||||
//dbg_log() << "HttpClientConnection::connected() request: " << current_request;
|
||||
asyncSendData(current_request);
|
||||
if (bytes_left_to_post)
|
||||
return PollState::WRITE;
|
||||
else
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
bool HttpClientConnection::parseChunkedEncodingLength(char *&buf, size_t &len) {
|
||||
// Between chunks, there is \r\n, a hex number, and \r\n again.
|
||||
// The number gives the size of next chunk.
|
||||
// The last size is 0, and then an extra \r\n to terminate the request.
|
||||
|
||||
size_t oldlen = chunked_info.size();
|
||||
chunked_info += std::string(buf, len<20 ? len : 20);
|
||||
|
||||
if (final_chunk) {
|
||||
len = 0;
|
||||
// We're waiting for the trailing \r\n.
|
||||
if (chunked_info == "\r\n") {
|
||||
// OK, all done!
|
||||
response_is_chunked = false;
|
||||
return true;
|
||||
}
|
||||
return (chunked_info == "\r");
|
||||
}
|
||||
|
||||
if (chunked_info.size() < 5) {
|
||||
// Wait for more, keeping the fragment in chunked_info
|
||||
len = 0;
|
||||
return true;
|
||||
}
|
||||
if (chunked_info.substr(0, 2) != "\r\n")
|
||||
return false;
|
||||
|
||||
size_t endOfSizePos = chunked_info.find("\r\n", 2);
|
||||
if (endOfSizePos == std::string::npos) {
|
||||
// Haven't got terminating \r\n yet.
|
||||
// Wait for more if len < 20, otherwise fail.
|
||||
len = 0;
|
||||
return chunked_info.size() < 20;
|
||||
}
|
||||
|
||||
size_t to_skip = endOfSizePos + 2 - oldlen;
|
||||
buf += to_skip;
|
||||
len -= to_skip;
|
||||
|
||||
try {
|
||||
std::size_t count;
|
||||
std::string num = "0x"+ chunked_info.substr(2);
|
||||
bytes_left_to_read = std::stoul(num, &count, 16);
|
||||
if (count != endOfSizePos)
|
||||
return false;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!bytes_left_to_read) {
|
||||
final_chunk = true;
|
||||
}
|
||||
|
||||
chunked_info.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
PollState HttpClientConnection::readData(char *buf, size_t len) {
|
||||
if (dbgIsOn()) {
|
||||
log() << socket() << "HttpClientConnection <" << std::string(buf, len) << ">";
|
||||
}
|
||||
if (!response_header_complete) {
|
||||
std::string::size_type old_length = response_header.size();
|
||||
std::string::size_type to_append = len;
|
||||
if (old_length + to_append > max_header_length)
|
||||
to_append = max_header_length - old_length;
|
||||
response_header.append(buf, to_append);
|
||||
std::string::size_type pos = response_header.find("\r\n\r\n");
|
||||
if (pos == std::string::npos) {
|
||||
if (response_header.size() >= max_header_length)
|
||||
return PollState::KILL;
|
||||
else
|
||||
return PollState::READ;
|
||||
}
|
||||
response_header.resize(pos + strlen("\r\n\r\n"));
|
||||
if (!parseResponseHeaders())
|
||||
return PollState::CLOSE;
|
||||
len -= (response_header.size() - old_length);
|
||||
if (len)
|
||||
buf += (response_header.size() - old_length);
|
||||
else if (!response_header_complete) {
|
||||
response_header.clear();
|
||||
http_response_headers.clear();
|
||||
return PollState::READ; // e.g. after PROXY CONNECT
|
||||
}
|
||||
}
|
||||
|
||||
if (is_websocket)
|
||||
return wsReadData(buf, len);
|
||||
|
||||
if (dbgIsOn()) {
|
||||
dbg_log() << socket() << " HEADER OK, PAYLOAD <"
|
||||
<< std::string(buf, len) << "> LEFT: "
|
||||
<< bytes_left_to_read << " CH: " << response_is_chunked;
|
||||
}
|
||||
// Reading payload
|
||||
if (len) {
|
||||
if (response_is_chunked && !bytes_left_to_read) {
|
||||
if (!parseChunkedEncodingLength(buf, len)) {
|
||||
err_log() << "content encoding error";
|
||||
return PollState::KILL;
|
||||
}
|
||||
}
|
||||
if (bytes_left_to_read) {
|
||||
size_t to_read = std::min(len, bytes_left_to_read);
|
||||
if (buffer_response)
|
||||
buffer.append(buf, to_read);
|
||||
else
|
||||
owner_task->payload(this, buf, to_read);
|
||||
bytes_left_to_read -= to_read;
|
||||
len -= to_read;
|
||||
buf += to_read;
|
||||
}
|
||||
}
|
||||
|
||||
if (response_is_chunked) {
|
||||
if (len > 0)
|
||||
return readData(buf, len);
|
||||
else
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
if (bytes_left_to_read > 0)
|
||||
return PollState::READ;
|
||||
|
||||
bool to_continue = owner_task->requestComplete(this);
|
||||
|
||||
if (len) {
|
||||
// Error
|
||||
err_log() << "got " << len << " bytes after request complete";
|
||||
return PollState::KILL;
|
||||
}
|
||||
if (to_continue) {
|
||||
current_request.clear();
|
||||
current_uri.clear();
|
||||
response_header.clear();
|
||||
response_header_complete = false;
|
||||
response_http_status = 0;
|
||||
buffer.clear();
|
||||
http_response_headers.clear();
|
||||
return connected(); // New request or close.
|
||||
} else {
|
||||
return PollState::KEEPALIVE;
|
||||
}
|
||||
}
|
||||
|
||||
bool HttpClientConnection::parseResponseHeaders() {
|
||||
if (response_http_status) {
|
||||
err_log() << "internal error: headers already parsed";
|
||||
return false;
|
||||
}
|
||||
response_header_complete = true;
|
||||
|
||||
std::string response_first_line;
|
||||
if (!HttpCommon::parseHeaders(response_header, response_first_line,
|
||||
http_response_headers)) {
|
||||
log() << "Bad HTTP headers: " << response_header;
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
// HTTP/1.1 200 OK
|
||||
unsigned int major, minor, status;
|
||||
int pos = sscanf(response_first_line.c_str(), "HTTP/%u.%u %u",
|
||||
&major, &minor, &status);
|
||||
if (pos != 3 || !status)
|
||||
return false;
|
||||
|
||||
set_http_version(major, minor);
|
||||
response_http_status = status;
|
||||
}
|
||||
|
||||
for (auto p = http_response_headers.lower_bound("set-cookie");
|
||||
p!=http_response_headers.upper_bound("set-cookie"); ++p)
|
||||
owner_task->setCookie(p->second, current_uri);
|
||||
|
||||
if (!websocket_rkey.empty()) {
|
||||
if (websocket_rkey == "CONNECT") {
|
||||
// Doing HTTP CONNECT through a proxy
|
||||
log() << "PROXY CONNECT status " << response_http_status;
|
||||
websocket_rkey.clear();
|
||||
proto_hostname.clear(); // No more proxy calls
|
||||
if (response_http_status < 200 || response_http_status >= 300)
|
||||
return false;
|
||||
response_header_complete = false;
|
||||
response_http_status = 0;
|
||||
current_request.clear();
|
||||
ws_get(current_uri);
|
||||
asyncSendData(current_request);
|
||||
return true;
|
||||
}
|
||||
|
||||
auto p = http_response_headers.find("sec-websocket-accept");
|
||||
if (p == http_response_headers.end() || p->second != websocket_rkey) {
|
||||
err_log() << "wrong Sec-WebSocket-Accept";
|
||||
return false;
|
||||
}
|
||||
is_websocket = true;
|
||||
response_is_chunked = false;
|
||||
bytes_left_to_read = 0;
|
||||
return owner_task->websocketUpgrade(this);
|
||||
}
|
||||
|
||||
try {
|
||||
auto p = http_response_headers.find("transfer-encoding");
|
||||
if (p != http_response_headers.end() && p->second == "chunked") {
|
||||
response_is_chunked = true;
|
||||
chunked_info = "\r\n";
|
||||
final_chunk = false;
|
||||
bytes_left_to_read = 0;
|
||||
} else {
|
||||
p = http_response_headers.find("content-length");
|
||||
if (p == http_response_headers.end()) {
|
||||
err_log() << "cannot find Content-Length";
|
||||
return false;
|
||||
}
|
||||
response_is_chunked = false;
|
||||
bytes_left_to_read = std::stoul(p->second);
|
||||
}
|
||||
} catch (std::exception &err) {
|
||||
err_log() << "Exception when parsing HTTP response: " << err.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
return owner_task->headerComplete(this);
|
||||
}
|
||||
|
||||
PollState HttpClientConnection::writeData() {
|
||||
if (is_websocket)
|
||||
return wsWriteData();
|
||||
|
||||
bytes_left_to_post -= owner_task->doPost(this, bytes_left_to_post);
|
||||
if (bytes_left_to_post)
|
||||
return PollState::WRITE;
|
||||
else
|
||||
return PollState::READ;
|
||||
}
|
||||
|
||||
std::string HttpClientConnection::urlencode(const std::string &val) {
|
||||
std::ostringstream res;
|
||||
res.fill('0');
|
||||
res << std::hex;
|
||||
|
||||
for (auto i=val.cbegin(); i!=val.cend(); ++i) {
|
||||
std::string::value_type c = *i;
|
||||
|
||||
if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
|
||||
res << c;
|
||||
} else {
|
||||
res << '%' << std::setw(2)
|
||||
<< std::uppercase << int(static_cast<unsigned char>(c))
|
||||
<< std::nouppercase;
|
||||
}
|
||||
}
|
||||
|
||||
return res.str();
|
||||
}
|
||||
|
||||
void HttpClientConnection::
|
||||
addUrlPars(std::string &url, const std::map<std::string, std::string> &pars) {
|
||||
auto p = pars.begin();
|
||||
if (p == pars.end())
|
||||
return;
|
||||
if (url.find('?') == std::string::npos)
|
||||
url += "?";
|
||||
else
|
||||
url += "&";
|
||||
((url += p->first) += "=") += urlencode(p->second);
|
||||
while (++p != pars.end())
|
||||
(((url += "&") += p->first) += "=") += urlencode(p->second);
|
||||
}
|
||||
|
||||
void HttpClientConnection::get(const std::string &url) {
|
||||
if (!idle()) {
|
||||
err_log() << "previous request not complete: " << current_request;
|
||||
return;
|
||||
}
|
||||
current_uri = url;
|
||||
current_request = "GET " + proto_hostname +
|
||||
url + " HTTP/1.1\r\nHost: " + real_hostname +
|
||||
"\r\n" + owner_task->httpHeaderLines(url) + "\r\n";
|
||||
}
|
||||
|
||||
void HttpClientConnection::ws_get(const std::string &url) {
|
||||
if (!idle()) {
|
||||
err_log() << "previous request not complete: " << current_request;
|
||||
return;
|
||||
}
|
||||
if (!proto_hostname.empty()) {
|
||||
current_uri = url;
|
||||
websocket_rkey = "CONNECT";
|
||||
current_request = "CONNECT " + real_hostname + " HTTP/1.1\r\n"
|
||||
"Hostname: " + real_hostname + "\r\n\r\n";
|
||||
return;
|
||||
}
|
||||
// The first 24 chars will be filled with random base64 chars,
|
||||
// the rest is the websocket uuid.
|
||||
static char dst[] {
|
||||
"01234567890123456789====258EAFA5-E914-47DA-95CA-C5AB0DC85B11" };
|
||||
|
||||
// 16 byte random data, convert to 24 base64 chars:
|
||||
int32_t rnd[4] = { rand(), rand(), rand(), rand() };
|
||||
const unsigned char *src = reinterpret_cast<const unsigned char *>(rnd);
|
||||
base64_encode(src, sizeof(rnd), dst);
|
||||
|
||||
current_uri = url;
|
||||
current_request = "GET " + proto_hostname +
|
||||
url + " HTTP/1.1\r\nHost: " + real_hostname +
|
||||
"\r\nSec-WebSocket-Key: " + std::string(dst, 24) +
|
||||
"\r\nSec-WebSocket-Version: 13\r\nUpgrade: websocket"
|
||||
"\r\nConnection: Upgrade"
|
||||
"\r\n" + owner_task->httpHeaderLines(url) + "\r\n";
|
||||
static char rkey[] = "012345678901234567890123456=";
|
||||
static SHA1 sha1(rkey);
|
||||
sha1.update(dst);
|
||||
// The server's response is supposed to contain this string:
|
||||
websocket_rkey = std::string(rkey, 28);
|
||||
}
|
||||
|
||||
void HttpClientConnection::post(const std::string &url,
|
||||
const std::string &data) {
|
||||
if (!idle()) {
|
||||
err_log() << "previous request not complete: " << current_request;
|
||||
return;
|
||||
}
|
||||
current_uri = url;
|
||||
current_request = "POST " + proto_hostname + url + " HTTP/1.1\r\nHost: " +
|
||||
real_hostname + "\r\n" + owner_task->httpHeaderLines(url) +
|
||||
"Content-Length: " + std::to_string(data.size()) + "\r\n\r\n" + data;
|
||||
}
|
||||
|
||||
void HttpClientConnection::post(const std::string &url, size_t len) {
|
||||
if (!idle()) {
|
||||
err_log() << "previous request not complete: " << current_request;
|
||||
return;
|
||||
}
|
||||
current_uri = url;
|
||||
current_request = "POST " + proto_hostname + url + " HTTP/1.1\r\nHost: " +
|
||||
real_hostname + "\r\n" + owner_task->httpHeaderLines(url) +
|
||||
"Content-Length: " + std::to_string(len) + "\r\n\r\n";
|
||||
bytes_left_to_post = len;
|
||||
}
|
||||
|
||||
std::string HttpClientConnection::cacheLabel() {
|
||||
return owner_task->cacheLabel();
|
||||
}
|
||||
|
||||
void HttpClientConnection::setOwner(Task *new_owner) {
|
||||
owner_task = dynamic_cast<HttpClientTask *>(new_owner);
|
||||
if (!owner_task)
|
||||
throw std::logic_error("expected HttClientTask");
|
||||
Socket::setOwner(new_owner);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue