From 57df190321ff8fd4cc20adef020a7d9da0448b0f Mon Sep 17 00:00:00 2001 From: achingbrain Date: Wed, 12 Jun 2024 09:38:25 +0100 Subject: [PATCH] feat: support setting ICE ufrag and pwd, and missing config values Updates the implementation to match the latest libdatachannel with features for setting ICE ufrag/pwd, reading the remote cert fingerprint and receiving callbacks for unhandled mux requests. Also adds pass-through for missing config values. --- API.md | 28 +++++ CMakeLists.txt | 1 + src/cpp/ice-udp-mux-listener-wrapper.cpp | 150 +++++++++++++++++++++++ src/cpp/ice-udp-mux-listener-wrapper.h | 43 +++++++ src/cpp/main.cpp | 2 + src/cpp/peer-connection-wrapper.cpp | 70 ++++++++++- src/cpp/peer-connection-wrapper.h | 1 + src/lib/index.ts | 19 ++- src/lib/types.ts | 25 ++++ src/polyfill/RTCPeerConnection.ts | 3 +- 10 files changed, 336 insertions(+), 6 deletions(-) create mode 100644 src/cpp/ice-udp-mux-listener-wrapper.cpp create mode 100644 src/cpp/ice-udp-mux-listener-wrapper.h diff --git a/API.md b/API.md index e95f9a96..c4aa5c91 100644 --- a/API.md +++ b/API.md @@ -16,12 +16,19 @@ export interface RtcConfig { bindAddress?: string; enableIceTcp?: boolean; enableIceUdpMux?: boolean; + disableAutoNegotiation?: boolean; + disableFingerprintVerification?: boolean; + disableAutoGathering?: boolean; + forceMediaTransport?: boolean; portRangeBegin?: number; portRangeEnd?: number; maxMessageSize?: number; mtu?: number; iceTransportPolicy?: TransportPolicy; disableFingerprintVerification?: boolean; + certificatePemFile?: string; + keyPemFile?: string; + keyPemPass?: string; } export const enum RelayType { @@ -69,6 +76,27 @@ export const enum DescriptionType { } ``` +**setLocalDescription: (sdp: string, init?: LocalDescriptionInit) => void** + +Set Local Description and optionally the ICE ufrag/pwd to use. These should not +be set as they will be generated automatically as per the spec. +``` +export interface LocalDescriptionInit { + iceUfrag?: string; + icePwd?: string; +} +``` + +**remoteFingerprint: () => CertificateFingerprint** + +Returns the certificate fingerprint used by the remote peer +``` +export interface CertificateFingerprint { + value: string; + algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2'; +} +``` + **addRemoteCandidate: (candidate: string, mid: string) => void** Add remote candidate info diff --git a/CMakeLists.txt b/CMakeLists.txt index 18a1fc8a..a43956f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_library(${PROJECT_NAME} SHARED src/cpp/media-audio-wrapper.cpp src/cpp/media-video-wrapper.cpp src/cpp/data-channel-wrapper.cpp + src/cpp/ice-udp-mux-listener-wrapper.cpp src/cpp/peer-connection-wrapper.cpp src/cpp/thread-safe-callback.cpp src/cpp/web-socket-wrapper.cpp diff --git a/src/cpp/ice-udp-mux-listener-wrapper.cpp b/src/cpp/ice-udp-mux-listener-wrapper.cpp new file mode 100644 index 00000000..07dc162d --- /dev/null +++ b/src/cpp/ice-udp-mux-listener-wrapper.cpp @@ -0,0 +1,150 @@ +#include "ice-udp-mux-listener-wrapper.h" + +#include "plog/Log.h" + +#include +#include + +Napi::FunctionReference IceUdpMuxListenerWrapper::constructor = Napi::FunctionReference(); +std::unordered_set IceUdpMuxListenerWrapper::instances; + +void IceUdpMuxListenerWrapper::StopAll() +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper StopAll() called"; + auto copy(instances); + for (auto inst : copy) + inst->doCleanup(); +} + +Napi::Object IceUdpMuxListenerWrapper::Init(Napi::Env env, Napi::Object exports) +{ + Napi::HandleScope scope(env); + + Napi::Function func = DefineClass( + env, + "IceUdpMuxListener", + { + InstanceMethod("stop", &IceUdpMuxListenerWrapper::stop), + InstanceMethod("onUnhandledStunRequest", &IceUdpMuxListenerWrapper::onUnhandledStunRequest), + InstanceMethod("port", &IceUdpMuxListenerWrapper::port), + InstanceMethod("address", &IceUdpMuxListenerWrapper::address) + }); + + // 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("IceUdpMuxListener", func); + return exports; +} + +IceUdpMuxListenerWrapper::IceUdpMuxListenerWrapper(const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper Constructor called"; + Napi::Env env = info.Env(); + int length = info.Length(); + + // We expect (Number, String?) as param + if (length > 0 && info[0].IsNumber()) { + // Port + mPort = info[0].As().ToNumber().Uint32Value(); + } else { + Napi::TypeError::New(env, "Port (Number) and optional Address (String) expected").ThrowAsJavaScriptException(); + return; + } + + if (length > 1 && info[1].IsString()) { + // Address + mAddress = info[1].As().ToString(); + } + + iceUdpMuxListenerPtr = std::make_unique(mPort, mAddress); + instances.insert(this); +} + +IceUdpMuxListenerWrapper::~IceUdpMuxListenerWrapper() +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper Destructor called"; + doCleanup(); +} + +void IceUdpMuxListenerWrapper::doCleanup() +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper::doCleanup() called"; + + if (iceUdpMuxListenerPtr) + { + iceUdpMuxListenerPtr->stop(); + iceUdpMuxListenerPtr.reset(); + } + + mOnUnhandledStunRequestCallback.reset(); + instances.erase(this); +} + +Napi::Value IceUdpMuxListenerWrapper::port(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + + return Napi::Number::New(env, mPort); +} + +Napi::Value IceUdpMuxListenerWrapper::address(const Napi::CallbackInfo &info) +{ + Napi::Env env = info.Env(); + + if (!mAddress.has_value()) { + return env.Undefined(); + } + + return Napi::String::New(env, mAddress.value()); +} + +void IceUdpMuxListenerWrapper::stop(const Napi::CallbackInfo &info) +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper::stop() called"; + doCleanup(); +} + +void IceUdpMuxListenerWrapper::onUnhandledStunRequest(const Napi::CallbackInfo &info) +{ + PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() called"; + Napi::Env env = info.Env(); + int length = info.Length(); + + if (!iceUdpMuxListenerPtr) + { + Napi::Error::New(env, "IceUdpMuxListenerWrapper::onUnhandledStunRequest() called on destroyed IceUdpMuxListener").ThrowAsJavaScriptException(); + return; + } + + if (length < 1 || !info[0].IsFunction()) + { + Napi::TypeError::New(env, "Function expected").ThrowAsJavaScriptException(); + return; + } + + // Callback + mOnUnhandledStunRequestCallback = std::make_unique(info[0].As()); + + iceUdpMuxListenerPtr->OnUnhandledStunRequest([&](rtc::IceUdpMuxRequest request) + { + PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() IceUdpMuxCallback call(1)"; + + if (mOnUnhandledStunRequestCallback) { + mOnUnhandledStunRequestCallback->call([request = std::move(request)](Napi::Env env, std::vector &args) { + Napi::Object reqObj = Napi::Object::New(env); + reqObj.Set("ufrag", request.remoteUfrag.c_str()); + reqObj.Set("host", request.remoteAddress.c_str()); + reqObj.Set("port", request.remotePort); + + args = {reqObj}; + }); + } + + PLOG_DEBUG << "IceUdpMuxListenerWrapper::onUnhandledStunRequest() IceUdpMuxCallback call(2)"; + }); +} diff --git a/src/cpp/ice-udp-mux-listener-wrapper.h b/src/cpp/ice-udp-mux-listener-wrapper.h new file mode 100644 index 00000000..bef870c9 --- /dev/null +++ b/src/cpp/ice-udp-mux-listener-wrapper.h @@ -0,0 +1,43 @@ +#ifndef ICE_UDP_MUX_LISTENER_WRAPPER_H +#define ICE_UDP_MUX_LISTENER_WRAPPER_H + +#include +#include +#include + +#include "thread-safe-callback.h" + +class IceUdpMuxListenerWrapper : public Napi::ObjectWrap +{ +public: + static Napi::Object Init(Napi::Env env, Napi::Object exports); + IceUdpMuxListenerWrapper(const Napi::CallbackInfo &info); + ~IceUdpMuxListenerWrapper(); + + // Functions + void stop(const Napi::CallbackInfo &info); + void onUnhandledStunRequest(const Napi::CallbackInfo &info); + + // Stop listening on all ports + static void StopAll(); + + // Properties + Napi::Value port(const Napi::CallbackInfo &info); + Napi::Value address(const Napi::CallbackInfo &info); + Napi::Value unhandledStunRequestCallback(const Napi::CallbackInfo &info); + + // Callback Ptrs + std::unique_ptr mOnUnhandledStunRequestCallback = nullptr; + +private: + static Napi::FunctionReference constructor; + static std::unordered_set instances; + + void doCleanup(); + + std::optional mAddress; + uint16_t mPort; + std::unique_ptr iceUdpMuxListenerPtr = nullptr; +}; + +#endif // ICE_UDP_MUX_LISTENER_WRAPPER_H diff --git a/src/cpp/main.cpp b/src/cpp/main.cpp index 0f566748..ea1d92ad 100644 --- a/src/cpp/main.cpp +++ b/src/cpp/main.cpp @@ -2,6 +2,7 @@ #include "rtc-wrapper.h" #include "peer-connection-wrapper.h" #include "data-channel-wrapper.h" +#include "ice-udp-mux-listener-wrapper.h" #include "media-rtcpreceivingsession-wrapper.h" #include "media-track-wrapper.h" #include "media-video-wrapper.h" @@ -17,6 +18,7 @@ Napi::Object InitAll(Napi::Env env, Napi::Object exports) VideoWrapper::Init(env, exports); AudioWrapper::Init(env, exports); DataChannelWrapper::Init(env, exports); + IceUdpMuxListenerWrapper::Init(env, exports); PeerConnectionWrapper::Init(env, exports); WebSocketWrapper::Init(env, exports); WebSocketServerWrapper::Init(env, exports); diff --git a/src/cpp/peer-connection-wrapper.cpp b/src/cpp/peer-connection-wrapper.cpp index 4e3d9e77..200a9cef 100644 --- a/src/cpp/peer-connection-wrapper.cpp +++ b/src/cpp/peer-connection-wrapper.cpp @@ -41,6 +41,7 @@ Napi::Object PeerConnectionWrapper::Init(Napi::Env env, Napi::Object exports) InstanceMethod("setRemoteDescription", &PeerConnectionWrapper::setRemoteDescription), InstanceMethod("localDescription", &PeerConnectionWrapper::localDescription), InstanceMethod("remoteDescription", &PeerConnectionWrapper::remoteDescription), + InstanceMethod("remoteFingerprint", &PeerConnectionWrapper::remoteFingerprint), InstanceMethod("addRemoteCandidate", &PeerConnectionWrapper::addRemoteCandidate), InstanceMethod("createDataChannel", &PeerConnectionWrapper::createDataChannel), InstanceMethod("addTrack", &PeerConnectionWrapper::addTrack), @@ -214,6 +215,10 @@ PeerConnectionWrapper::PeerConnectionWrapper(const Napi::CallbackInfo &info) : N if (config.Get("disableAutoNegotiation").IsBoolean()) rtcConfig.disableAutoNegotiation = config.Get("disableAutoNegotiation").As(); + // disableAutoGathering option + if (config.Get("disableAutoGathering").IsBoolean()) + rtcConfig.disableAutoGathering = config.Get("disableAutoGathering").As(); + // forceMediaTransport option if (config.Get("forceMediaTransport").IsBoolean()) rtcConfig.forceMediaTransport = config.Get("forceMediaTransport").As(); @@ -251,6 +256,17 @@ PeerConnectionWrapper::PeerConnectionWrapper(const Napi::CallbackInfo &info) : N rtcConfig.disableFingerprintVerification = config.Get("disableFingerprintVerification").As(); } + // Specify certificate to use if set + if (config.Get("certificatePemFile").IsString()) { + rtcConfig.certificatePemFile = config.Get("certificatePemFile").As().ToString(); + } + if (config.Get("keyPemFile").IsString()) { + rtcConfig.keyPemFile = config.Get("keyPemFile").As().ToString(); + } + if (config.Get("keyPemPass").IsString()) { + rtcConfig.keyPemPass = config.Get("keyPemPass").As().ToString(); + } + // Create peer-connection try { @@ -331,6 +347,7 @@ void PeerConnectionWrapper::setLocalDescription(const Napi::CallbackInfo &info) } rtc::Description::Type type = rtc::Description::Type::Unspec; + rtc::LocalDescriptionInit init; // optional if (length > 0) @@ -356,7 +373,29 @@ void PeerConnectionWrapper::setLocalDescription(const Napi::CallbackInfo &info) type = rtc::Description::Type::Rollback; } - mRtcPeerConnPtr->setLocalDescription(type); + // optional + if (length > 1) + { + PLOG_DEBUG << "setLocalDescription() called with LocalDescriptionInit"; + + if (info[1].IsObject()) + { + PLOG_DEBUG << "setLocalDescription() called with LocalDescriptionInit as object"; + Napi::Object obj = info[1].As(); + + if (obj.Get("iceUfrag").IsString()) { + PLOG_DEBUG << "setLocalDescription() has ufrag"; + init.iceUfrag = obj.Get("iceUfrag").As(); + } + + if (obj.Get("icePwd").IsString()) { + PLOG_DEBUG << "setLocalDescription() has password"; + init.icePwd = obj.Get("icePwd").As(); + } + } + } + + mRtcPeerConnPtr->setLocalDescription(type, init); } void PeerConnectionWrapper::setRemoteDescription(const Napi::CallbackInfo &info) @@ -1002,7 +1041,34 @@ Napi::Value PeerConnectionWrapper::maxMessageSize(const Napi::CallbackInfo &info try { - return Napi::Number::New(env, mRtcPeerConnPtr->remoteMaxMessageSize()); + return Napi::Array::New(env, mRtcPeerConnPtr->remoteMaxMessageSize()); + } + catch (std::exception &ex) + { + Napi::Error::New(env, std::string("libdatachannel error: ") + ex.what()).ThrowAsJavaScriptException(); + return Napi::Number::New(info.Env(), 0); + } +} + +Napi::Value PeerConnectionWrapper::remoteFingerprint(const Napi::CallbackInfo &info) +{ + PLOG_DEBUG << "remoteFingerprints() called"; + Napi::Env env = info.Env(); + + if (!mRtcPeerConnPtr) + { + return Napi::Number::New(info.Env(), 0); + } + + try + { + auto fingerprint = mRtcPeerConnPtr->remoteFingerprint(); + + Napi::Object fingerprintObject = Napi::Object::New(env); + fingerprintObject.Set("value", fingerprint.value); + fingerprintObject.Set("algorithm", rtc::CertificateFingerprint::AlgorithmIdentifier(fingerprint.algorithm)); + + return fingerprintObject; } catch (std::exception &ex) { diff --git a/src/cpp/peer-connection-wrapper.h b/src/cpp/peer-connection-wrapper.h index 5896e307..3865c082 100644 --- a/src/cpp/peer-connection-wrapper.h +++ b/src/cpp/peer-connection-wrapper.h @@ -33,6 +33,7 @@ class PeerConnectionWrapper : public Napi::ObjectWrap Napi::Value iceState(const Napi::CallbackInfo &info); Napi::Value signalingState(const Napi::CallbackInfo &info); Napi::Value gatheringState(const Napi::CallbackInfo &info); + Napi::Value remoteFingerprint(const Napi::CallbackInfo &info); // Callbacks void onLocalDescription(const Napi::CallbackInfo &info); diff --git a/src/lib/index.ts b/src/lib/index.ts index e8754f97..5f4d7856 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,8 +1,9 @@ import nodeDataChannel from './node-datachannel'; import _DataChannelStream from './datachannel-stream'; import { WebSocketServer } from './websocket-server'; -import { Channel, DataChannelInitConfig, DescriptionType, Direction, LogLevel, RtcConfig, RTCIceConnectionState, RTCIceGatheringState, RTCPeerConnectionState, RTCSignalingState, SctpSettings, SelectedCandidateInfo } from './types'; +import { CertificateFingerprint, Channel, DataChannelInitConfig, DescriptionType, Direction, LocalDescriptionInit, LogLevel, RtcConfig, RTCIceConnectionState, RTCIceGatheringState, RTCPeerConnectionState, RTCSignalingState, SctpSettings, SelectedCandidateInfo } from './types'; import { WebSocket } from './websocket'; +import type { IceUdpMuxRequest } from './types'; export function preload(): void { nodeDataChannel.preload(); } export function initLogger(level: LogLevel): void { nodeDataChannel.initLogger(level); } @@ -114,10 +115,11 @@ export const DataChannel: { export interface PeerConnection { close(): void; - setLocalDescription(type?: DescriptionType): void; + setLocalDescription(type?: DescriptionType, init?: LocalDescriptionInit): void; setRemoteDescription(sdp: string, type: DescriptionType): void; localDescription(): { type: DescriptionType; sdp: string } | null; remoteDescription(): { type: DescriptionType; sdp: string } | null; + remoteFingerprint(): CertificateFingerprint; addRemoteCandidate(candidate: string, mid: string): void; createDataChannel(label: string, config?: DataChannelInitConfig): DataChannel; addTrack(media: Video | Audio): Track; @@ -145,6 +147,16 @@ export const PeerConnection: { new(peerName: string, config: RtcConfig): PeerConnection } = nodeDataChannel.PeerConnection +export interface IceUdpMuxListener { + address?: string; + port: number; + stop(): void; + onUnhandledStunRequest(cb: (req: IceUdpMuxRequest) => void): void; +} +export const IceUdpMuxListener: { + new(port: number, address?: string): IceUdpMuxListener +} = nodeDataChannel.IceUdpMuxListener + export class RtcpReceivingSession { // } @@ -168,7 +180,8 @@ export default { PeerConnection, WebSocket, WebSocketServer, - DataChannelStream + DataChannelStream, + IceUdpMuxListener }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 72d543c5..814a556e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -73,6 +73,10 @@ export interface RtcConfig { mtu?: number; iceTransportPolicy?: TransportPolicy; disableFingerprintVerification?: boolean; + disableAutoGathering?: boolean; + certificatePemFile?: string; + keyPemFile?: string; + keyPemPass?: string; } // Lowercase to match the description type string from libdatachannel @@ -87,6 +91,10 @@ export type RTCIceGathererState = "complete" | "gathering" | "new"; export type RTCIceGatheringState = "complete" | "gathering" | "new"; export type RTCSignalingState = "closed" | "have-local-offer" | "have-local-pranswer" | "have-remote-offer" | "have-remote-pranswer" | "stable"; +export interface LocalDescriptionInit { + iceUfrag?: string; + icePwd?: string; +} export interface DataChannelInitConfig { protocol?: string; @@ -97,6 +105,17 @@ export interface DataChannelInitConfig { maxRetransmits?: number; // Reliability } +export interface CertificateFingerprint { + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#value + */ + value: string; + /** + * @see https://developer.mozilla.org/en-US/docs/Web/API/RTCCertificate/getFingerprints#algorithm + */ + algorithm: 'sha-1' | 'sha-224' | 'sha-256' | 'sha-384' | 'sha-512' | 'md5' | 'md2'; +} + export interface SelectedCandidateInfo { address: string; port: number; @@ -109,3 +128,9 @@ export interface SelectedCandidateInfo { // Must be same as rtc enum class Direction export type Direction = 'SendOnly' | 'RecvOnly' | 'SendRecv' | 'Inactive' | 'Unknown' + +export interface IceUdpMuxRequest { + ufrag: string; + host: string; + port: number; +} diff --git a/src/polyfill/RTCPeerConnection.ts b/src/polyfill/RTCPeerConnection.ts index 61920f32..59318042 100644 --- a/src/polyfill/RTCPeerConnection.ts +++ b/src/polyfill/RTCPeerConnection.ts @@ -12,6 +12,7 @@ import RTCCertificate from './RTCCertificate'; // extend RTCConfiguration with peerIdentity interface RTCConfiguration extends globalThis.RTCConfiguration { peerIdentity?: string; + peerConnection?: PeerConnection; } export default class RTCPeerConnection extends EventTarget implements globalThis.RTCPeerConnection { @@ -121,7 +122,7 @@ export default class RTCPeerConnection extends EventTarget implements globalThis try { const peerIdentity = (config as any)?.peerIdentity ?? `peer-${getRandomString(7)}`; - this.#peerConnection = new PeerConnection(peerIdentity, + this.#peerConnection = config.peerConnection ?? new PeerConnection(peerIdentity, { ...config, iceServers: