From 8cd87a2646b780bf774da9bb1bc3a40e769c66a3 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Thu, 13 Jul 2023 16:58:20 +0800 Subject: [PATCH] Use sourcecode for libjuice --- thirdparty/libjuice/.clang-format | 7 + thirdparty/libjuice/.clang-tidy | 45 + thirdparty/libjuice/.editorconfig | 11 + thirdparty/libjuice/.github/FUNDING.yml | 3 + .../libjuice/.github/workflows/build.yml | 43 + thirdparty/libjuice/.gitignore | 8 + thirdparty/libjuice/CMakeLists.txt | 249 ++ thirdparty/libjuice/LICENSE | 373 +++ thirdparty/libjuice/Makefile | 77 + thirdparty/libjuice/README.md | 103 + .../libjuice/cmake/LibJuiceConfig.cmake | 2 + .../libjuice/cmake/Modules/FindNettle.cmake | 142 + thirdparty/libjuice/fuzzer/README.md | 26 + thirdparty/libjuice/fuzzer/fuzzer.c | 41 + thirdparty/libjuice/fuzzer/input/message1.raw | Bin 0 -> 108 bytes thirdparty/libjuice/fuzzer/input/message2.raw | Bin 0 -> 164 bytes thirdparty/libjuice/include/juice/juice.h | 352 +-- thirdparty/libjuice/lib/juice.lib | Bin 542656 -> 0 bytes thirdparty/libjuice/src/addr.c | 310 ++ thirdparty/libjuice/src/addr.h | 45 + thirdparty/libjuice/src/agent.c | 2514 +++++++++++++++++ thirdparty/libjuice/src/agent.h | 227 ++ thirdparty/libjuice/src/base64.c | 90 + thirdparty/libjuice/src/base64.h | 24 + thirdparty/libjuice/src/conn.c | 249 ++ thirdparty/libjuice/src/conn.h | 44 + thirdparty/libjuice/src/conn_mux.c | 540 ++++ thirdparty/libjuice/src/conn_mux.h | 32 + thirdparty/libjuice/src/conn_poll.c | 433 +++ thirdparty/libjuice/src/conn_poll.h | 32 + thirdparty/libjuice/src/conn_thread.c | 278 ++ thirdparty/libjuice/src/conn_thread.h | 32 + thirdparty/libjuice/src/const_time.c | 34 + thirdparty/libjuice/src/const_time.h | 18 + thirdparty/libjuice/src/crc32.c | 38 + thirdparty/libjuice/src/crc32.h | 21 + thirdparty/libjuice/src/hash.c | 59 + thirdparty/libjuice/src/hash.h | 23 + thirdparty/libjuice/src/hmac.c | 43 + thirdparty/libjuice/src/hmac.h | 21 + thirdparty/libjuice/src/ice.c | 408 +++ thirdparty/libjuice/src/ice.h | 103 + thirdparty/libjuice/src/juice.c | 207 ++ thirdparty/libjuice/src/log.c | 129 + thirdparty/libjuice/src/log.h | 33 + thirdparty/libjuice/src/picohash.h | 741 +++++ thirdparty/libjuice/src/random.c | 126 + thirdparty/libjuice/src/random.h | 21 + thirdparty/libjuice/src/server.c | 1143 ++++++++ thirdparty/libjuice/src/server.h | 123 + thirdparty/libjuice/src/socket.h | 132 + thirdparty/libjuice/src/stun.c | 1236 ++++++++ thirdparty/libjuice/src/stun.h | 376 +++ thirdparty/libjuice/src/thread.h | 116 + thirdparty/libjuice/src/timestamp.c | 45 + thirdparty/libjuice/src/timestamp.h | 20 + thirdparty/libjuice/src/turn.c | 495 ++++ thirdparty/libjuice/src/turn.h | 111 + thirdparty/libjuice/src/udp.c | 604 ++++ thirdparty/libjuice/src/udp.h | 33 + thirdparty/libjuice/test/base64.c | 45 + thirdparty/libjuice/test/bind.c | 225 ++ thirdparty/libjuice/test/conflict.c | 203 ++ thirdparty/libjuice/test/connectivity.c | 228 ++ thirdparty/libjuice/test/crc32.c | 22 + thirdparty/libjuice/test/gathering.c | 95 + thirdparty/libjuice/test/main.c | 110 + thirdparty/libjuice/test/mux.c | 226 ++ thirdparty/libjuice/test/notrickle.c | 201 ++ thirdparty/libjuice/test/server.c | 274 ++ thirdparty/libjuice/test/stun.c | 155 + thirdparty/libjuice/test/thread.c | 220 ++ thirdparty/libjuice/test/turn.c | 241 ++ thirdparty/libjuice/xmake.lua | 12 + thirdparty/xmake.lua | 1 + xmake.lua | 7 +- 76 files changed, 14877 insertions(+), 179 deletions(-) create mode 100644 thirdparty/libjuice/.clang-format create mode 100644 thirdparty/libjuice/.clang-tidy create mode 100644 thirdparty/libjuice/.editorconfig create mode 100644 thirdparty/libjuice/.github/FUNDING.yml create mode 100644 thirdparty/libjuice/.github/workflows/build.yml create mode 100644 thirdparty/libjuice/.gitignore create mode 100644 thirdparty/libjuice/CMakeLists.txt create mode 100644 thirdparty/libjuice/LICENSE create mode 100644 thirdparty/libjuice/Makefile create mode 100644 thirdparty/libjuice/README.md create mode 100644 thirdparty/libjuice/cmake/LibJuiceConfig.cmake create mode 100644 thirdparty/libjuice/cmake/Modules/FindNettle.cmake create mode 100644 thirdparty/libjuice/fuzzer/README.md create mode 100644 thirdparty/libjuice/fuzzer/fuzzer.c create mode 100644 thirdparty/libjuice/fuzzer/input/message1.raw create mode 100644 thirdparty/libjuice/fuzzer/input/message2.raw delete mode 100644 thirdparty/libjuice/lib/juice.lib create mode 100644 thirdparty/libjuice/src/addr.c create mode 100644 thirdparty/libjuice/src/addr.h create mode 100644 thirdparty/libjuice/src/agent.c create mode 100644 thirdparty/libjuice/src/agent.h create mode 100644 thirdparty/libjuice/src/base64.c create mode 100644 thirdparty/libjuice/src/base64.h create mode 100644 thirdparty/libjuice/src/conn.c create mode 100644 thirdparty/libjuice/src/conn.h create mode 100644 thirdparty/libjuice/src/conn_mux.c create mode 100644 thirdparty/libjuice/src/conn_mux.h create mode 100644 thirdparty/libjuice/src/conn_poll.c create mode 100644 thirdparty/libjuice/src/conn_poll.h create mode 100644 thirdparty/libjuice/src/conn_thread.c create mode 100644 thirdparty/libjuice/src/conn_thread.h create mode 100644 thirdparty/libjuice/src/const_time.c create mode 100644 thirdparty/libjuice/src/const_time.h create mode 100644 thirdparty/libjuice/src/crc32.c create mode 100644 thirdparty/libjuice/src/crc32.h create mode 100644 thirdparty/libjuice/src/hash.c create mode 100644 thirdparty/libjuice/src/hash.h create mode 100644 thirdparty/libjuice/src/hmac.c create mode 100644 thirdparty/libjuice/src/hmac.h create mode 100644 thirdparty/libjuice/src/ice.c create mode 100644 thirdparty/libjuice/src/ice.h create mode 100644 thirdparty/libjuice/src/juice.c create mode 100644 thirdparty/libjuice/src/log.c create mode 100644 thirdparty/libjuice/src/log.h create mode 100644 thirdparty/libjuice/src/picohash.h create mode 100644 thirdparty/libjuice/src/random.c create mode 100644 thirdparty/libjuice/src/random.h create mode 100644 thirdparty/libjuice/src/server.c create mode 100644 thirdparty/libjuice/src/server.h create mode 100644 thirdparty/libjuice/src/socket.h create mode 100644 thirdparty/libjuice/src/stun.c create mode 100644 thirdparty/libjuice/src/stun.h create mode 100644 thirdparty/libjuice/src/thread.h create mode 100644 thirdparty/libjuice/src/timestamp.c create mode 100644 thirdparty/libjuice/src/timestamp.h create mode 100644 thirdparty/libjuice/src/turn.c create mode 100644 thirdparty/libjuice/src/turn.h create mode 100644 thirdparty/libjuice/src/udp.c create mode 100644 thirdparty/libjuice/src/udp.h create mode 100644 thirdparty/libjuice/test/base64.c create mode 100644 thirdparty/libjuice/test/bind.c create mode 100644 thirdparty/libjuice/test/conflict.c create mode 100644 thirdparty/libjuice/test/connectivity.c create mode 100644 thirdparty/libjuice/test/crc32.c create mode 100644 thirdparty/libjuice/test/gathering.c create mode 100644 thirdparty/libjuice/test/main.c create mode 100644 thirdparty/libjuice/test/mux.c create mode 100644 thirdparty/libjuice/test/notrickle.c create mode 100644 thirdparty/libjuice/test/server.c create mode 100644 thirdparty/libjuice/test/stun.c create mode 100644 thirdparty/libjuice/test/thread.c create mode 100644 thirdparty/libjuice/test/turn.c create mode 100644 thirdparty/libjuice/xmake.lua create mode 100644 thirdparty/xmake.lua diff --git a/thirdparty/libjuice/.clang-format b/thirdparty/libjuice/.clang-format new file mode 100644 index 0000000..ce57268 --- /dev/null +++ b/thirdparty/libjuice/.clang-format @@ -0,0 +1,7 @@ +--- +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: ForIndentation +ColumnLimit: 100 + diff --git a/thirdparty/libjuice/.clang-tidy b/thirdparty/libjuice/.clang-tidy new file mode 100644 index 0000000..b9df5c7 --- /dev/null +++ b/thirdparty/libjuice/.clang-tidy @@ -0,0 +1,45 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,-clang-analyzer-security.insecureAPI.strcpy' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none +CheckOptions: + - key: llvm-else-after-return.WarnOnConditionVariables + value: 'false' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: cert-str34-c.DiagnoseSignedUnsignedCharComparisons + value: 'false' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: cert-err33-c.CheckedFunctions + value: '::aligned_alloc;::asctime_s;::at_quick_exit;::atexit;::bsearch;::bsearch_s;::btowc;::c16rtomb;::c32rtomb;::calloc;::clock;::cnd_broadcast;::cnd_init;::cnd_signal;::cnd_timedwait;::cnd_wait;::ctime_s;::fclose;::fflush;::fgetc;::fgetpos;::fgets;::fgetwc;::fopen;::fopen_s;::fprintf;::fprintf_s;::fputc;::fputs;::fputwc;::fputws;::fread;::freopen;::freopen_s;::fscanf;::fscanf_s;::fseek;::fsetpos;::ftell;::fwprintf;::fwprintf_s;::fwrite;::fwscanf;::fwscanf_s;::getc;::getchar;::getenv;::getenv_s;::gets_s;::getwc;::getwchar;::gmtime;::gmtime_s;::localtime;::localtime_s;::malloc;::mbrtoc16;::mbrtoc32;::mbsrtowcs;::mbsrtowcs_s;::mbstowcs;::mbstowcs_s;::memchr;::mktime;::mtx_init;::mtx_lock;::mtx_timedlock;::mtx_trylock;::mtx_unlock;::printf_s;::putc;::putwc;::raise;::realloc;::remove;::rename;::scanf;::scanf_s;::setlocale;::setvbuf;::signal;::snprintf;::snprintf_s;::sprintf;::sprintf_s;::sscanf;::sscanf_s;::strchr;::strerror_s;::strftime;::strpbrk;::strrchr;::strstr;::strtod;::strtof;::strtoimax;::strtok;::strtok_s;::strtol;::strtold;::strtoll;::strtoul;::strtoull;::strtoumax;::strxfrm;::swprintf;::swprintf_s;::swscanf;::swscanf_s;::thrd_create;::thrd_detach;::thrd_join;::thrd_sleep;::time;::timespec_get;::tmpfile;::tmpfile_s;::tmpnam;::tmpnam_s;::tss_create;::tss_get;::tss_set;::ungetc;::ungetwc;::vfprintf;::vfprintf_s;::vfscanf;::vfscanf_s;::vfwprintf;::vfwprintf_s;::vfwscanf;::vfwscanf_s;::vprintf_s;::vscanf;::vscanf_s;::vsnprintf;::vsnprintf_s;::vsprintf;::vsprintf_s;::vsscanf;::vsscanf_s;::vswprintf;::vswprintf_s;::vswscanf;::vswscanf_s;::vwprintf_s;::vwscanf;::vwscanf_s;::wcrtomb;::wcschr;::wcsftime;::wcspbrk;::wcsrchr;::wcsrtombs;::wcsrtombs_s;::wcsstr;::wcstod;::wcstof;::wcstoimax;::wcstok;::wcstok_s;::wcstol;::wcstold;::wcstoll;::wcstombs;::wcstombs_s;::wcstoul;::wcstoull;::wcstoumax;::wcsxfrm;::wctob;::wctrans;::wctype;::wmemchr;::wprintf_s;::wscanf;::wscanf_s;' + - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField + value: 'false' + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: 'true' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' + - key: llvm-qualified-auto.AddConstToQualified + value: 'false' + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: llvm-else-after-return.WarnOnUnfixable + value: 'false' + - key: google-readability-function-size.StatementThreshold + value: '800' +... + diff --git a/thirdparty/libjuice/.editorconfig b/thirdparty/libjuice/.editorconfig new file mode 100644 index 0000000..d080c5f --- /dev/null +++ b/thirdparty/libjuice/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: https://EditorConfig.org + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = tab +indent_size = 4 + diff --git a/thirdparty/libjuice/.github/FUNDING.yml b/thirdparty/libjuice/.github/FUNDING.yml new file mode 100644 index 0000000..9b11c07 --- /dev/null +++ b/thirdparty/libjuice/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: ['paullouisageneau'] +custom: ['https://paypal.me/paullouisageneau'] + diff --git a/thirdparty/libjuice/.github/workflows/build.yml b/thirdparty/libjuice/.github/workflows/build.yml new file mode 100644 index 0000000..179aeeb --- /dev/null +++ b/thirdparty/libjuice/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build and test +on: + push: + branches: + - master + pull_request: +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: install packages + run: sudo apt update && sudo apt install nettle-dev clang-tidy + - name: cmake + run: cmake -B build -DUSE_NETTLE=1 -DWARNINGS_AS_ERRORS=1 -DCLANG_TIDY=ON + - name: make + run: (cd build; make) + - name: test + run: ./build/tests + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: cmake + run: cmake -B build -DWARNINGS_AS_ERRORS=1 -DENABLE_LOCAL_ADDRESS_TRANSLATION=1 + - name: make + run: (cd build; make) + - name: test + run: ./build/tests + build-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: ilammy/msvc-dev-cmd@v1 + - name: cmake + run: cmake -B build -G "NMake Makefiles" -DWARNINGS_AS_ERRORS=1 + - name: nmake + run: | + cd build + nmake + - name: test + run: build/tests.exe + diff --git a/thirdparty/libjuice/.gitignore b/thirdparty/libjuice/.gitignore new file mode 100644 index 0000000..d855c3d --- /dev/null +++ b/thirdparty/libjuice/.gitignore @@ -0,0 +1,8 @@ +build/ +*.d +*.o +*.a +*.so +compile_commands.json +tests + diff --git a/thirdparty/libjuice/CMakeLists.txt b/thirdparty/libjuice/CMakeLists.txt new file mode 100644 index 0000000..733efde --- /dev/null +++ b/thirdparty/libjuice/CMakeLists.txt @@ -0,0 +1,249 @@ +cmake_minimum_required(VERSION 3.7) +project(libjuice + VERSION 1.2.3 + LANGUAGES C) +set(PROJECT_DESCRIPTION "UDP Interactive Connectivity Establishment (ICE) library") + +option(USE_NETTLE "Use Nettle for hash functions" OFF) +option(NO_SERVER "Disable server support" OFF) +option(NO_TESTS "Disable tests build" OFF) +option(NO_EXPORT_HEADER "Disable export header" OFF) +option(WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) +option(FUZZER "Enable oss-fuzz fuzzing" OFF) +option(CLANG_TIDY "Enable clang-tidy" OFF) + +# Mitigations +option(DISABLE_CONSENT_FRESHNESS "Disable RFC 7675 Consent Freshness" OFF) +option(ENABLE_LOCALHOST_ADDRESS "List localhost addresses in candidates" OFF) +option(ENABLE_LOCAL_ADDRESS_TRANSLATION "Translate local addresses to localhost" OFF) + +set(C_STANDARD 11) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake/Modules) + +if(WIN32) + add_definitions(-DWIN32_LEAN_AND_MEAN) + + if(MSVC) + add_definitions(-DNOMINMAX) + add_definitions(-D_CRT_SECURE_NO_WARNINGS) + endif() +endif() + +if(CLANG_TIDY) + set(CMAKE_C_CLANG_TIDY clang-tidy) +endif() + +set(LIBJUICE_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/src/addr.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/agent.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/crc32.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/const_time.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/conn.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/conn_poll.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/conn_thread.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/conn_mux.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/base64.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/hash.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/hmac.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/ice.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/juice.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/log.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/random.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/server.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/stun.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/timestamp.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/turn.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/udp.c +) +source_group("Source Files" FILES "${LIBJUICE_SOURCES}") + +set(LIBJUICE_HEADERS + ${CMAKE_CURRENT_SOURCE_DIR}/include/juice/juice.h +) +source_group("Header Files" FILES "${LIBJUICE_HEADERS}") + +set(TESTS_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/test/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/crc32.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/base64.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/stun.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/gathering.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/connectivity.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/turn.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/thread.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/mux.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/notrickle.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/server.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/conflict.c + ${CMAKE_CURRENT_SOURCE_DIR}/test/bind.c +) +source_group("Test Files" FILES "${TESTS_SOURCES}") + +set(FUZZER_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/fuzzer/fuzzer.c +) +source_group("Fuzzer Files" FILES "${FUZZER_SOURCES}") + +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +add_library(juice SHARED EXCLUDE_FROM_ALL ${LIBJUICE_SOURCES}) +set_target_properties(juice PROPERTIES VERSION ${PROJECT_VERSION}) +target_include_directories(juice PUBLIC + $ + $) +target_include_directories(juice PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/juice) +target_include_directories(juice PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_compile_definitions(juice PRIVATE $<$:RELEASE=1>) +target_link_libraries(juice PRIVATE Threads::Threads) + +add_library(juice-static STATIC ${LIBJUICE_SOURCES}) +set_target_properties(juice-static PROPERTIES VERSION ${PROJECT_VERSION}) +target_include_directories(juice-static PUBLIC + $ + $) +target_include_directories(juice-static PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/juice) +target_include_directories(juice-static PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +target_compile_definitions(juice-static PRIVATE $<$:RELEASE=1>) +target_compile_definitions(juice-static PUBLIC JUICE_STATIC) +target_link_libraries(juice-static PRIVATE Threads::Threads) + +if(WIN32) + target_link_libraries(juice PRIVATE + ws2_32 # winsock2 + bcrypt) + target_link_libraries(juice-static PRIVATE + ws2_32 # winsock2 + bcrypt) +endif() + +if(USE_NETTLE) + find_package(Nettle REQUIRED) + target_compile_definitions(juice PRIVATE USE_NETTLE=1) + target_link_libraries(juice PRIVATE Nettle::Nettle) + target_compile_definitions(juice-static PRIVATE USE_NETTLE=1) + target_link_libraries(juice-static PRIVATE Nettle::Nettle) +else() + target_compile_definitions(juice PRIVATE USE_NETTLE=0) + target_compile_definitions(juice-static PRIVATE USE_NETTLE=0) +endif() + +if(NO_SERVER) + target_compile_definitions(juice PRIVATE NO_SERVER) + target_compile_definitions(juice-static PRIVATE NO_SERVER) +endif() + +if(APPLE) + # This seems to be necessary on MacOS + target_include_directories(juice PRIVATE /usr/local/include) + target_include_directories(juice-static PRIVATE /usr/local/include) +endif() + +set_target_properties(juice PROPERTIES EXPORT_NAME LibJuice) +add_library(LibJuice::LibJuice ALIAS juice) + +set_target_properties(juice-static PROPERTIES EXPORT_NAME LibJuiceStatic) +add_library(LibJuice::LibJuiceStatic ALIAS juice-static) + +install(TARGETS juice-static EXPORT LibJuiceTargets + RUNTIME DESTINATION bin + LIBRARY DESTINATION lib + ARCHIVE DESTINATION lib +) + +install(FILES ${LIBJUICE_HEADERS} DESTINATION include/juice) + +# Export Targets +install( + EXPORT LibJuiceTargets + FILE LibJuiceTargets.cmake + NAMESPACE LibJuice:: + DESTINATION lib/cmake/LibJuice +) + +# Export config +install( + FILES ${CMAKE_CURRENT_SOURCE_DIR}/cmake/LibJuiceConfig.cmake + DESTINATION lib/cmake/LibJuice +) + +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + ${CMAKE_BINARY_DIR}/LibJuiceConfigVersion.cmake + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) +install(FILES ${CMAKE_BINARY_DIR}/LibJuiceConfigVersion.cmake + DESTINATION lib/cmake/LibJuice) + +if(NOT NO_EXPORT_HEADER AND CMAKE_VERSION VERSION_GREATER_EQUAL "3.12") + include(GenerateExportHeader) + generate_export_header(juice + EXPORT_MACRO_NAME JUICE_EXPORT + NO_EXPORT_MACRO_NAME JUICE_NO_EXPORT + DEPRECATED_MACRO_NAME JUICE_DEPRECATED + STATIC_DEFINE JUICE_STATIC) + target_include_directories(juice PUBLIC $) + target_compile_definitions(juice PUBLIC -DJUICE_HAS_EXPORT_HEADER) + set_target_properties(juice PROPERTIES C_VISIBILITY_PRESET hidden) + install(FILES ${PROJECT_BINARY_DIR}/juice_export.h DESTINATION include/juice) +else() + target_compile_definitions(juice PRIVATE JUICE_EXPORTS) + target_compile_definitions(juice-static PRIVATE JUICE_EXPORTS) +endif() + +if(NOT MSVC) + target_compile_options(juice PRIVATE -Wall -Wextra) + target_compile_options(juice-static PRIVATE -Wall -Wextra) +endif() + +if(WARNINGS_AS_ERRORS) + if(MSVC) + target_compile_options(juice PRIVATE /WX) + target_compile_options(juice-static PRIVATE /WX) + else() + target_compile_options(juice PRIVATE -Werror) + target_compile_options(juice-static PRIVATE -Werror) + endif() +endif() + +if(DISABLE_CONSENT_FRESHNESS) + target_compile_definitions(juice PRIVATE JUICE_DISABLE_CONSENT_FRESHNESS=1) + target_compile_definitions(juice-static PRIVATE JUICE_DISABLE_CONSENT_FRESHNESS=1) +endif() + +if(ENABLE_LOCALHOST_ADDRESS) + target_compile_definitions(juice PRIVATE JUICE_ENABLE_LOCALHOST_ADDRESS=1) + target_compile_definitions(juice-static PRIVATE JUICE_ENABLE_LOCALHOST_ADDRESS=1) +endif() + +if(ENABLE_LOCAL_ADDRESS_TRANSLATION) + target_compile_definitions(juice PRIVATE JUICE_ENABLE_LOCAL_ADDRESS_TRANSLATION=1) + target_compile_definitions(juice-static PRIVATE JUICE_ENABLE_LOCAL_ADDRESS_TRANSLATION=1) +endif() + +# Tests +if(NOT NO_TESTS) + add_executable(juice-tests ${TESTS_SOURCES}) + target_include_directories(juice-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_include_directories(juice-tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/juice) + + set_target_properties(juice-tests PROPERTIES + VERSION ${PROJECT_VERSION} + OUTPUT_NAME tests) + + set_target_properties(juice-tests PROPERTIES + XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER com.github.paullouisageneau.libjuice.tests) + + target_link_libraries(juice-tests juice Threads::Threads) +endif() + +# Fuzzer +if(FUZZER) + add_executable(stun-fuzzer ${FUZZER_SOURCES}) + target_include_directories(stun-fuzzer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_include_directories(stun-fuzzer PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/juice) + + set_target_properties(stun-fuzzer PROPERTIES OUTPUT_NAME fuzzer) + target_link_libraries(stun-fuzzer juice-static ${LIB_FUZZING_ENGINE}) +endif() diff --git a/thirdparty/libjuice/LICENSE b/thirdparty/libjuice/LICENSE new file mode 100644 index 0000000..14e2f77 --- /dev/null +++ b/thirdparty/libjuice/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/thirdparty/libjuice/Makefile b/thirdparty/libjuice/Makefile new file mode 100644 index 0000000..986f594 --- /dev/null +++ b/thirdparty/libjuice/Makefile @@ -0,0 +1,77 @@ +# libjuice + +NAME=libjuice +CC=$(CROSS)gcc +AR=$(CROSS)ar +RM=rm -f +CFLAGS=-O2 -pthread -fPIC -Wno-address-of-packed-member +LDFLAGS=-pthread +LIBS= + +INCLUDES=-Iinclude/juice +LDLIBS= + +USE_NETTLE ?= 0 +ifneq ($(USE_NETTLE), 0) + CFLAGS+=-DUSE_NETTLE=1 + LIBS+=nettle +else + CFLAGS+=-DUSE_NETTLE=0 +endif + +NO_SERVER ?= 0 +ifneq ($(NO_SERVER), 0) + CFLAGS+=-DNO_SERVER +endif + +FORCE_M32 ?= 0 +ifneq ($(FORCE_M32), 0) + CFLAGS+= -m32 + LDFLAGS+= -m32 +endif + +CFLAGS+=-DJUICE_EXPORTS + +ifneq ($(LIBS), "") +INCLUDES+=$(if $(LIBS),$(shell pkg-config --cflags $(LIBS)),) +LDLIBS+=$(if $(LIBS), $(shell pkg-config --libs $(LIBS)),) +endif + +SRCS=$(shell printf "%s " src/*.c) +OBJS=$(subst .c,.o,$(SRCS)) + +TEST_SRCS=$(shell printf "%s " test/*.c) +TEST_OBJS=$(subst .c,.o,$(TEST_SRCS)) + +all: $(NAME).a $(NAME).so tests + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(INCLUDES) -MMD -MP -o $@ -c $< + +test/%.o: test/%.c + $(CC) $(CFLAGS) $(INCLUDES) -Iinclude -Isrc -MMD -MP -o $@ -c $< + +-include $(subst .c,.d,$(SRCS)) + +$(NAME).a: $(OBJS) + $(AR) crf $@ $(OBJS) + +$(NAME).so: $(OBJS) + $(CC) $(LDFLAGS) -shared -o $@ $(OBJS) $(LDLIBS) + +tests: $(NAME).a $(TEST_OBJS) + $(CC) $(LDFLAGS) -o $@ $(TEST_OBJS) $(LDLIBS) $(NAME).a + +clean: + -$(RM) include/juice/*.d *.d + -$(RM) src/*.o src/*.d + -$(RM) test/*.o test/*.d + +dist-clean: clean + -$(RM) $(NAME).a + -$(RM) $(NAME).so + -$(RM) tests + -$(RM) include/*~ + -$(RM) src/*~ + -$(RM) test/*~ + diff --git a/thirdparty/libjuice/README.md b/thirdparty/libjuice/README.md new file mode 100644 index 0000000..8875d46 --- /dev/null +++ b/thirdparty/libjuice/README.md @@ -0,0 +1,103 @@ +# libjuice - UDP Interactive Connectivity Establishment + +[![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-blue.svg)](https://www.mozilla.org/en-US/MPL/2.0/) +[![Build and test](https://github.com/paullouisageneau/libjuice/actions/workflows/build.yml/badge.svg)](https://github.com/paullouisageneau/libjuice/actions/workflows/build.yml) +[![AUR package](https://repology.org/badge/version-for-repo/aur/libjuice.svg)](https://repology.org/project/libjuice/versions) +[![Vcpkg package](https://repology.org/badge/version-for-repo/vcpkg/libjuice.svg)](https://repology.org/project/libjuice/versions) +[![Gitter](https://badges.gitter.im/libjuice/community.svg)](https://gitter.im/libjuice/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Discord](https://img.shields.io/discord/903257095539925006?logo=discord)](https://discord.gg/jXAP8jp3Nn) + +libjuice :lemon::sweat_drops: (_JUICE is a UDP Interactive Connectivity Establishment library_) allows to open bidirectionnal User Datagram Protocol (UDP) streams with Network Address Translator (NAT) traversal. + +The library is a simplified implementation of the Interactive Connectivity Establishment (ICE) protocol, client-side and server-side, written in C without dependencies for POSIX platforms (including GNU/Linux, Android, Apple macOS and iOS) and Microsoft Windows. The client supports only a single component over UDP per session in a standard single-gateway network topology, as this should be sufficient for the majority of use cases nowadays. + +libjuice is licensed under MPL 2.0, see [LICENSE](https://github.com/paullouisageneau/libjuice/blob/master/LICENSE). + +libjuice is available on [AUR](https://aur.archlinux.org/packages/libjuice/) and [vcpkg](https://vcpkg.info/port/libjuice). Bindings are available for [Rust](https://github.com/VollmondT/juice-rs). + +For a STUN/TURN server application based on libjuice, see [Violet](https://github.com/paullouisageneau/violet). + +## Compatibility + +The library implements a simplified but fully compatible ICE agent ([RFC5245](https://www.rfc-editor.org/rfc/rfc5245.html) then [RFC8445](https://www.rfc-editor.org/rfc/rfc8445.html)) featuring: +- STUN protocol ([RFC5389](https://www.rfc-editor.org/rfc/rfc5389.html) then [RFC8489](https://www.rfc-editor.org/rfc/rfc8489.html)) +- TURN relaying ([RFC5766](https://www.rfc-editor.org/rfc/rfc5766.html) then [RFC8656](https://www.rfc-editor.org/rfc/rfc8656.html)) +- Consent freshness ([RFC7675](https://www.rfc-editor.org/rfc/rfc7675.html)) +- SDP-based interface ([RFC8839](https://www.rfc-editor.org/rfc/rfc8839.html)) +- IPv4 and IPv6 dual-stack support +- Optional multiplexing on a single UDP port + +The limitations compared to a fully-featured ICE agent are: +- Only UDP is supported as transport protocol and other protocols are ignored. +- Only one component is supported, which is sufficient for WebRTC Data Channels and multiplexed RTP+RTCP. +- Candidates are gathered without binding to each network interface, which behaves identically to the full implementation on most client systems. + +It also implements a lightweight STUN/TURN server ([RFC8489](https://www.rfc-editor.org/rfc/rfc8489.html) and [RFC8656](https://www.rfc-editor.org/rfc/rfc8656.html)). The server can be disabled at compile-time with the `NO_SERVER` flag. + +## Dependencies + +None! + +Optionally, [Nettle](https://www.lysator.liu.se/~nisse/nettle/) can provide SHA1 and SHA256 algorithms instead of the internal implementation. + +## Building + +### Clone repository + +```bash +$ git clone https://github.com/paullouisageneau/libjuice.git +$ cd libjuice +``` + +### Build with CMake + +The CMake library targets `libjuice` and `libjuice-static` respectively correspond to the shared and static libraries. The default target will build the library and tests. It exports the targets with namespace `LibJuice::LibJuice` and `LibJuice::LibJuiceStatic` to link the library from another CMake project. + +#### POSIX-compliant operating systems (including Linux and Apple macOS) + +```bash +$ cmake -B build +$ cd build +$ make -j2 +``` + +The option `USE_NETTLE` allows to use the Nettle library instead of the internal implementation for HMAC-SHA1: +```bash +$ cmake -B build -DUSE_NETTLE=1 +$ cd build +$ make -j2 +``` + +#### Microsoft Windows with MinGW cross-compilation + +```bash +$ cmake -B build -DCMAKE_TOOLCHAIN_FILE=/usr/share/mingw/toolchain-x86_64-w64-mingw32.cmake # replace with your toolchain file +$ cd build +$ make -j2 +``` + +#### Microsoft Windows with Microsoft Visual C++ + +```bash +$ cmake -B build -G "NMake Makefiles" +$ cd build +$ nmake +``` + +### Build directly with Make (Linux only) + +```bash +$ make +``` + +The option `USE_NETTLE` allows to use the Nettle library instead of the internal implementation for HMAC-SHA1: +```bash +$ make USE_NETTLE=1 +``` + +## Example + +See [test/connectivity.c](https://github.com/paullouisageneau/libjuice/blob/master/test/connectivity.c) for a complete local connection example. + +See [test/server.c](https://github.com/paullouisageneau/libjuice/blob/master/test/server.c) for a server example. + diff --git a/thirdparty/libjuice/cmake/LibJuiceConfig.cmake b/thirdparty/libjuice/cmake/LibJuiceConfig.cmake new file mode 100644 index 0000000..a0c3537 --- /dev/null +++ b/thirdparty/libjuice/cmake/LibJuiceConfig.cmake @@ -0,0 +1,2 @@ +include("${CMAKE_CURRENT_LIST_DIR}/LibJuiceTargets.cmake") + diff --git a/thirdparty/libjuice/cmake/Modules/FindNettle.cmake b/thirdparty/libjuice/cmake/Modules/FindNettle.cmake new file mode 100644 index 0000000..80d59bb --- /dev/null +++ b/thirdparty/libjuice/cmake/Modules/FindNettle.cmake @@ -0,0 +1,142 @@ +# Copyright (C) 2020 Dieter Baron and Thomas Klausner +# +# The authors can be contacted at +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. +# +# 3. The names of the authors may not be used to endorse or promote +# products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY +# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER +# IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#[=======================================================================[.rst: +FindNettle +------- + +Finds the Nettle library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Nettle::Nettle`` + The Nettle library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``Nettle_FOUND`` + True if the system has the Nettle library. +``Nettle_VERSION`` + The version of the Nettle library which was found. +``Nettle_INCLUDE_DIRS`` + Include directories needed to use Nettle. +``Nettle_LIBRARIES`` + Libraries needed to link to Nettle. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Nettle_INCLUDE_DIR`` + The directory containing ``nettle/aes.h``. +``Nettle_LIBRARY`` + The path to the Nettle library. + +#]=======================================================================] + +find_package(PkgConfig) +pkg_check_modules(PC_Nettle QUIET nettle) + +find_path(Nettle_INCLUDE_DIR + NAMES nettle/aes.h nettle/md5.h nettle/pbkdf2.h nettle/ripemd160.h nettle/sha.h + PATHS ${PC_Nettle_INCLUDE_DIRS} +) +find_library(Nettle_LIBRARY + NAMES nettle + PATHS ${PC_Nettle_LIBRARY_DIRS} +) + +# Extract version information from the header file +if(Nettle_INCLUDE_DIR) + # This file only exists in nettle>=3.0 + if(EXISTS ${Nettle_INCLUDE_DIR}/nettle/version.h) + file(STRINGS ${Nettle_INCLUDE_DIR}/nettle/version.h _ver_major_line + REGEX "^#define NETTLE_VERSION_MAJOR *[0-9]+" + LIMIT_COUNT 1) + string(REGEX MATCH "[0-9]+" + Nettle_MAJOR_VERSION "${_ver_major_line}") + file(STRINGS ${Nettle_INCLUDE_DIR}/nettle/version.h _ver_minor_line + REGEX "^#define NETTLE_VERSION_MINOR *[0-9]+" + LIMIT_COUNT 1) + string(REGEX MATCH "[0-9]+" + Nettle_MINOR_VERSION "${_ver_minor_line}") + set(Nettle_VERSION "${Nettle_MAJOR_VERSION}.${Nettle_MINOR_VERSION}") + unset(_ver_major_line) + unset(_ver_minor_line) + else() + if(PC_Nettle_VERSION) + set(Nettle_VERSION ${PC_Nettle_VERSION}) + else() + set(Nettle_VERSION "1.0") + endif() + endif() +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Nettle + FOUND_VAR Nettle_FOUND + REQUIRED_VARS + Nettle_LIBRARY + Nettle_INCLUDE_DIR + VERSION_VAR Nettle_VERSION +) + +if(Nettle_FOUND) + set(Nettle_LIBRARIES ${Nettle_LIBRARY}) + set(Nettle_INCLUDE_DIRS ${Nettle_INCLUDE_DIR}) + set(Nettle_DEFINITIONS ${PC_Nettle_CFLAGS_OTHER}) +endif() + +if(Nettle_FOUND AND NOT TARGET Nettle::Nettle) + add_library(Nettle::Nettle UNKNOWN IMPORTED) + set_target_properties(Nettle::Nettle PROPERTIES + IMPORTED_LOCATION "${Nettle_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${PC_Nettle_CFLAGS_OTHER}" + INTERFACE_INCLUDE_DIRECTORIES "${Nettle_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced( + Nettle_INCLUDE_DIR + Nettle_LIBRARY +) + +# compatibility variables +set(Nettle_VERSION_STRING ${Nettle_VERSION}) + diff --git a/thirdparty/libjuice/fuzzer/README.md b/thirdparty/libjuice/fuzzer/README.md new file mode 100644 index 0000000..496f616 --- /dev/null +++ b/thirdparty/libjuice/fuzzer/README.md @@ -0,0 +1,26 @@ +## Fuzzer + +### Export Symbols +``` +export CC=clang +export CXX=clang++ +export CFLAGS=-fsanitize=fuzzer-no-link,address +export LIB_FUZZING_ENGINE=-fsanitize=fuzzer +export LDFLAGS=-fsanitize=address +``` + +### Build +``` +$ mkdir build +$ cd build +$ cmake -DCMAKE_BUILD_TYPE=Debug -DFUZZER=ON -DCMAKE_C_COMPILER=$CC \ +-DCMAKE_C_FLAGS=$CFLAGS -DCMAKE_EXE_LINKER_FLAGS=$CFLAGS \ +-DLIB_FUZZING_ENGINE=$LIB_FUZZING_ENGINE \ +../ +``` + +### Run +``` +$ mkdir coverage +$ ./fuzzer coverage/ ../fuzzer/input/ +``` diff --git a/thirdparty/libjuice/fuzzer/fuzzer.c b/thirdparty/libjuice/fuzzer/fuzzer.c new file mode 100644 index 0000000..60858bf --- /dev/null +++ b/thirdparty/libjuice/fuzzer/fuzzer.c @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2022 0x34d (https://github.com/0x34d) + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include +#include + +#include "stun.h" + +#define kMinInputLength 5 +#define kMaxInputLength 2048 + +extern int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + + if (Size < kMinInputLength || Size > kMaxInputLength) { + return 0; + } + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + + _juice_is_stun_datagram((void *)Data, Size); + _juice_stun_read((void *)Data, Size, &msg); + _juice_stun_check_integrity((void *)Data, Size, &msg, "VOkJxbRl1RmTxUk/WvJxBt"); + + return 0; +} diff --git a/thirdparty/libjuice/fuzzer/input/message1.raw b/thirdparty/libjuice/fuzzer/input/message1.raw new file mode 100644 index 0000000000000000000000000000000000000000..c0a79fa7abf1e8db609a06be9e4aa3b399a3c3e6 GIT binary patch literal 108 zcmV-y0F(a!0RUJb5~M=6=cfU@G}eathu^M%A^;FmR8>wObY*jNAY*K4Wo~o;Bme|% z00IAiDF6tQFZr=iCOb9&1^@|Vc64ewXf}3PARr(B2mlnC>Zc69*vnRU9`d!a)AC8d OwQ-_=C;$ZIdOOb{m?)kA literal 0 HcmV?d00001 diff --git a/thirdparty/libjuice/fuzzer/input/message2.raw b/thirdparty/libjuice/fuzzer/input/message2.raw new file mode 100644 index 0000000000000000000000000000000000000000..994779612879bb32188c5de37e414e341fc7d164 GIT binary patch literal 164 zcmZQzWSF2Rw8W`mt%>omwM7RsZ#n8Q$T28**?jJQpE+so%AjKvEaJELRT`&=hRsXZ z#Cjyp(VRbJD-v@Ha#Hp3i_#ewWEof(n1JFk3<_J1?`Me430fo*c=W}jACfA -#include -#include - -#ifdef JUICE_HAS_EXPORT_HEADER -#include "juice_export.h" -#else // no export header -#ifdef JUICE_STATIC -#define JUICE_EXPORT -#else // dynamic library -#ifdef _WIN32 -#if defined(JUICE_EXPORTS) || defined(juice_EXPORTS) -#define JUICE_EXPORT __declspec(dllexport) // building the library -#else -#define JUICE_EXPORT __declspec(dllimport) // using the library -#endif -#else // not WIN32 -#define JUICE_EXPORT -#endif -#endif -#endif - -#define JUICE_ERR_SUCCESS 0 -#define JUICE_ERR_INVALID -1 // invalid argument -#define JUICE_ERR_FAILED -2 // runtime error -#define JUICE_ERR_NOT_AVAIL -3 // element not available - -// ICE Agent - -#define JUICE_MAX_ADDRESS_STRING_LEN 64 -#define JUICE_MAX_CANDIDATE_SDP_STRING_LEN 256 -#define JUICE_MAX_SDP_STRING_LEN 4096 - -typedef struct juice_agent juice_agent_t; - -typedef enum juice_state { - JUICE_STATE_DISCONNECTED = 0, - JUICE_STATE_GATHERING, - JUICE_STATE_CONNECTING, - JUICE_STATE_CONNECTED, - JUICE_STATE_COMPLETED, - JUICE_STATE_FAILED -} juice_state_t; - -typedef void (*juice_cb_state_changed_t)(juice_agent_t *agent, juice_state_t state, void *user_ptr); -typedef void (*juice_cb_candidate_t)(juice_agent_t *agent, const char *sdp, void *user_ptr); -typedef void (*juice_cb_gathering_done_t)(juice_agent_t *agent, void *user_ptr); -typedef void (*juice_cb_recv_t)(juice_agent_t *agent, const char *data, size_t size, - void *user_ptr); - -typedef struct juice_turn_server { - const char *host; - const char *username; - const char *password; - uint16_t port; -} juice_turn_server_t; - -typedef enum juice_concurrency_mode { - JUICE_CONCURRENCY_MODE_POLL = 0, // Connections share a single thread - JUICE_CONCURRENCY_MODE_MUX, // Connections are multiplexed on a single UDP socket - JUICE_CONCURRENCY_MODE_THREAD, // Each connection runs in its own thread -} juice_concurrency_mode_t; - -typedef struct juice_config { - juice_concurrency_mode_t concurrency_mode; - - const char *stun_server_host; - uint16_t stun_server_port; - - juice_turn_server_t *turn_servers; - int turn_servers_count; - - const char *bind_address; - - uint16_t local_port_range_begin; - uint16_t local_port_range_end; - - juice_cb_state_changed_t cb_state_changed; - juice_cb_candidate_t cb_candidate; - juice_cb_gathering_done_t cb_gathering_done; - juice_cb_recv_t cb_recv; - - void *user_ptr; - -} juice_config_t; - -JUICE_EXPORT juice_agent_t *juice_create(const juice_config_t *config); -JUICE_EXPORT void juice_destroy(juice_agent_t *agent); - -JUICE_EXPORT int juice_gather_candidates(juice_agent_t *agent); -JUICE_EXPORT int juice_get_local_description(juice_agent_t *agent, char *buffer, size_t size); -JUICE_EXPORT int juice_set_remote_description(juice_agent_t *agent, const char *sdp); -JUICE_EXPORT int juice_add_remote_candidate(juice_agent_t *agent, const char *sdp); -JUICE_EXPORT int juice_set_remote_gathering_done(juice_agent_t *agent); -JUICE_EXPORT int juice_send(juice_agent_t *agent, const char *data, size_t size); -JUICE_EXPORT int juice_send_diffserv(juice_agent_t *agent, const char *data, size_t size, int ds); -JUICE_EXPORT juice_state_t juice_get_state(juice_agent_t *agent); -JUICE_EXPORT int juice_get_selected_candidates(juice_agent_t *agent, char *local, size_t local_size, - char *remote, size_t remote_size); -JUICE_EXPORT int juice_get_selected_addresses(juice_agent_t *agent, char *local, size_t local_size, - char *remote, size_t remote_size); -JUICE_EXPORT const char *juice_state_to_string(juice_state_t state); - -// ICE server - -typedef struct juice_server juice_server_t; - -typedef struct juice_server_credentials { - const char *username; - const char *password; - int allocations_quota; -} juice_server_credentials_t; - -typedef struct juice_server_config { - juice_server_credentials_t *credentials; - int credentials_count; - - int max_allocations; - int max_peers; - - const char *bind_address; - const char *external_address; - uint16_t port; - - uint16_t relay_port_range_begin; - uint16_t relay_port_range_end; - - const char *realm; - -} juice_server_config_t; - -JUICE_EXPORT juice_server_t *juice_server_create(const juice_server_config_t *config); -JUICE_EXPORT void juice_server_destroy(juice_server_t *server); - -JUICE_EXPORT uint16_t juice_server_get_port(juice_server_t *server); -JUICE_EXPORT int juice_server_add_credentials(juice_server_t *server, - const juice_server_credentials_t *credentials, - unsigned long lifetime_ms); - -// Logging - -typedef enum juice_log_level { - JUICE_LOG_LEVEL_VERBOSE = 0, - JUICE_LOG_LEVEL_DEBUG, - JUICE_LOG_LEVEL_INFO, - JUICE_LOG_LEVEL_WARN, - JUICE_LOG_LEVEL_ERROR, - JUICE_LOG_LEVEL_FATAL, - JUICE_LOG_LEVEL_NONE -} juice_log_level_t; - -typedef void (*juice_log_cb_t)(juice_log_level_t level, const char *message); - -JUICE_EXPORT void juice_set_log_level(juice_log_level_t level); -JUICE_EXPORT void juice_set_log_handler(juice_log_cb_t cb); - -#ifdef __cplusplus -} -#endif - -#endif +/** + * Copyright (c) 2020-2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_H +#define JUICE_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include + +#ifdef JUICE_HAS_EXPORT_HEADER +#include "juice_export.h" +#else // no export header +#ifdef JUICE_STATIC +#define JUICE_EXPORT +#else // dynamic library +#ifdef _WIN32 +#if defined(JUICE_EXPORTS) || defined(juice_EXPORTS) +#define JUICE_EXPORT __declspec(dllexport) // building the library +#else +#define JUICE_EXPORT __declspec(dllimport) // using the library +#endif +#else // not WIN32 +#define JUICE_EXPORT +#endif +#endif +#endif + +#define JUICE_ERR_SUCCESS 0 +#define JUICE_ERR_INVALID -1 // invalid argument +#define JUICE_ERR_FAILED -2 // runtime error +#define JUICE_ERR_NOT_AVAIL -3 // element not available + +// ICE Agent + +#define JUICE_MAX_ADDRESS_STRING_LEN 64 +#define JUICE_MAX_CANDIDATE_SDP_STRING_LEN 256 +#define JUICE_MAX_SDP_STRING_LEN 4096 + +typedef struct juice_agent juice_agent_t; + +typedef enum juice_state { + JUICE_STATE_DISCONNECTED = 0, + JUICE_STATE_GATHERING, + JUICE_STATE_CONNECTING, + JUICE_STATE_CONNECTED, + JUICE_STATE_COMPLETED, + JUICE_STATE_FAILED +} juice_state_t; + +typedef void (*juice_cb_state_changed_t)(juice_agent_t *agent, juice_state_t state, void *user_ptr); +typedef void (*juice_cb_candidate_t)(juice_agent_t *agent, const char *sdp, void *user_ptr); +typedef void (*juice_cb_gathering_done_t)(juice_agent_t *agent, void *user_ptr); +typedef void (*juice_cb_recv_t)(juice_agent_t *agent, const char *data, size_t size, + void *user_ptr); + +typedef struct juice_turn_server { + const char *host; + const char *username; + const char *password; + uint16_t port; +} juice_turn_server_t; + +typedef enum juice_concurrency_mode { + JUICE_CONCURRENCY_MODE_POLL = 0, // Connections share a single thread + JUICE_CONCURRENCY_MODE_MUX, // Connections are multiplexed on a single UDP socket + JUICE_CONCURRENCY_MODE_THREAD, // Each connection runs in its own thread +} juice_concurrency_mode_t; + +typedef struct juice_config { + juice_concurrency_mode_t concurrency_mode; + + const char *stun_server_host; + uint16_t stun_server_port; + + juice_turn_server_t *turn_servers; + int turn_servers_count; + + const char *bind_address; + + uint16_t local_port_range_begin; + uint16_t local_port_range_end; + + juice_cb_state_changed_t cb_state_changed; + juice_cb_candidate_t cb_candidate; + juice_cb_gathering_done_t cb_gathering_done; + juice_cb_recv_t cb_recv; + + void *user_ptr; + +} juice_config_t; + +JUICE_EXPORT juice_agent_t *juice_create(const juice_config_t *config); +JUICE_EXPORT void juice_destroy(juice_agent_t *agent); + +JUICE_EXPORT int juice_gather_candidates(juice_agent_t *agent); +JUICE_EXPORT int juice_get_local_description(juice_agent_t *agent, char *buffer, size_t size); +JUICE_EXPORT int juice_set_remote_description(juice_agent_t *agent, const char *sdp); +JUICE_EXPORT int juice_add_remote_candidate(juice_agent_t *agent, const char *sdp); +JUICE_EXPORT int juice_set_remote_gathering_done(juice_agent_t *agent); +JUICE_EXPORT int juice_send(juice_agent_t *agent, const char *data, size_t size); +JUICE_EXPORT int juice_send_diffserv(juice_agent_t *agent, const char *data, size_t size, int ds); +JUICE_EXPORT juice_state_t juice_get_state(juice_agent_t *agent); +JUICE_EXPORT int juice_get_selected_candidates(juice_agent_t *agent, char *local, size_t local_size, + char *remote, size_t remote_size); +JUICE_EXPORT int juice_get_selected_addresses(juice_agent_t *agent, char *local, size_t local_size, + char *remote, size_t remote_size); +JUICE_EXPORT const char *juice_state_to_string(juice_state_t state); + +// ICE server + +typedef struct juice_server juice_server_t; + +typedef struct juice_server_credentials { + const char *username; + const char *password; + int allocations_quota; +} juice_server_credentials_t; + +typedef struct juice_server_config { + juice_server_credentials_t *credentials; + int credentials_count; + + int max_allocations; + int max_peers; + + const char *bind_address; + const char *external_address; + uint16_t port; + + uint16_t relay_port_range_begin; + uint16_t relay_port_range_end; + + const char *realm; + +} juice_server_config_t; + +JUICE_EXPORT juice_server_t *juice_server_create(const juice_server_config_t *config); +JUICE_EXPORT void juice_server_destroy(juice_server_t *server); + +JUICE_EXPORT uint16_t juice_server_get_port(juice_server_t *server); +JUICE_EXPORT int juice_server_add_credentials(juice_server_t *server, + const juice_server_credentials_t *credentials, + unsigned long lifetime_ms); + +// Logging + +typedef enum juice_log_level { + JUICE_LOG_LEVEL_VERBOSE = 0, + JUICE_LOG_LEVEL_DEBUG, + JUICE_LOG_LEVEL_INFO, + JUICE_LOG_LEVEL_WARN, + JUICE_LOG_LEVEL_ERROR, + JUICE_LOG_LEVEL_FATAL, + JUICE_LOG_LEVEL_NONE +} juice_log_level_t; + +typedef void (*juice_log_cb_t)(juice_log_level_t level, const char *message); + +JUICE_EXPORT void juice_set_log_level(juice_log_level_t level); +JUICE_EXPORT void juice_set_log_handler(juice_log_cb_t cb); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/thirdparty/libjuice/lib/juice.lib b/thirdparty/libjuice/lib/juice.lib deleted file mode 100644 index 29bf3f362a09595d9050bf5874b503b5e4003981..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 542656 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 z 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

er@i_EcUm>d+JK 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≷JJk`-B{NVFP4_1(=dS=`J!j5q?X@AAnDt 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 BZ*u?u diff --git a/thirdparty/libjuice/src/addr.c b/thirdparty/libjuice/src/addr.c new file mode 100644 index 0000000..a8b2fab --- /dev/null +++ b/thirdparty/libjuice/src/addr.c @@ -0,0 +1,310 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "addr.h" +#include "log.h" + +#include +#include + +socklen_t addr_get_len(const struct sockaddr *sa) { + switch (sa->sa_family) { + case AF_INET: + return sizeof(struct sockaddr_in); + case AF_INET6: + return sizeof(struct sockaddr_in6); + default: + JLOG_WARN("Unknown address family %hu", sa->sa_family); + return 0; + } +} + +uint16_t addr_get_port(const struct sockaddr *sa) { + switch (sa->sa_family) { + case AF_INET: + return ntohs(((struct sockaddr_in *)sa)->sin_port); + case AF_INET6: + return ntohs(((struct sockaddr_in6 *)sa)->sin6_port); + default: + JLOG_WARN("Unknown address family %hu", sa->sa_family); + return 0; + } +} + +int addr_set_port(struct sockaddr *sa, uint16_t port) { + switch (sa->sa_family) { + case AF_INET: + ((struct sockaddr_in *)sa)->sin_port = htons(port); + return 0; + case AF_INET6: + ((struct sockaddr_in6 *)sa)->sin6_port = htons(port); + return 0; + default: + JLOG_WARN("Unknown address family %hu", sa->sa_family); + return -1; + } +} + +bool addr_is_any(const struct sockaddr *sa) { + switch (sa->sa_family) { + case AF_INET: { + const struct sockaddr_in *sin = (const struct sockaddr_in *)sa; + const uint8_t *b = (const uint8_t *)&sin->sin_addr; + for (int i = 0; i < 4; ++i) + if (b[i] != 0) + return false; + + return true; + } + case AF_INET6: { + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)sa; + if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) { + const uint8_t *b = (const uint8_t *)&sin6->sin6_addr + 12; + for (int i = 0; i < 4; ++i) + if (b[i] != 0) + return false; + } else { + const uint8_t *b = (const uint8_t *)&sin6->sin6_addr; + for (int i = 0; i < 16; ++i) + if (b[i] != 0) + return false; + } + return true; + } + default: + return false; + } +} + +bool addr_is_local(const struct sockaddr *sa) { + switch (sa->sa_family) { + case AF_INET: { + const struct sockaddr_in *sin = (const struct sockaddr_in *)sa; + const uint8_t *b = (const uint8_t *)&sin->sin_addr; + if (b[0] == 127) // loopback + return true; + if (b[0] == 169 && b[1] == 254) // link-local + return true; + return false; + } + case AF_INET6: { + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)sa; + if (IN6_IS_ADDR_LOOPBACK(&sin6->sin6_addr)) { + return true; + } + if (IN6_IS_ADDR_LINKLOCAL(&sin6->sin6_addr)) { + return true; + } + if (IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) { + const uint8_t *b = (const uint8_t *)&sin6->sin6_addr + 12; + if (b[0] == 127) // loopback + return true; + if (b[0] == 169 && b[1] == 254) // link-local + return true; + return false; + } + return false; + } + default: + return false; + } +} + +bool addr_unmap_inet6_v4mapped(struct sockaddr *sa, socklen_t *len) { + if (sa->sa_family != AF_INET6) + return false; + + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)sa; + if (!IN6_IS_ADDR_V4MAPPED(&sin6->sin6_addr)) + return false; + + struct sockaddr_in6 copy = *sin6; + sin6 = © + + struct sockaddr_in *sin = (struct sockaddr_in *)sa; + memset(sin, 0, sizeof(*sin)); + sin->sin_family = AF_INET; + sin->sin_port = sin6->sin6_port; + memcpy(&sin->sin_addr, ((const uint8_t *)&sin6->sin6_addr) + 12, 4); + *len = sizeof(*sin); + return true; +} + +bool addr_map_inet6_v4mapped(struct sockaddr_storage *ss, socklen_t *len) { + if (ss->ss_family != AF_INET) + return false; + + const struct sockaddr_in *sin = (const struct sockaddr_in *)ss; + struct sockaddr_in copy = *sin; + sin = © + + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ss; + memset(sin6, 0, sizeof(*sin6)); + sin6->sin6_family = AF_INET6; + sin6->sin6_port = sin->sin_port; + uint8_t *b = (uint8_t *)&sin6->sin6_addr; + memset(b, 0, 10); + memset(b + 10, 0xFF, 2); + memcpy(b + 12, (const uint8_t *)&sin->sin_addr, 4); + *len = sizeof(*sin6); + return true; +} + +bool addr_is_equal(const struct sockaddr *a, const struct sockaddr *b, bool compare_ports) { + if (a->sa_family != b->sa_family) + return false; + + switch (a->sa_family) { + case AF_INET: { + const struct sockaddr_in *ain = (const struct sockaddr_in *)a; + const struct sockaddr_in *bin = (const struct sockaddr_in *)b; + if (memcmp(&ain->sin_addr, &bin->sin_addr, 4) != 0) + return false; + if (compare_ports && ain->sin_port != bin->sin_port) + return false; + break; + } + case AF_INET6: { + const struct sockaddr_in6 *ain6 = (const struct sockaddr_in6 *)a; + const struct sockaddr_in6 *bin6 = (const struct sockaddr_in6 *)b; + if (memcmp(&ain6->sin6_addr, &bin6->sin6_addr, 16) != 0) + return false; + if (compare_ports && ain6->sin6_port != bin6->sin6_port) + return false; + break; + } + default: + return false; + } + + return true; +} + +int addr_to_string(const struct sockaddr *sa, char *buffer, size_t size) { + socklen_t salen = addr_get_len(sa); + if (salen == 0) + goto error; + + char host[ADDR_MAX_NUMERICHOST_LEN]; + char service[ADDR_MAX_NUMERICSERV_LEN]; + if (getnameinfo(sa, salen, host, ADDR_MAX_NUMERICHOST_LEN, service, ADDR_MAX_NUMERICSERV_LEN, + NI_NUMERICHOST | NI_NUMERICSERV | NI_DGRAM)) { + JLOG_ERROR("getnameinfo failed, errno=%d", sockerrno); + goto error; + } + + int len = snprintf(buffer, size, "%s:%s", host, service); + if (len < 0 || (size_t)len >= size) + goto error; + + return len; + +error: + // Make sure we still write a valid null-terminated string + snprintf(buffer, size, "?"); + return -1; +} + +// djb2 hash function +#define DJB2_INIT 5381 +static void djb2(unsigned long *hash, int i) { + *hash = ((*hash << 5) + *hash) + i; // hash * 33 + i +} + +unsigned long addr_hash(const struct sockaddr *sa, bool with_port) { + unsigned long hash = DJB2_INIT; + + djb2(&hash, sa->sa_family); + switch (sa->sa_family) { + case AF_INET: { + const struct sockaddr_in *sin = (const struct sockaddr_in *)sa; + const uint8_t *b = (const uint8_t *)&sin->sin_addr; + for (int i = 0; i < 4; ++i) + djb2(&hash, b[i]); + if (with_port) { + djb2(&hash, sin->sin_port >> 8); + djb2(&hash, sin->sin_port & 0xFF); + } + break; + } + case AF_INET6: { + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)sa; + const uint8_t *b = (const uint8_t *)&sin6->sin6_addr; + for (int i = 0; i < 16; ++i) + djb2(&hash, b[i]); + if (with_port) { + djb2(&hash, sin6->sin6_port >> 8); + djb2(&hash, sin6->sin6_port & 0xFF); + } + break; + } + default: + break; + } + + return hash; +} + +int addr_resolve(const char *hostname, const char *service, addr_record_t *records, size_t count) { + addr_record_t *end = records + count; + + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_ADDRCONFIG; + struct addrinfo *ai_list = NULL; + if (getaddrinfo(hostname, service, &hints, &ai_list)) { + JLOG_WARN("Address resolution failed for %s:%s", hostname, service); + return -1; + } + + int ret = 0; + for (struct addrinfo *ai = ai_list; ai; ai = ai->ai_next) { + if (ai->ai_family == AF_INET || ai->ai_family == AF_INET6) { + ++ret; + if (records != end) { + memcpy(&records->addr, ai->ai_addr, ai->ai_addrlen); + records->len = (socklen_t)ai->ai_addrlen; + ++records; + } + } + } + + freeaddrinfo(ai_list); + return ret; +} + +bool addr_is_numeric_hostname(const char *hostname) { + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_NUMERICHOST|AI_NUMERICSERV; + struct addrinfo *ai_list = NULL; + if (getaddrinfo(hostname, "9", &hints, &ai_list)) + return false; + + freeaddrinfo(ai_list); + return true; +} + +bool addr_record_is_equal(const addr_record_t *a, const addr_record_t *b, bool compare_ports) { + return addr_is_equal((const struct sockaddr *)&a->addr, (const struct sockaddr *)&b->addr, + compare_ports); +} + +int addr_record_to_string(const addr_record_t *record, char *buffer, size_t size) { + return addr_to_string((const struct sockaddr *)&record->addr, buffer, size); +} + +unsigned long addr_record_hash(const addr_record_t *record, bool with_port) { + return addr_hash((const struct sockaddr *)&record->addr, with_port); +} diff --git a/thirdparty/libjuice/src/addr.h b/thirdparty/libjuice/src/addr.h new file mode 100644 index 0000000..d4bdb74 --- /dev/null +++ b/thirdparty/libjuice/src/addr.h @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_ADDR_H +#define JUICE_ADDR_H + +#include "socket.h" + +#include +#include + +// IPv6 max representation length is 45 plus 4 for potential zone index +#define ADDR_MAX_NUMERICHOST_LEN 56 // 45 + 4 + 1 rounded up +#define ADDR_MAX_NUMERICSERV_LEN 8 // 5 + 1 rounded up +#define ADDR_MAX_STRING_LEN 64 + +socklen_t addr_get_len(const struct sockaddr *sa); +uint16_t addr_get_port(const struct sockaddr *sa); +int addr_set_port(struct sockaddr *sa, uint16_t port); +bool addr_is_any(const struct sockaddr *sa); +bool addr_is_local(const struct sockaddr *sa); +bool addr_unmap_inet6_v4mapped(struct sockaddr *sa, socklen_t *len); +bool addr_map_inet6_v4mapped(struct sockaddr_storage *ss, socklen_t *len); +bool addr_is_equal(const struct sockaddr *a, const struct sockaddr *b, bool compare_ports); +int addr_to_string(const struct sockaddr *sa, char *buffer, size_t size); +unsigned long addr_hash(const struct sockaddr *sa, bool with_port); + +typedef struct addr_record { + struct sockaddr_storage addr; + socklen_t len; +} addr_record_t; + +int addr_resolve(const char *hostname, const char *service, addr_record_t *records, size_t count); +bool addr_is_numeric_hostname(const char *hostname); + +bool addr_record_is_equal(const addr_record_t *a, const addr_record_t *b, bool compare_ports); +int addr_record_to_string(const addr_record_t *record, char *buffer, size_t size); +unsigned long addr_record_hash(const addr_record_t *record, bool with_port); + +#endif // JUICE_ADDR_H diff --git a/thirdparty/libjuice/src/agent.c b/thirdparty/libjuice/src/agent.c new file mode 100644 index 0000000..07aaecf --- /dev/null +++ b/thirdparty/libjuice/src/agent.c @@ -0,0 +1,2514 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "agent.h" +#include "ice.h" +#include "juice.h" +#include "log.h" +#include "random.h" +#include "stun.h" +#include "turn.h" +#include "udp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +// RFC 8656: The Permission Lifetime MUST be 300 seconds (= 5 minutes) +#define PERMISSION_LIFETIME 300000 // ms + +// RFC 8656: Channel bindings last for 10 minutes unless refreshed +#define BIND_LIFETIME 600000 // ms + +#define BUFFER_SIZE 4096 +#define DEFAULT_MAX_RECORDS_COUNT 8 + +static char *alloc_string_copy(const char *orig, bool *alloc_failed) { + if (!orig) + return NULL; + + char *copy = malloc(strlen(orig) + 1); + if (!copy) { + if (alloc_failed) + *alloc_failed = true; + + return NULL; + } + strcpy(copy, orig); + return copy; +} + +juice_agent_t *agent_create(const juice_config_t *config) { + JLOG_VERBOSE("Creating agent"); + +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { + JLOG_FATAL("WSAStartup failed"); + return NULL; + } +#endif + + juice_agent_t *agent = calloc(1, sizeof(juice_agent_t)); + if (!agent) { + JLOG_FATAL("Memory allocation for agent failed"); + return NULL; + } + + bool alloc_failed = false; + agent->config.concurrency_mode = config->concurrency_mode; + agent->config.stun_server_host = alloc_string_copy(config->stun_server_host, &alloc_failed); + agent->config.stun_server_port = config->stun_server_port; + agent->config.bind_address = alloc_string_copy(config->bind_address, &alloc_failed); + agent->config.local_port_range_begin = config->local_port_range_begin; + agent->config.local_port_range_end = config->local_port_range_end; + agent->config.cb_state_changed = config->cb_state_changed; + agent->config.cb_candidate = config->cb_candidate; + agent->config.cb_gathering_done = config->cb_gathering_done; + agent->config.cb_recv = config->cb_recv; + agent->config.user_ptr = config->user_ptr; + if (alloc_failed) { + JLOG_FATAL("Memory allocation for configuration copy failed"); + goto error; + } + + if (config->turn_servers_count <= 0) { + agent->config.turn_servers = NULL; + agent->config.turn_servers_count = 0; + } else { + agent->config.turn_servers = + calloc(config->turn_servers_count, sizeof(juice_turn_server_t)); + if (!agent->config.turn_servers) { + JLOG_FATAL("Memory allocation for TURN servers copy failed"); + goto error; + } + agent->config.turn_servers_count = config->turn_servers_count; + for (int i = 0; i < config->turn_servers_count; ++i) { + agent->config.turn_servers[i].host = + alloc_string_copy(config->turn_servers[i].host, &alloc_failed); + agent->config.turn_servers[i].username = + alloc_string_copy(config->turn_servers[i].username, &alloc_failed); + agent->config.turn_servers[i].password = + alloc_string_copy(config->turn_servers[i].password, &alloc_failed); + agent->config.turn_servers[i].port = config->turn_servers[i].port; + if (alloc_failed) { + JLOG_FATAL("Memory allocation for TURN server configuration copy failed"); + goto error; + } + } + } + + agent->state = JUICE_STATE_DISCONNECTED; + agent->mode = AGENT_MODE_UNKNOWN; + agent->selected_entry = ATOMIC_VAR_INIT(NULL); + + agent->conn_index = -1; + agent->conn_impl = NULL; + + ice_create_local_description(&agent->local); + + // RFC 8445: 16.1. Attributes + // The content of the [ICE-CONTROLLED/ICE-CONTROLLING] attribute is a 64-bit + // unsigned integer in network byte order, which contains a random number. + // The number is used for solving role conflicts, when it is referred to as + // the "tiebreaker value". An ICE agent MUST use the same number for + // all Binding requests, for all streams, within an ICE session, unless + // it has received a 487 response, in which case it MUST change the + // number. + juice_random(&agent->ice_tiebreaker, sizeof(agent->ice_tiebreaker)); + + return agent; + +error: + agent_destroy(agent); + return NULL; +} + +void agent_destroy(juice_agent_t *agent) { + JLOG_DEBUG("Destroying agent"); + + if (agent->resolver_thread_started) { + JLOG_VERBOSE("Waiting for resolver thread"); + thread_join(agent->resolver_thread, NULL); + } + + if (agent->conn_impl) { + conn_destroy(agent); + } + + // Free credentials in entries + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->turn) { + turn_destroy_map(&entry->turn->map); + free(entry->turn); + } + } + + // Free strings in config + free((void *)agent->config.stun_server_host); + for (int i = 0; i < agent->config.turn_servers_count; ++i) { + juice_turn_server_t *turn_server = agent->config.turn_servers + i; + free((void *)turn_server->host); + free((void *)turn_server->username); + free((void *)turn_server->password); + } + free(agent->config.turn_servers); + free((void *)agent->config.bind_address); + free(agent); + +#ifdef _WIN32 + WSACleanup(); +#endif + + JLOG_VERBOSE("Destroyed agent"); +} + +static bool has_nonnumeric_server_hostnames(const juice_config_t *config) { + if (config->stun_server_host && !addr_is_numeric_hostname(config->stun_server_host)) + return true; + + for (int i = 0; i < config->turn_servers_count; ++i) { + juice_turn_server_t *turn_server = config->turn_servers + i; + if (turn_server->host && !addr_is_numeric_hostname(turn_server->host)) + return true; + } + + return false; +} + +static thread_return_t THREAD_CALL resolver_thread_entry(void *arg) { + agent_resolve_servers((juice_agent_t *)arg); + return (thread_return_t)0; +} + +int agent_gather_candidates(juice_agent_t *agent) { + JLOG_VERBOSE("Gathering candidates"); + if (agent->conn_impl) { + JLOG_WARN("Candidates gathering already started"); + return 0; + } + + if (agent->mode == AGENT_MODE_UNKNOWN) { + JLOG_DEBUG("Assuming controlling mode"); + agent->mode = AGENT_MODE_CONTROLLING; + } + + agent_change_state(agent, JUICE_STATE_GATHERING); + + udp_socket_config_t socket_config; + memset(&socket_config, 0, sizeof(socket_config)); + socket_config.bind_address = agent->config.bind_address; + socket_config.port_begin = agent->config.local_port_range_begin; + socket_config.port_end = agent->config.local_port_range_end; + + if (conn_create(agent, &socket_config)) { + JLOG_FATAL("Connection creation for agent failed"); + return -1; + } + + addr_record_t records[ICE_MAX_CANDIDATES_COUNT - 1]; + int records_count = conn_get_addrs(agent, records, ICE_MAX_CANDIDATES_COUNT - 1); + if (records_count < 0) { + JLOG_ERROR("Failed to gather local host candidates"); + records_count = 0; + } else if (records_count == 0) { + JLOG_WARN("No local host candidates gathered"); + } else if (records_count > ICE_MAX_CANDIDATES_COUNT - 1) + records_count = ICE_MAX_CANDIDATES_COUNT - 1; + + conn_lock(agent); + + JLOG_VERBOSE("Adding %d local host candidates", records_count); + for (int i = 0; i < records_count; ++i) { + ice_candidate_t candidate; + if (ice_create_local_candidate(ICE_CANDIDATE_TYPE_HOST, 1, agent->local.candidates_count, + records + i, &candidate)) { + JLOG_ERROR("Failed to create host candidate"); + continue; + } + if (agent->local.candidates_count >= MAX_HOST_CANDIDATES_COUNT) { + JLOG_WARN("Local description already has the maximum number of host candidates"); + break; + } + if (ice_add_candidate(&candidate, &agent->local)) { + JLOG_ERROR("Failed to add candidate to local description"); + continue; + } + } + + ice_sort_candidates(&agent->local); + + for (int i = 0; i < agent->entries_count; ++i) + agent_translate_host_candidate_entry(agent, agent->entries + i); + + char buffer[BUFFER_SIZE]; + for (int i = 0; i < agent->local.candidates_count; ++i) { + ice_candidate_t *candidate = agent->local.candidates + i; + if (candidate->type != ICE_CANDIDATE_TYPE_HOST) + continue; + + if (ice_generate_candidate_sdp(candidate, buffer, BUFFER_SIZE) < 0) { + JLOG_ERROR("Failed to generate SDP for local candidate"); + continue; + } + + JLOG_DEBUG("Gathered host candidate: %s", buffer); + + if (agent->config.cb_candidate) + agent->config.cb_candidate(agent, buffer, agent->config.user_ptr); + } + + agent_change_state(agent, JUICE_STATE_CONNECTING); + conn_unlock(agent); + conn_interrupt(agent); + + if (has_nonnumeric_server_hostnames(&agent->config)) { + // Resolve server hostnames in a separate thread as it may block + JLOG_DEBUG("Starting resolver thread for servers"); + int ret = thread_init(&agent->resolver_thread, resolver_thread_entry, agent); + if (ret) { + JLOG_FATAL("Thread creation failed, error=%d", ret); + agent_update_gathering_done(agent); + return -1; + } + agent->resolver_thread_started = true; + } else { + JLOG_DEBUG("Resolving servers synchronously"); + if (agent_resolve_servers(agent) < 0) + return -1; + } + + return 0; +} + +int agent_resolve_servers(juice_agent_t *agent) { + conn_lock(agent); + + // TURN server resolution + juice_concurrency_mode_t mode = agent->config.concurrency_mode; + if (mode == JUICE_CONCURRENCY_MODE_MUX) { + if (agent->config.turn_servers_count > 0) + JLOG_WARN("TURN servers are not supported in mux mode"); + + } else if (agent->config.turn_servers_count > 0) { + int count = 0; + for (int i = 0; i < agent->config.turn_servers_count; ++i) { + if (count >= MAX_RELAY_ENTRIES_COUNT) + break; + + juice_turn_server_t *turn_server = agent->config.turn_servers + i; + if (!turn_server->host) + continue; + + if (!turn_server->port) + turn_server->port = 3478; // default TURN port + + char service[8]; + snprintf(service, 8, "%hu", turn_server->port); + addr_record_t records[DEFAULT_MAX_RECORDS_COUNT]; + int records_count = + addr_resolve(turn_server->host, service, records, DEFAULT_MAX_RECORDS_COUNT); + if (records_count > 0) { + if (records_count > DEFAULT_MAX_RECORDS_COUNT) + records_count = DEFAULT_MAX_RECORDS_COUNT; + + JLOG_INFO("Using TURN server %s:%s", turn_server->host, service); + + addr_record_t *record = NULL; + for (int j = 0; j < records_count; ++j) { + int family = records[j].addr.ss_family; + // Prefer IPv4 for TURN + if (family == AF_INET) { + record = records + j; + break; + } + if (family == AF_INET6 && !record) + record = records + j; + } + if (record) { + // Ignore duplicate TURN servers as they will cause conflicts + bool is_duplicate = false; + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->type == AGENT_STUN_ENTRY_TYPE_RELAY && + addr_record_is_equal(&entry->record, record, true)) { + is_duplicate = true; + break; + } + } + if (is_duplicate) { + JLOG_INFO("Duplicate TURN server, ignoring"); + continue; + } + + JLOG_VERBOSE("Registering STUN entry %d for relay request", + agent->entries_count); + agent_stun_entry_t *entry = agent->entries + agent->entries_count; + entry->type = AGENT_STUN_ENTRY_TYPE_RELAY; + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + entry->pair = NULL; + entry->record = *record; + entry->turn_redirections = 0; + entry->turn = calloc(1, sizeof(agent_turn_state_t)); + if (!entry->turn) { + JLOG_ERROR("Memory allocation for TURN state failed"); + break; + } + if (turn_init_map(&entry->turn->map, AGENT_TURN_MAP_SIZE) < 0) { + free(entry->turn); + break; + } + snprintf(entry->turn->credentials.username, STUN_MAX_USERNAME_LEN, "%s", + turn_server->username); + entry->turn->password = turn_server->password; + juice_random(entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + ++agent->entries_count; + + agent_arm_transmission(agent, entry, STUN_PACING_TIME * i); + + ++count; + } + } else { + JLOG_ERROR("TURN address resolution failed"); + } + } + } + + // STUN server resolution + // The entry is added after so the TURN server address will be matched in priority + if (agent->config.stun_server_host) { + if (!agent->config.stun_server_port) + agent->config.stun_server_port = 3478; // default STUN port + + char service[8]; + snprintf(service, 8, "%hu", agent->config.stun_server_port); + addr_record_t records[MAX_STUN_SERVER_RECORDS_COUNT]; + int records_count = addr_resolve(agent->config.stun_server_host, service, records, + MAX_STUN_SERVER_RECORDS_COUNT); + if (records_count > 0) { + if (records_count > MAX_STUN_SERVER_RECORDS_COUNT) + records_count = MAX_STUN_SERVER_RECORDS_COUNT; + + JLOG_INFO("Using STUN server %s:%s", agent->config.stun_server_host, service); + + for (int i = 0; i < records_count; ++i) { + if (i >= MAX_SERVER_ENTRIES_COUNT) + break; + JLOG_VERBOSE("Registering STUN entry %d for server request", agent->entries_count); + agent_stun_entry_t *entry = agent->entries + agent->entries_count; + entry->type = AGENT_STUN_ENTRY_TYPE_SERVER; + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + entry->pair = NULL; + entry->record = records[i]; + juice_random(entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + ++agent->entries_count; + + agent_arm_transmission(agent, entry, STUN_PACING_TIME * i); + } + } else { + JLOG_ERROR("STUN server address resolution failed"); + } + } + + agent_update_gathering_done(agent); + conn_unlock(agent); + conn_interrupt(agent); + return 0; +} + +int agent_get_local_description(juice_agent_t *agent, char *buffer, size_t size) { + conn_lock(agent); + if (ice_generate_sdp(&agent->local, buffer, size) < 0) { + JLOG_ERROR("Failed to generate local SDP description"); + conn_unlock(agent); + return -1; + } + JLOG_VERBOSE("Generated local SDP description: %s", buffer); + + if (agent->mode == AGENT_MODE_UNKNOWN) { + JLOG_DEBUG("Assuming controlling mode"); + agent->mode = AGENT_MODE_CONTROLLING; + } + conn_unlock(agent); + return 0; +} + +int agent_set_remote_description(juice_agent_t *agent, const char *sdp) { + conn_lock(agent); + JLOG_VERBOSE("Setting remote SDP description: %s", sdp); + + ice_description_t remote; + int ret = ice_parse_sdp(sdp, &remote); + if (ret < 0) { + if (ret == ICE_PARSE_MISSING_UFRAG) + JLOG_ERROR("Missing ICE user fragment in remote description"); + else if (ret == ICE_PARSE_MISSING_PWD) + JLOG_ERROR("Missing ICE password in remote description"); + else + JLOG_ERROR("Failed to parse remote SDP description"); + + conn_unlock(agent); + return -1; + } + + if (*agent->remote.ice_ufrag) { + // There is already a remote description + if (strcmp(agent->remote.ice_ufrag, remote.ice_ufrag) == 0 || + strcmp(agent->remote.ice_pwd, remote.ice_pwd) == 0) { + JLOG_DEBUG("Remote description is already set, ignoring"); + conn_unlock(agent); + return 0; + } + + JLOG_WARN("ICE restart is unsupported"); + conn_unlock(agent); + return -1; + } + + agent->remote = remote; + + if (agent->mode == AGENT_MODE_UNKNOWN) { + JLOG_DEBUG("Assuming controlled mode"); + agent->mode = AGENT_MODE_CONTROLLED; + } + + // There is only one component, therefore we can unfreeze already existing pairs now + JLOG_DEBUG("Unfreezing %d existing candidate pairs", (int)agent->candidate_pairs_count); + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + agent_unfreeze_candidate_pair(agent, agent->candidate_pairs + i); + } + JLOG_DEBUG("Adding %d candidates from remote description", (int)agent->remote.candidates_count); + for (int i = 0; i < agent->remote.candidates_count; ++i) { + ice_candidate_t *remote = agent->remote.candidates + i; + if (agent_add_candidate_pairs_for_remote(agent, remote)) + JLOG_WARN("Failed to add candidate pair from remote description"); + } + + conn_unlock(agent); + conn_interrupt(agent); + return 0; +} + +int agent_add_remote_candidate(juice_agent_t *agent, const char *sdp) { + conn_lock(agent); + JLOG_VERBOSE("Adding remote candidate: %s", sdp); + ice_candidate_t candidate; + int ret = ice_parse_candidate_sdp(sdp, &candidate); + if (ret < 0) { + if (ret == ICE_PARSE_IGNORED) + JLOG_DEBUG("Ignored SDP candidate: %s", sdp); + else if (ret == ICE_PARSE_ERROR) + JLOG_ERROR("Failed to parse remote SDP candidate: %s", sdp); + + conn_unlock(agent); + return -1; + } + if (ice_add_candidate(&candidate, &agent->remote)) { + JLOG_ERROR("Failed to add candidate to remote description"); + conn_unlock(agent); + return -1; + } + ice_candidate_t *remote = agent->remote.candidates + agent->remote.candidates_count - 1; + ret = agent_add_candidate_pairs_for_remote(agent, remote); + + conn_unlock(agent); + conn_interrupt(agent); + return ret; +} + +int agent_set_remote_gathering_done(juice_agent_t *agent) { + conn_lock(agent); + agent->remote.finished = true; + agent->fail_timestamp = 0; // So the bookkeeping will recompute it and fail + conn_unlock(agent); + return 0; +} + +int agent_send(juice_agent_t *agent, const char *data, size_t size, int ds) { + // Try not to lock in the send path + agent_stun_entry_t *selected_entry = atomic_load(&agent->selected_entry); + if (!selected_entry) { + JLOG_ERROR("Send while ICE is not connected"); + return -1; + } + + if (selected_entry->relay_entry) { + // The datagram should be sent through the relay, use a channel to minimize overhead + conn_lock(agent); // We have to lock + int ret = agent_channel_send(agent, selected_entry->relay_entry, &selected_entry->record, + data, size, ds); + conn_unlock(agent); + return ret; + } + + return agent_direct_send(agent, &selected_entry->record, data, size, ds); +} + +int agent_direct_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds) { + return conn_send(agent, dst, data, size, ds); +} + +int agent_relay_send(juice_agent_t *agent, agent_stun_entry_t *entry, const addr_record_t *dst, + const char *data, size_t size, int ds) { + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + JLOG_VERBOSE("Sending datagram via TURN Send Indication, size=%d", size); + + // Send CreatePermission if necessary + if (!turn_has_permission(&entry->turn->map, dst)) + if (agent_send_turn_create_permission_request(agent, entry, dst, ds)) + return -1; + + // Send the data in a TURN Send indication + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = STUN_CLASS_INDICATION; + msg.msg_method = STUN_METHOD_SEND; + juice_random(msg.transaction_id, STUN_TRANSACTION_ID_SIZE); + msg.peer = *dst; + msg.data = data; + msg.data_size = size; + + char buffer[BUFFER_SIZE]; + size = stun_write(buffer, BUFFER_SIZE, &msg, NULL); // no password + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + + return agent_direct_send(agent, &entry->record, buffer, size, ds); +} + +int agent_channel_send(juice_agent_t *agent, agent_stun_entry_t *entry, const addr_record_t *record, + const char *data, size_t size, int ds) { + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + // Send ChannelBind if necessary + uint16_t channel; + if (!turn_get_bound_channel(&entry->turn->map, record, &channel)) + if (agent_send_turn_channel_bind_request(agent, entry, record, ds, &channel) < 0) + return -1; + + JLOG_VERBOSE("Sending datagram via TURN ChannelData, channel=0x%hX, size=%d", channel, size); + + // Send the data wrapped as ChannelData + char buffer[BUFFER_SIZE]; + int len = turn_wrap_channel_data(buffer, BUFFER_SIZE, data, size, channel); + if (len <= 0) { + JLOG_ERROR("TURN ChannelData wrapping failed"); + return -1; + } + + return agent_direct_send(agent, &entry->record, buffer, len, ds); +} + +juice_state_t agent_get_state(juice_agent_t *agent) { + conn_lock(agent); + juice_state_t state = agent->state; + conn_unlock(agent); + return state; +} + +int agent_get_selected_candidate_pair(juice_agent_t *agent, ice_candidate_t *local, + ice_candidate_t *remote) { + conn_lock(agent); + ice_candidate_pair_t *pair = agent->selected_pair; + if (!pair) { + conn_unlock(agent); + return -1; + } + + if (local) + *local = pair->local ? *pair->local : agent->local.candidates[0]; + if (remote) + *remote = *pair->remote; + + conn_unlock(agent); + return 0; +} + +int agent_conn_update(juice_agent_t *agent, timestamp_t *next_timestamp) { + return agent_bookkeeping(agent, next_timestamp); +} + +int agent_conn_recv(juice_agent_t *agent, char *buf, size_t len, const addr_record_t *src) { + agent_input(agent, buf, len, src, NULL); + return 0; // ignore errors +} + +int agent_conn_fail(juice_agent_t *agent) { + agent_change_state(agent, JUICE_STATE_FAILED); + atomic_store(&agent->selected_entry, NULL); // disallow sending + return 0; +} + +int agent_input(juice_agent_t *agent, char *buf, size_t len, const addr_record_t *src, + const addr_record_t *relayed) { + JLOG_VERBOSE("Received datagram, size=%d", len); + + if (agent->state == JUICE_STATE_DISCONNECTED || agent->state == JUICE_STATE_GATHERING) + return 0; + + if (is_stun_datagram(buf, len)) { + if (JLOG_DEBUG_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + if (relayed) { + char relayed_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(relayed, relayed_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Received STUN datagram from %s relayed via %s", src_str, relayed_str); + } else { + JLOG_DEBUG("Received STUN datagram from %s", src_str); + } + } + stun_message_t msg; + if (stun_read(buf, len, &msg) < 0) { + JLOG_ERROR("STUN message reading failed"); + return -1; + } + return agent_dispatch_stun(agent, buf, len, &msg, src, relayed); + } + + if (JLOG_DEBUG_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + if (relayed) { + char relayed_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(relayed, relayed_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Received non-STUN datagram from %s relayed via %s", src_str, relayed_str); + } else { + JLOG_DEBUG("Received non-STUN datagram from %s", src_str); + } + } + agent_stun_entry_t *entry = agent_find_entry_from_record(agent, src, relayed); + if (!entry) { + JLOG_WARN("Received a datagram from unknown address, ignoring"); + return -1; + } + switch (entry->type) { + case AGENT_STUN_ENTRY_TYPE_RELAY: + if (is_channel_data(buf, len)) { + JLOG_DEBUG("Received ChannelData datagram"); + return agent_process_channel_data(agent, entry, buf, len); + } + break; + + case AGENT_STUN_ENTRY_TYPE_CHECK: + JLOG_DEBUG("Received application datagram"); + if (agent->config.cb_recv) + agent->config.cb_recv(agent, buf, len, agent->config.user_ptr); + return 0; + + default: + break; + } + + JLOG_WARN("Received unexpected non-STUN datagram, ignoring"); + return -1; +} + +int agent_bookkeeping(juice_agent_t *agent, timestamp_t *next_timestamp) { + JLOG_VERBOSE("Bookkeeping..."); + + timestamp_t now = current_timestamp(); + *next_timestamp = now + 6000000; + + if (agent->state == JUICE_STATE_DISCONNECTED || agent->state == JUICE_STATE_GATHERING) + return 0; + + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + + // STUN requests transmission or retransmission + if (entry->state == AGENT_STUN_ENTRY_STATE_PENDING) { + if (entry->next_transmission > now) + continue; + + if (entry->retransmissions >= 0) { + if (JLOG_DEBUG_ENABLED) { + char record_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&entry->record, record_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("STUN entry %d: Sending request to %s (%d retransmission%s left)", i, + record_str, entry->retransmissions, + entry->retransmissions >= 2 ? "s" : ""); + } + int ret; + switch (entry->type) { + case AGENT_STUN_ENTRY_TYPE_RELAY: + ret = agent_send_turn_allocate_request(agent, entry, STUN_METHOD_ALLOCATE); + break; + + default: + ret = agent_send_stun_binding(agent, entry, STUN_CLASS_REQUEST, 0, NULL, NULL); + break; + } + + if (ret >= 0) { + --entry->retransmissions; + entry->next_transmission = now + entry->retransmission_timeout; + entry->retransmission_timeout *= 2; + continue; + } + } + + // Failure sending or end of retransmissions + JLOG_DEBUG("STUN entry %d: Failed", i); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + entry->next_transmission = 0; + + switch (entry->type) { + case AGENT_STUN_ENTRY_TYPE_RELAY: + JLOG_INFO("TURN allocation failed"); + agent_update_gathering_done(agent); + break; + + case AGENT_STUN_ENTRY_TYPE_SERVER: + JLOG_INFO("STUN server binding failed"); + agent_update_gathering_done(agent); + break; + + default: + if (entry->pair) { + JLOG_DEBUG("Candidate pair check failed"); + entry->pair->state = ICE_CANDIDATE_PAIR_STATE_FAILED; + } + break; + } + } + // STUN keepalives + else if (entry->state == AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) { +#if JUICE_DISABLE_CONSENT_FRESHNESS + // No expiration +#else + // Consent freshness expiration + if (entry->pair && entry->pair->consent_expiry <= now) { + JLOG_INFO("STUN entry %d: Consent expired for candidate pair", i); + entry->pair->state = ICE_CANDIDATE_PAIR_STATE_FAILED; + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + entry->next_transmission = 0; + continue; + } +#endif + + if (entry->next_transmission > now) + continue; + + JLOG_DEBUG("STUN entry %d: Sending keepalive", i); + + juice_random(entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + + int ret; + switch (entry->type) { + case AGENT_STUN_ENTRY_TYPE_RELAY: + // RFC 8445 5.1.1.4. Keeping Candidates Alive: + // Refreshes for allocations are done using the Refresh transaction, as described in + // [RFC5766] + ret = agent_send_turn_allocate_request(agent, entry, STUN_METHOD_REFRESH); + break; + case AGENT_STUN_ENTRY_TYPE_SERVER: + // RFC 8445 5.1.1.4. Keeping Candidates Alive: + // For server-reflexive candidates learned through a Binding request, the bindings + // MUST be kept alive by additional Binding requests to the server. + ret = agent_send_stun_binding(agent, entry, STUN_CLASS_REQUEST, 0, NULL, NULL); + break; + default: +#if JUICE_DISABLE_CONSENT_FRESHNESS + // RFC 8445 11. Keepalives: + // All endpoints MUST send keepalives for each data session. [...] STUN keepalives + // MUST be used when an ICE agent is a full ICE implementation and is communicating + // with a peer that supports ICE (lite or full). [...] When STUN is being used for + // keepalives, a STUN Binding Indication is used [RFC5389]. + ret = agent_send_stun_binding(agent, entry, STUN_CLASS_INDICATION, 0, NULL, NULL); +#else + // RFC 7675 4. Design Considerations: + // STUN binding requests sent for consent freshness also serve the keepalive purpose + // (i.e., to keep NAT bindings alive). Because of that, dedicated keepalives (e.g., + // STUN Binding Indications) are not sent on candidate pairs where consent requests + // are sent, in accordance with Section 20.2.3 of [RFC5245]. + ret = agent_send_stun_binding(agent, entry, STUN_CLASS_REQUEST, 0, NULL, NULL); +#endif + break; + } + + if (ret < 0) { + JLOG_WARN("Sending keepalive failed"); + agent_arm_transmission(agent, entry, STUN_KEEPALIVE_PERIOD); + continue; + } + + agent_arm_keepalive(agent, entry); + + } else { + // Entry does not transmit, unset next transmission + entry->next_transmission = 0; + } + } + + if (agent->candidate_pairs_count == 0) + goto finally; + + int pending_count = 0; + ice_candidate_pair_t *nominated_pair = NULL; + ice_candidate_pair_t *selected_pair = NULL; + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + ice_candidate_pair_t *pair = agent->ordered_pairs[i]; + if (pair->nominated) { + // RFC 8445 8.1.1. Nominating Pairs: + // If more than one candidate pair is nominated by the controlling agent, and if the + // controlled agent accepts multiple nominations requests, the agents MUST produce the + // selected pairs and use the pairs with the highest priority. + if (!nominated_pair) { + nominated_pair = pair; + selected_pair = pair; + } + } else if (pair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) { + if (!selected_pair) + selected_pair = pair; + } else if (pair->state == ICE_CANDIDATE_PAIR_STATE_PENDING) { + if (agent->mode == AGENT_MODE_CONTROLLING && selected_pair) { + // A higher-priority pair will be used, we can stop checking. + // Entries will be synchronized after the current loop. + JLOG_VERBOSE("Cancelling check for lower-priority pair"); + pair->state = ICE_CANDIDATE_PAIR_STATE_FROZEN; + } else { + ++pending_count; + } + } + } + + // Cancel entries of frozen pairs + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair && entry->pair->state == ICE_CANDIDATE_PAIR_STATE_FROZEN && + entry->state != AGENT_STUN_ENTRY_STATE_IDLE && + entry->state != AGENT_STUN_ENTRY_STATE_CANCELLED) { + JLOG_DEBUG("STUN entry %d: Cancelled", i); + entry->state = AGENT_STUN_ENTRY_STATE_CANCELLED; + entry->next_transmission = 0; + } + } + + if (nominated_pair && nominated_pair->state == ICE_CANDIDATE_PAIR_STATE_FAILED) { + JLOG_WARN("Lost connectivity"); + agent_change_state(agent, JUICE_STATE_FAILED); + atomic_store(&agent->selected_entry, NULL); // disallow sending + return 0; + } + + if (selected_pair) { + // Succeeded + // Change selected entry if this is a new selected pair + if (agent->selected_pair != selected_pair) { + JLOG_DEBUG(selected_pair->nominated ? "New selected and nominated pair" + : "New selected pair"); + agent->selected_pair = selected_pair; + + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair == selected_pair) { + atomic_store(&agent->selected_entry, entry); + break; + } + } + } + + bool selected_pair_has_relay = + (selected_pair->local && selected_pair->local->type == ICE_CANDIDATE_TYPE_RELAYED) || + (selected_pair->remote && selected_pair->remote->type == ICE_CANDIDATE_TYPE_RELAYED); + + if (selected_pair->nominated || + (agent->mode == AGENT_MODE_CONTROLLING && !selected_pair_has_relay)) { + // Limit retransmissions of still pending entries + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair != selected_pair && + entry->state == AGENT_STUN_ENTRY_STATE_PENDING && entry->retransmissions > 1) + entry->retransmissions = 1; + } + } + + if (nominated_pair) { + // Completed + // Do not allow direct transition from connecting to completed + if (agent->state == JUICE_STATE_CONNECTING) + agent_change_state(agent, JUICE_STATE_CONNECTED); + + // Actually transition to finished only if controlled or if nothing is pending anymore + if (agent->mode == AGENT_MODE_CONTROLLED || pending_count == 0) + agent_change_state(agent, JUICE_STATE_COMPLETED); + + agent_stun_entry_t *nominated_entry = NULL; + agent_stun_entry_t *relay_entry = NULL; + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair && entry->pair == nominated_pair) { + nominated_entry = entry; + relay_entry = nominated_entry->relay_entry; + break; + } + } + + // Enable keepalive for the entry of the nominated pair + if (nominated_entry && + nominated_entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) { + nominated_entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE; + agent_arm_keepalive(agent, nominated_entry); + } + + // If the entry of the nominated candidate is relayed locally, we need also to + // refresh the corresponding TURN session regularly + if (relay_entry && relay_entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) { + relay_entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE; + agent_arm_keepalive(agent, relay_entry); + } + + // Disable keepalives for other entries + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry != nominated_entry && entry != relay_entry && + entry->state == AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED; + } + + } else { + // Connected + agent_change_state(agent, JUICE_STATE_CONNECTED); + + // RFC 8445 8.1.1. Nominating Pairs: + // Once the controlling agent has successfully nominated a candidate pair, the agent + // MUST NOT nominate another pair for same component of the data stream within the ICE + // session. + // For this reason, we wait until no pair is pending so the selected pair won't change. + if (agent->mode == AGENT_MODE_CONTROLLING && pending_count == 0 && selected_pair && + !selected_pair->nomination_requested) { + // Nominate selected + JLOG_DEBUG("Requesting pair nomination (controlling)"); + selected_pair->nomination_requested = true; + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair && entry->pair == selected_pair) { + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; // we don't want keepalives + agent_arm_transmission(agent, entry, 0); // transmit now + break; + } + } + } + } + } else if (pending_count == 0) { + // Failed + if (!agent->fail_timestamp) + agent->fail_timestamp = now + (agent->remote.finished ? 0 : ICE_FAIL_TIMEOUT); + + if (agent->fail_timestamp && now >= agent->fail_timestamp) { + agent_change_state(agent, JUICE_STATE_FAILED); + atomic_store(&agent->selected_entry, NULL); // disallow sending + return 0; + } + + if (*next_timestamp > agent->fail_timestamp) + *next_timestamp = agent->fail_timestamp; + } + +finally: + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->next_transmission && *next_timestamp > entry->next_transmission) + *next_timestamp = entry->next_transmission; + + if (entry->state == AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE && entry->pair && + *next_timestamp > entry->pair->consent_expiry) + *next_timestamp = selected_pair->consent_expiry; + } + + return 0; +} + +void agent_change_state(juice_agent_t *agent, juice_state_t state) { + if (state != agent->state) { + JLOG_INFO("Changing state to %s", juice_state_to_string(state)); + agent->state = state; + if (agent->config.cb_state_changed) + agent->config.cb_state_changed(agent, state, agent->config.user_ptr); + } +} + +int agent_verify_stun_binding(juice_agent_t *agent, void *buf, size_t size, + const stun_message_t *msg) { + if (msg->msg_method != STUN_METHOD_BINDING) + return -1; + + if (msg->msg_class == STUN_CLASS_INDICATION || msg->msg_class == STUN_CLASS_RESP_ERROR) + return 0; + + if (!msg->has_integrity) { + JLOG_WARN("Missing integrity in STUN message"); + return -1; + } + + // Check username (The USERNAME attribute is not present in responses) + if (msg->msg_class == STUN_CLASS_REQUEST) { + char username[STUN_MAX_USERNAME_LEN]; + strcpy(username, msg->credentials.username); + char *separator = strchr(username, ':'); + if (!separator) { + JLOG_WARN("STUN username invalid, username=\"%s\"", username); + return -1; + } + *separator = '\0'; + const char *local_ufrag = username; + const char *remote_ufrag = separator + 1; + if (strcmp(local_ufrag, agent->local.ice_ufrag) != 0) { + JLOG_WARN("STUN local ufrag check failed, expected=\"%s\", actual=\"%s\"", + agent->local.ice_ufrag, local_ufrag); + return -1; + } + // RFC 8445 7.3. STUN Server Procedures: + // It is possible (and in fact very likely) that the initiating agent will receive a Binding + // request prior to receiving the candidates from its peer. If this happens, the agent MUST + // immediately generate a response. + if (*agent->remote.ice_ufrag != '\0' && + strcmp(remote_ufrag, agent->remote.ice_ufrag) != 0) { + JLOG_WARN("STUN remote ufrag check failed, expected=\"%s\", actual=\"%s\"", + agent->remote.ice_ufrag, remote_ufrag); + return -1; + } + } + // Check password + const char *password = + msg->msg_class == STUN_CLASS_REQUEST ? agent->local.ice_pwd : agent->remote.ice_pwd; + if (*password == '\0') { + JLOG_WARN("STUN integrity check failed, unknown password"); + return -1; + } + if (!stun_check_integrity(buf, size, msg, password)) { + JLOG_WARN("STUN integrity check failed, password=\"%s\"", password); + return -1; + } + return 0; +} + +int agent_verify_credentials(juice_agent_t *agent, const agent_stun_entry_t *entry, void *buf, + size_t size, stun_message_t *msg) { + (void)agent; + + // RFC 8489: If the response is an error response with an error code of 400 (Bad Request) and + // does not contain either the MESSAGE-INTEGRITY or MESSAGE-INTEGRITY-SHA256 attribute, then the + // response MUST be discarded, as if it were never received. This means that retransmits, if + // applicable, will continue. + if (msg->msg_class == STUN_CLASS_INDICATION || + (msg->msg_class == STUN_CLASS_RESP_ERROR && msg->error_code != 400)) + return 0; + + if (!msg->has_integrity) { + JLOG_WARN("Missing integrity in STUN message"); + return -1; + } + if (!entry->turn) { + JLOG_WARN("No credentials for entry"); + return -1; + } + stun_credentials_t *credentials = &entry->turn->credentials; + const char *password = entry->turn->password; + + // Prepare credentials + strcpy(msg->credentials.realm, credentials->realm); + strcpy(msg->credentials.nonce, credentials->nonce); + strcpy(msg->credentials.username, credentials->username); + + // Check credentials + if (!stun_check_integrity(buf, size, msg, password)) { + JLOG_WARN("STUN integrity check failed"); + return -1; + } + return 0; +} + +int agent_dispatch_stun(juice_agent_t *agent, void *buf, size_t size, stun_message_t *msg, + const addr_record_t *src, const addr_record_t *relayed) { + if (msg->msg_method == STUN_METHOD_BINDING && msg->has_integrity) { + JLOG_VERBOSE("STUN message is from the remote peer"); + // Verify the message now + if (agent_verify_stun_binding(agent, buf, size, msg)) { + JLOG_WARN("STUN message verification failed"); + return -1; + } + if (!relayed) { + if (agent_add_remote_reflexive_candidate(agent, ICE_CANDIDATE_TYPE_PEER_REFLEXIVE, + msg->priority, src)) { + JLOG_WARN("Failed to add remote peer reflexive candidate from STUN message"); + } + } + } + + agent_stun_entry_t *entry = NULL; + if (STUN_IS_RESPONSE(msg->msg_class)) { + JLOG_VERBOSE("STUN message is a response, looking for transaction ID"); + entry = agent_find_entry_from_transaction_id(agent, msg->transaction_id); + if (!entry) { + JLOG_WARN("No STUN entry matching transaction ID, ignoring"); + return -1; + } + } else { + JLOG_VERBOSE("STUN message is a request or indication, looking for remote address"); + entry = agent_find_entry_from_record(agent, src, relayed); + if (entry) { + JLOG_VERBOSE("Found STUN entry matching remote address"); + } else { + // This may happen normally, for instance when there is no space left for reflexive + // candidates + JLOG_DEBUG("No STUN entry matching remote address, ignoring"); + return 0; + } + } + + switch (msg->msg_method) { + case STUN_METHOD_BINDING: + // Message was verified earlier, no need to re-verify + if (entry->type == AGENT_STUN_ENTRY_TYPE_CHECK && !msg->has_integrity && + (msg->msg_class == STUN_CLASS_REQUEST || msg->msg_class == STUN_CLASS_RESP_SUCCESS)) { + JLOG_WARN("Missing integrity in STUN Binding message from remote peer, ignoring"); + return -1; + } + return agent_process_stun_binding(agent, msg, entry, src, relayed); + + case STUN_METHOD_ALLOCATE: + case STUN_METHOD_REFRESH: + if (agent_verify_credentials(agent, entry, buf, size, msg)) { + JLOG_WARN("Ignoring invalid TURN Allocate message"); + return -1; + } + return agent_process_turn_allocate(agent, msg, entry); + + case STUN_METHOD_CREATE_PERMISSION: + if (agent_verify_credentials(agent, entry, buf, size, msg)) { + JLOG_WARN("Ignoring invalid TURN CreatePermission message"); + return -1; + } + return agent_process_turn_create_permission(agent, msg, entry); + + case STUN_METHOD_CHANNEL_BIND: + if (agent_verify_credentials(agent, entry, buf, size, msg)) { + JLOG_WARN("Ignoring invalid TURN ChannelBind message"); + return -1; + } + return agent_process_turn_channel_bind(agent, msg, entry); + + case STUN_METHOD_DATA: + return agent_process_turn_data(agent, msg, entry); + + default: + JLOG_WARN("Unknown STUN method 0x%X, ignoring", msg->msg_method); + return -1; + } +} + +int agent_process_stun_binding(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry, const addr_record_t *src, + const addr_record_t *relayed) { + + switch (msg->msg_class) { + case STUN_CLASS_REQUEST: { + JLOG_DEBUG("Received STUN Binding request"); + if (entry->type != AGENT_STUN_ENTRY_TYPE_CHECK) + return -1; + + ice_candidate_pair_t *pair = entry->pair; + if (msg->ice_controlling == msg->ice_controlled) { + JLOG_WARN("Controlling and controlled attributes mismatch in request"); + agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 400, msg->transaction_id, + NULL); + return -1; + } + // RFC8445 7.3.1.1. Detecting and Repairing Role Conflicts: + // If the agent is in the controlling role, and the ICE-CONTROLLING attribute is present in + // the request: + // * If the agent's tiebreaker value is larger than or equal to the contents of the + // ICE-CONTROLLING attribute, the agent generates a Binding error response and includes an + // ERROR-CODE attribute with a value of 487 (Role Conflict) but retains its role. + // * If the agent's tiebreaker value is less than the contents of the ICE-CONTROLLING + // attribute, the agent switches to the controlled role. + if (agent->mode == AGENT_MODE_CONTROLLING && msg->ice_controlling) { + JLOG_WARN("ICE role conflict (both controlling)"); + if (agent->ice_tiebreaker >= msg->ice_controlling) { + JLOG_DEBUG("Asking remote peer to switch roles"); + agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 487, + msg->transaction_id, NULL); + } else { + JLOG_DEBUG("Switching to controlled role"); + agent->mode = AGENT_MODE_CONTROLLED; + agent_update_candidate_pairs(agent); + } + break; + } + // If the agent is in the controlled role, and the ICE-CONTROLLED attribute is present in + // the request: + // * If the agent's tiebreaker value is larger than or equal to the contents of the + // ICE-CONTROLLED attribute, the agent switches to the controlling role. + // * If the agent's tiebreaker value is less than the contents of the ICE-CONTROLLED + // attribute, the agent generates a Binding error response and includes an ERROR-CODE + // attribute with a value of 487 (Role Conflict) but retains its role. + if (msg->ice_controlled && agent->mode == AGENT_MODE_CONTROLLED) { + JLOG_WARN("ICE role conflict (both controlled)"); + if (agent->ice_tiebreaker >= msg->ice_controlling) { + JLOG_DEBUG("Switching to controlling role"); + agent->mode = AGENT_MODE_CONTROLLING; + agent_update_candidate_pairs(agent); + } else { + JLOG_DEBUG("Asking remote peer to switch roles"); + agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 487, + msg->transaction_id, NULL); + } + break; + } + if (msg->use_candidate) { + if (!msg->ice_controlling) { + JLOG_WARN("STUN message use_candidate missing ice_controlling attribute"); + agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_ERROR, 400, + msg->transaction_id, NULL); + return -1; + } + // RFC 8445 7.3.1.5. Updating the Nominated Flag: + // If the state of this pair is Succeeded, it means that the check previously sent by + // this pair produced a successful response and generated a valid pair. The agent sets + // the nominated flag value of the valid pair to true. + if (pair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) { + JLOG_DEBUG("Got a nominated pair (controlled)"); + pair->nominated = true; + } else if (!pair->nomination_requested) { + JLOG_DEBUG("Pair nomination requested (controlled)"); + pair->nomination_requested = true; + } + } + // Response + if (agent_send_stun_binding(agent, entry, STUN_CLASS_RESP_SUCCESS, 0, msg->transaction_id, + src)) { + JLOG_ERROR("Failed to send STUN Binding response"); + return -1; + } + // Triggered check + // RFC 8445: If the state of that pair is Succeeded, nothing further is done. If the state + // of that pair is In-Progress, [...] the agent MUST [...] trigger a new connectivity check + // of the pair. [...] If the state of that pair is Waiting, Frozen, or Failed, the agent + // MUST [...] trigger a new connectivity check of the pair. + if (pair->state != ICE_CANDIDATE_PAIR_STATE_SUCCEEDED && *agent->remote.ice_ufrag != '\0') { + JLOG_DEBUG("Triggered pair check"); + pair->state = ICE_CANDIDATE_PAIR_STATE_PENDING; + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + agent_arm_transmission(agent, entry, STUN_PACING_TIME); + } + break; + } + case STUN_CLASS_RESP_SUCCESS: { + JLOG_DEBUG("Received STUN Binding success response from %s", + entry->type == AGENT_STUN_ENTRY_TYPE_CHECK ? "peer" : "server"); + + if (entry->type == AGENT_STUN_ENTRY_TYPE_SERVER) + JLOG_INFO("STUN server binding successful"); + + if (entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) { + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED; + entry->next_transmission = 0; + } + + if (!agent->selected_pair || !agent->selected_pair->nominated) { + // We want to send keepalives now + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE; + agent_arm_keepalive(agent, entry); + } + + if (msg->mapped.len && !relayed) { + JLOG_VERBOSE("Response has mapped address"); + + if (JLOG_INFO_ENABLED && entry->type != AGENT_STUN_ENTRY_TYPE_CHECK) { + char mapped_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&msg->mapped, mapped_str, ADDR_MAX_STRING_LEN); + JLOG_INFO("Got STUN mapped address %s from server", mapped_str); + } + + ice_candidate_type_t type = (entry->type == AGENT_STUN_ENTRY_TYPE_CHECK) + ? ICE_CANDIDATE_TYPE_PEER_REFLEXIVE + : ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE; + if (agent_add_local_reflexive_candidate(agent, type, &msg->mapped)) { + JLOG_WARN("Failed to add local peer reflexive candidate from STUN mapped address"); + } + } + + if (entry->type == AGENT_STUN_ENTRY_TYPE_CHECK) { + // 7.2.5.2.1. Non-Symmetric Transport Addresses: + // The ICE agent MUST check that the source and destination transport addresses in the + // Binding request and response are symmetric. [...] If the addresses are not symmetric, + // the agent MUST set the candidate pair state to Failed. + if (!addr_record_is_equal(src, &entry->record, true)) { + JLOG_DEBUG( + "Candidate pair check failed (non-symmetric source address in response)"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + break; + } + + ice_candidate_pair_t *pair = entry->pair; + if (!pair) { + JLOG_ERROR("STUN entry for candidate pair checking has no candidate pair"); + return -1; + } + + if (pair->state != ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) { + JLOG_DEBUG("Candidate pair check succeeded"); + pair->state = ICE_CANDIDATE_PAIR_STATE_SUCCEEDED; + } + + if (!pair->local && msg->mapped.len) + pair->local = ice_find_candidate_from_addr(&agent->local, &msg->mapped, + ICE_CANDIDATE_TYPE_UNKNOWN); + + // Update consent timestamp + pair->consent_expiry = current_timestamp() + CONSENT_TIMEOUT; + + // RFC 8445 7.3.1.5. Updating the Nominated Flag: + // [...] once the check is sent and if it generates a successful response, and + // generates a valid pair, the agent sets the nominated flag of the pair to true. + if (pair->nomination_requested) { + JLOG_DEBUG("Got a nominated pair (%s)", + agent->mode == AGENT_MODE_CONTROLLING ? "controlling" : "controlled"); + pair->nominated = true; + } + } else if (entry->type == AGENT_STUN_ENTRY_TYPE_SERVER) { + agent_update_gathering_done(agent); + } + break; + } + case STUN_CLASS_RESP_ERROR: { + if (msg->error_code != STUN_ERROR_INTERNAL_VALIDATION_FAILED) { + if (msg->error_code == 487) + JLOG_DEBUG("Got STUN Binding error response, code=%u", + (unsigned int)msg->error_code); + else + JLOG_WARN("Got STUN Binding error response, code=%u", + (unsigned int)msg->error_code); + } + + if (entry->type == AGENT_STUN_ENTRY_TYPE_CHECK) { + if (msg->error_code == 487) { + if (entry->mode == agent->mode) { + // RFC 8445 7.2.5.1. Role Conflict: + // If the Binding request generates a 487 (Role Conflict) error response, and if + // the ICE agent included an ICE-CONTROLLED attribute in the request, the agent + // MUST switch to the controlling role. If the agent included an ICE-CONTROLLING + // attribute in the request, the agent MUST switch to the controlled role. Once + // the agent has switched its role, the agent MUST [...] set the candidate pair + // state to Waiting [and] change the tiebreaker value. + JLOG_WARN("ICE role conflict"); + JLOG_DEBUG("Switching roles to %s as requested", + entry->mode == AGENT_MODE_CONTROLLING ? "controlled" + : "controlling"); + agent->mode = entry->mode == AGENT_MODE_CONTROLLING ? AGENT_MODE_CONTROLLED + : AGENT_MODE_CONTROLLING; + agent_update_candidate_pairs(agent); + + juice_random(&agent->ice_tiebreaker, sizeof(agent->ice_tiebreaker)); + if (entry->state != AGENT_STUN_ENTRY_STATE_IDLE) { // Check might not be started + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + agent_arm_transmission(agent, entry, 0); + } + } else { + JLOG_DEBUG("Already switched roles to %s as requested", + agent->mode == AGENT_MODE_CONTROLLING ? "controlling" + : "controlled"); + } + } else { + // 7.2.5.2.4. Unrecoverable STUN Response: + // If the Binding request generates a STUN error response that is unrecoverable + // [RFC5389], the ICE agent SHOULD set the candidate pair state to Failed. + JLOG_DEBUG("Chandidate pair check failed (unrecoverable error)"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + } + } else if (entry->type == AGENT_STUN_ENTRY_TYPE_SERVER) { + JLOG_INFO("STUN server binding failed (unrecoverable error)"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + } + break; + } + case STUN_CLASS_INDICATION: { + JLOG_VERBOSE("Received STUN Binding indication"); + break; + } + default: { + JLOG_WARN("Got STUN unexpected binding message, class=%u", (unsigned int)msg->msg_class); + return -1; + } + } + return 0; +} + +int agent_send_stun_binding(juice_agent_t *agent, agent_stun_entry_t *entry, stun_class_t msg_class, + unsigned int error_code, const uint8_t *transaction_id, + const addr_record_t *mapped) { + // Send STUN Binding + JLOG_DEBUG("Sending STUN Binding %s", + msg_class == STUN_CLASS_REQUEST + ? "request" + : (msg_class == STUN_CLASS_INDICATION ? "indication" : "response")); + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = msg_class; + msg.msg_method = STUN_METHOD_BINDING; + + if ((msg_class == STUN_CLASS_RESP_SUCCESS || msg_class == STUN_CLASS_RESP_ERROR) && + !transaction_id) { + JLOG_ERROR("No transaction ID specified for STUN response"); + return -1; + } + + if (transaction_id) + memcpy(msg.transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + else if (msg_class == STUN_CLASS_INDICATION) + juice_random(msg.transaction_id, STUN_TRANSACTION_ID_SIZE); + else + memcpy(msg.transaction_id, entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + + const char *password = NULL; + if (entry->type == AGENT_STUN_ENTRY_TYPE_CHECK) { + // RFC 8445 7.2.2. Forming Credentials: + // A connectivity-check Binding request MUST utilize the STUN short-term credential + // mechanism. The username for the credential is formed by concatenating the username + // fragment provided by the peer with the username fragment of the ICE agent sending the + // request, separated by a colon (":"). The password is equal to the password provided by + // the peer. + switch (msg_class) { + case STUN_CLASS_REQUEST: { + if (*agent->remote.ice_ufrag == '\0' || *agent->remote.ice_pwd == '\0') { + JLOG_DEBUG("Missing remote ICE credentials, dropping STUN binding request"); + return 0; + } + snprintf(msg.credentials.username, STUN_MAX_USERNAME_LEN, "%s:%s", + agent->remote.ice_ufrag, agent->local.ice_ufrag); + password = agent->remote.ice_pwd; + msg.ice_controlling = agent->mode == AGENT_MODE_CONTROLLING ? agent->ice_tiebreaker : 0; + msg.ice_controlled = agent->mode == AGENT_MODE_CONTROLLED ? agent->ice_tiebreaker : 0; + + // RFC 8445 7.1.1. PRIORITY + // The PRIORITY attribute MUST be included in a Binding request and be set to the value + // computed by the algorithm in Section 5.1.2 for the local candidate, but with the + // candidate type preference of peer-reflexive candidates. + int family = entry->record.addr.ss_family; + int index = entry->pair && entry->pair->local + ? (int)(entry->pair->local - agent->local.candidates) + : 0; + msg.priority = + ice_compute_priority(ICE_CANDIDATE_TYPE_PEER_REFLEXIVE, family, 1, index); + + // RFC 8445 8.1.1. Nominating Pairs: + // Once the controlling agent has picked a valid pair for nomination, it repeats the + // connectivity check that produced this valid pair [...], this time with the + // USE-CANDIDATE attribute. + msg.use_candidate = agent->mode == AGENT_MODE_CONTROLLING && entry->pair && + entry->pair->nomination_requested; + + entry->mode = agent->mode; // save current mode in case of conflict + break; + } + case STUN_CLASS_RESP_SUCCESS: + case STUN_CLASS_RESP_ERROR: { + password = agent->local.ice_pwd; + msg.error_code = error_code; + if (mapped) + msg.mapped = *mapped; + + break; + } + case STUN_CLASS_INDICATION: { + // RFC8445 11. Keepalives: + // When STUN is being used for keepalives, a STUN Binding Indication is used. The + // Indication MUST NOT utilize any authentication mechanism. It SHOULD contain the + // FINGERPRINT attribute to aid in demultiplexing, but it SHOULD NOT contain any other + // attributes. + } + } + } + + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, &msg, password); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + + if (entry->relay_entry) { + // The datagram must be sent through the relay + JLOG_DEBUG("Sending STUN message via relay"); + int ret; + if (agent->state == JUICE_STATE_COMPLETED) + ret = agent_channel_send(agent, entry->relay_entry, &entry->record, buffer, size, 0); + else + ret = agent_relay_send(agent, entry->relay_entry, &entry->record, buffer, size, 0); + + if (ret < 0) { + JLOG_WARN("STUN message send via relay failed"); + return -1; + } + return 0; + } + + // Direct send + if (agent_direct_send(agent, &entry->record, buffer, size, 0) < 0) { + JLOG_WARN("STUN message send failed"); + return -1; + } + return 0; +} + +int agent_process_turn_allocate(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry) { + if (msg->msg_method != STUN_METHOD_ALLOCATE && msg->msg_method != STUN_METHOD_REFRESH) + return -1; + + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_WARN("Received TURN %s message for a non-relay entry, ignoring", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + switch (msg->msg_class) { + case STUN_CLASS_RESP_SUCCESS: { + JLOG_DEBUG("Received TURN %s success response", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + + if (msg->msg_method == STUN_METHOD_REFRESH) { + JLOG_DEBUG("TURN refresh successful"); + // There is nothing to do + break; + } + + JLOG_DEBUG("TURN allocate successful"); + + if (!msg->relayed.len) { + JLOG_ERROR("Expected relayed address in TURN Allocate response"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + return -1; + } + + if (entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) { + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED; + entry->next_transmission = 0; + } + + if (!agent->selected_pair || !agent->selected_pair->nominated) { + // We want to send refresh requests for keepalive now + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE; + agent_arm_keepalive(agent, entry); + } + + if (msg->mapped.len) { + JLOG_VERBOSE("Response has mapped address"); + + if (JLOG_INFO_ENABLED) { + char mapped_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&msg->mapped, mapped_str, ADDR_MAX_STRING_LEN); + JLOG_INFO("Got STUN mapped address %s from TURN server", mapped_str); + } + + if (agent_add_local_reflexive_candidate(agent, ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE, + &msg->mapped)) { + JLOG_WARN("Failed to add local peer reflexive candidate from TURN mapped address"); + } + } + + entry->relayed = msg->relayed; + if (agent_add_local_relayed_candidate(agent, &msg->relayed)) { + JLOG_WARN("Failed to add local relayed candidate from TURN relayed address"); + return -1; + } + + if (JLOG_INFO_ENABLED) { + char relayed_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&entry->relayed, relayed_str, ADDR_MAX_STRING_LEN); + JLOG_INFO("Allocated TURN relayed address %s", relayed_str); + } + + agent_update_gathering_done(agent); + break; + } + case STUN_CLASS_RESP_ERROR: { + if (msg->error_code == 401) { // Unauthorized + JLOG_DEBUG("Got TURN %s Unauthorized response", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + if (*entry->turn->credentials.realm != '\0') { + JLOG_ERROR("TURN authentication failed"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + return -1; + } + if (*msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0') { + JLOG_ERROR("Expected realm and nonce in TURN error response"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + return -1; + } + + stun_process_credentials(&msg->credentials, &entry->turn->credentials); + + // Resend request when possible + agent_arm_transmission(agent, entry, 0); + + } else if (msg->error_code == 438) { // Stale Nonce + JLOG_DEBUG("Got TURN %s Stale Nonce response", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + if (*msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0') { + JLOG_ERROR("Expected realm and nonce in TURN error response"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + return -1; + } + + stun_process_credentials(&msg->credentials, &entry->turn->credentials); + + // Resend request when possible + agent_arm_transmission(agent, entry, 0); + + } else if (msg->msg_method == STUN_METHOD_ALLOCATE && + msg->error_code == 300) { // Try Alternate + // RFC 8489 10. ALTERNATE-SERVER Mechanism: + // A client using this extension handles a 300 (Try Alternate) error code as follows. + // The client looks for an ALTERNATE-SERVER attribute in the error response. If one is + // found, then the client considers the current transaction as failed and reattempts the + // request with the server specified in the attribute, using the same transport protocol + // used for the previous request. + if (!msg->alternate_server.len || + addr_record_is_equal(&msg->alternate_server, &entry->record, true)) { + JLOG_ERROR("Expected alternate server in TURN Allocate 300 Try Alternate response"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + return -1; + } + // Prevent infinite redirection loop + if (entry->turn_redirections >= MAX_TURN_REDIRECTIONS) { + JLOG_ERROR("Too many redirections for TURN Allocate"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + return -1; + } + + if (JLOG_INFO_ENABLED) { + char alternate_server_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&msg->alternate_server, alternate_server_str, + ADDR_MAX_STRING_LEN); + JLOG_INFO("Trying alternate TURN server %s", alternate_server_str); + } + + // Change record and resend request when possible + ++entry->turn_redirections; + entry->record = msg->alternate_server; + agent_arm_transmission(agent, entry, 0); + + } else { + if (msg->error_code != STUN_ERROR_INTERNAL_VALIDATION_FAILED) + JLOG_WARN("Got TURN %s error response, code=%u", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh", + (unsigned int)msg->error_code); + + JLOG_INFO("TURN allocation failed"); + entry->state = AGENT_STUN_ENTRY_STATE_FAILED; + agent_update_gathering_done(agent); + } + break; + } + default: { + JLOG_WARN("Got unexpected TURN %s message, class=%u", + msg->msg_method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh", + (unsigned int)msg->msg_class); + return -1; + } + } + return 0; +} + +int agent_send_turn_allocate_request(juice_agent_t *agent, const agent_stun_entry_t *entry, + stun_method_t method) { + if (method != STUN_METHOD_ALLOCATE && method != STUN_METHOD_REFRESH) + return -1; + + JLOG_DEBUG("Sending TURN %s request", method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_ERROR("Attempted to send a TURN %s request for a non-relay entry", + method == STUN_METHOD_ALLOCATE ? "Allocate" : "Refresh"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = STUN_CLASS_REQUEST; + msg.msg_method = method; + memcpy(msg.transaction_id, entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + + msg.credentials = entry->turn->credentials; + msg.lifetime = TURN_LIFETIME / 1000; // seconds + + // Include allocation attributes in Allocate request only + if (method == STUN_METHOD_ALLOCATE) { + msg.requested_transport = true; + } + + const char *password = *msg.credentials.nonce != '\0' ? entry->turn->password : NULL; + + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, &msg, password); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + if (agent_direct_send(agent, &entry->record, buffer, size, 0) < 0) { + JLOG_WARN("STUN message send failed"); + return -1; + } + return 0; +} + +int agent_process_turn_create_permission(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry) { + (void)(agent); + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_WARN("Received TURN CreatePermission message for a non-relay entry, ignoring"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + switch (msg->msg_class) { + case STUN_CLASS_RESP_SUCCESS: { + JLOG_DEBUG("Received TURN CreatePermission success response"); + if (!turn_set_permission(&entry->turn->map, msg->transaction_id, NULL, + PERMISSION_LIFETIME / 2)) + JLOG_WARN("Transaction ID from TURN CreatePermission response does not match"); + break; + } + case STUN_CLASS_RESP_ERROR: { + if (msg->error_code == 438) { // Stale Nonce + JLOG_DEBUG("Got TURN CreatePermission Stale Nonce response"); + if (*msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0') { + JLOG_ERROR("Expected realm and nonce in TURN error response"); + return -1; + } + + stun_process_credentials(&msg->credentials, &entry->turn->credentials); + + // Resend + addr_record_t record; + if (turn_retrieve_transaction_id(&entry->turn->map, msg->transaction_id, &record)) + agent_send_turn_create_permission_request(agent, entry, &record, 0); + + } else if (msg->error_code != STUN_ERROR_INTERNAL_VALIDATION_FAILED) { + JLOG_WARN("Got TURN CreatePermission error response, code=%u", + (unsigned int)msg->error_code); + } + break; + } + default: { + JLOG_WARN("Got unexpected TURN CreatePermission message, class=%u", + (unsigned int)msg->msg_class); + return -1; + } + } + return 0; +} + +int agent_send_turn_create_permission_request(juice_agent_t *agent, agent_stun_entry_t *entry, + const addr_record_t *record, int ds) { + if (JLOG_DEBUG_ENABLED) { + char record_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(record, record_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Sending TURN CreatePermission request for %s", record_str); + } + + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_ERROR("Attempted to send a TURN CreatePermission request for a non-relay entry"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + const stun_credentials_t *credentials = &entry->turn->credentials; + + if (*credentials->realm == '\0' || *credentials->nonce == '\0') { + JLOG_ERROR("Missing realm and nonce to send TURN CreatePermission request"); + return -1; + } + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = STUN_CLASS_REQUEST; + msg.msg_method = STUN_METHOD_CREATE_PERMISSION; + if (!turn_set_random_permission_transaction_id(&entry->turn->map, record, msg.transaction_id)) + return -1; + + msg.credentials = entry->turn->credentials; + msg.peer = *record; + + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, &msg, entry->turn->password); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + if (agent_direct_send(agent, &entry->record, buffer, size, ds) < 0) { + JLOG_WARN("STUN message send failed"); + return -1; + } + return 0; +} + +int agent_process_turn_channel_bind(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry) { + (void)agent; + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_WARN("Received TURN ChannelBind message for a non-relay entry, ignoring"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + + switch (msg->msg_class) { + case STUN_CLASS_RESP_SUCCESS: { + JLOG_DEBUG("Received TURN ChannelBind success response"); + if (!turn_bind_current_channel(&entry->turn->map, msg->transaction_id, NULL, + BIND_LIFETIME / 2)) + JLOG_WARN("Transaction ID from TURN ChannelBind response does not match"); + break; + } + case STUN_CLASS_RESP_ERROR: { + if (msg->error_code == 438) { // Stale Nonce + JLOG_DEBUG("Got TURN ChannelBind Stale Nonce response"); + if (*msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0') { + JLOG_ERROR("Expected realm and nonce in TURN error response"); + return -1; + } + + stun_process_credentials(&msg->credentials, &entry->turn->credentials); + + // Resend + addr_record_t record; + if (turn_retrieve_transaction_id(&entry->turn->map, msg->transaction_id, &record)) + agent_send_turn_channel_bind_request(agent, entry, &record, 0, NULL); + + } else if (msg->error_code != STUN_ERROR_INTERNAL_VALIDATION_FAILED) { + JLOG_WARN("Got TURN ChannelBind error response, code=%u", + (unsigned int)msg->error_code); + } + break; + } + default: { + JLOG_WARN("Got STUN unexpected ChannelBind message, class=%u", + (unsigned int)msg->msg_class); + return -1; + } + } + return 0; +} + +int agent_send_turn_channel_bind_request(juice_agent_t *agent, agent_stun_entry_t *entry, + const addr_record_t *record, int ds, + uint16_t *out_channel) { + if (JLOG_DEBUG_ENABLED) { + char record_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(record, record_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Sending TURN ChannelBind request for %s", record_str); + } + + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_ERROR("Attempted to send a TURN ChannelBind request for a non-relay entry"); + return -1; + } + if (!entry->turn) { + JLOG_ERROR("Missing TURN state on relay entry"); + return -1; + } + const stun_credentials_t *credentials = &entry->turn->credentials; + const char *password = entry->turn->password; + + if (*credentials->realm == '\0' || *credentials->nonce == '\0') { + JLOG_ERROR("Missing realm and nonce to send TURN ChannelBind request"); + return -1; + } + + uint16_t channel; + if (!turn_get_channel(&entry->turn->map, record, &channel)) + if (!turn_bind_random_channel(&entry->turn->map, record, &channel, 0)) + return -1; + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = STUN_CLASS_REQUEST; + msg.msg_method = STUN_METHOD_CHANNEL_BIND; + if (!turn_set_random_channel_transaction_id(&entry->turn->map, record, msg.transaction_id)) + return -1; + + msg.credentials = entry->turn->credentials; + msg.channel_number = channel; + msg.peer = *record; + + if (out_channel) + *out_channel = channel; + + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, &msg, password); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + if (agent_direct_send(agent, &entry->record, buffer, size, ds) < 0) { + JLOG_WARN("STUN message send failed"); + return -1; + } + return 0; +} + +int agent_process_turn_data(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry) { + if (entry->type != AGENT_STUN_ENTRY_TYPE_RELAY) { + JLOG_WARN("Received TURN Data message for a non-relay entry, ignoring"); + return -1; + } + if (msg->msg_class != STUN_CLASS_INDICATION) { + JLOG_WARN("Received non-indication TURN Data message, ignoring"); + return -1; + } + + JLOG_DEBUG("Received TURN Data indication"); + if (!msg->data) { + JLOG_WARN("Missing data in TURN Data indication"); + return -1; + } + if (!msg->peer.len) { + JLOG_WARN("Missing peer address in TURN Data indication"); + return -1; + } + return agent_input(agent, (char *)msg->data, msg->data_size, &msg->peer, &entry->relayed); +} + +int agent_process_channel_data(juice_agent_t *agent, agent_stun_entry_t *entry, char *buf, + size_t len) { + if (len < sizeof(struct channel_data_header)) { + JLOG_WARN("ChannelData is too short"); + return -1; + } + + const struct channel_data_header *header = (const struct channel_data_header *)buf; + buf += sizeof(struct channel_data_header); + len -= sizeof(struct channel_data_header); + uint16_t channel = ntohs(header->channel_number); + uint16_t length = ntohs(header->length); + JLOG_VERBOSE("Received ChannelData, channel=0x%hX, length=%hu", channel, length); + if (length > len) { + JLOG_WARN("ChannelData has invalid length"); + return -1; + } + + addr_record_t src; + if (!turn_find_channel(&entry->turn->map, channel, &src)) { + JLOG_WARN("Channel not found"); + return -1; + } + + return agent_input(agent, buf, length, &src, &entry->relayed); +} + +int agent_add_local_relayed_candidate(juice_agent_t *agent, const addr_record_t *record) { + if (ice_find_candidate_from_addr(&agent->local, record, ICE_CANDIDATE_TYPE_RELAYED)) { + JLOG_VERBOSE("The relayed local candidate already exists"); + return 0; + } + ice_candidate_t candidate; + if (ice_create_local_candidate(ICE_CANDIDATE_TYPE_RELAYED, 1, agent->local.candidates_count, + record, &candidate)) { + JLOG_ERROR("Failed to create relayed candidate"); + return -1; + } + if (ice_add_candidate(&candidate, &agent->local)) { + JLOG_ERROR("Failed to add candidate to local description"); + return -1; + } + + char buffer[BUFFER_SIZE]; + if (ice_generate_candidate_sdp(&candidate, buffer, BUFFER_SIZE) < 0) { + JLOG_ERROR("Failed to generate SDP for local candidate"); + return -1; + } + JLOG_DEBUG("Gathered relayed candidate: %s", buffer); + + // Relayed candidates must be differenciated, so match them with already known remote candidates + ice_candidate_t *local = agent->local.candidates + agent->local.candidates_count - 1; + for (int i = 0; i < agent->remote.candidates_count; ++i) { + ice_candidate_t *remote = agent->remote.candidates + i; + if (local->resolved.addr.ss_family == remote->resolved.addr.ss_family) + agent_add_candidate_pair(agent, local, remote); + } + + if (agent->config.cb_candidate) + agent->config.cb_candidate(agent, buffer, agent->config.user_ptr); + + return 0; +} + +int agent_add_local_reflexive_candidate(juice_agent_t *agent, ice_candidate_type_t type, + const addr_record_t *record) { + if (type != ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE && type != ICE_CANDIDATE_TYPE_PEER_REFLEXIVE) { + JLOG_ERROR("Invalid type for local reflexive candidate"); + return -1; + } + int family = record->addr.ss_family; + if (ice_find_candidate_from_addr(&agent->local, record, + family == AF_INET6 ? ICE_CANDIDATE_TYPE_UNKNOWN : type)) { + JLOG_VERBOSE("A local candidate exists for the mapped address"); + return 0; + } + ice_candidate_t candidate; + if (ice_create_local_candidate(type, 1, agent->local.candidates_count, record, &candidate)) { + JLOG_ERROR("Failed to create reflexive candidate"); + return -1; + } + if (candidate.type == ICE_CANDIDATE_TYPE_PEER_REFLEXIVE && + ice_candidates_count(&agent->local, ICE_CANDIDATE_TYPE_PEER_REFLEXIVE) >= + MAX_PEER_REFLEXIVE_CANDIDATES_COUNT) { + JLOG_INFO( + "Local description has the maximum number of peer reflexive candidates, ignoring"); + return 0; + } + if (ice_add_candidate(&candidate, &agent->local)) { + JLOG_ERROR("Failed to add candidate to local description"); + return -1; + } + + char buffer[BUFFER_SIZE]; + if (ice_generate_candidate_sdp(&candidate, buffer, BUFFER_SIZE) < 0) { + JLOG_ERROR("Failed to generate SDP for local candidate"); + return -1; + } + JLOG_DEBUG("Gathered reflexive candidate: %s", buffer); + + if (type != ICE_CANDIDATE_TYPE_PEER_REFLEXIVE && agent->config.cb_candidate) + agent->config.cb_candidate(agent, buffer, agent->config.user_ptr); + + return 0; +} + +int agent_add_remote_reflexive_candidate(juice_agent_t *agent, ice_candidate_type_t type, + uint32_t priority, const addr_record_t *record) { + if (type != ICE_CANDIDATE_TYPE_PEER_REFLEXIVE) { + JLOG_ERROR("Invalid type for remote reflexive candidate"); + return -1; + } + if (ice_find_candidate_from_addr(&agent->remote, record, ICE_CANDIDATE_TYPE_UNKNOWN)) { + JLOG_VERBOSE("A remote candidate exists for the remote address"); + return 0; + } + ice_candidate_t candidate; + if (ice_create_local_candidate(type, 1, agent->local.candidates_count, record, &candidate)) { + JLOG_ERROR("Failed to create reflexive candidate"); + return -1; + } + if (ice_candidates_count(&agent->remote, ICE_CANDIDATE_TYPE_PEER_REFLEXIVE) >= + MAX_PEER_REFLEXIVE_CANDIDATES_COUNT) { + JLOG_INFO( + "Remote description has the maximum number of peer reflexive candidates, ignoring"); + return 0; + } + if (ice_add_candidate(&candidate, &agent->remote)) { + JLOG_ERROR("Failed to add candidate to remote description"); + return -1; + } + + JLOG_DEBUG("Obtained a new remote reflexive candidate, priority=%lu", (unsigned long)priority); + + ice_candidate_t *remote = agent->remote.candidates + agent->remote.candidates_count - 1; + remote->priority = priority; + + return agent_add_candidate_pairs_for_remote(agent, remote); +} + +int agent_add_candidate_pair(juice_agent_t *agent, ice_candidate_t *local, // local may be NULL + ice_candidate_t *remote) { + ice_candidate_pair_t pair; + bool is_controlling = agent->mode == AGENT_MODE_CONTROLLING; + if (ice_create_candidate_pair(local, remote, is_controlling, &pair)) { + JLOG_ERROR("Failed to create candidate pair"); + return -1; + } + + if (agent->candidate_pairs_count >= MAX_CANDIDATE_PAIRS_COUNT) { + JLOG_WARN("Session already has the maximum number of candidate pairs"); + return -1; + } + + JLOG_VERBOSE("Adding new candidate pair, priority=%" PRIu64, pair.priority); + + // Add pair + ice_candidate_pair_t *pos = agent->candidate_pairs + agent->candidate_pairs_count; + *pos = pair; + ++agent->candidate_pairs_count; + + agent_update_ordered_pairs(agent); + + if (agent->entries_count == MAX_STUN_ENTRIES_COUNT) { + JLOG_WARN("No free STUN entry left for candidate pair checking"); + return -1; + } + + agent_stun_entry_t *relay_entry = NULL; + if (local && local->type == ICE_CANDIDATE_TYPE_RELAYED) { + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *other_entry = agent->entries + i; + if (other_entry->type == AGENT_STUN_ENTRY_TYPE_RELAY && + addr_record_is_equal(&other_entry->relayed, &local->resolved, true)) { + relay_entry = other_entry; + break; + } + } + if (!relay_entry) { + JLOG_ERROR("Relay entry not found"); + return -1; + } + } + + JLOG_VERBOSE("Registering STUN entry %d for candidate pair checking", agent->entries_count); + agent_stun_entry_t *entry = agent->entries + agent->entries_count; + entry->type = AGENT_STUN_ENTRY_TYPE_CHECK; + entry->state = AGENT_STUN_ENTRY_STATE_IDLE; + entry->mode = AGENT_MODE_UNKNOWN; + entry->pair = pos; + entry->record = pos->remote->resolved; + entry->relay_entry = relay_entry; + juice_random(entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + ++agent->entries_count; + + if (remote->type == ICE_CANDIDATE_TYPE_HOST) + agent_translate_host_candidate_entry(agent, entry); + + if (agent->mode == AGENT_MODE_CONTROLLING) { + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + ice_candidate_pair_t *ordered_pair = agent->ordered_pairs[i]; + if (ordered_pair == pos) { + JLOG_VERBOSE("Candidate pair has priority"); + break; + } + if (ordered_pair->state == ICE_CANDIDATE_PAIR_STATE_SUCCEEDED) { + // We found a succeeded pair with higher priority, ignore this one + JLOG_VERBOSE("Candidate pair doesn't have priority, keeping it frozen"); + return 0; + } + } + } + + // There is only one component, therefore we can unfreeze the pair and schedule it when possible + if (*agent->remote.ice_ufrag != '\0') { + JLOG_VERBOSE("Unfreezing the new candidate pair"); + agent_unfreeze_candidate_pair(agent, pos); + } + + return 0; +} + +int agent_add_candidate_pairs_for_remote(juice_agent_t *agent, ice_candidate_t *remote) { + // Here is the trick: local non-relayed candidates are undifferentiated for sending. + // Therefore, we don't need to match remote candidates with local ones. + if (agent_add_candidate_pair(agent, NULL, remote)) + return -1; + + // However, we need still to differenciate local relayed candidates + for (int i = 0; i < agent->local.candidates_count; ++i) { + ice_candidate_t *local = agent->local.candidates + i; + if (local->type == ICE_CANDIDATE_TYPE_RELAYED && + local->resolved.addr.ss_family == remote->resolved.addr.ss_family) + if (agent_add_candidate_pair(agent, local, remote)) + return -1; + } + + return 0; +} + +int agent_unfreeze_candidate_pair(juice_agent_t *agent, ice_candidate_pair_t *pair) { + if (pair->state != ICE_CANDIDATE_PAIR_STATE_FROZEN) + return 0; + + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair == pair) { + pair->state = ICE_CANDIDATE_PAIR_STATE_PENDING; + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + agent_arm_transmission(agent, entry, 0); // transmit now + return 0; + } + } + + JLOG_WARN("Unable to unfreeze the pair: no matching entry"); + return -1; +} + +void agent_arm_keepalive(juice_agent_t *agent, agent_stun_entry_t *entry) { + if (entry->state == AGENT_STUN_ENTRY_STATE_SUCCEEDED) + entry->state = AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE; + + if (entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) + return; + + timediff_t period; + switch (entry->type) { + case AGENT_STUN_ENTRY_TYPE_RELAY: + period = agent->remote.candidates_count > 0 ? TURN_REFRESH_PERIOD : STUN_KEEPALIVE_PERIOD; + break; + case AGENT_STUN_ENTRY_TYPE_SERVER: + period = STUN_KEEPALIVE_PERIOD; + break; + default: +#if JUICE_DISABLE_CONSENT_FRESHNESS + period = STUN_KEEPALIVE_PERIOD; +#else + period = MIN_CONSENT_CHECK_PERIOD + + juice_rand32() % (MAX_CONSENT_CHECK_PERIOD - MIN_CONSENT_CHECK_PERIOD + 1); +#endif + break; + } + + agent_arm_transmission(agent, entry, period); +} + +void agent_arm_transmission(juice_agent_t *agent, agent_stun_entry_t *entry, timediff_t delay) { + if (entry->state != AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE) + entry->state = AGENT_STUN_ENTRY_STATE_PENDING; + + // Arm transmission + entry->next_transmission = current_timestamp() + delay; + + if (entry->state == AGENT_STUN_ENTRY_STATE_PENDING) { + bool limit = agent->selected_pair && + (agent->selected_pair->nominated || (agent->selected_pair != entry->pair && + agent->mode == AGENT_MODE_CONTROLLING)); + entry->retransmissions = limit ? 1 : MAX_STUN_RETRANSMISSION_COUNT; + entry->retransmission_timeout = MIN_STUN_RETRANSMISSION_TIMEOUT; + } + + // Find a time slot + agent_stun_entry_t *other = agent->entries; + while (other != agent->entries + agent->entries_count) { + if (other != entry) { + timestamp_t other_transmission = other->next_transmission; + timediff_t timediff = entry->next_transmission - other_transmission; + if (other_transmission && abs((int)timediff) < STUN_PACING_TIME) { + entry->next_transmission = other_transmission + STUN_PACING_TIME; + other = agent->entries; + continue; + } + } + ++other; + } +} + +void agent_update_gathering_done(juice_agent_t *agent) { + JLOG_VERBOSE("Updating gathering status"); + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->type != AGENT_STUN_ENTRY_TYPE_CHECK && + entry->state == AGENT_STUN_ENTRY_STATE_PENDING) { + JLOG_VERBOSE("STUN server or relay entry %d is still pending", i); + return; + } + } + if (!agent->gathering_done) { + JLOG_INFO("Candidate gathering done"); + agent->local.finished = true; + agent->gathering_done = true; + + if (agent->config.cb_gathering_done) + agent->config.cb_gathering_done(agent, agent->config.user_ptr); + } +} + +void agent_update_candidate_pairs(juice_agent_t *agent) { + bool is_controlling = agent->mode == AGENT_MODE_CONTROLLING; + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + ice_candidate_pair_t *pair = agent->candidate_pairs + i; + ice_update_candidate_pair(pair, is_controlling); + } + agent_update_ordered_pairs(agent); +} + +void agent_update_ordered_pairs(juice_agent_t *agent) { + JLOG_VERBOSE("Updating ordered candidate pairs"); + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + ice_candidate_pair_t **begin = agent->ordered_pairs; + ice_candidate_pair_t **end = begin + i; + ice_candidate_pair_t **prev = end; + uint64_t priority = agent->candidate_pairs[i].priority; + while (--prev >= begin && (*prev)->priority < priority) + *(prev + 1) = *prev; + + *(prev + 1) = agent->candidate_pairs + i; + } +} + +static inline bool pair_is_relayed(const ice_candidate_pair_t *pair) { + return pair->local && pair->local->type == ICE_CANDIDATE_TYPE_RELAYED; +} + +static inline bool entry_is_relayed(const agent_stun_entry_t *entry) { + return entry->pair && pair_is_relayed(entry->pair); +} + +agent_stun_entry_t *agent_find_entry_from_transaction_id(juice_agent_t *agent, + const uint8_t *transaction_id) { + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (memcmp(transaction_id, entry->transaction_id, STUN_TRANSACTION_ID_SIZE) == 0) { + JLOG_VERBOSE("STUN entry %d matching incoming transaction ID", i); + return entry; + } + if (entry->turn) { + if (turn_retrieve_transaction_id(&entry->turn->map, transaction_id, NULL)) { + JLOG_VERBOSE("STUN entry %d matching incoming transaction ID (TURN)", i); + return entry; + } + } + } + return NULL; +} + +agent_stun_entry_t *agent_find_entry_from_record(juice_agent_t *agent, const addr_record_t *record, + const addr_record_t *relayed) { + agent_stun_entry_t *selected_entry = atomic_load(&agent->selected_entry); + + if (agent->state == JUICE_STATE_COMPLETED && selected_entry) { + // As an optimization, try to match the selected entry first + if (relayed) { + if (entry_is_relayed(selected_entry) && + addr_record_is_equal(&selected_entry->pair->local->resolved, relayed, true) && + addr_record_is_equal(&selected_entry->record, record, true)) { + JLOG_DEBUG("STUN selected entry matching incoming relayed address"); + return selected_entry; + } + } else { + if (!entry_is_relayed(selected_entry) && + addr_record_is_equal(&selected_entry->record, record, true)) { + JLOG_DEBUG("STUN selected entry matching incoming address"); + return selected_entry; + } + } + } + + if (relayed) { + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry_is_relayed(entry) && + addr_record_is_equal(&entry->pair->local->resolved, relayed, true) && + addr_record_is_equal(&entry->record, record, true)) { + JLOG_DEBUG("STUN entry %d matching incoming relayed address", i); + return entry; + } + } + } else { + // Try to match pairs by priority first + ice_candidate_pair_t *matching_pair = NULL; + for (int i = 0; i < agent->candidate_pairs_count; ++i) { + ice_candidate_pair_t *pair = agent->ordered_pairs[i]; + if (!pair_is_relayed(pair) && + addr_record_is_equal(&pair->remote->resolved, record, true)) { + matching_pair = pair; + break; + } + } + + if (matching_pair) { + // Just find the corresponding entry + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (entry->pair == matching_pair) { + JLOG_DEBUG("STUN entry %d pair matching incoming address", i); + return entry; + } + } + } + + // Try to match entries directly + for (int i = 0; i < agent->entries_count; ++i) { + agent_stun_entry_t *entry = agent->entries + i; + if (!entry_is_relayed(entry) && addr_record_is_equal(&entry->record, record, true)) { + JLOG_DEBUG("STUN entry %d matching incoming address", i); + return entry; + } + } + } + return NULL; +} + +void agent_translate_host_candidate_entry(juice_agent_t *agent, agent_stun_entry_t *entry) { + if (!entry->pair || entry->pair->remote->type != ICE_CANDIDATE_TYPE_HOST) + return; + +#if JUICE_ENABLE_LOCAL_ADDRESS_TRANSLATION + for (int i = 0; i < agent->local.candidates_count; ++i) { + ice_candidate_t *candidate = agent->local.candidates + i; + if (candidate->type != ICE_CANDIDATE_TYPE_HOST) + continue; + + if (addr_record_is_equal(&candidate->resolved, &entry->record, false)) { + JLOG_DEBUG("Entry remote address matches local candidate, translating to localhost"); + struct sockaddr_storage *addr = &entry->record.addr; + switch (addr->ss_family) { + case AF_INET6: { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)addr; + memset(&sin6->sin6_addr, 0, 16); + *((uint8_t *)&sin6->sin6_addr + 15) = 0x01; + break; + } + case AF_INET: { + struct sockaddr_in *sin = (struct sockaddr_in *)addr; + const uint8_t localhost[4] = {127, 0, 0, 1}; + memcpy(&sin->sin_addr, localhost, 4); + break; + } + default: + // Ignore + break; + } + break; + } + } +#else + (void)agent; +#endif +} diff --git a/thirdparty/libjuice/src/agent.h b/thirdparty/libjuice/src/agent.h new file mode 100644 index 0000000..65bb0ca --- /dev/null +++ b/thirdparty/libjuice/src/agent.h @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_AGENT_H +#define JUICE_AGENT_H + +#include "addr.h" +#include "conn.h" +#include "ice.h" +#include "juice.h" +#include "stun.h" +#include "thread.h" +#include "timestamp.h" +#include "turn.h" + +#include +#include + +// RFC 8445: Agents MUST NOT use an RTO value smaller than 500 ms. +#define MIN_STUN_RETRANSMISSION_TIMEOUT 500 // msecs +#define MAX_STUN_RETRANSMISSION_COUNT 5 // count (exponential backoff, will give ~30s) + +// RFC 8445: ICE agents SHOULD use a default Ta value, 50 ms, but MAY use +// another value based on the characteristics of the associated data. +#define STUN_PACING_TIME 50 // msecs + +// RFC 8445: Agents SHOULD use a Tr value of 15 seconds. Agents MAY use a bigger value but MUST NOT +// use a value smaller than 15 seconds. +#define STUN_KEEPALIVE_PERIOD 15000 // msecs + +// Consent freshness +// RFC 7675: Consent expires after 30 seconds. +#define CONSENT_TIMEOUT 30000 // msecs + +// RFC 7675: To prevent synchronization of consent checks, each interval MUST be randomized from +// between 0.8 and 1.2 times the basic period. Implementations SHOULD set a default interval of 5 +// seconds, resulting in a period between checks of 4 to 6 seconds. Implementations MUST NOT set the +// period between checks to less than 4 seconds. +#define MIN_CONSENT_CHECK_PERIOD 4000 // msecs +#define MAX_CONSENT_CHECK_PERIOD 6000 // msecs + +// TURN refresh period +#define TURN_LIFETIME 600000 // msecs, 10 min +#define TURN_REFRESH_PERIOD (TURN_LIFETIME - 60000) // msecs, lifetime - 1 min + +// ICE trickling timeout +#define ICE_FAIL_TIMEOUT 30000 // msecs + +// Max STUN and TURN server entries +#define MAX_SERVER_ENTRIES_COUNT 2 // max STUN server entries +#define MAX_RELAY_ENTRIES_COUNT 2 // max TURN server entries + +// Max TURN redirections for ALTERNATE-SERVER mechanism +#define MAX_TURN_REDIRECTIONS 1 + +// Compute max candidates and entries count +#define MAX_STUN_SERVER_RECORDS_COUNT MAX_SERVER_ENTRIES_COUNT +#define MAX_HOST_CANDIDATES_COUNT ((ICE_MAX_CANDIDATES_COUNT - MAX_STUN_SERVER_RECORDS_COUNT) / 2) +#define MAX_PEER_REFLEXIVE_CANDIDATES_COUNT MAX_HOST_CANDIDATES_COUNT +#define MAX_CANDIDATE_PAIRS_COUNT (ICE_MAX_CANDIDATES_COUNT * (1 + MAX_RELAY_ENTRIES_COUNT)) +#define MAX_STUN_ENTRIES_COUNT (MAX_CANDIDATE_PAIRS_COUNT + MAX_STUN_SERVER_RECORDS_COUNT) + +#define AGENT_TURN_MAP_SIZE ICE_MAX_CANDIDATES_COUNT + +typedef enum agent_mode { + AGENT_MODE_UNKNOWN, + AGENT_MODE_CONTROLLED, + AGENT_MODE_CONTROLLING +} agent_mode_t; + +typedef enum agent_stun_entry_type { + AGENT_STUN_ENTRY_TYPE_EMPTY, + AGENT_STUN_ENTRY_TYPE_SERVER, + AGENT_STUN_ENTRY_TYPE_RELAY, + AGENT_STUN_ENTRY_TYPE_CHECK +} agent_stun_entry_type_t; + +typedef enum agent_stun_entry_state { + AGENT_STUN_ENTRY_STATE_PENDING, + AGENT_STUN_ENTRY_STATE_CANCELLED, + AGENT_STUN_ENTRY_STATE_FAILED, + AGENT_STUN_ENTRY_STATE_SUCCEEDED, + AGENT_STUN_ENTRY_STATE_SUCCEEDED_KEEPALIVE, + AGENT_STUN_ENTRY_STATE_IDLE +} agent_stun_entry_state_t; + +typedef struct agent_turn_state { + turn_map_t map; + stun_credentials_t credentials; + const char *password; +} agent_turn_state_t; + +typedef struct agent_stun_entry { + agent_stun_entry_type_t type; + agent_stun_entry_state_t state; + agent_mode_t mode; + ice_candidate_pair_t *pair; + addr_record_t record; + addr_record_t relayed; + uint8_t transaction_id[STUN_TRANSACTION_ID_SIZE]; + timestamp_t next_transmission; + timediff_t retransmission_timeout; + int retransmissions; + + // TURN + agent_turn_state_t *turn; + unsigned int turn_redirections; + struct agent_stun_entry *relay_entry; + +} agent_stun_entry_t; + +struct juice_agent { + juice_config_t config; + juice_state_t state; + agent_mode_t mode; + + ice_description_t local; + ice_description_t remote; + + ice_candidate_pair_t candidate_pairs[MAX_CANDIDATE_PAIRS_COUNT]; + ice_candidate_pair_t *ordered_pairs[MAX_CANDIDATE_PAIRS_COUNT]; + ice_candidate_pair_t *selected_pair; + int candidate_pairs_count; + + agent_stun_entry_t entries[MAX_STUN_ENTRIES_COUNT]; + int entries_count; + atomic_ptr(agent_stun_entry_t) selected_entry; + + uint64_t ice_tiebreaker; + timestamp_t fail_timestamp; + bool gathering_done; + + int conn_index; + void *conn_impl; + + thread_t resolver_thread; + bool resolver_thread_started; +}; + +juice_agent_t *agent_create(const juice_config_t *config); +void agent_destroy(juice_agent_t *agent); + +int agent_gather_candidates(juice_agent_t *agent); +int agent_resolve_servers(juice_agent_t *agent); +int agent_get_local_description(juice_agent_t *agent, char *buffer, size_t size); +int agent_set_remote_description(juice_agent_t *agent, const char *sdp); +int agent_add_remote_candidate(juice_agent_t *agent, const char *sdp); +int agent_set_remote_gathering_done(juice_agent_t *agent); +int agent_send(juice_agent_t *agent, const char *data, size_t size, int ds); +int agent_direct_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); +int agent_relay_send(juice_agent_t *agent, agent_stun_entry_t *entry, const addr_record_t *dst, + const char *data, size_t size, int ds); +int agent_channel_send(juice_agent_t *agent, agent_stun_entry_t *entry, const addr_record_t *dst, + const char *data, size_t size, int ds); +juice_state_t agent_get_state(juice_agent_t *agent); +int agent_get_selected_candidate_pair(juice_agent_t *agent, ice_candidate_t *local, + ice_candidate_t *remote); + +int agent_conn_recv(juice_agent_t *agent, char *buf, size_t len, const addr_record_t *src); +int agent_conn_update(juice_agent_t *agent, timestamp_t *next_timestamp); +int agent_conn_fail(juice_agent_t *agent); + +int agent_input(juice_agent_t *agent, char *buf, size_t len, const addr_record_t *src, + const addr_record_t *relayed); // relayed may be NULL +int agent_bookkeeping(juice_agent_t *agent, timestamp_t *next_timestamp); +void agent_change_state(juice_agent_t *agent, juice_state_t state); +int agent_verify_stun_binding(juice_agent_t *agent, void *buf, size_t size, + const stun_message_t *msg); +int agent_verify_credentials(juice_agent_t *agent, const agent_stun_entry_t *entry, void *buf, + size_t size, stun_message_t *msg); +int agent_dispatch_stun(juice_agent_t *agent, void *buf, size_t size, stun_message_t *msg, + const addr_record_t *src, + const addr_record_t *relayed); // relayed may be NULL +int agent_process_stun_binding(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry, const addr_record_t *src, + const addr_record_t *relayed); // relayed may be NULL +int agent_send_stun_binding(juice_agent_t *agent, agent_stun_entry_t *entry, stun_class_t msg_class, + unsigned int error_code, const uint8_t *transaction_id, + const addr_record_t *mapped); +int agent_process_turn_allocate(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry); +int agent_send_turn_allocate_request(juice_agent_t *agent, const agent_stun_entry_t *entry, + stun_method_t method); +int agent_process_turn_create_permission(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry); +int agent_send_turn_create_permission_request(juice_agent_t *agent, agent_stun_entry_t *entry, + const addr_record_t *record, int ds); +int agent_process_turn_channel_bind(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry); +int agent_send_turn_channel_bind_request(juice_agent_t *agent, agent_stun_entry_t *entry, + const addr_record_t *record, int ds, + uint16_t *out_channel); // out_channel may be NULL +int agent_process_turn_data(juice_agent_t *agent, const stun_message_t *msg, + agent_stun_entry_t *entry); +int agent_process_channel_data(juice_agent_t *agent, agent_stun_entry_t *entry, char *buf, + size_t len); + +int agent_add_local_relayed_candidate(juice_agent_t *agent, const addr_record_t *record); +int agent_add_local_reflexive_candidate(juice_agent_t *agent, ice_candidate_type_t type, + const addr_record_t *record); +int agent_add_remote_reflexive_candidate(juice_agent_t *agent, ice_candidate_type_t type, + uint32_t priority, const addr_record_t *record); +int agent_add_candidate_pair(juice_agent_t *agent, ice_candidate_t *local, + ice_candidate_t *remote); // local may be NULL +int agent_add_candidate_pairs_for_remote(juice_agent_t *agent, ice_candidate_t *remote); +int agent_unfreeze_candidate_pair(juice_agent_t *agent, ice_candidate_pair_t *pair); + +void agent_arm_keepalive(juice_agent_t *agent, agent_stun_entry_t *entry); +void agent_arm_transmission(juice_agent_t *agent, agent_stun_entry_t *entry, timediff_t delay); +void agent_update_gathering_done(juice_agent_t *agent); +void agent_update_candidate_pairs(juice_agent_t *agent); +void agent_update_ordered_pairs(juice_agent_t *agent); + +agent_stun_entry_t *agent_find_entry_from_transaction_id(juice_agent_t *agent, + const uint8_t *transaction_id); +agent_stun_entry_t * +agent_find_entry_from_record(juice_agent_t *agent, const addr_record_t *record, + const addr_record_t *relayed); // relayed may be NULL +void agent_translate_host_candidate_entry(juice_agent_t *agent, agent_stun_entry_t *entry); + +#endif diff --git a/thirdparty/libjuice/src/base64.c b/thirdparty/libjuice/src/base64.c new file mode 100644 index 0000000..206cc6a --- /dev/null +++ b/thirdparty/libjuice/src/base64.c @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2021 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "base64.h" + +#include +#include + +int juice_base64_encode(const void *data, size_t size, char *out, size_t out_size) { + static const char tab[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + if (out_size < 4 * ((size + 2) / 3) + 1) + return -1; + + const uint8_t *in = (const uint8_t *)data; + char *w = out; + while (size >= 3) { + *w++ = tab[*in >> 2]; + *w++ = tab[((*in & 0x03) << 4) | (*(in + 1) >> 4)]; + *w++ = tab[((*(in + 1) & 0x0F) << 2) | (*(in + 2) >> 6)]; + *w++ = tab[*(in + 2) & 0x3F]; + in += 3; + size -= 3; + } + + if (size) { + *w++ = tab[*in >> 2]; + if (size == 1) { + *w++ = tab[(*in & 0x03) << 4]; + *w++ = '='; + } else { // size == 2 + *w++ = tab[((*in & 0x03) << 4) | (*(in + 1) >> 4)]; + *w++ = tab[(*(in + 1) & 0x0F) << 2]; + } + *w++ = '='; + } + + *w = '\0'; + return (int)(w - out); +} + +int juice_base64_decode(const char *str, void *out, size_t out_size) { + const uint8_t *in = (const uint8_t *)str; + uint8_t *w = (uint8_t *)out; + while (*in && *in != '=') { + uint8_t tab[4] = {0, 0, 0, 0}; + size_t size = 0; + while (*in && size < 4) { + uint8_t c = *in++; + if (isspace(c)) + continue; + if (c == '=') + break; + + if ('A' <= c && c <= 'Z') + tab[size++] = c - 'A'; + else if ('a' <= c && c <= 'z') + tab[size++] = c + 26 - 'a'; + else if ('0' <= c && c <= '9') + tab[size++] = c + 52 - '0'; + else if (c == '+' || c == '-') + tab[size++] = 62; + else if (c == '/' || c == '_') + tab[size++] = 63; + else + return -1; // Invalid character + } + + if (size > 0) { + if (out_size < size - 1) + return -1; + + out_size -= size - 1; + + *w++ = (tab[0] << 2) | (tab[1] >> 4); + if (size > 1) { + *w++ = (tab[1] << 4) | (tab[2] >> 2); + if (size > 2) + *w++ = (tab[2] << 6) | tab[3]; + } + } + } + + return (int)(w - (uint8_t *)out); +} diff --git a/thirdparty/libjuice/src/base64.h b/thirdparty/libjuice/src/base64.h new file mode 100644 index 0000000..21ebf20 --- /dev/null +++ b/thirdparty/libjuice/src/base64.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_BASE64_H +#define JUICE_BASE64_H + +#include "juice.h" + +#include +#include + +// RFC4648-compliant base64 encoder and decoder +JUICE_EXPORT int juice_base64_encode(const void *data, size_t size, char *out, size_t out_size); +JUICE_EXPORT int juice_base64_decode(const char *str, void *out, size_t out_size); + +#define BASE64_ENCODE(data, size, out, out_size) juice_base64_encode(data, size, out, out_size) +#define BASE64_DECODE(str, out, out_size) juice_base64_decode(str, out, out_size) + +#endif // JUICE_BASE64_H diff --git a/thirdparty/libjuice/src/conn.c b/thirdparty/libjuice/src/conn.c new file mode 100644 index 0000000..4d7893a --- /dev/null +++ b/thirdparty/libjuice/src/conn.c @@ -0,0 +1,249 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "conn.h" +#include "agent.h" +#include "conn_mux.h" +#include "conn_poll.h" +#include "conn_thread.h" +#include "log.h" + +#include +#include + +#define INITIAL_REGISTRY_SIZE 16 + +typedef struct conn_mode_entry { + int (*registry_init_func)(conn_registry_t *registry, udp_socket_config_t *config); + void (*registry_cleanup_func)(conn_registry_t *registry); + + int (*init_func)(juice_agent_t *agent, struct conn_registry *registry, + udp_socket_config_t *config); + void (*cleanup_func)(juice_agent_t *agent); + void (*lock_func)(juice_agent_t *agent); + void (*unlock_func)(juice_agent_t *agent); + int (*interrupt_func)(juice_agent_t *agent); + int (*send_func)(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); + int (*get_addrs_func)(juice_agent_t *agent, addr_record_t *records, size_t size); + + mutex_t mutex; + conn_registry_t *registry; +} conn_mode_entry_t; + +#define MODE_ENTRIES_SIZE 3 + +static conn_mode_entry_t mode_entries[MODE_ENTRIES_SIZE] = { + {conn_poll_registry_init, conn_poll_registry_cleanup, conn_poll_init, conn_poll_cleanup, + conn_poll_lock, conn_poll_unlock, conn_poll_interrupt, conn_poll_send, conn_poll_get_addrs, + MUTEX_INITIALIZER, NULL}, + {conn_mux_registry_init, conn_mux_registry_cleanup, conn_mux_init, conn_mux_cleanup, + conn_mux_lock, conn_mux_unlock, conn_mux_interrupt, conn_mux_send, conn_mux_get_addrs, + MUTEX_INITIALIZER, NULL}, + {NULL, NULL, conn_thread_init, conn_thread_cleanup, conn_thread_lock, conn_thread_unlock, + conn_thread_interrupt, conn_thread_send, conn_thread_get_addrs, MUTEX_INITIALIZER, NULL}}; + +static conn_mode_entry_t *get_mode_entry(juice_agent_t *agent) { + juice_concurrency_mode_t mode = agent->config.concurrency_mode; + assert(mode >= 0 && mode < MODE_ENTRIES_SIZE); + return mode_entries + (int)mode; +} + +static conn_registry_t *acquire_registry(conn_mode_entry_t *entry, udp_socket_config_t *config) { + // entry must be locked + conn_registry_t *registry = entry->registry; + if (!registry) { + if (!entry->registry_init_func) + return NULL; + + JLOG_DEBUG("Creating connections registry"); + + registry = calloc(1, sizeof(conn_registry_t)); + if (!registry) { + JLOG_FATAL("Memory allocation failed for connections registry"); + return NULL; + } + + registry->agents = malloc(INITIAL_REGISTRY_SIZE * sizeof(juice_agent_t *)); + if (!registry->agents) { + JLOG_FATAL("Memory allocation failed for connections array"); + free(registry); + return NULL; + } + + registry->agents_size = INITIAL_REGISTRY_SIZE; + registry->agents_count = 0; + memset(registry->agents, 0, INITIAL_REGISTRY_SIZE * sizeof(juice_agent_t *)); + + mutex_init(®istry->mutex, MUTEX_RECURSIVE); + mutex_lock(®istry->mutex); + + if (entry->registry_init_func(registry, config)) { + mutex_unlock(®istry->mutex); + free(registry->agents); + free(registry); + return NULL; + } + + entry->registry = registry; + + } else { + mutex_lock(®istry->mutex); + } + + // registry is locked + return registry; +} + +static void release_registry(conn_mode_entry_t *entry) { + // entry must be locked + conn_registry_t *registry = entry->registry; + if (!registry) + return; + + // registry must be locked + + if (registry->agents_count == 0) { + JLOG_DEBUG("No connection left, destroying connections registry"); + mutex_unlock(®istry->mutex); + + if (entry->registry_cleanup_func) + entry->registry_cleanup_func(registry); + + free(registry->agents); + free(registry); + entry->registry = NULL; + return; + } + + JLOG_VERBOSE("%d connection%s left", registry->agents_count, + registry->agents_count >= 2 ? "s" : ""); + + mutex_unlock(®istry->mutex); +} + +int conn_create(juice_agent_t *agent, udp_socket_config_t *config) { + conn_mode_entry_t *entry = get_mode_entry(agent); + mutex_lock(&entry->mutex); + conn_registry_t *registry = acquire_registry(entry, config); // locks the registry if created + mutex_unlock(&entry->mutex); + + JLOG_DEBUG("Creating connection"); + if (registry) { + int i = 0; + while (i < registry->agents_size && registry->agents[i]) + ++i; + + if (i == registry->agents_size) { + int new_size = registry->agents_size * 2; + JLOG_DEBUG("Reallocating connections array, new_size=%d", new_size); + assert(new_size > 0); + + juice_agent_t **new_agents = + realloc(registry->agents, new_size * sizeof(juice_agent_t *)); + if (!new_agents) { + JLOG_FATAL("Memory reallocation failed for connections array"); + mutex_unlock(®istry->mutex); + return -1; + } + + registry->agents = new_agents; + registry->agents_size = new_size; + memset(registry->agents + i, 0, (new_size - i) * sizeof(juice_agent_t *)); + } + + if (get_mode_entry(agent)->init_func(agent, registry, config)) { + mutex_unlock(®istry->mutex); + return -1; + } + + registry->agents[i] = agent; + agent->conn_index = i; + ++registry->agents_count; + + mutex_unlock(®istry->mutex); + + } else { + if (get_mode_entry(agent)->init_func(agent, NULL, config)) { + mutex_unlock(®istry->mutex); + return -1; + } + + agent->conn_index = -1; + } + + conn_interrupt(agent); + return 0; +} + +void conn_destroy(juice_agent_t *agent) { + conn_mode_entry_t *entry = get_mode_entry(agent); + mutex_lock(&entry->mutex); + + JLOG_DEBUG("Destroying connection"); + conn_registry_t *registry = entry->registry; + if (registry) { + mutex_lock(®istry->mutex); + + entry->cleanup_func(agent); + + if (agent->conn_index >= 0) { + int i = agent->conn_index; + assert(registry->agents[i] == agent); + registry->agents[i] = NULL; + agent->conn_index = -1; + } + + assert(registry->agents_count > 0); + --registry->agents_count; + + release_registry(entry); // unlocks the registry + + } else { + entry->cleanup_func(agent); + assert(agent->conn_index < 0); + } + + mutex_unlock(&entry->mutex); +} + +void conn_lock(juice_agent_t *agent) { + if (!agent->conn_impl) + return; + + get_mode_entry(agent)->lock_func(agent); +} + +void conn_unlock(juice_agent_t *agent) { + if (!agent->conn_impl) + return; + + get_mode_entry(agent)->unlock_func(agent); +} + +int conn_interrupt(juice_agent_t *agent) { + if (!agent->conn_impl) + return -1; + + return get_mode_entry(agent)->interrupt_func(agent); +} + +int conn_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds) { + if (!agent->conn_impl) + return -1; + + return get_mode_entry(agent)->send_func(agent, dst, data, size, ds); +} + +int conn_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size) { + if (!agent->conn_impl) + return -1; + + return get_mode_entry(agent)->get_addrs_func(agent, records, size); +} diff --git a/thirdparty/libjuice/src/conn.h b/thirdparty/libjuice/src/conn.h new file mode 100644 index 0000000..4aec7d0 --- /dev/null +++ b/thirdparty/libjuice/src/conn.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CONN_H +#define JUICE_CONN_H + +#include "addr.h" +#include "juice.h" +#include "thread.h" +#include "timestamp.h" +#include "udp.h" + +#include +#include + +typedef struct juice_agent juice_agent_t; + +// Generic connection interface for agents +// This interface abstracts sockets and polling to allow for different concurrency modes. +// See include/juice/juice.h for implemented concurrency modes + +typedef struct conn_registry { + void *impl; + mutex_t mutex; + juice_agent_t **agents; + int agents_size; + int agents_count; +} conn_registry_t; + +int conn_create(juice_agent_t *agent, udp_socket_config_t *config); +void conn_destroy(juice_agent_t *agent); +void conn_lock(juice_agent_t *agent); +void conn_unlock(juice_agent_t *agent); +int conn_interrupt(juice_agent_t *agent); +int conn_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); +int conn_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size); + +#endif diff --git a/thirdparty/libjuice/src/conn_mux.c b/thirdparty/libjuice/src/conn_mux.c new file mode 100644 index 0000000..56101ab --- /dev/null +++ b/thirdparty/libjuice/src/conn_mux.c @@ -0,0 +1,540 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "conn_mux.h" +#include "agent.h" +#include "log.h" +#include "socket.h" +#include "stun.h" +#include "thread.h" +#include "udp.h" + +#include +#include + +#define BUFFER_SIZE 4096 +#define INITIAL_MAP_SIZE 16 + +typedef enum map_entry_type { + MAP_ENTRY_TYPE_EMPTY = 0, + MAP_ENTRY_TYPE_DELETED, + MAP_ENTRY_TYPE_FULL +} map_entry_type_t; + +typedef struct map_entry { + map_entry_type_t type; + juice_agent_t *agent; + addr_record_t record; +} map_entry_t; + +typedef struct registry_impl { + thread_t thread; + socket_t sock; + mutex_t send_mutex; + int send_ds; + map_entry_t *map; + int map_size; + int map_count; +} registry_impl_t; + +typedef struct conn_impl { + conn_registry_t *registry; + timestamp_t next_timestamp; + bool finished; +} conn_impl_t; + +static bool is_ready(const juice_agent_t *agent) { + if (!agent) + return false; + + conn_impl_t *conn_impl = agent->conn_impl; + if (!conn_impl || conn_impl->finished) + return false; + + return true; +} + +static map_entry_t *find_map_entry(registry_impl_t *impl, const addr_record_t *record, + bool allow_deleted); +static int insert_map_entry(registry_impl_t *impl, const addr_record_t *record, + juice_agent_t *agent); +static int remove_map_entries(registry_impl_t *impl, juice_agent_t *agent); +static int grow_map(registry_impl_t *impl, int new_size); + +static map_entry_t *find_map_entry(registry_impl_t *impl, const addr_record_t *record, + bool allow_deleted) { + unsigned long key = addr_record_hash(record, false) % impl->map_size; + unsigned long pos = key; + while (true) { + map_entry_t *entry = impl->map + pos; + if (entry->type == MAP_ENTRY_TYPE_EMPTY || + addr_record_is_equal(&entry->record, record, true)) // compare ports + break; + + if (entry->type == MAP_ENTRY_TYPE_DELETED && allow_deleted) + break; + + pos = (pos + 1) % impl->map_size; + if (pos == key) + return NULL; + } + return impl->map + pos; +} + +static int insert_map_entry(registry_impl_t *impl, const addr_record_t *record, + juice_agent_t *agent) { + + map_entry_t *entry = find_map_entry(impl, record, true); // allow deleted + if (!entry || (entry->type != MAP_ENTRY_TYPE_FULL && impl->map_count * 2 >= impl->map_size)) { + grow_map(impl, impl->map_size * 2); + return insert_map_entry(impl, record, agent); + } + + if (entry->type != MAP_ENTRY_TYPE_FULL) + ++impl->map_count; + + entry->type = MAP_ENTRY_TYPE_FULL; + entry->agent = agent; + entry->record = *record; + + JLOG_VERBOSE("Added map entry, count=%d", impl->map_count); + return 0; +} + +static int remove_map_entries(registry_impl_t *impl, juice_agent_t *agent) { + int count = 0; + for (int i = 0; i < impl->map_size; ++i) { + map_entry_t *entry = impl->map + i; + if (entry->type == MAP_ENTRY_TYPE_FULL && entry->agent == agent) { + entry->type = MAP_ENTRY_TYPE_DELETED; + entry->agent = NULL; + ++count; + } + } + + assert(impl->map_count >= count); + impl->map_count -= count; + + JLOG_VERBOSE("Removed %d map entries, count=%d", count, impl->map_count); + return 0; +} + +static int grow_map(registry_impl_t *impl, int new_size) { + if (new_size <= impl->map_size) + return 0; + + JLOG_DEBUG("Growing map, new_size=%d", new_size); + + map_entry_t *new_map = calloc(1, new_size * sizeof(map_entry_t)); + if (!new_map) { + JLOG_FATAL("Memory allocation failed for map"); + return -1; + } + + map_entry_t *old_map = impl->map; + int old_size = impl->map_size; + impl->map = new_map; + impl->map_size = new_size; + impl->map_count = 0; + + for (int i = 0; i < old_size; ++i) { + map_entry_t *old_entry = old_map + i; + if (old_entry->type == MAP_ENTRY_TYPE_FULL) + insert_map_entry(impl, &old_entry->record, old_entry->agent); + } + + free(old_map); + return 0; +} + +int conn_mux_prepare(conn_registry_t *registry, struct pollfd *pfd, timestamp_t *next_timestamp); +int conn_mux_process(conn_registry_t *registry, struct pollfd *pfd); +int conn_mux_recv(conn_registry_t *registry, char *buffer, size_t size, addr_record_t *src); +void conn_mux_fail(conn_registry_t *registry); +int conn_mux_run(conn_registry_t *registry); + +static thread_return_t THREAD_CALL conn_mux_entry(void *arg) { + conn_registry_t *registry = (conn_registry_t *)arg; + conn_mux_run(registry); + return (thread_return_t)0; +} + +int conn_mux_registry_init(conn_registry_t *registry, udp_socket_config_t *config) { + (void)config; + registry_impl_t *registry_impl = calloc(1, sizeof(registry_impl_t)); + if (!registry_impl) { + JLOG_FATAL("Memory allocation failed for connections registry impl"); + return -1; + } + + registry_impl->map = calloc(INITIAL_MAP_SIZE, sizeof(map_entry_t)); + if (!registry_impl->map) { + JLOG_FATAL("Memory allocation failed for map"); + free(registry_impl); + return -1; + } + registry_impl->map_size = INITIAL_MAP_SIZE; + registry_impl->map_count = 0; + + registry_impl->sock = udp_create_socket(config); + if (registry_impl->sock == INVALID_SOCKET) { + JLOG_FATAL("UDP socket creation failed"); + free(registry_impl->map); + free(registry_impl); + return -1; + } + + mutex_init(®istry_impl->send_mutex, 0); + registry->impl = registry_impl; + + JLOG_DEBUG("Starting connections thread"); + int ret = thread_init(®istry_impl->thread, conn_mux_entry, registry); + if (ret) { + JLOG_FATAL("Thread creation failed, error=%d", ret); + goto error; + } + + return 0; + +error: + mutex_destroy(®istry_impl->send_mutex); + closesocket(registry_impl->sock); + free(registry_impl->map); + free(registry_impl); + registry->impl = NULL; + return -1; +} + +void conn_mux_registry_cleanup(conn_registry_t *registry) { + registry_impl_t *registry_impl = registry->impl; + + JLOG_VERBOSE("Waiting for connections thread"); + thread_join(registry_impl->thread, NULL); + + mutex_destroy(®istry_impl->send_mutex); + closesocket(registry_impl->sock); + free(registry_impl->map); + free(registry->impl); + registry->impl = NULL; +} + +int conn_mux_prepare(conn_registry_t *registry, struct pollfd *pfd, timestamp_t *next_timestamp) { + timestamp_t now = current_timestamp(); + *next_timestamp = now + 60000; + + mutex_lock(®istry->mutex); + registry_impl_t *registry_impl = registry->impl; + pfd->fd = registry_impl->sock; + pfd->events = POLLIN; + + for (int i = 0; i < registry->agents_size; ++i) { + juice_agent_t *agent = registry->agents[i]; + if (is_ready(agent)) { + conn_impl_t *conn_impl = agent->conn_impl; + if (*next_timestamp > conn_impl->next_timestamp) + *next_timestamp = conn_impl->next_timestamp; + } + } + + int count = registry->agents_count; + mutex_unlock(®istry->mutex); + return count; +} + +static juice_agent_t *lookup_agent(conn_registry_t *registry, char *buf, size_t len, + const addr_record_t *src) { + JLOG_VERBOSE("Looking up agent from address"); + + registry_impl_t *registry_impl = registry->impl; + map_entry_t *entry = find_map_entry(registry_impl, src, false); + juice_agent_t *agent = entry && entry->type == MAP_ENTRY_TYPE_FULL ? entry->agent : NULL; + if (agent) { + JLOG_DEBUG("Found agent from address"); + return agent; + } + + if (!is_stun_datagram(buf, len)) { + JLOG_INFO("Got non-STUN message from unknown source address"); + return NULL; + } + + JLOG_VERBOSE("Looking up agent from STUN message content"); + + stun_message_t msg; + if (stun_read(buf, len, &msg) < 0) { + JLOG_ERROR("STUN message reading failed"); + return NULL; + } + + if (msg.msg_class == STUN_CLASS_REQUEST && msg.msg_method == STUN_METHOD_BINDING && + msg.has_integrity) { + // Binding request from peer + char username[STUN_MAX_USERNAME_LEN]; + strcpy(username, msg.credentials.username); + char *separator = strchr(username, ':'); + if (!separator) { + JLOG_WARN("STUN username invalid, username=\"%s\"", username); + return NULL; + } + *separator = '\0'; + const char *local_ufrag = username; + for (int i = 0; i < registry->agents_size; ++i) { + agent = registry->agents[i]; + if (is_ready(agent)) { + if (strcmp(local_ufrag, agent->local.ice_ufrag) == 0) { + JLOG_DEBUG("Found agent from ICE ufrag"); + insert_map_entry(registry_impl, src, agent); + return agent; + } + } + } + + } else { + if (!STUN_IS_RESPONSE(msg.msg_class)) { + JLOG_INFO("Got unexpected STUN message from unknown source address"); + return NULL; + } + + for (int i = 0; i < registry->agents_size; ++i) { + agent = registry->agents[i]; + if (is_ready(agent)) { + if (agent_find_entry_from_transaction_id(agent, msg.transaction_id)) { + JLOG_DEBUG("Found agent from transaction ID"); + return agent; + } + } + } + } + + return NULL; +} + +int conn_mux_process(conn_registry_t *registry, struct pollfd *pfd) { + mutex_lock(®istry->mutex); + + if (pfd->revents & POLLNVAL || pfd->revents & POLLERR) { + JLOG_ERROR("Error when polling socket"); + conn_mux_fail(registry); + mutex_unlock(®istry->mutex); + return -1; + } + + if (pfd->revents & POLLIN) { + char buffer[BUFFER_SIZE]; + addr_record_t src; + int ret; + while ((ret = conn_mux_recv(registry, buffer, BUFFER_SIZE, &src)) > 0) { + if (JLOG_DEBUG_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(&src, src_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Demultiplexing incoming datagram from %s", src_str); + } + + juice_agent_t *agent = lookup_agent(registry, buffer, (size_t)ret, &src); + if (!agent || !is_ready(agent)) { + JLOG_DEBUG("Agent not found for incoming datagram, dropping"); + continue; + } + + conn_impl_t *conn_impl = agent->conn_impl; + if (agent_conn_recv(agent, buffer, (size_t)ret, &src) != 0) { + JLOG_WARN("Agent receive failed"); + conn_impl->finished = true; + continue; + } + + conn_impl->next_timestamp = current_timestamp(); + } + + if (ret < 0) { + conn_mux_fail(registry); + mutex_unlock(®istry->mutex); + return -1; + } + } + + for (int i = 0; i < registry->agents_size; ++i) { + juice_agent_t *agent = registry->agents[i]; + if (is_ready(agent)) { + conn_impl_t *conn_impl = agent->conn_impl; + if (conn_impl->next_timestamp <= current_timestamp()) { + if (agent_conn_update(agent, &conn_impl->next_timestamp) != 0) { + JLOG_WARN("Agent update failed"); + conn_impl->finished = true; + continue; + } + } + } + } + + mutex_unlock(®istry->mutex); + return 0; +} + +int conn_mux_recv(conn_registry_t *registry, char *buffer, size_t size, addr_record_t *src) { + JLOG_VERBOSE("Receiving datagram"); + registry_impl_t *registry_impl = registry->impl; + int len; + while ((len = udp_recvfrom(registry_impl->sock, buffer, size, src)) == 0) { + // Empty datagram (used to interrupt) + } + + if (len < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) { + JLOG_VERBOSE("No more datagrams to receive"); + return 0; + } + JLOG_ERROR("recvfrom failed, errno=%d", sockerrno); + return -1; + } + + addr_unmap_inet6_v4mapped((struct sockaddr *)&src->addr, &src->len); + return len; // len > 0 +} + +void conn_mux_fail(conn_registry_t *registry) { + for (int i = 0; i < registry->agents_size; ++i) { + juice_agent_t *agent = registry->agents[i]; + if (is_ready(agent)) { + conn_impl_t *conn_impl = agent->conn_impl; + agent_conn_fail(agent); + conn_impl->finished = true; + } + } +} + +int conn_mux_run(conn_registry_t *registry) { + struct pollfd pfd[1]; + timestamp_t next_timestamp; + while (conn_mux_prepare(registry, pfd, &next_timestamp) > 0) { + timediff_t timediff = next_timestamp - current_timestamp(); + if (timediff < 0) + timediff = 0; + + JLOG_VERBOSE("Entering poll for %d ms", (int)timediff); + int ret = poll(pfd, 1, (int)timediff); + JLOG_VERBOSE("Leaving poll"); + if (ret < 0) { + if (sockerrno == SEINTR || sockerrno == SEAGAIN) { + JLOG_VERBOSE("poll interrupted"); + continue; + } else { + JLOG_FATAL("poll failed, errno=%d", sockerrno); + break; + } + } + + if (conn_mux_process(registry, pfd) < 0) + break; + } + + JLOG_DEBUG("Leaving connections thread"); + return 0; +} + +int conn_mux_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config) { + (void)config; // ignored, only the config from the first connection is used + + conn_impl_t *conn_impl = calloc(1, sizeof(conn_impl_t)); + if (!conn_impl) { + JLOG_FATAL("Memory allocation failed for connection impl"); + return -1; + } + + conn_impl->registry = registry; + agent->conn_impl = conn_impl; + return 0; +} + +void conn_mux_cleanup(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + + mutex_lock(®istry->mutex); + registry_impl_t *registry_impl = registry->impl; + remove_map_entries(registry_impl, agent); + mutex_unlock(®istry->mutex); + + conn_mux_interrupt(agent); + + free(agent->conn_impl); + agent->conn_impl = NULL; +} + +void conn_mux_lock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + mutex_lock(®istry->mutex); +} + +void conn_mux_unlock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + mutex_unlock(®istry->mutex); +} + +int conn_mux_interrupt(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + + mutex_lock(®istry->mutex); + conn_impl->next_timestamp = current_timestamp(); + mutex_unlock(®istry->mutex); + + JLOG_VERBOSE("Interrupting connections thread"); + + registry_impl_t *registry_impl = registry->impl; + mutex_lock(®istry_impl->send_mutex); + if (udp_sendto_self(registry_impl->sock, NULL, 0) < 0) { + if (sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) { + JLOG_WARN("Failed to interrupt poll by triggering socket, errno=%d", sockerrno); + } + mutex_unlock(®istry_impl->send_mutex); + return -1; + } + mutex_unlock(®istry_impl->send_mutex); + return 0; +} + +int conn_mux_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds) { + conn_impl_t *conn_impl = agent->conn_impl; + registry_impl_t *registry_impl = conn_impl->registry->impl; + + mutex_lock(®istry_impl->send_mutex); + + if (registry_impl->send_ds >= 0 && registry_impl->send_ds != ds) { + JLOG_VERBOSE("Setting Differentiated Services field to 0x%X", ds); + if (udp_set_diffserv(registry_impl->sock, ds) == 0) + registry_impl->send_ds = ds; + else + registry_impl->send_ds = -1; // disable for next time + } + + JLOG_VERBOSE("Sending datagram, size=%d", size); + + int ret = udp_sendto(registry_impl->sock, data, size, dst); + if (ret < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) + JLOG_INFO("Send failed, buffer is full"); + else if (sockerrno == SEMSGSIZE) + JLOG_WARN("Send failed, datagram is too large"); + else + JLOG_WARN("Send failed, errno=%d", sockerrno); + } + + mutex_unlock(®istry_impl->send_mutex); + return ret; +} + +int conn_mux_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size) { + conn_impl_t *conn_impl = agent->conn_impl; + registry_impl_t *registry_impl = conn_impl->registry->impl; + + return udp_get_addrs(registry_impl->sock, records, size); +} diff --git a/thirdparty/libjuice/src/conn_mux.h b/thirdparty/libjuice/src/conn_mux.h new file mode 100644 index 0000000..9519f06 --- /dev/null +++ b/thirdparty/libjuice/src/conn_mux.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CONN_MUX_H +#define JUICE_CONN_MUX_H + +#include "addr.h" +#include "conn.h" +#include "thread.h" +#include "timestamp.h" + +#include +#include + +int conn_mux_registry_init(conn_registry_t *registry, udp_socket_config_t *config); +void conn_mux_registry_cleanup(conn_registry_t *registry); + +int conn_mux_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config); +void conn_mux_cleanup(juice_agent_t *agent); +void conn_mux_lock(juice_agent_t *agent); +void conn_mux_unlock(juice_agent_t *agent); +int conn_mux_interrupt(juice_agent_t *agent); +int conn_mux_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); +int conn_mux_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size); + +#endif diff --git a/thirdparty/libjuice/src/conn_poll.c b/thirdparty/libjuice/src/conn_poll.c new file mode 100644 index 0000000..2f5c3ec --- /dev/null +++ b/thirdparty/libjuice/src/conn_poll.c @@ -0,0 +1,433 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "conn_poll.h" +#include "agent.h" +#include "log.h" +#include "socket.h" +#include "thread.h" +#include "udp.h" + +#include +#include + +#define BUFFER_SIZE 4096 + +typedef struct registry_impl { + thread_t thread; +#ifdef _WIN32 + socket_t interrupt_sock; +#else + int interrupt_pipe_out; + int interrupt_pipe_in; +#endif +} registry_impl_t; + +typedef enum conn_state { CONN_STATE_NEW = 0, CONN_STATE_READY, CONN_STATE_FINISHED } conn_state_t; + +typedef struct conn_impl { + conn_registry_t *registry; + conn_state_t state; + socket_t sock; + mutex_t send_mutex; + int send_ds; + timestamp_t next_timestamp; +} conn_impl_t; + +typedef struct pfds_record { + struct pollfd *pfds; + nfds_t size; +} pfds_record_t; + +int conn_poll_prepare(conn_registry_t *registry, pfds_record_t *pfds, timestamp_t *next_timestamp); +int conn_poll_process(conn_registry_t *registry, pfds_record_t *pfds); +int conn_poll_recv(socket_t sock, char *buffer, size_t size, addr_record_t *src); +int conn_poll_run(conn_registry_t *registry); + +static thread_return_t THREAD_CALL conn_thread_entry(void *arg) { + conn_registry_t *registry = (conn_registry_t *)arg; + conn_poll_run(registry); + return (thread_return_t)0; +} + +int conn_poll_registry_init(conn_registry_t *registry, udp_socket_config_t *config) { + (void)config; + registry_impl_t *registry_impl = calloc(1, sizeof(registry_impl_t)); + if (!registry_impl) { + JLOG_FATAL("Memory allocation failed for connections registry impl"); + return -1; + } + +#ifdef _WIN32 + udp_socket_config_t interrupt_config; + memset(&interrupt_config, 0, sizeof(interrupt_config)); + interrupt_config.bind_address = "localhost"; + registry_impl->interrupt_sock = udp_create_socket(&interrupt_config); + if (registry_impl->interrupt_sock == INVALID_SOCKET) { + JLOG_FATAL("Dummy socket creation failed"); + free(registry_impl); + return -1; + } +#else + int pipefds[2]; + if (pipe(pipefds)) { + JLOG_FATAL("Pipe creation failed"); + free(registry_impl); + return -1; + } + + fcntl(pipefds[0], F_SETFL, O_NONBLOCK); + fcntl(pipefds[1], F_SETFL, O_NONBLOCK); + registry_impl->interrupt_pipe_out = pipefds[1]; // read + registry_impl->interrupt_pipe_in = pipefds[0]; // write +#endif + + registry->impl = registry_impl; + + JLOG_DEBUG("Starting connections thread"); + int ret = thread_init(®istry_impl->thread, conn_thread_entry, registry); + if (ret) { + JLOG_FATAL("Thread creation failed, error=%d", ret); + goto error; + } + + return 0; + +error: +#ifndef _WIN32 + close(registry_impl->interrupt_pipe_out); + close(registry_impl->interrupt_pipe_in); +#endif + free(registry_impl); + registry->impl = NULL; + return -1; +} + +void conn_poll_registry_cleanup(conn_registry_t *registry) { + registry_impl_t *registry_impl = registry->impl; + + JLOG_VERBOSE("Waiting for connections thread"); + thread_join(registry_impl->thread, NULL); + +#ifdef _WIN32 + closesocket(registry_impl->interrupt_sock); +#else + close(registry_impl->interrupt_pipe_out); + close(registry_impl->interrupt_pipe_in); +#endif + free(registry->impl); + registry->impl = NULL; +} + +int conn_poll_prepare(conn_registry_t *registry, pfds_record_t *pfds, timestamp_t *next_timestamp) { + timestamp_t now = current_timestamp(); + *next_timestamp = now + 60000; + + mutex_lock(®istry->mutex); + nfds_t size = (nfds_t)(1 + registry->agents_size); + if (pfds->size != size) { + struct pollfd *new_pfds = realloc(pfds->pfds, sizeof(struct pollfd) * size); + if (!new_pfds) { + JLOG_FATAL("Memory allocation for poll file descriptors failed"); + goto error; + } + pfds->pfds = new_pfds; + pfds->size = size; + } + + registry_impl_t *registry_impl = registry->impl; + struct pollfd *interrupt_pfd = pfds->pfds; + assert(interrupt_pfd); +#ifdef _WIN32 + interrupt_pfd->fd = registry_impl->interrupt_sock; +#else + interrupt_pfd->fd = registry_impl->interrupt_pipe_in; +#endif + interrupt_pfd->events = POLLIN; + + for (nfds_t i = 1; i < pfds->size; ++i) { + struct pollfd *pfd = pfds->pfds + i; + juice_agent_t *agent = registry->agents[i - 1]; + if (!agent) { + pfd->fd = INVALID_SOCKET; + pfd->events = 0; + continue; + } + + conn_impl_t *conn_impl = agent->conn_impl; + if (!conn_impl || + (conn_impl->state != CONN_STATE_NEW && conn_impl->state != CONN_STATE_READY)) { + pfd->fd = INVALID_SOCKET; + pfd->events = 0; + continue; + } + + if (conn_impl->state == CONN_STATE_NEW) + conn_impl->state = CONN_STATE_READY; + + if (*next_timestamp > conn_impl->next_timestamp) + *next_timestamp = conn_impl->next_timestamp; + + pfd->fd = conn_impl->sock; + pfd->events = POLLIN; + } + + int count = registry->agents_count; + mutex_unlock(®istry->mutex); + return count; + +error: + mutex_unlock(®istry->mutex); + return -1; +} + +int conn_poll_recv(socket_t sock, char *buffer, size_t size, addr_record_t *src) { + JLOG_VERBOSE("Receiving datagram"); + int len; + while ((len = udp_recvfrom(sock, buffer, size, src)) == 0) { + // Empty datagram, ignore + } + + if (len < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) { + JLOG_VERBOSE("No more datagrams to receive"); + return 0; + } + JLOG_ERROR("recvfrom failed, errno=%d", sockerrno); + return -1; + } + + addr_unmap_inet6_v4mapped((struct sockaddr *)&src->addr, &src->len); + return len; // len > 0 +} + +int conn_poll_process(conn_registry_t *registry, pfds_record_t *pfds) { + struct pollfd *interrupt_pfd = pfds->pfds; + if (interrupt_pfd->revents & POLLIN) { +#ifdef _WIN32 + char dummy; + addr_record_t src; + while (udp_recvfrom(interrupt_pfd->fd, &dummy, 1, &src) >= 0) { + // Ignore + } +#else + char dummy; + while (read(interrupt_pfd->fd, &dummy, 1) > 0) { + // Ignore + } +#endif + } + + for (nfds_t i = 1; i < pfds->size; ++i) { + struct pollfd *pfd = pfds->pfds + i; + if (pfd->fd == INVALID_SOCKET) + continue; + + mutex_lock(®istry->mutex); + juice_agent_t *agent = registry->agents[i - 1]; + if (!agent) + goto end; + + conn_impl_t *conn_impl = agent->conn_impl; + if (!conn_impl || conn_impl->sock != pfd->fd || conn_impl->state != CONN_STATE_READY) + goto end; + + if (pfd->revents & POLLNVAL || pfd->revents & POLLERR) { + JLOG_WARN("Error when polling socket"); + agent_conn_fail(agent); + conn_impl->state = CONN_STATE_FINISHED; + goto end; + } + + if (pfd->revents & POLLIN) { + char buffer[BUFFER_SIZE]; + addr_record_t src; + int ret = 0; + int left = 1000; // limit for fairness between sockets + while (left-- && + (ret = conn_poll_recv(conn_impl->sock, buffer, BUFFER_SIZE, &src)) > 0) { + if (agent_conn_recv(agent, buffer, (size_t)ret, &src) != 0) { + JLOG_WARN("Agent receive failed"); + conn_impl->state = CONN_STATE_FINISHED; + break; + } + } + if (conn_impl->state == CONN_STATE_FINISHED) + goto end; + + if (ret < 0) { + agent_conn_fail(agent); + conn_impl->state = CONN_STATE_FINISHED; + goto end; + } + + if (agent_conn_update(agent, &conn_impl->next_timestamp) != 0) { + JLOG_WARN("Agent update failed"); + conn_impl->state = CONN_STATE_FINISHED; + goto end; + } + + } else if (conn_impl->next_timestamp <= current_timestamp()) { + if (agent_conn_update(agent, &conn_impl->next_timestamp) != 0) { + JLOG_WARN("Agent update failed"); + conn_impl->state = CONN_STATE_FINISHED; + goto end; + } + } + + end: + mutex_unlock(®istry->mutex); + } + + return 0; +} + +int conn_poll_run(conn_registry_t *registry) { + pfds_record_t pfds; + pfds.pfds = NULL; + pfds.size = 0; + timestamp_t next_timestamp = 0; + int count; + while ((count = conn_poll_prepare(registry, &pfds, &next_timestamp)) > 0) { + timediff_t timediff = next_timestamp - current_timestamp(); + if (timediff < 0) + timediff = 0; + + JLOG_VERBOSE("Entering poll on %d sockets for %d ms", count, (int)timediff); + int ret = poll(pfds.pfds, pfds.size, (int)timediff); + JLOG_VERBOSE("Leaving poll"); + if (ret < 0) { +#ifdef _WIN32 + if (ret == WSAENOTSOCK) + continue; // prepare again as the fd has been removed +#endif + if (sockerrno == SEINTR || sockerrno == SEAGAIN) { + JLOG_VERBOSE("poll interrupted"); + continue; + } else { + JLOG_FATAL("poll failed, errno=%d", sockerrno); + break; + } + } + + if (conn_poll_process(registry, &pfds) < 0) + break; + } + + JLOG_DEBUG("Leaving connections thread"); + free(pfds.pfds); + return 0; +} + +int conn_poll_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config) { + conn_impl_t *conn_impl = calloc(1, sizeof(conn_impl_t)); + if (!conn_impl) { + JLOG_FATAL("Memory allocation failed for connection impl"); + return -1; + } + + conn_impl->sock = udp_create_socket(config); + if (conn_impl->sock == INVALID_SOCKET) { + JLOG_ERROR("UDP socket creation failed"); + free(conn_impl); + return -1; + } + + mutex_init(&conn_impl->send_mutex, 0); + conn_impl->registry = registry; + + agent->conn_impl = conn_impl; + return 0; +} + +void conn_poll_cleanup(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + + conn_poll_interrupt(agent); + + mutex_destroy(&conn_impl->send_mutex); + closesocket(conn_impl->sock); + free(agent->conn_impl); + agent->conn_impl = NULL; +} + +void conn_poll_lock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + mutex_lock(®istry->mutex); +} + +void conn_poll_unlock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + mutex_unlock(®istry->mutex); +} + +int conn_poll_interrupt(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + conn_registry_t *registry = conn_impl->registry; + registry_impl_t *registry_impl = registry->impl; + + mutex_lock(®istry->mutex); + conn_impl->next_timestamp = current_timestamp(); + mutex_unlock(®istry->mutex); + + JLOG_VERBOSE("Interrupting connections thread"); + +#ifdef _WIN32 + if (udp_sendto_self(registry_impl->interrupt_sock, NULL, 0) < 0) { + if (sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) { + JLOG_WARN("Failed to interrupt poll by triggering socket, errno=%d", sockerrno); + } + return -1; + } +#else + char dummy = 0; + if (write(registry_impl->interrupt_pipe_out, &dummy, 1) < 0 && errno != EAGAIN && + errno != EWOULDBLOCK) { + JLOG_WARN("Failed to interrupt poll by writing to pipe, errno=%d", errno); + } +#endif + return 0; +} + +int conn_poll_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds) { + conn_impl_t *conn_impl = agent->conn_impl; + + mutex_lock(&conn_impl->send_mutex); + + if (conn_impl->send_ds >= 0 && conn_impl->send_ds != ds) { + JLOG_VERBOSE("Setting Differentiated Services field to 0x%X", ds); + if (udp_set_diffserv(conn_impl->sock, ds) == 0) + conn_impl->send_ds = ds; + else + conn_impl->send_ds = -1; // disable for next time + } + + JLOG_VERBOSE("Sending datagram, size=%d", size); + + int ret = udp_sendto(conn_impl->sock, data, size, dst); + if (ret < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) + JLOG_INFO("Send failed, buffer is full"); + else if (sockerrno == SEMSGSIZE) + JLOG_WARN("Send failed, datagram is too large"); + else + JLOG_WARN("Send failed, errno=%d", sockerrno); + } + + mutex_unlock(&conn_impl->send_mutex); + return ret; +} + +int conn_poll_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size) { + conn_impl_t *conn_impl = agent->conn_impl; + + return udp_get_addrs(conn_impl->sock, records, size); +} diff --git a/thirdparty/libjuice/src/conn_poll.h b/thirdparty/libjuice/src/conn_poll.h new file mode 100644 index 0000000..b3a162a --- /dev/null +++ b/thirdparty/libjuice/src/conn_poll.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CONN_POLL_H +#define JUICE_CONN_POLL_H + +#include "addr.h" +#include "conn.h" +#include "thread.h" +#include "timestamp.h" + +#include +#include + +int conn_poll_registry_init(conn_registry_t *registry, udp_socket_config_t *config); +void conn_poll_registry_cleanup(conn_registry_t *registry); + +int conn_poll_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config); +void conn_poll_cleanup(juice_agent_t *agent); +void conn_poll_lock(juice_agent_t *agent); +void conn_poll_unlock(juice_agent_t *agent); +int conn_poll_interrupt(juice_agent_t *agent); +int conn_poll_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); +int conn_poll_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size); + +#endif diff --git a/thirdparty/libjuice/src/conn_thread.c b/thirdparty/libjuice/src/conn_thread.c new file mode 100644 index 0000000..4da821e --- /dev/null +++ b/thirdparty/libjuice/src/conn_thread.c @@ -0,0 +1,278 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "conn_thread.h" +#include "agent.h" +#include "log.h" +#include "socket.h" +#include "thread.h" +#include "udp.h" + +#include +#include + +#define BUFFER_SIZE 4096 + +typedef struct conn_impl { + thread_t thread; + socket_t sock; + mutex_t mutex; + mutex_t send_mutex; + int send_ds; + timestamp_t next_timestamp; + bool stopped; +} conn_impl_t; + +int conn_thread_run(juice_agent_t *agent); +int conn_thread_prepare(juice_agent_t *agent, struct pollfd *pfd, timestamp_t *next_timestamp); +int conn_thread_process(juice_agent_t *agent, struct pollfd *pfd); +int conn_thread_recv(socket_t sock, char *buffer, size_t size, addr_record_t *src); + +static thread_return_t THREAD_CALL conn_thread_entry(void *arg) { + juice_agent_t *agent = (juice_agent_t *)arg; + conn_thread_run(agent); + return (thread_return_t)0; +} + +int conn_thread_prepare(juice_agent_t *agent, struct pollfd *pfd, timestamp_t *next_timestamp) { + conn_impl_t *conn_impl = agent->conn_impl; + mutex_lock(&conn_impl->mutex); + if (conn_impl->stopped) { + mutex_unlock(&conn_impl->mutex); + return 0; + } + + pfd->fd = conn_impl->sock; + pfd->events = POLLIN; + + *next_timestamp = conn_impl->next_timestamp; + + mutex_unlock(&conn_impl->mutex); + return 1; +} + +int conn_thread_process(juice_agent_t *agent, struct pollfd *pfd) { + conn_impl_t *conn_impl = agent->conn_impl; + mutex_lock(&conn_impl->mutex); + if (conn_impl->stopped) { + mutex_unlock(&conn_impl->mutex); + return -1; + } + + if (pfd->revents & POLLNVAL || pfd->revents & POLLERR) { + JLOG_ERROR("Error when polling socket"); + agent_conn_fail(agent); + mutex_unlock(&conn_impl->mutex); + return -1; + } + + if (pfd->revents & POLLIN) { + char buffer[BUFFER_SIZE]; + addr_record_t src; + int ret; + while ((ret = conn_thread_recv(conn_impl->sock, buffer, BUFFER_SIZE, &src)) > 0) { + if (agent_conn_recv(agent, buffer, (size_t)ret, &src) != 0) { + JLOG_WARN("Agent receive failed"); + mutex_unlock(&conn_impl->mutex); + return -1; + } + } + + if (ret < 0) { + agent_conn_fail(agent); + mutex_unlock(&conn_impl->mutex); + return -1; + } + + if (agent_conn_update(agent, &conn_impl->next_timestamp) != 0) { + JLOG_WARN("Agent update failed"); + mutex_unlock(&conn_impl->mutex); + return -1; + } + + } else if (conn_impl->next_timestamp <= current_timestamp()) { + if (agent_conn_update(agent, &conn_impl->next_timestamp) != 0) { + JLOG_WARN("Agent update failed"); + mutex_unlock(&conn_impl->mutex); + return -1; + } + } + + mutex_unlock(&conn_impl->mutex); + return 0; +} + +int conn_thread_recv(socket_t sock, char *buffer, size_t size, addr_record_t *src) { + JLOG_VERBOSE("Receiving datagram"); + int len; + while ((len = udp_recvfrom(sock, buffer, size, src)) == 0) { + // Empty datagram (used to interrupt) + } + + if (len < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) { + JLOG_VERBOSE("No more datagrams to receive"); + return 0; + } + JLOG_ERROR("recvfrom failed, errno=%d", sockerrno); + return -1; + } + + addr_unmap_inet6_v4mapped((struct sockaddr *)&src->addr, &src->len); + return len; // len > 0 +} + +int conn_thread_run(juice_agent_t *agent) { + struct pollfd pfd[1]; + timestamp_t next_timestamp; + while (conn_thread_prepare(agent, pfd, &next_timestamp) > 0) { + timediff_t timediff = next_timestamp - current_timestamp(); + if (timediff < 0) + timediff = 0; + + JLOG_VERBOSE("Entering poll for %d ms", (int)timediff); + int ret = poll(pfd, 1, (int)timediff); + JLOG_VERBOSE("Leaving poll"); + if (ret < 0) { + if (sockerrno == SEINTR || sockerrno == SEAGAIN) { + JLOG_VERBOSE("poll interrupted"); + continue; + } else { + JLOG_FATAL("poll failed, errno=%d", sockerrno); + break; + } + } + + if (conn_thread_process(agent, pfd) < 0) + break; + } + + JLOG_DEBUG("Leaving connection thread"); + return 0; +} + +int conn_thread_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config) { + (void)registry; + + conn_impl_t *conn_impl = calloc(1, sizeof(conn_impl_t)); + if (!conn_impl) { + JLOG_FATAL("Memory allocation failed for connection impl"); + return -1; + } + + conn_impl->sock = udp_create_socket(config); + if (conn_impl->sock == INVALID_SOCKET) { + JLOG_ERROR("UDP socket creation failed"); + free(conn_impl); + return -1; + } + + mutex_init(&conn_impl->mutex, 0); + mutex_init(&conn_impl->send_mutex, 0); + + agent->conn_impl = conn_impl; + + JLOG_DEBUG("Starting connection thread"); + int ret = thread_init(&conn_impl->thread, conn_thread_entry, agent); + if (ret) { + JLOG_FATAL("Thread creation failed, error=%d", ret); + free(conn_impl); + agent->conn_impl = NULL; + return -1; + } + + return 0; +} + +void conn_thread_cleanup(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + + mutex_lock(&conn_impl->mutex); + conn_impl->stopped = true; + mutex_unlock(&conn_impl->mutex); + + conn_thread_interrupt(agent); + + JLOG_VERBOSE("Waiting for connection thread"); + thread_join(conn_impl->thread, NULL); + + closesocket(conn_impl->sock); + mutex_destroy(&conn_impl->mutex); + mutex_destroy(&conn_impl->send_mutex); + free(agent->conn_impl); + agent->conn_impl = NULL; +} + +void conn_thread_lock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + mutex_lock(&conn_impl->mutex); +} + +void conn_thread_unlock(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + mutex_unlock(&conn_impl->mutex); +} + +int conn_thread_interrupt(juice_agent_t *agent) { + conn_impl_t *conn_impl = agent->conn_impl; + + mutex_lock(&conn_impl->mutex); + conn_impl->next_timestamp = current_timestamp(); + mutex_unlock(&conn_impl->mutex); + + JLOG_VERBOSE("Interrupting connection thread"); + + mutex_lock(&conn_impl->send_mutex); + if (udp_sendto_self(conn_impl->sock, NULL, 0) < 0) { + if (sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) { + JLOG_WARN("Failed to interrupt poll by triggering socket, errno=%d", sockerrno); + } + mutex_unlock(&conn_impl->send_mutex); + return -1; + } + + mutex_unlock(&conn_impl->send_mutex); + return 0; +} + +int conn_thread_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds) { + conn_impl_t *conn_impl = agent->conn_impl; + + mutex_lock(&conn_impl->send_mutex); + + if (conn_impl->send_ds >= 0 && conn_impl->send_ds != ds) { + JLOG_VERBOSE("Setting Differentiated Services field to 0x%X", ds); + if (udp_set_diffserv(conn_impl->sock, ds) == 0) + conn_impl->send_ds = ds; + else + conn_impl->send_ds = -1; // disable for next time + } + + JLOG_VERBOSE("Sending datagram, size=%d", size); + + int ret = udp_sendto(conn_impl->sock, data, size, dst); + if (ret < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) + JLOG_INFO("Send failed, buffer is full"); + else if (sockerrno == SEMSGSIZE) + JLOG_WARN("Send failed, datagram is too large"); + else + JLOG_WARN("Send failed, errno=%d", sockerrno); + } + + mutex_unlock(&conn_impl->send_mutex); + return ret; +} + +int conn_thread_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size) { + conn_impl_t *conn_impl = agent->conn_impl; + + return udp_get_addrs(conn_impl->sock, records, size); +} + diff --git a/thirdparty/libjuice/src/conn_thread.h b/thirdparty/libjuice/src/conn_thread.h new file mode 100644 index 0000000..ceb23a4 --- /dev/null +++ b/thirdparty/libjuice/src/conn_thread.h @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CONN_THREAD_H +#define JUICE_CONN_THREAD_H + +#include "addr.h" +#include "conn.h" +#include "thread.h" +#include "timestamp.h" + +#include +#include + +int conn_thread_registry_init(conn_registry_t *registry, udp_socket_config_t *config); +void conn_thread_registry_cleanup(conn_registry_t *registry); + +int conn_thread_init(juice_agent_t *agent, conn_registry_t *registry, udp_socket_config_t *config); +void conn_thread_cleanup(juice_agent_t *agent); +void conn_thread_lock(juice_agent_t *agent); +void conn_thread_unlock(juice_agent_t *agent); +int conn_thread_interrupt(juice_agent_t *agent); +int conn_thread_send(juice_agent_t *agent, const addr_record_t *dst, const char *data, size_t size, + int ds); +int conn_thread_get_addrs(juice_agent_t *agent, addr_record_t *records, size_t size); + +#endif diff --git a/thirdparty/libjuice/src/const_time.c b/thirdparty/libjuice/src/const_time.c new file mode 100644 index 0000000..5529ca2 --- /dev/null +++ b/thirdparty/libjuice/src/const_time.c @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "const_time.h" + +int const_time_memcmp(const void *a, const void *b, size_t len) { + const unsigned char *ca = a; + const unsigned char *cb = b; + unsigned char x = 0; + for (size_t i = 0; i < len; i++) + x |= ca[i] ^ cb[i]; + + return x; +} + +int const_time_strcmp(const void *a, const void *b) { + const unsigned char *ca = a; + const unsigned char *cb = b; + unsigned char x = 0; + size_t i = 0; + for(;;) { + x |= ca[i] ^ cb[i]; + if (!ca[i] || !cb[i]) + break; + ++i; + } + + return x; +} diff --git a/thirdparty/libjuice/src/const_time.h b/thirdparty/libjuice/src/const_time.h new file mode 100644 index 0000000..d654a73 --- /dev/null +++ b/thirdparty/libjuice/src/const_time.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) 2021 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CONST_TIME_H +#define JUICE_CONST_TIME_H + +#include +#include + +int const_time_memcmp(const void *a, const void *b, size_t len); +int const_time_strcmp(const void *a, const void *b); + +#endif diff --git a/thirdparty/libjuice/src/crc32.c b/thirdparty/libjuice/src/crc32.c new file mode 100644 index 0000000..61cff0a --- /dev/null +++ b/thirdparty/libjuice/src/crc32.c @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "crc32.h" + +#define CRC32_REVERSED_POLY 0xEDB88320 +#define CRC32_INIT 0xFFFFFFFF +#define CRC32_XOR 0xFFFFFFFF + +static uint32_t crc32_byte(uint32_t crc) { + for (int i = 0; i < 8; ++i) + if (crc & 1) + crc = (crc >> 1) ^ CRC32_REVERSED_POLY; + else + crc = (crc >> 1); + return crc; +} + +static uint32_t crc32_table(const uint8_t *p, size_t size, uint32_t *table) { + uint32_t crc = CRC32_INIT; + while (size--) + crc = table[(uint8_t)(crc & 0xFF) ^ *p++] ^ (crc >> 8); + return crc ^ CRC32_XOR; +} + +JUICE_EXPORT uint32_t juice_crc32(const void *data, size_t size) { + static uint32_t table[256] = {0}; + if (table[0] == 0) + for (uint32_t i = 0; i < 256; ++i) + table[i] = crc32_byte(i); + + return crc32_table(data, size, table); +} diff --git a/thirdparty/libjuice/src/crc32.h b/thirdparty/libjuice/src/crc32.h new file mode 100644 index 0000000..9567209 --- /dev/null +++ b/thirdparty/libjuice/src/crc32.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_CRC32_H +#define JUICE_CRC32_H + +#include "juice.h" + +#include +#include + +JUICE_EXPORT uint32_t juice_crc32(const void *data, size_t size); + +#define CRC32(data, size) juice_crc32(data, size) + +#endif // JUICE_CRC32_H diff --git a/thirdparty/libjuice/src/hash.c b/thirdparty/libjuice/src/hash.c new file mode 100644 index 0000000..3f969e6 --- /dev/null +++ b/thirdparty/libjuice/src/hash.c @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "hash.h" + +#if USE_NETTLE +#include +#include +#include +#else +#include "picohash.h" +#endif + +void hash_md5(const void *message, size_t size, void *digest) { +#if USE_NETTLE + struct md5_ctx ctx; + md5_init(&ctx); + md5_update(&ctx, size, message); + md5_digest(&ctx, HASH_MD5_SIZE, digest); +#else + picohash_ctx_t ctx; + picohash_init_md5(&ctx); + picohash_update(&ctx, message, size); + picohash_final(&ctx, digest); +#endif +} + +void hash_sha1(const void *message, size_t size, void *digest) { +#if USE_NETTLE + struct sha1_ctx ctx; + sha1_init(&ctx); + sha1_update(&ctx, size, message); + sha1_digest(&ctx, HASH_SHA1_SIZE, digest); +#else + picohash_ctx_t ctx; + picohash_init_sha1(&ctx); + picohash_update(&ctx, message, size); + picohash_final(&ctx, digest); +#endif +} + +void hash_sha256(const void *message, size_t size, void *digest) { +#if USE_NETTLE + struct sha256_ctx ctx; + sha256_init(&ctx); + sha256_update(&ctx, size, message); + sha256_digest(&ctx, HASH_SHA256_SIZE, digest); +#else + picohash_ctx_t ctx; + picohash_init_sha256(&ctx); + picohash_update(&ctx, message, size); + picohash_final(&ctx, digest); +#endif +} diff --git a/thirdparty/libjuice/src/hash.h b/thirdparty/libjuice/src/hash.h new file mode 100644 index 0000000..31d392b --- /dev/null +++ b/thirdparty/libjuice/src/hash.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_HASH_H +#define JUICE_HASH_H + +#include +#include + +#define HASH_MD5_SIZE 16 +#define HASH_SHA1_SIZE 24 +#define HASH_SHA256_SIZE 32 + +void hash_md5(const void *message, size_t size, void *digest); +void hash_sha1(const void *message, size_t size, void *digest); +void hash_sha256(const void *message, size_t size, void *digest); + +#endif diff --git a/thirdparty/libjuice/src/hmac.c b/thirdparty/libjuice/src/hmac.c new file mode 100644 index 0000000..179b02f --- /dev/null +++ b/thirdparty/libjuice/src/hmac.c @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "hmac.h" + +#if USE_NETTLE +#include +#else +#include "picohash.h" +#endif + +void hmac_sha1(const void *message, size_t size, const void *key, size_t key_size, void *digest) { +#if USE_NETTLE + struct hmac_sha1_ctx ctx; + hmac_sha1_set_key(&ctx, key_size, key); + hmac_sha1_update(&ctx, size, message); + hmac_sha1_digest(&ctx, HMAC_SHA1_SIZE, digest); +#else + picohash_ctx_t ctx; + picohash_init_hmac(&ctx, picohash_init_sha1, key, key_size); + picohash_update(&ctx, message, size); + picohash_final(&ctx, digest); +#endif +} + +void hmac_sha256(const void *message, size_t size, const void *key, size_t key_size, void *digest) { +#if USE_NETTLE + struct hmac_sha256_ctx ctx; + hmac_sha256_set_key(&ctx, key_size, key); + hmac_sha256_update(&ctx, size, message); + hmac_sha256_digest(&ctx, HMAC_SHA256_SIZE, digest); +#else + picohash_ctx_t ctx; + picohash_init_hmac(&ctx, picohash_init_sha256, key, key_size); + picohash_update(&ctx, message, size); + picohash_final(&ctx, digest); +#endif +} diff --git a/thirdparty/libjuice/src/hmac.h b/thirdparty/libjuice/src/hmac.h new file mode 100644 index 0000000..2eedfc0 --- /dev/null +++ b/thirdparty/libjuice/src/hmac.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_HMAC_H +#define JUICE_HMAC_H + +#include +#include + +#define HMAC_SHA1_SIZE 20 +#define HMAC_SHA256_SIZE 32 + +void hmac_sha1(const void *message, size_t size, const void *key, size_t key_size, void *digest); +void hmac_sha256(const void *message, size_t size, const void *key, size_t key_size, void *digest); + +#endif diff --git a/thirdparty/libjuice/src/ice.c b/thirdparty/libjuice/src/ice.c new file mode 100644 index 0000000..859032e --- /dev/null +++ b/thirdparty/libjuice/src/ice.c @@ -0,0 +1,408 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "ice.h" +#include "log.h" +#include "random.h" + +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 1024 + +#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) + +// See RFC4566 for SDP format: https://www.rfc-editor.org/rfc/rfc4566.html + +static const char *skip_prefix(const char *str, const char *prefix) { + size_t len = strlen(prefix); + return strncmp(str, prefix, len) == 0 ? str + len : str; +} + +static bool match_prefix(const char *str, const char *prefix, const char **end) { + *end = skip_prefix(str, prefix); + return *end != str || !*prefix; +} + +static int parse_sdp_line(const char *line, ice_description_t *description) { + const char *arg; + if (match_prefix(line, "a=ice-ufrag:", &arg)) { + sscanf(arg, "%256s", description->ice_ufrag); + return 0; + } + if (match_prefix(line, "a=ice-pwd:", &arg)) { + sscanf(arg, "%256s", description->ice_pwd); + return 0; + } + if (match_prefix(line, "a=end-of-candidates:", &arg)) { + description->finished = true; + return 0; + } + ice_candidate_t candidate; + if (ice_parse_candidate_sdp(line, &candidate) == 0) { + ice_add_candidate(&candidate, description); + return 0; + } + return ICE_PARSE_IGNORED; +} + +static int parse_sdp_candidate(const char *line, ice_candidate_t *candidate) { + memset(candidate, 0, sizeof(*candidate)); + + line = skip_prefix(line, "a="); + line = skip_prefix(line, "candidate:"); + + char transport[32 + 1]; + char type[32 + 1]; + if (sscanf(line, "%32s %d %32s %u %256s %32s typ %32s", candidate->foundation, + &candidate->component, transport, &candidate->priority, candidate->hostname, + candidate->service, type) != 7) { + JLOG_WARN("Failed to parse candidate: %s", line); + return ICE_PARSE_ERROR; + } + + for (int i = 0; transport[i]; ++i) + transport[i] = toupper((unsigned char)transport[i]); + + for (int i = 0; type[i]; ++i) + type[i] = tolower((unsigned char)type[i]); + + if (strcmp(type, "host") == 0) + candidate->type = ICE_CANDIDATE_TYPE_HOST; + else if (strcmp(type, "srflx") == 0) + candidate->type = ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE; + else if (strcmp(type, "relay") == 0) + candidate->type = ICE_CANDIDATE_TYPE_RELAYED; + else { + JLOG_WARN("Ignoring candidate with unknown type \"%s\"", type); + return ICE_PARSE_IGNORED; + } + + if (strcmp(transport, "UDP") != 0) { + JLOG_WARN("Ignoring candidate with transport %s", transport); + return ICE_PARSE_IGNORED; + } + + return 0; +} + +int ice_parse_sdp(const char *sdp, ice_description_t *description) { + memset(description, 0, sizeof(*description)); + description->candidates_count = 0; + description->finished = false; + + char buffer[BUFFER_SIZE]; + size_t size = 0; + while (*sdp) { + if (*sdp == '\n') { + if (size) { + buffer[size++] = '\0'; + if(parse_sdp_line(buffer, description) == ICE_PARSE_ERROR) + return ICE_PARSE_ERROR; + + size = 0; + } + } else if (*sdp != '\r' && size + 1 < BUFFER_SIZE) { + buffer[size++] = *sdp; + } + ++sdp; + } + ice_sort_candidates(description); + + JLOG_DEBUG("Parsed remote description: ufrag=\"%s\", pwd=\"%s\", candidates=%d", + description->ice_ufrag, description->ice_pwd, description->candidates_count); + + if (*description->ice_ufrag == '\0') + return ICE_PARSE_MISSING_UFRAG; + + if (*description->ice_pwd == '\0') + return ICE_PARSE_MISSING_PWD; + + return 0; +} + +int ice_parse_candidate_sdp(const char *line, ice_candidate_t *candidate) { + const char *arg; + if (match_prefix(line, "a=candidate:", &arg)) { + int ret = parse_sdp_candidate(line, candidate); + if (ret < 0) + return ret; + ice_resolve_candidate(candidate, ICE_RESOLVE_MODE_SIMPLE); + return 0; + } + return ICE_PARSE_ERROR; +} + +int ice_create_local_description(ice_description_t *description) { + memset(description, 0, sizeof(*description)); + juice_random_str64(description->ice_ufrag, 4 + 1); + juice_random_str64(description->ice_pwd, 22 + 1); + description->candidates_count = 0; + description->finished = false; + JLOG_DEBUG("Created local description: ufrag=\"%s\", pwd=\"%s\"", description->ice_ufrag, + description->ice_pwd); + return 0; +} + +int ice_create_local_candidate(ice_candidate_type_t type, int component, int index, + const addr_record_t *record, ice_candidate_t *candidate) { + memset(candidate, 0, sizeof(*candidate)); + candidate->type = type; + candidate->component = component; + candidate->resolved = *record; + strcpy(candidate->foundation, "-"); + + candidate->priority = ice_compute_priority(candidate->type, candidate->resolved.addr.ss_family, + candidate->component, index); + + if (getnameinfo((struct sockaddr *)&record->addr, record->len, candidate->hostname, 256, + candidate->service, 32, NI_NUMERICHOST | NI_NUMERICSERV | NI_DGRAM)) { + JLOG_ERROR("getnameinfo failed, errno=%d", sockerrno); + return -1; + } + return 0; +} + +int ice_resolve_candidate(ice_candidate_t *candidate, ice_resolve_mode_t mode) { + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_ADDRCONFIG; + if (mode != ICE_RESOLVE_MODE_LOOKUP) + hints.ai_flags |= AI_NUMERICHOST | AI_NUMERICSERV; + struct addrinfo *ai_list = NULL; + if (getaddrinfo(candidate->hostname, candidate->service, &hints, &ai_list)) { + JLOG_INFO("Failed to resolve address: %s:%s", candidate->hostname, candidate->service); + candidate->resolved.len = 0; + return -1; + } + for (struct addrinfo *ai = ai_list; ai; ai = ai->ai_next) { + if (ai->ai_family == AF_INET || ai->ai_family == AF_INET6) { + candidate->resolved.len = (socklen_t)ai->ai_addrlen; + memcpy(&candidate->resolved.addr, ai->ai_addr, ai->ai_addrlen); + break; + } + } + freeaddrinfo(ai_list); + return 0; +} + +int ice_add_candidate(ice_candidate_t *candidate, ice_description_t *description) { + if (candidate->type == ICE_CANDIDATE_TYPE_UNKNOWN) + return -1; + + if (description->candidates_count >= ICE_MAX_CANDIDATES_COUNT) { + JLOG_WARN("Description already has the maximum number of candidates"); + return -1; + } + + if (strcmp(candidate->foundation, "-") == 0) + snprintf(candidate->foundation, 32, "%u", + (unsigned int)(description->candidates_count + 1)); + + ice_candidate_t *pos = description->candidates + description->candidates_count; + *pos = *candidate; + ++description->candidates_count; + return 0; +} + +void ice_sort_candidates(ice_description_t *description) { + // In-place insertion sort + ice_candidate_t *begin = description->candidates; + ice_candidate_t *end = begin + description->candidates_count; + ice_candidate_t *cur = begin; + while (++cur < end) { + uint32_t priority = cur->priority; + ice_candidate_t *prev = cur; + ice_candidate_t tmp = *prev; + while (--prev >= begin && prev->priority < priority) { + *(prev + 1) = *prev; + } + if (prev + 1 != cur) + *(prev + 1) = tmp; + } +} + +ice_candidate_t *ice_find_candidate_from_addr(ice_description_t *description, + const addr_record_t *record, + ice_candidate_type_t type) { + ice_candidate_t *cur = description->candidates; + ice_candidate_t *end = cur + description->candidates_count; + while (cur != end) { + if ((type == ICE_CANDIDATE_TYPE_UNKNOWN || cur->type == type) && + addr_is_equal((struct sockaddr *)&record->addr, (struct sockaddr *)&cur->resolved.addr, + true)) + return cur; + ++cur; + } + return NULL; +} + +int ice_generate_sdp(const ice_description_t *description, char *buffer, size_t size) { + if (!*description->ice_ufrag || !*description->ice_pwd) + return -1; + + int len = 0; + char *begin = buffer; + char *end = begin + size; + + // Round 0: description + // Round i with i>0 and icandidates_count + 2; ++i) { + int ret; + if (i == 0) { + ret = snprintf(begin, end - begin, "a=ice-ufrag:%s\r\na=ice-pwd:%s\r\n", + description->ice_ufrag, description->ice_pwd); + } else if (i < description->candidates_count + 1) { + const ice_candidate_t *candidate = description->candidates + i - 1; + if (candidate->type == ICE_CANDIDATE_TYPE_UNKNOWN || + candidate->type == ICE_CANDIDATE_TYPE_PEER_REFLEXIVE) + continue; + char tmp[BUFFER_SIZE]; + if (ice_generate_candidate_sdp(candidate, tmp, BUFFER_SIZE) < 0) + continue; + ret = snprintf(begin, end - begin, "%s\r\n", tmp); + } else { // i == description->candidates_count + 1 + // RFC 8445 10. ICE Option: An agent compliant to this specification MUST inform the + // peer about the compliance using the 'ice2' option. + if (description->finished) + ret = snprintf(begin, end - begin, "a=end-of-candidates\r\na=ice-options:ice2\r\n"); + else + ret = snprintf(begin, end - begin, "a=ice-options:ice2,trickle\r\n"); + } + if (ret < 0) + return -1; + + len += ret; + + if (begin < end) + begin += ret >= end - begin ? end - begin - 1 : ret; + } + return len; +} + +int ice_generate_candidate_sdp(const ice_candidate_t *candidate, char *buffer, size_t size) { + const char *type = NULL; + const char *suffix = NULL; + switch (candidate->type) { + case ICE_CANDIDATE_TYPE_HOST: + type = "host"; + break; + case ICE_CANDIDATE_TYPE_PEER_REFLEXIVE: + type = "prflx"; + break; + case ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: + type = "srflx"; + suffix = "raddr 0.0.0.0 rport 0"; // This is needed for compatibility with Firefox + break; + case ICE_CANDIDATE_TYPE_RELAYED: + type = "relay"; + suffix = "raddr 0.0.0.0 rport 0"; // This is needed for compatibility with Firefox + break; + default: + JLOG_ERROR("Unknown candidate type"); + return -1; + } + return snprintf(buffer, size, "a=candidate:%s %u UDP %u %s %s typ %s%s%s", + candidate->foundation, candidate->component, candidate->priority, + candidate->hostname, candidate->service, type, suffix ? " " : "", + suffix ? suffix : ""); +} + +int ice_create_candidate_pair(ice_candidate_t *local, ice_candidate_t *remote, bool is_controlling, + ice_candidate_pair_t *pair) { // local or remote might be NULL + if (local && remote && local->resolved.addr.ss_family != remote->resolved.addr.ss_family) { + JLOG_ERROR("Mismatching candidates address families"); + return -1; + } + + memset(pair, 0, sizeof(*pair)); + pair->local = local; + pair->remote = remote; + pair->state = ICE_CANDIDATE_PAIR_STATE_FROZEN; + return ice_update_candidate_pair(pair, is_controlling); +} + +int ice_update_candidate_pair(ice_candidate_pair_t *pair, bool is_controlling) { + // Compute pair priority according to RFC 8445, extended to support generic pairs missing local + // or remote See https://www.rfc-editor.org/rfc/rfc8445.html#section-6.1.2.3 + if (!pair->local && !pair->remote) + return 0; + uint64_t local_priority = + pair->local + ? pair->local->priority + : ice_compute_priority(ICE_CANDIDATE_TYPE_HOST, pair->remote->resolved.addr.ss_family, + pair->remote->component, 0); + uint64_t remote_priority = + pair->remote + ? pair->remote->priority + : ice_compute_priority(ICE_CANDIDATE_TYPE_HOST, pair->local->resolved.addr.ss_family, + pair->local->component, 0); + uint64_t g = is_controlling ? local_priority : remote_priority; + uint64_t d = is_controlling ? remote_priority : local_priority; + uint64_t min = g < d ? g : d; + uint64_t max = g > d ? g : d; + pair->priority = (min << 32) + (max << 1) + (g > d ? 1 : 0); + return 0; +} + +int ice_candidates_count(const ice_description_t *description, ice_candidate_type_t type) { + int count = 0; + for (int i = 0; i < description->candidates_count; ++i) { + const ice_candidate_t *candidate = description->candidates + i; + if (candidate->type == type) + ++count; + } + return count; +} + +uint32_t ice_compute_priority(ice_candidate_type_t type, int family, int component, int index) { + // Compute candidate priority according to RFC 8445 + // See https://www.rfc-editor.org/rfc/rfc8445.html#section-5.1.2.1 + uint32_t p = 0; + + switch (type) { + case ICE_CANDIDATE_TYPE_HOST: + p += ICE_CANDIDATE_PREF_HOST; + break; + case ICE_CANDIDATE_TYPE_PEER_REFLEXIVE: + p += ICE_CANDIDATE_PREF_PEER_REFLEXIVE; + break; + case ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE: + p += ICE_CANDIDATE_PREF_SERVER_REFLEXIVE; + break; + case ICE_CANDIDATE_TYPE_RELAYED: + p += ICE_CANDIDATE_PREF_RELAYED; + break; + default: + break; + } + p <<= 16; + + switch (family) { + case AF_INET: + p += 32767; + break; + case AF_INET6: + p += 65535; + break; + default: + break; + } + p -= CLAMP(index, 0, 32767); + p <<= 8; + + p += 256 - CLAMP(component, 1, 256); + return p; +} diff --git a/thirdparty/libjuice/src/ice.h b/thirdparty/libjuice/src/ice.h new file mode 100644 index 0000000..ea17aaf --- /dev/null +++ b/thirdparty/libjuice/src/ice.h @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_ICE_H +#define JUICE_ICE_H + +#include "addr.h" +#include "juice.h" +#include "timestamp.h" + +#include +#include + +#define ICE_MAX_CANDIDATES_COUNT 20 // ~ 500B * 20 = 10KB + +typedef enum ice_candidate_type { + ICE_CANDIDATE_TYPE_UNKNOWN, + ICE_CANDIDATE_TYPE_HOST, + ICE_CANDIDATE_TYPE_SERVER_REFLEXIVE, + ICE_CANDIDATE_TYPE_PEER_REFLEXIVE, + ICE_CANDIDATE_TYPE_RELAYED, +} ice_candidate_type_t; + +// RFC 8445: The RECOMMENDED values for type preferences are 126 for host candidates, 110 for +// peer-reflexive candidates, 100 for server-reflexive candidates, and 0 for relayed candidates. +#define ICE_CANDIDATE_PREF_HOST 126 +#define ICE_CANDIDATE_PREF_PEER_REFLEXIVE 110 +#define ICE_CANDIDATE_PREF_SERVER_REFLEXIVE 100 +#define ICE_CANDIDATE_PREF_RELAYED 0 + +typedef struct ice_candidate { + ice_candidate_type_t type; + uint32_t priority; + int component; + char foundation[32 + 1]; // 1 to 32 characters + char transport[32 + 1]; + char hostname[256 + 1]; + char service[32 + 1]; + addr_record_t resolved; +} ice_candidate_t; + +typedef struct ice_description { + char ice_ufrag[256 + 1]; // 4 to 256 characters + char ice_pwd[256 + 1]; // 22 to 256 characters + ice_candidate_t candidates[ICE_MAX_CANDIDATES_COUNT]; + int candidates_count; + bool finished; +} ice_description_t; + +typedef enum ice_candidate_pair_state { + ICE_CANDIDATE_PAIR_STATE_PENDING, + ICE_CANDIDATE_PAIR_STATE_SUCCEEDED, + ICE_CANDIDATE_PAIR_STATE_FAILED, + ICE_CANDIDATE_PAIR_STATE_FROZEN, +} ice_candidate_pair_state_t; + +typedef struct ice_candidate_pair { + ice_candidate_t *local; + ice_candidate_t *remote; + uint64_t priority; + ice_candidate_pair_state_t state; + bool nominated; + bool nomination_requested; + timestamp_t consent_expiry; +} ice_candidate_pair_t; + +typedef enum ice_resolve_mode { + ICE_RESOLVE_MODE_SIMPLE, + ICE_RESOLVE_MODE_LOOKUP, +} ice_resolve_mode_t; + +#define ICE_PARSE_ERROR -1 +#define ICE_PARSE_IGNORED -2 +#define ICE_PARSE_MISSING_UFRAG -3 +#define ICE_PARSE_MISSING_PWD -4 + +int ice_parse_sdp(const char *sdp, ice_description_t *description); +int ice_parse_candidate_sdp(const char *line, ice_candidate_t *candidate); +int ice_create_local_description(ice_description_t *description); +int ice_create_local_candidate(ice_candidate_type_t type, int component, int index, + const addr_record_t *record, ice_candidate_t *candidate); +int ice_resolve_candidate(ice_candidate_t *candidate, ice_resolve_mode_t mode); +int ice_add_candidate(ice_candidate_t *candidate, ice_description_t *description); +void ice_sort_candidates(ice_description_t *description); +ice_candidate_t *ice_find_candidate_from_addr(ice_description_t *description, + const addr_record_t *record, + ice_candidate_type_t type); +int ice_generate_sdp(const ice_description_t *description, char *buffer, size_t size); +int ice_generate_candidate_sdp(const ice_candidate_t *candidate, char *buffer, size_t size); +int ice_create_candidate_pair(ice_candidate_t *local, ice_candidate_t *remote, bool is_controlling, + ice_candidate_pair_t *pair); // local or remote might be NULL +int ice_update_candidate_pair(ice_candidate_pair_t *pair, bool is_controlling); + +int ice_candidates_count(const ice_description_t *description, ice_candidate_type_t type); + +uint32_t ice_compute_priority(ice_candidate_type_t type, int family, int component, int index); + +#endif diff --git a/thirdparty/libjuice/src/juice.c b/thirdparty/libjuice/src/juice.c new file mode 100644 index 0000000..6af80c9 --- /dev/null +++ b/thirdparty/libjuice/src/juice.c @@ -0,0 +1,207 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice.h" +#include "addr.h" +#include "agent.h" +#include "ice.h" + +#ifndef NO_SERVER +#include "server.h" +#endif + +#include + +JUICE_EXPORT juice_agent_t *juice_create(const juice_config_t *config) { + if (!config) + return NULL; + + return agent_create(config); +} + +JUICE_EXPORT void juice_destroy(juice_agent_t *agent) { + if (agent) + agent_destroy(agent); +} + +JUICE_EXPORT int juice_gather_candidates(juice_agent_t *agent) { + if (!agent) + return JUICE_ERR_INVALID; + + if (agent_gather_candidates(agent) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_get_local_description(juice_agent_t *agent, char *buffer, size_t size) { + if (!agent || (!buffer && size)) + return JUICE_ERR_INVALID; + + if (agent_get_local_description(agent, buffer, size) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_set_remote_description(juice_agent_t *agent, const char *sdp) { + if (!agent || !sdp) + return JUICE_ERR_INVALID; + + if (agent_set_remote_description(agent, sdp) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_add_remote_candidate(juice_agent_t *agent, const char *sdp) { + if (!agent || !sdp) + return JUICE_ERR_INVALID; + + if (agent_add_remote_candidate(agent, sdp) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_set_remote_gathering_done(juice_agent_t *agent) { + if (!agent) + return JUICE_ERR_INVALID; + + if (agent_set_remote_gathering_done(agent) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_send(juice_agent_t *agent, const char *data, size_t size) { + if (!agent || (!data && size)) + return JUICE_ERR_INVALID; + + if (agent_send(agent, data, size, 0) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_send_diffserv(juice_agent_t *agent, const char *data, size_t size, int ds) { + if (!agent || (!data && size)) + return JUICE_ERR_INVALID; + + if (agent_send(agent, data, size, ds) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT juice_state_t juice_get_state(juice_agent_t *agent) { return agent_get_state(agent); } + +JUICE_EXPORT int juice_get_selected_candidates(juice_agent_t *agent, char *local, size_t local_size, + char *remote, size_t remote_size) { + if (!agent || (!local && local_size) || (!remote && remote_size)) + return JUICE_ERR_INVALID; + + ice_candidate_t local_cand, remote_cand; + if (agent_get_selected_candidate_pair(agent, &local_cand, &remote_cand)) + return JUICE_ERR_NOT_AVAIL; + + if (local_size && ice_generate_candidate_sdp(&local_cand, local, local_size) < 0) + return JUICE_ERR_FAILED; + + if (remote_size && ice_generate_candidate_sdp(&remote_cand, remote, remote_size) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT int juice_get_selected_addresses(juice_agent_t *agent, char *local, size_t local_size, + char *remote, size_t remote_size) { + if (!agent || (!local && local_size) || (!remote && remote_size)) + return JUICE_ERR_INVALID; + + ice_candidate_t local_cand, remote_cand; + if (agent_get_selected_candidate_pair(agent, &local_cand, &remote_cand)) + return JUICE_ERR_NOT_AVAIL; + + if (local_size && addr_record_to_string(&local_cand.resolved, local, local_size) < 0) + return JUICE_ERR_FAILED; + + if (remote_size && addr_record_to_string(&remote_cand.resolved, remote, remote_size) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +} + +JUICE_EXPORT const char *juice_state_to_string(juice_state_t state) { + switch (state) { + case JUICE_STATE_DISCONNECTED: + return "disconnected"; + case JUICE_STATE_GATHERING: + return "gathering"; + case JUICE_STATE_CONNECTING: + return "connecting"; + case JUICE_STATE_CONNECTED: + return "connected"; + case JUICE_STATE_COMPLETED: + return "completed"; + case JUICE_STATE_FAILED: + return "failed"; + default: + return "unknown"; + } +} + +JUICE_EXPORT juice_server_t *juice_server_create(const juice_server_config_t *config) { +#ifndef NO_SERVER + if (!config) + return NULL; + + return server_create(config); +#else + (void)config; + JLOG_FATAL("The library was compiled without server support"); + return NULL; +#endif +} + +JUICE_EXPORT void juice_server_destroy(juice_server_t *server) { +#ifndef NO_SERVER + if (server) + server_destroy(server); +#else + (void)server; +#endif +} + +JUICE_EXPORT uint16_t juice_server_get_port(juice_server_t *server) { +#ifndef NO_SERVER + return server ? server_get_port(server) : 0; +#else + (void)server; + return 0; +#endif +} + +JUICE_EXPORT int juice_server_add_credentials(juice_server_t *server, + const juice_server_credentials_t *credentials, + unsigned long lifetime_ms) { +#ifndef NO_SERVER + if (!server || !credentials) + return JUICE_ERR_INVALID; + + if (server_add_credentials(server, credentials, (timediff_t)lifetime_ms) < 0) + return JUICE_ERR_FAILED; + + return JUICE_ERR_SUCCESS; +#else + (void)server; + (void)credentials; + (void)lifetime_ms; + return JUICE_ERR_INVALID; +#endif +} diff --git a/thirdparty/libjuice/src/log.c b/thirdparty/libjuice/src/log.c new file mode 100644 index 0000000..bd6dba7 --- /dev/null +++ b/thirdparty/libjuice/src/log.c @@ -0,0 +1,129 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "log.h" +#include "thread.h" // for mutexes and atomics + +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#endif + +#define BUFFER_SIZE 4096 + +static const char *log_level_names[] = {"VERBOSE", "DEBUG", "INFO", "WARN", "ERROR", "FATAL"}; + +static const char *log_level_colors[] = { + "\x1B[90m", // grey + "\x1B[96m", // cyan + "\x1B[39m", // default foreground + "\x1B[93m", // yellow + "\x1B[91m", // red + "\x1B[97m\x1B[41m" // white on red +}; + +static mutex_t log_mutex = MUTEX_INITIALIZER; +static volatile juice_log_cb_t log_cb = NULL; +static atomic(juice_log_level_t) log_level = ATOMIC_VAR_INIT(JUICE_LOG_LEVEL_WARN); + +static bool use_color(void) { +#ifdef _WIN32 + return false; +#else + return isatty(fileno(stdout)) != 0; +#endif +} + +static int get_localtime(const time_t *t, struct tm *buf) { +#ifdef _WIN32 + // Windows does not have POSIX localtime_r... + return localtime_s(buf, t) == 0 ? 0 : -1; +#else // POSIX + return localtime_r(t, buf) != NULL ? 0 : -1; +#endif +} + +JUICE_EXPORT void juice_set_log_level(juice_log_level_t level) { atomic_store(&log_level, level); } + +JUICE_EXPORT void juice_set_log_handler(juice_log_cb_t cb) { + mutex_lock(&log_mutex); + log_cb = cb; + mutex_unlock(&log_mutex); +} + +bool juice_log_is_enabled(juice_log_level_t level) { + return level != JUICE_LOG_LEVEL_NONE && level >= atomic_load(&log_level); +} + +void juice_log_write(juice_log_level_t level, const char *file, int line, const char *fmt, ...) { + if (!juice_log_is_enabled(level)) + return; + + mutex_lock(&log_mutex); + +#if !RELEASE + const char *filename = file + strlen(file); + while (filename != file && *filename != '/' && *filename != '\\') + --filename; + if (filename != file) + ++filename; +#else + (void)file; + (void)line; +#endif + + if (log_cb) { + char message[BUFFER_SIZE]; + int len = 0; +#if !RELEASE + len = snprintf(message, BUFFER_SIZE, "%s:%d: ", filename, line); + if (len < 0) + return; +#endif + if (len < BUFFER_SIZE) { + va_list args; + va_start(args, fmt); + vsnprintf(message + len, BUFFER_SIZE - len, fmt, args); + va_end(args); + } + + log_cb(level, message); + + } else { + time_t t = time(NULL); + struct tm lt; + char buffer[16]; + if (get_localtime(&t, <) != 0 || strftime(buffer, 16, "%H:%M:%S", <) == 0) + buffer[0] = '\0'; + + if (use_color()) + fprintf(stdout, "%s", log_level_colors[level]); + + fprintf(stdout, "%s %-7s ", buffer, log_level_names[level]); + +#if !RELEASE + fprintf(stdout, "%s:%d: ", filename, line); +#endif + + va_list args; + va_start(args, fmt); + vfprintf(stdout, fmt, args); + va_end(args); + + if (use_color()) + fprintf(stdout, "%s", "\x1B[0m\x1B[0K"); + + fprintf(stdout, "\n"); + fflush(stdout); + } + mutex_unlock(&log_mutex); +} diff --git a/thirdparty/libjuice/src/log.h b/thirdparty/libjuice/src/log.h new file mode 100644 index 0000000..4ac34a6 --- /dev/null +++ b/thirdparty/libjuice/src/log.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_LOG_H +#define JUICE_LOG_H + +#include "juice.h" + +#include + +bool juice_log_is_enabled(juice_log_level_t level); +void juice_log_write(juice_log_level_t level, const char *file, int line, const char *fmt, ...); + +#define JLOG_VERBOSE(...) juice_log_write(JUICE_LOG_LEVEL_VERBOSE, __FILE__, __LINE__, __VA_ARGS__) +#define JLOG_DEBUG(...) juice_log_write(JUICE_LOG_LEVEL_DEBUG, __FILE__, __LINE__, __VA_ARGS__) +#define JLOG_INFO(...) juice_log_write(JUICE_LOG_LEVEL_INFO, __FILE__, __LINE__, __VA_ARGS__) +#define JLOG_WARN(...) juice_log_write(JUICE_LOG_LEVEL_WARN, __FILE__, __LINE__, __VA_ARGS__) +#define JLOG_ERROR(...) juice_log_write(JUICE_LOG_LEVEL_ERROR, __FILE__, __LINE__, __VA_ARGS__) +#define JLOG_FATAL(...) juice_log_write(JUICE_LOG_LEVEL_FATAL, __FILE__, __LINE__, __VA_ARGS__) + +#define JLOG_VERBOSE_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_VERBOSE) +#define JLOG_DEBUG_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_DEBUG) +#define JLOG_INFO_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_INFO) +#define JLOG_WARN_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_WARN) +#define JLOG_ERROR_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_ERROR) +#define JLOG_FATAL_ENABLED juice_log_is_enabled(JUICE_LOG_LEVEL_FATAL) + +#endif // JUICE_LOG_H diff --git a/thirdparty/libjuice/src/picohash.h b/thirdparty/libjuice/src/picohash.h new file mode 100644 index 0000000..4a58374 --- /dev/null +++ b/thirdparty/libjuice/src/picohash.h @@ -0,0 +1,741 @@ +/** + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +#ifndef _picohash_h_ +#define _picohash_h_ + +#include +#include +#include + +#ifdef _WIN32 +/* assume Windows is little endian */ +#elif defined __BIG_ENDIAN__ +#define _PICOHASH_BIG_ENDIAN +#elif defined __LITTLE_ENDIAN__ +/* override */ +#elif defined __BYTE_ORDER +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define _PICOHASH_BIG_ENDIAN +#endif +#else // ! defined __LITTLE_ENDIAN__ +#include // machine/endian.h +#if __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ +#define _PICOHASH_BIG_ENDIAN +#endif +#endif + +#define PICOHASH_MD5_BLOCK_LENGTH 64 +#define PICOHASH_MD5_DIGEST_LENGTH 16 + +typedef struct { + uint_fast32_t lo, hi; + uint_fast32_t a, b, c, d; + unsigned char buffer[64]; + uint_fast32_t block[PICOHASH_MD5_DIGEST_LENGTH]; +} _picohash_md5_ctx_t; + +static void _picohash_md5_init(_picohash_md5_ctx_t *ctx); +static void _picohash_md5_update(_picohash_md5_ctx_t *ctx, const void *data, size_t size); +static void _picohash_md5_final(_picohash_md5_ctx_t *ctx, void *digest); + +#define PICOHASH_SHA1_BLOCK_LENGTH 64 +#define PICOHASH_SHA1_DIGEST_LENGTH 20 + +typedef struct { + uint32_t buffer[PICOHASH_SHA1_BLOCK_LENGTH / 4]; + uint32_t state[PICOHASH_SHA1_DIGEST_LENGTH / 4]; + uint64_t byteCount; + uint8_t bufferOffset; +} _picohash_sha1_ctx_t; + +static void _picohash_sha1_init(_picohash_sha1_ctx_t *ctx); +static void _picohash_sha1_update(_picohash_sha1_ctx_t *ctx, const void *input, size_t len); +static void _picohash_sha1_final(_picohash_sha1_ctx_t *ctx, void *digest); + +#define PICOHASH_SHA256_BLOCK_LENGTH 64 +#define PICOHASH_SHA256_DIGEST_LENGTH 32 +#define PICOHASH_SHA224_BLOCK_LENGTH PICOHASH_SHA256_BLOCK_LENGTH +#define PICOHASH_SHA224_DIGEST_LENGTH 28 + +typedef struct { + uint64_t length; + uint32_t state[PICOHASH_SHA256_DIGEST_LENGTH / 4]; + uint32_t curlen; + unsigned char buf[PICOHASH_SHA256_BLOCK_LENGTH]; +} _picohash_sha256_ctx_t; + +static void _picohash_sha256_init(_picohash_sha256_ctx_t *ctx); +static void _picohash_sha256_update(_picohash_sha256_ctx_t *ctx, const void *data, size_t len); +static void _picohash_sha256_final(_picohash_sha256_ctx_t *ctx, void *digest); +static void _picohash_sha224_init(_picohash_sha256_ctx_t *ctx); +static void _picohash_sha224_final(_picohash_sha256_ctx_t *ctx, void *digest); + +#define PICOHASH_MAX_BLOCK_LENGTH 64 +#define PICOHASH_MAX_DIGEST_LENGTH 32 + +typedef struct { + union { + _picohash_md5_ctx_t _md5; + _picohash_sha1_ctx_t _sha1; + _picohash_sha256_ctx_t _sha256; + }; + size_t block_length; + size_t digest_length; + void (*_reset)(void *ctx); + void (*_update)(void *ctx, const void *input, size_t len); + void (*_final)(void *ctx, void *digest); + struct { + unsigned char key[PICOHASH_MAX_BLOCK_LENGTH]; + void (*hash_reset)(void *ctx); + void (*hash_final)(void *ctx, void *digest); + } _hmac; +} picohash_ctx_t; + +static void picohash_init_md5(picohash_ctx_t *ctx); +static void picohash_init_sha1(picohash_ctx_t *ctx); +static void picohash_init_sha224(picohash_ctx_t *ctx); +static void picohash_init_sha256(picohash_ctx_t *ctx); +static void picohash_update(picohash_ctx_t *ctx, const void *input, size_t len); +static void picohash_final(picohash_ctx_t *ctx, void *digest); +static void picohash_reset(picohash_ctx_t *ctx); + +static void picohash_init_hmac(picohash_ctx_t *ctx, void (*initf)(picohash_ctx_t *), const void *key, size_t key_len); + +/* following are private definitions */ + +/* + * The basic MD5 functions. + * + * F is optimized compared to its RFC 1321 definition just like in Colin + * Plumb's implementation. + */ +#define _PICOHASH_MD5_F(x, y, z) ((z) ^ ((x) & ((y) ^ (z)))) +#define _PICOHASH_MD5_G(x, y, z) ((y) ^ ((z) & ((x) ^ (y)))) +#define _PICOHASH_MD5_H(x, y, z) ((x) ^ (y) ^ (z)) +#define _PICOHASH_MD5_I(x, y, z) ((y) ^ ((x) | ~(z))) + +/* + * The MD5 transformation for all four rounds. + */ +#define _PICOHASH_MD5_STEP(f, a, b, c, d, x, t, s) \ + (a) += f((b), (c), (d)) + (x) + (t); \ + (a) = (((a) << (s)) | (((a)&0xffffffff) >> (32 - (s)))); \ + (a) += (b); + +/* + * SET reads 4 input bytes in little-endian byte order and stores them + * in a properly aligned word in host byte order. + * + * Paul-Louis Ageneau: Removed optimization for little-endian architectures + * as it resulted in incorrect behavior when compiling with gcc optimizations. + */ +#define _PICOHASH_MD5_SET(n) \ + (ctx->block[(n)] = (uint_fast32_t)ptr[(n)*4] | ((uint_fast32_t)ptr[(n)*4 + 1] << 8) | ((uint_fast32_t)ptr[(n)*4 + 2] << 16) | \ + ((uint_fast32_t)ptr[(n)*4 + 3] << 24)) +#define _PICOHASH_MD5_GET(n) (ctx->block[(n)]) + +/* + * This processes one or more 64-byte data blocks, but does NOT update + * the bit counters. There're no alignment requirements. + */ +static const void *_picohash_md5_body(_picohash_md5_ctx_t *ctx, const void *data, size_t size) +{ + const unsigned char *ptr; + uint_fast32_t a, b, c, d; + uint_fast32_t saved_a, saved_b, saved_c, saved_d; + + ptr = data; + + a = ctx->a; + b = ctx->b; + c = ctx->c; + d = ctx->d; + + do { + saved_a = a; + saved_b = b; + saved_c = c; + saved_d = d; + + /* Round 1 */ + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, a, b, c, d, _PICOHASH_MD5_SET(0), 0xd76aa478, 7) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, d, a, b, c, _PICOHASH_MD5_SET(1), 0xe8c7b756, 12) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, c, d, a, b, _PICOHASH_MD5_SET(2), 0x242070db, 17) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, b, c, d, a, _PICOHASH_MD5_SET(3), 0xc1bdceee, 22) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, a, b, c, d, _PICOHASH_MD5_SET(4), 0xf57c0faf, 7) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, d, a, b, c, _PICOHASH_MD5_SET(5), 0x4787c62a, 12) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, c, d, a, b, _PICOHASH_MD5_SET(6), 0xa8304613, 17) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, b, c, d, a, _PICOHASH_MD5_SET(7), 0xfd469501, 22) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, a, b, c, d, _PICOHASH_MD5_SET(8), 0x698098d8, 7) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, d, a, b, c, _PICOHASH_MD5_SET(9), 0x8b44f7af, 12) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, c, d, a, b, _PICOHASH_MD5_SET(10), 0xffff5bb1, 17) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, b, c, d, a, _PICOHASH_MD5_SET(11), 0x895cd7be, 22) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, a, b, c, d, _PICOHASH_MD5_SET(12), 0x6b901122, 7) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, d, a, b, c, _PICOHASH_MD5_SET(13), 0xfd987193, 12) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, c, d, a, b, _PICOHASH_MD5_SET(14), 0xa679438e, 17) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_F, b, c, d, a, _PICOHASH_MD5_SET(15), 0x49b40821, 22) + + /* Round 2 */ + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, a, b, c, d, _PICOHASH_MD5_GET(1), 0xf61e2562, 5) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, d, a, b, c, _PICOHASH_MD5_GET(6), 0xc040b340, 9) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, c, d, a, b, _PICOHASH_MD5_GET(11), 0x265e5a51, 14) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, b, c, d, a, _PICOHASH_MD5_GET(0), 0xe9b6c7aa, 20) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, a, b, c, d, _PICOHASH_MD5_GET(5), 0xd62f105d, 5) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, d, a, b, c, _PICOHASH_MD5_GET(10), 0x02441453, 9) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, c, d, a, b, _PICOHASH_MD5_GET(15), 0xd8a1e681, 14) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, b, c, d, a, _PICOHASH_MD5_GET(4), 0xe7d3fbc8, 20) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, a, b, c, d, _PICOHASH_MD5_GET(9), 0x21e1cde6, 5) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, d, a, b, c, _PICOHASH_MD5_GET(14), 0xc33707d6, 9) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, c, d, a, b, _PICOHASH_MD5_GET(3), 0xf4d50d87, 14) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, b, c, d, a, _PICOHASH_MD5_GET(8), 0x455a14ed, 20) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, a, b, c, d, _PICOHASH_MD5_GET(13), 0xa9e3e905, 5) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, d, a, b, c, _PICOHASH_MD5_GET(2), 0xfcefa3f8, 9) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, c, d, a, b, _PICOHASH_MD5_GET(7), 0x676f02d9, 14) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_G, b, c, d, a, _PICOHASH_MD5_GET(12), 0x8d2a4c8a, 20) + + /* Round 3 */ + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, a, b, c, d, _PICOHASH_MD5_GET(5), 0xfffa3942, 4) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, d, a, b, c, _PICOHASH_MD5_GET(8), 0x8771f681, 11) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, c, d, a, b, _PICOHASH_MD5_GET(11), 0x6d9d6122, 16) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, b, c, d, a, _PICOHASH_MD5_GET(14), 0xfde5380c, 23) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, a, b, c, d, _PICOHASH_MD5_GET(1), 0xa4beea44, 4) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, d, a, b, c, _PICOHASH_MD5_GET(4), 0x4bdecfa9, 11) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, c, d, a, b, _PICOHASH_MD5_GET(7), 0xf6bb4b60, 16) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, b, c, d, a, _PICOHASH_MD5_GET(10), 0xbebfbc70, 23) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, a, b, c, d, _PICOHASH_MD5_GET(13), 0x289b7ec6, 4) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, d, a, b, c, _PICOHASH_MD5_GET(0), 0xeaa127fa, 11) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, c, d, a, b, _PICOHASH_MD5_GET(3), 0xd4ef3085, 16) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, b, c, d, a, _PICOHASH_MD5_GET(6), 0x04881d05, 23) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, a, b, c, d, _PICOHASH_MD5_GET(9), 0xd9d4d039, 4) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, d, a, b, c, _PICOHASH_MD5_GET(12), 0xe6db99e5, 11) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, c, d, a, b, _PICOHASH_MD5_GET(15), 0x1fa27cf8, 16) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_H, b, c, d, a, _PICOHASH_MD5_GET(2), 0xc4ac5665, 23) + + /* Round 4 */ + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, a, b, c, d, _PICOHASH_MD5_GET(0), 0xf4292244, 6) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, d, a, b, c, _PICOHASH_MD5_GET(7), 0x432aff97, 10) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, c, d, a, b, _PICOHASH_MD5_GET(14), 0xab9423a7, 15) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, b, c, d, a, _PICOHASH_MD5_GET(5), 0xfc93a039, 21) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, a, b, c, d, _PICOHASH_MD5_GET(12), 0x655b59c3, 6) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, d, a, b, c, _PICOHASH_MD5_GET(3), 0x8f0ccc92, 10) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, c, d, a, b, _PICOHASH_MD5_GET(10), 0xffeff47d, 15) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, b, c, d, a, _PICOHASH_MD5_GET(1), 0x85845dd1, 21) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, a, b, c, d, _PICOHASH_MD5_GET(8), 0x6fa87e4f, 6) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, d, a, b, c, _PICOHASH_MD5_GET(15), 0xfe2ce6e0, 10) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, c, d, a, b, _PICOHASH_MD5_GET(6), 0xa3014314, 15) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, b, c, d, a, _PICOHASH_MD5_GET(13), 0x4e0811a1, 21) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, a, b, c, d, _PICOHASH_MD5_GET(4), 0xf7537e82, 6) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, d, a, b, c, _PICOHASH_MD5_GET(11), 0xbd3af235, 10) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, c, d, a, b, _PICOHASH_MD5_GET(2), 0x2ad7d2bb, 15) + _PICOHASH_MD5_STEP(_PICOHASH_MD5_I, b, c, d, a, _PICOHASH_MD5_GET(9), 0xeb86d391, 21) + + a += saved_a; + b += saved_b; + c += saved_c; + d += saved_d; + + ptr += 64; + } while (size -= 64); + + ctx->a = a; + ctx->b = b; + ctx->c = c; + ctx->d = d; + + return ptr; +} + +inline void _picohash_md5_init(_picohash_md5_ctx_t *ctx) +{ + ctx->a = 0x67452301; + ctx->b = 0xefcdab89; + ctx->c = 0x98badcfe; + ctx->d = 0x10325476; + + ctx->lo = 0; + ctx->hi = 0; +} + +inline void _picohash_md5_update(_picohash_md5_ctx_t *ctx, const void *data, size_t size) +{ + uint_fast32_t saved_lo; + unsigned long used, free; + + saved_lo = ctx->lo; + if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) + ctx->hi++; + ctx->hi += (uint_fast32_t)(size >> 29); + + used = saved_lo & 0x3f; + + if (used) { + free = 64 - used; + + if (size < free) { + memcpy(&ctx->buffer[used], data, size); + return; + } + + memcpy(&ctx->buffer[used], data, free); + data = (const unsigned char *)data + free; + size -= free; + _picohash_md5_body(ctx, ctx->buffer, 64); + } + + if (size >= 64) { + data = _picohash_md5_body(ctx, data, size & ~(unsigned long)0x3f); + size &= 0x3f; + } + + memcpy(ctx->buffer, data, size); +} + +inline void _picohash_md5_final(_picohash_md5_ctx_t *ctx, void *_digest) +{ + unsigned char *digest = _digest; + unsigned long used, free; + + used = ctx->lo & 0x3f; + + ctx->buffer[used++] = 0x80; + + free = 64 - used; + + if (free < 8) { + memset(&ctx->buffer[used], 0, free); + _picohash_md5_body(ctx, ctx->buffer, 64); + used = 0; + free = 64; + } + + memset(&ctx->buffer[used], 0, free - 8); + + ctx->lo <<= 3; + ctx->buffer[56] = ctx->lo; + ctx->buffer[57] = ctx->lo >> 8; + ctx->buffer[58] = ctx->lo >> 16; + ctx->buffer[59] = ctx->lo >> 24; + ctx->buffer[60] = ctx->hi; + ctx->buffer[61] = ctx->hi >> 8; + ctx->buffer[62] = ctx->hi >> 16; + ctx->buffer[63] = ctx->hi >> 24; + + _picohash_md5_body(ctx, ctx->buffer, 64); + + digest[0] = ctx->a; + digest[1] = ctx->a >> 8; + digest[2] = ctx->a >> 16; + digest[3] = ctx->a >> 24; + digest[4] = ctx->b; + digest[5] = ctx->b >> 8; + digest[6] = ctx->b >> 16; + digest[7] = ctx->b >> 24; + digest[8] = ctx->c; + digest[9] = ctx->c >> 8; + digest[10] = ctx->c >> 16; + digest[11] = ctx->c >> 24; + digest[12] = ctx->d; + digest[13] = ctx->d >> 8; + digest[14] = ctx->d >> 16; + digest[15] = ctx->d >> 24; + + memset(ctx, 0, sizeof(*ctx)); +} + +#define _PICOHASH_SHA1_K0 0x5a827999 +#define _PICOHASH_SHA1_K20 0x6ed9eba1 +#define _PICOHASH_SHA1_K40 0x8f1bbcdc +#define _PICOHASH_SHA1_K60 0xca62c1d6 + +static inline uint32_t _picohash_sha1_rol32(uint32_t number, uint8_t bits) +{ + return ((number << bits) | (number >> (32 - bits))); +} + +static inline void _picohash_sha1_hash_block(_picohash_sha1_ctx_t *s) +{ + uint8_t i; + uint32_t a, b, c, d, e, t; + + a = s->state[0]; + b = s->state[1]; + c = s->state[2]; + d = s->state[3]; + e = s->state[4]; + for (i = 0; i < 80; i++) { + if (i >= 16) { + t = s->buffer[(i + 13) & 15] ^ s->buffer[(i + 8) & 15] ^ s->buffer[(i + 2) & 15] ^ s->buffer[i & 15]; + s->buffer[i & 15] = _picohash_sha1_rol32(t, 1); + } + if (i < 20) { + t = (d ^ (b & (c ^ d))) + _PICOHASH_SHA1_K0; + } else if (i < 40) { + t = (b ^ c ^ d) + _PICOHASH_SHA1_K20; + } else if (i < 60) { + t = ((b & c) | (d & (b | c))) + _PICOHASH_SHA1_K40; + } else { + t = (b ^ c ^ d) + _PICOHASH_SHA1_K60; + } + t += _picohash_sha1_rol32(a, 5) + e + s->buffer[i & 15]; + e = d; + d = c; + c = _picohash_sha1_rol32(b, 30); + b = a; + a = t; + } + s->state[0] += a; + s->state[1] += b; + s->state[2] += c; + s->state[3] += d; + s->state[4] += e; +} + +static inline void _picohash_sha1_add_uncounted(_picohash_sha1_ctx_t *s, uint8_t data) +{ + uint8_t *const b = (uint8_t *)s->buffer; +#ifdef _PICOHASH_BIG_ENDIAN + b[s->bufferOffset] = data; +#else + b[s->bufferOffset ^ 3] = data; +#endif + s->bufferOffset++; + if (s->bufferOffset == PICOHASH_SHA1_BLOCK_LENGTH) { + _picohash_sha1_hash_block(s); + s->bufferOffset = 0; + } +} + +inline void _picohash_sha1_init(_picohash_sha1_ctx_t *s) +{ + s->state[0] = 0x67452301; + s->state[1] = 0xefcdab89; + s->state[2] = 0x98badcfe; + s->state[3] = 0x10325476; + s->state[4] = 0xc3d2e1f0; + s->byteCount = 0; + s->bufferOffset = 0; +} + +inline void _picohash_sha1_update(_picohash_sha1_ctx_t *s, const void *_data, size_t len) +{ + const uint8_t *data = _data; + for (; len != 0; --len) { + ++s->byteCount; + _picohash_sha1_add_uncounted(s, *data++); + } +} + +inline void _picohash_sha1_final(_picohash_sha1_ctx_t *s, void *digest) +{ + // Pad with 0x80 followed by 0x00 until the end of the block + _picohash_sha1_add_uncounted(s, 0x80); + while (s->bufferOffset != 56) + _picohash_sha1_add_uncounted(s, 0x00); + + // Append length in the last 8 bytes + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 53)); // Shifting to multiply by 8 + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 45)); // as SHA-1 supports bitstreams + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 37)); // as well as byte. + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 29)); + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 21)); + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 13)); + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount >> 5)); + _picohash_sha1_add_uncounted(s, (uint8_t)(s->byteCount << 3)); + +#ifndef SHA_BIG_ENDIAN + { // Swap byte order back + int i; + for (i = 0; i < 5; i++) { + s->state[i] = (((s->state[i]) << 24) & 0xff000000) | (((s->state[i]) << 8) & 0x00ff0000) | + (((s->state[i]) >> 8) & 0x0000ff00) | (((s->state[i]) >> 24) & 0x000000ff); + } + } +#endif + + memcpy(digest, s->state, sizeof(s->state)); +} + +#define _picohash_sha256_ch(x, y, z) (z ^ (x & (y ^ z))) +#define _picohash_sha256_maj(x, y, z) (((x | y) & z) | (x & y)) +#define _picohash_sha256_s(x, y) \ + (((((uint32_t)(x)&0xFFFFFFFFUL) >> (uint32_t)((y)&31)) | ((uint32_t)(x) << (uint32_t)(32 - ((y)&31)))) & 0xFFFFFFFFUL) +#define _picohash_sha256_r(x, n) (((x)&0xFFFFFFFFUL) >> (n)) +#define _picohash_sha256_sigma0(x) (_picohash_sha256_s(x, 2) ^ _picohash_sha256_s(x, 13) ^ _picohash_sha256_s(x, 22)) +#define _picohash_sha256_sigma1(x) (_picohash_sha256_s(x, 6) ^ _picohash_sha256_s(x, 11) ^ _picohash_sha256_s(x, 25)) +#define _picohash_sha256_gamma0(x) (_picohash_sha256_s(x, 7) ^ _picohash_sha256_s(x, 18) ^ _picohash_sha256_r(x, 3)) +#define _picohash_sha256_gamma1(x) (_picohash_sha256_s(x, 17) ^ _picohash_sha256_s(x, 19) ^ _picohash_sha256_r(x, 10)) +#define _picohash_sha256_rnd(a, b, c, d, e, f, g, h, i) \ + t0 = h + _picohash_sha256_sigma1(e) + _picohash_sha256_ch(e, f, g) + K[i] + W[i]; \ + t1 = _picohash_sha256_sigma0(a) + _picohash_sha256_maj(a, b, c); \ + d += t0; \ + h = t0 + t1; + +static inline void _picohash_sha256_compress(_picohash_sha256_ctx_t *ctx, unsigned char *buf) +{ + static const uint32_t K[64] = { + 0x428a2f98UL, 0x71374491UL, 0xb5c0fbcfUL, 0xe9b5dba5UL, 0x3956c25bUL, 0x59f111f1UL, 0x923f82a4UL, 0xab1c5ed5UL, + 0xd807aa98UL, 0x12835b01UL, 0x243185beUL, 0x550c7dc3UL, 0x72be5d74UL, 0x80deb1feUL, 0x9bdc06a7UL, 0xc19bf174UL, + 0xe49b69c1UL, 0xefbe4786UL, 0x0fc19dc6UL, 0x240ca1ccUL, 0x2de92c6fUL, 0x4a7484aaUL, 0x5cb0a9dcUL, 0x76f988daUL, + 0x983e5152UL, 0xa831c66dUL, 0xb00327c8UL, 0xbf597fc7UL, 0xc6e00bf3UL, 0xd5a79147UL, 0x06ca6351UL, 0x14292967UL, + 0x27b70a85UL, 0x2e1b2138UL, 0x4d2c6dfcUL, 0x53380d13UL, 0x650a7354UL, 0x766a0abbUL, 0x81c2c92eUL, 0x92722c85UL, + 0xa2bfe8a1UL, 0xa81a664bUL, 0xc24b8b70UL, 0xc76c51a3UL, 0xd192e819UL, 0xd6990624UL, 0xf40e3585UL, 0x106aa070UL, + 0x19a4c116UL, 0x1e376c08UL, 0x2748774cUL, 0x34b0bcb5UL, 0x391c0cb3UL, 0x4ed8aa4aUL, 0x5b9cca4fUL, 0x682e6ff3UL, + 0x748f82eeUL, 0x78a5636fUL, 0x84c87814UL, 0x8cc70208UL, 0x90befffaUL, 0xa4506cebUL, 0xbef9a3f7UL, 0xc67178f2UL}; + uint32_t S[8], W[64], t, t0, t1; + int i; + + /* copy state into S */ + for (i = 0; i < 8; i++) + S[i] = ctx->state[i]; + + /* copy the state into 512-bits into W[0..15] */ + for (i = 0; i < 16; i++) + W[i] = + (uint32_t)buf[4 * i] << 24 | (uint32_t)buf[4 * i + 1] << 16 | (uint32_t)buf[4 * i + 2] << 8 | (uint32_t)buf[4 * i + 3]; + + /* fill W[16..63] */ + for (i = 16; i < 64; i++) + W[i] = _picohash_sha256_gamma1(W[i - 2]) + W[i - 7] + _picohash_sha256_gamma0(W[i - 15]) + W[i - 16]; + + /* Compress */ + for (i = 0; i < 64; ++i) { + _picohash_sha256_rnd(S[0], S[1], S[2], S[3], S[4], S[5], S[6], S[7], i); + t = S[7]; + S[7] = S[6]; + S[6] = S[5]; + S[5] = S[4]; + S[4] = S[3]; + S[3] = S[2]; + S[2] = S[1]; + S[1] = S[0]; + S[0] = t; + } + + /* feedback */ + for (i = 0; i < 8; i++) + ctx->state[i] = ctx->state[i] + S[i]; +} + +static inline void _picohash_sha256_do_final(_picohash_sha256_ctx_t *ctx, void *digest, size_t len) +{ + unsigned char *out = digest; + size_t i; + + /* increase the length of the message */ + ctx->length += ctx->curlen * 8; + + /* append the '1' bit */ + ctx->buf[ctx->curlen++] = (unsigned char)0x80; + + /* if the length is currently above 56 bytes we append zeros + * then compress. Then we can fall back to padding zeros and length + * encoding like normal. + */ + if (ctx->curlen > 56) { + while (ctx->curlen < 64) { + ctx->buf[ctx->curlen++] = (unsigned char)0; + } + _picohash_sha256_compress(ctx, ctx->buf); + ctx->curlen = 0; + } + + /* pad upto 56 bytes of zeroes */ + while (ctx->curlen < 56) { + ctx->buf[ctx->curlen++] = (unsigned char)0; + } + + /* store length */ + for (i = 0; i != 8; ++i) + ctx->buf[56 + i] = (unsigned char)(ctx->length >> (56 - 8 * i)); + _picohash_sha256_compress(ctx, ctx->buf); + + /* copy output */ + for (i = 0; i != len / 4; ++i) { + out[i * 4] = ctx->state[i] >> 24; + out[i * 4 + 1] = ctx->state[i] >> 16; + out[i * 4 + 2] = ctx->state[i] >> 8; + out[i * 4 + 3] = ctx->state[i]; + } +} + +inline void _picohash_sha256_init(_picohash_sha256_ctx_t *ctx) +{ + ctx->curlen = 0; + ctx->length = 0; + ctx->state[0] = 0x6A09E667UL; + ctx->state[1] = 0xBB67AE85UL; + ctx->state[2] = 0x3C6EF372UL; + ctx->state[3] = 0xA54FF53AUL; + ctx->state[4] = 0x510E527FUL; + ctx->state[5] = 0x9B05688CUL; + ctx->state[6] = 0x1F83D9ABUL; + ctx->state[7] = 0x5BE0CD19UL; +} + +inline void _picohash_sha256_update(_picohash_sha256_ctx_t *ctx, const void *data, size_t len) +{ + const unsigned char *in = data; + size_t n; + + while (len > 0) { + if (ctx->curlen == 0 && len >= PICOHASH_SHA256_BLOCK_LENGTH) { + _picohash_sha256_compress(ctx, (unsigned char *)in); + ctx->length += PICOHASH_SHA256_BLOCK_LENGTH * 8; + in += PICOHASH_SHA256_BLOCK_LENGTH; + len -= PICOHASH_SHA256_BLOCK_LENGTH; + } else { + n = PICOHASH_SHA256_BLOCK_LENGTH - ctx->curlen; + if (n > len) + n = len; + memcpy(ctx->buf + ctx->curlen, in, n); + ctx->curlen += (uint32_t)n; + in += n; + len -= n; + if (ctx->curlen == 64) { + _picohash_sha256_compress(ctx, ctx->buf); + ctx->length += 8 * PICOHASH_SHA256_BLOCK_LENGTH; + ctx->curlen = 0; + } + } + } +} + +inline void _picohash_sha256_final(_picohash_sha256_ctx_t *ctx, void *digest) +{ + _picohash_sha256_do_final(ctx, digest, PICOHASH_SHA256_DIGEST_LENGTH); +} + +inline void _picohash_sha224_init(_picohash_sha256_ctx_t *ctx) +{ + ctx->curlen = 0; + ctx->length = 0; + ctx->state[0] = 0xc1059ed8UL; + ctx->state[1] = 0x367cd507UL; + ctx->state[2] = 0x3070dd17UL; + ctx->state[3] = 0xf70e5939UL; + ctx->state[4] = 0xffc00b31UL; + ctx->state[5] = 0x68581511UL; + ctx->state[6] = 0x64f98fa7UL; + ctx->state[7] = 0xbefa4fa4UL; +} + +inline void _picohash_sha224_final(_picohash_sha256_ctx_t *ctx, void *digest) +{ + _picohash_sha256_do_final(ctx, digest, PICOHASH_SHA224_DIGEST_LENGTH); +} + +inline void picohash_init_md5(picohash_ctx_t *ctx) +{ + ctx->block_length = PICOHASH_MD5_BLOCK_LENGTH; + ctx->digest_length = PICOHASH_MD5_DIGEST_LENGTH; + ctx->_reset = (void *)_picohash_md5_init; + ctx->_update = (void *)_picohash_md5_update; + ctx->_final = (void *)_picohash_md5_final; + + _picohash_md5_init(&ctx->_md5); +} + +inline void picohash_init_sha1(picohash_ctx_t *ctx) +{ + ctx->block_length = PICOHASH_SHA1_BLOCK_LENGTH; + ctx->digest_length = PICOHASH_SHA1_DIGEST_LENGTH; + ctx->_reset = (void *)_picohash_sha1_init; + ctx->_update = (void *)_picohash_sha1_update; + ctx->_final = (void *)_picohash_sha1_final; + _picohash_sha1_init(&ctx->_sha1); +} + +inline void picohash_init_sha224(picohash_ctx_t *ctx) +{ + ctx->block_length = PICOHASH_SHA224_BLOCK_LENGTH; + ctx->digest_length = PICOHASH_SHA224_DIGEST_LENGTH; + ctx->_reset = (void *)_picohash_sha224_init; + ctx->_update = (void *)_picohash_sha256_update; + ctx->_final = (void *)_picohash_sha224_final; + _picohash_sha224_init(&ctx->_sha256); +} + +inline void picohash_init_sha256(picohash_ctx_t *ctx) +{ + ctx->block_length = PICOHASH_SHA256_BLOCK_LENGTH; + ctx->digest_length = PICOHASH_SHA256_DIGEST_LENGTH; + ctx->_reset = (void *)_picohash_sha256_init; + ctx->_update = (void *)_picohash_sha256_update; + ctx->_final = (void *)_picohash_sha256_final; + _picohash_sha256_init(&ctx->_sha256); +} + +inline void picohash_update(picohash_ctx_t *ctx, const void *input, size_t len) +{ + ctx->_update(ctx, input, len); +} + +inline void picohash_final(picohash_ctx_t *ctx, void *digest) +{ + ctx->_final(ctx, digest); +} + +inline void picohash_reset(picohash_ctx_t *ctx) +{ + ctx->_reset(ctx); +} + +static inline void _picohash_hmac_apply_key(picohash_ctx_t *ctx, unsigned char delta) +{ + size_t i; + for (i = 0; i != ctx->block_length; ++i) + ctx->_hmac.key[i] ^= delta; + picohash_update(ctx, ctx->_hmac.key, ctx->block_length); + for (i = 0; i != ctx->block_length; ++i) + ctx->_hmac.key[i] ^= delta; +} + +static void _picohash_hmac_final(picohash_ctx_t *ctx, void *digest) +{ + unsigned char inner_digest[PICOHASH_MAX_DIGEST_LENGTH]; + + ctx->_hmac.hash_final(ctx, inner_digest); + + ctx->_hmac.hash_reset(ctx); + _picohash_hmac_apply_key(ctx, 0x5c); + picohash_update(ctx, inner_digest, ctx->digest_length); + memset(inner_digest, 0, ctx->digest_length); + + ctx->_hmac.hash_final(ctx, digest); +} + +static inline void _picohash_hmac_reset(picohash_ctx_t *ctx) +{ + ctx->_hmac.hash_reset(ctx); + _picohash_hmac_apply_key(ctx, 0x36); +} + +inline void picohash_init_hmac(picohash_ctx_t *ctx, void (*initf)(picohash_ctx_t *), const void *key, size_t key_len) +{ + initf(ctx); + + memset(ctx->_hmac.key, 0, ctx->block_length); + if (key_len > ctx->block_length) { + /* hash the key if it is too long */ + picohash_update(ctx, key, key_len); + picohash_final(ctx, ctx->_hmac.key); + ctx->_hmac.hash_reset(ctx); + } else { + memcpy(ctx->_hmac.key, key, key_len); + } + + /* replace reset and final function */ + ctx->_hmac.hash_reset = ctx->_reset; + ctx->_hmac.hash_final = ctx->_final; + ctx->_reset = (void *)_picohash_hmac_reset; + ctx->_final = (void *)_picohash_hmac_final; + + /* start calculating the inner hash */ + _picohash_hmac_apply_key(ctx, 0x36); +} + +#endif diff --git a/thirdparty/libjuice/src/random.c b/thirdparty/libjuice/src/random.c new file mode 100644 index 0000000..46bd8f8 --- /dev/null +++ b/thirdparty/libjuice/src/random.c @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "random.h" +#include "log.h" +#include "thread.h" // for mutexes + +#include +#include +#include + +// getrandom() is not available in Android NDK API < 28 and needs glibc >= 2.25 +#if defined(__linux__) && !defined(__ANDROID__) && (!defined(__GLIBC__) || __GLIBC__ > 2 || __GLIBC_MINOR__ >= 25) + +#include +#include + +static int random_bytes(void *buf, size_t size) { + ssize_t ret = getrandom(buf, size, 0); + if (ret < 0) { + JLOG_WARN("getrandom failed, errno=%d", errno); + return -1; + } + if ((size_t)ret < size) { + JLOG_WARN("getrandom returned too few bytes, size=%zu, returned=%zu", size, (size_t)ret); + return -1; + } + return 0; +} + +#elif defined(_WIN32) + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 // Windows 7 +#endif + +#include +// +#include + +static int random_bytes(void *buf, size_t size) { + // Requires Windows 7 or later + NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)size, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + return !status ? 0 : -1; +} + +#else +static int random_bytes(void *buf, size_t size) { + (void)buf; + (void)size; + return -1; +} +#endif + +static unsigned int generate_seed() { +#ifdef _WIN32 + return (unsigned int)GetTickCount(); +#else + struct timespec ts; + if (clock_gettime(CLOCK_REALTIME, &ts) == 0) + return (unsigned int)(ts.tv_sec ^ ts.tv_nsec); + else + return (unsigned int)time(NULL); +#endif +} + +void juice_random(void *buf, size_t size) { + if (random_bytes(buf, size) == 0) + return; + + // rand() is not thread-safe + static mutex_t rand_mutex = MUTEX_INITIALIZER; + mutex_lock(&rand_mutex); + + static bool srandom_called = false; +#if defined(__linux__) || defined(__unix__) || defined(__APPLE__) +#define random_func random +#define srandom_func srandom + if (!srandom_called) + JLOG_DEBUG("Using random() for random bytes"); +#else +#define random_func rand +#define srandom_func srand + if (!srandom_called) + JLOG_WARN("Falling back on rand() for random bytes"); +#endif + if (!srandom_called) { + srandom_func(generate_seed()); + srandom_called = true; + } + // RAND_MAX is guaranteed to be at least 2^15 - 1 + uint8_t *bytes = buf; + for (size_t i = 0; i < size; ++i) + bytes[i] = (uint8_t)((random_func() & 0x7f80) >> 7); + + mutex_unlock(&rand_mutex); +} + +void juice_random_str64(char *buf, size_t size) { + static const char chars64[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i = 0; + for (i = 0; i + 1 < size; ++i) { + uint8_t byte = 0; + juice_random(&byte, 1); + buf[i] = chars64[byte & 0x3F]; + } + buf[i] = '\0'; +} + +uint32_t juice_rand32(void) { + uint32_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} + +uint64_t juice_rand64(void) { + uint64_t r = 0; + juice_random(&r, sizeof(r)); + return r; +} diff --git a/thirdparty/libjuice/src/random.h b/thirdparty/libjuice/src/random.h new file mode 100644 index 0000000..2985d87 --- /dev/null +++ b/thirdparty/libjuice/src/random.h @@ -0,0 +1,21 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_RANDOM_H +#define JUICE_RANDOM_H + +#include +#include + +void juice_random(void *buf, size_t size); +void juice_random_str64(char *buf, size_t size); + +uint32_t juice_rand32(void); +uint64_t juice_rand64(void); + +#endif // JUICE_RANDOM_H diff --git a/thirdparty/libjuice/src/server.c b/thirdparty/libjuice/src/server.c new file mode 100644 index 0000000..3dcfaf5 --- /dev/null +++ b/thirdparty/libjuice/src/server.c @@ -0,0 +1,1143 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef NO_SERVER + +#include "server.h" +#include "const_time.h" +#include "hmac.h" +#include "ice.h" +#include "juice.h" +#include "log.h" +#include "random.h" +#include "stun.h" +#include "turn.h" +#include "udp.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +#define ALLOCATION_LIFETIME 600000 // ms + +// RFC 8656: The Permission Lifetime MUST be 300 seconds (= 5 minutes) +#define PERMISSION_LIFETIME 300000 // ms + +// RFC 8656: Channel bindings last for 10 minutes unless refreshed +#define BIND_LIFETIME 600000 // ms + +#define MAX_RELAYED_RECORDS_COUNT 8 +#define BUFFER_SIZE 4096 + +static char *alloc_string_copy(const char *orig, bool *alloc_failed) { + if (!orig) + return NULL; + + char *copy = malloc(strlen(orig) + 1); + if (!copy) { + if (alloc_failed) + *alloc_failed = true; + + return NULL; + } + strcpy(copy, orig); + return copy; +} + +static server_turn_alloc_t *find_allocation(server_turn_alloc_t allocs[], int size, + const addr_record_t *record, bool allow_deleted) { + unsigned long key = addr_record_hash(record, true) % size; + unsigned long pos = key; + while (!(allocs[pos].state == SERVER_TURN_ALLOC_EMPTY || + (allow_deleted && allocs[pos].state == SERVER_TURN_ALLOC_DELETED) || + addr_record_is_equal(&allocs[pos].record, record, true))) { + pos = (pos + 1) % size; + if (pos == key) { + JLOG_VERBOSE("TURN allocation map is full"); + return NULL; + } + } + return allocs + pos; +} + +static void delete_allocation(server_turn_alloc_t *alloc) { + if (alloc->state != SERVER_TURN_ALLOC_FULL) + return; + + ++alloc->credentials->allocations_quota; + + alloc->state = SERVER_TURN_ALLOC_DELETED; + turn_destroy_map(&alloc->map); + closesocket(alloc->sock); + alloc->sock = INVALID_SOCKET; + alloc->credentials = NULL; +} + +static thread_return_t THREAD_CALL server_thread_entry(void *arg) { + server_run((juice_server_t *)arg); + return (thread_return_t)0; +} + +juice_server_t *server_create(const juice_server_config_t *config) { + JLOG_VERBOSE("Creating server"); + +#ifdef _WIN32 + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData)) { + JLOG_FATAL("WSAStartup failed"); + return NULL; + } +#endif + + juice_server_t *server = calloc(1, sizeof(juice_server_t)); + if (!server) { + JLOG_FATAL("Memory allocation for server data failed"); + return NULL; + } + + udp_socket_config_t socket_config; + memset(&socket_config, 0, sizeof(socket_config)); + socket_config.bind_address = config->bind_address; + socket_config.port_begin = config->port; + socket_config.port_end = config->port; + + server->sock = udp_create_socket(&socket_config); + if (server->sock == INVALID_SOCKET) { + JLOG_FATAL("Server socket opening failed"); + free(server); + return NULL; + } + + mutex_init(&server->mutex, MUTEX_RECURSIVE); + + bool alloc_failed = false; + server->config.max_allocations = + config->max_allocations > 0 ? config->max_allocations : SERVER_DEFAULT_MAX_ALLOCATIONS; + server->config.max_peers = config->max_peers; + server->config.bind_address = alloc_string_copy(config->bind_address, &alloc_failed); + server->config.external_address = alloc_string_copy(config->external_address, &alloc_failed); + server->config.port = config->port; + server->config.relay_port_range_begin = config->relay_port_range_begin; + server->config.relay_port_range_end = config->relay_port_range_end; + server->config.realm = alloc_string_copy( + config->realm && *config->realm != '\0' ? config->realm : SERVER_DEFAULT_REALM, + &alloc_failed); + if (alloc_failed) { + JLOG_FATAL("Memory allocation for server configuration failed"); + goto error; + } + + // Don't copy credentials but process them + server->config.credentials = NULL; + server->config.credentials_count = 0; + if (config->credentials_count <= 0) { + // TURN disabled + JLOG_INFO("TURN relaying disabled, STUN-only mode"); + server->allocs = NULL; + server->allocs_count = 0; + + } else { + // TURN enabled + server->allocs = calloc(server->config.max_allocations, sizeof(server_turn_alloc_t)); + if (!server->allocs) { + JLOG_FATAL("Memory allocation for TURN allocation table failed"); + goto error; + } + server->allocs_count = (int)server->config.max_allocations; + + for (int i = 0; i < config->credentials_count; ++i) { + juice_server_credentials_t *credentials = config->credentials + i; + if (server->config.max_allocations < credentials->allocations_quota) + server->config.max_allocations = credentials->allocations_quota; + + if (!server_do_add_credentials(server, credentials, 0)) { // never expires + JLOG_FATAL("Failed to add TURN credentials"); + goto error; + } + } + + juice_credentials_list_t *node = server->credentials; + while (node) { + juice_server_credentials_t *credentials = &node->credentials; + if (credentials->allocations_quota == 0) // unlimited + credentials->allocations_quota = server->config.max_allocations; + + node = node->next; + } + } + + server->config.port = udp_get_port(server->sock); + server->nonce_key_timestamp = 0; + if (server->config.max_peers == 0) + server->config.max_peers = SERVER_DEFAULT_MAX_PEERS; + + if (server->config.bind_address) + JLOG_INFO("Created server on %s:%hu", server->config.bind_address, server->config.port); + else + JLOG_INFO("Created server on port %hu", server->config.port); + + int ret = thread_init(&server->thread, server_thread_entry, server); + if (ret) { + JLOG_FATAL("Thread creation failed, error=%d", ret); + goto error; + } + + return server; + +error: + server_do_destroy(server); + return NULL; +} + +void server_do_destroy(juice_server_t *server) { + JLOG_DEBUG("Destroying server"); + + closesocket(server->sock); + mutex_destroy(&server->mutex); + + server_turn_alloc_t *end = server->allocs + server->allocs_count; + for (server_turn_alloc_t *alloc = server->allocs; alloc < end; ++alloc) { + delete_allocation(alloc); + } + free((void *)server->allocs); + + juice_credentials_list_t *node = server->credentials; + while (node) { + juice_credentials_list_t *prev = node; + node = node->next; + free((void *)prev->credentials.username); + free((void *)prev->credentials.password); + free(prev); + } + + free((void *)server->config.bind_address); + free((void *)server->config.external_address); + free((void *)server->config.realm); + free(server); + +#ifdef _WIN32 + WSACleanup(); +#endif + JLOG_VERBOSE("Destroyed server"); +} + +void server_destroy(juice_server_t *server) { + mutex_lock(&server->mutex); + + JLOG_VERBOSE("Waiting for server thread"); + server->thread_stopped = true; + mutex_unlock(&server->mutex); + server_interrupt(server); + thread_join(server->thread, NULL); + + server_do_destroy(server); +} + +uint16_t server_get_port(juice_server_t *server) { + mutex_lock(&server->mutex); + uint16_t port = server->config.port; // updated at creation + mutex_unlock(&server->mutex); + return port; +} + +int server_add_credentials(juice_server_t *server, const juice_server_credentials_t *credentials, + timediff_t lifetime) { + mutex_lock(&server->mutex); + + if (server->config.max_allocations < credentials->allocations_quota) + server->config.max_allocations = credentials->allocations_quota; + + if (server->allocs_count < (int)server->config.max_allocations) { + if (server->allocs_count == 0) + JLOG_INFO("Enabling TURN relaying"); + + server_turn_alloc_t *reallocated = + realloc(server->allocs, server->config.max_allocations * sizeof(server_turn_alloc_t)); + if (!reallocated) { + JLOG_ERROR("Memory allocation for TURN allocation table failed"); + mutex_unlock(&server->mutex); + return -1; + } + memset(reallocated + server->allocs_count, 0, + ((int)server->config.max_allocations - server->allocs_count) * + sizeof(server_turn_alloc_t)); + server->allocs_count = (int)server->config.max_allocations; + server->allocs = reallocated; + } + + juice_credentials_list_t *node = server_do_add_credentials(server, credentials, lifetime); + if (!node) { + mutex_unlock(&server->mutex); + return -1; + } + + if (node->credentials.allocations_quota == 0) // unlimited + node->credentials.allocations_quota = server->config.max_allocations; + + mutex_unlock(&server->mutex); + return 0; +} + +juice_credentials_list_t *server_do_add_credentials(juice_server_t *server, + const juice_server_credentials_t *credentials, + timediff_t lifetime) { + juice_credentials_list_t *node = calloc(1, sizeof(juice_credentials_list_t)); + if (!node) { + JLOG_ERROR("Memory allocation for TURN credentials failed"); + goto error; + } + + bool alloc_failed = false; + node->credentials.username = + alloc_string_copy(credentials->username ? credentials->username : "", &alloc_failed); + node->credentials.password = + alloc_string_copy(credentials->password ? credentials->password : "", &alloc_failed); + node->credentials.allocations_quota = credentials->allocations_quota; + if (alloc_failed) { + JLOG_ERROR("Memory allocation for TURN credentials failed"); + goto error; + } + + stun_compute_userhash(node->credentials.username, server->config.realm, node->userhash); + + if (lifetime > 0) + node->timestamp = current_timestamp() + lifetime; + else + node->timestamp = 0; // never expires + + node->next = server->credentials; + server->credentials = node; + return server->credentials; + +error: + if (node) { + free((void *)node->credentials.username); + free((void *)node->credentials.password); + free(node); + } + return NULL; +} + +void server_run(juice_server_t *server) { + mutex_lock(&server->mutex); + nfds_t nfd = 0; + struct pollfd *pfd = NULL; + + // Main loop + timestamp_t next_timestamp; + while (server_bookkeeping(server, &next_timestamp) == 0) { + timediff_t timediff = next_timestamp - current_timestamp(); + if (timediff < 0) + timediff = 0; + + if (!pfd || nfd != (nfds_t)(1 + server->allocs_count)) { + free(pfd); + nfd = (nfds_t)(1 + server->allocs_count); + pfd = calloc(nfd, sizeof(struct pollfd)); + if (!pfd) { + JLOG_FATAL("Memory allocation for poll descriptors failed"); + break; + } + } + + pfd[0].fd = server->sock; + pfd[0].events = POLLIN; + + for (int i = 0; i < server->allocs_count; ++i) { + server_turn_alloc_t *alloc = server->allocs + i; + if (alloc->state == SERVER_TURN_ALLOC_FULL) { + pfd[1 + i].fd = alloc->sock; + pfd[1 + i].events = POLLIN; + } else { + pfd[1 + i].fd = -1; // ignore + } + } + + JLOG_VERBOSE("Entering poll for %d ms", (int)timediff); + mutex_unlock(&server->mutex); + int ret = poll(pfd, nfd, (int)timediff); + mutex_lock(&server->mutex); + JLOG_VERBOSE("Leaving poll"); + if (ret < 0) { + if (sockerrno == SEINTR || sockerrno == SEAGAIN) { + JLOG_VERBOSE("poll interrupted"); + continue; + } else { + JLOG_FATAL("poll failed, errno=%d", sockerrno); + break; + } + } + + if (server->thread_stopped) { + JLOG_VERBOSE("Server destruction requested"); + break; + } + + if (pfd[0].revents & POLLNVAL || pfd[0].revents & POLLERR) { + JLOG_FATAL("Error when polling server socket"); + break; + } + + if (pfd[0].revents & POLLIN) { + if (server_recv(server) < 0) + break; + } + + for (int i = 0; i < server->allocs_count; ++i) { + server_turn_alloc_t *alloc = server->allocs + i; + if (alloc->state == SERVER_TURN_ALLOC_FULL && pfd[1 + i].revents & POLLIN) + server_forward(server, alloc); + } + } + + JLOG_DEBUG("Leaving server thread"); + free(pfd); + mutex_unlock(&server->mutex); +} + +int server_send(juice_server_t *server, const addr_record_t *dst, const char *data, size_t size) { + JLOG_VERBOSE("Sending datagram, size=%d", size); + + int ret = udp_sendto(server->sock, data, size, dst); + if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) + JLOG_WARN("Send failed, errno=%d", sockerrno); + + return ret; +} + +int server_stun_send(juice_server_t *server, const addr_record_t *dst, const stun_message_t *msg, + const char *password) { + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, msg, password); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + + if (server_send(server, dst, buffer, size) < 0) { + JLOG_WARN("STUN message send failed, errno=%d", sockerrno); + return -1; + } + return 0; +} + +int server_recv(juice_server_t *server) { + JLOG_VERBOSE("Receiving datagrams"); + while (true) { + char buffer[BUFFER_SIZE]; + addr_record_t record; + int len = udp_recvfrom(server->sock, buffer, BUFFER_SIZE, &record); + if (len < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) { + JLOG_VERBOSE("No more datagrams to receive"); + break; + } + JLOG_ERROR("recvfrom failed, errno=%d", sockerrno); + return -1; + } + if (len == 0) { + // Empty datagram (used to interrupt) + continue; + } + + addr_unmap_inet6_v4mapped((struct sockaddr *)&record.addr, &record.len); + server_input(server, buffer, len, &record); + } + + return 0; +} + +int server_forward(juice_server_t *server, server_turn_alloc_t *alloc) { + JLOG_VERBOSE("Forwarding datagrams"); + while (true) { + char buffer[BUFFER_SIZE]; + addr_record_t record; + int len = udp_recvfrom(alloc->sock, buffer, BUFFER_SIZE, &record); + if (len < 0) { + if (sockerrno == SEAGAIN || sockerrno == SEWOULDBLOCK) { + break; + } + JLOG_WARN("recvfrom failed, errno=%d", sockerrno); + return -1; + } + addr_unmap_inet6_v4mapped((struct sockaddr *)&record.addr, &record.len); + + uint16_t channel; + if (turn_get_bound_channel(&alloc->map, &record, &channel)) { + // Use ChannelData + len = turn_wrap_channel_data(buffer, BUFFER_SIZE, buffer, len, channel); + if (len <= 0) { + JLOG_ERROR("TURN ChannelData wrapping failed"); + return -1; + } + + JLOG_VERBOSE("Forwarding as ChannelData, size=%d", len); + + int ret = udp_sendto(server->sock, buffer, len, &alloc->record); + if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) + JLOG_WARN("Send failed, errno=%d", sockerrno); + + return ret; + + } else { + // Use TURN Data indication + JLOG_VERBOSE("Forwarding as TURN Data indication"); + + stun_message_t msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_class = STUN_CLASS_INDICATION; + msg.msg_method = STUN_METHOD_DATA; + msg.peer = record; + msg.data = buffer; + msg.data_size = len; + juice_random(msg.transaction_id, STUN_TRANSACTION_ID_SIZE); + + return server_stun_send(server, &alloc->record, &msg, NULL); + } + } + + return 0; +} + +int server_input(juice_server_t *server, char *buf, size_t len, const addr_record_t *src) { + JLOG_VERBOSE("Received datagram, size=%d", len); + + if (is_stun_datagram(buf, len)) { + if (JLOG_DEBUG_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Received STUN datagram from %s", src_str); + } + stun_message_t msg; + if (stun_read(buf, len, &msg) < 0) { + JLOG_ERROR("STUN message reading failed"); + return -1; + } + return server_dispatch_stun(server, buf, len, &msg, src); + } + + if (is_channel_data(buf, len)) { + if (JLOG_DEBUG_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + JLOG_DEBUG("Received ChannelData datagram from %s", src_str); + } + return server_process_channel_data(server, buf, len, src); + } + + if (JLOG_WARN_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + JLOG_WARN("Received unexpected non-STUN datagram from %s, ignoring", src_str); + } + return -1; +} + +int server_interrupt(juice_server_t *server) { + JLOG_VERBOSE("Interrupting server thread"); + mutex_lock(&server->mutex); + if (server->sock == INVALID_SOCKET) { + mutex_unlock(&server->mutex); + return -1; + } + + if (udp_sendto_self(server->sock, NULL, 0) < 0) { + if (sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) { + JLOG_WARN("Failed to interrupt thread by triggering socket, errno=%d", sockerrno); + mutex_unlock(&server->mutex); + return -1; + } + } + + mutex_unlock(&server->mutex); + return 0; +} + +int server_bookkeeping(juice_server_t *server, timestamp_t *next_timestamp) { + timestamp_t now = current_timestamp(); + *next_timestamp = now + 60000; + + // Handle allocations + for (int i = 0; i < server->allocs_count; ++i) { + server_turn_alloc_t *alloc = server->allocs + i; + if (alloc->state != SERVER_TURN_ALLOC_FULL) + continue; + + if (alloc->timestamp <= now) { + JLOG_DEBUG("Allocation timed out"); + delete_allocation(alloc); + continue; + } + + if (alloc->timestamp < *next_timestamp) + *next_timestamp = alloc->timestamp; + } + + // Handle credentials + juice_credentials_list_t **pnode = &server->credentials; // We are deleting some elements + while (*pnode) { + if ((*pnode)->timestamp && (*pnode)->timestamp <= now) { + JLOG_DEBUG("Credentials timed out"); + juice_credentials_list_t *next = (*pnode)->next; + free((void *)(*pnode)->credentials.username); + free((void *)(*pnode)->credentials.password); + free((*pnode)); + *pnode = next; + continue; + } + + pnode = &(*pnode)->next; + } + + return 0; +} + +void server_get_nonce(juice_server_t *server, const addr_record_t *src, char *nonce) { + timestamp_t now = current_timestamp(); + if (now >= server->nonce_key_timestamp) { + juice_random(server->nonce_key, SERVER_NONCE_KEY_SIZE); + server->nonce_key_timestamp = now + SERVER_NONCE_KEY_LIFETIME; + } + + uint8_t digest[HMAC_SHA256_SIZE]; + hmac_sha256(&src->addr, src->len, server->nonce_key, SERVER_NONCE_KEY_SIZE, digest); + + size_t len = HMAC_SHA256_SIZE; + if (len > STUN_MAX_NONCE_LEN) + len = STUN_MAX_NONCE_LEN; + + // RFC 4648 base64url character table + const char *table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; + for (size_t i = 0; i < len; ++i) + nonce[i] = table[digest[i] % 64]; + + nonce[len] = '\0'; + + stun_prepend_nonce_cookie(nonce); +} + +void server_prepare_credentials(juice_server_t *server, const addr_record_t *src, + const juice_server_credentials_t *credentials, + stun_message_t *msg) { + snprintf(msg->credentials.realm, STUN_MAX_REALM_LEN, "%s", server->config.realm); + server_get_nonce(server, src, msg->credentials.nonce); + + if (credentials) + snprintf(msg->credentials.username, STUN_MAX_USERNAME_LEN, "%s", credentials->username); +} + +int server_dispatch_stun(juice_server_t *server, void *buf, size_t size, stun_message_t *msg, + const addr_record_t *src) { + + if (!(msg->msg_class == STUN_CLASS_REQUEST || + (msg->msg_class == STUN_CLASS_INDICATION && + (msg->msg_method == STUN_METHOD_BINDING || msg->msg_method == STUN_METHOD_SEND)))) { + JLOG_WARN("Unexpected STUN message, class=0x%X, method=0x%X", msg->msg_class, + msg->msg_method); + return -1; + } + + if (server->allocs_count == 0 && msg->msg_method != STUN_METHOD_BINDING) { + // TURN support is disabled + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 400, // Bad request + NULL); + } + + if (msg->error_code == STUN_ERROR_INTERNAL_VALIDATION_FAILED) { + if (msg->msg_class == STUN_CLASS_REQUEST) { + JLOG_WARN("Invalid STUN message, answering bad request error response"); + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 400, // Bad request + NULL); + } else { + JLOG_WARN("Invalid STUN message, dropping"); + return -1; + } + } + + juice_server_credentials_t *credentials = NULL; + if (msg->msg_method != STUN_METHOD_BINDING && msg->msg_class != STUN_CLASS_INDICATION) { + if (!msg->has_integrity || // + *msg->credentials.realm == '\0' || *msg->credentials.nonce == '\0' || + (*msg->credentials.username == '\0' && !msg->credentials.enable_userhash)) { + JLOG_DEBUG("Answering STUN unauthorized error response"); + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 401, // Unauthorized + NULL); // No username + } + + char nonce[STUN_MAX_NONCE_LEN]; + server_get_nonce(server, src, nonce); + if (strcmp(msg->credentials.nonce, nonce) != 0 || + strcmp(msg->credentials.realm, server->config.realm) != 0) { + JLOG_DEBUG("Answering STUN stale nonce error response"); + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 438, // Stale nonce + NULL); // No username + } + + timestamp_t now = current_timestamp(); + if (msg->credentials.enable_userhash) { + juice_credentials_list_t *node = server->credentials; + while (node) { + if ((!node->timestamp || node->timestamp > now) && + const_time_memcmp(node->userhash, msg->credentials.userhash, USERHASH_SIZE) == + 0) { + credentials = &node->credentials; + } + node = node->next; + } + + if (credentials) + snprintf(msg->credentials.username, STUN_MAX_USERNAME_LEN, "%s", + credentials->username); + else + JLOG_WARN("No credentials for userhash"); + + } else { + juice_credentials_list_t *node = server->credentials; + while (node) { + if ((!node->timestamp || node->timestamp > now) && + const_time_strcmp(node->credentials.username, msg->credentials.username) == 0) { + credentials = &node->credentials; + } + node = node->next; + } + + if (!credentials) + JLOG_WARN("No credentials for username \"%s\"", msg->credentials.username); + } + if (!credentials) { + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 401, // Unauthorized + NULL); // No username + return -1; + } + + // Check credentials + if (!stun_check_integrity(buf, size, msg, credentials->password)) { + JLOG_WARN("STUN authentication failed for username \"%s\"", msg->credentials.username); + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 401, // Unauthorized + NULL); // No username + return -1; + } + } + + switch (msg->msg_method) { + case STUN_METHOD_BINDING: + return server_process_stun_binding(server, msg, src); + + case STUN_METHOD_ALLOCATE: + case STUN_METHOD_REFRESH: + return server_process_turn_allocate(server, msg, src, credentials); + + case STUN_METHOD_CREATE_PERMISSION: + return server_process_turn_create_permission(server, msg, src, credentials); + + case STUN_METHOD_CHANNEL_BIND: + return server_process_turn_channel_bind(server, msg, src, credentials); + + case STUN_METHOD_SEND: + return server_process_turn_send(server, msg, src); + + default: + JLOG_WARN("Unknown STUN method 0x%X, ignoring", msg->msg_method); + return -1; + } +} + +int server_answer_stun_binding(juice_server_t *server, const uint8_t *transaction_id, + const addr_record_t *src) { + JLOG_DEBUG("Answering STUN Binding request"); + + stun_message_t ans; + memset(&ans, 0, sizeof(ans)); + ans.msg_class = STUN_CLASS_RESP_SUCCESS; + ans.msg_method = STUN_METHOD_BINDING; + ans.mapped = *src; + memcpy(ans.transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + + char buffer[BUFFER_SIZE]; + int size = stun_write(buffer, BUFFER_SIZE, &ans, NULL); + if (size <= 0) { + JLOG_ERROR("STUN message write failed"); + return -1; + } + + if (server_send(server, src, buffer, size) < 0) { + JLOG_WARN("STUN message send failed, errno=%d", sockerrno); + return -1; + } + + return 0; +} + +int server_answer_stun_error(juice_server_t *server, const uint8_t *transaction_id, + const addr_record_t *src, stun_method_t method, unsigned int code, + const juice_server_credentials_t *credentials) { + JLOG_DEBUG("Answering STUN error response with code %u", code); + + stun_message_t ans; + memset(&ans, 0, sizeof(ans)); + ans.msg_class = STUN_CLASS_RESP_ERROR; + ans.msg_method = method; + ans.error_code = code; + memcpy(ans.transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + + if (method != STUN_METHOD_BINDING) + server_prepare_credentials(server, src, credentials, &ans); + + return server_stun_send(server, src, &ans, credentials ? credentials->password : NULL); +} + +int server_process_stun_binding(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src) { + if (JLOG_INFO_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + JLOG_INFO("Got STUN binding from client %s", src_str); + } + + return server_answer_stun_binding(server, msg->transaction_id, src); +} + +int server_process_turn_allocate(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, + juice_server_credentials_t *credentials) { + if (msg->msg_class != STUN_CLASS_REQUEST) + return -1; + + if (msg->msg_method != STUN_METHOD_ALLOCATE && msg->msg_method != STUN_METHOD_REFRESH) + return -1; + + JLOG_DEBUG("Processing TURN Allocate request"); + + server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, true); + if (!alloc) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 486, // Allocation quota reached + credentials); + } + + if (alloc->state == SERVER_TURN_ALLOC_FULL) { + // Allocation exists + if (msg->msg_method == STUN_METHOD_ALLOCATE && + memcmp(alloc->transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE) != 0) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 437, // Allocation mismatch + credentials); + } + + if (alloc->credentials != credentials) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 441, // Wrong credentials + credentials); + } + } else { + // Allocation does not exist + if (msg->msg_method == STUN_METHOD_REFRESH) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 437, // Allocation mismatch + credentials); + } + + if (credentials->allocations_quota <= 0) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 486, // Allocation quota reached + credentials); + } + + udp_socket_config_t socket_config; + memset(&socket_config, 0, sizeof(socket_config)); + socket_config.bind_address = server->config.bind_address; + socket_config.port_begin = server->config.relay_port_range_begin; + socket_config.port_end = server->config.relay_port_range_end; + alloc->sock = udp_create_socket(&socket_config); + if (alloc->sock == INVALID_SOCKET) { + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, + credentials); + return -1; + } + if (turn_init_map(&alloc->map, server->config.max_peers) < 0) { + closesocket(alloc->sock); + alloc->sock = INVALID_SOCKET; + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, + credentials); + return -1; + } + + alloc->state = SERVER_TURN_ALLOC_FULL; + alloc->record = *src; + alloc->credentials = credentials; + + --credentials->allocations_quota; + } + + uint32_t lifetime = ALLOCATION_LIFETIME / 1000; + if (msg->lifetime_set && msg->lifetime < lifetime) + lifetime = msg->lifetime; + + alloc->timestamp = current_timestamp() + lifetime * 1000; + memcpy(alloc->transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE); + + addr_record_t records[MAX_RELAYED_RECORDS_COUNT]; + const addr_record_t *relayed = NULL; + if (lifetime == 0) { + delete_allocation(alloc); + + } else { + int count = 0; + if (server->config.external_address) { + char service[8]; + snprintf(service, 8, "%hu", udp_get_port(alloc->sock)); + count = addr_resolve(server->config.external_address, service, records, + MAX_RELAYED_RECORDS_COUNT); + if (count <= 0) { + JLOG_ERROR("Specified external address is invalid"); + goto error; + } + } else { + count = udp_get_addrs(alloc->sock, records, MAX_RELAYED_RECORDS_COUNT); + if (count <= 0) { + JLOG_ERROR("No local address found"); + goto error; + } + } + + if (count > MAX_RELAYED_RECORDS_COUNT) + count = MAX_RELAYED_RECORDS_COUNT; + + for (int i = 0; i < count; ++i) { + const addr_record_t *record = records + i; + if (record->addr.ss_family == AF_INET || !relayed) { + relayed = record; + if (record->addr.ss_family == AF_INET) + break; + } + } + + if (!relayed) { + JLOG_ERROR("No advertisable relayed address found"); + goto error; + } + + if (JLOG_INFO_ENABLED) { + char src_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(src, src_str, ADDR_MAX_STRING_LEN); + char relayed_str[ADDR_MAX_STRING_LEN]; + addr_record_to_string(relayed, relayed_str, ADDR_MAX_STRING_LEN); + JLOG_INFO("Allocated TURN relayed address %s for client %s", relayed_str, src_str); + } + } + + stun_message_t ans; + memset(&ans, 0, sizeof(ans)); + ans.msg_class = STUN_CLASS_RESP_SUCCESS; + ans.msg_method = msg->msg_method; + ans.lifetime = lifetime; + ans.lifetime_set = true; + ans.mapped = *src; + if (relayed) + ans.relayed = *relayed; + memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE); + + server_prepare_credentials(server, src, credentials, &ans); + + return server_stun_send(server, src, &ans, credentials->password); + +error: + delete_allocation(alloc); + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, credentials); + return -1; +} + +int server_process_turn_create_permission(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, + const juice_server_credentials_t *credentials) { + if (msg->msg_class != STUN_CLASS_REQUEST) + return -1; + + JLOG_DEBUG("Processing STUN CreatePermission request"); + + if (!msg->peer.len) { + JLOG_WARN("Missing peer address in TURN CreatePermission request"); + return -1; + } + + server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false); + if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 437, // Allocation mismatch + credentials); + } + if (alloc->credentials != credentials) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 441, // Wrong credentials + credentials); + } + + if (!turn_set_permission(&alloc->map, msg->transaction_id, &msg->peer, PERMISSION_LIFETIME)) { + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, + credentials); + return -1; + } + + stun_message_t ans; + memset(&ans, 0, sizeof(ans)); + ans.msg_class = STUN_CLASS_RESP_SUCCESS; + ans.msg_method = STUN_METHOD_CREATE_PERMISSION; + memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE); + + server_prepare_credentials(server, src, credentials, &ans); + + return server_stun_send(server, src, &ans, credentials->password); +} + +int server_process_turn_channel_bind(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, + const juice_server_credentials_t *credentials) { + if (msg->msg_class != STUN_CLASS_REQUEST) + return -1; + + JLOG_DEBUG("Processing STUN ChannelBind request"); + + if (!msg->peer.len) { + JLOG_WARN("Missing peer address in TURN ChannelBind request"); + return -1; + } + if (!msg->channel_number) { + JLOG_WARN("Missing channel number in TURN ChannelBind request"); + return -1; + } + + server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false); + if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 437, // Allocation mismatch + credentials); + } + if (alloc->credentials != credentials) { + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 441, // Wrong credentials + credentials); + } + + uint16_t channel = msg->channel_number; + if (!is_valid_channel(channel)) { + JLOG_WARN("TURN channel 0x%hX is invalid", channel); + return server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, + 400, // Bad request + credentials); + } + + if (!turn_bind_channel(&alloc->map, &msg->peer, msg->transaction_id, channel, BIND_LIFETIME)) { + server_answer_stun_error(server, msg->transaction_id, src, msg->msg_method, 500, + credentials); + return -1; + } + + stun_message_t ans; + memset(&ans, 0, sizeof(ans)); + ans.msg_class = STUN_CLASS_RESP_SUCCESS; + ans.msg_method = STUN_METHOD_CHANNEL_BIND; + memcpy(ans.transaction_id, msg->transaction_id, STUN_TRANSACTION_ID_SIZE); + + server_prepare_credentials(server, src, credentials, &ans); + + return server_stun_send(server, src, &ans, credentials->password); +} + +int server_process_turn_send(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src) { + if (msg->msg_class != STUN_CLASS_INDICATION) + return -1; + + JLOG_DEBUG("Processing STUN Send indication"); + + if (!msg->data) { + JLOG_WARN("Missing data in TURN Send indication"); + return -1; + } + if (!msg->peer.len) { + JLOG_WARN("Missing peer address in TURN Send indication"); + return -1; + } + + server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false); + if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) { + JLOG_WARN("Allocation mismatch for TURN Send indication"); + return -1; + } + + if (!turn_has_permission(&alloc->map, &msg->peer)) { + JLOG_WARN("No permission for peer address"); + return -1; + } + + JLOG_VERBOSE("Forwarding datagram to peer, size=%zu", msg->data_size); + + int ret = udp_sendto(alloc->sock, msg->data, msg->data_size, &msg->peer); + if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) + JLOG_WARN("Forwarding failed, errno=%d", sockerrno); + + return ret; +} + +int server_process_channel_data(juice_server_t *server, char *buf, size_t len, + const addr_record_t *src) { + server_turn_alloc_t *alloc = find_allocation(server->allocs, server->allocs_count, src, false); + if (!alloc || alloc->state != SERVER_TURN_ALLOC_FULL) { + JLOG_WARN("Allocation mismatch for TURN Channel Data"); + return -1; + } + + if (len < sizeof(struct channel_data_header)) { + JLOG_WARN("ChannelData is too short"); + return -1; + } + + const struct channel_data_header *header = (const struct channel_data_header *)buf; + buf += sizeof(struct channel_data_header); + len -= sizeof(struct channel_data_header); + uint16_t channel = ntohs(header->channel_number); + uint16_t length = ntohs(header->length); + JLOG_VERBOSE("Received ChannelData, channel=0x%hX, length=%hu", channel, length); + if (length > len) { + JLOG_WARN("ChannelData has invalid length"); + return -1; + } + len = length; + + addr_record_t record; + if (!turn_find_bound_channel(&alloc->map, channel, &record)) { + JLOG_WARN("Channel 0x%hX is not bound", channel); + return -1; + } + + JLOG_VERBOSE("Forwarding datagram to peer, size=%zu", len); + + int ret = udp_sendto(alloc->sock, buf, len, &record); + if (ret < 0 && sockerrno != SEAGAIN && sockerrno != SEWOULDBLOCK) + JLOG_WARN("Send failed, errno=%d", sockerrno); + + return 0; +} + +#endif // ifndef NO_SERVER diff --git a/thirdparty/libjuice/src/server.h b/thirdparty/libjuice/src/server.h new file mode 100644 index 0000000..a7adf10 --- /dev/null +++ b/thirdparty/libjuice/src/server.h @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_SERVER_H +#define JUICE_SERVER_H + +#ifndef NO_SERVER + +#include "addr.h" +#include "juice.h" +#include "socket.h" +#include "stun.h" +#include "thread.h" +#include "timestamp.h" +#include "turn.h" + +#include +#include + +#define SERVER_DEFAULT_REALM "libjuice" +#define SERVER_DEFAULT_MAX_ALLOCATIONS 1000 // should be 1024-1 or less to be safe for poll() +#define SERVER_DEFAULT_MAX_PEERS 16 + +#define SERVER_NONCE_KEY_SIZE 32 + +// RFC 8656: The server [...] SHOULD expire the nonce at least once every hour during the lifetime +// of the allocation +#define SERVER_NONCE_KEY_LIFETIME 600 * 1000 // 10 min + +typedef enum server_turn_alloc_state { + SERVER_TURN_ALLOC_EMPTY, + SERVER_TURN_ALLOC_DELETED, + SERVER_TURN_ALLOC_FULL +} server_turn_alloc_state_t; + +typedef struct server_turn_alloc { + server_turn_alloc_state_t state; + addr_record_t record; + juice_server_credentials_t *credentials; + uint8_t transaction_id[STUN_TRANSACTION_ID_SIZE]; + timestamp_t timestamp; + socket_t sock; + turn_map_t map; +} server_turn_alloc_t; + +typedef struct juice_credentials_list { + struct juice_credentials_list *next; + juice_server_credentials_t credentials; + uint8_t userhash[USERHASH_SIZE]; + timestamp_t timestamp; +} juice_credentials_list_t; + +typedef struct juice_server { + juice_server_config_t config; // Note config.credentials will be empty + juice_credentials_list_t *credentials; // Credentials are stored in this list + uint8_t nonce_key[SERVER_NONCE_KEY_SIZE]; + timestamp_t nonce_key_timestamp; + socket_t sock; + thread_t thread; + mutex_t mutex; + bool thread_stopped; + server_turn_alloc_t *allocs; + int allocs_count; +} juice_server_t; + +juice_server_t *server_create(const juice_server_config_t *config); +void server_do_destroy(juice_server_t *server); +void server_destroy(juice_server_t *server); + +uint16_t server_get_port(juice_server_t *server); +int server_add_credentials(juice_server_t *server, const juice_server_credentials_t *credentials, + timediff_t lifetime); + +juice_credentials_list_t *server_do_add_credentials(juice_server_t *server, + const juice_server_credentials_t *credentials, + timediff_t lifetime); // internal + +void server_run(juice_server_t *server); +int server_send(juice_server_t *agent, const addr_record_t *dst, const char *data, size_t size); +int server_stun_send(juice_server_t *server, const addr_record_t *dst, const stun_message_t *msg, + const char *password // password may be NULL +); +int server_recv(juice_server_t *server); +int server_forward(juice_server_t *server, server_turn_alloc_t *alloc); +int server_input(juice_server_t *agent, char *buf, size_t len, const addr_record_t *src); +int server_interrupt(juice_server_t *server); +int server_bookkeeping(juice_server_t *agent, timestamp_t *next_timestamp); + +void server_get_nonce(juice_server_t *server, const addr_record_t *src, char *nonce); +void server_prepare_credentials(juice_server_t *server, const addr_record_t *src, + const juice_server_credentials_t *credentials, stun_message_t *msg); + +int server_dispatch_stun(juice_server_t *server, void *buf, size_t size, stun_message_t *msg, + const addr_record_t *src); +int server_answer_stun_binding(juice_server_t *server, const uint8_t *transaction_id, + const addr_record_t *src); +int server_answer_stun_error(juice_server_t *server, const uint8_t *transaction_id, + const addr_record_t *src, stun_method_t method, unsigned int code, + const juice_server_credentials_t *credentials); + +int server_process_stun_binding(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src); +int server_process_turn_allocate(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, juice_server_credentials_t *credentials); +int server_process_turn_create_permission(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, + const juice_server_credentials_t *credentials); +int server_process_turn_channel_bind(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src, + const juice_server_credentials_t *credentials); +int server_process_turn_send(juice_server_t *server, const stun_message_t *msg, + const addr_record_t *src); +int server_process_channel_data(juice_server_t *server, char *buf, size_t len, + const addr_record_t *src); + +#endif // ifndef NO_SERVER + +#endif diff --git a/thirdparty/libjuice/src/socket.h b/thirdparty/libjuice/src/socket.h new file mode 100644 index 0000000..d126f34 --- /dev/null +++ b/thirdparty/libjuice/src/socket.h @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_SOCKET_H +#define JUICE_SOCKET_H + +#ifdef _WIN32 + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 // Windows 7 +#endif +#ifndef __MSVCRT_VERSION__ +#define __MSVCRT_VERSION__ 0x0601 +#endif + +#include +#include +// +#include +#include + +#ifdef __MINGW32__ +#include +#include +#ifndef IPV6_V6ONLY +#define IPV6_V6ONLY 27 +#endif +#endif + +#define NO_IFADDRS +#define NO_PMTUDISC + +typedef SOCKET socket_t; +typedef SOCKADDR sockaddr; +typedef ULONG ctl_t; +typedef DWORD sockopt_t; +#define sockerrno ((int)WSAGetLastError()) +#define IP_DONTFRAG IP_DONTFRAGMENT +#define HOST_NAME_MAX 256 + +#define poll WSAPoll +typedef ULONG nfds_t; + +#define SEADDRINUSE WSAEADDRINUSE +#define SEINTR WSAEINTR +#define SEAGAIN WSAEWOULDBLOCK +#define SEACCES WSAEACCES +#define SEWOULDBLOCK WSAEWOULDBLOCK +#define SEINPROGRESS WSAEINPROGRESS +#define SECONNREFUSED WSAECONNREFUSED +#define SECONNRESET WSAECONNRESET +#define SENETRESET WSAENETRESET +#define SEMSGSIZE WSAEMSGSIZE + +#else // assume POSIX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef __linux__ +#define NO_PMTUDISC +#endif + +#ifdef __ANDROID__ +#define NO_IFADDRS +#else +#include +#endif + +typedef int socket_t; +typedef int ctl_t; +typedef int sockopt_t; +#define sockerrno errno +#define INVALID_SOCKET -1 +#define ioctlsocket ioctl +#define closesocket close + +#define SEADDRINUSE EADDRINUSE +#define SEINTR EINTR +#define SEAGAIN EAGAIN +#define SEACCES EACCES +#define SEWOULDBLOCK EWOULDBLOCK +#define SEINPROGRESS EINPROGRESS +#define SECONNREFUSED ECONNREFUSED +#define SECONNRESET ECONNRESET +#define SENETRESET ENETRESET +#define SEMSGSIZE EMSGSIZE + +#endif // _WIN32 + +#ifndef IN6_IS_ADDR_LOOPBACK +#define IN6_IS_ADDR_LOOPBACK(a) \ + (((const uint32_t *)(a))[0] == 0 && ((const uint32_t *)(a))[1] == 0 && \ + ((const uint32_t *)(a))[2] == 0 && ((const uint32_t *)(a))[3] == htonl(1)) +#endif + +#ifndef IN6_IS_ADDR_LINKLOCAL +#define IN6_IS_ADDR_LINKLOCAL(a) \ + ((((const uint32_t *)(a))[0] & htonl(0xffc00000)) == htonl(0xfe800000)) +#endif + +#ifndef IN6_IS_ADDR_SITELOCAL +#define IN6_IS_ADDR_SITELOCAL(a) \ + ((((const uint32_t *)(a))[0] & htonl(0xffc00000)) == htonl(0xfec00000)) +#endif + +#ifndef IN6_IS_ADDR_V4MAPPED +#define IN6_IS_ADDR_V4MAPPED(a) \ + ((((const uint32_t *)(a))[0] == 0) && (((const uint32_t *)(a))[1] == 0) && \ + (((const uint32_t *)(a))[2] == htonl(0xFFFF))) +#endif + +#endif // JUICE_SOCKET_H diff --git a/thirdparty/libjuice/src/stun.c b/thirdparty/libjuice/src/stun.c new file mode 100644 index 0000000..f14b1d3 --- /dev/null +++ b/thirdparty/libjuice/src/stun.c @@ -0,0 +1,1236 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "stun.h" +#include "base64.h" +#include "const_time.h" +#include "crc32.h" +#include "juice.h" +#include "log.h" +#include "udp.h" + +#include +#include +#include +#include +#include +#include + +#define STUN_MAGIC 0x2112A442 +#define STUN_FINGERPRINT_XOR 0x5354554E // "STUN" +#define STUN_ATTR_SIZE sizeof(struct stun_attr) + +// STUN_MAX_PASSWORD_LEN > HASH_SHA256_SIZE > HASH_MD5_SIZE +#define MAX_HMAC_KEY_LEN STUN_MAX_PASSWORD_LEN + +#define MAX_HMAC_INPUT_LEN (STUN_MAX_USERNAME_LEN + STUN_MAX_REALM_LEN + STUN_MAX_PASSWORD_LEN + 2) + +#define MAX_USERHASH_INPUT_LEN (STUN_MAX_USERNAME_LEN + STUN_MAX_REALM_LEN + 1) + +#ifndef htonll +#define htonll(x) \ + ((uint64_t)(((uint64_t)htonl((uint32_t)(x))) << 32) | (uint64_t)htonl((uint32_t)((x) >> 32))) +#endif +#ifndef ntohll +#define ntohll(x) htonll(x) +#endif + +static size_t align32(size_t len) { + while (len & 0x03) + ++len; + return len; +} + +static size_t generate_hmac_key(const stun_message_t *msg, const char *password, void *key) { + if (*msg->credentials.realm != '\0') { + // long-term credentials + if (*msg->credentials.username == '\0') + JLOG_WARN("Generating HMAC key for long-term credentials with empty STUN username"); + + char input[MAX_HMAC_INPUT_LEN]; + int input_len = snprintf(input, MAX_HMAC_INPUT_LEN, "%s:%s:%s", msg->credentials.username, + msg->credentials.realm, password ? password : ""); + if (input_len < 0) + return 0; + + if (input_len >= MAX_HMAC_INPUT_LEN) + input_len = MAX_HMAC_INPUT_LEN - 1; + + switch (msg->credentials.password_algorithm) { + case STUN_PASSWORD_ALGORITHM_SHA256: + hash_sha256(input, input_len, key); + return HASH_SHA256_SIZE; + default: + hash_md5(input, input_len, key); + return HASH_MD5_SIZE; + } + } else { + // short-term credentials + int key_len = snprintf((char *)key, MAX_HMAC_KEY_LEN, "%s", password ? password : ""); + if (key_len < 0) + return 0; + + if (key_len >= MAX_HMAC_KEY_LEN) + key_len = MAX_HMAC_KEY_LEN - 1; + + return key_len; + } +} + +static size_t generate_password_algorithms_attr(uint8_t *attr) { + // attr size must be at least STUN_PASSWORD_ALGORITHMS_ATTR_MAX_SIZE + struct stun_value_password_algorithm *pwa = (struct stun_value_password_algorithm *)attr; + pwa->algorithm = htons(STUN_PASSWORD_ALGORITHM_SHA256); + pwa->parameters_length = 0; + ++pwa; + pwa->algorithm = htons(STUN_PASSWORD_ALGORITHM_MD5); + pwa->parameters_length = 0; + ++pwa; + return (uint8_t *)pwa - attr; +} + +int stun_write(void *buf, size_t size, const stun_message_t *msg, const char *password) { + uint8_t *begin = buf; + uint8_t *pos = begin; + uint8_t *end = begin + size; + + JLOG_VERBOSE("Writing STUN message, class=0x%X, method=0x%X", (unsigned int)msg->msg_class, + (unsigned int)msg->msg_method); + + size_t len = + stun_write_header(pos, end - pos, msg->msg_class, msg->msg_method, msg->transaction_id); + if (len <= 0) + goto overflow; + pos += len; + uint8_t *attr_begin = pos; + + if (msg->error_code) { + const char *reason = stun_get_error_reason(msg->error_code); + char buffer[sizeof(struct stun_value_error_code) + STUN_MAX_ERROR_REASON_LEN + 1]; + struct stun_value_error_code *error = (struct stun_value_error_code *)buffer; + memset(error, 0, sizeof(*error)); + error->code_class = (msg->error_code / 100) & 0x07; + error->code_number = msg->error_code % 100; + strcpy((char *)error->reason, reason); + len = stun_write_attr(pos, end - pos, STUN_ATTR_ERROR_CODE, error, + sizeof(struct stun_value_error_code) + strlen(reason)); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->mapped.len) { + JLOG_VERBOSE("Writing XOR mapped address"); + uint8_t value[32]; + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + int value_len = stun_write_value_mapped_address( + value, 32, (const struct sockaddr *)&msg->mapped.addr, msg->mapped.len, mask); + if (value_len > 0) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_MAPPED_ADDRESS, value, value_len); + if (len <= 0) + goto overflow; + pos += len; + } + } + if (msg->priority) { + uint32_t priority = htonl(msg->priority); + len = stun_write_attr(pos, end - pos, STUN_ATTR_PRIORITY, &priority, 4); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->use_candidate) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_USE_CANDIDATE, NULL, 0); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->ice_controlling) { + uint64_t ice_controlling = htonll(msg->ice_controlling); + len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLING, &ice_controlling, 8); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->ice_controlled) { + uint64_t ice_controlled = htonll(msg->ice_controlled); + len = stun_write_attr(pos, end - pos, STUN_ATTR_ICE_CONTROLLED, &ice_controlled, 8); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->channel_number) { + struct stun_value_channel_number channel_number; + memset(&channel_number, 0, sizeof(channel_number)); + channel_number.channel_number = htons(msg->channel_number); + len = stun_write_attr(pos, end - pos, STUN_ATTR_CHANNEL_NUMBER, &channel_number, + sizeof(channel_number)); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->lifetime_set || msg->lifetime) { + uint32_t lifetime = htonl(msg->lifetime); + len = stun_write_attr(pos, end - pos, STUN_ATTR_LIFETIME, &lifetime, 4); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->peer.len) { + JLOG_VERBOSE("Writing XOR peer address"); + uint8_t value[32]; + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + int value_len = stun_write_value_mapped_address( + value, 32, (const struct sockaddr *)&msg->peer.addr, msg->peer.len, mask); + if (value_len > 0) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_PEER_ADDRESS, value, value_len); + if (len <= 0) + goto overflow; + pos += len; + } + } + if (msg->relayed.len) { + JLOG_VERBOSE("Writing XOR relay address"); + uint8_t value[32]; + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + int value_len = stun_write_value_mapped_address( + value, 32, (const struct sockaddr *)&msg->relayed.addr, msg->relayed.len, mask); + if (value_len > 0) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_XOR_RELAYED_ADDRESS, value, value_len); + if (len <= 0) + goto overflow; + pos += len; + } + } + if (msg->data) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_DATA, (const uint8_t *)msg->data, + msg->data_size); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->even_port) { + struct stun_value_even_port even_port; + memset(&even_port, 0, sizeof(even_port)); + if (msg->next_port) + even_port.r |= 0x80; + len = stun_write_attr(pos, end - pos, STUN_ATTR_CHANNEL_NUMBER, &even_port, + sizeof(even_port)); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->requested_transport) { + struct stun_value_requested_transport requested_transport; + memset(&requested_transport, 0, sizeof(requested_transport)); + requested_transport.protocol = 17; + len = stun_write_attr(pos, end - pos, STUN_ATTR_REQUESTED_TRANSPORT, &requested_transport, + sizeof(requested_transport)); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->dont_fragment) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_DONT_FRAGMENT, NULL, 0); + if (len <= 0) + goto overflow; + pos += len; + } + if (msg->reservation_token) { + uint64_t reservation_token = htonll(msg->reservation_token); + len = stun_write_attr(pos, end - pos, STUN_ATTR_RESERVATION_TOKEN, &reservation_token, 8); + if (len <= 0) + goto overflow; + pos += len; + } + + const char *software = "libjuice"; + len = stun_write_attr(pos, end - pos, STUN_ATTR_SOFTWARE, software, strlen(software)); + if (len <= 0) + goto overflow; + pos += len; + + if (msg->msg_class == STUN_CLASS_REQUEST) { + if (msg->credentials.enable_userhash) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_USERHASH, msg->credentials.userhash, + USERHASH_SIZE); + if (len <= 0) + goto overflow; + pos += len; + + } else if (*msg->credentials.username != '\0') { + len = stun_write_attr(pos, end - pos, STUN_ATTR_USERNAME, msg->credentials.username, + strlen(msg->credentials.username)); + if (len <= 0) + goto overflow; + pos += len; + } + } + if (msg->msg_class == STUN_CLASS_REQUEST || + (msg->msg_class == STUN_CLASS_RESP_ERROR && + (msg->error_code == 401 || msg->error_code == 438) // Unauthenticated or Stale Nonce + )) { + if (*msg->credentials.realm != '\0') { + len = stun_write_attr(pos, end - pos, STUN_ATTR_REALM, msg->credentials.realm, + strlen(msg->credentials.realm)); + if (len <= 0) + goto overflow; + pos += len; + } + if (*msg->credentials.nonce != '\0') { + len = stun_write_attr(pos, end - pos, STUN_ATTR_NONCE, msg->credentials.nonce, + strlen(msg->credentials.nonce)); + if (len <= 0) + goto overflow; + pos += len; + + if (msg->credentials.password_algorithm > 0) { + len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHMS, + msg->credentials.password_algorithms_value, + msg->credentials.password_algorithms_value_size); + if (len <= 0) + goto overflow; + pos += len; + + } else if (msg->msg_class != STUN_CLASS_REQUEST) { + uint8_t pwa_value[STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE]; + size_t pwa_size = generate_password_algorithms_attr(pwa_value); + len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHMS, pwa_value, + pwa_size); + if (len <= 0) + goto overflow; + pos += len; + } + + if (msg->msg_class == STUN_CLASS_REQUEST && + msg->credentials.password_algorithm != STUN_PASSWORD_ALGORITHM_UNSET) { + struct stun_value_password_algorithm pwa; + pwa.algorithm = htons(msg->credentials.password_algorithm); + len = stun_write_attr(pos, end - pos, STUN_ATTR_PASSWORD_ALGORITHM, &pwa, + sizeof(pwa)); + if (len <= 0) + goto overflow; + pos += len; + } + } + } + if (msg->msg_class != STUN_CLASS_INDICATION && password) { + uint8_t key[MAX_HMAC_KEY_LEN]; + size_t key_len = generate_hmac_key(msg, password, key); + + size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA1_SIZE; + stun_update_header_length(begin, tmp_length); + + uint8_t hmac[HMAC_SHA1_SIZE]; + hmac_sha1(begin, pos - begin, key, key_len, hmac); + len = stun_write_attr(pos, end - pos, STUN_ATTR_MESSAGE_INTEGRITY, hmac, HMAC_SHA1_SIZE); + if (len <= 0) + goto overflow; + pos += len; + + // According to RFC 8489, the agent must include both MESSAGE-INTEGRITY and + // MESSAGE-INTEGRITY-SHA256. However, this makes legacy agents and servers fail with error + // 420 Unknown Attribute. Therefore, unless the password algorithm SHA-256 is enabled, only + // MESSAGE-INTEGRITY is included in the message for compatibility. + if (msg->credentials.password_algorithm != STUN_PASSWORD_ALGORITHM_UNSET) { + // If the response contains a PASSWORD-ALGORITHMS attribute, all the + // subsequent requests MUST be authenticated using MESSAGE-INTEGRITY- + // SHA256 only. + size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA256_SIZE; + stun_update_header_length(begin, tmp_length); + + uint8_t hmac[HMAC_SHA256_SIZE]; + hmac_sha256(begin, pos - begin, key, key_len, hmac); + len = stun_write_attr(pos, end - pos, STUN_ATTR_MESSAGE_INTEGRITY_SHA256, hmac, + HMAC_SHA256_SIZE); + if (len <= 0) + goto overflow; + pos += len; + } + } + + size_t length = pos - attr_begin + STUN_ATTR_SIZE + 4; + if (length & 0x03) { + JLOG_ERROR("Written STUN message length is not multiple of 4, length=%zu", length); + return -1; + } + stun_update_header_length(begin, length); + + uint32_t fingerprint = htonl(CRC32(buf, pos - begin) ^ STUN_FINGERPRINT_XOR); + len = stun_write_attr(pos, end - pos, STUN_ATTR_FINGERPRINT, &fingerprint, 4); + if (len <= 0) + goto overflow; + pos += len; + + return (int)(pos - begin); + +overflow: + JLOG_ERROR("Not enough space in buffer for STUN message, size=%zu", size); + return -1; +} + +int stun_write_header(void *buf, size_t size, stun_class_t class, stun_method_t method, + const uint8_t *transaction_id) { + if (size < sizeof(struct stun_header)) + return -1; + + uint16_t type = (uint16_t) class | (uint16_t)method; + + struct stun_header *header = buf; + header->type = htons(type); + header->length = htons(0); + header->magic = htonl(STUN_MAGIC); + memcpy(header->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + + return sizeof(struct stun_header); +} + +size_t stun_update_header_length(void *buf, size_t length) { + struct stun_header *header = buf; + size_t previous = ntohs(header->length); + header->length = htons((uint16_t)length); + return previous; +} + +int stun_write_attr(void *buf, size_t size, uint16_t type, const void *value, size_t length) { + JLOG_VERBOSE("Writing STUN attribute type 0x%X, length=%zu", (unsigned int)type, length); + + if (size < sizeof(struct stun_attr) + length) + return -1; + + struct stun_attr *attr = buf; + attr->type = htons(type); + attr->length = htons((uint16_t)length); + + if (length > 0) { + memcpy(attr->value, value, length); + + // Pad to align on 4 bytes + while (length & 0x03) + attr->value[length++] = 0; + } + + return (int)(sizeof(struct stun_attr) + length); +} + +int stun_write_value_mapped_address(void *buf, size_t size, const struct sockaddr *addr, + socklen_t addrlen, const uint8_t *mask) { + if (size < sizeof(struct stun_value_mapped_address)) + return -1; + + struct stun_value_mapped_address *value = buf; + value->padding = 0; + switch (addr->sa_family) { + case AF_INET: { + value->family = STUN_ADDRESS_FAMILY_IPV4; + if (size < sizeof(struct stun_value_mapped_address) + 4) + return -1; + if (addrlen < (socklen_t)sizeof(struct sockaddr_in)) + return -1; + JLOG_VERBOSE("Writing IPv4 address"); + const struct sockaddr_in *sin = (const struct sockaddr_in *)addr; + value->port = sin->sin_port ^ *((uint16_t *)mask); + const uint8_t *bytes = (const uint8_t *)&sin->sin_addr; + for (int i = 0; i < 4; ++i) + value->address[i] = bytes[i] ^ mask[i]; + return sizeof(struct stun_value_mapped_address) + 4; + } + case AF_INET6: { + value->family = STUN_ADDRESS_FAMILY_IPV6; + if (size < sizeof(struct stun_value_mapped_address) + 16) + return -1; + if (addrlen < (socklen_t)sizeof(struct sockaddr_in6)) + return -1; + JLOG_VERBOSE("Writing IPv6 address"); + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)addr; + value->port = sin6->sin6_port ^ *((uint16_t *)mask); + const uint8_t *bytes = (const uint8_t *)&sin6->sin6_addr; + for (int i = 0; i < 16; ++i) + value->address[i] = bytes[i] ^ mask[i]; + return sizeof(struct stun_value_mapped_address) + 16; + } + default: { + JLOG_DEBUG("Unknown address family %u", (unsigned int)addr->sa_family); + return -1; + } + } +} + +bool is_stun_datagram(const void *data, size_t size) { + // RFC 8489: The most significant 2 bits of every STUN message MUST be zeroes. This can be used + // to differentiate STUN packets from other protocols when STUN is multiplexed with other + // protocols on the same port. + if (!size || *((uint8_t *)data) & 0xC0) { + JLOG_VERBOSE("Not a STUN message: first 2 bits are not zeroes"); + return false; + } + + if (size < sizeof(struct stun_header)) { + JLOG_VERBOSE("Not a STUN message: message too short, size=%zu", size); + return false; + } + + const struct stun_header *header = data; + if (ntohl(header->magic) != STUN_MAGIC) { + JLOG_VERBOSE("Not a STUN message: magic number invalid"); + return false; + } + + // RFC 8489: The message length MUST contain the size of the message in bytes, not including the + // 20-byte STUN header. Since all STUN attributes are padded to a multiple of 4 bytes, the last + // 2 bits of this field are always zero. This provides another way to distinguish STUN packets + // from packets of other protocols. + const size_t length = ntohs(header->length); + if (length & 0x03) { + JLOG_VERBOSE("Not a STUN message: invalid length %zu not multiple of 4", length); + return false; + } + if (size != sizeof(struct stun_header) + length) { + JLOG_VERBOSE("Not a STUN message: invalid length %zu while expecting %zu", length, + size - sizeof(struct stun_header)); + return false; + } + + return true; +} + +int stun_read(void *data, size_t size, stun_message_t *msg) { + memset(msg, 0, sizeof(*msg)); + + if (size < sizeof(struct stun_header)) { + JLOG_ERROR("STUN message too short, size=%zu", size); + return -1; + } + + const struct stun_header *header = data; + const size_t length = ntohs(header->length); + if (size < sizeof(struct stun_header) + length) { + JLOG_ERROR("Invalid STUN message length, length=%zu, available=%zu", length, + size - sizeof(struct stun_header)); + return -1; + } + + uint16_t type = ntohs(header->type); + msg->msg_class = (stun_class_t)(type & STUN_CLASS_MASK); + msg->msg_method = (stun_method_t)(type & ~STUN_CLASS_MASK); + memcpy(msg->transaction_id, header->transaction_id, STUN_TRANSACTION_ID_SIZE); + JLOG_VERBOSE("Reading STUN message, class=0x%X, method=0x%X", (unsigned int)msg->msg_class, + (unsigned int)msg->msg_method); + + uint32_t security_bits = 0; + + uint8_t *begin = data; + uint8_t *attr_begin = begin + sizeof(struct stun_header); + uint8_t *end = attr_begin + length; + const uint8_t *pos = attr_begin; + while (pos < end) { + int ret = stun_read_attr(pos, end - pos, msg, begin, attr_begin, &security_bits); + if (ret <= 0) { + JLOG_DEBUG("Reading STUN attribute failed"); + return -1; + } + pos += ret; + } + + JLOG_VERBOSE("Finished reading STUN attributes"); + + stun_credentials_t *credentials = &msg->credentials; + + // RFC 8489: If the response is an error response with an error code of 401 (Unauthenticated) or + // 438 (Stale Nonce), the client MUST test if the NONCE attribute value starts with the "nonce + // cookie". If so and the "nonce cookie" has the STUN Security Feature "Password algorithms" + // bit set to 1 but no PASSWORD-ALGORITHMS attribute is present, then the client MUST NOT retry + // the request with a new transaction. See + // https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2.5 + if (msg->msg_class == STUN_CLASS_RESP_ERROR && + (msg->error_code == 401 || msg->error_code == 438) && + security_bits & STUN_SECURITY_PASSWORD_ALGORITHMS_BIT && + credentials->password_algorithms_value_size == 0) { + JLOG_INFO("STUN Security Feature \"Password algorithms\" bit is set in %u error response " + "but the corresponding attribute is missing", + msg->error_code); + msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED; // so the agent will give up + } + + // RFC 8489: If the request contains neither the PASSWORD-ALGORITHMS nor the + // PASSWORD-ALGORITHM algorithm, then the request is processed as though + // PASSWORD-ALGORITHM were MD5. + // Otherwise, unless (1) PASSWORD-ALGORITHM and PASSWORD-ALGORITHMS are both + // present, (2) PASSWORD-ALGORITHMS matches the value sent in the response that sent + // this NONCE, and (3) PASSWORD-ALGORITHM matches one of the entries in + // PASSWORD-ALGORITHMS, the server MUST generate an error response with an error code of + // 400 (Bad Request). See https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2.4 + if (!STUN_IS_RESPONSE(msg->msg_class)) { + if (credentials->password_algorithms_value_size == 0 && + credentials->password_algorithm == STUN_PASSWORD_ALGORITHM_UNSET) { + credentials->password_algorithm = STUN_PASSWORD_ALGORITHM_MD5; + + } else if (credentials->password_algorithm == STUN_PASSWORD_ALGORITHM_UNSET) { + JLOG_INFO("No suitable password algorithm in STUN request"); + msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED; + + } else if (credentials->password_algorithms_value_size == 0) { + JLOG_INFO("Missing password algorithms list in STUN request"); + msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED; + + } else { + uint8_t pwa_value[STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE]; + size_t pwa_size = generate_password_algorithms_attr(pwa_value); + if (pwa_size != credentials->password_algorithms_value_size || + memcmp(credentials->password_algorithms_value, pwa_value, pwa_size) != 0) { + JLOG_INFO("Password algorithms list is invalid in STUN request"); + msg->error_code = STUN_ERROR_INTERNAL_VALIDATION_FAILED; + } + } + } + + if (security_bits & STUN_SECURITY_USERNAME_ANONYMITY_BIT) { + JLOG_DEBUG("Remote agent supports user anonymity"); + credentials->enable_userhash = true; + } + + return (int)(sizeof(struct stun_header) + length); +} + +int stun_read_attr(const void *data, size_t size, stun_message_t *msg, uint8_t *begin, + uint8_t *attr_begin, uint32_t *security_bits) { + // RFC 8489: When present, the FINGERPRINT attribute MUST be the last attribute in the + // message and thus will appear after MESSAGE-INTEGRITY and MESSAGE-INTEGRITY-SHA256. + if (msg->has_fingerprint) { + JLOG_DEBUG("Invalid STUN attribute after fingerprint"); + return -1; + } + + if (size < sizeof(struct stun_attr)) { + JLOG_VERBOSE("STUN attribute too short"); + return -1; + } + + const struct stun_attr *attr = data; + size_t length = ntohs(attr->length); + stun_attr_type_t type = (stun_attr_type_t)ntohs(attr->type); + JLOG_VERBOSE("Reading attribute 0x%X, length=%zu", (unsigned int)type, length); + if (size < sizeof(struct stun_attr) + length) { + JLOG_DEBUG("STUN attribute length invalid, length=%zu, available=%zu", length, + size - sizeof(struct stun_attr)); + return -1; + } + + // RFC 8489: Note that agents MUST ignore all attributes that follow MESSAGE-INTEGRITY, with + // the exception of the MESSAGE-INTEGRITY-SHA256 and FINGERPRINT attributes. + if (msg->has_integrity && type != STUN_ATTR_MESSAGE_INTEGRITY && + type != STUN_ATTR_MESSAGE_INTEGRITY_SHA256 && type != STUN_ATTR_FINGERPRINT) { + JLOG_DEBUG("Ignoring STUN attribute 0x%X after message integrity", (unsigned int)type); + while (length & 0x03) + ++length; // attributes are aligned on 4 bytes + return (int)(sizeof(struct stun_attr) + length); + } + + switch (type) { + case STUN_ATTR_MAPPED_ADDRESS: { + JLOG_VERBOSE("Reading mapped address"); + uint8_t zero_mask[16] = {0}; + if (stun_read_value_mapped_address(attr->value, length, &msg->mapped, zero_mask) < 0) + return -1; + break; + } + case STUN_ATTR_XOR_MAPPED_ADDRESS: { + JLOG_VERBOSE("Reading XOR mapped address"); + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + if (stun_read_value_mapped_address(attr->value, length, &msg->mapped, mask) < 0) + return -1; + break; + } + case STUN_ATTR_ALTERNATE_SERVER: { + JLOG_VERBOSE("Reading alternate server"); + uint8_t zero_mask[16] = {0}; + if (stun_read_value_mapped_address(attr->value, length, &msg->alternate_server, zero_mask) < + 0) + return -1; + break; + } + case STUN_ATTR_ERROR_CODE: { + JLOG_VERBOSE("Reading error code"); + if (length < sizeof(struct stun_value_error_code)) { + JLOG_DEBUG("STUN error code value too short, length=%zu", length); + return -1; + } + const struct stun_value_error_code *error = + (const struct stun_value_error_code *)attr->value; + msg->error_code = (error->code_class & 0x07) * 100 + error->code_number; + + if (msg->error_code == 401 || msg->error_code == 438) { // Unauthenticated or Stale Nonce + JLOG_DEBUG("Got STUN error code %u", msg->error_code); + + } else if (JLOG_INFO_ENABLED) { + size_t reason_length = length - sizeof(struct stun_value_error_code); + if (reason_length >= STUN_MAX_ERROR_REASON_LEN) + reason_length = STUN_MAX_ERROR_REASON_LEN - 1; + + char buffer[STUN_MAX_ERROR_REASON_LEN]; + memcpy(buffer, (const char *)error->reason, reason_length); + buffer[reason_length] = '\0'; + + JLOG_INFO("Got STUN error code %u, reason \"%s\"", msg->error_code, buffer); + } + break; + } + case STUN_ATTR_UNKNOWN_ATTRIBUTES: { + JLOG_VERBOSE("Reading STUN unknown attributes"); + const uint16_t *attributes = (const uint16_t *)attr->value; + for (int i = 0; i < (int)ntohs(attr->length) / 2; ++i) { + stun_attr_type_t type = (stun_attr_type_t)ntohs(attributes[i]); + JLOG_INFO("Got unknown attribute response for attribute 0x%X", (unsigned int)type); + } + break; + } + case STUN_ATTR_USERNAME: { + JLOG_VERBOSE("Reading username"); + if (length + 1 > STUN_MAX_USERNAME_LEN) { + JLOG_WARN("STUN username attribute value too long, length=%zu", length); + return -1; + } + memcpy(msg->credentials.username, (const char *)attr->value, length); + msg->credentials.username[length] = '\0'; + JLOG_VERBOSE("Got username: %s", msg->credentials.username); + break; + } + case STUN_ATTR_MESSAGE_INTEGRITY: { + JLOG_VERBOSE("Reading message integrity"); + if (length != HMAC_SHA1_SIZE) { + JLOG_DEBUG("STUN message integrity length invalid, length=%zu", length); + return -1; + } + msg->has_integrity = true; + break; + } + case STUN_ATTR_MESSAGE_INTEGRITY_SHA256: { + JLOG_VERBOSE("Reading message integrity SHA256"); + if (length != HMAC_SHA256_SIZE) { + JLOG_DEBUG("STUN message integrity SHA256 length invalid, length=%zu", length); + return -1; + } + msg->has_integrity = true; + break; + } + case STUN_ATTR_FINGERPRINT: { + JLOG_VERBOSE("Reading fingerprint"); + if (length != 4) { + JLOG_DEBUG("STUN fingerprint length invalid, length=%zu", length); + return -1; + } + size_t tmp_length = (uint8_t *)data - attr_begin + STUN_ATTR_SIZE + 4; + size_t prev_length = stun_update_header_length(begin, tmp_length); + uint32_t expected = CRC32(begin, (uint8_t *)data - begin) ^ STUN_FINGERPRINT_XOR; + stun_update_header_length(begin, prev_length); + + uint32_t fingerprint = ntohl(*((uint32_t *)attr->value)); + if (fingerprint != expected) { + JLOG_ERROR("STUN fingerprint check failed, expected=%lX, actual=%lX", + (unsigned long)expected, (unsigned long)fingerprint); + return -1; + } + JLOG_VERBOSE("STUN fingerprint check succeeded"); + msg->has_fingerprint = true; + break; + } + case STUN_ATTR_REALM: { + JLOG_VERBOSE("Reading realm"); + if (length + 1 > STUN_MAX_REALM_LEN) { + JLOG_WARN("STUN realm attribute value too long, length=%zu", length); + return -1; + } + memcpy(msg->credentials.realm, (const char *)attr->value, length); + msg->credentials.realm[length] = '\0'; + JLOG_VERBOSE("Got realm: %s", msg->credentials.realm); + break; + } + case STUN_ATTR_NONCE: { + JLOG_VERBOSE("Reading nonce"); + if (length + 1 > STUN_MAX_NONCE_LEN) { + JLOG_WARN("STUN nonce attribute value too long, length=%zu", length); + return -1; + } + memcpy(msg->credentials.nonce, (const char *)attr->value, length); + msg->credentials.nonce[length] = '\0'; + JLOG_VERBOSE("Got nonce: %s", msg->credentials.nonce); + + // If the nonce of a response starts with the nonce cookie, decode the Security Feature bits + // See https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2 + if (STUN_IS_RESPONSE(msg->msg_class) && + strlen(msg->credentials.nonce) > STUN_NONCE_COOKIE_LEN + 4 && + strncmp(msg->credentials.nonce, STUN_NONCE_COOKIE, STUN_NONCE_COOKIE_LEN) == 0) { + char encoded_security_bits[5]; + memcpy(encoded_security_bits, msg->credentials.nonce + STUN_NONCE_COOKIE_LEN, 4); + encoded_security_bits[4] = '\0'; + + uint8_t bytes[4]; + bytes[0] = 0; + int len = BASE64_DECODE(encoded_security_bits, bytes + 1, 3); + if (len == 3) { + *security_bits = ntohl(*((uint32_t *)bytes)); + JLOG_VERBOSE("Nonce has cookie, Security Feature bits are 0x%lX", + (unsigned long)*security_bits); + } else { + JLOG_WARN("Nonce has cookie, but the encoded Security Feature bits field \"%s\" is " + "invalid", + encoded_security_bits); + security_bits = 0; + } + } else if (msg->msg_class == STUN_CLASS_RESP_ERROR) { + JLOG_DEBUG("Remote agent does not support RFC 8489"); + } + break; + } + case STUN_ATTR_PASSWORD_ALGORITHM: { + JLOG_VERBOSE("Reading password algorithm"); + if (length < sizeof(struct stun_value_password_algorithm)) { + JLOG_WARN("STUN password algorithm value too short, length=%zu", length); + return -1; + } + if (!STUN_IS_RESPONSE(msg->msg_class)) { + const struct stun_value_password_algorithm *pwa = + (const struct stun_value_password_algorithm *)attr->value; + stun_password_algorithm_t algorithm = ntohs(pwa->algorithm); + if (algorithm == STUN_PASSWORD_ALGORITHM_MD5 || + algorithm == STUN_PASSWORD_ALGORITHM_SHA256) + msg->credentials.password_algorithm = algorithm; + else + JLOG_WARN("Unknown password algorithm 0x%hX", algorithm); + } else { + JLOG_WARN("Found password algorithm in response, ignoring"); + } + break; + } + case STUN_ATTR_PASSWORD_ALGORITHMS: { + JLOG_VERBOSE("Reading password algorithms list"); + if (length < sizeof(struct stun_value_password_algorithm)) { + JLOG_WARN("STUN password algorithms list too short, length=%zu", length); + return -1; + } + if (length > STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE) { + JLOG_WARN("STUN password algorithms list too long, length=%zu", length); + return -1; + } + + memcpy(msg->credentials.password_algorithms_value, attr->value, length); + msg->credentials.password_algorithms_value_size = length; + + if (!STUN_IS_RESPONSE(msg->msg_class)) { + const uint8_t *pos = attr->value; + const uint8_t *end = pos + length; + while (pos < end) { + if ((size_t)(end - pos) < sizeof(struct stun_value_password_algorithm)) { + JLOG_WARN("STUN password algorithms list truncated, available=%zu", end - pos); + return -1; + } + const struct stun_value_password_algorithm *pwa = + (const struct stun_value_password_algorithm *)pos; + stun_password_algorithm_t algorithm = ntohs(pwa->algorithm); + size_t parameters_length = ntohs(pwa->parameters_length); + size_t padded_length = align32(parameters_length); + + pos += sizeof(struct stun_value_password_algorithm); + + if ((size_t)(end - pos) < padded_length) { + JLOG_WARN( + "STUN password algorithm parameters too long, length=%zu, padded=%zu, " + "available=%zu", + parameters_length, padded_length, end - pos); + return -1; + } + + pos += padded_length; + + if (algorithm == STUN_PASSWORD_ALGORITHM_MD5 || + algorithm == STUN_PASSWORD_ALGORITHM_SHA256) { + msg->credentials.password_algorithm = algorithm; + break; + } + + JLOG_DEBUG("Unknown password algorithm 0x%hX", algorithm); + } + } + break; + } + case STUN_ATTR_USERHASH: { + JLOG_VERBOSE("Reading user hash"); + if (length != USERHASH_SIZE) { + JLOG_WARN("STUN user hash value too long, length=%zu", length); + return -1; + } + memcpy(msg->credentials.userhash, attr->value, USERHASH_SIZE); + msg->credentials.enable_userhash = true; + break; + } + case STUN_ATTR_SOFTWARE: { + JLOG_VERBOSE("Reading software"); + if (length + 1 > STUN_MAX_SOFTWARE_LEN) { + JLOG_WARN("STUN software attribute value too long, length=%zu", length); + return -1; + } + char buffer[STUN_MAX_SOFTWARE_LEN]; + memcpy(buffer, (const char *)attr->value, length); + buffer[length] = '\0'; + JLOG_VERBOSE("Remote agent is \"%s\"", buffer); + break; + } + case STUN_ATTR_PRIORITY: { + JLOG_VERBOSE("Reading priority"); + if (length != 4) { + JLOG_DEBUG("STUN priority length invalid, length=%zu", length); + return -1; + } + msg->priority = ntohl(*((uint32_t *)attr->value)); + JLOG_VERBOSE("Got priority: %lu", (unsigned long)msg->priority); + break; + } + case STUN_ATTR_USE_CANDIDATE: { + JLOG_VERBOSE("Found use candidate flag"); + msg->use_candidate = true; + break; + } + case STUN_ATTR_ICE_CONTROLLING: { + JLOG_VERBOSE("Found ICE controlling attribute"); + if (length != 8) { + JLOG_DEBUG("STUN ICE controlling attribute length invalid, length=%zu", length); + return -1; + } + msg->ice_controlling = ntohll(*((uint64_t *)attr->value)); + break; + } + case STUN_ATTR_ICE_CONTROLLED: { + JLOG_VERBOSE("Found ICE controlled attribute"); + if (length != 8) { + JLOG_DEBUG("STUN ICE controlled attribute length invalid, length=%zu", length); + return -1; + } + msg->ice_controlled = ntohll(*((uint64_t *)attr->value)); + break; + } + case STUN_ATTR_CHANNEL_NUMBER: { + JLOG_VERBOSE("Reading channel number attribute"); + if (length < sizeof(struct stun_value_channel_number)) { + JLOG_DEBUG("STUN channel number attribute value too short, length=%zu", length); + return -1; + } + const struct stun_value_channel_number *channel_number = + (const struct stun_value_channel_number *)attr->value; + msg->channel_number = ntohs(channel_number->channel_number); + break; + } + case STUN_ATTR_LIFETIME: { + JLOG_VERBOSE("Reading lifetime attribute"); + if (length != 4) { + JLOG_DEBUG("STUN lifetime attribute length invalid, length=%zu", length); + return -1; + } + msg->lifetime = ntohl(*((uint32_t *)attr->value)); + msg->lifetime_set = true; + break; + } + case STUN_ATTR_XOR_PEER_ADDRESS: { + JLOG_VERBOSE("Reading XOR peer address"); + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + if (stun_read_value_mapped_address(attr->value, length, &msg->peer, mask) < 0) + return -1; + break; + } + case STUN_ATTR_XOR_RELAYED_ADDRESS: { + JLOG_VERBOSE("Reading XOR relayed address"); + uint8_t mask[16]; + *((uint32_t *)mask) = htonl(STUN_MAGIC); + memcpy(mask + 4, msg->transaction_id, 12); + if (stun_read_value_mapped_address(attr->value, length, &msg->relayed, mask) < 0) + return -1; + break; + } + case STUN_ATTR_DATA: { + JLOG_VERBOSE("Found data"); + msg->data = (const char *)attr->value; + msg->data_size = length; + break; + } + case STUN_ATTR_EVEN_PORT: { + JLOG_VERBOSE("Found even port attribute"); + if (length < 1) { + JLOG_DEBUG("STUN even port attribute length invalid, length=%zu", length); + return -1; + } + msg->even_port = true; + msg->next_port = ((struct stun_value_even_port *)attr->value)->r & 0x80; + break; + } + case STUN_ATTR_REQUESTED_TRANSPORT: { + JLOG_VERBOSE("Found requested transport attribute"); + if (length < sizeof(struct stun_value_requested_transport)) { + JLOG_DEBUG("STUN requested transport attribute length invalid, length=%zu", length); + return -1; + } + const struct stun_value_requested_transport *requested_transport = + (const struct stun_value_requested_transport *)attr->value; + if (requested_transport->protocol != 17) { // UDP + JLOG_WARN("Unexpected requested transport protocol: %d", + (int)requested_transport->protocol); + return -1; + } + msg->requested_transport = true; + break; + } + case STUN_ATTR_DONT_FRAGMENT: { + JLOG_VERBOSE("Found don't fragment attribute"); + msg->dont_fragment = true; + break; + } + case STUN_ATTR_RESERVATION_TOKEN: { + JLOG_VERBOSE("Found reservation token"); + if (length != 8) { + JLOG_DEBUG("STUN reservation token length invalid, length=%zu", length); + return -1; + } + msg->reservation_token = ntohll(*((uint64_t *)attr->value)); + break; + } + default: { + // Ignore + if (STUN_IS_OPTIONAL_ATTR(type)) + JLOG_DEBUG("Ignoring unknown optional STUN attribute type 0x%X", (unsigned int)type); + else + JLOG_WARN("Unknown STUN attribute type 0x%X, ignoring", (unsigned int)type); + break; + } + } + return (int)(sizeof(struct stun_attr) + align32(length)); +} + +int stun_read_value_mapped_address(const void *data, size_t size, addr_record_t *mapped, + const uint8_t *mask) { + size_t len = sizeof(struct stun_value_mapped_address); + if (size < len) { + JLOG_VERBOSE("STUN mapped address value too short, size=%zu", size); + return -1; + } + const struct stun_value_mapped_address *value = data; + stun_address_family_t family = (stun_address_family_t)value->family; + switch (family) { + case STUN_ADDRESS_FAMILY_IPV4: { + len += 4; + if (size < len) { + JLOG_DEBUG("IPv4 mapped address value too short, size=%zu", size); + return -1; + } + JLOG_VERBOSE("Reading IPv4 address"); + mapped->len = sizeof(struct sockaddr_in); + struct sockaddr_in *sin = (struct sockaddr_in *)&mapped->addr; + sin->sin_family = AF_INET; + sin->sin_port = value->port ^ *((uint16_t *)mask); + uint8_t *bytes = (uint8_t *)&sin->sin_addr; + for (int i = 0; i < 4; ++i) + bytes[i] = value->address[i] ^ mask[i]; + break; + } + case STUN_ADDRESS_FAMILY_IPV6: { + len += 16; + if (size < len) { + JLOG_DEBUG("IPv6 mapped address value too short, size=%zu", size); + return -1; + } + JLOG_VERBOSE("Reading IPv6 address"); + mapped->len = sizeof(struct sockaddr_in6); + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&mapped->addr; + sin6->sin6_family = AF_INET6; + sin6->sin6_port = value->port ^ *((uint16_t *)mask); + uint8_t *bytes = (uint8_t *)&sin6->sin6_addr; + for (int i = 0; i < 16; ++i) + bytes[i] = value->address[i] ^ mask[i]; + break; + } + default: { + JLOG_DEBUG("Unknown STUN address family 0x%X", (unsigned int)family); + len = size; + break; + } + } + return (int)len; +} + +bool stun_check_integrity(void *buf, size_t size, const stun_message_t *msg, const char *password) { + if (!msg->has_integrity) + return false; + + const struct stun_header *header = buf; + const size_t length = ntohs(header->length); + if (size < sizeof(struct stun_header) + length) + return false; + + uint8_t key[MAX_HMAC_KEY_LEN]; + size_t key_len = generate_hmac_key(msg, password, key); + + bool success = false; + uint8_t *begin = buf; + const uint8_t *attr_begin = begin + sizeof(struct stun_header); + const uint8_t *end = attr_begin + length; + const uint8_t *pos = attr_begin; + while (pos < end) { + const struct stun_attr *attr = (const struct stun_attr *)pos; + size_t attr_length = ntohs(attr->length); + if (size < sizeof(struct stun_attr) + attr_length) + return false; + + stun_attr_type_t type = (stun_attr_type_t)ntohs(attr->type); + switch (type) { + case STUN_ATTR_MESSAGE_INTEGRITY: { + if (attr_length != HMAC_SHA1_SIZE) + return false; + + size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA1_SIZE; + size_t prev_length = stun_update_header_length(begin, tmp_length); + uint8_t hmac[HMAC_SHA1_SIZE]; + hmac_sha1(begin, pos - begin, key, key_len, hmac); + stun_update_header_length(begin, prev_length); + + const uint8_t *expected_hmac = attr->value; + if (const_time_memcmp(hmac, expected_hmac, HMAC_SHA1_SIZE) != 0) { + JLOG_DEBUG("STUN message integrity SHA1 check failed"); + return false; + } + + success = true; + break; + } + case STUN_ATTR_MESSAGE_INTEGRITY_SHA256: { + if (attr_length != HMAC_SHA256_SIZE) + return false; + + size_t tmp_length = pos - attr_begin + STUN_ATTR_SIZE + HMAC_SHA256_SIZE; + size_t prev_length = stun_update_header_length(begin, tmp_length); + uint8_t hmac[HMAC_SHA256_SIZE]; + hmac_sha256(begin, pos - begin, key, key_len, hmac); + stun_update_header_length(begin, prev_length); + + const uint8_t *expected_hmac = attr->value; + if (const_time_memcmp(hmac, expected_hmac, HMAC_SHA256_SIZE) != 0) { + JLOG_DEBUG("STUN message integrity SHA256 check failed"); + return false; + } + + success = true; + break; + } + default: + // Ignore + break; + } + + pos += sizeof(struct stun_attr) + align32(attr_length); + } + + if (!success) + return false; + + JLOG_VERBOSE("STUN message integrity check succeeded"); + return true; +} + +void stun_prepend_nonce_cookie(char *nonce) { + // RFC 8489: To indicate that it supports this specification, a server MUST prepend the + // NONCE attribute value with the character string composed of "obMatJos2" concatenated with + // the (4-character) base64 [RFC4648] encoding of the 24-bit STUN Security Features See + // https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2 + char copy[STUN_MAX_NONCE_LEN]; + strcpy(copy, nonce); + + char encoded_security_bits[5]; + uint32_t security_bits = + htonl(STUN_SECURITY_PASSWORD_ALGORITHMS_BIT | STUN_SECURITY_USERNAME_ANONYMITY_BIT); + BASE64_ENCODE((uint8_t *)&security_bits + 1, 3, encoded_security_bits, 5); + + snprintf(nonce, STUN_MAX_NONCE_LEN, "%s%s%.*s", STUN_NONCE_COOKIE, encoded_security_bits, + STUN_MAX_NONCE_LEN - (STUN_NONCE_COOKIE_LEN + 5), copy); +} + +void stun_compute_userhash(const char *username, const char *realm, uint8_t *out) { + char input[MAX_USERHASH_INPUT_LEN]; + int input_len = snprintf(input, MAX_USERHASH_INPUT_LEN, "%s:%s", username, realm); + if (input_len < 0) + return; + + if (input_len >= MAX_USERHASH_INPUT_LEN) + input_len = MAX_USERHASH_INPUT_LEN - 1; + + hash_sha256(input, input_len, out); +} + +void stun_process_credentials(const stun_credentials_t *credentials, stun_credentials_t *dst) { + char username[STUN_MAX_USERNAME_LEN]; + strcpy(username, dst->username); + *dst = *credentials; + strcpy(dst->username, username); + + if (credentials->enable_userhash) + stun_compute_userhash(username, credentials->realm, dst->userhash); +} + +const char *stun_get_error_reason(unsigned int code) { + switch (code) { + case 0: + return ""; + case 300: + return "Try Alternate"; + case 400: + return "Bad Request"; + case 401: + return "Unauthenticated"; + case 403: + return "Forbidden"; + case 420: + return "Unknown Attribute"; + case 437: + return "Allocation Mismatch"; + case 438: + return "Stale Nonce"; + case 440: + return "Address Family not Supported"; + case 441: + return "Wrong credentials"; + case 442: + return "Unsupported Transport Protocol"; + case 443: + return "Peer Address Family Mismatch"; + case 486: + return "Allocation Quota Reached"; + case 500: + return "Server Error"; + case 508: + return "Insufficient Capacity"; + default: + return "Error"; + } +} + +JUICE_EXPORT bool _juice_is_stun_datagram(const void *data, size_t size) { + return is_stun_datagram(data, size); +} + +JUICE_EXPORT int _juice_stun_read(void *data, size_t size, stun_message_t *msg) { + return stun_read(data, size, msg); +} + +JUICE_EXPORT bool _juice_stun_check_integrity(void *buf, size_t size, const stun_message_t *msg, + const char *password) { + return stun_check_integrity(buf, size, msg, password); +} diff --git a/thirdparty/libjuice/src/stun.h b/thirdparty/libjuice/src/stun.h new file mode 100644 index 0000000..b3aeade --- /dev/null +++ b/thirdparty/libjuice/src/stun.h @@ -0,0 +1,376 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_STUN_H +#define JUICE_STUN_H + +#include "juice.h" + +#include "addr.h" +#include "hash.h" +#include "hmac.h" + +#include +#include + +#pragma pack(push, 1) +/* + * STUN message header (20 bytes) + * See https://www.rfc-editor.org/rfc/rfc8489.html#section-5 + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |0 0| STUN Message Type | Message Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Magic Cookie = 0x2112A442 | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | + * | Transaction ID (96 bits) | + * | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +#define STUN_TRANSACTION_ID_SIZE 12 + +struct stun_header { + uint16_t type; + uint16_t length; + uint32_t magic; + uint8_t transaction_id[STUN_TRANSACTION_ID_SIZE]; +}; + +/* + * Format of STUN Message Type Field + * + * 0 1 + * 2 3 4 5 6 7 8 9 0 1 2 3 4 5 + * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + * |M |M |M|M|M|C|M|M|M|C|M|M|M|M| + * |11|10|9|8|7|1|6|5|4|0|3|2|1|0| + * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ + * Request: C=b00 + * Indication: C=b01 + * Response: C=b10 (success) + * C=b11 (error) + */ +#define STUN_CLASS_MASK 0x0110 + +typedef enum stun_class { + STUN_CLASS_REQUEST = 0x0000, + STUN_CLASS_INDICATION = 0x0010, + STUN_CLASS_RESP_SUCCESS = 0x0100, + STUN_CLASS_RESP_ERROR = 0x0110 +} stun_class_t; + +typedef enum stun_method { + STUN_METHOD_BINDING = 0x0001, + + // Methods for TURN + // See https://www.rfc-editor.org/rfc/rfc8656.html#section-17 + STUN_METHOD_ALLOCATE = 0x003, + STUN_METHOD_REFRESH = 0x004, + STUN_METHOD_SEND = 0x006, + STUN_METHOD_DATA = 0x007, + STUN_METHOD_CREATE_PERMISSION = 0x008, + STUN_METHOD_CHANNEL_BIND = 0x009 +} stun_method_t; + +#define STUN_IS_RESPONSE(msg_class) (msg_class & 0x0100) + +/* + * STUN attribute header + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Type | Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Value (variable) ... + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +struct stun_attr { + uint16_t type; + uint16_t length; + uint8_t value[]; +}; + +typedef enum stun_attr_type { + // Comprehension-required + STUN_ATTR_MAPPED_ADDRESS = 0x0001, + STUN_ATTR_USERNAME = 0x0006, + STUN_ATTR_MESSAGE_INTEGRITY = 0x0008, + STUN_ATTR_ERROR_CODE = 0x0009, + STUN_ATTR_UNKNOWN_ATTRIBUTES = 0x000A, + STUN_ATTR_REALM = 0x0014, + STUN_ATTR_NONCE = 0x0015, + STUN_ATTR_MESSAGE_INTEGRITY_SHA256 = 0x001C, + STUN_ATTR_PASSWORD_ALGORITHM = 0x001D, + STUN_ATTR_USERHASH = 0x001E, + STUN_ATTR_XOR_MAPPED_ADDRESS = 0x0020, + STUN_ATTR_PRIORITY = 0x0024, + STUN_ATTR_USE_CANDIDATE = 0x0025, + + // Comprehension-optional + STUN_ATTR_PASSWORD_ALGORITHMS = 0x8002, + STUN_ATTR_ALTERNATE_DOMAIN = 0x8003, + STUN_ATTR_SOFTWARE = 0x8022, + STUN_ATTR_ALTERNATE_SERVER = 0x8023, + STUN_ATTR_FINGERPRINT = 0x8028, + STUN_ATTR_ICE_CONTROLLED = 0x8029, + STUN_ATTR_ICE_CONTROLLING = 0x802A, + + // Attributes for TURN + // See https://www.rfc-editor.org/rfc/rfc8656.html#section-18 + STUN_ATTR_CHANNEL_NUMBER = 0x000C, + STUN_ATTR_LIFETIME = 0x000D, + STUN_ATTR_XOR_PEER_ADDRESS = 0x0012, + STUN_ATTR_DATA = 0x0013, + STUN_ATTR_XOR_RELAYED_ADDRESS = 0x0016, + STUN_ATTR_EVEN_PORT = 0x0018, + STUN_ATTR_REQUESTED_TRANSPORT = 0x0019, + STUN_ATTR_DONT_FRAGMENT = 0x001A, + STUN_ATTR_RESERVATION_TOKEN = 0x0022 +} stun_attr_type_t; + +#define STUN_IS_OPTIONAL_ATTR(attr_type) (attr_type & 0x8000) + +/* + * STUN attribute value for MAPPED-ADDRESS or XOR-MAPPED-ADDRESS + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * |X X X X X X X X| Family | Port or X-Port | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | + * | Address or X-Address (32 bits or 128 bits) | + * | | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +struct stun_value_mapped_address { + uint8_t padding; + uint8_t family; + uint16_t port; + uint8_t address[]; +}; + +typedef enum stun_address_family { + STUN_ADDRESS_FAMILY_IPV4 = 0x01, + STUN_ADDRESS_FAMILY_IPV6 = 0x02, +} stun_address_family_t; + +/* + * STUN attribute value for ERROR-CODE + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reserved, should be 0 |Class| Number | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Reason Phrase (variable) ... + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +struct stun_value_error_code { + uint16_t reserved; + uint8_t code_class; // lower 3 bits only, higher bits are reserved + uint8_t code_number; + uint8_t reason[]; +}; + +#define STUN_ERROR_INTERNAL_VALIDATION_FAILED 599 + +/* + * STUN attribute for CHANNEL-NUMBER + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Channel Number | RFFU = 0 | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +struct stun_value_channel_number { + uint16_t channel_number; + uint16_t reserved; +}; + +/* + * STUN attribute for EVEN-PORT + * + * 0 + * 0 1 2 3 4 5 6 7 + * +-+-+-+-+-+-+-+-+ + * |R| RFFU | + * +-+-+-+-+-+-+-+-+ + */ +struct stun_value_even_port { + uint8_t r; +}; + +/* + * STUN attribute for REQUESTED-TRANSPORT + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Protocol | RFFU | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ +struct stun_value_requested_transport { + uint8_t protocol; + uint8_t reserved1; + uint16_t reserved2; +}; + +/* + * STUN attribute value for PASSWORD-ALGORITHM and PASSWORD-ALGORITHMS + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Algorithm 1 | Algorithm 1 Parameters Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Algorithm 1 Parameters (variable) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Algorithm 2 | Algorithm 2 Parameters Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Algorithm 2 Parameters (variable) + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | ... + */ +struct stun_value_password_algorithm { + uint16_t algorithm; + uint16_t parameters_length; + uint8_t parameters[]; +}; + +typedef enum stun_password_algorithm { + STUN_PASSWORD_ALGORITHM_UNSET = 0x0000, + STUN_PASSWORD_ALGORITHM_MD5 = 0x0001, + STUN_PASSWORD_ALGORITHM_SHA256 = 0x0002, +} stun_password_algorithm_t; + +#pragma pack(pop) + +// The value of USERNAME is a variable-length value. It MUST contain a UTF-8 [RFC3629] encoded +// sequence of less than 513 bytes [...] +#define STUN_MAX_USERNAME_LEN 513 + 1 + +// The REALM attribute [...] MUST be a UTF-8 [RFC3629] encoded sequence of less than 128 characters +// (which can be as long as 763 bytes) +#define STUN_MAX_REALM_LEN 763 + 1 + +// The NONCE attribute may be present in requests and responses. It [...] MUST be less than 128 +// characters (which can be as long as 763 bytes) +#define STUN_MAX_NONCE_LEN 763 + 1 + +// The value of SOFTWARE is variable length. It MUST be a UTF-8 [RFC3629] encoded sequence of less +// than 128 characters (which can be as long as 763 bytes) +#define STUN_MAX_SOFTWARE_LEN 763 + 1 + +// The reason phrase MUST be a UTF-8-encoded [RFC3629] sequence of fewer than 128 characters (which +// can be as long as 509 bytes when encoding them or 763 bytes when decoding them). +#define STUN_MAX_ERROR_REASON_LEN 763 + 1 + +#define STUN_MAX_PASSWORD_LEN STUN_MAX_USERNAME_LEN + +// Nonce cookie prefix as specified in https://www.rfc-editor.org/rfc/rfc8489.html#section-9.2 +#define STUN_NONCE_COOKIE "obMatJos2" +#define STUN_NONCE_COOKIE_LEN 9 + +// USERHASH is a SHA256 digest +#define USERHASH_SIZE HASH_SHA256_SIZE + +// STUN Security Feature bits as defined in https://www.rfc-editor.org/rfc/rfc8489.html#section-18.1 +// See errata about bit order: https://www.rfc-editor.org/errata_search.php?rfc=8489 +// Bits are assigned starting from the least significant side of the bit set, so Bit 0 is the rightmost bit, and Bit 23 is the leftmost bit. +// Bit 0: Password algorithms +// Bit 1: Username anonymity +// Bit 2-23: Unassigned + +#define STUN_SECURITY_PASSWORD_ALGORITHMS_BIT 0x01 +#define STUN_SECURITY_USERNAME_ANONYMITY_BIT 0x02 + +#define STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE 256 + +typedef struct stun_credentials { + char username[STUN_MAX_USERNAME_LEN]; + char realm[STUN_MAX_REALM_LEN]; + char nonce[STUN_MAX_NONCE_LEN]; + uint8_t userhash[USERHASH_SIZE]; + bool enable_userhash; + stun_password_algorithm_t password_algorithm; + uint8_t password_algorithms_value[STUN_MAX_PASSWORD_ALGORITHMS_VALUE_SIZE]; + size_t password_algorithms_value_size; +} stun_credentials_t; + +typedef struct stun_message { + stun_class_t msg_class; + stun_method_t msg_method; + uint8_t transaction_id[STUN_TRANSACTION_ID_SIZE]; + unsigned int error_code; + uint32_t priority; + uint64_t ice_controlling; + uint64_t ice_controlled; + bool use_candidate; + addr_record_t mapped; + + stun_credentials_t credentials; + + // Only for reading + bool has_integrity; + bool has_fingerprint; + + // TURN + addr_record_t peer; + addr_record_t relayed; + addr_record_t alternate_server; + const char *data; + size_t data_size; + uint32_t lifetime; + uint16_t channel_number; + bool lifetime_set; + bool even_port; + bool next_port; + bool dont_fragment; + bool requested_transport; + uint64_t reservation_token; + +} stun_message_t; + +int stun_write(void *buf, size_t size, const stun_message_t *msg, + const char *password); // password may be NULL +int stun_write_header(void *buf, size_t size, stun_class_t class, stun_method_t method, + const uint8_t *transaction_id); +size_t stun_update_header_length(void *buf, size_t length); +int stun_write_attr(void *buf, size_t size, uint16_t type, const void *value, size_t length); +int stun_write_value_mapped_address(void *buf, size_t size, const struct sockaddr *addr, + socklen_t addrlen, const uint8_t *mask); + +bool is_stun_datagram(const void *data, size_t size); + +int stun_read(void *data, size_t size, stun_message_t *msg); +int stun_read_attr(const void *data, size_t size, stun_message_t *msg, uint8_t *begin, + uint8_t *attr_begin, uint32_t *security_bits); +int stun_read_value_mapped_address(const void *data, size_t size, addr_record_t *mapped, + const uint8_t *mask); + +bool stun_check_integrity(void *buf, size_t size, const stun_message_t *msg, const char *password); + +void stun_compute_userhash(const char *username, const char *realm, uint8_t *out); +void stun_prepend_nonce_cookie(char *nonce); +void stun_process_credentials(const stun_credentials_t *credentials, stun_credentials_t *dst); + +const char *stun_get_error_reason(unsigned int code); + +// Export for tests +JUICE_EXPORT bool _juice_is_stun_datagram(const void *data, size_t size); +JUICE_EXPORT int _juice_stun_read(void *data, size_t size, stun_message_t *msg); +JUICE_EXPORT bool _juice_stun_check_integrity(void *buf, size_t size, const stun_message_t *msg, + const char *password); + +#endif diff --git a/thirdparty/libjuice/src/thread.h b/thirdparty/libjuice/src/thread.h new file mode 100644 index 0000000..6ea89e6 --- /dev/null +++ b/thirdparty/libjuice/src/thread.h @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_THREAD_H +#define JUICE_THREAD_H + +#ifdef _WIN32 + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 // Windows 7 +#endif +#ifndef __MSVCRT_VERSION__ +#define __MSVCRT_VERSION__ 0x0601 +#endif + +#include + +typedef HANDLE mutex_t; +typedef HANDLE thread_t; +typedef DWORD thread_return_t; +#define THREAD_CALL __stdcall + +#define MUTEX_INITIALIZER NULL + +#define MUTEX_PLAIN 0x0 +#define MUTEX_RECURSIVE 0x0 // mutexes are recursive on Windows + +static inline int mutex_init_impl(mutex_t *m) { + return ((*(m) = CreateMutex(NULL, FALSE, NULL)) != NULL ? 0 : (int)GetLastError()); +} + +static inline int mutex_lock_impl(volatile mutex_t *m) { + // Atomically initialize the mutex on first lock + if (*(m) == NULL) { + HANDLE cm = CreateMutex(NULL, FALSE, NULL); + if (cm == NULL) + return (int)GetLastError(); + if (InterlockedCompareExchangePointer(m, cm, NULL) != NULL) + CloseHandle(cm); + } + return WaitForSingleObject(*m, INFINITE) != WAIT_FAILED ? 0 : (int)GetLastError(); +} + +#define mutex_init(m, flags) mutex_init_impl(m) +#define mutex_lock(m) mutex_lock_impl(m) +#define mutex_unlock(m) (void)ReleaseMutex(*(m)) +#define mutex_destroy(m) (void)CloseHandle(*(m)) + +static inline void thread_join_impl(thread_t t, thread_return_t *res) { + WaitForSingleObject(t, INFINITE); + if (res) + GetExitCodeThread(t, res); + CloseHandle(t); +} + +#define thread_init(t, func, arg) \ + ((*(t) = CreateThread(NULL, 0, func, arg, 0, NULL)) != NULL ? 0 : (int)GetLastError()) +#define thread_join(t, res) thread_join_impl(t, res) + +#else // POSIX + +#include + +typedef pthread_mutex_t mutex_t; +typedef pthread_t thread_t; +typedef void *thread_return_t; +#define THREAD_CALL + +#define MUTEX_INITIALIZER PTHREAD_MUTEX_INITIALIZER + +#define MUTEX_PLAIN PTHREAD_MUTEX_NORMAL +#define MUTEX_RECURSIVE PTHREAD_MUTEX_RECURSIVE + +static inline int mutex_init_impl(mutex_t *m, int flags) { + pthread_mutexattr_t mutexattr; + pthread_mutexattr_init(&mutexattr); + pthread_mutexattr_settype(&mutexattr, flags); + int ret = pthread_mutex_init(m, &mutexattr); + pthread_mutexattr_destroy(&mutexattr); + return ret; +} + +#define mutex_init(m, flags) mutex_init_impl(m, flags) +#define mutex_lock(m) pthread_mutex_lock(m) +#define mutex_unlock(m) (void)pthread_mutex_unlock(m) +#define mutex_destroy(m) (void)pthread_mutex_destroy(m) + +#define thread_init(t, func, arg) pthread_create(t, NULL, func, arg) +#define thread_join(t, res) (void)pthread_join(t, res) + +#endif // ifdef _WIN32 + +#if __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_ATOMICS__) + +#include +#define atomic(T) _Atomic(T) +#define atomic_ptr(T) _Atomic(T*) + +#else // no atomics + +// Since we don't need compare-and-swap, just assume store and load are atomic +#define atomic(T) volatile T +#define atomic_ptr(T) T* volatile +#define atomic_store(a, v) (void)(*(a) = (v)) +#define atomic_load(a) (*(a)) +#define ATOMIC_VAR_INIT(v) (v) + +#endif // if atomics + +#endif // JUICE_THREAD_H + diff --git a/thirdparty/libjuice/src/timestamp.c b/thirdparty/libjuice/src/timestamp.c new file mode 100644 index 0000000..2a7cc98 --- /dev/null +++ b/thirdparty/libjuice/src/timestamp.c @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "timestamp.h" + +#ifdef _WIN32 +#include +#else +#include + +// clock_gettime() is not implemented on older versions of OS X (< 10.12) +#if defined(__APPLE__) && !defined(CLOCK_MONOTONIC) +#include +#define CLOCK_MONOTONIC 0 +int clock_gettime(int clk_id, struct timespec *t) { + (void)clk_id; + + // gettimeofday() does not return monotonic time but it should be good enough. + struct timeval now; + if (gettimeofday(&now, NULL)) + return -1; + + t->tv_sec = now.tv_sec; + t->tv_nsec = now.tv_usec * 1000; + return 0; +} +#endif // defined(__APPLE__) && !defined(CLOCK_MONOTONIC) + +#endif + +timestamp_t current_timestamp() { +#ifdef _WIN32 + return (timestamp_t)GetTickCount(); +#else // POSIX + struct timespec ts; + if (clock_gettime(CLOCK_MONOTONIC, &ts)) + return 0; + return (timestamp_t)ts.tv_sec * 1000 + (timestamp_t)ts.tv_nsec / 1000000; +#endif +} diff --git a/thirdparty/libjuice/src/timestamp.h b/thirdparty/libjuice/src/timestamp.h new file mode 100644 index 0000000..fabc8bb --- /dev/null +++ b/thirdparty/libjuice/src/timestamp.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_TIMESTAMP_H +#define JUICE_TIMESTAMP_H + +#include +#include + +typedef int64_t timestamp_t; +typedef timestamp_t timediff_t; + +timestamp_t current_timestamp(); + +#endif diff --git a/thirdparty/libjuice/src/turn.c b/thirdparty/libjuice/src/turn.c new file mode 100644 index 0000000..5f4abdb --- /dev/null +++ b/thirdparty/libjuice/src/turn.c @@ -0,0 +1,495 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "turn.h" +#include "log.h" +#include "random.h" +#include "socket.h" + +#include + +static bool memory_is_zero(const void *data, size_t size) { + const char *d = data; + for (size_t i = 0; i < size; ++i) + if (d[i]) + return false; + + return true; +} + +static uint16_t random_channel_number() { + /* + * RFC 8656 12. Channels + * The ChannelData message (see Section 12.4) starts with a two-byte + * field that carries the channel number. The values of this field are + * allocated as follows: + * + * +------------------------+--------------------------------------+ + * | 0x0000 through 0x3FFF: | These values can never be used for | + * | | channel numbers. | + * +------------------------+--------------------------------------+ + * | 0x4000 through 0x4FFF: | These values are the allowed channel | + * | | numbers (4096 possible values). | + * +------------------------+--------------------------------------+ + * | 0x5000 through 0xFFFF: | Reserved (For DTLS-SRTP multiplexing | + * | | collision avoidance, see [RFC7983]). | + * +------------------------+--------------------------------------+ + */ + uint16_t r; + juice_random(&r, 2); + return 0x4000 | (r & 0x0FFF); +} + +bool is_channel_data(const void *data, size_t size) { + // According RFC 8656, first byte in [64..79] is TURN Channel + if (size == 0) + return false; + uint8_t b = *((const uint8_t *)data); + return b >= 64 && b <= 79; +} + +bool is_valid_channel(uint16_t channel) { return channel >= 0x4000; } + +int turn_wrap_channel_data(char *buffer, size_t size, const char *data, size_t data_size, + uint16_t channel) { + if (!is_valid_channel(channel)) { + JLOG_WARN("Invalid channel number: 0x%hX", channel); + return -1; + } + if (data_size >= 65536) { + JLOG_WARN("ChannelData is too long, size=%zu", size); + return -1; + } + if (size < sizeof(struct channel_data_header) + data_size) { + JLOG_WARN("Buffer is too small to add ChannelData header, size=%zu, needed=%zu", size, + sizeof(struct channel_data_header) + data_size); + return -1; + } + + memmove(buffer + sizeof(struct channel_data_header), data, data_size); + struct channel_data_header *header = (struct channel_data_header *)buffer; + header->channel_number = htons((uint16_t)channel); + header->length = htons((uint16_t)data_size); + return (int)(sizeof(struct channel_data_header) + data_size); +} + +static int find_ordered_channel_rec(turn_entry_t *const ordered_channels[], uint16_t channel, + int begin, int end) { + int d = end - begin; + if (d <= 0) + return begin; + + int pivot = begin + d / 2; + const turn_entry_t *entry = ordered_channels[pivot]; + if (channel < entry->channel) + return find_ordered_channel_rec(ordered_channels, channel, begin, pivot); + else if (channel > entry->channel) + return find_ordered_channel_rec(ordered_channels, channel, pivot + 1, end); + else + return pivot; +} + +static int find_ordered_channel(const turn_map_t *map, uint16_t channel) { + return find_ordered_channel_rec(map->ordered_channels, channel, 0, map->channels_count); +} + +static int find_ordered_transaction_id_rec(turn_entry_t *const ordered_transaction_ids[], + const uint8_t *transaction_id, int begin, int end) { + int d = end - begin; + if (d <= 0) + return begin; + + int pivot = begin + d / 2; + const turn_entry_t *entry = ordered_transaction_ids[pivot]; + int ret = memcmp(transaction_id, entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + if (ret < 0) + return find_ordered_transaction_id_rec(ordered_transaction_ids, transaction_id, begin, + pivot); + else if (ret > 0) + return find_ordered_transaction_id_rec(ordered_transaction_ids, transaction_id, pivot + 1, + end); + else + return pivot; +} + +static int find_ordered_transaction_id(const turn_map_t *map, const uint8_t *transaction_id) { + return find_ordered_transaction_id_rec(map->ordered_transaction_ids, transaction_id, 0, + map->transaction_ids_count); +} + +static void remove_ordered_transaction_id(turn_map_t *map, const uint8_t *transaction_id) { + int pos = find_ordered_transaction_id(map, transaction_id); + if (pos < map->transaction_ids_count) { + memmove(map->ordered_transaction_ids + pos, map->ordered_transaction_ids + pos + 1, + (map->transaction_ids_count - (pos + 1)) * sizeof(turn_entry_t *)); + map->transaction_ids_count--; + } +} +/* +static void remove_ordered_channel(turn_map_t *map, uint16_t channel) { + int pos = find_ordered_channel(map, channel); + if (pos < map->channels_count) { + memmove(map->ordered_channels + pos, map->ordered_channels + pos + 1, + (map->channels_count - (pos + 1)) * sizeof(turn_entry_t *)); + map->channels_count--; + } +} + +static void delete_entry(turn_map_t *map, turn_entry_t *entry) { + if (entry->type == TURN_ENTRY_TYPE_EMPTY || entry->type == TURN_ENTRY_TYPE_DELETED) + return; + + if (!memory_is_zero(entry->transaction_id, STUN_TRANSACTION_ID_SIZE)) + remove_ordered_transaction_id(map, entry->transaction_id); + + if (entry->type == TURN_ENTRY_TYPE_CHANNEL && entry->channel) + remove_ordered_channel(map, entry->channel); + + memset(entry, 0, sizeof(*entry)); + entry->type = TURN_ENTRY_TYPE_DELETED; +} +*/ +static turn_entry_t *find_entry(turn_map_t *map, const addr_record_t *record, + turn_entry_type_t type, bool allow_deleted) { + unsigned long key = (addr_record_hash(record, false) + (int)type) % map->map_size; + unsigned long pos = key; + while (true) { + turn_entry_t *entry = map->map + pos; + if (entry->type == TURN_ENTRY_TYPE_EMPTY || + (entry->type == type && addr_record_is_equal(&entry->record, record, false))) + break; + + if (allow_deleted && entry->type == TURN_ENTRY_TYPE_DELETED) + break; + + pos = (pos + 1) % map->map_size; + if (pos == key) { + JLOG_VERBOSE("TURN map is full"); + return NULL; + } + } + return map->map + pos; +} + +static bool update_timestamp(turn_map_t *map, turn_entry_type_t type, const uint8_t *transaction_id, + const addr_record_t *record, timediff_t duration) { + turn_entry_t *entry; + if (record) { + entry = find_entry(map, record, type, true); + if (!entry) + return false; + + if (entry->type == type) { + if (memcmp(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE) == 0) + return true; + } else { + entry->type = type; + entry->record = *record; + } + + if (!memory_is_zero(entry->transaction_id, STUN_TRANSACTION_ID_SIZE)) + remove_ordered_transaction_id(map, entry->transaction_id); + + memcpy(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + + } else { + int pos = find_ordered_transaction_id(map, transaction_id); + if (pos == map->transaction_ids_count) + return false; + + entry = map->ordered_transaction_ids[pos]; + if (entry->type != type || + memcmp(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE) != 0) + return false; + } + + entry->timestamp = current_timestamp() + duration; + entry->fresh_transaction_id = false; + return true; +} + +int turn_init_map(turn_map_t *map, int size) { + memset(map, 0, sizeof(*map)); + + map->map_size = size * 2; + map->channels_count = 0; + map->transaction_ids_count = 0; + + map->map = calloc(map->map_size, sizeof(turn_entry_t)); + map->ordered_channels = calloc(map->map_size, sizeof(turn_entry_t *)); + map->ordered_transaction_ids = calloc(map->map_size, sizeof(turn_entry_t *)); + + if (!map->map || !map->ordered_channels || !map->ordered_transaction_ids) { + JLOG_ERROR("Failed to allocate TURN map of size %d", size); + turn_destroy_map(map); + return -1; + } + + return 0; +} + +void turn_destroy_map(turn_map_t *map) { + free(map->map); + free(map->ordered_channels); + free(map->ordered_transaction_ids); +} + +bool turn_set_permission(turn_map_t *map, const uint8_t *transaction_id, + const addr_record_t *record, timediff_t duration) { + return update_timestamp(map, TURN_ENTRY_TYPE_PERMISSION, transaction_id, record, duration); +} + +bool turn_has_permission(turn_map_t *map, const addr_record_t *record) { + turn_entry_t *entry = find_entry(map, record, TURN_ENTRY_TYPE_PERMISSION, false); + if (!entry || entry->type != TURN_ENTRY_TYPE_PERMISSION) + return false; + + return current_timestamp() < entry->timestamp; +} + +bool turn_bind_channel(turn_map_t *map, const addr_record_t *record, const uint8_t *transaction_id, + uint16_t channel, timediff_t duration) { + if (!is_valid_channel(channel)) { + JLOG_ERROR("Invalid channel number: 0x%hX", channel); + return false; + } + + turn_entry_t *entry = find_entry(map, record, TURN_ENTRY_TYPE_CHANNEL, true); + if (!entry) + return false; + + if (entry->type == TURN_ENTRY_TYPE_CHANNEL && entry->channel) { + if (entry->channel != channel) { + JLOG_WARN("The record is already bound to a channel"); + return false; + } + + entry->timestamp = current_timestamp() + duration; + return true; + } + + int pos = find_ordered_channel(map, channel); + if (pos < map->channels_count) { + const turn_entry_t *other_entry = map->ordered_channels[pos]; + if (other_entry->channel == channel) { + JLOG_WARN("The channel is already bound to a record"); + return false; + } + } + + if (entry->type != TURN_ENTRY_TYPE_CHANNEL) { + entry->type = TURN_ENTRY_TYPE_CHANNEL; + entry->record = *record; + } + + memmove(map->ordered_channels + pos + 1, map->ordered_channels + pos, + (map->channels_count - pos) * sizeof(turn_entry_t *)); + map->ordered_channels[pos] = entry; + map->channels_count++; + + entry->channel = channel; + entry->timestamp = current_timestamp() + duration; + + if (transaction_id) { + memcpy(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + entry->fresh_transaction_id = true; + } + + return true; +} + +bool turn_bind_random_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel, + timediff_t duration) { + uint16_t c; + do { + c = random_channel_number(); + } while (turn_find_channel(map, c, NULL)); + + if (!turn_bind_channel(map, record, NULL, c, duration)) + return false; + + if (channel) + *channel = c; + + return true; +} + +bool turn_bind_current_channel(turn_map_t *map, const uint8_t *transaction_id, + const addr_record_t *record, timediff_t duration) { + return update_timestamp(map, TURN_ENTRY_TYPE_CHANNEL, transaction_id, record, duration); +} + +bool turn_get_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel) { + turn_entry_t *entry = find_entry(map, record, TURN_ENTRY_TYPE_CHANNEL, false); + if (!entry || entry->type != TURN_ENTRY_TYPE_CHANNEL) + return false; + + if (channel) + *channel = entry->channel; + + return true; +} + +bool turn_get_bound_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel) { + turn_entry_t *entry = find_entry(map, record, TURN_ENTRY_TYPE_CHANNEL, false); + if (!entry || entry->type != TURN_ENTRY_TYPE_CHANNEL) + return false; + + if (!entry->channel || current_timestamp() >= entry->timestamp) + return false; + + if (channel) + *channel = entry->channel; + + return true; +} + +bool turn_find_channel(turn_map_t *map, uint16_t channel, addr_record_t *record) { + if (!is_valid_channel(channel)) { + JLOG_WARN("Invalid channel number: 0x%hX", channel); + return false; + } + + int pos = find_ordered_channel(map, channel); + if (pos == map->channels_count) + return false; + + const turn_entry_t *entry = map->ordered_channels[pos]; + if (entry->channel != channel) + return false; + + if (record) + *record = entry->record; + + return true; +} + +bool turn_find_bound_channel(turn_map_t *map, uint16_t channel, addr_record_t *record) { + if (!is_valid_channel(channel)) { + JLOG_WARN("Invalid channel number: 0x%hX", channel); + return false; + } + + int pos = find_ordered_channel(map, channel); + if (pos == map->channels_count) + return false; + + const turn_entry_t *entry = map->ordered_channels[pos]; + if (entry->channel != channel || current_timestamp() >= entry->timestamp) + return false; + + if (record) + *record = entry->record; + + return true; +} + +static bool set_transaction_id(turn_map_t *map, turn_entry_type_t type, const addr_record_t *record, + const uint8_t *transaction_id) { + if (type != TURN_ENTRY_TYPE_PERMISSION && type != TURN_ENTRY_TYPE_CHANNEL) + return false; + + turn_entry_t *entry = find_entry(map, record, type, true); + if (!entry) + return false; + + if (entry->type == type && !memory_is_zero(entry->transaction_id, STUN_TRANSACTION_ID_SIZE)) + remove_ordered_transaction_id(map, entry->transaction_id); + + int pos = find_ordered_transaction_id(map, transaction_id); + memmove(map->ordered_transaction_ids + pos + 1, map->ordered_transaction_ids + pos, + (map->transaction_ids_count - pos) * sizeof(turn_entry_t *)); + map->ordered_transaction_ids[pos] = entry; + map->transaction_ids_count++; + + if (entry->type != type) { + entry->type = type; + entry->record = *record; + } + + memcpy(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE); + entry->fresh_transaction_id = true; + return true; +} + +static bool find_transaction_id(turn_map_t *map, const uint8_t *transaction_id, + addr_record_t *record) { + int pos = find_ordered_transaction_id(map, transaction_id); + if (pos == map->transaction_ids_count) + return false; + + const turn_entry_t *entry = map->ordered_transaction_ids[pos]; + if (memcmp(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE) != 0) + return false; + + if (record) + *record = entry->record; + + return true; +} + +static bool set_random_transaction_id(turn_map_t *map, turn_entry_type_t type, + const addr_record_t *record, uint8_t *transaction_id) { + turn_entry_t *entry = find_entry(map, record, type, false); + if (entry && entry->fresh_transaction_id) { + if (transaction_id) + memcpy(transaction_id, entry->transaction_id, STUN_TRANSACTION_ID_SIZE); + + return true; + } + + uint8_t tid[STUN_TRANSACTION_ID_SIZE]; + do { + juice_random(tid, STUN_TRANSACTION_ID_SIZE); + } while (find_transaction_id(map, tid, NULL)); + + if (!set_transaction_id(map, type, record, tid)) + return false; + + if (transaction_id) + memcpy(transaction_id, tid, STUN_TRANSACTION_ID_SIZE); + + return true; +} + +bool turn_set_permission_transaction_id(turn_map_t *map, const addr_record_t *record, + const uint8_t *transaction_id) { + return set_transaction_id(map, TURN_ENTRY_TYPE_PERMISSION, record, transaction_id); +} + +bool turn_set_channel_transaction_id(turn_map_t *map, const addr_record_t *record, + const uint8_t *transaction_id) { + return set_transaction_id(map, TURN_ENTRY_TYPE_CHANNEL, record, transaction_id); +} + +bool turn_set_random_permission_transaction_id(turn_map_t *map, const addr_record_t *record, + uint8_t *transaction_id) { + return set_random_transaction_id(map, TURN_ENTRY_TYPE_PERMISSION, record, transaction_id); +} + +bool turn_set_random_channel_transaction_id(turn_map_t *map, const addr_record_t *record, + uint8_t *transaction_id) { + return set_random_transaction_id(map, TURN_ENTRY_TYPE_CHANNEL, record, transaction_id); +} + +bool turn_retrieve_transaction_id(turn_map_t *map, const uint8_t *transaction_id, + addr_record_t *record) { + int pos = find_ordered_transaction_id(map, transaction_id); + if (pos == map->transaction_ids_count) + return false; + + turn_entry_t *entry = map->ordered_transaction_ids[pos]; + if (memcmp(entry->transaction_id, transaction_id, STUN_TRANSACTION_ID_SIZE) != 0) + return false; + + if (record) + *record = entry->record; + + entry->fresh_transaction_id = false; + return true; +} diff --git a/thirdparty/libjuice/src/turn.h b/thirdparty/libjuice/src/turn.h new file mode 100644 index 0000000..c4e9fc6 --- /dev/null +++ b/thirdparty/libjuice/src/turn.h @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_TURN_H +#define JUICE_TURN_H + +#include "addr.h" +#include "ice.h" +#include "juice.h" +#include "log.h" +#include "stun.h" +#include "timestamp.h" + +#include + +#pragma pack(push, 1) +/* + * TURN ChannelData Message + * See https://www.rfc-editor.org/rfc/rfc8656.html#section-12.4 + * + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | Channel Number | Length | + * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + * | | + * / Application Data / + * / / + * | | + * | +-------------------------------+ + * | | + * +-------------------------------+ + */ + +struct channel_data_header { + uint16_t channel_number; + uint16_t length; +}; + +#pragma pack(pop) + +bool is_channel_data(const void *data, size_t size); +bool is_valid_channel(uint16_t channel); + +int turn_wrap_channel_data(char *buffer, size_t size, const char *data, size_t data_size, + uint16_t channel); + +// TURN state map + +typedef enum turn_entry_type { + TURN_ENTRY_TYPE_EMPTY = 0, + TURN_ENTRY_TYPE_DELETED, + TURN_ENTRY_TYPE_PERMISSION, + TURN_ENTRY_TYPE_CHANNEL +} turn_entry_type_t; + +typedef struct turn_entry { + turn_entry_type_t type; + timestamp_t timestamp; + addr_record_t record; + uint8_t transaction_id[STUN_TRANSACTION_ID_SIZE]; + uint16_t channel; + bool fresh_transaction_id; +} turn_entry_t; + +typedef struct turn_map { + turn_entry_t *map; + turn_entry_t **ordered_channels; + turn_entry_t **ordered_transaction_ids; + int map_size; + int channels_count; + int transaction_ids_count; +} turn_map_t; + +int turn_init_map(turn_map_t *map, int size); +void turn_destroy_map(turn_map_t *map); + +bool turn_set_permission(turn_map_t *map, const uint8_t *transaction_id, + const addr_record_t *record, // record may be NULL + timediff_t duration); +bool turn_has_permission(turn_map_t *map, const addr_record_t *record); + +bool turn_bind_channel(turn_map_t *map, const addr_record_t *record, + const uint8_t *transaction_id, // transaction_id may be NULL + uint16_t channel, timediff_t duration); +bool turn_bind_random_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel, + timediff_t duration); +bool turn_bind_current_channel(turn_map_t *map, const uint8_t *transaction_id, + const addr_record_t *record, // record may be NULL + timediff_t duration); +bool turn_get_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel); +bool turn_get_bound_channel(turn_map_t *map, const addr_record_t *record, uint16_t *channel); +bool turn_find_channel(turn_map_t *map, uint16_t channel, addr_record_t *record); +bool turn_find_bound_channel(turn_map_t *map, uint16_t channel, addr_record_t *record); + +bool turn_set_permission_transaction_id(turn_map_t *map, const addr_record_t *record, + const uint8_t *transaction_id); +bool turn_set_channel_transaction_id(turn_map_t *map, const addr_record_t *record, + const uint8_t *transaction_id); +bool turn_set_random_permission_transaction_id(turn_map_t *map, const addr_record_t *record, + uint8_t *transaction_id); +bool turn_set_random_channel_transaction_id(turn_map_t *map, const addr_record_t *record, + uint8_t *transaction_id); +bool turn_retrieve_transaction_id(turn_map_t *map, const uint8_t *transaction_id, + addr_record_t *record); +#endif diff --git a/thirdparty/libjuice/src/udp.c b/thirdparty/libjuice/src/udp.c new file mode 100644 index 0000000..7c7582e --- /dev/null +++ b/thirdparty/libjuice/src/udp.c @@ -0,0 +1,604 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "udp.h" +#include "addr.h" +#include "log.h" +#include "random.h" +#include "thread.h" // for mutexes + +#include +#include +#include +#include + +static struct addrinfo *find_family(struct addrinfo *ai_list, int family) { + struct addrinfo *ai = ai_list; + while (ai && ai->ai_family != family) + ai = ai->ai_next; + return ai; +} + +static uint16_t get_next_port_in_range(uint16_t begin, uint16_t end) { + if (begin == 0) + begin = 1024; + if (end == 0) + end = 0xFFFF; + if (begin == end) + return begin; + + static volatile uint32_t count = 0; + if (count == 0) + count = juice_rand32(); + + static mutex_t mutex = MUTEX_INITIALIZER; + mutex_lock(&mutex); + uint32_t diff = end > begin ? end - begin : 0; + uint16_t next = begin + count++ % (diff + 1); + mutex_unlock(&mutex); + return next; +} + +socket_t udp_create_socket(const udp_socket_config_t *config) { + socket_t sock = INVALID_SOCKET; + + // Obtain local Address + struct addrinfo *ai_list = NULL; + struct addrinfo hints; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV; + if (getaddrinfo(config->bind_address, "0", &hints, &ai_list) != 0) { + JLOG_ERROR("getaddrinfo for binding address failed, errno=%d", sockerrno); + return INVALID_SOCKET; + } + + // Create socket + struct addrinfo *ai = NULL; + const int families[2] = {AF_INET6, AF_INET}; // Prefer IPv6 + const char *names[2] = {"IPv6", "IPv4"}; + for (int i = 0; i < 2; ++i) { + ai = find_family(ai_list, families[i]); + if (!ai) + continue; + + sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (sock == INVALID_SOCKET) { + JLOG_WARN("UDP socket creation for %s family failed, errno=%d", names[i], sockerrno); + continue; + } + + break; + } + + if (sock == INVALID_SOCKET) { + JLOG_ERROR("UDP socket creation failed: no suitable address family"); + goto error; + } + + assert(ai != NULL); + + // Listen on both IPv6 and IPv4 + const sockopt_t disabled = 0; + if (ai->ai_family == AF_INET6) + setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, (const char *)&disabled, sizeof(disabled)); + + // Set DF flag +#ifndef NO_PMTUDISC + const sockopt_t val = IP_PMTUDISC_DO; + setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, (const char *)&val, sizeof(val)); +#ifdef IPV6_MTU_DISCOVER + if (ai->ai_family == AF_INET6) + setsockopt(sock, IPPROTO_IPV6, IPV6_MTU_DISCOVER, (const char *)&val, sizeof(val)); +#endif +#else + // It seems Mac OS lacks a way to set the DF flag... + const sockopt_t enabled = 1; +#ifdef IP_DONTFRAG + setsockopt(sock, IPPROTO_IP, IP_DONTFRAG, (const char *)&enabled, sizeof(enabled)); +#endif +#ifdef IPV6_DONTFRAG + if (ai->ai_family == AF_INET6) + setsockopt(sock, IPPROTO_IPV6, IPV6_DONTFRAG, (const char *)&enabled, sizeof(enabled)); +#endif +#endif + + // Set buffer size up to 1 MiB for performance + const sockopt_t buffer_size = 1 * 1024 * 1024; + setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (const char *)&buffer_size, sizeof(buffer_size)); + setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (const char *)&buffer_size, sizeof(buffer_size)); + + ctl_t nbio = 1; + if (ioctlsocket(sock, FIONBIO, &nbio)) { + JLOG_ERROR("Setting non-blocking mode on UDP socket failed, errno=%d", sockerrno); + goto error; + } + + // Bind it + if (config->port_begin == 0 && config->port_end == 0) { + if (bind(sock, ai->ai_addr, (socklen_t)ai->ai_addrlen) == 0) { + JLOG_DEBUG("UDP socket bound to %s:%hu", + config->bind_address ? config->bind_address : "any", udp_get_port(sock)); + freeaddrinfo(ai_list); + return sock; + } + + JLOG_ERROR("UDP socket binding failed, errno=%d", sockerrno); + + } else if (config->port_begin == config->port_end) { + uint16_t port = config->port_begin; + struct sockaddr_storage addr; + socklen_t addrlen = (socklen_t)ai->ai_addrlen; + memcpy(&addr, ai->ai_addr, addrlen); + addr_set_port((struct sockaddr *)&addr, port); + + if (bind(sock, (struct sockaddr *)&addr, addrlen) == 0) { + JLOG_DEBUG("UDP socket bound to %s:%hu", + config->bind_address ? config->bind_address : "any", port); + freeaddrinfo(ai_list); + return sock; + } + + JLOG_ERROR("UDP socket binding failed on port %hu, errno=%d", port, sockerrno); + + } else { + struct sockaddr_storage addr; + socklen_t addrlen = (socklen_t)ai->ai_addrlen; + memcpy(&addr, ai->ai_addr, addrlen); + + int retries = config->port_end - config->port_begin; + do { + uint16_t port = get_next_port_in_range(config->port_begin, config->port_end); + addr_set_port((struct sockaddr *)&addr, port); + if (bind(sock, (struct sockaddr *)&addr, addrlen) == 0) { + JLOG_DEBUG("UDP socket bound to %s:%hu", + config->bind_address ? config->bind_address : "any", port); + freeaddrinfo(ai_list); + return sock; + } + } while ((sockerrno == SEADDRINUSE || sockerrno == SEACCES) && retries-- > 0); + + JLOG_ERROR("UDP socket binding failed on port range %s:[%hu,%hu], errno=%d", + config->bind_address ? config->bind_address : "any", config->port_begin, + config->port_end, sockerrno); + } + +error: + freeaddrinfo(ai_list); + if (sock != INVALID_SOCKET) + closesocket(sock); + + return INVALID_SOCKET; +} + +int udp_recvfrom(socket_t sock, char *buffer, size_t size, addr_record_t *src) { + while (true) { + src->len = sizeof(src->addr); + int len = + recvfrom(sock, buffer, (socklen_t)size, 0, (struct sockaddr *)&src->addr, &src->len); + if (len >= 0) { + addr_unmap_inet6_v4mapped((struct sockaddr *)&src->addr, &src->len); + + } else if (sockerrno == SECONNRESET || sockerrno == SENETRESET || + sockerrno == SECONNREFUSED) { + // On Windows, if a UDP socket receives an ICMP port unreachable response after + // sending a datagram, this error is stored, and the next call to recvfrom() returns + // WSAECONNRESET (port unreachable) or WSAENETRESET (TTL expired). + // Therefore, it may be ignored. + JLOG_DEBUG("Ignoring %s returned by recvfrom", + sockerrno == SECONNRESET + ? "ECONNRESET" + : (sockerrno == SENETRESET ? "ENETRESET" : "ECONNREFUSED")); + continue; + } + return len; + } +} + +int udp_sendto(socket_t sock, const char *data, size_t size, const addr_record_t *dst) { +#ifndef __linux__ + addr_record_t tmp = *dst; + addr_record_t name; + name.len = sizeof(name.addr); + if (getsockname(sock, (struct sockaddr *)&name.addr, &name.len) == 0) { + if (name.addr.ss_family == AF_INET6) + addr_map_inet6_v4mapped(&tmp.addr, &tmp.len); + } else { + JLOG_WARN("getsockname failed, errno=%d", sockerrno); + } + return sendto(sock, data, (socklen_t)size, 0, (const struct sockaddr *)&tmp.addr, tmp.len); +#else + return sendto(sock, data, size, 0, (const struct sockaddr *)&dst->addr, dst->len); +#endif +} + +int udp_sendto_self(socket_t sock, const char *data, size_t size) { + addr_record_t local; + if (udp_get_local_addr(sock, AF_UNSPEC, &local) < 0) + return -1; + + int ret; +#ifndef __linux__ + // We know local has the same address family as sock here + ret = sendto(sock, data, (socklen_t)size, 0, (const struct sockaddr *)&local.addr, local.len); +#else + ret = sendto(sock, data, size, 0, (const struct sockaddr *)&local.addr, local.len); +#endif + if (ret >= 0 || local.addr.ss_family != AF_INET6) + return ret; + + // Fallback as IPv6 may be disabled on the loopback interface + if (udp_get_local_addr(sock, AF_INET, &local) < 0) + return -1; + +#ifndef __linux__ + addr_map_inet6_v4mapped(&local.addr, &local.len); + return sendto(sock, data, (socklen_t)size, 0, (const struct sockaddr *)&local.addr, local.len); +#else + return sendto(sock, data, size, 0, (const struct sockaddr *)&local.addr, local.len); +#endif +} + +int udp_set_diffserv(socket_t sock, int ds) { +#ifdef _WIN32 + // IP_TOS has been intentionally broken on Windows in favor of a convoluted proprietary + // mechanism called qWave. Thank you Microsoft! + // TODO: Investigate if DSCP can be still set directly without administrator flow configuration. + (void)sock; + (void)ds; + JLOG_INFO("IP Differentiated Services are not supported on Windows"); + return -1; +#else + addr_record_t name; + name.len = sizeof(name.addr); + if (getsockname(sock, (struct sockaddr *)&name.addr, &name.len) < 0) { + JLOG_WARN("getsockname failed, errno=%d", sockerrno); + return -1; + } + + switch (name.addr.ss_family) { + case AF_INET: +#ifdef IP_TOS + if (setsockopt(sock, IPPROTO_IP, IP_TOS, &ds, sizeof(ds)) < 0) { + JLOG_WARN("Setting IP ToS failed, errno=%d", sockerrno); + return -1; + } + return 0; +#else + JLOG_INFO("Setting IP ToS is not supported"); + return -1; +#endif + + case AF_INET6: +#ifdef IPV6_TCLASS + if (setsockopt(sock, IPPROTO_IPV6, IPV6_TCLASS, &ds, sizeof(ds)) < 0) { + JLOG_WARN("Setting IPv6 traffic class failed, errno=%d", sockerrno); + return -1; + } +#ifdef IP_TOS + // Attempt to also set IP_TOS for IPv4, in case the system requires it + setsockopt(sock, IPPROTO_IP, IP_TOS, &ds, sizeof(ds)); +#endif + return 0; +#else + JLOG_INFO("Setting IPv6 traffic class is not supported"); + return -1; +#endif + default: + return -1; + } +#endif +} + +uint16_t udp_get_port(socket_t sock) { + addr_record_t record; + if (udp_get_bound_addr(sock, &record) < 0) + return 0; + return addr_get_port((struct sockaddr *)&record.addr); +} + +int udp_get_bound_addr(socket_t sock, addr_record_t *record) { + record->len = sizeof(record->addr); + if (getsockname(sock, (struct sockaddr *)&record->addr, &record->len)) { + JLOG_WARN("getsockname failed, errno=%d", sockerrno); + return -1; + } + return 0; +} + +int udp_get_local_addr(socket_t sock, int family_hint, addr_record_t *record) { + if (udp_get_bound_addr(sock, record) < 0) + return -1; + + // If the socket is bound to a particular address, return it + if (!addr_is_any((struct sockaddr *)&record->addr)) { + if (record->addr.ss_family == AF_INET && family_hint == AF_INET6) + addr_map_inet6_v4mapped(&record->addr, &record->len); + + return 0; + } + + if (record->addr.ss_family == AF_INET6 && family_hint == AF_INET) { + // Generate an IPv4 instead (socket is listening to any IPv4 or IPv6) + + uint16_t port = addr_get_port((struct sockaddr *)&record->addr); + if (port == 0) + return -1; + + struct sockaddr_in *sin = (struct sockaddr_in *)&record->addr; + memset(sin, 0, sizeof(*sin)); + sin->sin_family = AF_INET; + sin->sin_port = htons(port); + record->len = sizeof(*sin); + } + + switch (record->addr.ss_family) { + case AF_INET: { + struct sockaddr_in *sin = (struct sockaddr_in *)&record->addr; + const uint8_t localhost[4] = {127, 0, 0, 1}; + memcpy(&sin->sin_addr, localhost, 4); + break; + } + case AF_INET6: { + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&record->addr; + uint8_t *b = (uint8_t *)&sin6->sin6_addr; + memset(b, 0, 15); + b[15] = 0x01; // localhost + break; + } + default: + // Ignore + break; + } + + if (record->addr.ss_family == AF_INET && family_hint == AF_INET6) + addr_map_inet6_v4mapped(&record->addr, &record->len); + + return 0; +} + +// Helper function to check if a similar address already exists in records +// This function ignores the port +static int has_duplicate_addr(struct sockaddr *addr, const addr_record_t *records, size_t count) { + for (size_t i = 0; i < count; ++i) { + const addr_record_t *record = records + i; + if (record->addr.ss_family == addr->sa_family) { + switch (addr->sa_family) { + case AF_INET: { + // For IPv4, compare the whole address + const struct sockaddr_in *rsin = (const struct sockaddr_in *)&record->addr; + const struct sockaddr_in *asin = (const struct sockaddr_in *)addr; + if (memcmp(&rsin->sin_addr, &asin->sin_addr, 4) == 0) + return true; + break; + } + case AF_INET6: { + // For IPv6, compare the network part only + const struct sockaddr_in6 *rsin6 = (const struct sockaddr_in6 *)&record->addr; + const struct sockaddr_in6 *asin6 = (const struct sockaddr_in6 *)addr; + if (memcmp(&rsin6->sin6_addr, &asin6->sin6_addr, 8) == 0) // compare first 64 bits + return true; + break; + } + } + } + } + return false; +} + +#if !defined(_WIN32) && defined(NO_IFADDRS) +// Helper function to get the IPv6 address of the default interface +static int get_local_default_inet6(uint16_t port, struct sockaddr_in6 *result) { + const char *dummy_host = "2001:db8::1"; // dummy public unreachable address + const uint16_t dummy_port = 9; // discard port + + struct sockaddr_in6 sin6; + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_port = htons(dummy_port); + if (inet_pton(AF_INET6, dummy_host, &sin6.sin6_addr) != 1) + return -1; + + socket_t sock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock == INVALID_SOCKET) + return -1; + + if (connect(sock, (const struct sockaddr *)&sin6, sizeof(sin6))) + goto error; + + socklen_t result_len = sizeof(*result); + if (getsockname(sock, (struct sockaddr *)result, &result_len)) + goto error; + + if (result_len != sizeof(*result)) + goto error; + + addr_set_port((struct sockaddr *)result, port); + closesocket(sock); + return 0; + +error: + closesocket(sock); + return -1; +} +#endif + +int udp_get_addrs(socket_t sock, addr_record_t *records, size_t count) { + addr_record_t bound; + if (udp_get_bound_addr(sock, &bound) < 0) { + JLOG_ERROR("Getting UDP bound address failed"); + return -1; + } + + if (!addr_is_any((struct sockaddr *)&bound.addr)) { + if (count > 0) + records[0] = bound; + + return 1; + } + + uint16_t port = addr_get_port((struct sockaddr *)&bound.addr); + + // RFC 8445 5.1.1.1. Host Candidates: + // Addresses from a loopback interface MUST NOT be included in the candidate addresses. + // [...] + // If gathering one or more host candidates that correspond to an IPv6 address that was + // generated using a mechanism that prevents location tracking [RFC7721], host candidates + // that correspond to IPv6 addresses that do allow location tracking, are configured on the + // same interface, and are part of the same network prefix MUST NOT be gathered. Similarly, + // when host candidates corresponding to an IPv6 address generated using a mechanism that + // prevents location tracking are gathered, then host candidates corresponding to IPv6 + // link-local addresses [RFC4291] MUST NOT be gathered. The IPv6 default address selection + // specification [RFC6724] specifies that temporary addresses [RFC4941] are to be preferred + // over permanent addresses. + + // IPv6 IIDs generated by modern systems are opaque so there is no way to reliably differentiate + // privacy-enabled IPv6 addresses here. Therefore, we hope the preferred addresses are listed + // first, and we never list link-local addresses. + + addr_record_t *current = records; + addr_record_t *end = records + count; + int ret = 0; + +#if JUICE_ENABLE_LOCALHOST_ADDRESS + // Add localhost for test purposes + addr_record_t local; + if (bound.addr.ss_family == AF_INET6 && udp_get_local_addr(sock, AF_INET6, &local) == 0) { + ++ret; + if (current != end) { + *current = local; + ++current; + } + } + if (udp_get_local_addr(sock, AF_INET, &local) == 0) { + ++ret; + if (current != end) { + *current = local; + ++current; + } + } +#endif + +#ifdef _WIN32 + char buf[4096]; + DWORD len = 0; + if (WSAIoctl(sock, SIO_ADDRESS_LIST_QUERY, NULL, 0, buf, sizeof(buf), &len, NULL, NULL)) { + JLOG_ERROR("WSAIoctl with SIO_ADDRESS_LIST_QUERY failed, errno=%d", WSAGetLastError()); + return -1; + } + + SOCKET_ADDRESS_LIST *list = (SOCKET_ADDRESS_LIST *)buf; + for (int i = 0; i < list->iAddressCount; ++i) { + struct sockaddr *sa = list->Address[i].lpSockaddr; + socklen_t len = list->Address[i].iSockaddrLength; + if ((sa->sa_family == AF_INET || + (sa->sa_family == AF_INET6 && bound.addr.ss_family == AF_INET6)) && + !addr_is_local(sa)) { + if (!has_duplicate_addr(sa, records, current - records)) { + ++ret; + if (current != end) { + memcpy(¤t->addr, sa, len); + current->len = len; + addr_unmap_inet6_v4mapped((struct sockaddr *)¤t->addr, ¤t->len); + addr_set_port((struct sockaddr *)¤t->addr, port); + ++current; + } + } + } + } +#else // POSIX +#ifndef NO_IFADDRS + struct ifaddrs *ifas; + if (getifaddrs(&ifas)) { + JLOG_ERROR("getifaddrs failed, errno=%d", sockerrno); + return -1; + } + + for (struct ifaddrs *ifa = ifas; ifa; ifa = ifa->ifa_next) { + unsigned int flags = ifa->ifa_flags; + if (!(flags & IFF_UP) || (flags & IFF_LOOPBACK)) + continue; + if (strcmp(ifa->ifa_name, "docker0") == 0) + continue; + + struct sockaddr *sa = ifa->ifa_addr; + socklen_t len; + if (sa && + (sa->sa_family == AF_INET || + (sa->sa_family == AF_INET6 && bound.addr.ss_family == AF_INET6)) && + !addr_is_local(sa) && (len = addr_get_len(sa)) > 0) { + if (!has_duplicate_addr(sa, records, current - records)) { + ++ret; + if (current != end) { + memcpy(¤t->addr, sa, len); + current->len = len; + addr_set_port((struct sockaddr *)¤t->addr, port); + ++current; + } + } + } + } + + freeifaddrs(ifas); + +#else // NO_IFADDRS defined + char buf[4096]; + struct ifconf ifc; + memset(&ifc, 0, sizeof(ifc)); + ifc.ifc_len = sizeof(buf); + ifc.ifc_buf = buf; + + if (ioctlsocket(sock, SIOCGIFCONF, &ifc)) { + JLOG_ERROR("ioctl for SIOCGIFCONF failed, errno=%d", sockerrno); + return -1; + } + + bool ifconf_has_inet6 = false; + int n = ifc.ifc_len / sizeof(struct ifreq); + for (int i = 0; i < n; ++i) { + struct ifreq *ifr = ifc.ifc_req + i; + struct sockaddr *sa = &ifr->ifr_addr; + if (sa->sa_family == AF_INET6) + ifconf_has_inet6 = true; + + socklen_t len; + if ((sa->sa_family == AF_INET || + (sa->sa_family == AF_INET6 && bound.addr.ss_family == AF_INET6)) && + !addr_is_local(sa) && (len = addr_get_len(sa)) > 0) { + if (!has_duplicate_addr(sa, records, current - records)) { + ++ret; + if (current != end) { + memcpy(¤t->addr, sa, len); + current->len = len; + addr_set_port((struct sockaddr *)¤t->addr, port); + ++current; + } + } + } + } + + if (!ifconf_has_inet6 && bound.addr.ss_family == AF_INET6) { + struct sockaddr_in6 sin6; + if (get_local_default_inet6(port, &sin6) == 0) { + if (!addr_is_local((const struct sockaddr *)&sin6)) { + ++ret; + if (current != end) { + memcpy(¤t->addr, &sin6, sizeof(sin6)); + current->len = sizeof(sin6); + ++current; + } + } + } + } +#endif +#endif + + return ret; +} diff --git a/thirdparty/libjuice/src/udp.h b/thirdparty/libjuice/src/udp.h new file mode 100644 index 0000000..c318eec --- /dev/null +++ b/thirdparty/libjuice/src/udp.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef JUICE_UDP_H +#define JUICE_UDP_H + +#include "addr.h" +#include "socket.h" + +#include + +typedef struct udp_socket_config { + const char *bind_address; + uint16_t port_begin; + uint16_t port_end; +} udp_socket_config_t; + +socket_t udp_create_socket(const udp_socket_config_t *config); +int udp_recvfrom(socket_t sock, char *buffer, size_t size, addr_record_t *src); +int udp_sendto(socket_t sock, const char *data, size_t size, const addr_record_t *dst); +int udp_sendto_self(socket_t sock, const char *data, size_t size); +int udp_set_diffserv(socket_t sock, int ds); +uint16_t udp_get_port(socket_t sock); +int udp_get_bound_addr(socket_t sock, addr_record_t *record); +int udp_get_local_addr(socket_t sock, int family, addr_record_t *record); // family may be AF_UNSPEC +int udp_get_addrs(socket_t sock, addr_record_t *records, size_t count); + +#endif // JUICE_UDP_H diff --git a/thirdparty/libjuice/test/base64.c b/thirdparty/libjuice/test/base64.c new file mode 100644 index 0000000..1525c96 --- /dev/null +++ b/thirdparty/libjuice/test/base64.c @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "base64.h" + +#include +#include + +#define BUFFER_SIZE 1024 + +int test_base64(void) { + const char *str = "Man is distinguished, not only by his reason, but by this singular passion " + "from other animals, which is a lust of the mind, that by a perseverance of " + "delight in the continued and indefatigable generation of knowledge, exceeds " + "the short vehemence of any carnal pleasure."; + const char *expected = + "TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIH" + "Bhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBw" + "ZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb2" + "4gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4" + "="; + + char buffer1[BUFFER_SIZE]; + if (BASE64_ENCODE(str, strlen(str), buffer1, BUFFER_SIZE) <= 0) + return -1; + + if (strcmp(buffer1, expected) != 0) + return -1; + + char buffer2[BUFFER_SIZE]; + int len = BASE64_DECODE(buffer1, buffer2, BUFFER_SIZE); + if (len <= 0) + return -1; + + buffer2[len] = '\0'; + if (strcmp(buffer2, str) != 0) + return -1; + + return 0; +} diff --git a/thirdparty/libjuice/test/bind.c b/thirdparty/libjuice/test/bind.c new file mode 100644 index 0000000..4bd2b62 --- /dev/null +++ b/thirdparty/libjuice/test/bind.c @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 +#define BIND_ADDRESS "127.0.0.1" + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_bind() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + + config1.bind_address = BIND_ADDRESS; + + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + + config2.bind_address = BIND_ADDRESS; + + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + bool success = true; + /* + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + */ + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Filter host candidates for the bind address + if(!strstr(sdp, "host") || !strstr(sdp, BIND_ADDRESS)) + return; + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Filter host candidates for the bind address + if(!strstr(sdp, "host") || !strstr(sdp, BIND_ADDRESS)) + return; + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/conflict.c b/thirdparty/libjuice/test/conflict.c new file mode 100644 index 0000000..7b34180 --- /dev/null +++ b/thirdparty/libjuice/test/conflict.c @@ -0,0 +1,203 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_conflict() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Setting the remote description now on both agents will hint them both into controlling mode, + // creating an ICE role conflict + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/connectivity.c b/thirdparty/libjuice/test/connectivity.c new file mode 100644 index 0000000..53778cd --- /dev/null +++ b/thirdparty/libjuice/test/connectivity.c @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_connectivity() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + + // STUN server example + config1.stun_server_host = "stun.l.google.com"; + config1.stun_server_port = 19302; + + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + + // STUN server example + config2.stun_server_host = "stun.l.google.com"; + config2.stun_server_port = 19302; + + // Port range example + config2.local_port_range_begin = 60000; + config2.local_port_range_end = 61000; + + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/crc32.c b/thirdparty/libjuice/test/crc32.c new file mode 100644 index 0000000..bf24d7f --- /dev/null +++ b/thirdparty/libjuice/test/crc32.c @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "crc32.h" + +#include +#include + +int test_crc32(void) { + const char *str = "The quick brown fox jumps over the lazy dog"; + uint32_t expected = 0x414fa339; + + if (CRC32(str, strlen(str)) != expected) + return -1; + + return 0; +} diff --git a/thirdparty/libjuice/test/gathering.c b/thirdparty/libjuice/test/gathering.c new file mode 100644 index 0000000..6cfddfd --- /dev/null +++ b/thirdparty/libjuice/test/gathering.c @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent; +static bool success = false; +static bool done = false; + +static void on_state_changed(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_candidate(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_gathering_done(juice_agent_t *agent, void *user_ptr); + +int test_gathering() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Create agent + juice_config_t config; + memset(&config, 0, sizeof(config)); + + // STUN server example + config.stun_server_host = "stun.l.google.com"; + config.stun_server_port = 19302; + + config.cb_state_changed = on_state_changed; + config.cb_candidate = on_candidate; + config.cb_gathering_done = on_gathering_done; + config.user_ptr = NULL; + + agent = juice_create(&config); + + // Generate local description + char sdp[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent, sdp, JUICE_MAX_SDP_STRING_LEN); + printf("Local description:\n%s\n", sdp); + + // Gather candidates + juice_gather_candidates(agent); + + // Wait until gathering done + int secs = 10; + while (secs-- && !done && !success) + sleep(1); + + // Destroy + juice_destroy(agent); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// On state changed +static void on_state_changed(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State: %s\n", juice_state_to_string(state)); +} + +// On local candidate gathered +static void on_candidate(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate: %s\n", sdp); + + // Success if a valid srflx candidate is emitted + if (strstr(sdp, " typ srflx raddr 0.0.0.0 rport 0")) + success = true; +} + +// On local candidates gathering done +static void on_gathering_done(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done\n"); + + done = true; +} diff --git a/thirdparty/libjuice/test/main.c b/thirdparty/libjuice/test/main.c new file mode 100644 index 0000000..5caeba7 --- /dev/null +++ b/thirdparty/libjuice/test/main.c @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include + +int test_crc32(void); +int test_base64(void); +int test_stun(void); +int test_connectivity(void); +int test_thread(void); +int test_mux(void); +int test_notrickle(void); +int test_gathering(void); +int test_turn(void); +int test_conflict(void); +int test_bind(void); + +#ifndef NO_SERVER +int test_server(void); +#endif + +int main(int argc, char **argv) { + juice_set_log_level(JUICE_LOG_LEVEL_WARN); + + printf("\nRunning CRC32 implementation test...\n"); + if (test_crc32()) { + fprintf(stderr, "CRC32 implementation test failed\n"); + return -2; + } + + printf("\nRunning base64 implementation test...\n"); + if (test_base64()) { + fprintf(stderr, "base64 implementation test failed\n"); + return -2; + } + + printf("\nRunning STUN parsing implementation test...\n"); + if (test_stun()) { + fprintf(stderr, "STUN parsing implementation test failed\n"); + return -3; + } + + printf("\nRunning candidates gathering test...\n"); + if (test_gathering()) { + fprintf(stderr, "Candidates gathering test failed\n"); + return -1; + } + + printf("\nRunning connectivity test...\n"); + if (test_connectivity()) { + fprintf(stderr, "Connectivity test failed\n"); + return -1; + } + +// Disabled as the Open Relay TURN server is unreliable +/* + printf("\nRunning TURN connectivity test...\n"); + if (test_turn()) { + fprintf(stderr, "TURN connectivity test failed\n"); + return -1; + } +*/ + printf("\nRunning thread-mode connectivity test...\n"); + if (test_thread()) { + fprintf(stderr, "Thread-mode connectivity test failed\n"); + return -1; + } + + printf("\nRunning mux-mode connectivity test...\n"); + if (test_mux()) { + fprintf(stderr, "Mux-mode connectivity test failed\n"); + return -1; + } + + printf("\nRunning non-trickled connectivity test...\n"); + if (test_notrickle()) { + fprintf(stderr, "Non-trickled connectivity test failed\n"); + return -1; + } + + printf("\nRunning connectivity test with role conflict...\n"); + if (test_conflict()) { + fprintf(stderr, "Connectivity test with role conflict failed\n"); + return -1; + } + + printf("\nRunning connectivity test with bind address...\n"); + if (test_bind()) { + fprintf(stderr, "Connectivity test with bind address failed\n"); + return -1; + } + +#ifndef NO_SERVER + printf("\nRunning server test...\n"); + if (test_server()) { + fprintf(stderr, "Server test failed\n"); + return -1; + } +#endif + + return 0; +} + diff --git a/thirdparty/libjuice/test/mux.c b/thirdparty/libjuice/test/mux.c new file mode 100644 index 0000000..4262d5b --- /dev/null +++ b/thirdparty/libjuice/test/mux.c @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +bool endswith(const char *str, const char *suffix) { + size_t str_len = strlen(str); + size_t suffix_len = strlen(suffix); + return str_len >= suffix_len && memcmp(str + str_len - suffix_len, suffix, suffix_len) == 0; +} + +int test_mux() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + config1.stun_server_host = "stun.l.google.com"; + config1.stun_server_port = 19302; + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + agent1 = juice_create(&config1); + + // Agent 2: Create agent in mux mode on port 60000 + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + config2.concurrency_mode = JUICE_CONCURRENCY_MODE_MUX; + config2.local_port_range_begin = 60000; + config2.local_port_range_end = 60000; + config2.cb_state_changed = on_state_changed1; + config2.cb_candidate = on_candidate1; + config2.cb_gathering_done = on_gathering_done1; + config2.cb_recv = on_recv1; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + success &= endswith(remoteAddr, ":60000"); + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + success &= endswith(localAddr, ":60000"); + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/notrickle.c b/thirdparty/libjuice/test/notrickle.c new file mode 100644 index 0000000..42d466d --- /dev/null +++ b/thirdparty/libjuice/test/notrickle.c @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_notrickle() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + + // STUN server example + config1.stun_server_host = "stun.l.google.com"; + config1.stun_server_port = 19302; + + config1.cb_state_changed = on_state_changed1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + + // STUN server example + config2.stun_server_host = "stun.l.google.com"; + config2.stun_server_port = 19302; + + config2.cb_state_changed = on_state_changed2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Gather candidates + juice_gather_candidates(agent1); + + sleep(4); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Gather candidates + juice_gather_candidates(agent2); +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/server.c b/thirdparty/libjuice/test/server.c new file mode 100644 index 0000000..b40852d --- /dev/null +++ b/thirdparty/libjuice/test/server.c @@ -0,0 +1,274 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#ifndef NO_SERVER + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +#define TURN_USERNAME1 "server_test1" +#define TURN_PASSWORD1 "79874638521694" + +#define TURN_USERNAME2 "server_test2" +#define TURN_PASSWORD2 "36512189907731" + +static juice_server_t *server; +static juice_agent_t *agent1; +static juice_agent_t *agent2; +static bool srflx_success = false; +static bool relay_success = false; +static bool success = false; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_server() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Create server + juice_server_credentials_t credentials[1]; + memset(&credentials, 0, sizeof(credentials)); + credentials[0].username = TURN_USERNAME1; + credentials[0].password = TURN_PASSWORD1; + + juice_server_config_t server_config; + memset(&server_config, 0, sizeof(server_config)); + server_config.port = 3478; + server_config.credentials = credentials; + server_config.credentials_count = 1; + server_config.max_allocations = 100; + server_config.realm = "Juice test server"; + server = juice_server_create(&server_config); + + if(juice_server_get_port(server) != 3478) { + printf("juice_server_get_port failed\n"); + juice_server_destroy(server); + return -1; + } + + // Added credentials example + juice_server_credentials_t added_credentials[1]; + memset(&added_credentials, 0, sizeof(added_credentials)); + added_credentials[0].username = TURN_USERNAME2; + added_credentials[0].password = TURN_PASSWORD2; + juice_server_add_credentials(server, added_credentials, 60000); // 60s + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + + // Set STUN server + config1.stun_server_host = "localhost"; + config1.stun_server_port = 3478; + + // Set TURN server + juice_turn_server_t turn_server1; + memset(&turn_server1, 0, sizeof(turn_server1)); + turn_server1.host = "localhost"; + turn_server1.port = 3478; + turn_server1.username = TURN_USERNAME1; + turn_server1.password = TURN_PASSWORD1; + config1.turn_servers = &turn_server1; + config1.turn_servers_count = 1; + + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + + // Set STUN server + config2.stun_server_host = "localhost"; + config2.stun_server_port = 3478; + + // Set TURN server + juice_turn_server_t turn_server2; + memset(&turn_server2, 0, sizeof(turn_server2)); + turn_server2.host = "localhost"; + turn_server2.port = 3478; + turn_server2.username = TURN_USERNAME2; + turn_server2.password = TURN_PASSWORD2; + config2.turn_servers = &turn_server2; + config2.turn_servers_count = 1; + + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + // Destroy server + juice_server_destroy(server); + + if (srflx_success && relay_success && success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Success if a valid srflx candidate is emitted + if (strstr(sdp, " typ srflx raddr 0.0.0.0 rport 0")) + srflx_success = true; + + // Success if a valid relay candidate is emitted + if (strstr(sdp, " typ relay raddr 0.0.0.0 rport 0")) + relay_success = true; + + // Filter relayed candidates + if (!strstr(sdp, "relay")) + return; + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Success if a valid srflx candidate is emitted + if (strstr(sdp, " typ srflx raddr 0.0.0.0 rport 0")) + srflx_success = true; + + // Success if a valid relay candidate is emitted + if (strstr(sdp, " typ relay raddr 0.0.0.0 rport 0")) + relay_success = true; + + // Filter relayed candidates + if (!strstr(sdp, "relay")) + return; + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); + success = true; +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); + success = true; +} + +#endif // ifndef NO_SERVER diff --git a/thirdparty/libjuice/test/stun.c b/thirdparty/libjuice/test/stun.c new file mode 100644 index 0000000..361da0b --- /dev/null +++ b/thirdparty/libjuice/test/stun.c @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "stun.h" + +#include +#include + +int test_stun(void) { + stun_message_t msg; + + uint8_t message1[] = { + 0x00, 0x01, 0x00, 0x58, // Request type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0xb7, 0xe7, 0xa7, 0x01, // Transaction ID + 0xbc, 0x34, 0xd6, 0x86, // + 0xfa, 0x87, 0xdf, 0xae, // + 0x80, 0x22, 0x00, 0x10, // SOFTWARE attribute header + 0x53, 0x54, 0x55, 0x4e, // + 0x20, 0x74, 0x65, 0x73, // + 0x74, 0x20, 0x63, 0x6c, // + 0x69, 0x65, 0x6e, 0x74, // + 0x00, 0x24, 0x00, 0x04, // PRIORITY attribute header + 0x6e, 0x00, 0x01, 0xff, // + 0x80, 0x29, 0x00, 0x08, // ICE-CONTROLLED attribute header + 0x93, 0x2f, 0xf9, 0xb1, // + 0x51, 0x26, 0x3b, 0x36, // + 0x00, 0x06, 0x00, 0x09, // USERNAME attribute header + 0x65, 0x76, 0x74, 0x6a, // + 0x3a, 0x68, 0x36, 0x76, // + 0x59, 0x20, 0x20, 0x20, // + 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header + 0x9a, 0xea, 0xa7, 0x0c, // + 0xbf, 0xd8, 0xcb, 0x56, // + 0x78, 0x1e, 0xf2, 0xb5, // + 0xb2, 0xd3, 0xf2, 0x49, // + 0xc1, 0xb5, 0x71, 0xa2, // + 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header + 0xe5, 0x7a, 0x3b, 0xcf, // + }; + + memset(&msg, 0, sizeof(msg)); + + if (_juice_stun_read(message1, sizeof(message1), &msg) <= 0) + return -1; + + if(msg.msg_class != STUN_CLASS_REQUEST || msg.msg_method != STUN_METHOD_BINDING) + return -1; + + if (memcmp(msg.transaction_id, message1 + 8, 12) != 0) + return -1; + + if (msg.priority != 0x6e0001ff) + return -1; + + if (msg.ice_controlled != 0x932ff9b151263b36LL) + return -1; + + if (!msg.has_integrity) + return -1; + + if (!_juice_stun_check_integrity(message1, sizeof(message1), &msg, "VOkJxbRl1RmTxUk/WvJxBt")) + return -1; + + if(msg.error_code != 0) + return -1; + + // The test vector in RFC 8489 is completely wrong + // See https://www.rfc-editor.org/errata_search.php?rfc=8489 + uint8_t message2[] = { + 0x00, 0x01, 0x00, 0x90, // Request type and message length + 0x21, 0x12, 0xa4, 0x42, // Magic cookie + 0x78, 0xad, 0x34, 0x33, // Transaction ID + 0xc6, 0xad, 0x72, 0xc0, // + 0x29, 0xda, 0x41, 0x2e, // + 0x00, 0x1e, 0x00, 0x20, // USERHASH attribute header + 0x4a, 0x3c, 0xf3, 0x8f, // Userhash value (32 bytes) + 0xef, 0x69, 0x92, 0xbd, // + 0xa9, 0x52, 0xc6, 0x78, // + 0x04, 0x17, 0xda, 0x0f, // + 0x24, 0x81, 0x94, 0x15, // + 0x56, 0x9e, 0x60, 0xb2, // + 0x05, 0xc4, 0x6e, 0x41, // + 0x40, 0x7f, 0x17, 0x04, // + 0x00, 0x15, 0x00, 0x29, // NONCE attribute header + 0x6f, 0x62, 0x4d, 0x61, // Nonce value and padding (3 bytes) + 0x74, 0x4a, 0x6f, 0x73, // + 0x32, 0x41, 0x41, 0x41, // + 0x43, 0x66, 0x2f, 0x2f, // + 0x34, 0x39, 0x39, 0x6b, // + 0x39, 0x35, 0x34, 0x64, // + 0x36, 0x4f, 0x4c, 0x33, // + 0x34, 0x6f, 0x4c, 0x39, // + 0x46, 0x53, 0x54, 0x76, // + 0x79, 0x36, 0x34, 0x73, // + 0x41, 0x00, 0x00, 0x00, // + 0x00, 0x14, 0x00, 0x0b, // REALM attribute header + 0x65, 0x78, 0x61, 0x6d, // Realm value (11 bytes) and padding (1 byte) + 0x70, 0x6c, 0x65, 0x2e, // + 0x6f, 0x72, 0x67, 0x00, // + 0x00, 0x1d, 0x00, 0x04, // PASSWORD-ALGORITHM attribute header + 0x00, 0x02, 0x00, 0x00, // PASSWORD-ALGORITHM value (4 bytes) + 0x00, 0x1c, 0x00, 0x20, // MESSAGE-INTEGRITY-SHA256 attribute header + 0xb5, 0xc7, 0xbf, 0x00, // HMAC-SHA256 value + 0x5b, 0x6c, 0x52, 0xa2, // + 0x1c, 0x51, 0xc5, 0xe8, // + 0x92, 0xf8, 0x19, 0x24, // + 0x13, 0x62, 0x96, 0xcb, // + 0x92, 0x7c, 0x43, 0x14, // + 0x93, 0x09, 0x27, 0x8c, // + 0xc6, 0x51, 0x8e, 0x65, // + }; + + memset(&msg, 0, sizeof(msg)); + + if (_juice_stun_read(message2, sizeof(message2), &msg) <= 0) + return -1; + + if(msg.msg_class != STUN_CLASS_REQUEST || msg.msg_method != STUN_METHOD_BINDING) + return -1; + + if (memcmp(msg.transaction_id, message2 + 8, 12) != 0) + return -1; + + if (!msg.credentials.enable_userhash) + return -1; + + if (memcmp(msg.credentials.userhash, message2 + 24, 32) != 0) + return -1; + + if (strcmp(msg.credentials.realm, "example.org") != 0) + return -1; + + if (strcmp(msg.credentials.nonce, "obMatJos2AAACf//499k954d6OL34oL9FSTvy64sA") != 0) + return -1; + + if (!msg.has_integrity) + return -1; + + // Username is "" or "マトリックス" + // aka "The Matrix" in Japanese + strcpy(msg.credentials.username, "マトリックス"); + if (!_juice_stun_check_integrity(message2, sizeof(message2), &msg, "TheMatrIX")) + return -1; + + if(msg.error_code != STUN_ERROR_INTERNAL_VALIDATION_FAILED) + return -1; + + return 0; +} diff --git a/thirdparty/libjuice/test/thread.c b/thirdparty/libjuice/test/thread.c new file mode 100644 index 0000000..f185914 --- /dev/null +++ b/thirdparty/libjuice/test/thread.c @@ -0,0 +1,220 @@ +/** + * Copyright (c) 2022 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_thread() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent in thread concurrency mode + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + config1.concurrency_mode = JUICE_CONCURRENCY_MODE_THREAD; + config1.stun_server_host = "stun.l.google.com"; + config1.stun_server_port = 19302; + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent in thread concurrency mode + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + config2.concurrency_mode = JUICE_CONCURRENCY_MODE_THREAD; + config2.stun_server_host = "stun.l.google.com"; + config2.stun_server_port = 19302; + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = (state1 == JUICE_STATE_COMPLETED && state2 == JUICE_STATE_COMPLETED); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + if ((!strstr(local, "typ host") && !strstr(local, "typ prflx")) || + (!strstr(remote, "typ host") && !strstr(remote, "typ prflx"))) + success = false; // local connection should be possible + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 1: %s\n", sdp); + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + printf("Candidate 2: %s\n", sdp); + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/test/turn.c b/thirdparty/libjuice/test/turn.c new file mode 100644 index 0000000..da700be --- /dev/null +++ b/thirdparty/libjuice/test/turn.c @@ -0,0 +1,241 @@ +/** + * Copyright (c) 2020 Paul-Louis Ageneau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +#include "juice/juice.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +static void sleep(unsigned int secs) { Sleep(secs * 1000); } +#else +#include // for sleep +#endif + +#define BUFFER_SIZE 4096 + +static juice_agent_t *agent1; +static juice_agent_t *agent2; + +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr); +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr); + +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr); +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr); + +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr); +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr); + +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr); + +int test_turn() { + juice_set_log_level(JUICE_LOG_LEVEL_DEBUG); + + // Agent 1: Create agent + juice_config_t config1; + memset(&config1, 0, sizeof(config1)); + + // STUN server example (use your own server in production) + config1.stun_server_host = "openrelay.metered.ca"; + config1.stun_server_port = 80; + + // TURN server example (use your own server in production) + juice_turn_server_t turn_server; + memset(&turn_server, 0, sizeof(turn_server)); + turn_server.host = "openrelay.metered.ca"; + turn_server.port = 80; + turn_server.username = "openrelayproject"; + turn_server.password = "openrelayproject"; + config1.turn_servers = &turn_server; + config1.turn_servers_count = 1; + + config1.cb_state_changed = on_state_changed1; + config1.cb_candidate = on_candidate1; + config1.cb_gathering_done = on_gathering_done1; + config1.cb_recv = on_recv1; + config1.user_ptr = NULL; + + agent1 = juice_create(&config1); + + // Agent 2: Create agent + juice_config_t config2; + memset(&config2, 0, sizeof(config2)); + + // STUN server example (use your own server in production) + config2.stun_server_host = "openrelay.metered.ca"; + config2.stun_server_port = 80; + + config2.cb_state_changed = on_state_changed2; + config2.cb_candidate = on_candidate2; + config2.cb_gathering_done = on_gathering_done2; + config2.cb_recv = on_recv2; + config2.user_ptr = NULL; + + agent2 = juice_create(&config2); + + // Agent 1: Generate local description + char sdp1[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent1, sdp1, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 1:\n%s\n", sdp1); + + // Agent 2: Receive description from agent 1 + juice_set_remote_description(agent2, sdp1); + + // Agent 2: Generate local description + char sdp2[JUICE_MAX_SDP_STRING_LEN]; + juice_get_local_description(agent2, sdp2, JUICE_MAX_SDP_STRING_LEN); + printf("Local description 2:\n%s\n", sdp2); + + // Agent 1: Receive description from agent 2 + juice_set_remote_description(agent1, sdp2); + + // Agent 1: Gather candidates (and send them to agent 2) + juice_gather_candidates(agent1); + sleep(2); + + // Agent 2: Gather candidates (and send them to agent 1) + juice_gather_candidates(agent2); + sleep(2); + + // -- Connection should be finished -- + + // Check states + juice_state_t state1 = juice_get_state(agent1); + juice_state_t state2 = juice_get_state(agent2); + bool success = ((state1 == JUICE_STATE_COMPLETED || state1 == JUICE_STATE_CONNECTED) && + (state2 == JUICE_STATE_CONNECTED || state2 == JUICE_STATE_COMPLETED)); + + // Retrieve candidates + char local[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + char remote[JUICE_MAX_CANDIDATE_SDP_STRING_LEN]; + if (success &= + (juice_get_selected_candidates(agent1, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 1: %s\n", local); + printf("Remote candidate 1: %s\n", remote); + + success &= (strstr(local, "relay") != NULL); + } + if (success &= + (juice_get_selected_candidates(agent2, local, JUICE_MAX_CANDIDATE_SDP_STRING_LEN, remote, + JUICE_MAX_CANDIDATE_SDP_STRING_LEN) == 0)) { + printf("Local candidate 2: %s\n", local); + printf("Remote candidate 2: %s\n", remote); + + success &= (strstr(remote, "relay") != NULL); + } + + // Retrieve addresses + char localAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + char remoteAddr[JUICE_MAX_ADDRESS_STRING_LEN]; + if (success &= (juice_get_selected_addresses(agent1, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 1: %s\n", localAddr); + printf("Remote address 1: %s\n", remoteAddr); + } + if (success &= (juice_get_selected_addresses(agent2, localAddr, JUICE_MAX_ADDRESS_STRING_LEN, + remoteAddr, JUICE_MAX_ADDRESS_STRING_LEN) == 0)) { + printf("Local address 2: %s\n", localAddr); + printf("Remote address 2: %s\n", remoteAddr); + } + + // Agent 1: destroy + juice_destroy(agent1); + + // Agent 2: destroy + juice_destroy(agent2); + + if (success) { + printf("Success\n"); + return 0; + } else { + printf("Failure\n"); + return -1; + } +} + +// Agent 1: on state changed +static void on_state_changed1(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 1: %s\n", juice_state_to_string(state)); + + if (state == JUICE_STATE_CONNECTED) { + // Agent 1: on connected, send a message + const char *message = "Hello from 1"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 2: on state changed +static void on_state_changed2(juice_agent_t *agent, juice_state_t state, void *user_ptr) { + printf("State 2: %s\n", juice_state_to_string(state)); + if (state == JUICE_STATE_CONNECTED) { + // Agent 2: on connected, send a message + const char *message = "Hello from 2"; + juice_send(agent, message, strlen(message)); + } +} + +// Agent 1: on local candidate gathered +static void on_candidate1(juice_agent_t *agent, const char *sdp, void *user_ptr) { + // Filter relayed candidates + if (!strstr(sdp, "relay")) + return; + + printf("Candidate 1: %s\n", sdp); + + // Agent 2: Receive it from agent 1 + juice_add_remote_candidate(agent2, sdp); +} + +// Agent 2: on local candidate gathered +static void on_candidate2(juice_agent_t *agent, const char *sdp, void *user_ptr) { + // Filter server reflexive candidates + if (!strstr(sdp, "srflx")) + return; + + printf("Candidate 2: %s\n", sdp); + + // Agent 1: Receive it from agent 2 + juice_add_remote_candidate(agent1, sdp); +} + +// Agent 1: on local candidates gathering done +static void on_gathering_done1(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 1\n"); + juice_set_remote_gathering_done(agent2); // optional +} + +// Agent 2: on local candidates gathering done +static void on_gathering_done2(juice_agent_t *agent, void *user_ptr) { + printf("Gathering done 2\n"); + juice_set_remote_gathering_done(agent1); // optional +} + +// Agent 1: on message received +static void on_recv1(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 1: %s\n", buffer); +} + +// Agent 2: on message received +static void on_recv2(juice_agent_t *agent, const char *data, size_t size, void *user_ptr) { + char buffer[BUFFER_SIZE]; + if (size > BUFFER_SIZE - 1) + size = BUFFER_SIZE - 1; + memcpy(buffer, data, size); + buffer[size] = '\0'; + printf("Received 2: %s\n", buffer); +} diff --git a/thirdparty/libjuice/xmake.lua b/thirdparty/libjuice/xmake.lua new file mode 100644 index 0000000..4339343 --- /dev/null +++ b/thirdparty/libjuice/xmake.lua @@ -0,0 +1,12 @@ +package("libjuice") + add_deps("cmake") + set_sourcedir(path.join(os.scriptdir(), "")) + on_install(function (package) + local configs = {} + table.insert(configs, "-DNO_EXPORT_HEADER=ON -DNO_TESTS=ON") + import("package.tools.cmake").install(package, configs) + end) + -- on_test(function (package) + -- assert(package:has_cfuncs("juice_create", {includes = "juice/juice.h"})) + -- end) +package_end() \ No newline at end of file diff --git a/thirdparty/xmake.lua b/thirdparty/xmake.lua new file mode 100644 index 0000000..7b1cc7d --- /dev/null +++ b/thirdparty/xmake.lua @@ -0,0 +1 @@ +includes("libjuice") \ No newline at end of file diff --git a/xmake.lua b/xmake.lua index 6e97152..3f9a75f 100644 --- a/xmake.lua +++ b/xmake.lua @@ -7,6 +7,7 @@ set_languages("c++17") add_rules("mode.release", "mode.debug") add_requires("asio 1.24.0", "nlohmann_json", "spdlog 1.11.0") +add_requires("libjuice", {system = false}) add_defines("JUICE_STATIC") add_defines("ASIO_STANDALONE","_WEBSOCKETPP_CPP11_INTERNAL_", "ASIO_HAS_STD_TYPE_TRAITS", "ASIO_HAS_STD_SHARED_PTR", @@ -17,15 +18,15 @@ add_links("ws2_32", "Bcrypt") add_cxflags("-MD") add_packages("spdlog") +includes("thirdparty") + target("ice") set_kind("static") add_deps("log", "ws") - add_packages("asio", "nlohmann_json") + add_packages("asio", "nlohmann_json", "libjuice") add_files("src/ice/*.cpp") - add_links("juice") add_includedirs("src/ws") add_includedirs("thirdparty/libjuice/include", {public = true}) - add_linkdirs("thirdparty/libjuice/lib") target("ws") set_kind("static")