initial commit

This commit is contained in:
Ivan Baidakou
2019-08-11 16:00:58 +03:00
commit 1567b52fe9
24 changed files with 8256 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
---
Language: Cpp
# BasedOnStyle: LLVM
AccessModifierOffset: -2
AlignAfterOpenBracket: true
AlignConsecutiveAssignments: false
AlignEscapedNewlinesLeft: false
AlignOperands: true
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: false
BinPackArguments: true
BinPackParameters: true
BreakBeforeBinaryOperators: None
BreakBeforeBraces: Attach
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
ColumnLimit: 120
CommentPragmas: '^ IWYU pragma:'
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ]
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: true
MacroBlockBegin: ''
MacroBlockEnd: ''
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Right
SortIncludes: false
SpaceAfterCStyleCast: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: true
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
...
+12
View File
@@ -0,0 +1,12 @@
[submodule "lib/rotor"]
path = lib/rotor
url = git@github.com:basiliscos/cpp-rotor.git
[submodule "lib/spdlog"]
path = lib/spdlog
url = git@github.com:gabime/spdlog.git
[submodule "lib/fmt"]
path = lib/fmt
url = https://github.com/fmtlib/fmt.git
[submodule "lib/json"]
path = lib/json
url = https://github.com/nlohmann/json.git
+44
View File
@@ -0,0 +1,44 @@
cmake_minimum_required (VERSION 3.2)
project (syncspirit)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(
Boost
COMPONENTS
filesystem
program_options
system
REQUIRED
)
find_package(OpenSSL REQUIRED)
add_executable(syncspirit
src/configuration.cpp
src/main.cpp
src/net/global_discovery_actor.cpp
src/net/net_supervisor.cpp
src/net/upnp_actor.cpp
src/utils/error_code.cpp
src/utils/uri.cpp
src/utils/upnp_support.cpp
)
add_subdirectory("lib/rotor")
add_subdirectory("lib/fmt")
target_include_directories(syncspirit PUBLIC
${syncspirit_SOURCE_DIR}/lib/rotor/include
${syncspirit_SOURCE_DIR}/lib/spdlog/include
${syncspirit_SOURCE_DIR}/lib/fmt/include
${syncspirit_SOURCE_DIR}/lib/json/include
${syncspirit_SOURCE_DIR}/lib/outcome/include
)
target_link_libraries(syncspirit
rotor_asio
fmt::fmt
${Boost_LIBRARIES}
OpenSSL::SSL
)
Submodule
+1
Submodule lib/fmt added at 9e554999ce
Submodule
+1
Submodule lib/json added at a015b78e81
File diff suppressed because it is too large Load Diff
Submodule
+1
Submodule lib/rotor added at 1b1723fdcc
Submodule
+1
Submodule lib/spdlog added at a7148b718e
+117
View File
@@ -0,0 +1,117 @@
#include "configuration.h"
#include <boost/program_options.hpp>
#include <boost/tokenizer.hpp>
namespace po = boost::program_options;
namespace syncspirit::config {
spdlog::level::level_enum get_log_level(const std::string &log_level) {
using namespace spdlog::level;
level_enum value = info;
if (log_level == "trace")
value = trace;
if (log_level == "debug")
value = debug;
if (log_level == "info")
value = info;
if (log_level == "warn")
value = warn;
if (log_level == "error")
value = err;
if (log_level == "crit")
value = critical;
if (log_level == "off")
value = off;
return value;
}
static void fill_logging_config(po::variables_map &vm, configuration_t &cfg) {
// sinks
boost::optional<console_sink_t> console_sink;
if (vm.count("sink_console.level")) {
auto level = get_log_level(vm["sink_console.level"].as<std::string>());
console_sink = console_sink_t{level};
}
boost::optional<file_sink_t> file_sink;
if (vm.count("sink_file.file")) {
auto file = vm["sink_file.file"].as<std::string>();
auto level = get_log_level(vm["sink_file.level"].as<std::string>());
file_sink = file_sink_t{level, file};
}
if (vm.count("logging.sinks")) {
using tokenizer = boost::tokenizer<boost::char_separator<char>>;
auto sinks = vm["logging.sinks"].as<std::string>();
boost::char_separator<char> separator(",");
tokenizer tokens(sinks, separator);
for (auto &sink_name : tokens) {
if (sink_name == "console" && console_sink) {
cfg.logging_config.sinks.push_back(*console_sink);
}
if (sink_name == "file" && file_sink) {
cfg.logging_config.sinks.push_back(*file_sink);
}
}
}
}
boost::optional<configuration_t> get_config(std::ifstream &config) {
using result_t = boost::optional<configuration_t>;
result_t result{};
configuration_t cfg;
// clang-format off
po::options_description descr("Allowed options");
descr.add_options()
("sink_console.level", po::value<std::string>()->default_value("info"),"log level (default: info)")
("sink_file.level", po::value<std::string>()->default_value("info"), "log level (default: info)")
("sink_file.file", po::value<std::string>(), "log file")
("logging.sinks", po::value<std::string>()->default_value("console"), "comma-separated list of sinks, mandatory")
("local_announce.enabled", po::value<bool>()->default_value(true), "enable LAN-announcements")
("local_announce.port", po::value<std::uint16_t>()->default_value(21027), "LAN-announcement port")
("global_discovery.server", po::value<std::string>()->default_value("https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW"), "Global announce server")
("global_discovery.cert_file", po::value<std::string>()->default_value("~/.config/syncthing/cert.pem"), "certificate file path")
("global_discovery.key_file", po::value<std::string>()->default_value("~/.config/syncthing/key.pem"), "key file path")
("global_discovery.timeout", po::value<std::uint32_t>()->default_value(1), "discovery timeout (seconds)")
("upnp.timeout", po::value<std::uint32_t>()->default_value(5), "max wait discovery timeout")
;
// clang-format on
po::variables_map vm;
po::store(po::parse_config_file(config, descr, false), vm);
po::notify(vm);
fill_logging_config(vm, cfg);
cfg.local_announce_config.enabled = vm["local_announce.enabled"].as<bool>();
cfg.local_announce_config.port = vm["local_announce.port"].as<std::uint16_t>();
auto server_url_raw = vm["global_discovery.server"].as<std::string>();
auto server_url = utils::parse(server_url_raw.c_str());
if (!server_url) {
std::cout << "wrong server_url: " << server_url_raw << "\n";
std::cout << descr << "\n";
return result;
}
cfg.global_announce_config.server_url = *server_url;
cfg.global_announce_config.cert_file = vm["global_discovery.cert_file"].as<std::string>();
cfg.global_announce_config.key_file = vm["global_discovery.key_file"].as<std::string>();
cfg.global_announce_config.timeout = vm["global_discovery.timeout"].as<std::uint32_t>();
cfg.upnp_config.timeout = vm["upnp.timeout"].as<std::uint32_t>();
// checks
if (cfg.logging_config.sinks.empty()) {
std::cout << descr << "\n";
return result;
}
// all OK
result = std::move(cfg);
return result;
}
} // namespace syncspirit::config
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include "utils/uri.h"
#include <boost/optional.hpp>
#include <boost/variant.hpp>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <spdlog/spdlog.h>
namespace syncspirit::config {
struct console_sink_t {
spdlog::level::level_enum level;
};
struct file_sink_t {
spdlog::level::level_enum level;
std::string file;
};
using sink_config_t = boost::variant<console_sink_t, file_sink_t>;
struct logging_config_t {
std::vector<sink_config_t> sinks;
};
struct local_announce_config_t {
std::uint16_t port;
bool enabled;
};
struct global_announce_config_t {
syncspirit::utils::URI server_url;
std::string cert_file;
std::string key_file;
std::uint32_t timeout;
};
struct upnp_config_t {
std::uint32_t timeout;
};
struct configuration_t {
logging_config_t logging_config;
local_announce_config_t local_announce_config;
upnp_config_t upnp_config;
global_announce_config_t global_announce_config;
};
spdlog::level::level_enum get_log_level(const std::string &log_level);
boost::optional<configuration_t> get_config(std::ifstream &config);
} // namespace syncspirit::config
+106
View File
@@ -0,0 +1,106 @@
#include <chrono>
#include <csignal>
#include <fstream>
#include <iostream>
#include <thread>
#include <vector>
#include "configuration.h"
#include "net/net_supervisor.h"
#include <boost/program_options.hpp>
#include <rotor/asio.hpp>
#include <spdlog/spdlog.h>
namespace po = boost::program_options;
namespace pt = boost::posix_time;
namespace ra = rotor::asio;
namespace asio = boost::asio;
using namespace syncspirit;
static std::atomic<bool> signal_shutdown_flag{false};
int main(int argc, char **argv) {
using guard_t = asio::executor_work_guard<asio::io_context::executor_type>;
try {
// clang-format off
/* parse command-line & config options */
po::options_description cmdline_descr("Allowed options");
cmdline_descr.add_options()
("help", "show this help message")
("log_level", po::value<std::string>()->default_value("info"), "initial log level")
( "config_file", po::value<std::string>()->default_value("syncspirit.conf"), "configuration file path");
// clang-format on
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, cmdline_descr), vm);
po::notify(vm);
bool show_help = vm.count("help");
if (show_help) {
std::cout << cmdline_descr << "\n";
return 1;
}
auto log_level_str = vm["log_level"].as<std::string>();
auto log_level = config::get_log_level(log_level_str);
spdlog::set_level(log_level);
std::string config_file_path = vm["config_file"].as<std::string>();
std::ifstream config_file(config_file_path.c_str());
if (!config_file) {
spdlog::error("Cannot open config file {}", config_file_path);
return 1;
}
auto cfg_option = config::get_config(config_file);
if (!cfg_option) {
spdlog::error("Config file {} is incorrect", config_file_path);
return 1;
}
spdlog::trace("configuration seems OK");
spdlog::info("starting");
/* pre-init actors */
asio::io_context io_context;
ra::system_context_ptr_t sys_context{new ra::system_context_asio_t{io_context}};
ra::supervisor_config_t sup_conf{pt::milliseconds{500}};
auto sup_net = sys_context->create_supervisor<net::net_supervisor_t>(sup_conf, *cfg_option);
sup_net->start();
/* launch actors */
guard_t net_guard{asio::make_work_guard(io_context)};
auto net_thread = std::thread([&io_context]() {
io_context.run();
spdlog::trace("net thread has been terminated");
signal_shutdown_flag = true;
});
struct sigaction act;
act.sa_handler = [](int) { signal_shutdown_flag = true; };
if (sigaction(SIGINT, &act, nullptr) != 0) {
spdlog::critical("cannot set signal handler");
}
auto console_thread = std::thread([] {
while (!signal_shutdown_flag) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
spdlog::trace("console thread has been terminated");
});
spdlog::trace("waiting actors terminations");
console_thread.join();
net_guard.reset();
net_thread.join();
spdlog::trace("everything has been terminated");
} catch (const std::exception &ex) {
spdlog::critical("Starting failure : {}", ex.what());
return 1;
}
/* exit */
spdlog::info("normal exit");
return 0;
}
+148
View File
@@ -0,0 +1,148 @@
#include "global_discovery_actor.h"
#include <spdlog/spdlog.h>
using namespace syncspirit::net;
namespace ssl = boost::asio::ssl;
namespace pt = boost::posix_time;
global_discovery_actor_t::global_discovery_actor_t(ra::supervisor_asio_t &sup,
const config::global_announce_config_t &cfg_)
: r::actor_base_t{sup}, cfg{cfg_}, strand{static_cast<ra::supervisor_asio_t &>(supervisor).get_strand()},
io_context{strand.get_io_context()}, ssl_context{ssl::context::tls}, resolver{io_context},
stream{io_context, ssl_context}, timer{io_context}, activities_flag{0} {}
void global_discovery_actor_t::on_initialize(r::message_t<r::payload::initialize_actor_t> &msg) noexcept {
spdlog::trace("global_discovery_actor_t::on_initialize");
ssl_context.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2);
ssl_context.use_certificate_chain_file(cfg.cert_file);
ssl_context.use_private_key_file(cfg.key_file, ssl::context::pem);
// Set SNI Hostname (many hosts need this to handshake successfully)
if (!SSL_set_tlsext_host_name(stream.native_handle(), cfg.server_url.host.c_str())) {
sys::error_code ec{static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()};
spdlog::error("global_discovery_actor:: Set SNI Hostname : {}", ec.message());
trigger_shutdown();
return;
}
spdlog::trace("ssl socket has been initialized");
r::actor_base_t::on_initialize(msg);
}
void global_discovery_actor_t::trigger_shutdown() noexcept {
if (!(activities_flag & SHUTDOWN_ACTIVE)) {
activities_flag |= SHUTDOWN_ACTIVE;
do_shutdown();
}
}
void global_discovery_actor_t::on_shutdown(r::message_t<r::payload::shutdown_request_t> &msg) noexcept {
spdlog::trace("global_discovery_actor::on_shutdown");
if (activities_flag & TIMER_ACTIVE) {
sys::error_code ec;
timer.cancel(ec);
if (ec) {
spdlog::error("global_discovery_actor:: timer cancellaction : {}", ec.message());
}
activities_flag &= ~TIMER_ACTIVE;
}
if (activities_flag & RESOLVER_ACTIVE) {
resolver.cancel();
activities_flag &= ~RESOLVER_ACTIVE;
}
if (activities_flag & SOCKET_ACTIVE) {
sys::error_code ec;
stream.next_layer().cancel(ec);
if (ec) {
spdlog::error("global_discovery_actor:: socket cancellaction : {}", ec.message());
}
activities_flag &= ~SOCKET_ACTIVE;
}
r::actor_base_t::on_shutdown(msg);
}
void global_discovery_actor_t::on_timeout_trigger() noexcept {
spdlog::error("global_discovery_actor:: timeout");
activities_flag &= ~TIMER_ACTIVE;
trigger_shutdown();
}
void global_discovery_actor_t::on_timeout_error(const sys::error_code &ec) noexcept {
activities_flag &= ~TIMER_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("global_discovery_actor::on_timer_error :: {}", ec.message());
trigger_shutdown();
}
}
void global_discovery_actor_t::on_start(r::message_t<r::payload::start_actor_t> &msg) noexcept {
spdlog::trace("global_discovery_actor_t::on_start");
/* start timer */
timer.expires_from_now(pt::seconds{cfg.timeout});
auto fwd_timeout = ra::forwarder_t(*this, &global_discovery_actor_t::on_timeout_trigger,
&global_discovery_actor_t::on_timeout_error);
timer.async_wait(std::move(fwd_timeout));
activities_flag |= TIMER_ACTIVE;
/* start resolver */
auto &url = cfg.server_url;
spdlog::trace("global_discovery_actor resolving {}:{}", url.host, url.port);
auto fwd_resolver =
ra::forwarder_t(*this, &global_discovery_actor_t::on_resolve, &global_discovery_actor_t::on_resolve_error);
resolver.async_resolve(cfg.server_url.host, url.proto, std::move(fwd_resolver));
activities_flag |= RESOLVER_ACTIVE;
r::actor_base_t::on_start(msg);
}
void global_discovery_actor_t::on_resolve_error(const sys::error_code &ec) noexcept {
activities_flag &= ~RESOLVER_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("global_discovery_actor_t::on_resolve_error :: {}", ec.message());
trigger_shutdown();
}
}
void global_discovery_actor_t::on_resolve(resolve_results_t results) noexcept {
spdlog::trace("global_discovery_actor_t::on_resolve");
activities_flag &= ~RESOLVER_ACTIVE;
activities_flag |= SOCKET_ACTIVE;
auto fwd =
ra::forwarder_t(*this, &global_discovery_actor_t::on_connect, &global_discovery_actor_t::on_connect_error);
asio::async_connect(stream.next_layer(), results.begin(), results.end(), std::move(fwd));
}
void global_discovery_actor_t::on_connect_error(const sys::error_code &ec) noexcept {
activities_flag &= ~SOCKET_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("gvcp_actor::on_connect_error :: {}", ec.message());
trigger_shutdown();
}
}
void global_discovery_actor_t::on_connect(resolve_it_t it) noexcept {
std::stringstream buff;
buff << it->endpoint();
spdlog::trace("global_discovery_actor::on_connect success ({})", buff.str());
auto fwd =
ra::forwarder_t(*this, &global_discovery_actor_t::on_handshake, &global_discovery_actor_t::on_handshake_error);
stream.async_handshake(ssl::stream_base::client, std::move(fwd));
}
void global_discovery_actor_t::on_handshake_error(const sys::error_code &ec) noexcept {
if (ec != asio::error::operation_aborted) {
spdlog::warn("gvcp_actor::on_handshake_error :: {}", ec.message());
trigger_shutdown();
}
}
void global_discovery_actor_t::on_handshake() noexcept {
spdlog::trace("global_discovery_actor::on_handshake success");
trigger_shutdown();
}
+58
View File
@@ -0,0 +1,58 @@
#pragma once
#include "../configuration.h"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <rotor/asio/supervisor_asio.h>
namespace syncspirit {
namespace net {
namespace r = rotor;
namespace ra = rotor::asio;
namespace asio = boost::asio;
namespace sys = boost::system;
struct global_discovery_actor_t : public r::actor_base_t {
using resolve_results_t = asio::ip::tcp::resolver::results_type;
using resolve_it_t = asio::ip::tcp::resolver::results_type::iterator;
global_discovery_actor_t(ra::supervisor_asio_t &sup, const config::global_announce_config_t &cfg);
virtual void on_initialize(r::message_t<r::payload::initialize_actor_t> &) noexcept override;
virtual void on_start(r::message_t<r::payload::start_actor_t> &) noexcept override;
virtual void on_shutdown(r::message_t<r::payload::shutdown_request_t> &) noexcept override;
void trigger_shutdown() noexcept;
void on_timeout_trigger() noexcept;
void on_timeout_error(const sys::error_code &ec) noexcept;
void on_resolve_error(const sys::error_code &ec) noexcept;
void on_resolve(resolve_results_t results) noexcept;
void on_connect_error(const sys::error_code &ec) noexcept;
void on_connect(resolve_it_t endpoint) noexcept;
void on_handshake_error(const sys::error_code &ec) noexcept;
void on_handshake() noexcept;
private:
using socket_t = asio::ip::tcp::socket;
using endpoint_t = asio::ip::tcp::endpoint;
using stream_t = asio::ssl::stream<socket_t>;
using timer_t = asio::deadline_timer;
const static constexpr std::uint32_t SHUTDOWN_ACTIVE = 0b0000'0001;
const static constexpr std::uint32_t TIMER_ACTIVE = 0b0000'0010;
const static constexpr std::uint32_t RESOLVER_ACTIVE = 0b0000'0100;
const static constexpr std::uint32_t SOCKET_ACTIVE = 0b0000'1000;
config::global_announce_config_t cfg;
asio::io_context::strand &strand;
asio::io_context &io_context;
asio::ssl::context ssl_context;
asio::ip::tcp::resolver resolver;
stream_t stream;
timer_t timer;
std::uint32_t activities_flag;
};
} // namespace net
} // namespace syncspirit
+22
View File
@@ -0,0 +1,22 @@
#include "net_supervisor.h"
#include "global_discovery_actor.h"
#include "upnp_actor.h"
#include <spdlog/spdlog.h>
using namespace syncspirit::net;
net_supervisor_t::net_supervisor_t(ra::supervisor_asio_t *sup, ra::system_context_ptr_t ctx,
const ra::supervisor_config_t &sup_cfg, const config::configuration_t &cfg)
: ra::supervisor_asio_t(sup, ctx, sup_cfg), cfg{cfg} {}
void net_supervisor_t::on_start(r::message_t<r::payload::start_actor_t> &msg) noexcept {
spdlog::trace("net_supervisor_t::on_start");
ra::supervisor_asio_t::on_start(msg);
launch_upnp();
}
void net_supervisor_t::launch_discovery() noexcept {
create_actor<global_discovery_actor_t>(cfg.global_announce_config);
}
void net_supervisor_t::launch_upnp() noexcept { create_actor<upnp_actor_t>(cfg.upnp_config); }
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "../configuration.h"
#include <boost/asio.hpp>
#include <rotor/asio/supervisor_asio.h>
namespace syncspirit {
namespace net {
namespace r = rotor;
namespace ra = rotor::asio;
namespace asio = boost::asio;
struct net_supervisor_t : public ra::supervisor_asio_t {
net_supervisor_t(ra::supervisor_asio_t *sup, ra::system_context_ptr_t ctx, const ra::supervisor_config_t &sup_cfg,
const config::configuration_t &cfg);
virtual void on_start(r::message_t<r::payload::start_actor_t> &) noexcept override;
private:
void launch_discovery() noexcept;
void launch_upnp() noexcept;
config::configuration_t cfg;
};
} // namespace net
} // namespace syncspirit
+202
View File
@@ -0,0 +1,202 @@
#include "upnp_actor.h"
#include <spdlog/spdlog.h>
using namespace syncspirit::net;
using namespace syncspirit::utils;
using udp = boost::asio::ip::udp;
using tcp = boost::asio::ip::tcp;
using v4 = asio::ip::address_v4;
constexpr std::size_t rx_buff_sz = 1500;
upnp_actor_t::upnp_actor_t(ra::supervisor_asio_t &sup, const config::upnp_config_t &cfg_)
: r::actor_base_t{sup}, cfg{cfg_}, strand{static_cast<ra::supervisor_asio_t &>(supervisor).get_strand()},
io_context{strand.get_io_context()}, resolver{io_context}, udp_socket{io_context, udp::endpoint(udp::v4(), 0)},
tcp_socket{io_context}, timer{io_context}, activities_flag{0} {}
void upnp_actor_t::trigger_shutdown() noexcept {
if (!(activities_flag & SHUTDOWN_ACTIVE)) {
activities_flag |= SHUTDOWN_ACTIVE;
do_shutdown();
}
}
void upnp_actor_t::on_timeout_trigger() noexcept {
spdlog::error("upnp_actor_t:: timeout");
activities_flag &= ~TIMER_ACTIVE;
trigger_shutdown();
}
void upnp_actor_t::on_timeout_error(const sys::error_code &ec) noexcept {
activities_flag &= ~TIMER_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("upnp_actor_t::on_timer_error :: {}", ec.message());
trigger_shutdown();
}
}
void upnp_actor_t::on_shutdown(r::message_t<r::payload::shutdown_request_t> &msg) noexcept {
spdlog::trace("upnp_actor_t::on_shutdown");
if (activities_flag & TIMER_ACTIVE) {
sys::error_code ec;
timer.cancel(ec);
if (ec) {
spdlog::error("upnp_actor_t:: timer cancellation : {}", ec.message());
}
activities_flag &= ~TIMER_ACTIVE;
}
if (activities_flag & UDP_ACTIVE) {
sys::error_code ec;
udp_socket.cancel(ec);
if (ec) {
spdlog::error("upnp_actor_t:: udp socket cancellation : {}", ec.message());
}
activities_flag &= ~UDP_ACTIVE;
}
if (activities_flag & RESOLVER_ACTIVE) {
resolver.cancel();
activities_flag &= ~RESOLVER_ACTIVE;
}
if (activities_flag & TCP_ACTIVE) {
sys::error_code ec;
tcp_socket.cancel(ec);
if (ec) {
spdlog::error("upnp_actor_t:: tcp socket cancellation : {}", ec.message());
}
activities_flag &= ~TCP_ACTIVE;
}
r::actor_base_t::on_shutdown(msg);
}
void upnp_actor_t::on_start(r::message_t<r::payload::start_actor_t> &msg) noexcept {
spdlog::trace("upnp_actor::on_start");
/* start timer */
timer.expires_from_now(pt::seconds{cfg.timeout});
auto fwd_timeout = ra::forwarder_t(*this, &upnp_actor_t::on_timeout_trigger, &upnp_actor_t::on_timeout_error);
timer.async_wait(std::move(fwd_timeout));
activities_flag |= TIMER_ACTIVE;
auto destination = udp::endpoint(v4::from_string(upnp_addr), upnp_port);
auto request_result = make_discovery_request(tx_buff, cfg.timeout);
if (!request_result) {
spdlog::error("upnp_actor:: can't serialize upnp discovery request: {}", request_result.error().message());
return trigger_shutdown();
}
spdlog::trace("upnp_actor:: sending multicast request to {}:{} ({} bytes)", upnp_addr, upnp_port, tx_buff.size());
auto fwd_discovery = ra::forwarder_t(*this, &upnp_actor_t::on_discovery_sent, &upnp_actor_t::on_udp_error);
auto buff = asio::buffer(tx_buff.data(), tx_buff.size());
udp_socket.async_send_to(buff, destination, std::move(fwd_discovery));
activities_flag |= UDP_ACTIVE;
r::actor_base_t::on_start(msg);
}
void upnp_actor_t::on_udp_error(const sys::error_code &ec) noexcept {
activities_flag &= ~UDP_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("upnp_actor_t::on_udp_error :: {}", ec.message());
return trigger_shutdown();
}
}
void upnp_actor_t::on_discovery_sent(std::size_t bytes) noexcept {
spdlog::trace("upnp_actor_t::on_discovery_sent");
activities_flag &= ~UDP_ACTIVE;
if (bytes != tx_buff.size()) {
spdlog::warn("upnp_actor_t::on_discovery_sent :: tx buff size mismatch {} vs {}", bytes, tx_buff.size());
return trigger_shutdown();
}
spdlog::trace("upnp_actor::will wait discovery reply");
auto fwd = ra::forwarder_t(*this, &upnp_actor_t::on_discovery_received, &upnp_actor_t::on_udp_error);
auto buff = rx_buff.prepare(rx_buff_sz);
udp_socket.async_receive(buff, std::move(fwd));
activities_flag |= UDP_ACTIVE;
}
void upnp_actor_t::on_discovery_received(std::size_t bytes) noexcept {
spdlog::trace("upnp_actor_t::on_discovery_received ({} bytes)", bytes);
activities_flag &= ~UDP_ACTIVE;
const std::uint8_t *buff = reinterpret_cast<const std::uint8_t *>(rx_buff.data().data());
auto discovery_result = parse(buff, bytes);
if (!discovery_result) {
spdlog::warn("upnp_actor:: can't get discovery result: {}", discovery_result.error().message());
return trigger_shutdown();
}
rx_buff.consume(bytes);
discovery_option = discovery_result.value();
auto &location = discovery_option->location;
if (location.proto != "http") {
spdlog::warn("upnp_actor:: unsupported protocol ({}) for location url {}", location.proto, location.full);
return trigger_shutdown();
}
spdlog::trace("upnp_actor going to discover IGN at {}", location.full);
auto fwd = ra::forwarder_t(*this, &upnp_actor_t::on_resolve, &upnp_actor_t::on_resolve_error);
resolver.async_resolve(location.host, location.service, std::move(fwd));
activities_flag |= RESOLVER_ACTIVE;
}
void upnp_actor_t::on_resolve_error(const sys::error_code &ec) noexcept {
activities_flag &= ~RESOLVER_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("upnp_actor_t::on_resolve_error :: {}", ec.message());
return trigger_shutdown();
}
}
void upnp_actor_t::on_resolve(resolve_results_t results) noexcept {
spdlog::trace("upnp_actor_t::on_resolve");
activities_flag &= ~RESOLVER_ACTIVE;
activities_flag |= TCP_ACTIVE;
auto fwd = ra::forwarder_t(*this, &upnp_actor_t::on_connect, &upnp_actor_t::on_tcp_error);
asio::async_connect(tcp_socket, results.begin(), results.end(), std::move(fwd));
}
void upnp_actor_t::on_tcp_error(const sys::error_code &ec) noexcept {
activities_flag &= ~TCP_ACTIVE;
if (ec != asio::error::operation_aborted) {
spdlog::warn("upnp_actor_t::on_tcp_error :: {}", ec.message());
return trigger_shutdown();
}
}
void upnp_actor_t::on_connect(resolve_it_t it) noexcept {
std::stringstream str_buff;
str_buff << it->endpoint();
spdlog::trace("upnp_actor_t::on_connect ({})", str_buff.str());
auto request_result = make_description_request(tx_buff, *discovery_option);
if (!request_result) {
spdlog::warn("upnp_actor:: can't serialize description request: {}", request_result.error().message());
return trigger_shutdown();
}
spdlog::trace("upnp_actor:: making description request ({} bytes) to {} ", tx_buff.size(),
discovery_option->location.path);
auto fwd = ra::forwarder_t(*this, &upnp_actor_t::on_description_requested, &upnp_actor_t::on_tcp_error);
auto buff = asio::buffer(tx_buff.data(), tx_buff.size());
asio::async_write(tcp_socket, buff, std::move(fwd));
}
void upnp_actor_t::on_description_requested(std::size_t) noexcept {
spdlog::trace("upnp_actor_t::on_description_requested");
auto fwd = ra::forwarder_t(*this, &upnp_actor_t::on_description_received, &upnp_actor_t::on_tcp_error);
http::async_read(tcp_socket, rx_buff, responce, std::move(fwd));
}
void upnp_actor_t::on_description_received(std::size_t bytes) noexcept {
spdlog::trace("upnp_actor_t::on_description_received ({} bytes)", bytes);
trigger_shutdown();
}
+75
View File
@@ -0,0 +1,75 @@
#pragma once
#include "../configuration.h"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/smart_ptr/local_shared_ptr.hpp>
#include <boost/optional.hpp>
#include <rotor/asio/supervisor_asio.h>
#include "../utils/upnp_support.h"
namespace syncspirit {
namespace net {
namespace r = rotor;
namespace ra = rotor::asio;
namespace asio = boost::asio;
namespace sys = boost::system;
namespace pt = boost::posix_time;
namespace http = boost::beast::http;
using udp = asio::ip::udp;
using tcp = asio::ip::tcp;
struct upnp_actor_t : public r::actor_base_t {
using udp_socket_t = asio::ip::udp::socket;
using tcp_socket_t = asio::ip::tcp::socket;
using timer_t = asio::deadline_timer;
using discovery_option_t = boost::optional<utils::discovery_result>;
using resolve_results_t = asio::ip::tcp::resolver::results_type;
using resolve_it_t = asio::ip::tcp::resolver::results_type::iterator;
using http_response_t = boost::beast::http::response<boost::beast::http::string_body>;
upnp_actor_t(ra::supervisor_asio_t &sup, const config::upnp_config_t &cfg);
virtual void on_start(r::message_t<r::payload::start_actor_t> &) noexcept override;
virtual void on_shutdown(r::message_t<r::payload::shutdown_request_t> &) noexcept override;
void trigger_shutdown() noexcept;
void on_timeout_trigger() noexcept;
void on_timeout_error(const sys::error_code &ec) noexcept;
void on_discovery_sent(std::size_t bytes) noexcept;
void on_udp_error(const sys::error_code &ec) noexcept;
void on_discovery_received(std::size_t bytes) noexcept;
void on_resolve_error(const sys::error_code &ec) noexcept;
void on_resolve(resolve_results_t results) noexcept;
void on_tcp_error(const sys::error_code &ec) noexcept;
void on_connect(resolve_it_t endpoint) noexcept;
void on_description_requested(std::size_t bytes) noexcept;
void on_description_received(std::size_t bytes) noexcept;
private:
const static constexpr std::uint32_t SHUTDOWN_ACTIVE = 1 << 1;
const static constexpr std::uint32_t TIMER_ACTIVE = 1 << 2;
const static constexpr std::uint32_t UDP_ACTIVE = 1 << 3;
const static constexpr std::uint32_t TCP_ACTIVE = 1 << 4;
const static constexpr std::uint32_t RESOLVER_ACTIVE = 1 << 5;
config::upnp_config_t cfg;
asio::io_context::strand &strand;
asio::io_context &io_context;
asio::ip::tcp::resolver resolver;
udp_socket_t udp_socket;
tcp_socket_t tcp_socket;
timer_t timer;
std::uint32_t activities_flag;
fmt::memory_buffer tx_buff;
boost::beast::flat_buffer rx_buff;
discovery_option_t discovery_option;
http_response_t responce;
};
} // namespace net
} // namespace syncspirit
+33
View File
@@ -0,0 +1,33 @@
#include "error_code.h"
namespace syncspirit::utils::detail {
const char *error_code_category::name() const noexcept { return "syncspirit_error"; }
std::string error_code_category::message(int c) const {
switch (static_cast<error_code>(c)) {
case error_code::success:
return "success";
case error_code::no_location:
return "no location";
case error_code::incomplete_discovery_reply:
return "incomplete discovery reply";
case error_code::no_st:
return "no st (search target)";
case error_code::no_usn:
return "no usn";
case error_code::igd_mismatch:
return "IGD (InternetGatewayDevice) mismatch";
default:
return "unknown";
}
}
} // namespace syncspirit::utils::detail
namespace syncspirit::utils {
const static detail::error_code_category category;
const detail::error_code_category &error_code_category() { return category; }
} // namespace syncspirit::utils
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <string>
#include <system_error>
namespace syncspirit::utils {
enum class error_code {
success = 0,
incomplete_discovery_reply,
no_location,
no_st,
no_usn,
igd_mismatch,
};
namespace detail {
class error_code_category : public std::error_category {
virtual const char *name() const noexcept override;
virtual std::string message(int c) const override;
};
} // namespace detail
const detail::error_code_category &error_code_category();
inline std::error_code make_error_code(error_code e) { return {static_cast<int>(e), error_code_category()}; }
} // namespace syncspirit::utils
namespace std {
template <> struct is_error_code_enum<syncspirit::utils::error_code> : std::true_type {};
} // namespace std
+130
View File
@@ -0,0 +1,130 @@
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <string_view>
#include "error_code.h"
#include "upnp_support.h"
namespace syncspirit::utils {
const char *upnp_fields::st = "ST";
const char *upnp_fields::man = "MAN";
const char *upnp_fields::mx = "MX";
const char *upnp_fields::usn = "USN";
const char *upnp_addr = "239.255.255.250";
static const char *igd_v1_st_v = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";
static const char *igd_man_v = "\"ssdp:discover\"";
constexpr unsigned http_version = 11;
namespace http = boost::beast::http;
namespace asio = boost::asio;
namespace sys = boost::system;
outcome::result<void> make_discovery_request(fmt::memory_buffer &buff, std::uint32_t max_wait) noexcept {
std::string upnp_host = fmt::format("{}:{}", upnp_addr, upnp_port);
std::string upnp_max_wait = fmt::format("{}", max_wait);
auto req = http::request<http::empty_body>();
req.version(http_version);
req.method(http::verb::msearch);
req.target("*");
req.set(http::field::host, upnp_host.data());
req.set(upnp_fields::st, igd_v1_st_v);
req.set(upnp_fields::man, igd_man_v);
req.set(upnp_fields::mx, upnp_max_wait.data());
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
auto serializer = http::serializer<true, http::empty_body>(req);
serializer.split(false);
sys::error_code ec;
serializer.next(ec, [&](auto ec, const auto &buff_seq) {
if (!ec) {
auto sz = buffer_size(buff_seq);
buff.resize(sz);
buffer_copy(asio::mutable_buffer(buff.data(), sz), buff_seq);
}
});
if (ec) {
return ec;
};
return outcome::success();
}
outcome::result<discovery_result> parse(const std::uint8_t *data, std::size_t bytes) noexcept {
http::parser<false, http::empty_body> parser;
auto buff = asio::const_buffers_1(data, bytes);
sys::error_code ec;
parser.put(buff, ec);
if (ec) {
return ec;
}
parser.put_eof(ec);
if (ec) {
return ec;
}
if (!parser.is_done()) {
return error_code::incomplete_discovery_reply;
}
auto &message = parser.get();
auto it_location = message.find(http::field::location);
if (it_location == message.end()) {
return error_code::no_location;
}
// std::string_view location_str = it_location->value();
auto location_option = parse(it_location->value());
auto it_st = message.find(upnp_fields::st);
if (it_st == message.end()) {
return error_code::no_st;
}
auto it_usn = message.find(upnp_fields::usn);
if (it_usn == message.end()) {
return error_code::no_usn;
}
auto st = it_st->value();
if (st != igd_v1_st_v) {
return error_code::igd_mismatch;
}
auto usn = it_usn->value();
return discovery_result{
*location_option,
std::string(st.begin(), st.end()),
std::string(usn.begin(), usn.end()),
};
}
outcome::result<void> make_description_request(fmt::memory_buffer &buff, const discovery_result &dr) noexcept {
auto &location = dr.location;
http::request<http::empty_body> req;
req.method(http::verb::get);
req.version(http_version);
req.target(location.path);
req.set(http::field::host, location.host);
req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
sys::error_code ec;
auto serializer = http::serializer<true, http::empty_body>(req);
serializer.next(ec, [&](auto ec, const auto &buff_seq) {
if (!ec) {
auto sz = buffer_size(buff_seq);
buff.resize(sz);
buffer_copy(asio::mutable_buffer(buff.data(), sz), buff_seq);
}
});
if (ec) {
return ec;
};
return outcome::success();
}
} // namespace syncspirit::utils
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include "uri.h"
#include <fmt/format.h>
#include <outcome.hpp>
#include <string>
#include <vector>
namespace syncspirit::utils {
namespace outcome = OUTCOME_V2_NAMESPACE;
struct upnp_fields {
static const char *st;
static const char *man;
static const char *mx;
static const char *usn;
};
extern const char *upnp_addr;
constexpr std::uint16_t upnp_port = 1900;
struct discovery_result {
URI location;
std::string search_target;
std::string usn;
};
outcome::result<discovery_result> parse(const std::uint8_t *data, std::size_t bytes) noexcept;
outcome::result<void> make_discovery_request(fmt::memory_buffer &buff, std::uint32_t max_wait) noexcept;
outcome::result<void> make_description_request(fmt::memory_buffer &buff, const discovery_result &dr) noexcept;
} // namespace syncspirit::utils
+38
View File
@@ -0,0 +1,38 @@
#include "uri.h"
#include <charconv>
#include <regex>
namespace syncspirit::utils {
boost::optional<URI> parse(const char *uri) { return parse(boost::string_view(uri, strlen(uri))); }
boost::optional<URI> parse(const boost::string_view &uri) {
using result_t = boost::optional<URI>;
std::regex re("(\\w+)://([^/ :]+):?([^/ ]*)(/?[^ #?]*)\\x3f?([^ #]*)#?([^ ]*)");
std::cmatch what;
if (regex_match(uri.begin(), uri.end(), what, re)) {
auto proto = std::string(what[1].first, what[1].length());
int port = 0;
std::from_chars(what[3].first, what[3].second, port);
if (!port) {
if (proto == "http") {
port = 80;
} else if (proto == "https") {
port = 443;
}
}
return result_t{URI{
std::string(uri.begin(), uri.end()), std::string(what[2].first, what[2].length()), // host
static_cast<std::uint16_t>(port), // port
proto, // proto
std::string(what[3].first, what[3].length()), // service
std::string(what[4].first, what[4].length()), // path
std::string(what[5].first, what[5].length()), // query
std::string(what[6].first, what[6].length()), // fragment
}};
}
return result_t{};
}
}; // namespace syncspirit::utils
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <boost/optional.hpp>
#include <boost/utility/string_view.hpp>
#include <string>
namespace syncspirit::utils {
struct URI {
std::string full;
std::string host;
std::uint16_t port;
std::string proto;
std::string service;
std::string path;
std::string query;
std::string fragment;
};
boost::optional<URI> parse(const char *uri);
boost::optional<URI> parse(const boost::string_view &uri);
} // namespace syncspirit::utils
+24
View File
@@ -0,0 +1,24 @@
[sink_console]
level = trace
# pattern = [%^%l%$] %v
[sink_file]
file = syncspirit.log
level = info
[logging]
sinks = console,file
[local_announce]
enabled = 1
port = 21027
[global_discovery]
server = https://discovery.syncthing.net/v2/?noannounce&id=LYXKCHX-VI3NYZR-ALCJBHF-WMZYSPK-QG6QJA3-MPFYMSO-U56GTUK-NA2MIAW
cert_file = /home/b/.config/syncthing/https-cert.pem
key_file = /home/b/.config/syncthing/https-key.pem
timeout = 5
[upnp]
timeout = 10