From 3a17933beb28c9d55be8da0d2c7cecd82d79eea6 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Wed, 6 May 2026 19:20:47 -0400 Subject: [PATCH] wg: DragonRelay registration client implementation (libcurl) --- src/wg/relayreg.cpp | 187 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/wg/relayreg.cpp diff --git a/src/wg/relayreg.cpp b/src/wg/relayreg.cpp new file mode 100644 index 0000000..70d4f76 --- /dev/null +++ b/src/wg/relayreg.cpp @@ -0,0 +1,187 @@ +// src/wg/relayreg.cpp — DragonRelay HTTP registration client for Artemis. +// +// Depends on: libcurl, nlohmann/json (header-only, shipped with Sunshine) +// +// Build: add relayreg.cpp to the CMake target and link curl. + +#include "relayreg.h" + +#include +#include + +#include +#include + +namespace wg { + +using json = nlohmann::json; + +// ── libcurl write callback ──────────────────────────────────────────────────── + +static size_t curlWrite(void *data, size_t size, size_t nmemb, void *userp) { + size_t total = size * nmemb; + static_cast(userp)->append(static_cast(data), total); + return total; +} + +// ── RelayReg ────────────────────────────────────────────────────────────────── + +RelayReg::RelayReg() { + curl_global_init(CURL_GLOBAL_DEFAULT); +} + +RelayReg::~RelayReg() { + curl_global_cleanup(); +} + +// ── HTTP helper ─────────────────────────────────────────────────────────────── + +int RelayReg::request(const std::string &method, + const std::string &path, + const std::string &body, + std::string &responseOut) { + responseOut.clear(); + + CURL *curl = curl_easy_init(); + if (!curl) return -1; + + std::string url = m_base + path; + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWrite); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseOut); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); + + // Headers + struct curl_slist *hdrs = nullptr; + hdrs = curl_slist_append(hdrs, "Content-Type: application/json"); + if (!m_jwt.empty()) { + std::string auth = "Authorization: Bearer " + m_jwt; + hdrs = curl_slist_append(hdrs, auth.c_str()); + } + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs); + + // Method + body + if (method == "POST" || method == "PUT") { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str()); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)body.size()); + } else if (method == "DELETE") { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "DELETE"); + } + // GET is default + + CURLcode res = curl_easy_perform(curl); + long httpCode = 0; + if (res == CURLE_OK) + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); + + curl_slist_free_all(hdrs); + curl_easy_cleanup(curl); + + if (res != CURLE_OK) { + if (m_log) m_log(std::string("curl error: ") + curl_easy_strerror(res)); + return -1; + } + return (int)httpCode; +} + +// ── login ───────────────────────────────────────────────────────────────────── + +bool RelayReg::login(const std::string &username, const std::string &password, + std::string &errOut) { + json body = { {"username", username}, {"password", password} }; + std::string resp; + int code = request("POST", "/api/auth/login", body.dump(), resp); + if (code != 200) { + errOut = "login failed (HTTP " + std::to_string(code) + "): " + resp; + return false; + } + try { + auto j = json::parse(resp); + m_jwt = j.at("token").get(); + if (m_log) m_log("Logged in to DragonRelay"); + return true; + } catch (const std::exception &e) { + errOut = std::string("login JSON parse: ") + e.what(); + return false; + } +} + +void RelayReg::logout() { + m_jwt.clear(); +} + +// ── provisionVPN ────────────────────────────────────────────────────────────── + +bool RelayReg::provisionVPN(const std::string &deviceName, + VPNConf &out, std::string &errOut) { + json body = { {"device_name", deviceName} }; + std::string resp; + int code = request("POST", "/api/vpn/peer", body.dump(), resp); + if (code != 200 && code != 201) { + errOut = "provisionVPN failed (HTTP " + std::to_string(code) + "): " + resp; + return false; + } + try { + auto j = json::parse(resp); + out.id = j.at("id").get(); + out.name = j.at("name").get(); + out.publicKey = j.at("public_key").get(); + out.allowedIP = j.at("allowed_ip").get(); + out.conf = j.at("conf").get(); + if (m_log) m_log("VPN peer provisioned: " + out.name + " @ " + out.allowedIP); + return true; + } catch (const std::exception &e) { + errOut = std::string("provisionVPN JSON: ") + e.what(); + return false; + } +} + +bool RelayReg::deleteVPNPeer(const std::string &peerId, std::string &errOut) { + std::string resp; + int code = request("DELETE", "/api/vpn/peer/" + peerId, "", resp); + if (code != 200 && code != 204) { + errOut = "deleteVPNPeer failed (HTTP " + std::to_string(code) + ")"; + return false; + } + return true; +} + +// ── registerHost ────────────────────────────────────────────────────────────── + +bool RelayReg::registerHost(const std::string &name, const std::string &wgIP, + int port, std::string &errOut) { + json body = { {"name", name}, {"wg_ip", wgIP}, {"port", port} }; + std::string resp; + int code = request("POST", "/api/host/register", body.dump(), resp); + if (code != 200 && code != 201) { + errOut = "registerHost failed (HTTP " + std::to_string(code) + "): " + resp; + return false; + } + if (m_log) m_log("Registered host '" + name + "' at " + wgIP + ":" + std::to_string(port)); + return true; +} + +bool RelayReg::heartbeat(std::string &errOut) { + std::string resp; + int code = request("PUT", "/api/host/heartbeat", "{}", resp); + if (code != 200 && code != 204) { + errOut = "heartbeat failed (HTTP " + std::to_string(code) + ")"; + return false; + } + return true; +} + +bool RelayReg::unregisterHost(std::string &errOut) { + std::string resp; + int code = request("DELETE", "/api/host/unregister", "", resp); + if (code != 200 && code != 204) { + errOut = "unregisterHost failed (HTTP " + std::to_string(code) + ")"; + return false; + } + if (m_log) m_log("Unregistered from DragonRelay"); + return true; +} + +} // namespace wg