secure chat addon and stun

This commit is contained in:
ariska committed 2026-07-02 09:12:57 +07:00
1 parent a45fa67ed2
commit 813d3aa06e
40 files changed
+2767 -189

No files matched your search

+405
View File
@@ -0,0 +1,405 @@
/*
* Copyright (c)2019 ZeroTier, Inc.
*
* Use of this software is governed by the Business Source License included
* in the LICENSE.TXT file in the project's root directory.
*/
/****/
#include "CommsController.hpp"
#include "../node/Mutex.hpp"
#include "../node/Utils.hpp"
#include "../osdep/OSUtils.hpp"
#include <map>
#include <set>
#include <sstream>
#include <vector>
namespace ZeroTier {
namespace {
struct IceEntry
{
std::string fromPeerId;
std::string candidate;
std::string sdpMid;
int sdpMLineIndex;
int64_t at;
};
struct CallState
{
std::string callId;
std::string callType;
std::string networkId;
std::string callerId;
std::string calleeId;
std::string callerName;
std::string state;
std::string offerSdp;
std::string answerSdp;
int64_t createdAt;
};
Mutex g_commsMutex;
std::map<std::string, CallState> g_calls;
std::map<std::string, std::vector<IceEntry> > g_iceByCall;
std::map<std::string, std::set<std::string> > g_callsByPeer;
std::string lowerId(const std::string &id)
{
std::string out = id;
for (std::string::iterator i(out.begin()); i != out.end(); ++i) {
if (*i >= 'A' && *i <= 'F') {
*i = (char)(*i - 'A' + 'a');
}
}
return out;
}
std::string newCallId()
{
uint8_t randBytes[8];
Utils::getSecureRandom(randBytes, sizeof(randBytes));
char randHex[17];
Utils::hex(randBytes, (unsigned int)sizeof(randBytes), randHex);
return std::string("call_") + std::to_string(OSUtils::now()) + "_" + randHex;
}
void trackCallForPeer(const std::string &peerId, const std::string &callId)
{
if (peerId.empty()) {
return;
}
g_callsByPeer[peerId].insert(callId);
}
void untrackCall(const CallState &call)
{
const std::string caller = lowerId(call.callerId);
const std::string callee = lowerId(call.calleeId);
if (!caller.empty()) {
g_callsByPeer[caller].erase(call.callId);
if (g_callsByPeer[caller].empty()) {
g_callsByPeer.erase(caller);
}
}
if (!callee.empty()) {
g_callsByPeer[callee].erase(call.callId);
if (g_callsByPeer[callee].empty()) {
g_callsByPeer.erase(callee);
}
}
}
void purgeCall(const std::string &callId)
{
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
if (it != g_calls.end()) {
untrackCall(it->second);
g_calls.erase(it);
}
g_iceByCall.erase(callId);
}
nlohmann::json callToIncomingJson(const CallState &call)
{
nlohmann::json out = nlohmann::json::object();
out["callId"] = call.callId;
out["callType"] = call.callType;
out["networkId"] = call.networkId;
out["callerId"] = call.callerId;
out["callerName"] = call.callerName;
out["sdp"] = call.offerSdp;
out["state"] = call.state;
return out;
}
nlohmann::json webrtcConfigFromLocal(const nlohmann::json &localConfig)
{
nlohmann::json out = nlohmann::json::object();
out["icePreferOverlay"] = true;
out["iceFallbackTimeoutMs"] = 8000;
out["webhubPort"] = 9994;
out["iceServers"] = nlohmann::json::array();
out["iceServers"].push_back(nlohmann::json::object({ { "urls", "stun:stun.l.google.com:19302" } }));
try {
if (localConfig.contains("settings") && localConfig["settings"].is_object()) {
const nlohmann::json &settings = localConfig["settings"];
if (settings.contains("icePreferOverlay") && settings["icePreferOverlay"].is_boolean()) {
out["icePreferOverlay"] = settings["icePreferOverlay"].get<bool>();
}
if (settings.contains("iceFallbackTimeoutMs") && settings["iceFallbackTimeoutMs"].is_number_integer()) {
out["iceFallbackTimeoutMs"] = settings["iceFallbackTimeoutMs"].get<int64_t>();
}
if (settings.contains("webhubPort") && settings["webhubPort"].is_number_integer()) {
out["webhubPort"] = settings["webhubPort"].get<int64_t>();
}
if (settings.contains("iceServers") && settings["iceServers"].is_array()) {
out["iceServers"] = settings["iceServers"];
}
}
} catch ( ... ) {
}
return out;
}
} // namespace
void CommsController::configureRoutes(
httplib::Server &s,
httplib::Server &sv6,
const std::function<nlohmann::json()> &localConfigGetter,
const std::function<void(const httplib::Request&, httplib::Response&, std::string)> &setContent)
{
auto webrtcConfigGet = [localConfigGetter, setContent](const httplib::Request &req, httplib::Response &res) {
nlohmann::json localConfig = nlohmann::json::object();
try {
localConfig = localConfigGetter();
} catch ( ... ) {
}
setContent(req, res, webrtcConfigFromLocal(localConfig).dump());
};
s.Get("/app/api/webrtc-config", webrtcConfigGet);
sv6.Get("/app/api/webrtc-config", webrtcConfigGet);
auto callCreate = [setContent](const httplib::Request &req, httplib::Response &res) {
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
if (!body.is_object()) {
res.status = 400;
setContent(req, res, "{\"error\":\"body must be a JSON object\"}");
return;
}
const std::string targetId = lowerId(body.contains("targetId") && body["targetId"].is_string()
? body["targetId"].get<std::string>() : "");
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
? body["sdp"].get<std::string>() : "";
if (targetId.empty() || sdp.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"targetId and sdp are required\"}");
return;
}
std::string callType = "voice";
if (body.contains("callType") && body["callType"].is_string()) {
callType = body["callType"].get<std::string>();
} else if (body.contains("type") && body["type"].is_string()) {
const std::string t = body["type"].get<std::string>();
if (t == "voice" || t == "video") {
callType = t;
}
}
const std::string networkId = body.contains("networkId") && body["networkId"].is_string()
? lowerId(body["networkId"].get<std::string>()) : "";
const std::string callerId = body.contains("callerId") && body["callerId"].is_string()
? lowerId(body["callerId"].get<std::string>()) : "local";
const std::string callerName = body.contains("callerName") && body["callerName"].is_string()
? body["callerName"].get<std::string>() : callerId;
CallState call;
call.callId = newCallId();
call.callType = callType;
call.networkId = networkId;
call.callerId = callerId;
call.calleeId = targetId;
call.callerName = callerName;
call.state = "ringing";
call.offerSdp = sdp;
call.createdAt = OSUtils::now();
Mutex::Lock _l(g_commsMutex);
g_calls[call.callId] = call;
trackCallForPeer(call.callerId, call.callId);
trackCallForPeer(call.calleeId, call.callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = call.callId;
out["state"] = call.state;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call", callCreate);
sv6.Post("/controller/comms/call", callCreate);
auto callIncoming = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
if (peerId.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"peerId query parameter is required\"}");
return;
}
nlohmann::json calls = nlohmann::json::array();
Mutex::Lock _l(g_commsMutex);
std::map<std::string, std::set<std::string> >::const_iterator it = g_callsByPeer.find(peerId);
if (it != g_callsByPeer.end()) {
for (std::set<std::string>::const_iterator c = it->second.begin(); c != it->second.end(); ++c) {
std::map<std::string, CallState>::const_iterator callIt = g_calls.find(*c);
if (callIt == g_calls.end()) {
continue;
}
if (lowerId(callIt->second.calleeId) != peerId || callIt->second.state != "ringing") {
continue;
}
calls.push_back(callToIncomingJson(callIt->second));
}
}
nlohmann::json out = nlohmann::json::object();
out["calls"] = calls;
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/incoming", callIncoming);
sv6.Get("/controller/comms/call/incoming", callIncoming);
auto callAnswer = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
const std::string sdp = body.contains("sdp") && body["sdp"].is_string()
? body["sdp"].get<std::string>() : "";
if (sdp.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"sdp is required\"}");
return;
}
Mutex::Lock _l(g_commsMutex);
std::map<std::string, CallState>::iterator it = g_calls.find(callId);
if (it == g_calls.end()) {
res.status = 404;
setContent(req, res, "{\"error\":\"call not found\"}");
return;
}
it->second.answerSdp = sdp;
it->second.state = "accepted";
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "accepted";
out["sdp"] = sdp;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
sv6.Post("/controller/comms/call/([^/]+)/answer", callAnswer);
auto callReject = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
purgeCall(callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "rejected";
setContent(req, res, out.dump());
};
s.Post("/controller/comms/call/([^/]+)/reject", callReject);
sv6.Post("/controller/comms/call/([^/]+)/reject", callReject);
auto callEnd = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
purgeCall(callId);
nlohmann::json out = nlohmann::json::object();
out["callId"] = callId;
out["state"] = "ended";
setContent(req, res, out.dump());
};
s.Post("/controller/comms/call/([^/]+)/end", callEnd);
sv6.Post("/controller/comms/call/([^/]+)/end", callEnd);
auto callIcePost = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
try {
nlohmann::json body = OSUtils::jsonParse(req.body);
IceEntry entry;
entry.fromPeerId = body.contains("fromPeerId") && body["fromPeerId"].is_string()
? lowerId(body["fromPeerId"].get<std::string>()) : "";
entry.candidate = body.contains("candidate") && body["candidate"].is_string()
? body["candidate"].get<std::string>() : "";
entry.sdpMid = body.contains("sdpMid") && body["sdpMid"].is_string()
? body["sdpMid"].get<std::string>() : "";
entry.sdpMLineIndex = body.contains("sdpMLineIndex") && body["sdpMLineIndex"].is_number_integer()
? (int)body["sdpMLineIndex"].get<int64_t>() : 0;
entry.at = OSUtils::now();
if (entry.candidate.empty()) {
res.status = 400;
setContent(req, res, "{\"error\":\"candidate is required\"}");
return;
}
Mutex::Lock _l(g_commsMutex);
g_iceByCall[callId].push_back(entry);
nlohmann::json out = nlohmann::json::object();
out["ok"] = true;
setContent(req, res, out.dump());
} catch ( ... ) {
res.status = 400;
setContent(req, res, "{\"error\":\"invalid JSON\"}");
}
};
s.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
sv6.Post("/controller/comms/call/([^/]+)/ice", callIcePost);
auto callIceGet = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
int64_t since = 0;
if (req.has_param("since")) {
try {
since = std::stoll(req.get_param_value("since"));
} catch ( ... ) {
since = 0;
}
}
const std::string peerId = lowerId(req.has_param("peerId") ? req.get_param_value("peerId") : "");
nlohmann::json candidates = nlohmann::json::array();
Mutex::Lock _l(g_commsMutex);
std::map<std::string, std::vector<IceEntry> >::const_iterator it = g_iceByCall.find(callId);
if (it != g_iceByCall.end()) {
for (std::vector<IceEntry>::const_iterator e = it->second.begin(); e != it->second.end(); ++e) {
if (e->at <= since) {
continue;
}
if (!peerId.empty() && e->fromPeerId == peerId) {
continue;
}
nlohmann::json item = nlohmann::json::object();
item["candidate"] = e->candidate;
item["sdpMid"] = e->sdpMid;
item["sdpMLineIndex"] = e->sdpMLineIndex;
item["at"] = e->at;
candidates.push_back(item);
}
}
nlohmann::json out = nlohmann::json::object();
out["candidates"] = candidates;
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
sv6.Get("/controller/comms/call/([^/]+)/ice", callIceGet);
auto callGet = [setContent](const httplib::Request &req, httplib::Response &res) {
const std::string callId = req.matches[1];
Mutex::Lock _l(g_commsMutex);
std::map<std::string, CallState>::const_iterator it = g_calls.find(callId);
if (it == g_calls.end()) {
res.status = 404;
setContent(req, res, "{\"error\":\"call not found\"}");
return;
}
nlohmann::json out = callToIncomingJson(it->second);
if (it->second.state == "accepted" && !it->second.answerSdp.empty()) {
out["answerSdp"] = it->second.answerSdp;
}
setContent(req, res, out.dump());
};
s.Get("/controller/comms/call/([^/]+)", callGet);
sv6.Get("/controller/comms/call/([^/]+)", callGet);
}
} // namespace ZeroTier