code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022-2025 Ivan Baidakou

#include "relay_support.h"
#include "utils/error_code.h"
#include <nlohmann/json.hpp>
#include <boost/endian/arithmetic.hpp>
#include <boost/endian/conversion.hpp>
#include <ares.h>
#include <cstring>

namespace be = boost::endian;
using json = nlohmann::json;

template <class> inline constexpr bool always_false_v = false;

namespace syncspirit::proto::relay {

struct header_t {
    uint32_t magic;
    uint32_t type;
    uint32_t length;
};

enum class type_t {
    ping = 0,
    pong = 1,
    join_relay_request = 2,
    join_session_request = 3,
    response = 4,
    connect_request = 5,
    session_invitation = 6,
    LAST = session_invitation,
};

static constexpr auto header_sz = sizeof(header_t);
static constexpr auto max_packet_sz = size_t{1400};
static constexpr uint32_t magic = 0x9E79BC40;

static pt::time_duration parse_interval(const std::string_view in) noexcept {
    auto ptr = in.data();
    auto end = in.data() + in.size();
    auto s = ptr;
    auto e = ptr;
    while (ptr < end && *ptr != 'm') {
        ++ptr;
    }
    if (*ptr == 'm') {
        e = ptr++;
    };
    int mins = 0;
    while (s < e) {
        mins *= 10;
        mins += *s - '0';
        ++s;
    }

    s = e = ptr;
    while (ptr < end && *ptr != 's') {
        ++ptr;
    }
    if (*ptr == 's') {
        e = ptr;
    };

    int secs = 0;
    while (s < e) {
        secs *= 10;
        secs += *s - '0';
        ++s;
    }

    if (mins == 0 && secs == 0) {
        mins = 1;
    }

    return pt::minutes{mins} + pt::seconds{secs};
}

static void serialize_header(unsigned char *ptr, type_t type, size_t payload_sz) noexcept {
    auto h = header_t{be::native_to_big(magic), be::native_to_big(static_cast<uint32_t>(type)),
                      be::native_to_big(static_cast<uint32_t>(payload_sz))};
    auto in = reinterpret_cast<const char *>(&h);
    std::copy(in, in + header_sz, ptr);
}

static inline void write32_be(void *ptr, std::uint32_t value) {
    auto v = be::native_to_big(value);
    std::memcpy(ptr, &v, sizeof(value));
}

static inline std::uint32_t read32_be(const void *ptr) {
    std::uint32_t value;
    std::memcpy(&value, ptr, sizeof(value));
    return be::big_to_native(value);
}

size_t serialize(const message_t &msg, utils::bytes_t &out) noexcept {
    return std::visit(
        [&](auto &it) -> size_t {
            using T = std::decay_t<decltype(it)>;
            if constexpr (std::is_same_v<T, ping_t>) {
                out.resize(header_sz);
                serialize_header(out.data(), type_t::ping, 0);
                return header_sz;
            } else if constexpr (std::is_same_v<T, pong_t>) {
                out.resize(header_sz);
                serialize_header(out.data(), type_t::pong, 0);
                return header_sz;
            } else if constexpr (std::is_same_v<T, join_relay_request_t>) {
                out.resize(header_sz);
                serialize_header(out.data(), type_t::join_relay_request, 0);
                return header_sz;
            } else if constexpr (std::is_same_v<T, join_session_request_t>) {
                auto key_sz = it.key.size();
                auto payload_sz = sizeof(uint32_t) + key_sz;
                auto sz = header_sz + payload_sz;
                out.resize(sz);
                auto ptr = out.data();
                serialize_header(ptr, type_t::join_session_request, payload_sz);
                ptr += header_sz;
                *(reinterpret_cast<uint32_t *>(ptr)) = be::native_to_big((uint32_t)key_sz);
                ptr += sizeof(uint32_t);
                std::copy(it.key.begin(), it.key.end(), ptr);
                return sz;
            } else if constexpr (std::is_same_v<T, response_t>) {
                auto msg_sz = it.details.size();
                auto payload_sz = sizeof(uint32_t) + sizeof(uint32_t) + msg_sz;
                auto sz = header_sz + payload_sz;
                out.resize(sz);
                auto ptr = out.data();
                serialize_header(ptr, type_t::response, payload_sz);
                ptr += header_sz;
                *(reinterpret_cast<std::int32_t *>(ptr)) = be::native_to_big(it.code);
                ptr += sizeof(int32_t);
                *(reinterpret_cast<uint32_t *>(ptr)) = be::native_to_big((uint32_t)msg_sz);
                ptr += sizeof(uint32_t);
                std::copy(it.details.begin(), it.details.end(), ptr);
                return sz;
            } else if constexpr (std::is_same_v<T, connect_request_t>) {
                auto id_sz = it.device_id.size();
                auto payload_sz = sizeof(uint32_t) + id_sz;
                auto sz = header_sz + payload_sz;
                out.resize(sz);
                auto ptr = out.data();
                serialize_header(ptr, type_t::connect_request, payload_sz);
                ptr += header_sz;
                *(reinterpret_cast<uint32_t *>(ptr)) = be::native_to_big((uint32_t)id_sz);
                ptr += sizeof(uint32_t);
                std::copy(it.device_id.begin(), it.device_id.end(), ptr);
                return sz;
            } else if constexpr (std::is_same_v<T, session_invitation_t>) {
                auto &from = it.from;
                auto &key = it.key;
                auto address_raw = std::uint32_t{0};
                if (it.address.has_value()) {
                    address_raw = be::native_to_big(it.address->to_uint());
                }
                auto address = std::string_view(reinterpret_cast<char *>(&address_raw), 4);
                auto payload_sz = sizeof(uint32_t) * 6 + from.size() + key.size() + address.size();
                auto sz = header_sz + payload_sz;
                out.resize(sz);
                auto ptr = out.data();
                serialize_header(ptr, type_t::session_invitation, payload_sz);
                ptr += header_sz;
                write32_be(ptr, (uint32_t)from.size());
                ptr += sizeof(uint32_t);
                std::copy(from.begin(), from.end(), ptr);
                ptr += from.size();

                write32_be(ptr, (uint32_t)key.size());
                ptr += sizeof(uint32_t);
                std::copy(key.begin(), key.end(), ptr);
                ptr += key.size();

                write32_be(ptr, (uint32_t)address.size());
                ptr += sizeof(uint32_t);
                std::copy(address.begin(), address.end(), ptr);
                ptr += address.size();

                write32_be(ptr, (uint32_t)it.port);
                ptr += sizeof(uint32_t);

                write32_be(ptr, (uint32_t)it.server_socket);
                return sz;
            } else {
                static_assert(always_false_v<T>, "non-exhaustive visitor!");
            }
        },
        msg);
}

static parse_result_t parse_ping(utils::bytes_view_t) noexcept { return wrapped_message_t{header_sz, ping_t{}}; }

static parse_result_t parse_pong(utils::bytes_view_t) noexcept { return wrapped_message_t{header_sz, pong_t{}}; }

static parse_result_t parse_join_relay_request(utils::bytes_view_t) noexcept {
    return wrapped_message_t{header_sz, join_relay_request_t{}};
}

static parse_result_t parse_join_session_request(utils::bytes_view_t data) noexcept {
    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto ptr = data.data();
    auto sz = be::big_to_native(*reinterpret_cast<const uint32_t *>(ptr));
    if (sz + sizeof(uint32_t) > data.size()) {
        return protocol_error_t{};
    }
    auto tail = data.subspan(sizeof(uint32_t));
    auto tail_bytes = utils::bytes_t(tail.begin(), tail.end());
    return wrapped_message_t{header_sz + data.size(), join_session_request_t{std::move(tail_bytes)}};
}

static parse_result_t parse_response(utils::bytes_view_t data) noexcept {
    if (data.size() < sizeof(uint32_t) * 2) {
        return protocol_error_t{};
    }
    auto ptr = data.data();
    auto code = be::big_to_native(*reinterpret_cast<const uint32_t *>(ptr));
    ptr += sizeof(uint32_t);
    auto sz = be::big_to_native(*reinterpret_cast<const uint32_t *>(ptr));
    if (sz + sizeof(uint32_t) * 2 > data.size()) {
        return protocol_error_t{};
    }
    auto tail = data.subspan(sizeof(uint32_t) * 2, sz);
    auto tail_str = std::string_view((const char *)tail.data(), tail.size());
    return wrapped_message_t{header_sz + data.size(), response_t{code, std::string(tail_str)}};
}

static parse_result_t parse_connect_request(utils::bytes_view_t data) noexcept {
    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto ptr = data.data();
    auto sz = be::big_to_native(*reinterpret_cast<const uint32_t *>(ptr));
    if (sz + sizeof(uint32_t) != data.size()) {
        return protocol_error_t{};
    }
    auto tail = data.subspan(sizeof(uint32_t));
    auto tail_bytes = utils::bytes_t(tail.begin(), tail.end());
    return wrapped_message_t{header_sz + data.size(), connect_request_t{std::move(tail_bytes)}};
}

static parse_result_t parse_session_invitation(utils::bytes_view_t data) noexcept {
    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto orig = data;

    auto from_sz = read32_be(data.data());
    data = data.subspan(sizeof(uint32_t));
    if (data.size() < from_sz) {
        return protocol_error_t{};
    }
    auto from = data.subspan(0, from_sz);
    data = data.subspan(from_sz);

    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto key_sz = read32_be(data.data());
    data = data.subspan(sizeof(uint32_t));
    if (data.size() < key_sz) {
        return protocol_error_t{};
    }
    auto key = data.subspan(0, key_sz);
    data = data.subspan(key_sz);

    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto addr_sz = read32_be(data.data());
    data = data.subspan(sizeof(uint32_t));
    if (data.size() < addr_sz) {
        return protocol_error_t{};
    }
    auto addr = data.subspan(0, addr_sz);
    data = data.subspan(addr_sz);
    auto ip = ipv4_option_t{};
    if (addr_sz == 4) {
        auto number = std::uint32_t{0};
        std::memcpy(&number, addr.data(), addr_sz);
        if (number) {
            ip = boost::asio::ip::address_v4(be::big_to_native(number));
        }
    }
    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto port = read32_be(data.data());
    data = data.subspan(sizeof(uint32_t));

    if (data.size() < sizeof(uint32_t)) {
        return protocol_error_t{};
    }
    auto server_socket = read32_be(data.data());
    data = data.subspan(sizeof(uint32_t));

    if (!addr.empty() && !addr[0]) {
        addr = {};
    };

    auto from_bytes = utils::bytes_t(from.begin(), from.end());
    auto key_bytes = utils::bytes_t(key.begin(), key.end());
    return wrapped_message_t{header_sz + orig.size(), session_invitation_t{std::move(from_bytes), std::move(key_bytes),
                                                                           ip, port, (bool)server_socket}};
}

parse_result_t parse(utils::bytes_view_t data) noexcept {
    if (data.size() < header_sz) {
        return incomplete_t{};
    }
    auto ptr = data.data();
    auto header = reinterpret_cast<const header_t *>(ptr);
    if (be::big_to_native(header->magic) != magic) {
        return protocol_error_t{};
    }
    auto type = be::big_to_native(header->type);
    if (type > (uint32_t)type_t::LAST) {
        return protocol_error_t{};
    }
    auto sz = be::big_to_native(header->length);
    if (sz > max_packet_sz) {
        return protocol_error_t{};
    }
    if (data.size() < header_sz + sz) {
        return incomplete_t{};
    }
    auto tail = data.subspan(header_sz);

    switch ((type_t)type) {
    case type_t::ping:
        return parse_ping(tail);
    case type_t::pong:
        return parse_pong(tail);
    case type_t::join_relay_request:
        return parse_join_relay_request(tail);
    case type_t::join_session_request:
        return parse_join_session_request(tail);
    case type_t::response:
        return parse_response(tail);
    case type_t::connect_request:
        return parse_connect_request(tail);
    case type_t::session_invitation:
        return parse_session_invitation(tail);
    default:
        return protocol_error_t{};
    }
}

std::optional<model::device_id_t> parse_device(const utils::uri_ptr_t &uri) noexcept {
    if (uri->scheme() != "relay") {
        return {};
    }
    auto device_id_str = std::string{};
    auto params = uri->params();
    for (auto p : params) {
        if (p.key == "id") {
            device_id_str = p.value;
            break;
        }
    }
    if (device_id_str.empty()) {
        return {};
    }
    auto device_opt = model::device_id_t::from_string(device_id_str);
    if (!device_opt) {
        return {};
    }
    return std::move(device_opt.value());
}

outcome::result<relay_infos_t> parse_endpoint(std::string_view buff) noexcept {
    using namespace syncspirit::utils;
    auto data = json::parse(buff.begin(), buff.end(), nullptr, false);
    if (data.is_discarded()) {
        return make_error_code(error_code_t::malformed_json);
    }
    if (!data.is_object()) {
        return make_error_code(error_code_t::incorrect_json);
    }

    auto &relays = data["relays"];
    if (!relays.is_array()) {
        return make_error_code(error_code_t::incorrect_json);
    }

    auto r = relay_infos_t{};
    for (auto &it : relays) {
        if (!it.is_object()) {
            continue;
            return make_error_code(error_code_t::incorrect_json);
        }
        auto &url = it["url"];
        if (!url.is_string()) {
            continue;
        }
        auto uri_str = url.get<std::string>();
        auto uri = utils::parse(uri_str.c_str());
        if (!uri) {
            continue;
        }
        if (uri->scheme() != "relay") {
            continue;
        }
        auto device_id_str = std::string{};
        auto ping_interval_str = std::string{};
        auto params = uri->params();
        for (auto p : params) {
            if (p.key == "id") {
                device_id_str = p.value;
            } else if (p.key == "pingInterval") {
                ping_interval_str = p.value;
            }
        }
        if (device_id_str.empty()) {
            continue;
        }
        auto device_id = model::device_id_t::from_string(device_id_str);
        if (!device_id) {
            continue;
        }

        auto ping_interval = parse_interval(ping_interval_str);

        auto location = location_t{};
        auto &j_location = it["location"];
        if (j_location.is_object()) {
            auto &latitude = j_location["latitude"];
            if (latitude.is_number_float()) {
                location.latitude = latitude.get<float>();
            }
            auto &longitude = j_location["longitude"];
            if (longitude.is_number_float()) {
                location.longitude = longitude.get<float>();
            }
            auto &city = j_location["city"];
            if (city.is_string()) {
                location.city = city.get<std::string>();
            }
            auto &country = j_location["country"];
            if (country.is_string()) {
                location.country = country.get<std::string>();
            }
            auto &continent = j_location["continent"];
            if (continent.is_string()) {
                location.continent = continent.get<std::string>();
            }
        }
        auto relay =
            relay_info_ptr_t{new relay_info_t{std::move(uri), device_id.value(), std::move(location), ping_interval}};
        r.emplace_back(std::move(relay));
    }
    return r;
}

} // namespace syncspirit::proto::relay


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2022-2025 Ivan Baidakou

#pragma once

#include "model/misc/arc.hpp"
#include "model/device_id.h"
#include "utils/uri.h"
#include "syncspirit-export.h"

#include <string>
#include <cstdint>
#include <variant>
#include <vector>
#include <optional>

#include <boost/asio/ip/address_v4.hpp>
#include <boost/outcome.hpp>
#include <boost/date_time/posix_time/posix_time.hpp>

namespace syncspirit::proto::relay {

namespace outcome = boost::outcome_v2;
namespace pt = boost::posix_time;

using ipv4_option_t = std::optional<boost::asio::ip::address_v4>;

struct ping_t {};

struct pong_t {};

struct join_relay_request_t {};

struct join_session_request_t {
    utils::bytes_t key;
};

struct response_t {
    std::uint32_t code;
    std::string details;
};

struct connect_request_t {
    utils::bytes_t device_id;
};

struct session_invitation_t {
    utils::bytes_t from;
    utils::bytes_t key;
    ipv4_option_t address;
    std::uint32_t port;
    bool server_socket;
};

using message_t = std::variant<ping_t, pong_t, join_relay_request_t, join_session_request_t, response_t,
                               connect_request_t, session_invitation_t>;

// support
struct incomplete_t {};
struct protocol_error_t {};
struct wrapped_message_t {
    size_t length;
    message_t message;
};

using parse_result_t = std::variant<incomplete_t, protocol_error_t, wrapped_message_t>;

SYNCSPIRIT_API size_t serialize(const relay::message_t &, utils::bytes_t &out) noexcept;
SYNCSPIRIT_API parse_result_t parse(utils::bytes_view_t data) noexcept;

struct location_t {
    float latitude;
    float longitude;
    std::string city;
    std::string country;
    std::string continent;
};

struct relay_info_t : model::arc_base_t<relay_info_t> {
    inline relay_info_t(utils::uri_ptr_t uri_, const model::device_id_t &device_id_, location_t location_,
                        const pt::time_duration &ping_interval_) noexcept
        : uri(std::move(uri_)), device_id{device_id_}, location{std::move(location_)}, ping_interval{ping_interval_} {}
    utils::uri_ptr_t uri;
    model::device_id_t device_id;
    location_t location;
    pt::time_duration ping_interval;
};

using relay_info_ptr_t = model::intrusive_ptr_t<relay_info_t>;
using relay_infos_t = std::vector<relay_info_ptr_t>;

SYNCSPIRIT_API std::optional<model::device_id_t> parse_device(const utils::uri_ptr_t &uri) noexcept;
SYNCSPIRIT_API outcome::result<relay_infos_t> parse_endpoint(std::string_view data) noexcept;

} // namespace syncspirit::proto::relay

repo_name: basiliscos/syncspirit

license: mit


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include <boost/beast/http.hpp>
#include <pugixml.hpp>
#include <spdlog/spdlog.h>
#include "upnp_support.h"
#include "utils/error_code.h"
#include "utils/beast_support.h"

using namespace syncspirit::utils;
using namespace syncspirit::model;

namespace syncspirit::proto {

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\"";
static const char *igd_wan_xpath = "//service[../../deviceType = 'urn:schemas-upnp-org:device:WANConnectionDevice:1' "
                                   "and serviceType = 'urn:schemas-upnp-org:service:WANIPConnection:1']";
static const char *igd_wan_service = "urn:schemas-upnp-org:service:WANIPConnection:1";
static const char *soap_GetExternalIPAddress = "GetExternalIPAddress";
static const char *soap_AddPortMapping = "AddPortMapping";
static const char *soap_DeletePortMapping = "DeletePortMapping";
static const char *soap_GetSpecificPortMappingEntry = "GetSpecificPortMappingEntry";
static const char *external_ip_xpath = "//NewExternalIPAddress";
static const char *port_mapping_success_xpath = "//*[local-name() = 'AddPortMappingResponse']";
static const char *port_unmapping_success_xpath = "//*[local-name() = 'DeletePortMappingResponse']";
static const char *port_mapping_validation_success_xpath = "//*[local-name() = 'GetSpecificPortMappingEntryResponse']";

constexpr unsigned http_version = 11;

namespace http = boost::beast::http;
namespace asio = boost::asio;
namespace sys = boost::system;

outcome::result<void> make_discovery_request(utils::bytes_t &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());

    return serialize(req, buff);
}

outcome::result<discovery_result> parse(const char *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_t::incomplete_discovery_reply;
    }

    auto &message = parser.get();
    auto it_location = message.find(http::field::location);
    if (it_location == message.end()) {
        return error_code_t::no_location;
    }
    auto location_option = utils::parse(it_location->value());

    auto it_st = message.find(upnp_fields::st);
    if (it_st == message.end()) {
        return error_code_t::no_st;
    }

    auto it_usn = message.find(upnp_fields::usn);
    if (it_usn == message.end()) {
        return error_code_t::no_usn;
    }
    auto st = it_st->value();
    if (st != igd_v1_st_v) {
        std::string v((const char *)st.data(), st.size());
        spdlog::warn("upnp_support, igd version {}", v);
        return error_code_t::igd_mismatch;
    }

    auto usn = it_usn->value();
    return discovery_result{
        std::move(location_option),
        std::string(st.begin(), st.end()),
        std::string(usn.begin(), usn.end()),
    };
}

outcome::result<void> make_description_request(utils::bytes_t &buff, const uri_ptr_t &uri) noexcept {
    http::request<http::empty_body> req;
    req.method(http::verb::get);
    req.version(http_version);
    req.target(uri->encoded_target());
    req.set(http::field::host, uri->host());
    req.set(http::field::connection, "close");

    return serialize(req, buff);
}

outcome::result<igd_result> parse_igd(const char *data, std::size_t bytes) noexcept {
    pugi::xml_document doc;
    auto result = doc.load_buffer(data, bytes);
    if (!result) {
        return error_code_t::xml_parse_error;
    }
    auto node = doc.select_node(igd_wan_xpath);
    if (!node) {
        return error_code_t::wan_notfound;
    }

    auto control_url = node.node().child_value("controlURL");
    auto description_url = node.node().child_value("SCPDURL");
    if (control_url && description_url) {
        return igd_result{control_url, description_url};
    }
    return error_code_t::wan_notfound;
}

outcome::result<void> make_external_ip_request(utils::bytes_t &buff, const uri_ptr_t &uri) noexcept {
    http::request<http::string_body> req;
    std::string soap_action = fmt::format("\"{0}#{1}\"", igd_wan_service, soap_GetExternalIPAddress);
    req.method(http::verb::post);
    req.version(http_version);
    req.target(uri->encoded_target());
    req.set(http::field::connection, "close");
    req.set(http::field::host, uri->host());
    req.set(http::field::soapaction, soap_action);
    req.set(http::field::pragma, "no-cache");
    req.set(http::field::cache_control, "no-cache");
    req.set(http::field::content_type, "text/xml");

    std::string body = fmt::format("<?xml version='1.0'?>"
                                   "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' "
                                   "s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
                                   "<s:Body><u:{0} xmlns:u='{1}'></u:{0}></s:Body></s:Envelope>",
                                   soap_GetExternalIPAddress, igd_wan_service);
    req.body() = body;
    req.prepare_payload();
    return serialize(req, buff);
}

outcome::result<std::string> parse_external_ip(const char *data, std::size_t bytes) noexcept {
    pugi::xml_document doc;
    auto result = doc.load_buffer(data, bytes);
    if (!result) {
        return error_code_t::xml_parse_error;
    }
    auto node = doc.select_node(external_ip_xpath);
    if (!node) {
        return error_code_t::xml_parse_error;
    }

    return node.node().child_value();
}

outcome::result<void> make_mapping_request(utils::bytes_t &buff, const uri_ptr_t &uri, std::uint16_t external_port,
                                           const std::string &internal_ip, std::uint16_t internal_port) noexcept {
    http::request<http::string_body> req;
    std::string soap_action = fmt::format("\"{0}#{1}\"", igd_wan_service, soap_AddPortMapping);
    req.method(http::verb::post);
    req.version(http_version);
    req.target(uri->encoded_target());
    req.set(http::field::host, uri->host());
    req.set(http::field::connection, "close");
    req.set(http::field::soapaction, soap_action);
    req.set(http::field::pragma, "no-cache");
    req.set(http::field::cache_control, "no-cache");
    req.set(http::field::content_type, "text/xml");

    std::string body = fmt::format("<?xml version='1.0'?>"
                                   "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' "
                                   "s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
                                   "<s:Body><u:{0} xmlns:u='{1}'>"
                                   "<NewRemoteHost></NewRemoteHost>"
                                   "<NewExternalPort>{2}</NewExternalPort>"
                                   "<NewProtocol>TCP</NewProtocol>"
                                   "<NewInternalPort>{3}</NewInternalPort>"
                                   "<NewInternalClient>{4}</NewInternalClient>"
                                   "<NewEnabled>1</NewEnabled>"
                                   "<NewPortMappingDescription>syncspirit</NewPortMappingDescription>"
                                   "<NewLeaseDuration>0</NewLeaseDuration>"
                                   "</u:{0}></s:Body></s:Envelope>",
                                   soap_AddPortMapping, igd_wan_service, external_port, internal_port, internal_ip);
    req.body() = body;
    req.prepare_payload();

    return serialize(req, buff);
}

outcome::result<void> make_unmapping_request(utils::bytes_t &buff, const uri_ptr_t &uri,
                                             std::uint16_t external_port) noexcept {
    http::request<http::string_body> req;
    std::string soap_action = fmt::format("\"{0}#{1}\"", igd_wan_service, soap_DeletePortMapping);
    req.method(http::verb::post);
    req.version(http_version);
    req.target(uri->encoded_target());
    req.set(http::field::host, uri->host());
    req.set(http::field::connection, "close");
    req.set(http::field::soapaction, soap_action);
    req.set(http::field::pragma, "no-cache");
    req.set(http::field::cache_control, "no-cache");
    req.set(http::field::content_type, "text/xml");

    std::string body = fmt::format("<?xml version='1.0'?>"
                                   "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' "
                                   "s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
                                   "<s:Body><u:{0} xmlns:u='{1}'>"
                                   "<NewRemoteHost></NewRemoteHost>"
                                   "<NewExternalPort>{2}</NewExternalPort>"
                                   "<NewProtocol>TCP</NewProtocol>"
                                   "</u:{0}></s:Body></s:Envelope>",
                                   soap_DeletePortMapping, igd_wan_service, external_port);
    req.body() = body;
    req.prepare_payload();

    return serialize(req, buff);
}

outcome::result<void> make_mapping_validation_request(utils::bytes_t &buff, const uri_ptr_t &uri,
                                                      std::uint16_t external_port) noexcept {
    http::request<http::string_body> req;
    std::string soap_action = fmt::format("\"{0}#{1}\"", igd_wan_service, soap_GetSpecificPortMappingEntry);
    req.method(http::verb::post);
    req.version(http_version);
    req.target(uri->encoded_target());
    req.set(http::field::host, uri->host());
    req.set(http::field::connection, "close");
    req.set(http::field::soapaction, soap_action);
    req.set(http::field::pragma, "no-cache");
    req.set(http::field::cache_control, "no-cache");
    req.set(http::field::content_type, "text/xml");

    std::string body = fmt::format("<?xml version='1.0'?>"
                                   "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' "
                                   "s:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
                                   "<s:Body><u:{0} xmlns:u='{1}'>"
                                   "<NewRemoteHost></NewRemoteHost>"
                                   "<NewExternalPort>{2}</NewExternalPort>"
                                   "<NewProtocol>TCP</NewProtocol>"
                                   "</u:{0}></s:Body></s:Envelope>",
                                   soap_GetSpecificPortMappingEntry, igd_wan_service, external_port);
    req.body() = body;
    req.prepare_payload();

    return serialize(req, buff);
}

outcome::result<bool> parse_mapping(const char *data, std::size_t bytes) noexcept {
    pugi::xml_document doc;
    auto result = doc.load_buffer(data, bytes);
    if (!result) {
        return error_code_t::xml_parse_error;
    }
    auto node = doc.select_node(port_mapping_success_xpath);
    return static_cast<bool>(node);
}

outcome::result<bool> parse_unmapping(const char *data, std::size_t bytes) noexcept {
    pugi::xml_document doc;
    auto result = doc.load_buffer(data, bytes);
    if (!result) {
        return error_code_t::xml_parse_error;
    }
    auto node = doc.select_node(port_unmapping_success_xpath);
    return static_cast<bool>(node);
}

outcome::result<bool> parse_mapping_validation(const char *data, std::size_t bytes) noexcept {
    pugi::xml_document doc;
    auto result = doc.load_buffer(data, bytes);
    if (!result) {
        return error_code_t::xml_parse_error;
    }
    auto node = doc.select_node(port_mapping_validation_success_xpath);
    return static_cast<bool>(node);
}

} // namespace syncspirit::proto


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once
#include "utils/bytes.h"
#include "utils/uri.h"
#include "model/misc/upnp.h"
#include "syncspirit-export.h"
#include <string>
#include <boost/outcome.hpp>

namespace syncspirit::proto {

namespace outcome = boost::outcome_v2;

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;

SYNCSPIRIT_API outcome::result<model::discovery_result> parse(const char *data, std::size_t bytes) noexcept;

SYNCSPIRIT_API outcome::result<void> make_discovery_request(utils::bytes_t &buff, std::uint32_t max_wait) noexcept;

SYNCSPIRIT_API outcome::result<void> make_description_request(utils::bytes_t &buff,
                                                              const utils::uri_ptr_t &uri) noexcept;

SYNCSPIRIT_API outcome::result<model::igd_result> parse_igd(const char *data, std::size_t bytes) noexcept;

SYNCSPIRIT_API outcome::result<void> make_external_ip_request(utils::bytes_t &buff,
                                                              const utils::uri_ptr_t &uri) noexcept;

SYNCSPIRIT_API outcome::result<std::string> parse_external_ip(const char *data, std::size_t bytes) noexcept;

SYNCSPIRIT_API outcome::result<void> make_mapping_request(utils::bytes_t &buff, const utils::uri_ptr_t &uri,
                                                          std::uint16_t external_port, const std::string &internal_ip,
                                                          std::uint16_t internal_port) noexcept;

SYNCSPIRIT_API outcome::result<void> make_unmapping_request(utils::bytes_t &buff, const utils::uri_ptr_t &uri,
                                                            std::uint16_t external_port) noexcept;

SYNCSPIRIT_API outcome::result<void> make_mapping_validation_request(utils::bytes_t &buff, const utils::uri_ptr_t &uri,
                                                                     std::uint16_t external_port) noexcept;

SYNCSPIRIT_API outcome::result<bool> parse_mapping(const char *data, std::size_t bytes) noexcept;

SYNCSPIRIT_API outcome::result<bool> parse_unmapping(const char *data, std::size_t bytes) noexcept;

SYNCSPIRIT_API outcome::result<bool> parse_mapping_validation(const char *data, std::size_t bytes) noexcept;

} // namespace syncspirit::proto


code:
syntax = "proto3";

package syncspirit.proto;

message Announce {
    bytes           id          = 1;
    repeated string addresses   = 2;
    int64           instance_id = 3;
}

// --- Pre-auth ---

message Hello {
    string device_name    = 1;
    string client_name    = 2;
    string client_version = 3;
}

// --- Header ---

message Header {
    MessageType        type        = 1;
    MessageCompression compression = 2;
}

enum MessageType {
    CLUSTER_CONFIG    = 0;
    INDEX             = 1;
    INDEX_UPDATE      = 2;
    REQUEST           = 3;
    RESPONSE          = 4;
    DOWNLOAD_PROGRESS = 5;
    PING              = 6;
    CLOSE             = 7;
}

enum MessageCompression {
    NONE = 0;
    LZ4  = 1;
}

// --- Actual messages ---

// Cluster Config

message ClusterConfig {
    repeated Folder folders = 1;
}

message Folder {
    string id                   = 1;
    string label                = 2;
    bool   read_only            = 3;
    bool   ignore_permissions   = 4;
    bool   ignore_delete        = 5;
    bool   disable_temp_indexes = 6;
    bool   paused               = 7;

    repeated Device devices = 16;
}

message Device {
    bytes           id                         = 1;
    string          name                       = 2;
    repeated string addresses                  = 3;
    Compression     compression                = 4;
    string          cert_name                  = 5;
    int64           max_sequence               = 6;
    bool            introducer                 = 7;
    uint64          index_id                   = 8;
    bool            skip_introduction_removals = 9;
}

enum Compression {
    METADATA = 0;
    NEVER    = 1;
    ALWAYS   = 2;
}

// Index and Index Update

message Index {
    string            folder = 1;
    repeated FileInfo files  = 2;
}

message IndexUpdate {
    string            folder = 1;
    repeated FileInfo files  = 2;
}

message FileInfo {
    //option (gogoproto.goproto_stringer) = false;
    string             name           = 1;
    FileInfoType       type           = 2;
    int64              size           = 3;
    uint32             permissions    = 4;
    int64              modified_s     = 5;
    int32              modified_ns    = 11;
    uint64             modified_by    = 12;
    bool               deleted        = 6;
    bool               invalid        = 7;
    bool               no_permissions = 8;
    Vector             version        = 9;
    int64              sequence       = 10;
    int32              block_size     = 13;
    repeated BlockInfo Blocks         = 16;
    string             symlink_target = 17;

    // The local_flags fields stores flags that are relevant to the local
    // host only. It is not part of the protocol, doesn't get sent or
    // received (we make sure to zero it), nonetheless we need it on our
    // struct and to be able to serialize it to/from the database.
    uint32 local_flags = 1000;
}

enum FileInfoType {
    FILE              = 0;
    DIRECTORY         = 1;
    SYMLINK_FILE      = 2;
    SYMLINK_DIRECTORY = 3;
    SYMLINK           = 4;
}

message BlockInfo {
    //option (gogoproto.goproto_stringer) = false;
    int64  offset    = 1;
    int32  size      = 2;
    bytes  hash      = 3;
    uint32 weak_hash = 4;
}

message Vector {
    repeated Counter counters = 1;
}

message Counter {
    uint64 id    = 1;
    uint64 value = 2;
}

// Request

message Request {
    int32  id             = 1;
    string folder         = 2;
    string name           = 3;
    int64  offset         = 4;
    int32  size           = 5;
    bytes  hash           = 6;
    bool   from_temporary = 7;
    uint32 weak_hash      = 8;
}

// Response

message Response {
    int32     id   = 1;
    bytes     data = 2;
    ErrorCode code = 3;
}

enum ErrorCode {
    NO_BEP_ERROR = 0;
    GENERIC      = 1;
    NO_SUCH_FILE = 2;
    INVALID_FILE = 3;
}

// DownloadProgress

message DownloadProgress {
    string                              folder  = 1;
    repeated FileDownloadProgressUpdate updates = 2;
}

message FileDownloadProgressUpdate {
    FileDownloadProgressUpdateType update_type   = 1;
    string                         name          = 2;
    Vector                         version       = 3;
    repeated int32                 block_indexes = 4 [packed=false];
}

enum FileDownloadProgressUpdateType {
    APPEND = 0;
    FORGET = 1;
}

// Ping

message Ping {
}

// Close

message Close {
    string reason = 1;
}


code:
syntax = "proto3";

package syncspirit.db;

import "bep.proto";

message Device {
    string          name                        = 2;
    repeated string addresses                   = 3;
    syncspirit.proto.Compression    compression = 4;
    string          cert_name                   = 5;
    bool            introducer                  = 6;
    bool            skip_introduction_removals  = 7;
    bool            auto_accept                 = 8;
    bool            paused                      = 9;
    int64           last_seen                   = 10;
}

message IgnoredFolder {
    string id    = 1;
    string label = 2;
}

message Folder {
    string      id                       = 1;
    string      label                    = 2;
    bool        read_only                = 3;
    bool        ignore_permissions       = 4;
    bool        ignore_delete            = 5;
    bool        disable_temp_indexes     = 6;
    bool        paused                   = 7;
    bool        scheduled                = 8;
    string      path                     = 9;
    FolderType  folder_type              = 10;
    PullOrder   pull_order               = 11;
    uint32      rescan_interval          = 12;
}

enum FolderType {
    send             = 0;
    receive          = 1;
    send_and_receive = 2;
}

enum PullOrder {
    random      = 0;
    alphabetic  = 1;
    smallest    = 2;
    largest     = 3;
    oldest      = 4;
    newest      = 5;
}

message FolderInfo {
    uint64 index_id     = 1;
    int64  max_sequence = 2;
}

message PendingFolder {
    Folder folder          = 1;
    FolderInfo folder_info = 2;
}

message FileInfo {
    string                          name           = 1;
    syncspirit.proto.FileInfoType   type           = 2;
    int64                           size           = 3;
    uint32                          permissions    = 4;
    int64                           modified_s     = 5;
    int32                           modified_ns    = 11;
    uint64                          modified_by    = 12;
    bool                            deleted        = 6;
    bool                            invalid        = 7;
    bool                            no_permissions = 8;
    syncspirit.proto.Vector         version        = 9;
    int64                           sequence       = 10;
    int32                           block_size     = 13;
    string                          symlink_target = 16;
    repeated                        bytes blocks   = 17;
}

message IngoredFolder {
    string label = 1;
}

message BlockInfo {
    uint32          weak_hash   = 1;
    int32           size        = 2;
}

message SomeDevice {
    string name           = 1;
    string client_name    = 2;
    string client_version = 3;
    string address        = 4;
    int64 last_seen       = 5;
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once

#include <memory>
#include <functional>
#include <optional>
#include <rotor/asio.hpp>
#include "model/device_id.h"
#include "model/misc/arc.hpp"
#include "utils/tls.h"
#include "utils/uri.h"
#include "syncspirit-export.h"

namespace syncspirit::transport {

namespace asio = boost::asio;
namespace sys = boost::system;
namespace ra = rotor::asio;

using tcp = asio::ip::tcp;

using strand_t = asio::io_context::strand;
using addresses_t = std::vector<tcp::endpoint>;
using resolved_hosts_t = std::shared_ptr<addresses_t>;

using connect_fn_t = std::function<void(const tcp::endpoint &)>;
using error_fn_t = std::function<void(const sys::error_code &)>;
using handshake_fn_t =
    std::function<void(bool valid, utils::x509_t &peer, const tcp::endpoint &, const model::device_id_t *peer_device)>;
using io_fn_t = std::function<void(std::size_t)>;

struct ssl_junction_t {
    model::device_id_t peer;
    const utils::key_pair_t *me;
    bool sni_extension;
    std::string_view alpn; /* application layer protocol names? */
};

using ssl_option_t = std::optional<ssl_junction_t>;

struct transport_config_t {
    ssl_option_t ssl_junction;
    utils::uri_ptr_t uri;
    ra::supervisor_asio_t &supervisor;
    std::optional<tcp::socket> sock;
    bool active;
};

struct stream_base_t;

using stream_sp_t = model::intrusive_ptr_t<stream_base_t>;

} // namespace syncspirit::transport


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#include "utils/format.hpp"
#include "http.h"
#include "impl.hpp"

namespace syncspirit::transport {

template <typename Sock> struct http_impl_t : base_impl_t<Sock>, interface_t<http_impl_t<Sock>, Sock, http_base_t> {
    using self_t = http_impl_t;
    using socket_t = Sock;
    using base_t = base_impl_t<Sock>;
    using parent_t = base_impl_t<Sock>;
    using parent_t::parent_t;
    using rx_buff_t = boost::beast::flat_buffer;
    using response_t = http::response<http::string_body>;

    void async_read(rx_buff_t &rx_buff, response_t &response, io_fn_t &on_read,
                    error_fn_t &on_error) noexcept override {
        auto owner = curry_io<self_t>(*this, on_read, on_error);
        http::async_read(base_t::sock, rx_buff, response, [owner = std::move(owner)](auto ec, auto bytes) {
            auto &strand = owner->backend->strand;
            if (ec) {
                strand.post([ec = ec, owner = std::move(owner)]() {
                    if (ec == asio::error::operation_aborted) {
                        owner->backend->cancelling = false;
                    }
                    owner->error(ec);
                });
                return;
            }
            strand.post([owner = std::move(owner), bytes]() { owner->success(bytes); });
        });
    }
};

http_sp_t initiate_http(transport_config_t &config) noexcept {
    auto proto = config.uri->scheme();
    if (proto == "http") {
        return new http_impl_t<tcp_socket_t>(config);
    } else if (proto == "https") {
        return new http_impl_t<ssl_socket_t>(config);
    }
    return http_sp_t();
}

} // namespace syncspirit::transport


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once
#include "base.h"
#include "stream.h"
#include <boost/beast.hpp>

namespace syncspirit::transport {

namespace http = boost::beast::http;

struct http_interface_t {
    using rx_buff_t = boost::beast::flat_buffer;
    using response_t = http::response<http::string_body>;
    virtual void async_read(rx_buff_t &rx_buff, response_t &response, io_fn_t &on_read,
                            error_fn_t &on_error) noexcept = 0;
};

struct SYNCSPIRIT_API http_base_t : model::arc_base_t<http_base_t>, http_interface_t, stream_interface_t {
    virtual ~http_base_t() = default;
};

using http_sp_t = model::intrusive_ptr_t<http_base_t>;

http_sp_t initiate_http(transport_config_t &config) noexcept;

} // namespace syncspirit::transport


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2026 Ivan Baidakou

#pragma once

#include "base.h"
#include "utils/platform.h"
#include "utils/log.h"
#include "stream.h"
#include <boost/asio/ssl.hpp>

#ifdef WIN32_LEAN_AND_MEAN
#include <malloc.h>
#else
#include <alloca.h>
#endif

namespace syncspirit::transport {

namespace ssl = asio::ssl;
using tcp_socket_t = tcp::socket;
using ssl_socket_t = ssl::stream<tcp::socket>;

template <typename T> struct error_curry_t : model::arc_base_t<error_curry_t<T>> {
    using backend_ptr_t = model::intrusive_ptr_t<T>;
    error_curry_t(T &owner, error_fn_t &on_error) noexcept : backend{&owner}, on_error_fn{std::move(on_error)} {}
    virtual ~error_curry_t() = default;

    void error(const sys::error_code &ec) noexcept {
        on_error_fn(ec);
        backend->supervisor.do_process();
    }

    backend_ptr_t backend;
    error_fn_t on_error_fn;
};

template <typename T, typename F> struct success_curry_t : error_curry_t<T> {
    using parent_t = error_curry_t<T>;
    using backend_ptr_t = model::intrusive_ptr_t<T>;

    success_curry_t(T &owner, F &success_fn_, error_fn_t &on_error) noexcept
        : parent_t(owner, on_error), success_fn{std::move(success_fn_)} {}

    template <typename... Args> void success(Args &&...args) noexcept {
        success_fn(std::forward<Args>(args)...);
        parent_t::backend->supervisor.do_process();
    }

    F success_fn;
};

template <typename T, typename... Args> auto curry_connect(Args &&...args) {
    using sp_t = model::intrusive_ptr_t<success_curry_t<T, connect_fn_t>>;
    return sp_t(new typename sp_t::element_type(std::forward<Args>(args)...));
}

template <typename T, typename... Args> auto curry_handshake(Args &&...args) {
    using sp_t = model::intrusive_ptr_t<success_curry_t<T, handshake_fn_t>>;
    return sp_t(new typename sp_t::element_type(std::forward<Args>(args)...));
}

template <typename T, typename... Args> auto curry_io(Args &&...args) {
    using sp_t = model::intrusive_ptr_t<success_curry_t<T, io_fn_t>>;
    return sp_t(new typename sp_t::element_type(std::forward<Args>(args)...));
}

// --------------------------------

template <typename Sock> struct base_impl_t;

template <> struct base_impl_t<tcp_socket_t> {
    rotor::asio::supervisor_asio_t &supervisor;
    strand_t &strand;
    tcp_socket_t sock;
    bool cancelling = false;
    bool active;

    static tcp_socket_t mk_sock(transport_config_t &config, strand_t &strand) noexcept {
        if (config.sock) {
            return tcp::socket(std::move(config.sock.value()));
        } else {
            return tcp::socket(strand.context());
        }
    }

    base_impl_t(transport_config_t &config) noexcept
        : supervisor{config.supervisor}, strand{supervisor.get_strand()}, sock(mk_sock(config, strand)),
          active{config.active} {}

    tcp_socket_t &get_physical_layer() noexcept { return sock; }
};

template <> struct base_impl_t<ssl_socket_t> {
    using self_t = base_impl_t<ssl_socket_t>;

    rotor::asio::supervisor_asio_t &supervisor;
    strand_t &strand;
    model::device_id_t expected_peer;
    model::device_id_t actual_peer;
    const utils::key_pair_t *me = nullptr;
    ssl::context ctx;
    ssl::stream_base::handshake_type role;
    ssl_socket_t sock;
    utils::logger_t log;
    bool validation_passed = false;
    bool cancelling = false;
    bool active;

    static ssl::context get_context(self_t &source, std::string_view alpn) noexcept {
        ssl::context ctx(ssl::context::tls);
        ctx.set_options(ssl::context::default_workarounds | ssl::context::no_sslv2);

        auto me = source.me;
        if (me) {
            auto &cert_data = me->cert_data;
            auto &key_data = me->key_data;
            ctx.use_certificate(asio::const_buffer(cert_data.data(), cert_data.size()), ssl::context::asn1);
            ctx.use_private_key(asio::const_buffer(key_data.data(), key_data.size()), ssl::context::asn1);
        }

        if (alpn.size()) {
            std::byte *wire_alpn = (std::byte *)alloca(alpn.size() + 1);
            wire_alpn[0] = (std::byte)(alpn.size());
            auto b = reinterpret_cast<const std::byte *>(alpn.data());
            std::copy(b, b + alpn.size(), wire_alpn + 1);
            auto r = SSL_CTX_set_alpn_protos(ctx.native_handle(), (const unsigned char *)wire_alpn, alpn.size() + 1);
            assert(r == 0 && "SSL_CTX_set_alpn_protos");
            (void)r;
        }

        return ctx;
    }

    static ssl_socket_t mk_sock(transport_config_t &config, ssl::context &ctx, strand_t &strand) noexcept {
        if (config.sock) {
            tcp::socket sock(std::move(config.sock.value()));
            return {std::move(sock), ctx};
        } else {
            tcp::socket sock(strand.context());
            return {std::move(sock), ctx};
        }
    }

    base_impl_t(transport_config_t &config) noexcept
        : supervisor{config.supervisor}, strand{supervisor.get_strand()}, expected_peer{config.ssl_junction->peer},
          me(config.ssl_junction->me), ctx(get_context(*this, config.ssl_junction->alpn)),
          role(!config.active ? ssl::stream_base::server : ssl::stream_base::client),
          sock(mk_sock(config, ctx, strand)) {
        log = utils::get_logger("transport.tls");
        if (config.ssl_junction->sni_extension) {
            auto host = config.uri->host();
            if (!SSL_set_tlsext_host_name(sock.native_handle(), host.c_str())) {
                sys::error_code ec{static_cast<int>(::ERR_get_error()), asio::error::get_ssl_category()};
                log->error("http_actor_t:: Set SNI Hostname : {}", ec.message());
            }
        }
        if (me) {
            auto mode = ssl::verify_peer | ssl::verify_fail_if_no_peer_cert | ssl::verify_client_once;
            sock.set_verify_mode(mode);
            sock.set_verify_depth(1);
            sock.set_verify_callback([&](bool, ssl::verify_context &peer_ctx) -> bool {
                auto native = peer_ctx.native_handle();
                auto peer_cert = X509_STORE_CTX_get_current_cert(native);
                if (!peer_cert) {
                    log->warn("no peer certificate");
                    return false;
                }
                auto der_option = utils::as_serialized_der(peer_cert);
                if (!der_option) {
                    log->warn("peer certificate cannot be serialized as der : {}", der_option.error().message());
                    return false;
                }

                auto &der_bytes = der_option.value();
                auto cert_data = utils::cert_data_t(std::move(der_bytes));
                auto peer_option = model::device_id_t::from_cert(cert_data);
                if (!peer_option) {
                    log->warn("cannot get device_id from peer");
                    return false;
                }

                auto peer = std::move(peer_option.value());
                if (!actual_peer) {
                    actual_peer = std::move(peer);
                    log->trace("peer device_id = {}", actual_peer);
                }

                if (role == ssl::stream_base::handshake_type::client) {
                    if (actual_peer != expected_peer) {
                        log->warn("unexpected peer device_id. Got: {}, expected: {}", actual_peer.get_value(),
                                  expected_peer.get_value());
                        return false;
                    }
                }
                validation_passed = true;
                return true;
            });
        }
    }

    tcp_socket_t &get_physical_layer() noexcept { return sock.next_layer(); }
};

// --------------------------------

template <typename Sock, typename Owner> inline void generic_async_send(Owner owner, asio::const_buffer buff) noexcept {
    auto &sock = owner->backend->sock;
    asio::async_write(sock, buff, [owner = std::move(owner)](auto ec, auto bytes) mutable {
        auto &strand = owner->backend->strand;
        if (ec) {
            strand.post([ec = ec, owner = std::move(owner)]() mutable {
                if (ec == asio::error::operation_aborted) {
                    owner->backend->cancelling = false;
                }
                owner->error(ec);
            });
            return;
        }
        strand.post([owner = std::move(owner), bytes]() mutable { owner->success(bytes); });
    });
}

template <typename Sock, typename Owner>
inline void generic_async_recv(Owner owner, asio::mutable_buffer buff) noexcept {
    auto &sock = owner->backend->sock;
    sock.async_read_some(buff, [owner = std::move(owner)](auto ec, auto bytes) mutable {
        auto &strand = owner->backend->strand;
        if (ec) {
            strand.post([ec = ec, owner = std::move(owner)]() mutable {
                if (ec == asio::error::operation_aborted) {
                    owner->backend->cancelling = false;
                }
                owner->error(ec);
            });
            return;
        }
        strand.post([owner = std::move(owner), bytes]() mutable { owner->success(bytes); });
    });
}

template <typename T> struct impl;

template <> struct impl<tcp_socket_t> {
    using socket_t = tcp_socket_t;

    template <typename Owner> inline static void async_connect(Owner owner, resolved_hosts_t hosts) noexcept {
        auto &sock = owner->backend->get_physical_layer();
        asio::async_connect(sock, hosts->begin(), hosts->end(), [owner, hosts](auto ec, auto it) mutable {
            auto &strand = owner->backend->strand;
            if (ec) {
                strand.post([ec = ec, owner = std::move(owner)]() mutable {
                    if (ec == asio::error::operation_aborted) {
                        owner->backend->cancelling = false;
                    }
                    owner->error(ec);
                });
                return;
            }
            using T = std::remove_cv_t<decltype(it)>;
            if constexpr (std::is_same_v<T, tcp::endpoint>) {
                strand.post([addr = it, owner = std::move(owner), hosts = std::move(hosts)]() mutable {
                    owner->success(addr);
                });
            } else {
                strand.post([addr = *it, owner = std::move(owner), hosts = std::move(hosts)]() mutable {
                    owner->success(addr);
                });
            }
        });
    }

    template <typename Owner> inline static void async_handshake(Owner owner) noexcept {
        sys::error_code ec;
        auto endpoint = owner->backend->sock.remote_endpoint(ec);
        auto &strand = owner->backend->strand;
        if (ec) {
            strand.post([ec = ec, owner = std::move(owner)]() mutable { owner->error(ec); });
            return;
        } else {
            strand.post([endpoint, owner = std::move(owner)]() mutable {
                utils::x509_t x509;
                owner->success(true, x509, endpoint, nullptr);
            });
        }
    }

    template <typename Owner> inline static void async_send(Owner owner, asio::const_buffer buff) noexcept {
        generic_async_send<socket_t, Owner>(std::move(owner), buff);
    }

    template <typename Owner> inline static void async_recv(Owner owner, asio::mutable_buffer buff) noexcept {
        generic_async_recv<socket_t, Owner>(std::move(owner), buff);
    }

    template <typename Backend> inline static void cancel(Backend &backend, socket_t &sock) noexcept {
        if (!backend.cancelling) {
            backend.cancelling = true;
            sys::error_code ec;
            sock.cancel(ec);
            if (ec) {
                utils::get_logger("transport.sock")->error("impl<tcp::socket>::cancel() :: {}", ec.message());
            }
        }
    }
};

template <> struct impl<ssl_socket_t> {
    using socket_t = ssl_socket_t;

    template <typename Owner> inline static void async_connect(Owner owner, resolved_hosts_t hosts) noexcept {
        impl<tcp_socket_t>::async_connect<Owner>(std::move(owner), std::move(hosts));
    }

    template <typename Owner> inline static void async_handshake(Owner owner) noexcept {
        auto &role = owner->backend->role;
        auto &sock = owner->backend->sock;
        sock.async_handshake(role, [owner = std::move(owner)](auto ec) {
            auto &strand = owner->backend->strand;
            if (ec) {
                strand.post([ec = ec, owner = std::move(owner)]() { owner->error(ec); });
                return;
            }
            auto endpoint = owner->backend->get_physical_layer().remote_endpoint(ec);
            if (ec) {
                strand.post([ec, owner = std::move(owner)]() {
                    if (ec == asio::error::operation_aborted) {
                        owner->backend->cancelling = false;
                    }
                    owner->error(ec);
                });
                return;
            }
            strand.post([endpoint, owner = std::move(owner)]() {
                auto &backend = owner->backend;
                auto &sock = backend->sock;
                auto peer_cert = SSL_get_peer_certificate(sock.native_handle());
                auto x509 = utils::x509_t(peer_cert);
                owner->success(true, x509, endpoint, &backend->actual_peer);
            });
        });
    }

    template <typename Owner> inline static void async_send(Owner owner, asio::const_buffer buff) noexcept {
        generic_async_send<socket_t, Owner>(std::move(owner), buff);
    }

    template <typename Owner> inline static void async_recv(Owner owner, asio::mutable_buffer buff) noexcept {
        generic_async_recv<socket_t, Owner>(std::move(owner), buff);
    }

    template <typename Backend> inline static void cancel(Backend &backend, socket_t &sock) noexcept {
        impl<tcp_socket_t>::cancel(backend, sock.next_layer());
    }
};

// -------------------------------------

template <typename T, typename Sock, typename P> struct interface_t : P {
    using self_t = T;

    inline self_t &get_self() noexcept { return static_cast<self_t &>(*this); }

    void async_connect(resolved_hosts_t hosts, connect_fn_t &on_connect, error_fn_t &on_error) noexcept override {
        auto curry = curry_connect<self_t>(get_self(), on_connect, on_error);
        impl<Sock>::async_connect(std::move(curry), std::move(hosts));
    }

    void async_handshake(handshake_fn_t &on_handshake, error_fn_t &on_error) noexcept override {
        auto curry = curry_handshake<self_t>(get_self(), on_handshake, on_error);
        impl<Sock>::async_handshake(std::move(curry));
    }

    void async_send(asio::const_buffer buff, io_fn_t &on_write, error_fn_t &on_error) noexcept override {
        auto curry = curry_io<self_t>(get_self(), on_write, on_error);
        impl<Sock>::async_send(std::move(curry), buff);
    }

    void async_recv(asio::mutable_buffer buff, io_fn_t &on_read, error_fn_t &on_error) noexcept override {
        auto curry = curry_io<self_t>(get_self(), on_read, on_error);
        impl<Sock>::async_recv(std::move(curry), buff);
    }

    void cancel() noexcept override { impl<Sock>::cancel(get_self(), get_self().sock); }

    asio::ip::address local_address(sys::error_code &ec) noexcept override {
        auto &sock = get_self().get_physical_layer();
        auto endpoint = sock.local_endpoint(ec);
        if (!ec) {
            return endpoint.address();
        }
        return {};
    }

    ~interface_t() { get_self().get_physical_layer().close(); }
};

} // namespace syncspirit::transport


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2023 Ivan Baidakou

#include "utils/format.hpp"
#include "stream.h"
#include "impl.hpp"

namespace syncspirit::transport {

template <typename Sock, typename Interface>
struct generic_steam_impl_t : base_impl_t<Sock>, interface_t<generic_steam_impl_t<Sock, Interface>, Sock, Interface> {
    using self_t = generic_steam_impl_t;
    using socket_t = Sock;
    using parent_t = base_impl_t<Sock>;
    using parent_t::parent_t;
};

using ssl_stream_impl_t = generic_steam_impl_t<ssl_socket_t, stream_base_t>;

struct tcp_stream_impl_t final : generic_steam_impl_t<tcp_socket_t, upgradeable_stream_base_t> {
    using parent_t = generic_steam_impl_t;

    tcp_stream_impl_t(transport_config_t &config) noexcept : parent_t(config), uri(config.uri) {}

    using parent_t::parent_t;

    stream_sp_t upgrade(ssl_junction_t &ssl, bool active_role) noexcept {
        transport_config_t cfg{ssl_option_t(ssl), uri, supervisor, std::move(sock), active_role};
        return new ssl_stream_impl_t(cfg);
    }

    utils::uri_ptr_t uri;
};

stream_sp_t initiate_tls_passive(ra::supervisor_asio_t &sup, const utils::key_pair_t &my_keys, tcp::socket peer_sock,
                                 std::string_view alpn) noexcept {
    ssl_junction_t ssl{{}, &my_keys, false, alpn};
    transport_config_t cfg{ssl_option_t(ssl), {}, sup, std::move(peer_sock), false};
    return new ssl_stream_impl_t(cfg);
}

stream_sp_t initiate_tls_active(ra::supervisor_asio_t &sup, const utils::key_pair_t &my_keys,
                                const model::device_id_t &expected_peer, const utils::uri_ptr_t &uri, bool sni,
                                std::string_view alpn) noexcept {
    ssl_junction_t ssl{expected_peer, &my_keys, sni, alpn};
    transport_config_t cfg{ssl_option_t(ssl), uri, sup, {}, true};
    return new ssl_stream_impl_t(cfg);
}

stream_sp_t initiate_stream(transport_config_t &config) noexcept {
    assert(config.uri->scheme() == "tcp");
    if (config.ssl_junction) {
        return new ssl_stream_impl_t(config);
    } else {
        return new tcp_stream_impl_t(config);
    }
}

} // namespace syncspirit::transport


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2024 Ivan Baidakou

#pragma once
#include "base.h"

namespace syncspirit::transport {

struct stream_interface_t {
    virtual asio::ip::address local_address(sys::error_code &ec) noexcept = 0;
    virtual void async_connect(resolved_hosts_t hosts, connect_fn_t &on_connect, error_fn_t &on_error) noexcept = 0;
    virtual void async_handshake(handshake_fn_t &on_handshake, error_fn_t &on_error) noexcept = 0;
    virtual void async_send(asio::const_buffer buff, io_fn_t &on_write, error_fn_t &on_error) noexcept = 0;
    virtual void async_recv(asio::mutable_buffer buff, io_fn_t &on_read, error_fn_t &on_error) noexcept = 0;
    virtual void cancel() noexcept = 0;
};

struct stream_base_t : model::arc_base_t<stream_base_t>, stream_interface_t {
    virtual ~stream_base_t() = default;
};

struct upgradeable_stream_base_t : stream_base_t {
    virtual stream_sp_t upgrade(ssl_junction_t &ssl, bool active) noexcept = 0;
};

stream_sp_t SYNCSPIRIT_API initiate_tls_active(ra::supervisor_asio_t &supervisor, const utils::key_pair_t &my_keys,
                                               const model::device_id_t &expected_peer, const utils::uri_ptr_t &uri,
                                               bool sni = false, std::string_view alpn = "") noexcept;
stream_sp_t SYNCSPIRIT_API initiate_tls_passive(ra::supervisor_asio_t &supervisor, const utils::key_pair_t &my_keys,
                                                tcp::socket sock, std::string_view alpn = "") noexcept;
stream_sp_t SYNCSPIRIT_API initiate_stream(transport_config_t &config) noexcept;

} // namespace syncspirit::transport


code:
cmake_minimum_required (VERSION 3.19)

add_executable(syncspirit-daemon
    command/add_folder.cpp
    command/add_peer.cpp
    command/inactivate.cpp
    command/pair_iterator.cpp
    command/share_folder.cpp
    command.cpp
    error_code.cpp
    governor_actor.cpp
    main.cpp
)

target_link_libraries(syncspirit-daemon
    syncspirit_core
    $<$<PLATFORM_ID:Windows>:ws2_32>
    $<$<PLATFORM_ID:Windows>:wsock32>
)
target_include_directories(syncspirit-daemon PUBLIC ${syncspirit_SOURCE_DIR}/src)
install_executable("syncspirit-daemon")

if (("${CMAKE_BUILD_TYPE}" STREQUAL "Release") AND (NOT WIN32))
    include(CheckIPOSupported)
    check_ipo_supported(RESULT supported OUTPUT error)
    if( supported )
        message(STATUS "IPO / LTO enabled for the syncspirit-daemon")
        set_property(TARGET syncspirit-daemon PROPERTY INTERPROCEDURAL_OPTIMIZATION TRUE)
    else()
        message(STATUS "IPO / LTO not supported")
    endif()
endif()

if (CMAKE_BUILD_TYPE MATCHES "^([Rr]elease)|(MinSizeRel)")
    set_target_properties(syncspirit-daemon PROPERTIES LINK_FLAGS -s)
endif()


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "command.h"
#include "error_code.h"
#include "command/add_peer.h"
#include "command/add_folder.h"
#include "command/inactivate.h"
#include "command/share_folder.h"

namespace syncspirit::daemon {

outcome::result<command_ptr_t> command_t::parse(std::string_view in) noexcept {
    auto colon = in.find(":");
    if (colon == in.npos) {
        return make_error_code(error_code_t::command_is_missing);
    }
    auto cmd = in.substr(0, colon);
    if (cmd == "add_peer") {
        return command::add_peer_t::construct(in.substr(colon + 1));
    } else if (cmd == "add_folder") {
        return command::add_folder_t::construct(in.substr(colon + 1));
    } else if (cmd == "share") {
        return command::share_folder_t::construct(in.substr(colon + 1));
    } else if (cmd == "inactivate") {
        return command::inactivate_t::construct(in.substr(colon + 1));
    }
    return make_error_code(error_code_t::unknown_command);
}

void command_t::finish() noexcept {}

} // namespace syncspirit::daemon


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <memory>
#include <list>
#include <boost/outcome.hpp>
#include "../utils/log.h"
#include "utils/format.hpp"

namespace syncspirit::daemon {

namespace outcome = boost::outcome_v2;

struct governor_actor_t;

struct command_t;
using command_ptr_t = std::unique_ptr<command_t>;

struct command_t {
    virtual ~command_t() = default;
    virtual bool execute(governor_actor_t &) noexcept = 0;
    virtual void finish() noexcept;

    static outcome::result<command_ptr_t> parse(std::string_view) noexcept;
    utils::logger_t log;
};

using Commands = std::list<command_ptr_t>;

} // namespace syncspirit::daemon


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "add_folder.h"
#include "../governor_actor.h"
#include "../error_code.h"
#include "utils/format.hpp"
#include "utils/base32.h"
#include "model/diff/modify/upsert_folder.h"
#include "pair_iterator.h"
#include <random>

namespace bfs = std::filesystem;

namespace syncspirit::daemon::command {

outcome::result<command_ptr_t> add_folder_t::construct(std::string_view in) noexcept {
    std::string_view label, path;
    std::string id;

    auto it = pair_iterator_t(in);
    bool skip_colon = false;
    while (true) {
        auto r = it.next(skip_colon);
        if (r) {
            auto &v = r.value();
            if (v.first == "label") {
                label = v.second;
            } else if (v.first == "path") {
                path = v.second;
            } else if (v.first == "id") {
                id = v.second;
                skip_colon = true;
            }
        } else {
            break;
        }
    }

    if (label.empty()) {
        return make_error_code(error_code_t::missing_folder_label);
    }
    if (path.empty()) {
        return make_error_code(error_code_t::missing_folder_path);
    }
    if (id.empty()) {
        std::random_device rd;
        std::uniform_int_distribution<std::uint64_t> distribution;
        std::mt19937 generator(rd());
        auto intId = distribution(generator);
        auto id_view = utils::bytes_view_t((const unsigned char *)&intId, sizeof(intId));
        auto raw = utils::base32::encode(id_view);
        id = raw.substr(0, raw.size() / 2);
        id += "-";
        id += raw.substr(raw.size() / 2);
    }

    db::Folder f;
    db::set_path(f, path);
    db::set_id(f, id);
    db::set_label(f, label);
    db::set_ignore_permissions(f, false);
    db::set_ignore_delete(f, false);
    db::set_disable_temp_indexes(f, false);
    db::set_paused(f, false);
    db::set_scheduled(f, false);
    db::set_folder_type(f, db::FolderType::send_and_receive);
    db::set_pull_order(f, db::PullOrder::random);
    db::set_rescan_interval(f, 3600);
    return command_ptr_t(new add_folder_t(std::move(f)));
}

bool add_folder_t::execute(governor_actor_t &actor) noexcept {
    using namespace model::diff;
    log = actor.log;
    auto &cluster = actor.cluster;

    for (auto it : cluster->get_folders()) {
        auto f = it.item;
        if ((f->get_id() == db::get_id(folder))) {
            log->warn("{}, folder with id = {} is already present is the cluster", actor.get_identity(), f->get_id());
            return false;
        }
        if (f->get_label() == db::get_label(folder)) {
            log->warn("{}, folder with label = {} is already present is the cluster", actor.get_identity(),
                      f->get_label());
            return false;
        }
        if (f->get_path().string() == db::get_path(folder)) {
            log->warn("{}, folder with path = {} is already present is the cluster", actor.get_identity(),
                      f->get_path());
            return false;
        }
    }

    auto opt = modify::upsert_folder_t::create(*cluster, *actor.sequencer, folder, 0);
    if (opt.has_error()) {
        auto message = opt.assume_error().message();
        log->warn("{}, cannot create folder '{}' on '{}': {}", actor.get_identity(), db::get_label(folder),
                  db::get_path(folder), message);
        return false;
    }

    log->debug("{}, going to add folder '{}' on '{}'", actor.get_identity(), db::get_label(folder),
               db::get_path(folder));

    actor.send_command(std::move(opt.value()), *this);
    return true;
}

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "../command.h"
#include "proto/proto-helpers.h"
#include "model/folder.h"

namespace syncspirit::daemon::command {

struct add_folder_t final : command_t {
    bool execute(governor_actor_t &) noexcept override;
    static outcome::result<command_ptr_t> construct(std::string_view in) noexcept;

    inline add_folder_t() noexcept = default;
    inline add_folder_t(db::Folder &&f) noexcept : folder{std::move(f)} {}

  private:
    db::Folder folder;
};

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "add_peer.h"
#include "../governor_actor.h"
#include "../error_code.h"
#include "model/diff/modify/update_peer.h"

namespace syncspirit::daemon::command {

outcome::result<command_ptr_t> add_peer_t::construct(std::string_view in) noexcept {
    auto colon = in.find(":");
    if (colon == in.npos) {
        return make_error_code(error_code_t::missing_device_label);
    }

    auto label = in.substr(0, colon);

    auto opt = model::device_id_t::from_string(in.substr(colon + 1));
    if (!opt) {
        return make_error_code(error_code_t::invalid_device_id);
    }
    return std::make_unique<command::add_peer_t>(opt.value(), label);
}

bool add_peer_t::execute(governor_actor_t &actor) noexcept {
    using namespace model::diff;
    log = actor.log;
    auto &cluster = *actor.cluster;
    auto &devices = cluster.get_devices();
    auto found = devices.by_sha256(peer.get_sha256());
    if (found) {
        log->warn("device {} is already added, skipping", peer);
        return false;
    }

    db::Device db_dev;
    db::set_name(db_dev, label);

    auto diff = cluster_diff_ptr_t(new modify::update_peer_t(std::move(db_dev), peer, cluster));
    actor.send_command(std::move(diff), *this);
    return true;
}

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include "../command.h"
#include "model/device_id.h"
#include "proto/proto-helpers.h"

namespace syncspirit::daemon::command {

struct add_peer_t final : command_t {
    static outcome::result<command_ptr_t> construct(std::string_view in) noexcept;
    bool execute(governor_actor_t &) noexcept override;

    inline add_peer_t() noexcept = default;
    inline add_peer_t(model::device_id_t peer_, std::string_view label_) noexcept : peer{peer_}, label(label_) {}

  private:
    model::device_id_t peer;
    std::string label;
};

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "inactivate.h"
#include "../governor_actor.h"
#include "../error_code.h"
#include "utils/error_code.h"
#include <charconv>

namespace syncspirit::daemon::command {

outcome::result<command_ptr_t> inactivate_t::construct(std::string_view in) noexcept {
    std::uint32_t seconds;
    auto pair = std::from_chars(in.data(), in.data() + in.size(), seconds);
    if (pair.ec != std::errc()) {
        auto ec = std::make_error_code(pair.ec);
        return utils::adapt(ec);
    }
    return std::make_unique<command::inactivate_t>(seconds);
}

bool inactivate_t::execute(governor_actor_t &actor) noexcept {
    if (seconds) {
        actor.inactivity_seconds = seconds;
        actor.track_inactivity();
    }
    return true;
}

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include "../command.h"
#include "model/device_id.h"

namespace syncspirit::daemon::command {

struct inactivate_t final : command_t {
    static outcome::result<command_ptr_t> construct(std::string_view in) noexcept;
    bool execute(governor_actor_t &) noexcept override;

    inline inactivate_t() noexcept = default;
    inline inactivate_t(std::uint32_t seconds_) noexcept : seconds{seconds_} {}

  private:
    std::uint32_t seconds;
};

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "pair_iterator.h"

using namespace syncspirit::daemon::command;

std::optional<pair_iterator_t::pair_t> pair_iterator_t::next(bool skip_colon) noexcept {
    auto colon = in.size();
    if (!skip_colon) {
        auto colon_pos = in.find(":");
        if (colon_pos != in.npos) {
            colon = colon_pos;
        }
    }

    auto it = in.substr(0, colon);
    auto eq = it.find("=");
    if (eq == it.npos) {
        return {};
    }

    auto f = in.substr(0, eq);
    auto s = in.substr(eq + 1, colon - (eq + 1));
    if (colon < in.size()) {
        in = in.substr(colon + 1);
    } else {
        in = "";
    }
    return pair_t{f, s};
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include <string_view>
#include <utility>
#include <optional>

namespace syncspirit::daemon::command {

struct pair_iterator_t {
    using pair_t = std::pair<std::string_view, std::string_view>;

    inline pair_iterator_t(std::string_view in_) noexcept : in(in_) {}

    std::optional<pair_t> next(bool skip_colon = false) noexcept;
    std::string_view in;
};

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "share_folder.h"
#include "pair_iterator.h"
#include "../governor_actor.h"
#include "../error_code.h"
#include "model/diff/modify/share_folder.h"

namespace syncspirit::daemon::command {

outcome::result<command_ptr_t> share_folder_t::construct(std::string_view in) noexcept {
    std::string folder, peer;
    auto it = pair_iterator_t(in);
    while (true) {
        auto r = it.next();
        if (r) {
            auto &v = r.value();
            if (v.first == "folder") {
                folder = v.second;
            } else if (v.first == "device") {
                peer = v.second;
            }
        } else {
            break;
        }
    }
    if (folder.empty()) {
        return make_error_code(error_code_t::missing_folder);
    }
    if (peer.empty()) {
        return make_error_code(error_code_t::missing_device);
    }

    return command_ptr_t(new share_folder_t(std::move(folder), std::move(peer)));
}

bool share_folder_t::execute(governor_actor_t &actor) noexcept {
    using namespace model::diff;
    log = actor.log;
    model::folder_ptr_t folder;
    for (auto it : actor.cluster->get_folders()) {
        auto &f = it.item;
        if ((f->get_id() == this->folder)) {
            folder = f;
            break;
        }
        if (f->get_label() == this->folder) {
            folder = f;
            break;
        }
    }
    if (!folder) {
        log->warn("{}, folder {} not found", actor.get_identity(), this->folder);
        return false;
    }

    model::device_ptr_t device;
    for (auto it : actor.cluster->get_devices()) {
        auto &d = it.item;
        if (d->device_id().get_short() == this->peer) {
            device = d;
            break;
        } else if (d->device_id().get_value() == this->peer) {
            device = d;
            break;
        } else if (d->get_name() == this->peer) {
            device = d;
            break;
        }
    }
    if (!device) {
        log->warn("{}, device {} not found", actor.get_identity(), this->peer);
        return false;
    }
    if (device == actor.cluster->get_device()) {
        log->warn("{}, folder {} cannot be share with local device ({})", actor.get_identity(), this->folder,
                  this->peer);
        return false;
    }
    auto fi = folder->get_folder_infos().by_device(*device);
    if (fi) {
        log->warn("{}, folder {} is already shared with {}", actor.get_identity(), this->folder, this->peer);
        return false;
    }

    auto &self = actor.cluster->get_device()->device_id();
    auto opt = modify::share_folder_t::create(*actor.cluster, *actor.sequencer, *device, self, *folder);
    if (!opt) {
        log->warn("{}, cannot share: ", actor.get_identity(), opt.assume_error().message());
        return false;
    }
    actor.send_command(std::move(opt.value()), *this);
    return true;
}

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include "../command.h"
#include "../../model/folder.h"

namespace syncspirit::daemon::command {

struct share_folder_t final : command_t {
    bool execute(governor_actor_t &) noexcept override;
    static outcome::result<command_ptr_t> construct(std::string_view in) noexcept;

    inline share_folder_t() noexcept = default;
    template <typename Folder, typename Peer>
    inline share_folder_t(Folder &&f, Peer &&p) noexcept
        : folder{std::forward<Folder>(f)}, peer{std::forward<Peer>(p)} {}

  private:
    std::string folder;
    std::string peer;
};

} // namespace syncspirit::daemon::command


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#include "error_code.h"

namespace syncspirit::daemon::detail {

const char *error_code_category::name() const noexcept { return "syncspirit_daemon_error"; }

std::string error_code_category::message(int c) const {
    std::string r;
    switch (static_cast<error_code_t>(c)) {
    case error_code_t::success:
        r = "success";
        break;
    case error_code_t::command_is_missing:
        r = "command is missing";
        break;
    case error_code_t::invalid_device_id:
        r = "invalid device id";
        break;
    case error_code_t::missing_device_label:
        r = "device label is missing";
        break;
    case error_code_t::missing_folder_label:
        r = "folder label is missing";
        break;
    case error_code_t::missing_folder:
        r = "folder is missing";
        break;
    case error_code_t::missing_device:
        r = "device is is missing";
        break;
    case error_code_t::missing_folder_path:
        r = "folder path is missing";
        break;
    case error_code_t::incorrect_number:
        r = "specified argument is not a number";
        break;
    default:
        r = "unknown";
    }
    r += " (";
    r += std::to_string(c) + ")";
    return r;
};

} // namespace syncspirit::daemon::detail

namespace syncspirit::daemon {

const static detail::error_code_category category;

const detail::error_code_category &error_code_category() { return category; }

} // namespace syncspirit::daemon


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2022 Ivan Baidakou

#pragma once

#include <boost/system/error_code.hpp>

namespace syncspirit::daemon {

enum class error_code_t {
    success = 0,
    command_is_missing,
    unknown_command,
    invalid_device_id,
    missing_device_label,
    missing_folder_label,
    missing_folder,
    missing_device,
    missing_folder_path,
    incorrect_number,
};

namespace detail {

class error_code_category : public boost::system::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 boost::system::error_code make_error_code(error_code_t e) {
    return {static_cast<int>(e), error_code_category()};
}

} // namespace syncspirit::daemon

namespace boost {
namespace system {

template <> struct is_error_code_enum<syncspirit::daemon::error_code_t> : std::true_type {
    static const bool value = true;
};
} // namespace system
} // namespace boost


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#include "governor_actor.h"
#include "net/names.h"
#include "fs/messages.h"
#include "utils/format.hpp"
#include "utils/error_code.h"
#include "model/diff/advance/remote_copy.h"
#include "model/diff/local/io_failure.h"
#include "model/diff/modify/append_block.h"
#include "model/diff/modify/clone_block.h"
#include "model/diff/peer/cluster_update.h"
#include "model/diff/peer/update_folder.h"

using namespace syncspirit::daemon;

governor_actor_t::governor_actor_t(config_t &cfg)
    : r::actor_base_t{cfg}, commands{std::move(cfg.commands)}, sequencer{std::move(cfg.sequencer)},
      cluster{std::move(cfg.cluster)} {}

void governor_actor_t::configure(r::plugin::plugin_base_t &plugin) noexcept {
    r::actor_base_t::configure(plugin);
    plugin.with_casted<r::plugin::address_maker_plugin_t>([&](auto &p) {
        p.set_identity("deamon.governor", false);
        log = utils::get_logger(identity);
    });
    plugin.with_casted<r::plugin::registry_plugin_t>([&](auto &p) {
        p.discover_name(net::names::coordinator, coordinator, true).link(false).callback([&](auto phase, auto &ec) {
            if (!ec && phase == r::plugin::registry_plugin_t::phase_t::linking) {
                auto p = get_plugin(r::plugin::starter_plugin_t::class_identity);
                auto plugin = static_cast<r::plugin::starter_plugin_t *>(p);
                plugin->subscribe_actor(&governor_actor_t::on_model_update, coordinator);
                plugin->subscribe_actor(&governor_actor_t::on_command);
            }
        });
    });
}

void governor_actor_t::on_start() noexcept {
    LOG_TRACE(log, "on_start");
    process();
    r::actor_base_t::on_start();
}

void governor_actor_t::shutdown_start() noexcept {
    LOG_TRACE(log, "shutdown_start");
    r::actor_base_t::shutdown_start();
}

void governor_actor_t::on_model_update(model::message::model_update_t &message) noexcept {
    auto custom = message.payload.custom;
    LOG_TRACE(log, "on_model_update, this = {}, payload = {}", (void *)this, custom);
    auto &diff = *message.payload.diff;
    auto r = diff.visit(*this, nullptr);
    if (!r) {
        auto ee = make_error(r.assume_error());
        do_shutdown(ee);
    }
}

void governor_actor_t::send_command(model::diff::cluster_diff_ptr_t diff, command_t &source) noexcept {
    auto message =
        r::make_routed_message<model::payload::model_update_t>(coordinator, address, std::move(diff), &source);
    get_supervisor().put(std::move(message));
}

void governor_actor_t::on_command(model::message::model_update_t &message) noexcept {
    auto custom = message.payload.custom;
    LOG_TRACE(log, "on_command, this = {}, payload = {}", (void *)this, custom);
    for (auto it = commands.begin(); it != commands.end(); ++it) {
        if (it->get() == custom) {
            (*it)->finish();
            commands.erase(it);
            process();
            break;
        }
    }
}

void governor_actor_t::process() noexcept {
    LOG_DEBUG(log, "process");
NEXT:
    if (commands.empty()) {
        log->debug("no commands left for processing");
        return;
    }
    auto &cmd = commands.front();
    bool keep = cmd->execute(*this);
    if (!keep) {
        commands.pop_front();
        goto NEXT;
    }
}

void governor_actor_t::track_inactivity() noexcept {
    LOG_DEBUG(log, "will track inactivity for the next {} seconds", inactivity_seconds);
    assert(inactivity_seconds);
    auto timeout = r::pt::seconds(inactivity_seconds);
    auto now = clock_t::local_time();
    deadline = now + timeout;
    start_timer(timeout, *this, &governor_actor_t::on_inactivity_timer);
}

void governor_actor_t::on_inactivity_timer(r::request_id_t, bool cancelled) noexcept {
    LOG_DEBUG(log, "on_inactivity_timer");
    if (cancelled) {
        if (state == r::state_t::OPERATIONAL) {
            track_inactivity();
        }
        return;
    }

    auto now = clock_t::local_time();
    if (now < deadline) {
        return track_inactivity();
    }
    LOG_INFO(log, "inactivity timeout, exiting...");
    do_shutdown();
}

void governor_actor_t::refresh_deadline() noexcept {
    auto timeout = r::pt::seconds(inactivity_seconds);
    auto now = clock_t::local_time();
    deadline = now + timeout;
}

auto governor_actor_t::operator()(const model::diff::advance::remote_copy_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    refresh_deadline();
    return diff.visit_next(*this, custom);
}

auto governor_actor_t::operator()(const model::diff::local::io_failure_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    LOG_TRACE(log, "on_io_error, count = {}", diff.errors.size());
    for (auto &err : diff.errors) {
        LOG_WARN(log, "on_io_error (ignored) path: {}, problem: {}", err.path, err.ec.message());
    }
    return diff.visit_next(*this, custom);
}

auto governor_actor_t::operator()(const model::diff::peer::cluster_update_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    refresh_deadline();
    return diff.visit_next(*this, custom);
}

auto governor_actor_t::operator()(const model::diff::peer::update_folder_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    refresh_deadline();
    return diff.visit_next(*this, custom);
}

auto governor_actor_t::operator()(const model::diff::modify::append_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    refresh_deadline();
    return diff.visit_next(*this, custom);
}

auto governor_actor_t::operator()(const model::diff::modify::clone_block_t &diff, void *custom) noexcept
    -> outcome::result<void> {
    refresh_deadline();
    return diff.visit_next(*this, custom);
}


code:
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2019-2025 Ivan Baidakou

#pragma once

#include <rotor.hpp>
#include "command.h"
#include "model/messages.h"
#include "model/diff/cluster_visitor.h"
#include "model/misc/sequencer.h"
#include "utils/log.h"

namespace syncspirit::daemon {

namespace r = rotor;

struct governor_actor_config_t : r::actor_config_t {
    Commands commands;
    model::sequencer_ptr_t sequencer;
    model::cluster_ptr_t cluster;
};

template <typename Actor> struct governor_actor_config_builder_t : r::actor_config_builder_t<Actor> {
    using builder_t = typename Actor::template config_builder_t<Actor>;
    using parent_t = r::actor_config_builder_t<Actor>;
    using parent_t::parent_t;

    builder_t &&commands(Commands &&value) && noexcept {
        parent_t::config.commands = std::move(value);
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
    builder_t &&cluster(const model::cluster_ptr_t &value) && noexcept {
        parent_t::config.cluster = value;
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
    builder_t &&sequencer(model::sequencer_ptr_t value) && noexcept {
        parent_t::config.sequencer = std::move(value);
        return std::move(*static_cast<typename parent_t::builder_t *>(this));
    }
};

struct governor_actor_t : public r::actor_base_t, private model::diff::cluster_visitor_t {
    using config_t = governor_actor_config_t;
    template <typename Actor> using config_builder_t = governor_actor_config_builder_t<Actor>;

    explicit governor_actor_t(config_t &cfg);

    void configure(r::plugin::plugin_base_t &plugin) noexcept override;
    void on_start() noexcept override;
    void shutdown_start() noexcept override;
    void track_inactivity() noexcept;
    void process() noexcept;
    void send_command(model::diff::cluster_diff_ptr_t diff, command_t &source) noexcept;

    r::address_ptr_t coordinator;
    r::address_ptr_t fs_scanner;
    Commands commands;
    model::sequencer_ptr_t sequencer;
    model::cluster_ptr_t cluster;
    utils::logger_t log;
    std::uint32_t inactivity_seconds;

  private:
    using clock_t = r::pt::microsec_clock;

    void on_model_update(model::message::model_update_t &message) noexcept;
    void on_command(model::message::model_update_t &message) noexcept;
    void on_inactivity_timer(r::request_id_t, bool cancelled) noexcept;

    void refresh_deadline() noexcept;

    outcome::result<void> operator()(const model::diff::advance::remote_copy_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::local::io_failure_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::append_block_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::modify::clone_block_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::peer::cluster_update_t &, void *) noexcept override;
    outcome::result<void> operator()(const model::diff::peer::update_folder_t &, void *) noexcept override;

    r::pt::ptime deadline;
};

} // namespace syncspirit::daemon