// 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