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 0000000..c0a79fa Binary files /dev/null and b/thirdparty/libjuice/fuzzer/input/message1.raw differ diff --git a/thirdparty/libjuice/fuzzer/input/message2.raw b/thirdparty/libjuice/fuzzer/input/message2.raw new file mode 100644 index 0000000..9947796 Binary files /dev/null and b/thirdparty/libjuice/fuzzer/input/message2.raw differ diff --git a/thirdparty/libjuice/include/juice/juice.h b/thirdparty/libjuice/include/juice/juice.h index 9e4b9c1..06bcc13 100644 --- a/thirdparty/libjuice/include/juice/juice.h +++ b/thirdparty/libjuice/include/juice/juice.h @@ -1,176 +1,176 @@ -/** - * 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 +/** + * 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 29bf3f3..0000000 Binary files a/thirdparty/libjuice/lib/juice.lib and /dev/null differ 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")