secure chat addon and stun
This commit is contained in:
1 parent
a45fa67ed2
commit
813d3aa06e
40 files changed
+2767
-189
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
/****/
|
||||
|
||||
#ifndef ZT_COMMSCONTROLLER_HPP
|
||||
#define ZT_COMMSCONTROLLER_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <cpp-httplib/httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace ZeroTier {
|
||||
|
||||
/**
|
||||
* Ephemeral in-memory WebRTC signaling fallback and webrtc-config API.
|
||||
*/
|
||||
class CommsController
|
||||
{
|
||||
public:
|
||||
void 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);
|
||||
};
|
||||
|
||||
} // namespace ZeroTier
|
||||
|
||||
#endif
|
||||
Reference in new issue
Block a user