UNPKG

node-datachannel

Version:

WebRTC For Node.js and Electron. libdatachannel node bindings.

770 lines (662 loc) 24.8 kB
#include "web-socket-wrapper.h" #include "plog/Log.h" Napi::FunctionReference WebSocketWrapper::constructor = Napi::FunctionReference(); std::unordered_set<WebSocketWrapper *> WebSocketWrapper::instances; void WebSocketWrapper::CloseAll() { PLOG_DEBUG << "CloseAll() called"; auto copy(instances); for (auto inst : copy) inst->doClose(); } void WebSocketWrapper::CleanupAll() { PLOG_DEBUG << "CleanupAll() called"; auto copy(instances); for (auto inst : copy) inst->doCleanup(); } Napi::Object WebSocketWrapper::Init(Napi::Env env, Napi::Object exports) { Napi::HandleScope scope(env); Napi::Function func = DefineClass( env, "WebSocket", { InstanceMethod("open", &WebSocketWrapper::open), InstanceMethod("close", &WebSocketWrapper::close), InstanceMethod("forceClose", &WebSocketWrapper::forceClose), InstanceMethod("sendMessage", &WebSocketWrapper::sendMessage), InstanceMethod("sendMessageBinary", &WebSocketWrapper::sendMessageBinary), InstanceMethod("isOpen", &WebSocketWrapper::isOpen), InstanceMethod("bufferedAmount", &WebSocketWrapper::bufferedAmount), InstanceMethod("maxMessageSize", &WebSocketWrapper::maxMessageSize), InstanceMethod("setBufferedAmountLowThreshold", &WebSocketWrapper::setBufferedAmountLowThreshold), InstanceMethod("onOpen", &WebSocketWrapper::onOpen), InstanceMethod("onClosed", &WebSocketWrapper::onClosed), InstanceMethod("onError", &WebSocketWrapper::onError), InstanceMethod("onBufferedAmountLow", &WebSocketWrapper::onBufferedAmountLow), InstanceMethod("onMessage", &WebSocketWrapper::onMessage), InstanceMethod("remoteAddress", &WebSocketWrapper::remoteAddress), InstanceMethod("path", &WebSocketWrapper::path), }); // If this is not the first call, we don't want to reassign the constructor (hot-reload problem) if(constructor.IsEmpty()) { constructor = Napi::Persistent(func); constructor.SuppressDestruct(); } exports.Set("WebSocket", func); return exports; } WebSocketWrapper::WebSocketWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap<WebSocketWrapper>(info) { PLOG_DEBUG << "Constructor called"; Napi::Env env = info.Env(); // Create WebSocket using rtc::WebSocket provided by WebSocketServer if (info.Length() > 1) { mWebSocketPtr = *(info[1].As<Napi::External<std::shared_ptr<rtc::WebSocket>>>().Data()); PLOG_DEBUG << "Using WebSocket got from WebSocketServer"; instances.insert(this); // Closed callback must be set to trigger cleanup mOnClosedCallback = std::make_unique<ThreadSafeCallback>(Napi::Function::New(info.Env(), [](const Napi::CallbackInfo &) {})); return; } // Create WebSocket without config if (info.Length() == 0) { try { PLOG_DEBUG << "Creating a new WebSocket without config"; mWebSocketPtr = std::make_unique<rtc::WebSocket>(); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error while creating WebSocket without config: ") + ex.what()).ThrowAsJavaScriptException(); return; } instances.insert(this); // Closed callback must be set to trigger cleanup mOnClosedCallback = std::make_unique<ThreadSafeCallback>(Napi::Function::New(info.Env(), [](const Napi::CallbackInfo &) {})); return; } // Create WebSocket with config PLOG_DEBUG << "Creating a new WebSocket with config"; Napi::Object config = info[0].As<Napi::Object>(); rtc::WebSocketConfiguration webSocketConfig; if (config.Has("disableTlsVerification")) { if (!config.Get("disableTlsVerification").IsBoolean()) { Napi::TypeError::New(info.Env(), "disableTlsVerification must be boolean").ThrowAsJavaScriptException(); return; } webSocketConfig.disableTlsVerification = config.Get("disableTlsVerification").ToBoolean(); } // Proxy Server if (config.Has("proxyServer") && config.Get("proxyServer").IsObject()) { Napi::Object proxyServer = config.Get("proxyServer").As<Napi::Object>(); // IP std::string ip = proxyServer.Get("ip").As<Napi::String>(); // Port uint16_t port = proxyServer.Get("port").As<Napi::Number>().Uint32Value(); // Type std::string strType = proxyServer.Get("type").As<Napi::String>().ToString(); rtc::ProxyServer::Type type = rtc::ProxyServer::Type::Http; if (strType == "Socks5") type = rtc::ProxyServer::Type::Socks5; // Username & Password std::string username = ""; std::string password = ""; if (proxyServer.Get("username").IsString()) username = proxyServer.Get("username").As<Napi::String>().ToString(); if (proxyServer.Get("password").IsString()) password = proxyServer.Get("password").As<Napi::String>().ToString(); webSocketConfig.proxyServer = rtc::ProxyServer(type, ip, port, username, password); } if (config.Has("protocols")) { if (!config.Get("protocols").IsArray()) { Napi::TypeError::New(info.Env(), "protocols must be an array").ThrowAsJavaScriptException(); return; } Napi::Array protocols = config.Get("protocols").As<Napi::Array>(); for (uint32_t i = 0; i < protocols.Length(); i++) { webSocketConfig.protocols.push_back(protocols.Get(i).ToString()); } } if (config.Has("connectionTimeout")) { if (!config.Get("connectionTimeout").IsNumber()) { Napi::TypeError::New(info.Env(), "connectionTimeout must be a number").ThrowAsJavaScriptException(); return; } webSocketConfig.connectionTimeout = std::chrono::milliseconds(config.Get("connectionTimeout").ToNumber().Int64Value()); } if (config.Has("pingInterval")) { if (!config.Get("pingInterval").IsNumber()) { Napi::TypeError::New(info.Env(), "pingInterval must be a number").ThrowAsJavaScriptException(); return; } webSocketConfig.pingInterval = std::chrono::milliseconds(config.Get("pingInterval").ToNumber().Int64Value()); } if (config.Has("maxOutstandingPings")) { if (!config.Get("maxOutstandingPings").IsNumber()) { Napi::TypeError::New(info.Env(), "maxOutstandingPings must be a number").ThrowAsJavaScriptException(); return; } webSocketConfig.maxOutstandingPings = config.Get("maxOutstandingPings").ToNumber().Int32Value(); } if (config.Has("caCertificatePemFile")) { if (!config.Get("caCertificatePemFile").IsString()) { Napi::TypeError::New(info.Env(), "caCertificatePemFile must be a string").ThrowAsJavaScriptException(); return; } webSocketConfig.caCertificatePemFile = config.Get("caCertificatePemFile").ToString(); } if (config.Has("certificatePemFile")) { if (!config.Get("certificatePemFile").IsString()) { Napi::TypeError::New(info.Env(), "certificatePemFile must be a string").ThrowAsJavaScriptException(); return; } webSocketConfig.certificatePemFile = config.Get("certificatePemFile").ToString(); } if (config.Has("keyPemFile")) { if (!config.Get("keyPemFile").IsString()) { Napi::TypeError::New(info.Env(), "keyPemFile must be a string").ThrowAsJavaScriptException(); return; } webSocketConfig.keyPemFile = config.Get("keyPemFile").ToString(); } if (config.Has("keyPemPass")) { if (!config.Get("keyPemPass").IsString()) { Napi::TypeError::New(info.Env(), "keyPemPass must be a string").ThrowAsJavaScriptException(); return; } webSocketConfig.keyPemPass = config.Get("keyPemPass").ToString(); } if (config.Has("maxMessageSize")) { if (!config.Get("maxMessageSize").IsNumber()) { Napi::TypeError::New(info.Env(), "maxMessageSize must be a number").ThrowAsJavaScriptException(); return; } webSocketConfig.maxMessageSize = config.Get("maxMessageSize").ToNumber().Int32Value(); } // Create WebSocket try { PLOG_DEBUG << "Creating a new WebSocket"; mWebSocketPtr = std::make_unique<rtc::WebSocket>(webSocketConfig); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error while creating WebSocket: ") + ex.what()).ThrowAsJavaScriptException(); return; } PLOG_DEBUG << "WebSocket created"; // Closed callback must set to trigger cleanup mOnClosedCallback = std::make_unique<ThreadSafeCallback>(Napi::Function::New(info.Env(), [](const Napi::CallbackInfo &) {})); instances.insert(this); } WebSocketWrapper::~WebSocketWrapper() { PLOG_DEBUG << "Destructor called"; doClose(); } void WebSocketWrapper::doClose() { PLOG_DEBUG << "doClose() called"; if (mWebSocketPtr) { PLOG_DEBUG << "Closing..."; try { mWebSocketPtr->close(); mWebSocketPtr.reset(); } catch (std::exception &ex) { std::cerr << std::string("libdatachannel error while closing WebSocket: ") + ex.what() << std::endl; return; } } mOnOpenCallback.reset(); mOnErrorCallback.reset(); mOnBufferedAmountLowCallback.reset(); mOnMessageCallback.reset(); } void WebSocketWrapper::doForceClose() { PLOG_DEBUG << "doForceClose() called"; if (mWebSocketPtr) { PLOG_DEBUG << "Force closing..."; try { mWebSocketPtr->forceClose(); mWebSocketPtr.reset(); } catch (std::exception &ex) { std::cerr << std::string("libdatachannel error while force closing WebSocket: ") + ex.what() << std::endl; return; } } mOnOpenCallback.reset(); mOnErrorCallback.reset(); mOnBufferedAmountLowCallback.reset(); mOnMessageCallback.reset(); } void WebSocketWrapper::doCleanup() { PLOG_DEBUG << "doCleanup() called"; mOnClosedCallback.reset(); instances.erase(this); } void WebSocketWrapper::open(const Napi::CallbackInfo &info) { PLOG_DEBUG << "open() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { Napi::Error::New(env, "open() called on destroyed WebSocket").ThrowAsJavaScriptException(); return; } if (info.Length() < 1 || !info[0].IsString()) { Napi::TypeError::New(env, "url must be string").ThrowAsJavaScriptException(); return; } try { mWebSocketPtr->open(info[0].As<Napi::String>().ToString()); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error while opening WebSocket: ") + ex.what()).ThrowAsJavaScriptException(); return; } } void WebSocketWrapper::close(const Napi::CallbackInfo &info) { PLOG_DEBUG << "close() called"; doClose(); } void WebSocketWrapper::forceClose(const Napi::CallbackInfo &info) { PLOG_DEBUG << "forceClose() called"; doForceClose(); } Napi::Value WebSocketWrapper::sendMessage(const Napi::CallbackInfo &info) { PLOG_DEBUG << "sendMessage() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "sendMessage() called on destroyed channel").ThrowAsJavaScriptException(); return info.Env().Null(); } Napi::Env env = info.Env(); int length = info.Length(); // Allow call with NULL if (length < 1 || (!info[0].IsString() && !info[0].IsNull())) { Napi::TypeError::New(env, "String or Null expected").ThrowAsJavaScriptException(); return info.Env().Null(); } try { return Napi::Boolean::New(info.Env(), mWebSocketPtr->send(info[0].As<Napi::String>().ToString())); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error while sending data channel message: ") + ex.what()).ThrowAsJavaScriptException(); return Napi::Boolean::New(info.Env(), false); } } Napi::Value WebSocketWrapper::sendMessageBinary(const Napi::CallbackInfo &info) { PLOG_DEBUG << "sendMessageBinary() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "sendMessagBinary() called on destroyed channel").ThrowAsJavaScriptException(); return info.Env().Null(); } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsBuffer()) { Napi::TypeError::New(env, "Buffer expected").ThrowAsJavaScriptException(); return info.Env().Null(); } try { Napi::Uint8Array buffer = info[0].As<Napi::Uint8Array>(); return Napi::Boolean::New(info.Env(), mWebSocketPtr->send((std::byte *)buffer.Data(), buffer.ByteLength())); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error while sending data channel message: ") + ex.what()).ThrowAsJavaScriptException(); return Napi::Boolean::New(info.Env(), false); } } Napi::Value WebSocketWrapper::isOpen(const Napi::CallbackInfo &info) { PLOG_DEBUG << "isOpen() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { return Napi::Boolean::New(info.Env(), false); } try { return Napi::Boolean::New(info.Env(), mWebSocketPtr->isOpen()); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return Napi::Boolean::New(info.Env(), false); } } Napi::Value WebSocketWrapper::bufferedAmount(const Napi::CallbackInfo &info) { PLOG_DEBUG << "bufferedAmount() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { return Napi::Number::New(info.Env(), 0); } try { return Napi::Number::New(info.Env(), mWebSocketPtr->bufferedAmount()); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return Napi::Number::New(info.Env(), 0); } } Napi::Value WebSocketWrapper::maxMessageSize(const Napi::CallbackInfo &info) { PLOG_DEBUG << "maxMessageSize() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { return Napi::Number::New(info.Env(), 0); } try { return Napi::Number::New(info.Env(), mWebSocketPtr->maxMessageSize()); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return Napi::Number::New(info.Env(), 0); } } Napi::Value WebSocketWrapper::remoteAddress(const Napi::CallbackInfo &info) { PLOG_DEBUG << "remoteAddress() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { return env.Undefined(); } try { auto address = mWebSocketPtr->remoteAddress(); if (address.has_value()) { return Napi::String::New(info.Env(), address.value()); } else { return env.Undefined(); } } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return env.Undefined(); } } Napi::Value WebSocketWrapper::path(const Napi::CallbackInfo &info) { PLOG_DEBUG << "path() called"; Napi::Env env = info.Env(); if (!mWebSocketPtr) { return env.Undefined(); } try { auto path = mWebSocketPtr->path(); if (path.has_value()) { return Napi::String::New(info.Env(), path.value()); } else { return env.Undefined(); } } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return env.Undefined(); } } void WebSocketWrapper::setBufferedAmountLowThreshold(const Napi::CallbackInfo &info) { PLOG_DEBUG << "setBufferedAmountLowThreshold() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "setBufferedAmountLowThreshold() called on destroyed channel").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsNumber()) { Napi::TypeError::New(env, "Number expected").ThrowAsJavaScriptException(); return; } try { mWebSocketPtr->setBufferedAmountLowThreshold(info[0].ToNumber().Uint32Value()); } catch (std::exception &ex) { Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); return; } } void WebSocketWrapper::onOpen(const Napi::CallbackInfo &info) { PLOG_DEBUG << "new onOpen() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "onOpen() called on destroyed channel").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); return; } // Callback mOnOpenCallback = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>()); mWebSocketPtr->onOpen([&]() { PLOG_DEBUG << "onOpen cb received from rtc"; if (mOnOpenCallback) mOnOpenCallback->call([this](Napi::Env env, std::vector<napi_value> &args) { PLOG_DEBUG << "mOnOpenCallback call(1)"; // Check the WebSocket is not closed if(instances.find(this) == instances.end()) { PLOG_DEBUG << "WebSocket not found in instances"; throw ThreadSafeCallback::CancelException(); } // This will run in main thread and needs to construct the // arguments for the call args = {}; PLOG_DEBUG << "mOnOpenCallback call(2)"; }); }); } void WebSocketWrapper::onClosed(const Napi::CallbackInfo &info) { PLOG_DEBUG << "onClosed() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "onClosed() called on destroyed WebSocket").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); return; } // Callback mOnClosedCallback = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>()); mWebSocketPtr->onClosed([&]() { PLOG_DEBUG << "onClosed cb received from rtc"; if (mOnClosedCallback) mOnClosedCallback->call([this](Napi::Env env, std::vector<napi_value> &args) { PLOG_DEBUG << "mOnClosedCallback call"; // Do not check if the data channel has been closed here // This will run in main thread and needs to construct the // arguments for the call args = {}; },[this]{ doCleanup(); }); }); } void WebSocketWrapper::onError(const Napi::CallbackInfo &info) { PLOG_DEBUG << "onError() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "onError() called on destroyed channel").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); return; } // Callback mOnErrorCallback = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>()); mWebSocketPtr->onError([&](std::string error) { PLOG_DEBUG << "onError cb received from rtc"; if (mOnErrorCallback) mOnErrorCallback->call([this, error = std::move(error)](Napi::Env env, std::vector<napi_value> &args) { PLOG_DEBUG << "mOnErrorCallback call(1)"; // Check the data channel is not closed if(instances.find(this) == instances.end()) throw ThreadSafeCallback::CancelException(); // This will run in main thread and needs to construct the // arguments for the call args = {Napi::String::New(env, error)}; PLOG_DEBUG << "mOnErrorCallback call(2)"; }); }); } void WebSocketWrapper::onBufferedAmountLow(const Napi::CallbackInfo &info) { PLOG_DEBUG << "onBufferedAmountLow() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "onBufferedAmountLow() called on destroyed channel").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); return; } // Callback mOnBufferedAmountLowCallback = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>()); mWebSocketPtr->onBufferedAmountLow([&]() { PLOG_DEBUG << "onBufferedAmountLow cb received from rtc"; if (mOnBufferedAmountLowCallback) mOnBufferedAmountLowCallback->call([this](Napi::Env env, std::vector<napi_value> &args) { PLOG_DEBUG << "mOnBufferedAmountLowCallback call(1)"; // Check the data channel is not closed if(instances.find(this) == instances.end()) throw ThreadSafeCallback::CancelException(); // This will run in main thread and needs to construct the // arguments for the call args = {}; PLOG_DEBUG << "mOnBufferedAmountLowCallback call(2)"; }); }); } void WebSocketWrapper::onMessage(const Napi::CallbackInfo &info) { PLOG_DEBUG << "onMessage() called"; if (!mWebSocketPtr) { Napi::Error::New(info.Env(), "onMessage() called on destroyed channel").ThrowAsJavaScriptException(); return; } Napi::Env env = info.Env(); int length = info.Length(); if (length < 1 || !info[0].IsFunction()) { Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); return; } // Callback mOnMessageCallback = std::make_unique<ThreadSafeCallback>(info[0].As<Napi::Function>()); PLOG_DEBUG << "setting onMessage cb on mWebSocketPtr"; mWebSocketPtr->onMessage([&](std::variant<rtc::binary, std::string> message) { PLOG_DEBUG << "onMessage cb received from rtc"; if (mOnMessageCallback) mOnMessageCallback->call([this, message = std::move(message)](Napi::Env env, std::vector<napi_value> &args) { PLOG_DEBUG << "mOnMessageCallback call(1)"; // Check the data channel is not closed if(instances.find(this) == instances.end()) throw ThreadSafeCallback::CancelException(); // This will run in main thread and needs to construct the // arguments for the call if (std::holds_alternative<std::string>(message)) { args = {Napi::String::New(env, std::get<std::string>(message))}; } else { auto bin = std::get<rtc::binary>(std::move(message)); args = {Napi::Buffer<std::byte>::Copy(env, bin.data(), bin.size())}; } PLOG_DEBUG << "mOnMessageCallback call(2)"; }); }); }