/* * 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 #include #include #include 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 g_calls; std::map > g_iceByCall; std::map > 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::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(); } if (settings.contains("iceFallbackTimeoutMs") && settings["iceFallbackTimeoutMs"].is_number_integer()) { out["iceFallbackTimeoutMs"] = settings["iceFallbackTimeoutMs"].get(); } if (settings.contains("webhubPort") && settings["webhubPort"].is_number_integer()) { out["webhubPort"] = settings["webhubPort"].get(); } 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 &localConfigGetter, const std::function &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() : ""); const std::string sdp = body.contains("sdp") && body["sdp"].is_string() ? body["sdp"].get() : ""; 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(); } else if (body.contains("type") && body["type"].is_string()) { const std::string t = body["type"].get(); if (t == "voice" || t == "video") { callType = t; } } const std::string networkId = body.contains("networkId") && body["networkId"].is_string() ? lowerId(body["networkId"].get()) : ""; const std::string callerId = body.contains("callerId") && body["callerId"].is_string() ? lowerId(body["callerId"].get()) : "local"; const std::string callerName = body.contains("callerName") && body["callerName"].is_string() ? body["callerName"].get() : 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 >::const_iterator it = g_callsByPeer.find(peerId); if (it != g_callsByPeer.end()) { for (std::set::const_iterator c = it->second.begin(); c != it->second.end(); ++c) { std::map::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() : ""; if (sdp.empty()) { res.status = 400; setContent(req, res, "{\"error\":\"sdp is required\"}"); return; } Mutex::Lock _l(g_commsMutex); std::map::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()) : ""; entry.candidate = body.contains("candidate") && body["candidate"].is_string() ? body["candidate"].get() : ""; entry.sdpMid = body.contains("sdpMid") && body["sdpMid"].is_string() ? body["sdpMid"].get() : ""; entry.sdpMLineIndex = body.contains("sdpMLineIndex") && body["sdpMLineIndex"].is_number_integer() ? (int)body["sdpMLineIndex"].get() : 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 >::const_iterator it = g_iceByCall.find(callId); if (it != g_iceByCall.end()) { for (std::vector::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::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