Procházet zdrojové kódy

Merge pull request #34 from paullouisageneau/libjuice

Libjuice as alternative to Libnice
Paul-Louis Ageneau před 5 roky
rodič
revize
ed68ba5402

+ 3 - 0
.gitmodules

@@ -4,3 +4,6 @@
 [submodule "deps/plog"]
 	path = deps/plog
 	url = https://github.com/SergiusTheBest/plog
+[submodule "deps/libjuice"]
+	path = deps/libjuice
+	url = https://github.com/paullouisageneau/libjuice

+ 37 - 20
CMakeLists.txt

@@ -1,9 +1,12 @@
 cmake_minimum_required (VERSION 3.7)
 project (libdatachannel
 	DESCRIPTION "WebRTC Data Channels Library"
-	VERSION 0.2.1
+	VERSION 0.3.2
 	LANGUAGES CXX)
 
+option(USE_GNUTLS "Use GnuTLS instead of OpenSSL" OFF)
+option(USE_JUICE "Use libjuice instead of libnice" OFF)
+
 set(CMAKE_POSITION_INDEPENDENT_CODE ON)
 set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules)
 
@@ -33,6 +36,7 @@ set(TESTS_ANSWERER_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/test/p2p/answerer.cpp
 )
 
+
 # Hack because usrsctp uses CMAKE_SOURCE_DIR instead of CMAKE_CURRENT_SOURCE_DIR
 set(CMAKE_REQUIRED_FLAGS "-I${CMAKE_CURRENT_SOURCE_DIR}/deps/usrsctp/usrsctplib")
 
@@ -58,9 +62,8 @@ else()
   endif()
 endif()
 
-option(USE_GNUTLS "Use GnuTLS instead of OpenSSL" OFF)
-
-find_package(LibNice REQUIRED)
+add_library(Usrsctp::Usrsctp ALIAS usrsctp)
+add_library(Usrsctp::UsrsctpStatic ALIAS usrsctp-static)
 
 set(THREADS_PREFER_PTHREAD_FLAG ON)
 find_package(Threads REQUIRED)
@@ -74,10 +77,9 @@ target_include_directories(datachannel PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/includ
 target_include_directories(datachannel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/rtc)
 target_include_directories(datachannel PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
 target_include_directories(datachannel PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/plog/include)
-target_link_libraries(datachannel 
+target_link_libraries(datachannel
 						Threads::Threads
-						usrsctp-static 
-						LibNice::LibNice						
+						Usrsctp::UsrsctpStatic
 						)
 
 add_library(datachannel-static STATIC EXCLUDE_FROM_ALL ${LIBDATACHANNEL_SOURCES})
@@ -91,8 +93,7 @@ target_include_directories(datachannel-static PRIVATE ${CMAKE_CURRENT_SOURCE_DIR
 target_include_directories(datachannel-static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/plog/include)
 target_link_libraries(datachannel-static
 						Threads::Threads
-						usrsctp-static 
-						LibNice::LibNice						
+						Usrsctp::UsrsctpStatic
 						)
 
 if (USE_GNUTLS)
@@ -117,29 +118,45 @@ else()
 	target_link_libraries(datachannel-static OpenSSL::SSL)
 endif()
 
+if (USE_JUICE)
+	add_subdirectory(deps/libjuice EXCLUDE_FROM_ALL)
+	target_compile_definitions(datachannel PRIVATE USE_JUICE=1)
+	target_link_libraries(datachannel LibJuice::LibJuiceStatic)
+	target_compile_definitions(datachannel-static PRIVATE USE_JUICE=1)
+	target_link_libraries(datachannel-static LibJuice::LibJuiceStatic)
+else()
+	find_package(LibNice REQUIRED)
+	target_compile_definitions(datachannel PRIVATE USE_JUICE=0)
+	target_link_libraries(datachannel LibNice::LibNice)
+	target_compile_definitions(datachannel-static PRIVATE USE_JUICE=0)
+	target_link_libraries(datachannel-static LibNice::LibNice)
+endif()
+
 add_library(LibDataChannel::LibDataChannel ALIAS datachannel)
 add_library(LibDataChannel::LibDataChannelStatic ALIAS datachannel-static)
 
 # Main Test
-add_executable(tests ${TESTS_SOURCES})
-set_target_properties(tests PROPERTIES
+add_executable(datachannel-tests ${TESTS_SOURCES})
+set_target_properties(datachannel-tests PROPERTIES
 	VERSION ${PROJECT_VERSION}
 	CXX_STANDARD 17)
-
-target_link_libraries(tests datachannel)
+set_target_properties(datachannel-tests PROPERTIES OUTPUT_NAME tests)
+target_include_directories(datachannel-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
+target_link_libraries(datachannel-tests datachannel)
 
 # P2P Test: offerer
-add_executable(offerer ${TESTS_OFFERER_SOURCES})
-set_target_properties(offerer PROPERTIES
+add_executable(datachannel-offerer ${TESTS_OFFERER_SOURCES})
+set_target_properties(datachannel-offerer PROPERTIES
 	VERSION ${PROJECT_VERSION}
 	CXX_STANDARD 17)
-
-target_link_libraries(offerer datachannel)
+set_target_properties(datachannel-offerer PROPERTIES OUTPUT_NAME offerer)
+target_link_libraries(datachannel-offerer datachannel)
 
 # P2P Test: answerer
-add_executable(answerer ${TESTS_ANSWERER_SOURCES})
-set_target_properties(answerer PROPERTIES
+add_executable(datachannel-answerer ${TESTS_ANSWERER_SOURCES})
+set_target_properties(datachannel-answerer PROPERTIES
 	VERSION ${PROJECT_VERSION}
 	CXX_STANDARD 17)
+set_target_properties(datachannel-answerer PROPERTIES OUTPUT_NAME datachannel)
+target_link_libraries(datachannel-answerer datachannel)
 
-target_link_libraries(answerer datachannel)

+ 20 - 2
Jamfile

@@ -7,15 +7,17 @@ lib libdatachannel
 	: # requirements
 	<include>./include/rtc
 	<define>USE_GNUTLS=0
-	<cxxflags>"`pkg-config --cflags openssl glib-2.0 gobject-2.0 nice`"
+	<define>USE_JUICE=1
+	<cxxflags>"`pkg-config --cflags openssl`"
 	<library>/libdatachannel//usrsctp
+	<library>/libdatachannel//juice
 	: # default build
 	<link>static
 	: # usage requirements
 	<include>./include
 	<library>/libdatachannel//plog
 	<cxxflags>-pthread
-	<linkflags>"`pkg-config --libs openssl glib-2.0 gobject-2.0 nice`"
+	<linkflags>"`pkg-config --libs openssl`"
 	;
 
 alias plog
@@ -35,6 +37,15 @@ alias usrsctp
 	<library>libusrsctp.a
     ;
 
+alias juice
+    : # no sources
+    : # no build requirements
+    : # no default build
+    : # usage requirements
+    <include>./deps/libjuice/include
+	<library>libjuice.a
+    ;
+
 make libusrsctp.a : : @make_libusrsctp ;
 actions make_libusrsctp
 {
@@ -45,3 +56,10 @@ actions make_libusrsctp
     cp $(CWD)/deps/usrsctp/usrsctplib/.libs/libusrsctp.a $(<)
 }
 
+make libjuice.a : : @make_libjuice ;
+actions make_libjuice
+{
+	(cd $(CWD)/deps/libjuice && make)
+    cp $(CWD)/deps/libjuice/libjuice.a $(<)
+}
+

+ 31 - 11
Makefile

@@ -7,21 +7,37 @@ RM=rm -f
 CPPFLAGS=-O2 -pthread -fPIC -Wall -Wno-address-of-packed-member
 CXXFLAGS=-std=c++17
 LDFLAGS=-pthread
-LIBS=glib-2.0 gobject-2.0 nice
+LIBS=
+LOCALLIBS=libusrsctp.a
 USRSCTP_DIR=deps/usrsctp
+JUICE_DIR=deps/libjuice
 PLOG_DIR=deps/plog
 
+INCLUDES=-Iinclude/rtc -I$(PLOG_DIR)/include -I$(USRSCTP_DIR)/usrsctplib
+LDLIBS=
+
 USE_GNUTLS ?= 0
 ifneq ($(USE_GNUTLS), 0)
-        CPPFLAGS+= -DUSE_GNUTLS=1
-        LIBS+= gnutls
+        CPPFLAGS+=-DUSE_GNUTLS=1
+        LIBS+=gnutls
+else
+        CPPFLAGS+=-DUSE_GNUTLS=0
+        LIBS+=openssl
+endif
+
+USE_JUICE ?= 0
+ifneq ($(USE_JUICE), 0)
+        CPPFLAGS+=-DUSE_JUICE=1
+        INCLUDES+=-I$(JUICE_DIR)/include
+        LOCALLIBS+=libjuice.a
+        LIBS+=nettle
 else
-        CPPFLAGS+= -DUSE_GNUTLS=0
-        LIBS+= openssl
+        CPPFLAGS+=-DUSE_JUICE=0
+        LIBS+=glib-2.0 gobject-2.0 nice
 endif
 
-LDLIBS= $(shell pkg-config --libs $(LIBS))
-INCLUDES=-Iinclude/rtc -I$(PLOG_DIR)/include -I$(USRSCTP_DIR)/usrsctplib $(shell pkg-config --cflags $(LIBS))
+INCLUDES+=$(shell pkg-config --cflags $(LIBS))
+LDLIBS+=$(LOCALLIBS) $(shell pkg-config --libs $(LIBS))
 
 SRCS=$(shell printf "%s " src/*.cpp)
 OBJS=$(subst .cpp,.o,$(SRCS))
@@ -32,18 +48,18 @@ src/%.o: src/%.cpp
 	$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(INCLUDES) -MMD -MP -o $@ -c $<
 
 test/%.o: test/%.cpp
-	$(CXX) $(CXXFLAGS) $(CPPFLAGS) -Iinclude -I$(PLOG_DIR)/include -MMD -MP -o $@ -c $<
+	$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(INCLUDES) -MMD -MP -o $@ -c $<
 
 -include $(subst .cpp,.d,$(SRCS))
 
 $(NAME).a: $(OBJS)
 	$(AR) crf $@ $(OBJS)
 
-$(NAME).so: libusrsctp.a $(OBJS)
-	$(CXX) $(LDFLAGS) -shared -o $@ $(OBJS) $(LDLIBS) libusrsctp.a
+$(NAME).so: $(LOCALLIBS) $(OBJS)
+	$(CXX) $(LDFLAGS) -shared -o $@ $(OBJS) $(LDLIBS)
 
 tests: $(NAME).a test/main.o
-	$(CXX) $(LDFLAGS) -o $@ test/main.o $(LDLIBS) $(NAME).a libusrsctp.a
+	$(CXX) $(LDFLAGS) -o $@ test/main.o $(NAME).a $(LDLIBS)
 
 clean:
 	-$(RM) include/rtc/*.d *.d
@@ -67,3 +83,7 @@ libusrsctp.a:
 		make
 	cp $(USRSCTP_DIR)/usrsctplib/.libs/libusrsctp.a .
 
+libjuice.a:
+	cd $(JUICE_DIR) && make
+	cp $(JUICE_DIR)/libjuice.a .
+

+ 5 - 2
README.md

@@ -12,11 +12,14 @@ The library aims at fully implementing WebRTC SCTP DataChannels ([draft-ietf-rtc
 
 ## Dependencies
 
-- libnice: https://github.com/libnice/libnice
 - GnuTLS: https://www.gnutls.org/ or OpenSSL: https://www.openssl.org/
 
+Optional:
+- libnice: https://github.com/libnice/libnice (substituable with libjuice)
+
 Submodules:
 - usrsctp: https://github.com/sctplab/usrsctp
+- libjuice: https://github.com/paullouisageneau/libjuice
 
 ## Building
 
@@ -24,7 +27,7 @@ Submodules:
 $ git submodule update --init --recursive
 $ mkdir build
 $ cd build
-$ cmake -DUSE_GNUTLS=1 ..
+$ cmake -DUSE_JUICE=1 -DUSE_GNUTLS=1 ..
 $ make
 ```
 

+ 1 - 0
deps/libjuice

@@ -0,0 +1 @@
+Subproject commit a592b96eabc9c0e52515fc4a4e6d837e8f2b510b

+ 2 - 0
include/rtc/description.hpp

@@ -57,6 +57,8 @@ public:
 
 	operator string() const;
 
+	string generateSdp(const string &eol) const;
+
 private:
 	Type mType;
 	Role mRole;

+ 14 - 9
include/rtc/include.hpp

@@ -44,6 +44,8 @@ using std::uint32_t;
 using std::uint64_t;
 using std::uint8_t;
 
+// Constants
+
 const size_t MAX_NUMERICNODE_LEN = 48; // Max IPv6 string representation length
 const size_t MAX_NUMERICSERV_LEN = 6;  // Max port string representation length
 
@@ -51,16 +53,9 @@ const uint16_t DEFAULT_SCTP_PORT = 5000; // SCTP port to use by default
 const size_t DEFAULT_MAX_MESSAGE_SIZE = 65536;    // Remote max message size if not specified in SDP
 const size_t LOCAL_MAX_MESSAGE_SIZE = 256 * 1024; // Local max message size
 
-inline void InitLogger(plog::Severity severity, plog::IAppender *appender = nullptr) {
-	static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
-	if (!appender)
-		appender = &consoleAppender;
-	plog::init(severity, appender);
-	PLOG_DEBUG << "Logger initialized";
-}
+// Log
 
-// Don't change, it must match plog severity
-enum class LogLevel {
+enum class LogLevel { // Don't change, it must match plog severity
 	None = 0,
 	Fatal = 1,
 	Error = 2,
@@ -70,8 +65,18 @@ enum class LogLevel {
 	Verbose = 6
 };
 
+inline void InitLogger(plog::Severity severity, plog::IAppender *appender = nullptr) {
+	static plog::ColorConsoleAppender<plog::TxtFormatter> consoleAppender;
+	if (!appender)
+		appender = &consoleAppender;
+	plog::init(severity, appender);
+	PLOG_DEBUG << "Logger initialized";
+}
+
 inline void InitLogger(LogLevel level) { InitLogger(static_cast<plog::Severity>(level)); }
 
+// Utils
+
 template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
 template <class... Ts> overloaded(Ts...)->overloaded<Ts...>;
 

+ 22 - 20
src/description.cpp

@@ -130,37 +130,39 @@ std::vector<Candidate> Description::extractCandidates() {
 	return result;
 }
 
-Description::operator string() const {
+Description::operator string() const { return generateSdp("\r\n"); }
+
+string Description::generateSdp(const string &eol) const {
 	if (!mFingerprint)
-		throw std::logic_error("Fingerprint must be set to generate a SDP");
+		throw std::logic_error("Fingerprint must be set to generate an SDP string");
 
 	std::ostringstream sdp;
-	sdp << "v=0\n";
-	sdp << "o=- " << mSessionId << " 0 IN IP4 127.0.0.1\n";
-	sdp << "s=-\n";
-	sdp << "t=0 0\n";
-	sdp << "a=group:BUNDLE 0\n";
-	sdp << "m=application 9 UDP/DTLS/SCTP webrtc-datachannel\n";
-	sdp << "c=IN IP4 0.0.0.0\n";
-	sdp << "a=ice-ufrag:" << mIceUfrag << "\n";
-	sdp << "a=ice-pwd:" << mIcePwd << "\n";
+	sdp << "v=0" << eol;
+	sdp << "o=- " << mSessionId << " 0 IN IP4 127.0.0.1" << eol;
+	sdp << "s=-" << eol;
+	sdp << "t=0 0" << eol;
+	sdp << "a=group:BUNDLE 0" << eol;
+	sdp << "m=application 9 UDP/DTLS/SCTP webrtc-datachannel" << eol;
+	sdp << "c=IN IP4 0.0.0.0" << eol;
+	sdp << "a=ice-ufrag:" << mIceUfrag << eol;
+	sdp << "a=ice-pwd:" << mIcePwd << eol;
 	if (mTrickle)
-		sdp << "a=ice-options:trickle\n";
-	sdp << "a=mid:" << mMid << "\n";
-	sdp << "a=setup:" << roleToString(mRole) << "\n";
-	sdp << "a=dtls-id:1\n";
+		sdp << "a=ice-options:trickle" << eol;
+	sdp << "a=mid:" << mMid << eol;
+	sdp << "a=setup:" << roleToString(mRole) << eol;
+	sdp << "a=dtls-id:1" << eol;
 	if (mFingerprint)
-		sdp << "a=fingerprint:sha-256 " << *mFingerprint << "\n";
+		sdp << "a=fingerprint:sha-256 " << *mFingerprint << eol;
 	if (mSctpPort)
-		sdp << "a=sctp-port:" << *mSctpPort << "\n";
+		sdp << "a=sctp-port:" << *mSctpPort << eol;
 	if (mMaxMessageSize)
-		sdp << "a=max-message-size:" << *mMaxMessageSize << "\n";
+		sdp << "a=max-message-size:" << *mMaxMessageSize << eol;
 	for (const auto &candidate : mCandidates) {
-		sdp << string(candidate) << "\n";
+		sdp << string(candidate) << eol;
 	}
 
 	if (!mTrickle)
-		sdp << "a=end-of-candidates\n";
+		sdp << "a=end-of-candidates" << eol;
 
 	return sdp.str();
 }

+ 66 - 27
src/dtlstransport.cpp

@@ -18,6 +18,7 @@
 
 #include "dtlstransport.hpp"
 #include "icetransport.hpp"
+#include "message.hpp"
 
 #include <cassert>
 #include <chrono>
@@ -270,7 +271,7 @@ int DtlsTransport::TimeoutCallback(gnutls_transport_ptr_t ptr, unsigned int ms)
 
 } // namespace rtc
 
-#else
+#else // USE_GNUTLS==0
 
 #include <openssl/bio.h>
 #include <openssl/ec.h>
@@ -318,11 +319,21 @@ bool check_openssl_ret(SSL *ssl, int ret, const string &message = "OpenSSL error
 
 namespace rtc {
 
+BIO_METHOD *DtlsTransport::BioMethods = NULL;
 int DtlsTransport::TransportExIndex = -1;
 std::mutex DtlsTransport::GlobalMutex;
 
 void DtlsTransport::GlobalInit() {
 	std::lock_guard lock(GlobalMutex);
+	if (!BioMethods) {
+		BioMethods = BIO_meth_new(BIO_TYPE_BIO, "DTLS writer");
+		if (!BioMethods)
+			throw std::runtime_error("Unable to BIO methods for DTLS writer");
+		BIO_meth_set_create(BioMethods, BioMethodNew);
+		BIO_meth_set_destroy(BioMethods, BioMethodFree);
+		BIO_meth_set_write(BioMethods, BioMethodWrite);
+		BIO_meth_set_ctrl(BioMethods, BioMethodCtrl);
+	}
 	if (TransportExIndex < 0) {
 		TransportExIndex = SSL_get_ex_new_index(0, NULL, NULL, NULL, NULL);
 	}
@@ -346,7 +357,7 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, shared_ptr<Certific
 	// RFC 8261: SCTP performs segmentation and reassembly based on the path MTU.
 	// Therefore, the DTLS layer MUST NOT use any compression algorithm.
 	// See https://tools.ietf.org/html/rfc8261#section-5
-	SSL_CTX_set_options(mCtx, SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION);
+	SSL_CTX_set_options(mCtx, SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | SSL_OP_NO_QUERY_MTU);
 	SSL_CTX_set_min_proto_version(mCtx, DTLS1_VERSION);
 	SSL_CTX_set_read_ahead(mCtx, 1);
 	SSL_CTX_set_quiet_shutdown(mCtx, 1);
@@ -362,7 +373,7 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, shared_ptr<Certific
 	check_openssl(SSL_CTX_check_private_key(mCtx), "SSL local private key check failed");
 
 	if (!(mSsl = SSL_new(mCtx)))
-	    throw std::runtime_error("Unable to create SSL instance");
+		throw std::runtime_error("Unable to create SSL instance");
 
 	SSL_set_ex_data(mSsl, TransportExIndex, this);
 	SSL_set_mtu(mSsl, 1280 - 40 - 8); // min MTU over UDP/IPv6
@@ -372,11 +383,11 @@ DtlsTransport::DtlsTransport(shared_ptr<IceTransport> lower, shared_ptr<Certific
 	else
 		SSL_set_accept_state(mSsl);
 
-    if (!(mInBio = BIO_new(BIO_s_mem())) || !(mOutBio = BIO_new(BIO_s_mem())))
-	    throw std::runtime_error("Unable to create BIO");
+	if (!(mInBio = BIO_new(BIO_s_mem())) || !(mOutBio = BIO_new(BioMethods)))
+		throw std::runtime_error("Unable to create BIO");
 
 	BIO_set_mem_eof_return(mInBio, BIO_EOF);
-	BIO_set_mem_eof_return(mOutBio, BIO_EOF);
+	BIO_set_data(mOutBio, this);
 	SSL_set_bio(mSsl, mInBio, mOutBio);
 
 	auto ecdh = unique_ptr<EC_KEY, decltype(&EC_KEY_free)>(
@@ -403,7 +414,6 @@ void DtlsTransport::stop() {
 		mRecvThread.join();
 
 		SSL_shutdown(mSsl);
-		writePending();
 	}
 }
 
@@ -416,11 +426,8 @@ bool DtlsTransport::send(message_ptr message) {
 	PLOG_VERBOSE << "Send size=" << message->size();
 
 	int ret = SSL_write(mSsl, message->data(), message->size());
-	if (!check_openssl_ret(mSsl, ret)) {
+	if (!check_openssl_ret(mSsl, ret))
 		return false;
-	}
-
-	writePending();
 	return true;
 }
 
@@ -437,15 +444,14 @@ void DtlsTransport::changeState(State state) {
 }
 
 void DtlsTransport::runRecvLoop() {
-	const size_t bufferSize = 4096;
-	byte buffer[bufferSize];
-
+	const size_t maxMtu = 4096;
 	try {
 		changeState(State::Connecting);
 
 		SSL_do_handshake(mSsl);
-		writePending();
 
+		const size_t bufferSize = maxMtu;
+		byte buffer[bufferSize];
 		while (auto next = mIncomingQueue.pop()) {
 			auto message = *next;
 			BIO_write(mInBio, message->data(), message->size());
@@ -459,9 +465,13 @@ void DtlsTransport::runRecvLoop() {
 				if (unsigned long err = ERR_get_error())
 					throw std::runtime_error("handshake failed: " + openssl_error_string(err));
 
-				writePending();
-				if (SSL_is_init_finished(mSsl))
+				if (SSL_is_init_finished(mSsl)) {
 					changeState(State::Connected);
+
+					// RFC 8261: DTLS MUST support sending messages larger than the current path MTU
+					// See https://tools.ietf.org/html/rfc8261#section-5
+					SSL_set_mtu(mSsl, maxMtu + 1);
+				}
 			}
 
 			if (decrypted)
@@ -481,16 +491,6 @@ void DtlsTransport::runRecvLoop() {
 	}
 }
 
-void DtlsTransport::writePending() {
-	const size_t bufferSize = 4096;
-	byte buffer[bufferSize];
-	while (BIO_ctrl_pending(mOutBio) > 0) {
-		int ret = BIO_read(mOutBio, buffer, bufferSize);
-		if (check_openssl_ret(mSsl, ret) && ret > 0)
-			outgoing(make_message(buffer, buffer + ret));
-	}
-}
-
 int DtlsTransport::CertificateCallback(int preverify_ok, X509_STORE_CTX *ctx) {
 	SSL *ssl =
 	    static_cast<SSL *>(X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()));
@@ -515,6 +515,45 @@ void DtlsTransport::InfoCallback(const SSL *ssl, int where, int ret) {
 	}
 }
 
+int DtlsTransport::BioMethodNew(BIO *bio) {
+	BIO_set_init(bio, 1);
+	BIO_set_data(bio, NULL);
+	BIO_set_shutdown(bio, 0);
+	return 1;
+}
+
+int DtlsTransport::BioMethodFree(BIO *bio) {
+	if (!bio)
+		return 0;
+	BIO_set_data(bio, NULL);
+	return 1;
+}
+
+int DtlsTransport::BioMethodWrite(BIO *bio, const char *in, int inl) {
+	if (inl <= 0)
+		return inl;
+	auto transport = reinterpret_cast<DtlsTransport *>(BIO_get_data(bio));
+	if (!transport)
+		return -1;
+	auto b = reinterpret_cast<const byte *>(in);
+	return transport->outgoing(make_message(b, b + inl)) ? inl : 0;
+}
+
+long DtlsTransport::BioMethodCtrl(BIO *bio, int cmd, long num, void *ptr) {
+	switch (cmd) {
+	case BIO_CTRL_FLUSH:
+		return 1;
+	case BIO_CTRL_DGRAM_QUERY_MTU:
+		return 0; // SSL_OP_NO_QUERY_MTU must be set
+	case BIO_CTRL_WPENDING:
+	case BIO_CTRL_PENDING:
+		return 0;
+	default:
+		break;
+	}
+	return 0;
+}
+
 } // namespace rtc
 
 #endif

+ 6 - 2
src/dtlstransport.hpp

@@ -79,18 +79,22 @@ private:
 	static ssize_t ReadCallback(gnutls_transport_ptr_t ptr, void *data, size_t maxlen);
 	static int TimeoutCallback(gnutls_transport_ptr_t ptr, unsigned int ms);
 #else
-	void writePending();
-
 	SSL_CTX *mCtx;
 	SSL *mSsl;
 	BIO *mInBio, *mOutBio;
 
+	static BIO_METHOD *BioMethods;
 	static int TransportExIndex;
 	static std::mutex GlobalMutex;
 
 	static void GlobalInit();
 	static int CertificateCallback(int preverify_ok, X509_STORE_CTX *ctx);
 	static void InfoCallback(const SSL *ssl, int where, int ret);
+
+	static int BioMethodNew(BIO *bio);
+	static int BioMethodFree(BIO *bio);
+	static int BioMethodWrite(BIO *bio, const char *in, int inl);
+	static long BioMethodCtrl(BIO *bio, int cmd, long num, void *ptr);
 #endif
 };
 

+ 242 - 14
src/icetransport.cpp

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 Paul-Louis Ageneau
+ * Copyright (c) 2019-2020 Paul-Louis Ageneau
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -28,23 +28,245 @@
 #include <random>
 #include <sstream>
 
-namespace rtc {
-
 using namespace std::chrono_literals;
 
 using std::shared_ptr;
 using std::weak_ptr;
 
+#if USE_JUICE
+
+namespace rtc {
+
+IceTransport::IceTransport(const Configuration &config, Description::Role role,
+                           candidate_callback candidateCallback, state_callback stateChangeCallback,
+                           gathering_state_callback gatheringStateChangeCallback)
+    : mRole(role), mMid("0"), mState(State::Disconnected), mGatheringState(GatheringState::New),
+      mCandidateCallback(std::move(candidateCallback)),
+      mStateChangeCallback(std::move(stateChangeCallback)),
+      mGatheringStateChangeCallback(std::move(gatheringStateChangeCallback)),
+      mAgent(nullptr, nullptr) {
+
+	PLOG_DEBUG << "Initializing ICE transport (libjuice)";
+	if (config.enableIceTcp) {
+		PLOG_WARNING << "ICE-TCP is not supported with libjuice";
+	}
+	juice_set_log_handler(IceTransport::LogCallback);
+	juice_set_log_level(JUICE_LOG_LEVEL_VERBOSE);
+
+	juice_config_t jconfig = {};
+	jconfig.cb_state_changed = IceTransport::StateChangeCallback;
+	jconfig.cb_candidate = IceTransport::CandidateCallback;
+	jconfig.cb_gathering_done = IceTransport::GatheringDoneCallback;
+	jconfig.cb_recv = IceTransport::RecvCallback;
+	jconfig.user_ptr = this;
+
+	// Randomize servers order
+	std::vector<IceServer> servers = config.iceServers;
+	unsigned seed = std::chrono::system_clock::now().time_since_epoch().count();
+	std::shuffle(servers.begin(), servers.end(), std::default_random_engine(seed));
+
+	// Pick a STUN server
+	for (auto &server : servers) {
+		if (!server.hostname.empty() && server.type == IceServer::Type::Stun) {
+			if (server.service.empty())
+				server.service = "3478"; // STUN UDP port
+			PLOG_DEBUG << "Using STUN server \"" << server.hostname << ":" << server.service
+			           << "\"";
+			mStunHostname = server.hostname;
+			mStunService = server.service;
+			jconfig.stun_server_host = mStunHostname.c_str();
+			jconfig.stun_server_port = std::stoul(mStunService);
+		}
+	}
+
+	// TURN support is not implemented yet
+
+	// Create agent
+	mAgent = decltype(mAgent)(juice_create(&jconfig), juice_destroy);
+	if (!mAgent)
+		throw std::runtime_error("Failed to create the ICE agent");
+}
+
+IceTransport::~IceTransport() { stop(); }
+
+void IceTransport::stop() {
+	// Nothing to do
+}
+
+Description::Role IceTransport::role() const { return mRole; }
+
+IceTransport::State IceTransport::state() const { return mState; }
+
+Description IceTransport::getLocalDescription(Description::Type type) const {
+	char sdp[JUICE_MAX_SDP_STRING_LEN];
+	if (juice_get_local_description(mAgent.get(), sdp, JUICE_MAX_SDP_STRING_LEN) < 0)
+		throw std::runtime_error("Failed to generate local SDP");
+
+	return Description(string(sdp), type, mRole);
+}
+
+void IceTransport::setRemoteDescription(const Description &description) {
+	mRole = description.role() == Description::Role::Active ? Description::Role::Passive
+	                                                        : Description::Role::Active;
+	mMid = description.mid();
+	// TODO
+	// mTrickleTimeout = description.trickleEnabled() ? 30s : 0s;
+
+	if (juice_set_remote_description(mAgent.get(), string(description).c_str()) < 0)
+		throw std::runtime_error("Failed to parse remote SDP");
+}
+
+bool IceTransport::addRemoteCandidate(const Candidate &candidate) {
+	// Don't try to pass unresolved candidates for more safety
+	if (!candidate.isResolved())
+		return false;
+
+	return juice_add_remote_candidate(mAgent.get(), string(candidate).c_str()) >= 0;
+}
+
+void IceTransport::gatherLocalCandidates() {
+	// Change state now as candidates calls can be synchronous
+	changeGatheringState(GatheringState::InProgress);
+
+	if (juice_gather_candidates(mAgent.get()) < 0) {
+		throw std::runtime_error("Failed to gather local ICE candidates");
+	}
+}
+
+std::optional<string> IceTransport::getLocalAddress() const {
+	char str[JUICE_MAX_ADDRESS_STRING_LEN];
+	if (juice_get_selected_addresses(mAgent.get(), str, JUICE_MAX_ADDRESS_STRING_LEN, NULL, 0) ==
+	    0) {
+		return std::make_optional(string(str));
+	}
+	return nullopt;
+}
+std::optional<string> IceTransport::getRemoteAddress() const {
+	char str[JUICE_MAX_ADDRESS_STRING_LEN];
+	if (juice_get_selected_addresses(mAgent.get(), NULL, 0, str, JUICE_MAX_ADDRESS_STRING_LEN) ==
+	    0) {
+		return std::make_optional(string(str));
+	}
+	return nullopt;
+}
+
+bool IceTransport::send(message_ptr message) {
+	if (!message || (mState != State::Connected && mState != State::Completed))
+		return false;
+
+	PLOG_VERBOSE << "Send size=" << message->size();
+	return outgoing(message);
+}
+
+void IceTransport::incoming(message_ptr message) { recv(message); }
+
+void IceTransport::incoming(const byte *data, int size) {
+	incoming(make_message(data, data + size));
+}
+
+bool IceTransport::outgoing(message_ptr message) {
+	return juice_send(mAgent.get(), reinterpret_cast<const char *>(message->data()),
+	                  message->size()) >= 0;
+}
+
+void IceTransport::changeState(State state) {
+	if (mState.exchange(state) != state)
+		mStateChangeCallback(mState);
+}
+
+void IceTransport::changeGatheringState(GatheringState state) {
+	if (mGatheringState.exchange(state) != state)
+		mGatheringStateChangeCallback(mGatheringState);
+}
+
+void IceTransport::processStateChange(unsigned int state) {
+	changeState(static_cast<State>(state));
+}
+
+void IceTransport::processCandidate(const string &candidate) {
+	mCandidateCallback(Candidate(candidate, mMid));
+}
+
+void IceTransport::processGatheringDone() { changeGatheringState(GatheringState::Complete); }
+
+void IceTransport::StateChangeCallback(juice_agent_t *agent, juice_state_t state, void *user_ptr) {
+	auto iceTransport = static_cast<rtc::IceTransport *>(user_ptr);
+	try {
+		iceTransport->processStateChange(static_cast<unsigned int>(state));
+	} catch (const std::exception &e) {
+		PLOG_WARNING << e.what();
+	}
+}
+
+void IceTransport::CandidateCallback(juice_agent_t *agent, const char *sdp, void *user_ptr) {
+	auto iceTransport = static_cast<rtc::IceTransport *>(user_ptr);
+	try {
+		iceTransport->processCandidate(sdp);
+	} catch (const std::exception &e) {
+		PLOG_WARNING << e.what();
+	}
+}
+
+void IceTransport::GatheringDoneCallback(juice_agent_t *agent, void *user_ptr) {
+	auto iceTransport = static_cast<rtc::IceTransport *>(user_ptr);
+	try {
+		iceTransport->processGatheringDone();
+	} catch (const std::exception &e) {
+		PLOG_WARNING << e.what();
+	}
+}
+
+void IceTransport::RecvCallback(juice_agent_t *agent, const char *data, size_t size,
+                                void *user_ptr) {
+	auto iceTransport = static_cast<rtc::IceTransport *>(user_ptr);
+	try {
+		iceTransport->incoming(reinterpret_cast<const byte *>(data), size);
+	} catch (const std::exception &e) {
+		PLOG_WARNING << e.what();
+	}
+}
+
+void IceTransport::LogCallback(juice_log_level_t level, const char *message) {
+	plog::Severity severity;
+	switch (level) {
+	case JUICE_LOG_LEVEL_FATAL:
+		severity = plog::fatal;
+		break;
+	case JUICE_LOG_LEVEL_ERROR:
+		severity = plog::error;
+		break;
+	case JUICE_LOG_LEVEL_WARN:
+		severity = plog::warning;
+		break;
+	case JUICE_LOG_LEVEL_INFO:
+		severity = plog::info;
+		break;
+	case JUICE_LOG_LEVEL_DEBUG:
+		severity = plog::debug;
+		break;
+	default:
+		severity = plog::verbose;
+		break;
+	}
+	PLOG(severity) << "juice: " << message;
+}
+
+} // namespace rtc
+
+#else // USE_JUICE == 0
+
+namespace rtc {
+
 IceTransport::IceTransport(const Configuration &config, Description::Role role,
                            candidate_callback candidateCallback, state_callback stateChangeCallback,
                            gathering_state_callback gatheringStateChangeCallback)
     : mRole(role), mMid("0"), mState(State::Disconnected), mGatheringState(GatheringState::New),
-      mNiceAgent(nullptr, nullptr), mMainLoop(nullptr, nullptr),
       mCandidateCallback(std::move(candidateCallback)),
       mStateChangeCallback(std::move(stateChangeCallback)),
-      mGatheringStateChangeCallback(std::move(gatheringStateChangeCallback)) {
+      mGatheringStateChangeCallback(std::move(gatheringStateChangeCallback)),
+      mNiceAgent(nullptr, nullptr), mMainLoop(nullptr, nullptr) {
 
-	PLOG_DEBUG << "Initializing ICE transport";
+	PLOG_DEBUG << "Initializing ICE transport (libnice)";
 
 	g_log_set_handler("libnice", G_LOG_LEVEL_MASK, LogCallback, this);
 
@@ -117,6 +339,8 @@ IceTransport::IceTransport(const Configuration &config, Description::Role role,
 				if (getnameinfo(p->ai_addr, p->ai_addrlen, nodebuffer, MAX_NUMERICNODE_LEN,
 				                servbuffer, MAX_NUMERICNODE_LEN,
 				                NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
+					PLOG_DEBUG << "Using STUN server \"" << server.hostname << ":" << server.service
+					           << "\"";
 					g_object_set(G_OBJECT(mNiceAgent.get()), "stun-server", nodebuffer, nullptr);
 					g_object_set(G_OBJECT(mNiceAgent.get()), "stun-server-port",
 					             std::stoul(servbuffer), nullptr);
@@ -231,7 +455,8 @@ void IceTransport::setRemoteDescription(const Description &description) {
 	mMid = description.mid();
 	mTrickleTimeout = description.trickleEnabled() ? 30s : 0s;
 
-	if (nice_agent_parse_remote_sdp(mNiceAgent.get(), string(description).c_str()) < 0)
+	// Warning: libnice expects "\n" as end of line
+	if (nice_agent_parse_remote_sdp(mNiceAgent.get(), description.generateSdp("\n").c_str()) < 0)
 		throw std::runtime_error("Failed to parse remote SDP");
 }
 
@@ -272,6 +497,7 @@ std::optional<string> IceTransport::getLocalAddress() const {
 	}
 	return nullopt;
 }
+
 std::optional<string> IceTransport::getRemoteAddress() const {
 	NiceCandidate *local = nullptr;
 	NiceCandidate *remote = nullptr;
@@ -305,24 +531,24 @@ void IceTransport::changeState(State state) {
 		mStateChangeCallback(mState);
 }
 
+void IceTransport::changeGatheringState(GatheringState state) {
+	if (mGatheringState.exchange(state) != state)
+		mGatheringStateChangeCallback(mGatheringState);
+}
+
 void IceTransport::processTimeout() {
 	PLOG_WARNING << "ICE timeout";
 	mTimeoutId = 0;
 	changeState(State::Failed);
 }
 
-void IceTransport::changeGatheringState(GatheringState state) {
-	mGatheringState = state;
-	mGatheringStateChangeCallback(mGatheringState);
-}
-
 void IceTransport::processCandidate(const string &candidate) {
 	mCandidateCallback(Candidate(candidate, mMid));
 }
 
 void IceTransport::processGatheringDone() { changeGatheringState(GatheringState::Complete); }
 
-void IceTransport::processStateChange(uint32_t state) {
+void IceTransport::processStateChange(unsigned int state) {
 	if (state == NICE_COMPONENT_STATE_FAILED && mTrickleTimeout.count() > 0) {
 		if (mTimeoutId)
 			g_source_remove(mTimeoutId);
@@ -415,7 +641,9 @@ void IceTransport::LogCallback(const gchar *logDomain, GLogLevelFlags logLevel,
 	else
 		severity = plog::verbose; // libnice debug as verbose
 
-	PLOG(severity) << message;
+	PLOG(severity) << "nice: " << message;
 }
 
 } // namespace rtc
+
+#endif

+ 35 - 12
src/icetransport.hpp

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 Paul-Louis Ageneau
+ * Copyright (c) 2019-2020 Paul-Louis Ageneau
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of the GNU Lesser General Public
@@ -26,9 +26,11 @@
 #include "peerconnection.hpp"
 #include "transport.hpp"
 
-extern "C" {
+#if USE_JUICE
+#include <juice/juice.h>
+#else
 #include <nice/agent.h>
-}
+#endif
 
 #include <atomic>
 #include <chrono>
@@ -38,14 +40,23 @@ namespace rtc {
 
 class IceTransport : public Transport {
 public:
-	enum class State : uint32_t {
+#if USE_JUICE
+	enum class State : unsigned int{
+	    Disconnected = JUICE_STATE_DISCONNECTED,
+	    Connecting = JUICE_STATE_CONNECTING,
+	    Connected = JUICE_STATE_CONNECTED,
+	    Completed = JUICE_STATE_COMPLETED,
+	    Failed = JUICE_STATE_FAILED,
+	};
+#else
+	enum class State : unsigned int {
 		Disconnected = NICE_COMPONENT_STATE_DISCONNECTED,
 		Connecting = NICE_COMPONENT_STATE_CONNECTING,
 		Connected = NICE_COMPONENT_STATE_CONNECTED,
 		Completed = NICE_COMPONENT_STATE_READY,
-		Failed = NICE_COMPONENT_STATE_FAILED
+		Failed = NICE_COMPONENT_STATE_FAILED,
 	};
-
+#endif
 	enum class GatheringState { New = 0, InProgress = 1, Complete = 2 };
 
 	using candidate_callback = std::function<void(const Candidate &candidate)>;
@@ -79,9 +90,9 @@ private:
 	void changeState(State state);
 	void changeGatheringState(GatheringState state);
 
+	void processStateChange(unsigned int state);
 	void processCandidate(const string &candidate);
 	void processGatheringDone();
-	void processStateChange(uint32_t state);
 	void processTimeout();
 
 	Description::Role mRole;
@@ -90,27 +101,39 @@ private:
 	std::atomic<State> mState;
 	std::atomic<GatheringState> mGatheringState;
 
+	candidate_callback mCandidateCallback;
+	state_callback mStateChangeCallback;
+	gathering_state_callback mGatheringStateChangeCallback;
+
+#if USE_JUICE
+	std::unique_ptr<juice_agent_t, void (*)(juice_agent_t *)> mAgent;
+	string mStunHostname;
+	string mStunService;
+
+	static void StateChangeCallback(juice_agent_t *agent, juice_state_t state, void *user_ptr);
+	static void CandidateCallback(juice_agent_t *agent, const char *sdp, void *user_ptr);
+	static void GatheringDoneCallback(juice_agent_t *agent, void *user_ptr);
+	static void RecvCallback(juice_agent_t *agent, const char *data, size_t size, void *user_ptr);
+	static void LogCallback(juice_log_level_t level, const char *message);
+#else
 	uint32_t mStreamId = 0;
 	std::unique_ptr<NiceAgent, void (*)(gpointer)> mNiceAgent;
 	std::unique_ptr<GMainLoop, void (*)(GMainLoop *)> mMainLoop;
 	std::thread mMainLoopThread;
 	guint mTimeoutId = 0;
 
-	candidate_callback mCandidateCallback;
-	state_callback mStateChangeCallback;
-	gathering_state_callback mGatheringStateChangeCallback;
-
 	static string AddressToString(const NiceAddress &addr);
 
 	static void CandidateCallback(NiceAgent *agent, NiceCandidate *candidate, gpointer userData);
 	static void GatheringDoneCallback(NiceAgent *agent, guint streamId, gpointer userData);
 	static void StateChangeCallback(NiceAgent *agent, guint streamId, guint componentId,
-	                                 guint state, gpointer userData);
+	                                guint state, gpointer userData);
 	static void RecvCallback(NiceAgent *agent, guint stream_id, guint component_id, guint len,
 	                         gchar *buf, gpointer userData);
 	static gboolean TimeoutCallback(gpointer userData);
 	static void LogCallback(const gchar *log_domain, GLogLevelFlags log_level, const gchar *message,
 	                        gpointer user_data);
+#endif
 };
 
 } // namespace rtc

+ 10 - 1
test/main.cpp

@@ -29,7 +29,7 @@ using namespace std;
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
 int main(int argc, char **argv) {
-	// InitLogger(LogLevel::Debug);
+	InitLogger(LogLevel::Warning);
 
 	Configuration config;
 	// config.iceServers.emplace_back("stun:stun.l.google.com:19302");
@@ -113,6 +113,15 @@ int main(int argc, char **argv) {
 
 	this_thread::sleep_for(3s);
 
+	if (auto addr = pc1->localAddress())
+		cout << "Local address 1:  " << *addr << endl;
+	if (auto addr = pc1->remoteAddress())
+		cout << "Remote address 1: " << *addr << endl;
+	if (auto addr = pc2->localAddress())
+		cout << "Local address 2:  " << *addr << endl;
+	if (auto addr = pc2->remoteAddress())
+		cout << "Remote address 2: " << *addr << endl;
+
 	if (dc1->isOpen() && dc2->isOpen()) {
 		pc1->close();
 		pc2->close();

+ 1 - 1
test/p2p/answerer.cpp

@@ -28,7 +28,7 @@ using namespace std;
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
 int main(int argc, char **argv) {
-	// InitLogger(LogLevel::Debug);
+	InitLogger(LogLevel::Warning);
 
 	Configuration config;
 	// config.iceServers.emplace_back("stun.l.google.com:19302");

+ 1 - 1
test/p2p/offerer.cpp

@@ -28,7 +28,7 @@ using namespace std;
 template <class T> weak_ptr<T> make_weak_ptr(shared_ptr<T> ptr) { return ptr; }
 
 int main(int argc, char **argv) {
-	// InitLogger(LogLevel::Debug);
+	InitLogger(LogLevel::Warning);
 
 	Configuration config;
 	// config.iceServers.emplace_back("stun.l.google.com:19302");