From 7a3ef9ba13cc92edfef3e5391df0d1f8c0379921 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 22:15:24 -0700 Subject: [PATCH 01/24] Add WebSocket compression build and dependency plumbing --- .gitmodules | 3 + .../libHttpClient.xcodeproj/project.pbxproj | 8 ++ Build/libHttpClient.CMake/GetLibHCFlags.cmake | 4 + .../libHttpClient.GDK.Shared.vcxitems | 22 +++++- .../libHttpClient.GDK.Shared.vcxitems.filters | 14 +++- Build/libHttpClient.Linux/CMakeLists.txt | 74 ++++++++++++++++++- .../libHttpClient_Linux.bash | 27 +++++-- .../libHttpClient.Win32.Shared.vcxitems | 19 ++++- External/asio | 2 +- External/boost-wintls | 1 + External/websocketpp | 2 +- NOTICE.txt | 2 +- cgmanifest.json | 2 +- hc_settings.props.example | 6 +- libHttpClient.vs2019.sln | 65 +++++++++++++++- libHttpClient.vs2022.sln | 65 +++++++++++++++- 16 files changed, 298 insertions(+), 18 deletions(-) create mode 160000 External/boost-wintls diff --git a/.gitmodules b/.gitmodules index 30ecbeee..f902a102 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "External/zlib"] path = External/zlib url = https://github.com/madler/zlib +[submodule "External/boost-wintls"] + path = External/boost-wintls + url = https://github.com/laudrup/boost-wintls.git diff --git a/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj b/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj index 728d5c9b..9b45947a 100644 --- a/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj +++ b/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj @@ -1828,6 +1828,7 @@ GCC_PREFIX_HEADER = "$(SRCROOT)/../../Source/Common/pch.h"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", + "HC_ENABLE_WEBSOCKET_COMPRESSION=1", "HC_LINK_STATIC=1", "HC_TRACE_BUILD_LEVEL=HC_PRIVATE_TRACE_LEVEL_VERBOSE", ); @@ -1835,6 +1836,7 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", + "$(SRCROOT)/../../External/zlib", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -1860,6 +1862,7 @@ GCC_PREFIX_HEADER = "$(SRCROOT)/../../Source/Common/pch.h"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", + "HC_ENABLE_WEBSOCKET_COMPRESSION=1", "HC_LINK_STATIC=1", "HC_TRACE_BUILD_LEVEL=HC_PRIVATE_TRACE_LEVEL_IMPORTANT", ); @@ -1867,6 +1870,7 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", + "$(SRCROOT)/../../External/zlib", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -2005,6 +2009,7 @@ GCC_PREFIX_HEADER = "$(SRCROOT)/../../Source/Common/pch.h"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", + "HC_ENABLE_WEBSOCKET_COMPRESSION=1", "HC_LINK_STATIC=1", "HC_TRACE_BUILD_LEVEL=HC_PRIVATE_TRACE_LEVEL_VERBOSE", ); @@ -2013,6 +2018,7 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", + "$(SRCROOT)/../../External/zlib", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -2040,6 +2046,7 @@ GCC_PREFIX_HEADER = "$(SRCROOT)/../../Source/Common/pch.h"; GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", + "HC_ENABLE_WEBSOCKET_COMPRESSION=1", "HC_LINK_STATIC=1", "HC_TRACE_BUILD_LEVEL=HC_PRIVATE_TRACE_LEVEL_IMPORTANT", ); @@ -2048,6 +2055,7 @@ HEADER_SEARCH_PATHS = ( "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", + "$(SRCROOT)/../../External/zlib", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", diff --git a/Build/libHttpClient.CMake/GetLibHCFlags.cmake b/Build/libHttpClient.CMake/GetLibHCFlags.cmake index 22504d9b..cb68d32d 100644 --- a/Build/libHttpClient.CMake/GetLibHCFlags.cmake +++ b/Build/libHttpClient.CMake/GetLibHCFlags.cmake @@ -22,6 +22,10 @@ function(GET_LIBHC_FLAGS OUT_FLAGS OUT_FLAGS_DEBUG OUT_FLAGS_RELEASE) list(APPEND FLAGS "-DHC_NOZLIB=1") endif() + if (HC_ENABLE_WEBSOCKET_COMPRESSION) + list(APPEND FLAGS "-DHC_ENABLE_WEBSOCKET_COMPRESSION=1") + endif() + set("${OUT_FLAGS}" "${FLAGS}" PARENT_SCOPE) set("${OUT_FLAGS_DEBUG}" diff --git a/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems b/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems index a6d3700d..af162ecf 100644 --- a/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems +++ b/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems @@ -5,6 +5,10 @@ true {8ca3b500-0d89-4db1-ba8b-98aeb468ca13} + + true + false + %(AdditionalIncludeDirectories);$(MSBuildThisFileDirectory) @@ -21,9 +25,21 @@ + + HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + - + + HC_ENABLE_WEBSOCKET_COMPRESSION=1;HC_ENABLE_GDK_XBOX_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + + + $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp;$(HCRoot)\External\boost-wintls\include;$(HCRoot)\External\zlib;%(AdditionalIncludeDirectories) + _CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;_WEBSOCKETPP_CPP11_STL_;_WEBSOCKETPP_CPP11_RANDOM_DEVICE_;HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + /std:c++17 %(AdditionalOptions) + NotUsing + @@ -33,5 +49,7 @@ + + - \ No newline at end of file + diff --git a/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems.filters b/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems.filters index 0222713d..4b8f7bbc 100644 --- a/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems.filters +++ b/Build/libHttpClient.GDK.Shared/libHttpClient.GDK.Shared.vcxitems.filters @@ -31,6 +31,9 @@ {e917aa51-b574-4f8d-a4e6-01c61d3592a5} + + {3bb5d493-f184-4f6b-82fc-846f0170b41b} + @@ -61,6 +64,9 @@ Source\HTTP\Curl + + Source\WebSocket\Websocketpp + @@ -84,5 +90,11 @@ Source\HTTP\Curl + + Source\WebSocket\Websocketpp + + + Source\WebSocket\Websocketpp + - \ No newline at end of file + diff --git a/Build/libHttpClient.Linux/CMakeLists.txt b/Build/libHttpClient.Linux/CMakeLists.txt index fd00de44..03215df8 100644 --- a/Build/libHttpClient.Linux/CMakeLists.txt +++ b/Build/libHttpClient.Linux/CMakeLists.txt @@ -4,6 +4,11 @@ get_filename_component(PATH_TO_ROOT "../.." ABSOLUTE) project("libHttpClient.Linux") +include(CTest) +enable_testing() + +option(HC_ENABLE_WEBSOCKET_COMPRESSION "Enable compression-capable WebSocket provider support" ON) + set(CMAKE_C_COMPILER clang) set(CMAKE_CXX_COMPILER clang++) set(CMAKE_STATIC_LIBRARY_PREFIX "") @@ -168,6 +173,10 @@ target_include_directories( "${ZLIB_INCLUDE_DIRS}" ) +if (NOT DEFINED HC_NOZLIB) + target_compile_definitions("${PROJECT_NAME}" PRIVATE Z_HAVE_UNISTD_H=1) +endif() + include("../libHttpClient.CMake/GetLibHCFlags.cmake") get_libhc_flags(FLAGS FLAGS_DEBUG FLAGS_RELEASE) @@ -181,4 +190,67 @@ target_set_flags( set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") -export(TARGETS ${PROJECT_NAME} FILE ${PROJECT_NAME}Config.cmake) \ No newline at end of file +if (HC_ENABLE_WEBSOCKET_COMPRESSION) + if (DEFINED HC_NOZLIB) + message(FATAL_ERROR "HC_ENABLE_WEBSOCKET_COMPRESSION requires zlib support") + endif() + + find_package(Threads REQUIRED) + + add_executable( + "WebSocketCompressionTests.Linux" + "${PATH_TO_ROOT}/Tests/WebSocketCompression/WebSocketCompressionTests.cpp" + "${ZLIB_SOURCE_FILES}" + ) + + target_include_directories( + "WebSocketCompressionTests.Linux" + PRIVATE + "${COMMON_INCLUDE_DIRS}" + "${LINUX_INCLUDE_DIRS}" + "${ZLIB_INCLUDE_DIRS}" + ) + + target_compile_definitions( + "WebSocketCompressionTests.Linux" + PRIVATE + ASIO_STANDALONE + Z_HAVE_UNISTD_H=1 + ) + + target_link_libraries( + "WebSocketCompressionTests.Linux" + PRIVATE + "${PROJECT_NAME}" + Threads::Threads + ) + + if (NOT BUILD_SHARED_LIBS) + target_link_libraries( + "WebSocketCompressionTests.Linux" + PRIVATE + "${LIBCURL_BINARY_PATH}" + "${LIBSSL_BINARY_PATH}" + "${LIBCRYPTO_BINARY_PATH}" + ) + endif() + + target_set_flags( + "WebSocketCompressionTests.Linux" + "${FLAGS}" + "${FLAGS_DEBUG}" + "${FLAGS_RELEASE}" + ) + + add_test( + NAME websocket-compression-linux + COMMAND "WebSocketCompressionTests.Linux" + ) + set_tests_properties( + websocket-compression-linux + PROPERTIES + SKIP_RETURN_CODE 125 + ) +endif() + +export(TARGETS ${PROJECT_NAME} FILE ${PROJECT_NAME}Config.cmake) diff --git a/Build/libHttpClient.Linux/libHttpClient_Linux.bash b/Build/libHttpClient.Linux/libHttpClient_Linux.bash index 5300706e..3e359536 100755 --- a/Build/libHttpClient.Linux/libHttpClient_Linux.bash +++ b/Build/libHttpClient.Linux/libHttpClient_Linux.bash @@ -16,6 +16,7 @@ BUILD_CURL=true BUILD_SSL=true BUILD_STATIC=false BUILD_UNREAL_ENGINE_4=false +ENABLE_WEBSOCKET_COMPRESSION=true C_COMPILER="clang" CXX_COMPILER="clang++" INSTALL_DEPENDENCIES=false @@ -44,6 +45,14 @@ while [[ $# -gt 0 ]]; do INSTALL_DEPENDENCIES=true shift ;; + -wc|--websocket-compression) + ENABLE_WEBSOCKET_COMPRESSION=true + shift + ;; + -nwc|--no-websocket-compression) + ENABLE_WEBSOCKET_COMPRESSION=false + shift + ;; -sg|--skipaptget) # NOOP. allow user to specify old --skipaptget args before that became the default shift @@ -98,6 +107,7 @@ set -e # re-enable exit-on-error after dependency installation check log "CONFIGURATION = ${CONFIGURATION}" log "BUILD SSL = ${BUILD_SSL}" log "BUILD CURL = ${BUILD_CURL}" +log "WS COMPRESSION = ${ENABLE_WEBSOCKET_COMPRESSION}" log "CMakeLists.txt = ${SCRIPT_DIR}" log "CMake output = ${SCRIPT_DIR}/../../Int/CMake/libHttpClient.Linux" @@ -117,18 +127,23 @@ if [ "$BUILD_SSL" = true ]; then fi if [ "$BUILD_CURL" = true ]; then - log "Building cURL" - sed -i -e 's/\r$//' "$SCRIPT_DIR"/curl_Linux.bash - bash "$SCRIPT_DIR"/curl_Linux.bash -c "$CONFIGURATION" + log "Building cURL" + sed -i -e 's/\r$//' "$SCRIPT_DIR"/curl_Linux.bash + bash "$SCRIPT_DIR"/curl_Linux.bash -c "$CONFIGURATION" +fi + +WEBSOCKET_COMPRESSION_CMAKE_ARG="-DHC_ENABLE_WEBSOCKET_COMPRESSION=ON" +if [ "$ENABLE_WEBSOCKET_COMPRESSION" != true ]; then + WEBSOCKET_COMPRESSION_CMAKE_ARG="-DHC_ENABLE_WEBSOCKET_COMPRESSION=OFF" fi MAKE_PARALLELISM="-j$(nproc)" # run Make in parallel to speed up the build process if [ "$BUILD_STATIC" = false ]; then # make libHttpClient shared - cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux -D CMAKE_BUILD_TYPE=$CONFIGURATION -D CMAKE_C_COMPILER=$C_COMPILER -D CMAKE_CXX_COMPILER=$CXX_COMPILER -D BUILD_SHARED_LIBS=ON + cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux -D CMAKE_BUILD_TYPE=$CONFIGURATION -D CMAKE_C_COMPILER=$C_COMPILER -D CMAKE_CXX_COMPILER=$CXX_COMPILER -D BUILD_SHARED_LIBS=ON $WEBSOCKET_COMPRESSION_CMAKE_ARG make $MAKE_PARALLELISM -C "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux else # make libHttpClient static - cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux -D CMAKE_BUILD_TYPE=$CONFIGURATION -D CMAKE_C_COMPILER=$C_COMPILER -D CMAKE_CXX_COMPILER=$CXX_COMPILER -D BUILD_SHARED_LIBS=OFF + cmake -S "$SCRIPT_DIR" -B "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux -D CMAKE_BUILD_TYPE=$CONFIGURATION -D CMAKE_C_COMPILER=$C_COMPILER -D CMAKE_CXX_COMPILER=$CXX_COMPILER -D BUILD_SHARED_LIBS=OFF $WEBSOCKET_COMPRESSION_CMAKE_ARG make $MAKE_PARALLELISM -C "$SCRIPT_DIR"/../../Int/CMake/libHttpClient.Linux -fi \ No newline at end of file +fi diff --git a/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems b/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems index 05b877ae..4089c9b8 100644 --- a/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems +++ b/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems @@ -5,6 +5,9 @@ true {d980f91b-92bf-4cc4-89fd-1cd99dba870f} + + true + @@ -13,15 +16,29 @@ + + HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + - + + _CRT_SECURE_NO_WARNINGS;_WEBSOCKETPP_CPP11_RANDOM_DEVICE_;HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + _CRT_SECURE_NO_WARNINGS;_WEBSOCKETPP_CPP11_RANDOM_DEVICE_;%(PreprocessorDefinitions) + + + $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp;$(HCRoot)\External\boost-wintls\include;$(HCRoot)\External\zlib;%(AdditionalIncludeDirectories) + _CRT_SECURE_NO_WARNINGS;_CRT_NONSTDC_NO_WARNINGS;_WEBSOCKETPP_CPP11_STL_;_WEBSOCKETPP_CPP11_RANDOM_DEVICE_;HC_ENABLE_WEBSOCKET_COMPRESSION=1;%(PreprocessorDefinitions) + /std:c++17 %(AdditionalOptions) + NotUsing + + + diff --git a/External/asio b/External/asio index 22afb860..03ae834e 160000 --- a/External/asio +++ b/External/asio @@ -1 +1 @@ -Subproject commit 22afb86087a77037cd296d27134756c9b0d2cb75 +Subproject commit 03ae834edbace31a96157b89bf50e5ee464e5ef9 diff --git a/External/boost-wintls b/External/boost-wintls new file mode 160000 index 00000000..5a16a857 --- /dev/null +++ b/External/boost-wintls @@ -0,0 +1 @@ +Subproject commit 5a16a857e03b8d75d0aed8a5ad918bca442dbe27 diff --git a/External/websocketpp b/External/websocketpp index c6d7e295..56123c87 160000 --- a/External/websocketpp +++ b/External/websocketpp @@ -1 +1 @@ -Subproject commit c6d7e295bf5a0ab9b5f896720cc1a0e0fdc397a7 +Subproject commit 56123c87598f8b1dd471be83ca841ceae07f95ba diff --git a/NOTICE.txt b/NOTICE.txt index 7c7ef900..dcee89f5 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -148,7 +148,7 @@ BSD-2-Clause AND BSD-3-Clause --------------------------------------------------------- -zaphoyd/websocketpp c6d7e295bf5a0ab9b5f896720cc1a0e0fdc397a7 - BSD-3-Clause +zaphoyd/websocketpp 56123c87598f8b1dd471be83ca841ceae07f95ba - BSD-3-Clause Copyright (c) 2011, Peter Thorson. diff --git a/cgmanifest.json b/cgmanifest.json index dbfcab7e..32ee9ecf 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -17,7 +17,7 @@ "Type": "git", "git": { "RepositoryUrl": "https://github.com/zaphoyd/websocketpp", - "CommitHash": "c6d7e295bf5a0ab9b5f896720cc1a0e0fdc397a7" + "CommitHash": "56123c87598f8b1dd471be83ca841ceae07f95ba" } }, "DevelopmentDependency": false diff --git a/hc_settings.props.example b/hc_settings.props.example index 1ef2f7be..a655a2b5 100644 --- a/hc_settings.props.example +++ b/hc_settings.props.example @@ -5,7 +5,11 @@ true true + + + + true - \ No newline at end of file + diff --git a/libHttpClient.vs2019.sln b/libHttpClient.vs2019.sln index 3d271437..9e450ee8 100644 --- a/libHttpClient.vs2019.sln +++ b/libHttpClient.vs2019.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.34301.259 @@ -31,8 +31,14 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TE", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest", "Build\libHttpClient.UnitTest\libHttpClient.UnitTest.vcxitems", "{8EF7009A-36CF-4D82-9FB7-6D69154893CF}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketSemanticsTests.Win32", "Tests\WebSocketSemantics\WebSocketSemanticsTests.Win32.vcxproj", "{6E9D9094-EC44-4E0C-B271-25E8CB5C9734}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TAEF", "Build\libHttpClient.UnitTest.TAEF\libHttpClient.UnitTest.TAEF.vcxproj", "{E885BB30-F51E-4BAB-9300-4B303144BB49}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionTests.Win32.vcxproj", "{4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionIntegrationTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionIntegrationTests.Win32.vcxproj", "{F5D76018-3E61-4C5E-9C26-6A49A9C24E73}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UWP", "Build\libHttpClient.UWP\libHttpClient.UWP.vcxitems", "{47564F2B-9ED7-4527-997A-5D76C36998D1}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Http", "Samples\UWP-Http\Http.vcxproj", "{2DFBBF3A-6D4B-4FF8-BD01-C9527A1FE0AC}" @@ -211,6 +217,42 @@ Global {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x64.Build.0 = Release|x64 {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x86.ActiveCfg = Release|Win32 {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x86.Build.0 = Release|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM.ActiveCfg = Debug|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM.ActiveCfg = Debug|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM.Build.0 = Debug|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM.Build.0 = Debug|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM64.Build.0 = Debug|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM64.Build.0 = Debug|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x64.ActiveCfg = Debug|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x64.ActiveCfg = Debug|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x64.Build.0 = Debug|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x64.Build.0 = Debug|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x86.ActiveCfg = Debug|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x86.ActiveCfg = Debug|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x86.Build.0 = Debug|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x86.Build.0 = Debug|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM.ActiveCfg = Release|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM.ActiveCfg = Release|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM.Build.0 = Release|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM.Build.0 = Release|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM64.ActiveCfg = Release|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM64.ActiveCfg = Release|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM64.Build.0 = Release|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM64.Build.0 = Release|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|Gaming.Desktop.x64.ActiveCfg = Release|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|Gaming.Desktop.x64.ActiveCfg = Release|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x64.ActiveCfg = Release|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x64.ActiveCfg = Release|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x64.Build.0 = Release|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x64.Build.0 = Release|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x86.ActiveCfg = Release|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x86.ActiveCfg = Release|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x86.Build.0 = Release|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x86.Build.0 = Release|Win32 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Debug|ARM.ActiveCfg = Debug|ARM {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Debug|ARM.Build.0 = Debug|ARM {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -229,6 +271,24 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x64.Build.0 = Release|x64 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.ActiveCfg = Release|Win32 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.Build.0 = Release|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.ActiveCfg = Debug|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.Build.0 = Debug|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.Build.0 = Debug|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.ActiveCfg = Debug|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.Build.0 = Debug|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.ActiveCfg = Debug|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.Build.0 = Debug|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.ActiveCfg = Release|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.Build.0 = Release|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.ActiveCfg = Release|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.Build.0 = Release|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|Gaming.Desktop.x64.ActiveCfg = Release|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.ActiveCfg = Release|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.Build.0 = Release|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.ActiveCfg = Release|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.Build.0 = Release|Win32 {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.ActiveCfg = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.Build.0 = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -392,6 +452,9 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {8EF7009A-36CF-4D82-9FB7-6D69154893CF} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {E885BB30-F51E-4BAB-9300-4B303144BB49} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {47564F2B-9ED7-4527-997A-5D76C36998D1} = {6FB21B4B-86D1-4883-85A2-B51771884ED8} {2DFBBF3A-6D4B-4FF8-BD01-C9527A1FE0AC} = {5B7AC447-943C-45D3-AD4E-3A61DA208FD8} {99542110-3CCD-4525-B607-DB8F6E76AEAC} = {5B7AC447-943C-45D3-AD4E-3A61DA208FD8} diff --git a/libHttpClient.vs2022.sln b/libHttpClient.vs2022.sln index 178f4ed8..b319effc 100644 --- a/libHttpClient.vs2022.sln +++ b/libHttpClient.vs2022.sln @@ -1,4 +1,4 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.5.33627.172 @@ -62,6 +62,12 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TAEF EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TE", "Build\libHttpClient.UnitTest.TE\libHttpClient.UnitTest.TE.vcxproj", "{9DD2BA60-6505-493A-8C41-8085C44E9F1F}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionTests.Win32.vcxproj", "{4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionIntegrationTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionIntegrationTests.Win32.vcxproj", "{F5D76018-3E61-4C5E-9C26-6A49A9C24E73}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketSemanticsTests.Win32", "Tests\WebSocketSemantics\WebSocketSemanticsTests.Win32.vcxproj", "{6E9D9094-EC44-4E0C-B271-25E8CB5C9734}" +EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.XAsync", "Build\libHttpClient.XAsync\libHttpClient.XAsync.vcxitems", "{B5118956-53CC-4B24-8807-DE8D7D226B40}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.Zlib", "Build\libHttpClient.Zlib\libHttpClient.Zlib.vcxitems", "{F9061DCA-255B-4D5E-8DF5-4AFBFB4B98EF}" @@ -290,6 +296,42 @@ Global {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x64.Build.0 = Release|x64 {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x86.ActiveCfg = Release|Win32 {267629EF-20A2-4FFB-8DC8-A8534E98FCEB}.Release|x86.Build.0 = Release|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM.ActiveCfg = Debug|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM.ActiveCfg = Debug|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM.Build.0 = Debug|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM.Build.0 = Debug|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|ARM64.Build.0 = Debug|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|ARM64.Build.0 = Debug|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x64.ActiveCfg = Debug|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x64.ActiveCfg = Debug|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x64.Build.0 = Debug|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x64.Build.0 = Debug|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x86.ActiveCfg = Debug|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x86.ActiveCfg = Debug|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Debug|x86.Build.0 = Debug|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Debug|x86.Build.0 = Debug|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM.ActiveCfg = Release|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM.ActiveCfg = Release|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM.Build.0 = Release|ARM + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM.Build.0 = Release|ARM + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM64.ActiveCfg = Release|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM64.ActiveCfg = Release|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|ARM64.Build.0 = Release|ARM64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|ARM64.Build.0 = Release|ARM64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|Gaming.Desktop.x64.ActiveCfg = Release|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|Gaming.Desktop.x64.ActiveCfg = Release|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x64.ActiveCfg = Release|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x64.ActiveCfg = Release|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x64.Build.0 = Release|x64 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x64.Build.0 = Release|x64 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x86.ActiveCfg = Release|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x86.ActiveCfg = Release|Win32 + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}.Release|x86.Build.0 = Release|Win32 + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73}.Release|x86.Build.0 = Release|Win32 {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.ActiveCfg = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.Build.0 = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -326,6 +368,24 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x64.Build.0 = Release|x64 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.ActiveCfg = Release|Win32 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.Build.0 = Release|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.ActiveCfg = Debug|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.Build.0 = Debug|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.Build.0 = Debug|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.ActiveCfg = Debug|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.Build.0 = Debug|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.ActiveCfg = Debug|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.Build.0 = Debug|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.ActiveCfg = Release|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.Build.0 = Release|ARM + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.ActiveCfg = Release|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.Build.0 = Release|ARM64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|Gaming.Desktop.x64.ActiveCfg = Release|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.ActiveCfg = Release|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.Build.0 = Release|x64 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.ActiveCfg = Release|Win32 + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.Build.0 = Release|Win32 {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM.ActiveCfg = Debug|ARM {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM.Build.0 = Debug|ARM {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -399,6 +459,9 @@ Global {8EF7009A-36CF-4D82-9FB7-6D69154893CF} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {E885BB30-F51E-4BAB-9300-4B303144BB49} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {9DD2BA60-6505-493A-8C41-8085C44E9F1F} = {6BBCD917-A163-48D8-9385-3F6135E031FE} + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1} = {6BBCD917-A163-48D8-9385-3F6135E031FE} + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73} = {6BBCD917-A163-48D8-9385-3F6135E031FE} + {6E9D9094-EC44-4E0C-B271-25E8CB5C9734} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {D980F91B-92BF-4CC4-89FD-1CD99DBA870F} = {118840A6-8EB2-4D70-B0EE-65EE13E2FEAB} {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8} = {118840A6-8EB2-4D70-B0EE-65EE13E2FEAB} {8CA3B500-0D89-4DB1-BA8B-98AEB468CA13} = {348C2EBE-5E0D-4008-8E9C-BD2ECF40F4BC} From b739781198ca5ed9ca91d5ebc03bef0fbeee0498 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 19:10:45 -0700 Subject: [PATCH 02/24] Introduce the websocketpp-backed Windows transport Add the websocketpp WebSocket provider, the WinHTTP hybrid routing layer, and the platform-specific plumbing needed to select that provider on Win32 and GDK. Update the WinHTTP connection path, WinRT and Apple helpers, network-state handling, and the websocketpp configuration headers so the new transport can own deterministic and compression-capable connections. --- Source/Common/Apple/utils_apple.h | 2 +- Source/Common/Apple/utils_apple.mm | 6 +- Source/Global/NetworkState.cpp | 41 +- Source/Global/NetworkState.h | 7 +- Source/HTTP/WinHttp/winhttp_connection.cpp | 275 +++- Source/HTTP/WinHttp/winhttp_connection.h | 1 + Source/HTTP/WinHttp/winhttp_provider.cpp | 14 + Source/HTTP/WinHttp/winhttp_provider.h | 5 +- .../HTTP/WinHttp/winhttp_websocket_hybrid.cpp | 140 ++ .../HTTP/WinHttp/winhttp_websocket_hybrid.h | 52 + .../Platform/GDK/PlatformComponents_GDK.cpp | 96 +- .../Win32/PlatformComponents_Win32.cpp | 7 + ...socketpp_configured_permessage_deflate.hpp | 56 + ...ebsocketpp_disabled_permessage_deflate.hpp | 97 ++ .../Websocketpp/websocketpp_websocket.cpp | 1282 ++++++++++++++--- .../Websocketpp/websocketpp_websocket.h | 32 +- .../WebSocket/Websocketpp/wintls_socket.hpp | 289 ++++ Source/WebSocket/WinRT/winrt_websocket.cpp | 62 +- Source/WebSocket/iOS/ios_websocket.cpp | 32 - 19 files changed, 2134 insertions(+), 362 deletions(-) create mode 100644 Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp create mode 100644 Source/HTTP/WinHttp/winhttp_websocket_hybrid.h create mode 100644 Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp create mode 100644 Source/WebSocket/Websocketpp/websocketpp_disabled_permessage_deflate.hpp create mode 100644 Source/WebSocket/Websocketpp/wintls_socket.hpp delete mode 100644 Source/WebSocket/iOS/ios_websocket.cpp diff --git a/Source/Common/Apple/utils_apple.h b/Source/Common/Apple/utils_apple.h index 9f8d353c..8bbb2e4b 100644 --- a/Source/Common/Apple/utils_apple.h +++ b/Source/Common/Apple/utils_apple.h @@ -4,6 +4,6 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN -bool getSystemProxyForUri(const Uri& inUri, Uri* outUri, std::string* outUsername, std::string* outPassword); +bool getSystemProxyForUri(const Uri& inUri, Uri* outUri, String* outUsername, String* outPassword); NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/Common/Apple/utils_apple.mm b/Source/Common/Apple/utils_apple.mm index b5621f05..2eee8f26 100644 --- a/Source/Common/Apple/utils_apple.mm +++ b/Source/Common/Apple/utils_apple.mm @@ -23,7 +23,7 @@ - (BOOL) getProxy:(NSURL** _Nonnull)outUrl withUsername:(NSString** _Nullable)ou NAMESPACE_XBOX_HTTP_CLIENT_BEGIN -bool getSystemProxyForUri(const Uri& uri, Uri* outUri, std::string* outUsername, std::string* outPassword) +bool getSystemProxyForUri(const Uri& uri, Uri* outUri, String* outUsername, String* outPassword) { NSURL* url = [NSURL URLWithString:[NSString stringWithCString:uri.FullPath().c_str() encoding:NSUTF8StringEncoding]]; ProxyResolver* resolver = [ProxyResolver withTargetUrl:url]; @@ -42,12 +42,12 @@ bool getSystemProxyForUri(const Uri& uri, Uri* outUri, std::string* outUsername, if (outUsername && proxyUsername) { - *outUsername = [proxyUsername UTF8String]; + *outUsername = String{ [proxyUsername UTF8String] }; } if (outPassword && proxyPassword) { - *outPassword = [proxyPassword UTF8String]; + *outPassword = String{ [proxyPassword UTF8String] }; } return true; } diff --git a/Source/Global/NetworkState.cpp b/Source/Global/NetworkState.cpp index a02742c1..a2391e5a 100644 --- a/Source/Global/NetworkState.cpp +++ b/Source/Global/NetworkState.cpp @@ -8,10 +8,35 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN #ifndef HC_NOWEBSOCKETS -NetworkState::NetworkState(UniquePtr httpProvider, UniquePtr webSocketProvider) noexcept : +namespace +{ + +void NotifyProviderSuspending(IWebSocketProvider* provider) noexcept +{ + if (auto lifecycle = GetProviderLifecycle(provider)) + { + lifecycle->OnSuspending(); + } +} + +void NotifyProviderResuming(IWebSocketProvider* provider) noexcept +{ + if (auto lifecycle = GetProviderLifecycle(provider)) + { + lifecycle->OnResuming(); + } +} + +} + +NetworkState::NetworkState( + UniquePtr httpProvider, + UniquePtr webSocketProvider +) noexcept : m_httpProvider{ std::move(httpProvider) }, m_webSocketProvider{ std::move(webSocketProvider) } { + assert(m_webSocketProvider); } Result> NetworkState::Initialize( @@ -242,6 +267,20 @@ IWebSocketProvider& NetworkState::WebSocketProvider() noexcept return *m_webSocketProvider; } +void NetworkState::NotifyWebSocketSuspending() noexcept +{ + // Lifecycle notifications are scoped to the built-in provider path. + // External websocket callback overrides are app-owned and are not treated as lifecycle-capable providers. + assert(m_webSocketProvider); + NotifyProviderSuspending(m_webSocketProvider.get()); +} + +void NetworkState::NotifyWebSocketResuming() noexcept +{ + assert(m_webSocketProvider); + NotifyProviderResuming(m_webSocketProvider.get()); +} + Result> NetworkState::WebSocketCreate() noexcept { auto httpSingleton = get_http_singleton(); diff --git a/Source/Global/NetworkState.h b/Source/Global/NetworkState.h index 2e93dc8a..2d9cf0b1 100644 --- a/Source/Global/NetworkState.h +++ b/Source/Global/NetworkState.h @@ -55,6 +55,8 @@ class NetworkState #ifndef HC_NOWEBSOCKETS public: // WebSocket IWebSocketProvider& WebSocketProvider() noexcept; + void NotifyWebSocketSuspending() noexcept; + void NotifyWebSocketResuming() noexcept; Result> WebSocketCreate() noexcept; @@ -68,7 +70,10 @@ class NetworkState private: #ifndef HC_NOWEBSOCKETS - NetworkState(UniquePtr httpProvider, UniquePtr webSocketProvider) noexcept; + NetworkState( + UniquePtr httpProvider, + UniquePtr webSocketProvider + ) noexcept; #else NetworkState(UniquePtr httpProvider) noexcept; #endif diff --git a/Source/HTTP/WinHttp/winhttp_connection.cpp b/Source/HTTP/WinHttp/winhttp_connection.cpp index 2678c3fe..100d64e9 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.cpp +++ b/Source/HTTP/WinHttp/winhttp_connection.cpp @@ -11,6 +11,7 @@ #if HC_PLATFORM == HC_PLATFORM_GDK #include #include +#include "XSystem.h" #include #include #endif @@ -28,6 +29,71 @@ using namespace xbox::httpclient; NAMESPACE_XBOX_HTTP_CLIENT_BEGIN +namespace +{ + +bool TryParseWebSocketProxyUri( + http_internal_string const& rawProxyUri, + Uri& proxyUri +) +{ + proxyUri = Uri{ rawProxyUri }; + if (proxyUri.IsValid()) + { + return true; + } + + if (rawProxyUri.find("://") == http_internal_string::npos) + { + proxyUri = Uri{ "http://" + rawProxyUri }; + } + + return proxyUri.IsValid(); +} + +#ifndef HC_NOWEBSOCKETS +HRESULT ApplyExplicitWebSocketProxy( + HINTERNET hRequest, + HCWebsocketHandle websocketHandle, + uint64_t callId +) +{ + if (!websocketHandle || websocketHandle->websocket->ProxyUri().empty()) + { + return S_OK; + } + + Uri explicitProxyUri; + if (!TryParseWebSocketProxyUri(websocketHandle->websocket->ProxyUri(), explicitProxyUri)) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: explicit proxy URI was invalid", TO_ULL(websocketHandle->websocket->id)); + return E_INVALIDARG; + } + + if (!explicitProxyUri.UserInfo().empty()) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: WinHTTP WebSocket proxy credentials are not pre-authenticated; continuing with the explicit proxy endpoint only", TO_ULL(websocketHandle->websocket->id)); + } + + auto proxyName = WinHttpProvider::BuildNamedProxyString(explicitProxyUri); + WINHTTP_PROXY_INFO proxyInfo{}; + proxyInfo.dwAccessType = WINHTTP_ACCESS_TYPE_NAMED_PROXY; + proxyInfo.lpszProxy = const_cast(proxyName.c_str()); + proxyInfo.lpszProxyBypass = WINHTTP_NO_PROXY_BYPASS; + + if (!WinHttpSetOption(hRequest, WINHTTP_OPTION_PROXY, &proxyInfo, sizeof(proxyInfo))) + { + DWORD dwError = GetLastError(); + HC_TRACE_ERROR(HTTPCLIENT, "WinHttpConnection [ID %llu] [TID %ul] WinHttpSetOption(WINHTTP_OPTION_PROXY) errorcode %d", TO_ULL(callId), GetCurrentThreadId(), dwError); + return HRESULT_FROM_WIN32(dwError); + } + + return S_OK; +} +#endif + +} + WinHttpConnection::WinHttpConnection( HINTERNET hSession, HCCallHandle call, @@ -108,6 +174,16 @@ Result> WinHttpConnection::Initialize( RETURN_IF_FAILED(HCHttpCallCreate(&webSocketCall)); RETURN_IF_FAILED(HCHttpCallRequestSetUrl(webSocketCall, "GET", uri)); + if (webSocket->websocket->ProxyDecryptsHttps()) + { +#if HC_PLATFORM == HC_PLATFORM_GDK + if (XSystemGetDeviceType() == XSystemDeviceType::Pc) +#endif + { + RETURN_IF_FAILED(HCHttpCallRequestSetSSLValidation(webSocketCall, false)); + } + } + auto initResult = WinHttpConnection::Initialize(hSession, webSocketCall, proxyType, std::move(securityInformation)); if (FAILED(initResult.hr)) { @@ -233,11 +309,21 @@ HRESULT WinHttpConnection::Initialize() } #endif +#ifndef HC_NOWEBSOCKETS + bool const hasExplicitWebSocketProxy = m_websocketHandle && !m_websocketHandle->websocket->ProxyUri().empty(); + if (hasExplicitWebSocketProxy) + { + RETURN_IF_FAILED(ApplyExplicitWebSocketProxy(m_hRequest, m_websocketHandle, HCHttpCallGetId(m_call))); + } +#else + bool const hasExplicitWebSocketProxy = false; +#endif + // Note: maxReceiveBufferSize will be used later during WinHttpReadData calls // The deprecated WINHTTP_OPTION_READ_BUFFER_SIZE option has no effect and should not be used #if HC_PLATFORM != HC_PLATFORM_GDK - if (m_proxyType == proxy_type::autodiscover_proxy) + if (!hasExplicitWebSocketProxy && m_proxyType == proxy_type::autodiscover_proxy) { RETURN_IF_FAILED(set_autodiscover_proxy()); } @@ -943,6 +1029,34 @@ uint32_t WinHttpConnection::parse_status_code( return statusCode; } +HRESULT WinHttpConnection::query_and_parse_headers( + _In_ HCCallHandle call, + _In_ HINTERNET hRequestHandle) +{ + DWORD headerBufferLength = 0; + RETURN_IF_FAILED(query_header_length(call, hRequestHandle, WINHTTP_QUERY_RAW_HEADERS_CRLF, &headerBufferLength)); + + http_internal_vector headerRawBuffer; + headerRawBuffer.resize(headerBufferLength); + auto headerBuffer = reinterpret_cast(headerRawBuffer.data()); + if (!WinHttpQueryHeaders( + hRequestHandle, + WINHTTP_QUERY_RAW_HEADERS_CRLF, + WINHTTP_HEADER_NAME_BY_INDEX, + headerBuffer, + &headerBufferLength, + WINHTTP_NO_HEADER_INDEX)) + { + DWORD dwError = GetLastError(); + HC_TRACE_ERROR(HTTPCLIENT, "HCHttpCallPerform [ID %llu] [TID %ul] WinHttpQueryHeaders errorcode %d", TO_ULL(HCHttpCallGetId(call)), GetCurrentThreadId(), dwError); + return HRESULT_FROM_WIN32(dwError); + } + + call->responseHeaders.clear(); + parse_headers_string(call, headerBuffer); + return S_OK; +} + void WinHttpConnection::parse_headers_string( _In_ HCCallHandle call, @@ -976,35 +1090,14 @@ void WinHttpConnection::callback_status_headers_available( { HC_TRACE_INFORMATION(HTTPCLIENT, "WinHttpConnection [ID %llu] [TID %ul] WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE", TO_ULL(HCHttpCallGetId(pRequestContext->m_call)), GetCurrentThreadId() ); - // First need to query to see what the headers size is. - DWORD headerBufferLength = 0; - HRESULT hr = query_header_length(pRequestContext->m_call, hRequestHandle, WINHTTP_QUERY_RAW_HEADERS_CRLF, &headerBufferLength); + HRESULT hr = query_and_parse_headers(pRequestContext->m_call, hRequestHandle); if (FAILED(hr)) { pRequestContext->complete_task(E_FAIL, hr); return; } - // Now allocate buffer for headers and query for them. - http_internal_vector header_raw_buffer; - header_raw_buffer.resize(headerBufferLength); - wchar_t* headerBuffer = reinterpret_cast(&header_raw_buffer[0]); - if (!WinHttpQueryHeaders( - hRequestHandle, - WINHTTP_QUERY_RAW_HEADERS_CRLF, - WINHTTP_HEADER_NAME_BY_INDEX, - headerBuffer, - &headerBufferLength, - WINHTTP_NO_HEADER_INDEX)) - { - DWORD dwError = GetLastError(); - HC_TRACE_ERROR(HTTPCLIENT, "WinHttpConnection [ID %llu] [TID %ul] WinHttpQueryHeaders errorcode %d", TO_ULL(HCHttpCallGetId(pRequestContext->m_call)), GetCurrentThreadId(), dwError); - pRequestContext->complete_task(E_FAIL, HRESULT_FROM_WIN32(dwError)); - return; - } - parse_status_code(pRequestContext->m_call, hRequestHandle, pRequestContext); - parse_headers_string(pRequestContext->m_call, headerBuffer); read_next_response_chunk(pRequestContext, 0); } @@ -1604,14 +1697,17 @@ void WinHttpConnection::on_websocket_disconnected(_In_ USHORT closeReason) void* functionContext = nullptr; HCWebSocketGetEventFunctions(m_websocketHandle, nullptr, nullptr, &disconnectFunc, &functionContext); - try + if (disconnectFunc) { - HCWebSocketCloseStatus closeStatus = static_cast(closeReason); - disconnectFunc(m_websocketHandle, closeStatus, functionContext); - } - catch (...) - { - HC_TRACE_WARNING(HTTPCLIENT, "WinHttpConnection: Caught unhandled exception in client disconnect handler."); + try + { + HCWebSocketCloseStatus closeStatus = static_cast(closeReason); + disconnectFunc(m_websocketHandle, closeStatus, functionContext); + } + catch (...) + { + HC_TRACE_WARNING(HTTPCLIENT, "WinHttpConnection: Caught unhandled exception in client disconnect handler."); + } } StartWinHttpClose(); @@ -1800,58 +1896,95 @@ void WinHttpConnection::callback_websocket_status_headers_available( { #ifndef HC_NOWEBSOCKETS auto winHttpConnection = winHttpContext->winHttpConnection; - winHttpConnection->m_lock.lock(); + HRESULT completionHr{ S_OK }; + uint32_t completionPlatformError{ S_OK }; + bool shouldCompleteTask{ false }; - HC_TRACE_INFORMATION(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] Websocket WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId()); + { + win32_cs_autolock autoCriticalSection(&winHttpConnection->m_lock); - // Check HTTP status code returned by the server and behave accordingly. - const uint32_t statusCode = parse_status_code(winHttpConnection->m_call, hRequestHandle, winHttpConnection.get()); + HC_TRACE_INFORMATION(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] Websocket WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId()); - if (statusCode == 0) // parse_statusCode failed and already called WinHttpConnection::complete_task, simply return - { - return; - } - else if (statusCode != HTTP_STATUS_SWITCH_PROTOCOLS) - { - HC_TRACE_ERROR(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] Upgrade request status code %ul", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), statusCode); - winHttpConnection->m_lock.unlock(); - winHttpConnection->complete_task(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, statusCode), S_OK); - return; - } + // Check HTTP status code returned by the server and behave accordingly. + // Note: parse_status_code may call complete_task internally on failure (returning 0). + // That call happens while m_lock is held, which is safe because m_lock is a recursive + // CRITICAL_SECTION. The RAII autolock ensures the lock is released on early return. + const uint32_t statusCode = parse_status_code(winHttpConnection->m_call, hRequestHandle, winHttpConnection.get()); - assert(winHttpConnection->m_winHttpWebSocketExports.completeUpgrade); + if (statusCode == 0) + { + return; + } - winHttpConnection->m_hRequest = winHttpConnection->m_winHttpWebSocketExports.completeUpgrade(hRequestHandle, (DWORD_PTR)(winHttpContext)); - if (winHttpConnection->m_hRequest == NULL) - { - DWORD dwError = GetLastError(); - HC_TRACE_ERROR(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] WinHttpWebSocketCompleteUpgrade errorcode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); - winHttpConnection->m_lock.unlock(); - winHttpConnection->complete_task(E_FAIL, HRESULT_FROM_WIN32(dwError)); - return; - } + auto headersHr = query_and_parse_headers(winHttpConnection->m_call, hRequestHandle); + if (FAILED(headersHr)) + { + HC_TRACE_WARNING(WEBSOCKET, "WinHttpConnection [ID %llu] [TID %ul] Failed to query websocket upgrade response headers 0x%0.8x", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), headersHr); + } + else + { + auto copyHr = winHttpConnection->m_websocketHandle->websocket->SetResponseHeaders(HttpHeaders{ winHttpConnection->m_call->responseHeaders }); + if (FAILED(copyHr)) + { + HC_TRACE_WARNING(WEBSOCKET, "WinHttpConnection [ID %llu] [TID %ul] Failed to cache websocket upgrade response headers 0x%0.8x", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), copyHr); + } + } - DWORD keepAliveMs = std::min(winHttpConnection->m_websocketHandle->websocket->PingInterval() * 1000, WINHTTP_WEB_SOCKET_MIN_KEEPALIVE_VALUE); - bool status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL, (LPVOID)&keepAliveMs, sizeof(DWORD)); - if (!status) - { - DWORD dwError = GetLastError(); - HC_TRACE_ERROR(HTTPCLIENT, "WinHttpConnection [ID %llu] [TID %ul] WinHttpSetOption errrocode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); + if (statusCode != HTTP_STATUS_SWITCH_PROTOCOLS) + { + HC_TRACE_ERROR(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] Upgrade request status code %ul", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), statusCode); + completionHr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, statusCode); + shouldCompleteTask = true; + } + else + { + assert(winHttpConnection->m_winHttpWebSocketExports.completeUpgrade); + + winHttpConnection->m_hRequest = winHttpConnection->m_winHttpWebSocketExports.completeUpgrade(hRequestHandle, (DWORD_PTR)(winHttpContext)); + if (winHttpConnection->m_hRequest == NULL) + { + DWORD dwError = GetLastError(); + HC_TRACE_ERROR(WEBSOCKET, "HCHttpCallPerform [ID %llu] [TID %ul] WinHttpWebSocketCompleteUpgrade errorcode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); + completionHr = E_FAIL; + completionPlatformError = HRESULT_FROM_WIN32(dwError); + shouldCompleteTask = true; + } + else + { + const uint32_t pingIntervalSeconds = winHttpConnection->m_websocketHandle->websocket->PingInterval(); + if (pingIntervalSeconds > 0) + { + const uint64_t keepAliveMsValue = std::max(static_cast(pingIntervalSeconds) * 1000, WINHTTP_WEB_SOCKET_MIN_KEEPALIVE_VALUE); + const DWORD keepAliveMs = keepAliveMsValue > MAXDWORD ? MAXDWORD : static_cast(keepAliveMsValue); + bool status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL, (LPVOID)&keepAliveMs, sizeof(DWORD)); + if (!status) + { + DWORD dwError = GetLastError(); + HC_TRACE_ERROR(HTTPCLIENT, "WinHttpConnection [ID %llu] [TID %ul] WinHttpSetOption errrocode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); + } + } + + constexpr DWORD closeTimeoutMs = 1000; + bool status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT, (LPVOID)&closeTimeoutMs, sizeof(DWORD)); + if (!status) + { + DWORD dwError = GetLastError(); + HC_TRACE_ERROR(WEBSOCKET, "WinHttpConnection [ID %llu] [TID %ul] WinHttpSetOption errorcode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); + } + + winHttpConnection->m_state = ConnectionState::WebSocketConnected; + + WinHttpCloseHandle(hRequestHandle); // The old request handle is not needed anymore. We're using pRequestContext->m_hRequest now + } + } } - constexpr DWORD closeTimeoutMs = 1000; - status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT, (LPVOID)&closeTimeoutMs, sizeof(DWORD)); - if (!status) + if (shouldCompleteTask) { - DWORD dwError = GetLastError(); - HC_TRACE_ERROR(WEBSOCKET, "WinHttpConnection [ID %llu] [TID %ul] WinHttpSetOption errorcode %d", TO_ULL(HCHttpCallGetId(winHttpConnection->m_call)), GetCurrentThreadId(), dwError); + winHttpConnection->complete_task(completionHr, completionPlatformError); + return; } - winHttpConnection->m_state = ConnectionState::WebSocketConnected; - - WinHttpCloseHandle(hRequestHandle); // The old request handle is not needed anymore. We're using pRequestContext->m_hRequest now - winHttpConnection->m_lock.unlock(); - // This will now complete the WebSocket Connect operation winHttpConnection->complete_task(S_OK, S_OK); diff --git a/Source/HTTP/WinHttp/winhttp_connection.h b/Source/HTTP/WinHttp/winhttp_connection.h index eedd2eba..be17818d 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.h +++ b/Source/HTTP/WinHttp/winhttp_connection.h @@ -191,6 +191,7 @@ class WinHttpConnection : public std::enable_shared_from_this HRESULT Initialize(); static HRESULT query_header_length(_In_ HCCallHandle call, _In_ HINTERNET hRequestHandle, _In_ DWORD header, _Out_ DWORD* pLength); + static HRESULT query_and_parse_headers(_In_ HCCallHandle call, _In_ HINTERNET hRequestHandle); static uint32_t parse_status_code( _In_ HCCallHandle call, _In_ HINTERNET hRequestHandle, diff --git a/Source/HTTP/WinHttp/winhttp_provider.cpp b/Source/HTTP/WinHttp/winhttp_provider.cpp index 7c511d4f..54417605 100644 --- a/Source/HTTP/WinHttp/winhttp_provider.cpp +++ b/Source/HTTP/WinHttp/winhttp_provider.cpp @@ -733,6 +733,20 @@ HRESULT WinHttp_WebSocketProvider::Disconnect( { return WinHttpProvider->Disconnect(websocketHandle, closeStatus); } + +void WinHttp_WebSocketProvider::OnSuspending() noexcept +{ +#if HC_PLATFORM == HC_PLATFORM_GDK + WinHttpProvider->Suspend(); +#endif +} + +void WinHttp_WebSocketProvider::OnResuming() noexcept +{ +#if HC_PLATFORM == HC_PLATFORM_GDK + WinHttpProvider->Resume(); +#endif +} #endif // !HC_NOWEBSOCKETS NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/HTTP/WinHttp/winhttp_provider.h b/Source/HTTP/WinHttp/winhttp_provider.h index 7ed94820..5e5410ef 100644 --- a/Source/HTTP/WinHttp/winhttp_provider.h +++ b/Source/HTTP/WinHttp/winhttp_provider.h @@ -157,7 +157,7 @@ class WinHttp_HttpProvider : public IHttpProvider }; #ifndef HC_NOWEBSOCKETS -class WinHttp_WebSocketProvider : public IWebSocketProvider +class WinHttp_WebSocketProvider : public IWebSocketProvider, public IProviderLifecycle { public: WinHttp_WebSocketProvider(std::shared_ptr provider); @@ -187,6 +187,9 @@ class WinHttp_WebSocketProvider : public IWebSocketProvider HCWebSocketCloseStatus closeStatus ) noexcept override; + void OnSuspending() noexcept override; + void OnResuming() noexcept override; + SharedPtr const WinHttpProvider; }; #endif diff --git a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp new file mode 100644 index 00000000..6bae5a9d --- /dev/null +++ b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp @@ -0,0 +1,140 @@ +#include "pch.h" +#include "winhttp_websocket_hybrid.h" + +#if !defined(HC_NOWEBSOCKETS) && defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + +#include "winhttp_connection.h" +#include "WebSocket/websocket_options.h" +#include "WebSocket/Websocketpp/websocketpp_websocket.h" + +NAMESPACE_XBOX_HTTP_CLIENT_BEGIN + +WinHttpHybrid_WebSocketProvider::WinHttpHybrid_WebSocketProvider(std::shared_ptr provider) : + m_winHttpProvider{ http_allocate_unique(provider) }, + m_wsppProvider{ http_allocate_unique() } +{ +} + +HRESULT WinHttpHybrid_WebSocketProvider::ConnectAsync( + String const& uri, + String const& subprotocol, + HCWebsocketHandle websocketHandle, + XAsyncBlock* async +) noexcept +{ + return ConnectProvider(websocketHandle).ConnectAsync(uri, subprotocol, websocketHandle, async); +} + +HRESULT WinHttpHybrid_WebSocketProvider::SendAsync( + HCWebsocketHandle websocketHandle, + const char* message, + XAsyncBlock* async +) noexcept +{ + auto provider = ActiveProvider(websocketHandle); + RETURN_HR_IF(E_UNEXPECTED, !provider); + return provider->SendAsync(websocketHandle, message, async); +} + +HRESULT WinHttpHybrid_WebSocketProvider::SendBinaryAsync( + HCWebsocketHandle websocketHandle, + const uint8_t* payloadBytes, + uint32_t payloadSize, + XAsyncBlock* asyncBlock +) noexcept +{ + auto provider = ActiveProvider(websocketHandle); + RETURN_HR_IF(E_UNEXPECTED, !provider); + return provider->SendBinaryAsync(websocketHandle, payloadBytes, payloadSize, asyncBlock); +} + +HRESULT WinHttpHybrid_WebSocketProvider::Disconnect( + HCWebsocketHandle websocketHandle, + HCWebSocketCloseStatus closeStatus +) noexcept +{ + auto provider = ActiveProvider(websocketHandle); + RETURN_HR_IF(E_UNEXPECTED, !provider); + return provider->Disconnect(websocketHandle, closeStatus); +} + +HRESULT WinHttpHybrid_WebSocketProvider::OptionsResult(HCWebSocketOptions options) const noexcept +{ + if (HasUnsupportedWebSocketOptions(options)) + { + return E_NOT_SUPPORTED; + } + + if (RequestsLegacyWebSocketSemantics(options)) + { + return S_OK; + } + + return m_wsppProvider->OptionsResult(options); +} + +void WinHttpHybrid_WebSocketProvider::OnSuspending() noexcept +{ + if (auto lifecycle = GetProviderLifecycle(m_winHttpProvider.get())) + { + lifecycle->OnSuspending(); + } + + if (auto lifecycle = GetProviderLifecycle(m_wsppProvider.get())) + { + lifecycle->OnSuspending(); + } +} + +void WinHttpHybrid_WebSocketProvider::OnResuming() noexcept +{ + if (auto lifecycle = GetProviderLifecycle(m_winHttpProvider.get())) + { + lifecycle->OnResuming(); + } + + if (auto lifecycle = GetProviderLifecycle(m_wsppProvider.get())) + { + lifecycle->OnResuming(); + } +} + +IWebSocketProvider& WinHttpHybrid_WebSocketProvider::ConnectProvider(HCWebsocketHandle websocketHandle) noexcept +{ + if (websocketHandle->websocket->UsesDeterministicSemantics()) + { + return *m_wsppProvider; + } + + return *m_winHttpProvider; +} + +IWebSocketProvider* WinHttpHybrid_WebSocketProvider::ActiveProvider(HCWebsocketHandle websocketHandle) noexcept +{ + if (websocketHandle == nullptr || !websocketHandle->websocket) + { + return nullptr; + } + + auto const& impl = websocketHandle->websocket->impl; + if (!impl) + { + return nullptr; + } + + if (std::dynamic_pointer_cast(impl)) + { + return m_winHttpProvider.get(); + } + + if (IsWebSocketppConnection(impl)) + { + return m_wsppProvider.get(); + } + + return nullptr; +} + +NAMESPACE_XBOX_HTTP_CLIENT_END + +#endif // !HC_NOWEBSOCKETS && HC_ENABLE_WEBSOCKET_COMPRESSION diff --git a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h new file mode 100644 index 00000000..7b9a240d --- /dev/null +++ b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h @@ -0,0 +1,52 @@ +#pragma once + +#include "winhttp_provider.h" + +NAMESPACE_XBOX_HTTP_CLIENT_BEGIN + +#if !defined(HC_NOWEBSOCKETS) && defined(HC_ENABLE_WEBSOCKET_COMPRESSION) +class WinHttpHybrid_WebSocketProvider final : public IWebSocketProvider, public IProviderLifecycle +{ +public: + WinHttpHybrid_WebSocketProvider(std::shared_ptr provider); + + HRESULT ConnectAsync( + String const& uri, + String const& subprotocol, + HCWebsocketHandle websocketHandle, + XAsyncBlock* async + ) noexcept override; + + HRESULT SendAsync( + HCWebsocketHandle websocketHandle, + const char* message, + XAsyncBlock* async + ) noexcept override; + + HRESULT SendBinaryAsync( + HCWebsocketHandle websocketHandle, + const uint8_t* payloadBytes, + uint32_t payloadSize, + XAsyncBlock* asyncBlock + ) noexcept override; + + HRESULT Disconnect( + HCWebsocketHandle websocketHandle, + HCWebSocketCloseStatus closeStatus + ) noexcept override; + + HRESULT OptionsResult(HCWebSocketOptions options) const noexcept override; + + void OnSuspending() noexcept override; + void OnResuming() noexcept override; + +private: + IWebSocketProvider& ConnectProvider(HCWebsocketHandle websocketHandle) noexcept; + IWebSocketProvider* ActiveProvider(HCWebsocketHandle websocketHandle) noexcept; + + UniquePtr m_winHttpProvider; + UniquePtr m_wsppProvider; +}; +#endif // !HC_NOWEBSOCKETS && HC_ENABLE_WEBSOCKET_COMPRESSION + +NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/Platform/GDK/PlatformComponents_GDK.cpp b/Source/Platform/GDK/PlatformComponents_GDK.cpp index 869ed197..2bc512b6 100644 --- a/Source/Platform/GDK/PlatformComponents_GDK.cpp +++ b/Source/Platform/GDK/PlatformComponents_GDK.cpp @@ -2,6 +2,9 @@ #include "Platform/PlatformComponents.h" #include "HTTP/Curl/CurlProvider.h" #include "HTTP/WinHttp/winhttp_provider.h" +#if !defined(HC_NOWEBSOCKETS) && defined(HC_ENABLE_WEBSOCKET_COMPRESSION) +#include "HTTP/WinHttp/winhttp_websocket_hybrid.h" +#endif #if HC_PLATFORM == HC_PLATFORM_GDK #include "XSystem.h" @@ -9,25 +12,50 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN -// Helper function to detect if running on Xbox console hardware +// On GDK, desktop PC is the special case. Treat every non-PC device type as console +// so new console device types continue to take the safer console path by default. static bool IsRunningOnXboxConsole() { #if HC_PLATFORM == HC_PLATFORM_GDK - auto deviceType = XSystemGetDeviceType(); - - // Explicitly list all Xbox console device types - return deviceType == XSystemDeviceType::XboxOne || - deviceType == XSystemDeviceType::XboxOneS || - deviceType == XSystemDeviceType::XboxOneX || - deviceType == XSystemDeviceType::XboxOneXDevkit || - deviceType == XSystemDeviceType::XboxScarlettLockhart || // Xbox Series S - deviceType == XSystemDeviceType::XboxScarlettAnaconda || // Xbox Series X - deviceType == XSystemDeviceType::XboxScarlettDevkit; // Xbox Series Devkit + return XSystemGetDeviceType() != XSystemDeviceType::Pc; #else return false; #endif } +#ifndef HC_NOWEBSOCKETS +static bool IsGdkXboxCompressionWebSocketProviderEnabled() noexcept +{ +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) && defined(HC_ENABLE_GDK_XBOX_WEBSOCKET_COMPRESSION) + return true; +#else + return false; +#endif +} + +static HRESULT InitializeGdkWebSocketProviders(PlatformComponents& components, bool enableCompressionWebSocketProvider) +{ + auto initWinHttpResult = WinHttpProvider::Initialize(); + RETURN_IF_FAILED(initWinHttpResult.hr); + + auto winHttpProvider = initWinHttpResult.ExtractPayload(); + auto sharedWinHttpProvider = SharedPtr{ winHttpProvider.release(), std::move(winHttpProvider.get_deleter()), http_stl_allocator{} }; + +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + if (enableCompressionWebSocketProvider) + { + components.WebSocketProvider = http_allocate_unique(sharedWinHttpProvider); + return S_OK; + } +#else + UNREFERENCED_PARAMETER(enableCompressionWebSocketProvider); +#endif + + components.WebSocketProvider = http_allocate_unique(sharedWinHttpProvider); + return S_OK; +} +#endif + HRESULT PlatformInitialize(PlatformComponents& components, HCInitArgs* initArgs) { // We don't expect initArgs on GDK @@ -45,12 +73,13 @@ HRESULT PlatformInitialize(PlatformComponents& components, HCInitArgs* initArgs) components.HttpProvider = initXCurlResult.ExtractPayload(); #ifndef HC_NOWEBSOCKETS - // For Xbox consoles with XCurl HTTP, still use WinHttp for WebSockets - auto initWinHttpResult = WinHttpProvider::Initialize(); - RETURN_IF_FAILED(initWinHttpResult.hr); - - auto winHttpProvider = initWinHttpResult.ExtractPayload(); - components.WebSocketProvider = http_allocate_unique(SharedPtr{ winHttpProvider.release(), std::move(winHttpProvider.get_deleter()), http_stl_allocator{} }); + // For Xbox consoles with XCurl HTTP, still use WinHttp for the default WebSocket path. + bool const enableCompressionWebSocketProvider = IsGdkXboxCompressionWebSocketProviderEnabled(); + if (!enableCompressionWebSocketProvider) + { + HC_TRACE_INFORMATION(HTTPCLIENT, "PlatformInitialize: Xbox console compression WebSocket provider is disabled by build policy"); + } + RETURN_IF_FAILED(InitializeGdkWebSocketProviders(components, enableCompressionWebSocketProvider)); #endif } else @@ -62,52 +91,45 @@ HRESULT PlatformInitialize(PlatformComponents& components, HCInitArgs* initArgs) RETURN_IF_FAILED(initWinHttpResult.hr); auto winHttpProvider = initWinHttpResult.ExtractPayload(); - - // Use the same WinHttpProvider instance for both HTTP and WebSocket + + // Use the same WinHttpProvider instance for both HTTP and the default WebSocket path. auto sharedWinHttpProvider = SharedPtr{ winHttpProvider.release(), std::move(winHttpProvider.get_deleter()), http_stl_allocator{} }; components.HttpProvider = http_allocate_unique(sharedWinHttpProvider); #ifndef HC_NOWEBSOCKETS +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + components.WebSocketProvider = http_allocate_unique(sharedWinHttpProvider); +#else components.WebSocketProvider = http_allocate_unique(sharedWinHttpProvider); +#endif #endif } return S_OK; } -// Test hooks for GDK Suspend/Resume testing -// Note: These hooks assume WinHttp WebSocket provider is available. -// They will work correctly on both Xbox consoles and non-console platforms -// since both configurations use WinHttp for WebSockets. +// Test hooks for GDK suspend/resume testing. These now notify the built-in +// websocket providers through the provider lifecycle capability rather than +// reaching through NetworkState to a concrete provider type. STDAPI_(void) HCWinHttpSuspend() { auto httpSingleton = get_http_singleton(); - if (!httpSingleton) - { - return; - } - auto* winHttpWebSocketProvider = dynamic_cast(&httpSingleton->m_networkState->WebSocketProvider()); - if (!winHttpWebSocketProvider) + if (!httpSingleton || !httpSingleton->m_networkState) { return; } - winHttpWebSocketProvider->WinHttpProvider->Suspend(); + httpSingleton->m_networkState->NotifyWebSocketSuspending(); } STDAPI_(void) HCWinHttpResume() { auto httpSingleton = get_http_singleton(); - if (!httpSingleton) - { - return; - } - auto* winHttpWebSocketProvider = dynamic_cast(&httpSingleton->m_networkState->WebSocketProvider()); - if (!winHttpWebSocketProvider) + if (!httpSingleton || !httpSingleton->m_networkState) { return; } - winHttpWebSocketProvider->WinHttpProvider->Resume(); + httpSingleton->m_networkState->NotifyWebSocketResuming(); } NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/Platform/Win32/PlatformComponents_Win32.cpp b/Source/Platform/Win32/PlatformComponents_Win32.cpp index ff1aeed5..ae834029 100644 --- a/Source/Platform/Win32/PlatformComponents_Win32.cpp +++ b/Source/Platform/Win32/PlatformComponents_Win32.cpp @@ -1,6 +1,9 @@ #include "pch.h" #include "Platform/PlatformComponents.h" #include "HTTP/WinHttp/winhttp_provider.h" +#if !defined(HC_NOWEBSOCKETS) && defined(HC_ENABLE_WEBSOCKET_COMPRESSION) +#include "HTTP/WinHttp/winhttp_websocket_hybrid.h" +#endif NAMESPACE_XBOX_HTTP_CLIENT_BEGIN @@ -18,7 +21,11 @@ HRESULT PlatformInitialize(PlatformComponents& components, HCInitArgs* initArgs) components.HttpProvider = http_allocate_unique(sharedProvider); #ifndef HC_NOWEBSOCKETS +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + components.WebSocketProvider = http_allocate_unique(sharedProvider); +#else components.WebSocketProvider = http_allocate_unique(sharedProvider); +#endif #endif return S_OK; diff --git a/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp b/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp new file mode 100644 index 00000000..7c6613a3 --- /dev/null +++ b/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include +#if defined(__clang__) +// Keep this third-party warning suppression local to the wrapper instead of patching the submodule. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wshorten-64-to-32" +#endif +#include +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +namespace xbox { +namespace httpclient { + +// Wrap websocketpp's vendored permessage-deflate extension so libHttpClient can +// choose the outbound offer parameters without modifying the submodule copy. +template +class configured_permessage_deflate : + public websocketpp::extensions::permessage_deflate::enabled +{ +public: + using base = websocketpp::extensions::permessage_deflate::enabled; + + std::string generate_offer() const + { + if (RequestClientNoContextTakeover) + { + // Preserve websocketpp's tracking that the client offered + // client_no_context_takeover so init(false) selects Z_FULL_FLUSH. + (void)base::generate_offer(); + } + + std::string offer = "permessage-deflate"; + + if (RequestServerNoContextTakeover) + { + offer += "; server_no_context_takeover"; + } + + if (RequestClientNoContextTakeover) + { + offer += "; client_no_context_takeover"; + } + + offer += "; client_max_window_bits"; + return offer; + } +}; + +} // namespace httpclient +} // namespace xbox diff --git a/Source/WebSocket/Websocketpp/websocketpp_disabled_permessage_deflate.hpp b/Source/WebSocket/Websocketpp/websocketpp_disabled_permessage_deflate.hpp new file mode 100644 index 00000000..df07854e --- /dev/null +++ b/Source/WebSocket/Websocketpp/websocketpp_disabled_permessage_deflate.hpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2014, Peter Thorson. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name of the WebSocket++ Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 PETER THORSON 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. + * + */ + +#pragma once + +// Work around a bug in the vendored websocketpp disabled permessage-deflate +// header so libHttpClient does not depend on a patched submodule checkout. +#ifndef WEBSOCKETPP_EXTENSION_PERMESSAGE_DEFLATE_DISABLED_HPP +#define WEBSOCKETPP_EXTENSION_PERMESSAGE_DEFLATE_DISABLED_HPP + +#include +#include +#include + +#include +#include + +#include +#include +#include + +namespace websocketpp { +namespace extensions { +namespace permessage_deflate { + +template +class disabled { + typedef std::pair err_str_pair; + +public: + err_str_pair negotiate(http::attribute_list const &) { + return make_pair( + websocketpp::extensions::error::make_error_code(websocketpp::extensions::error::disabled), + std::string() + ); + } + + lib::error_code init(bool) { + return lib::error_code(); + } + + bool is_implemented() const { + return false; + } + + bool is_enabled() const { + return false; + } + + void set_server_mode(bool) {} + + void enable_server_no_context_takeover() {} + + void enable_client_no_context_takeover() {} + + std::string generate_offer() const { + return ""; + } + + lib::error_code compress(std::string const &, std::string &) { + return websocketpp::extensions::error::make_error_code(websocketpp::extensions::error::disabled); + } + + lib::error_code decompress(uint8_t const *, size_t, std::string &) { + return websocketpp::extensions::error::make_error_code(websocketpp::extensions::error::disabled); + } +}; + +} // namespace permessage_deflate +} // namespace extensions +} // namespace websocketpp + +#endif // WEBSOCKETPP_EXTENSION_PERMESSAGE_DEFLATE_DISABLED_HPP diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index c12018e2..a53c1026 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -6,8 +6,20 @@ #ifndef HC_NOWEBSOCKETS #include "websocketpp_websocket.h" +#include "Global/global.h" #include "uri.h" +#include "WebSocket/websocket_options.h" +#include "websocketpp_configured_permessage_deflate.hpp" +#include "websocketpp_disabled_permessage_deflate.hpp" +#if !HC_PLATFORM_IS_MICROSOFT #include "x509_cert_utilities.hpp" +#else +#include "wintls_socket.hpp" +#endif + +#if HC_PLATFORM == HC_PLATFORM_GDK +#include "XSystem.h" +#endif #if HC_PLATFORM_IS_APPLE #include "Apple/utils_apple.h" @@ -19,9 +31,6 @@ #ifdef _WIN32 #pragma warning( push ) #pragma warning( disable : 4100 4127 4512 4996 4701 4267 4244 ) -#define _WEBSOCKETPP_CPP11_STL_ -#define _WEBSOCKETPP_CONSTEXPR_TOKEN_ -#define _SCL_SECURE_NO_WARNINGS #if (_MSC_VER >= 1900) #define ASIO_ERROR_CATEGORY_NOEXCEPT noexcept(true) #endif // (_MSC_VER >= 1900) @@ -31,8 +40,10 @@ #pragma clang diagnostic ignored "-Wshorten-64-to-32" #endif -#include #include +#if !HC_PLATFORM_IS_MICROSOFT +#include +#endif #include #include #include @@ -40,6 +51,9 @@ #include "../HTTP/Android/android_platform_context.h" #endif +#include +#include + #if defined(_WIN32) #pragma warning( pop ) #elif defined(__clang__) @@ -49,12 +63,306 @@ #define SUB_PROTOCOL_HEADER "Sec-WebSocket-Protocol" #define WSPP_PING_INTERVAL_MS 1000 #define WSPP_SHUTDOWN_TIMEOUT_MS 5000 +#define WSPP_SHUTDOWN_POLL_INTERVAL_MS 100 using namespace xbox::httpclient; namespace { +constexpr size_t WsppConfiguredMaxMessageSize = websocketpp::config::core_client::max_message_size; +constexpr size_t WsppMaxZlibInputSize = static_cast((std::numeric_limits::max)()); +constexpr size_t WebSocketCallbackPayloadSizeLimit = static_cast((std::numeric_limits::max)()); + +static_assert( + WsppConfiguredMaxMessageSize <= WsppMaxZlibInputSize, + "websocketpp max message size must fit in zlib's uInt input width"); +static_assert( + WsppConfiguredMaxMessageSize <= WebSocketCallbackPayloadSizeLimit, + "websocketpp max message size must fit in websocket callback size fields"); + +constexpr uint32_t RequestCompressionOptionMask = static_cast(HCWebSocketOptions::RequestCompression); +constexpr uint32_t ServerNoContextTakeoverOptionMask = static_cast(HCWebSocketOptions::CompressionServerNoContextTakeover); +constexpr uint32_t ClientNoContextTakeoverOptionMask = static_cast(HCWebSocketOptions::CompressionClientNoContextTakeover); + +enum class CompressionClientPolicy +{ + Default, + CompressionServerNoContextTakeover, + CompressionClientNoContextTakeover, + ServerAndClientNoContextTakeover +}; + +bool HasCompressionOption(HCWebSocketOptions options, uint32_t optionMask) noexcept +{ + return (static_cast(options) & optionMask) != 0; +} + +bool ShouldUseCompression(HCWebsocketHandle websocketHandle) noexcept +{ +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + auto const options = websocketHandle->websocket->Options(); + return HasCompressionOption(options, RequestCompressionOptionMask); +#else + UNREFERENCED_PARAMETER(websocketHandle); + return false; +#endif +} + +bool ShouldRequestServerNoContextTakeover(HCWebsocketHandle websocketHandle) noexcept +{ +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + return HasCompressionOption(websocketHandle->websocket->Options(), ServerNoContextTakeoverOptionMask); +#else + UNREFERENCED_PARAMETER(websocketHandle); + return false; +#endif +} + +bool ShouldRequestClientNoContextTakeover(HCWebsocketHandle websocketHandle) noexcept +{ +#if defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + return HasCompressionOption(websocketHandle->websocket->Options(), ClientNoContextTakeoverOptionMask); +#else + UNREFERENCED_PARAMETER(websocketHandle); + return false; +#endif +} + +CompressionClientPolicy GetCompressionClientPolicy(HCWebsocketHandle websocketHandle) noexcept +{ + bool const requestServerNoContextTakeover = ShouldRequestServerNoContextTakeover(websocketHandle); + bool const requestClientNoContextTakeover = ShouldRequestClientNoContextTakeover(websocketHandle); + + if (requestServerNoContextTakeover) + { + return requestClientNoContextTakeover ? + CompressionClientPolicy::ServerAndClientNoContextTakeover : + CompressionClientPolicy::CompressionServerNoContextTakeover; + } + + return requestClientNoContextTakeover ? + CompressionClientPolicy::CompressionClientNoContextTakeover : + CompressionClientPolicy::Default; +} + +long ClampWsppPongTimeoutMs(uint32_t pingIntervalSeconds) noexcept +{ + auto const pingIntervalMs = static_cast(pingIntervalSeconds) * 1000ULL; + auto const maxPongTimeoutMs = static_cast((std::numeric_limits::max)()); + return pingIntervalMs > maxPongTimeoutMs ? (std::numeric_limits::max)() : static_cast(pingIntervalMs); +} + +size_t ResolveWsppMaxMessageSize(HCWebsocketHandle websocketHandle) noexcept +{ + auto const& websocket = websocketHandle->websocket; + if (!websocket->UsesDeterministicSemantics()) + { + return WsppConfiguredMaxMessageSize; + } + + return websocket->MaxReceiveBufferSizeExplicitlySet() ? + websocket->MaxReceiveBufferSize() : + WsppConfiguredMaxMessageSize; +} + +bool TryParseProxyUri( + http_internal_string const& rawProxyUri, + Uri& proxyUri +) +{ + proxyUri = Uri{ rawProxyUri }; + if (proxyUri.IsValid()) + { + return true; + } + + if (rawProxyUri.find("://") == http_internal_string::npos) + { + proxyUri = Uri{ "http://" + rawProxyUri }; + } + + return proxyUri.IsValid(); +} + +http_internal_string BuildProxyEndpointUri(Uri const& proxyUri) +{ + http_internal_string proxyEndpointUri{ proxyUri.Scheme() }; + proxyEndpointUri += "://"; + proxyEndpointUri += proxyUri.Host(); + if (!proxyUri.IsPortDefault() && proxyUri.Port() > 0) + { + proxyEndpointUri += ":"; + proxyEndpointUri += std::to_string(proxyUri.Port()); + } + + return proxyEndpointUri; +} + +bool TryPercentDecodeUserInfo(http_internal_string const& value, http_internal_string& decoded) +{ + decoded.clear(); + decoded.reserve(value.size()); + + for (size_t i = 0; i < value.size(); ++i) + { + if (value[i] == '%') + { + if (value.size() - i < 3) + { + return false; + } + + uint8_t decodedByte = 0; + if (!HexDecodePair(value[i + 1], value[i + 2], decodedByte)) + { + return false; + } + + decoded.push_back(static_cast(decodedByte)); + i += 2; + continue; + } + + decoded.push_back(value[i]); + } + + return true; +} + +bool ParseProxyCredentials( + Uri const& proxyUri, + http_internal_string& username, + http_internal_string& password +) +{ + auto const& userInfo = proxyUri.UserInfo(); + if (userInfo.empty()) + { + return true; + } + + auto const separator = userInfo.find(':'); + if (separator == http_internal_string::npos) + { + password.clear(); + return TryPercentDecodeUserInfo(userInfo, username); + } + + return TryPercentDecodeUserInfo(userInfo.substr(0, separator), username) && + TryPercentDecodeUserInfo(userInfo.substr(separator + 1), password); +} + +template +HRESULT ApplyProxySettings( + ConnectionPtr const& con, + Uri const& proxyUri, + http_internal_string const& username, + http_internal_string const& password, + uint64_t websocketId +) +{ + websocketpp::lib::error_code ec; + auto const proxyEndpointUri = BuildProxyEndpointUri(proxyUri); + con->set_proxy(proxyEndpointUri.data(), ec); + if (ec) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy failed", TO_ULL(websocketId)); + return E_FAIL; + } + + if (!username.empty()) + { + con->set_proxy_basic_auth(std::string{ username.c_str() }, std::string{ password.c_str() }, ec); + if (ec) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy_basic_auth failed", TO_ULL(websocketId)); + return E_FAIL; + } + } + + return S_OK; +} + +HRESULT ResolveEffectiveProxyDecryptsHttpsSetting( + HCWebsocketHandle websocket, + bool requestedAllowProxyToDecryptHttps, + bool& effectiveAllowProxyToDecryptHttps +) +{ + effectiveAllowProxyToDecryptHttps = requestedAllowProxyToDecryptHttps; + +#if HC_PLATFORM == HC_PLATFORM_GDK + if (requestedAllowProxyToDecryptHttps && XSystemGetDeviceType() != XSystemDeviceType::Pc) + { + auto httpSingleton = get_http_singleton(); + RETURN_HR_IF(E_HC_NOT_INITIALISED, !httpSingleton); + + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: On GDK console, TLS server validation is enforced regardless of ProxyDecryptsHttps to match the built-in WinHTTP WebSocket path", TO_ULL(websocket->websocket->id)); + if (!httpSingleton->m_disableAssertsForSSLValidationInDevSandboxes) + { + HC_TRACE_ERROR(WEBSOCKET, "On GDK console, TLS server validation is enforced regardless of ProxyDecryptsHttps to match the built-in WinHTTP WebSocket path."); + HC_TRACE_ERROR(WEBSOCKET, "Call HCHttpDisableAssertsForSSLValidationInDevSandboxes() to turn this assert off"); + assert(false && "On GDK console, TLS server validation is enforced regardless of ProxyDecryptsHttps() to match the built-in WinHTTP WebSocket path. See Output for more detail"); + } + + char sandbox[XSystemXboxLiveSandboxIdMaxBytes] = { 0 }; + HRESULT hr = XSystemGetXboxLiveSandboxId(XSystemXboxLiveSandboxIdMaxBytes, sandbox, nullptr); + if (ShouldForceTlsValidationForGdkSandbox(hr, sandbox)) + { + // Fail-closed: if we cannot determine the sandbox, or if the sandbox + // is RETAIL, enforce TLS server validation unconditionally. + effectiveAllowProxyToDecryptHttps = false; + } + } +#else + UNREFERENCED_PARAMETER(websocket); +#endif + + return S_OK; +} + +HRESULT HResultFromPlatformNetworkError(uint32_t platformErrorCode) noexcept +{ + if (platformErrorCode == 0) + { + return S_OK; + } + + auto const signedPlatformErrorCode = static_cast(platformErrorCode); + if (signedPlatformErrorCode <= 0) + { + return static_cast(signedPlatformErrorCode); + } + + return __HRESULT_FROM_WIN32(platformErrorCode); +} + +HRESULT HResultFromConnectError( + websocketpp::lib::error_code const& connectError, + websocketpp::http::status_code::value connectStatusCode +) noexcept +{ + if (!connectError) + { + return S_OK; + } + + if (connectError == make_error_code(websocketpp::processor::error::invalid_http_status)) + { + return MAKE_HRESULT(1, FACILITY_HTTP, connectStatusCode); + } + + if (connectError.category() == std::system_category() || + connectError.category() == std::generic_category() || + connectError.category() == asio::error::get_system_category()) + { + return HResultFromPlatformNetworkError(static_cast(connectError.value())); + } + + return E_FAIL; +} + struct alevel_logger : websocketpp::log::stub { using websocketpp::log::stub::stub; @@ -131,8 +439,8 @@ struct elevel_logger : websocketpp::log::stub #endif }; -template -struct httpclient_config : base +template +struct httpclient_config : base_config { /// Logging policies using alog_type = alevel_logger; @@ -144,17 +452,111 @@ struct httpclient_config : base /// Default static access logging channels static const websocketpp::log::level elog_level = websocketpp::log::elevel::all; - struct transport_config : public base::transport_config + struct transport_config : public base_config::transport_config { - using alog_type = alog_type; - using elog_type = elog_type; + using concurrency_type = typename base_config::transport_config::concurrency_type; + using alog_type = typename httpclient_config::alog_type; + using elog_type = typename httpclient_config::elog_type; + using request_type = typename base_config::transport_config::request_type; + using response_type = typename base_config::transport_config::response_type; + using socket_type = typename base_config::transport_config::socket_type; }; using transport_type = websocketpp::transport::asio::endpoint; }; using ws = httpclient_config; +#if HC_PLATFORM_IS_MICROSOFT +struct wintls_asio_client_config : public websocketpp::config::core_client +{ + typedef wintls_asio_client_config type; + typedef websocketpp::config::core_client base; + + typedef base::concurrency_type concurrency_type; + typedef base::request_type request_type; + typedef base::response_type response_type; + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config + { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::wintls_socket::endpoint socket_type; + }; + + typedef websocketpp::transport::asio::endpoint transport_type; +}; + +using wss = httpclient_config; +#else using wss = httpclient_config; +#endif + +template +struct httpclient_compression_config : httpclient_config +{ + struct permessage_deflate_config + { + using request_type = typename httpclient_config::request_type; + + // Keep websocketpp's response-side negotiation behavior explicit: accept + // server requests to disable context takeover and reduce the outgoing + // compression window as far as the vendored extension allows. + static const bool allow_disabling_context_takeover = true; + static const uint8_t minimum_outgoing_window_bits = 8; + }; + + using permessage_deflate_type = + xbox::httpclient::configured_permessage_deflate< + permessage_deflate_config, + RequestServerNoContextTakeover, + RequestClientNoContextTakeover>; +}; + +using ws_compression = httpclient_compression_config; +using ws_compression_server_no_context_takeover = httpclient_compression_config; +using ws_compression_client_no_context_takeover = httpclient_compression_config; +using ws_compression_server_and_client_no_context_takeover = httpclient_compression_config; +#if HC_PLATFORM_IS_MICROSOFT +using wss_compression = httpclient_compression_config; +using wss_compression_server_no_context_takeover = httpclient_compression_config; +using wss_compression_client_no_context_takeover = httpclient_compression_config; +using wss_compression_server_and_client_no_context_takeover = httpclient_compression_config; +#else +using wss_compression = httpclient_compression_config; +using wss_compression_server_no_context_takeover = httpclient_compression_config; +using wss_compression_client_no_context_takeover = httpclient_compression_config; +using wss_compression_server_and_client_no_context_takeover = httpclient_compression_config; +#endif + +template +struct compression_client_config_types; + +template<> +struct compression_client_config_types +{ + using default_type = wss_compression; + using server_no_context_takeover_type = wss_compression_server_no_context_takeover; + using client_no_context_takeover_type = wss_compression_client_no_context_takeover; + using server_and_client_no_context_takeover_type = wss_compression_server_and_client_no_context_takeover; +}; + +template<> +struct compression_client_config_types +{ + using default_type = ws_compression; + using server_no_context_takeover_type = ws_compression_server_no_context_takeover; + using client_no_context_takeover_type = ws_compression_client_no_context_takeover; + using server_and_client_no_context_takeover_type = ws_compression_server_and_client_no_context_takeover; +}; struct websocket_outgoing_message { @@ -201,78 +603,32 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared HRESULT connect(XAsyncBlock* async) { - if (m_uri.Scheme() == "wss") - { - m_client = std::unique_ptr(new websocketpp_tls_client()); - - auto sharedThis{ shared_from_this() }; - - // Options specific to TLS client. - auto &client = m_client->impl(); - client.set_tls_init_handler([sharedThis](websocketpp::connection_hdl) - { - auto sslContext = websocketpp::lib::shared_ptr(new asio::ssl::context(asio::ssl::context::sslv23)); - sslContext->set_default_verify_paths(); - sslContext->set_options(asio::ssl::context::default_workarounds); - sslContext->set_verify_mode(asio::ssl::context::verify_peer); - - sharedThis->m_opensslFailed = false; - sslContext->set_verify_callback([sharedThis](bool preverified, asio::ssl::verify_context &verifyCtx) - { - // allow to use proxies that decrypt https for debugging - if (sharedThis->m_hcWebsocketHandle->websocket->ProxyDecryptsHttps()) - { - return true; - } - - // On OS X, iOS, and Android, OpenSSL doesn't have access to where the OS - // stores keychains. If OpenSSL fails we will doing verification at the - // end using the whole certificate chain so wait until the 'leaf' cert. - // For now return true so OpenSSL continues down the certificate chain. - if (!preverified) - { - sharedThis->m_opensslFailed = true; - } - if (sharedThis->m_opensslFailed) - { - return xbox::httpclient::verify_cert_chain_platform_specific(verifyCtx, sharedThis->m_uri.Host()); - } - asio::ssl::rfc2818_verification rfc2818(sharedThis->m_uri.Host().data()); - return rfc2818(preverified, verifyCtx); - }); - - // OpenSSL stores some per thread state that never will be cleaned up until - // the dll is unloaded. If static linking, like we do, the state isn't cleaned up - // at all and will be reported as leaks. - // See http://www.openssl.org/support/faq.html#PROG13 - // This is necessary here because it is called on the user's thread calling connect(...) - // eventually through websocketpp::client::get_connection(...) -#if HC_PLATFORM == HC_PLATFORM_ANDROID || HC_PLATFORM_IS_APPLE || HC_PLATFORM == HC_PLATFORM_LINUX -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - ERR_remove_thread_state(nullptr); -#pragma clang diagnostic pop -#else - ERR_remove_thread_state(nullptr); -#endif // HC_ANDROID_API || HC_PLATFORM_IS_APPLE || HC_PLATFORM_LINUX + bool const enableCompression = ShouldUseCompression(m_hcWebsocketHandle); + auto const compressionClientPolicy = GetCompressionClientPolicy(m_hcWebsocketHandle); + bool allowProxyToDecryptHttps = m_hcWebsocketHandle->websocket->ProxyDecryptsHttps(); + RETURN_IF_FAILED(ResolveEffectiveProxyDecryptsHttpsSetting( + m_hcWebsocketHandle, + m_hcWebsocketHandle->websocket->ProxyDecryptsHttps(), + allowProxyToDecryptHttps)); - return sslContext; - }); + bool const isTlsClient = m_uri.Scheme() == "wss"; - // Options specific to underlying socket. - client.set_socket_init_handler([sharedThis](websocketpp::connection_hdl, asio::ssl::stream &ssl_stream) + if (enableCompression) + { + if (isTlsClient) { - // If user specified server name is empty default to use URI host name. - SSL_set_tlsext_host_name(ssl_stream.native_handle(), sharedThis->m_uri.Host().data()); - }); + return create_compression_client(compressionClientPolicy, async, allowProxyToDecryptHttps); + } - return connect_impl(async); + return create_compression_client(compressionClientPolicy, async, allowProxyToDecryptHttps); } - else + + if (isTlsClient) { - m_client = std::unique_ptr(new websocketpp_client()); - return connect_impl(async); + return create_client(async, allowProxyToDecryptHttps); } + + return create_client(async, allowProxyToDecryptHttps); } HRESULT send(XAsyncBlock* async, const char* payloadPtr) @@ -299,6 +655,7 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared { return E_INVALIDARG; } + RETURN_HR_IF(E_INVALIDARG, payload.length() > WsppMaxZlibInputSize); websocket_outgoing_message message; message.async = async; @@ -342,6 +699,7 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared { return E_INVALIDARG; } + RETURN_HR_IF(E_INVALIDARG, static_cast(payloadSize) > WsppMaxZlibInputSize); websocket_outgoing_message message; message.async = async; @@ -375,16 +733,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared m_state = DISCONNECTING; websocketpp::lib::error_code ec{}; - if (m_client->is_tls_client()) - { - auto &client = m_client->impl(); - client.close(m_con, static_cast(status), std::string(), ec); - } - else + invoke_wspp_client([this, status, &ec](auto& client) { - auto &client = m_client->impl(); client.close(m_con, static_cast(status), std::string(), ec); - } + }); return ec ? E_FAIL : S_OK; } @@ -395,6 +747,226 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared } private: + template + HRESULT create_client(XAsyncBlock* async, bool allowProxyToDecryptHttps) + { + m_client = std::unique_ptr(new websocketpp_client_impl()); + return create_client_impl( + async, + allowProxyToDecryptHttps, + std::integral_constant{}); + } + + template + HRESULT create_compression_client( + CompressionClientPolicy compressionClientPolicy, + XAsyncBlock* async, + bool allowProxyToDecryptHttps) + { + using config_types = compression_client_config_types; + + switch (compressionClientPolicy) + { + case CompressionClientPolicy::Default: + return create_client(async, allowProxyToDecryptHttps); + + case CompressionClientPolicy::CompressionServerNoContextTakeover: + return create_client(async, allowProxyToDecryptHttps); + + case CompressionClientPolicy::CompressionClientNoContextTakeover: + return create_client(async, allowProxyToDecryptHttps); + + case CompressionClientPolicy::ServerAndClientNoContextTakeover: + return create_client(async, allowProxyToDecryptHttps); + } + + ASSERT(false); + return E_FAIL; + } + + template + HRESULT create_client_impl(XAsyncBlock* async, bool allowProxyToDecryptHttps, std::false_type) + { + UNREFERENCED_PARAMETER(allowProxyToDecryptHttps); + return connect_impl(async); + } + + template + HRESULT create_client_impl(XAsyncBlock* async, bool allowProxyToDecryptHttps, std::true_type) + { + // Configure the TLS-specific websocketpp hooks after connect() has selected the + // concrete client type for this connection. +#if HC_PLATFORM_IS_MICROSOFT +#if HC_PLATFORM == HC_PLATFORM_GDK + // Defense-in-depth: never disable cert validation on GDK console, + // regardless of sandbox. ResolveEffectiveProxyDecryptsHttpsSetting is + // the primary gate; this is the fail-closed backstop at the point + // where the WinTLS context is actually configured. + allowProxyToDecryptHttps = ApplyTlsValidationBackstopForGdkConsole( + allowProxyToDecryptHttps, + XSystemGetDeviceType() == XSystemDeviceType::Pc); +#endif + auto& client = m_client->impl(); + client.set_certificate_revocation_check(!allowProxyToDecryptHttps); + client.set_tls_init_handler([allowProxyToDecryptHttps](websocketpp::connection_hdl) + { + auto tlsContext = websocketpp::lib::shared_ptr(new wintls::context(wintls::method::system_default)); + tlsContext->use_default_certificates(true); + tlsContext->verify_server_certificate(!allowProxyToDecryptHttps); + return tlsContext; + }); +#else + auto sharedThis{ shared_from_this() }; + auto& client = m_client->impl(); + client.set_tls_init_handler([sharedThis](websocketpp::connection_hdl) + { + auto sslContext = websocketpp::lib::shared_ptr(new asio::ssl::context(asio::ssl::context::sslv23)); + sslContext->set_default_verify_paths(); + sslContext->set_options(asio::ssl::context::default_workarounds); + sslContext->set_verify_mode(asio::ssl::context::verify_peer); + + sharedThis->m_opensslFailed = false; + sslContext->set_verify_callback([sharedThis](bool preverified, asio::ssl::verify_context &verifyCtx) + { + // Allow debugging proxies that decrypt HTTPS and re-sign the connection. + if (sharedThis->m_hcWebsocketHandle->websocket->ProxyDecryptsHttps()) + { + return true; + } + + // Record the first OpenSSL verification failure and keep walking the chain so + // verify_cert_chain_platform_specific(...) can make the final decision at the leaf. + if (!preverified) + { + sharedThis->m_opensslFailed = true; + } + if (sharedThis->m_opensslFailed) + { + return xbox::httpclient::verify_cert_chain_platform_specific(verifyCtx, sharedThis->m_uri.Host()); + } + asio::ssl::rfc2818_verification rfc2818(sharedThis->m_uri.Host().data()); + return rfc2818(preverified, verifyCtx); + }); + + // OpenSSL stores some per-thread state that is only reclaimed when the library is + // unloaded. Because websocketpp::client::get_connection(...) creates the TLS context + // on the caller's connect(...) thread, clean up that thread-local state here as well + // as on the background websocketpp thread below. +#if HC_PLATFORM == HC_PLATFORM_ANDROID || HC_PLATFORM_IS_APPLE || HC_PLATFORM == HC_PLATFORM_LINUX +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + ERR_remove_thread_state(nullptr); +#pragma clang diagnostic pop +#else + ERR_remove_thread_state(nullptr); +#endif + + return sslContext; + }); + + // Configure the underlying TLS socket after the SSL context is ready. libHttpClient does + // not expose a separate SNI override, so use the URI host name for TLS SNI. + client.set_socket_init_handler([sharedThis](websocketpp::connection_hdl, asio::ssl::stream &ssl_stream) + { + SSL_set_tlsext_host_name(ssl_stream.native_handle(), sharedThis->m_uri.Host().data()); + }); +#endif + + return connect_impl(async); + } + + template + void invoke_compression_wspp_client(Operation&& operation) + { + using config_types = compression_client_config_types; + + switch (GetCompressionClientPolicy(m_hcWebsocketHandle)) + { + case CompressionClientPolicy::Default: + operation(m_client->impl()); + return; + + case CompressionClientPolicy::CompressionServerNoContextTakeover: + operation(m_client->impl()); + return; + + case CompressionClientPolicy::CompressionClientNoContextTakeover: + operation(m_client->impl()); + return; + + case CompressionClientPolicy::ServerAndClientNoContextTakeover: + operation(m_client->impl()); + return; + } + + ASSERT(false); + } + + template + void invoke_compression_wspp_client(Operation&& operation) + { + if (m_client->is_tls_client()) + { + invoke_compression_wspp_client(std::forward(operation)); + return; + } + + invoke_compression_wspp_client(std::forward(operation)); + } + + template + void invoke_wspp_client(Operation&& operation) + { + ASSERT(m_client != nullptr); + + bool const enableCompression = m_client->uses_compression(); + if (enableCompression) + { + invoke_compression_wspp_client(std::forward(operation)); + return; + } + + if (m_client->is_tls_client()) + { + operation(m_client->impl()); + } + else + { + operation(m_client->impl()); + } + } + + template + void complete_connect_start_failure( + XAsyncBlock* async, + websocketpp::lib::error_code connectError + ) + { + { + std::lock_guard lock{ m_wsppClientLock }; + + if (m_client != nullptr) + { + auto& client = m_client->impl(); + client.stop_perpetual(); + client.stop(); + m_client.reset(); + } + + m_state = DISCONNECTED; + } + + { + std::lock_guard lock{ m_websocketThreadStateMutex }; + m_websocketThreadExited = true; + } + m_websocketThreadStateCondition.notify_all(); + + m_connectError = connectError; + m_connectStatusCode = websocketpp::http::status_code::value{}; + XAsyncComplete(async, S_OK, sizeof(WebSocketCompletionResult)); + } + template HRESULT connect_impl(XAsyncBlock* async) { @@ -407,7 +979,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared auto &client = m_client->impl(); - const long pingIntervalMs = m_hcWebsocketHandle->websocket->PingInterval() * 1000; + // Keep inbound websocketpp payloads within the widths required by zlib and our callback signatures. + client.set_max_message_size(ResolveWsppMaxMessageSize(m_hcWebsocketHandle)); + + const auto pingIntervalMs = ClampWsppPongTimeoutMs(m_hcWebsocketHandle->websocket->PingInterval()); client.set_pong_timeout(pingIntervalMs); // default ping interval is 0, which disables the timeout client.init_asio(); @@ -416,19 +991,21 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared auto sharedThis { shared_from_this() }; ASSERT(m_state == DISCONNECTED); - client.set_open_handler([sharedThis, async](websocketpp::connection_hdl) + client.set_open_handler([sharedThis, async](websocketpp::connection_hdl hdl) { ASSERT(sharedThis->m_state == CONNECTING); sharedThis->m_state = CONNECTED; + sharedThis->set_response_headers(hdl); sharedThis->set_connection_error(); sharedThis->set_connect_status(); sharedThis->send_ping(); XAsyncComplete(async, S_OK, sizeof(WebSocketCompletionResult)); }); - client.set_fail_handler([sharedThis, async](websocketpp::connection_hdl) + client.set_fail_handler([sharedThis, async](websocketpp::connection_hdl hdl) { ASSERT(sharedThis->m_state == CONNECTING); + sharedThis->set_response_headers(hdl); sharedThis->set_connection_error(); sharedThis->set_connect_status(); sharedThis->shutdown_wspp_impl( @@ -441,29 +1018,25 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared }); }); - client.set_message_handler([sharedThis](websocketpp::connection_hdl, const ws::message_type::ptr &msg) + client.set_message_handler([sharedThis](websocketpp::connection_hdl, typename WebsocketConfigType::message_type::ptr const& msg) { HCWebSocketMessageFunction messageFunc{ nullptr }; HCWebSocketBinaryMessageFunction binaryMessageFunc{ nullptr }; - void* context{ nullptr }; - auto hr = HCWebSocketGetEventFunctions(sharedThis->m_hcWebsocketHandle, &messageFunc, &binaryMessageFunc, nullptr, &context); + void* callbackContext{ nullptr }; + auto hr = HCWebSocketGetEventFunctions(sharedThis->m_hcWebsocketHandle, &messageFunc, &binaryMessageFunc, nullptr, &callbackContext); if (SUCCEEDED(hr)) { ASSERT(messageFunc && binaryMessageFunc); - // TODO: hook up HCWebSocketCloseEventFunction handler upon unexpected disconnect - // TODO: verify auto disconnect when closing client's websocket handle - + auto const& payload = msg->get_raw_payload(); if (msg->get_opcode() == websocketpp::frame::opcode::text) { - auto& payload = msg->get_raw_payload(); - messageFunc(sharedThis->m_hcWebsocketHandle, payload.data(), context); + messageFunc(sharedThis->m_hcWebsocketHandle, payload.c_str(), callbackContext); } else if (msg->get_opcode() == websocketpp::frame::opcode::binary) { - auto& payload = msg->get_raw_payload(); - binaryMessageFunc(sharedThis->m_hcWebsocketHandle, (uint8_t*)payload.data(), (uint32_t)payload.size(), context); + binaryMessageFunc(sharedThis->m_hcWebsocketHandle, (uint8_t*)payload.data(), (uint32_t)payload.size(), callbackContext); } } }); @@ -533,58 +1106,48 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared // Setup proxy options. if (!m_hcWebsocketHandle->websocket->ProxyUri().empty()) { - con->set_proxy(m_hcWebsocketHandle->websocket->ProxyUri().data(), ec); - if (ec) + Uri explicitProxyUri; + if (!TryParseProxyUri(m_hcWebsocketHandle->websocket->ProxyUri(), explicitProxyUri)) { - HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy failed", TO_ULL(m_hcWebsocketHandle->websocket->id)); - return E_FAIL; + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: explicit proxy URI was invalid", TO_ULL(m_hcWebsocketHandle->websocket->id)); + return E_INVALIDARG; + } + + http_internal_string username; + http_internal_string password; + if (!ParseProxyCredentials(explicitProxyUri, username, password)) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: explicit proxy URI credentials were invalid", TO_ULL(m_hcWebsocketHandle->websocket->id)); + return E_INVALIDARG; } + + RETURN_IF_FAILED(ApplyProxySettings(con, explicitProxyUri, username, password, m_hcWebsocketHandle->websocket->id)); } #if HC_PLATFORM_IS_MICROSOFT else { // On windows platforms use the IE proxy if the user didn't specify one Uri proxyUri; - auto proxyType = get_ie_proxy_info(proxy_protocol::websocket, proxyUri); + auto proxyType = get_ie_proxy_info(proxy_protocol::https, proxyUri); if (proxyType == proxy_type::named_proxy) { - con->set_proxy(proxyUri.FullPath().data(), ec); - if (ec) - { - HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy failed", TO_ULL(m_hcWebsocketHandle->websocket->id)); - return E_FAIL; - } + RETURN_IF_FAILED(ApplyProxySettings(con, proxyUri, http_internal_string{}, http_internal_string{}, m_hcWebsocketHandle->websocket->id)); } } #elif HC_PLATFORM_IS_APPLE else { Uri proxyUri; - std::string username; - std::string password; + http_internal_string username; + http_internal_string password; if (getSystemProxyForUri(m_uri, &proxyUri, &username, &password)) { - con->set_proxy(proxyUri.FullPath().data(), ec); - if (ec) - { - HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy failed", TO_ULL(m_hcWebsocketHandle->websocket->id)); - return E_FAIL; - } - - if (!username.empty() && !password.empty()) - { - con->set_proxy_basic_auth(username, password, ec); - if (ec) - { - HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: wspp set_proxy_basic_auth failed", TO_ULL(m_hcWebsocketHandle->websocket->id)); - return E_FAIL; - } - } + RETURN_IF_FAILED(ApplyProxySettings(con, proxyUri, username, password, m_hcWebsocketHandle->websocket->id)); } } #endif - // Initialize the 'connect' XAsyncBlock here, but the actually work will happen on the ASIO background thread below + // Initialize the 'connect' XAsyncBlock here, but the actual work will happen on the ASIO background thread below. auto hr = XAsyncBegin(async, shared_ptr_cache::store(shared_from_this()), (void*)HCWebSocketConnectAsync, __FUNCTION__, [](XAsyncOp op, const XAsyncProviderData* data) { @@ -598,17 +1161,8 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared auto result = reinterpret_cast(data->buffer); result->websocket = context->m_hcWebsocketHandle; - result->platformErrorCode = context->m_connectError.value(); - - // capture http status - if (context->m_connectError == make_error_code(websocketpp::processor::error::invalid_http_status)) - { - result->errorCode = MAKE_HRESULT(1, FACILITY_HTTP, context->m_connectStatusCode); - } - else - { - result->errorCode = context->m_connectError ? E_FAIL : S_OK; - } + result->platformErrorCode = static_cast(context->m_connectError.value()); + result->errorCode = HResultFromConnectError(context->m_connectError, context->m_connectStatusCode); } else if (op == XAsyncOp::Cleanup) { @@ -620,6 +1174,11 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared if (SUCCEEDED(hr)) { m_state = CONNECTING; + { + std::lock_guard lock{ m_websocketThreadStateMutex }; + m_websocketThreadExited = false; + } + client.connect(con); try @@ -631,8 +1190,20 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared }; auto context = http_allocate_shared(client); - m_websocketThread = std::thread([context, id{ m_hcWebsocketHandle->websocket->id }]() + m_websocketThread = std::thread([context, sharedThis, id{ m_hcWebsocketHandle->websocket->id }]() { + struct thread_exit_guard + { + std::shared_ptr websocket; + + ~thread_exit_guard() + { + std::lock_guard lock{ websocket->m_websocketThreadStateMutex }; + websocket->m_websocketThreadExited = true; + websocket->m_websocketThreadStateCondition.notify_all(); + } + } threadExitGuard{ sharedThis }; + HC_TRACE_INFORMATION(WEBSOCKET, "id=%u Wspp client work thread starting", id); #if HC_PLATFORM == HC_PLATFORM_ANDROID @@ -672,7 +1243,7 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared // the dll is unloaded. If static linking, like we do, the state isn't cleaned up // at all and will be reported as leaks. // See http://www.openssl.org/support/faq.html#PROG13 -#if HC_PLATFORM == HC_PLATFORM_ANDROID || HC_PLATFORM_IS_APPLE || HC_PLATFORM == HC_PLATFORM_LINUX +#if !HC_PLATFORM_IS_MICROSOFT #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" ERR_remove_thread_state(nullptr); @@ -680,18 +1251,29 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared #if HC_PLATFORM == HC_PLATFORM_ANDROID javaVm->DetachCurrentThread(); #endif // HC_PLATFORM_ANDROID -#else - ERR_remove_thread_state(nullptr); -#endif // HC_PLATFORM_ANDROID || HC_PLATFORM_IS_APPLE || HC_PLATFORM_LINUX +#endif // !HC_PLATFORM_IS_MICROSOFT HC_TRACE_INFORMATION(WEBSOCKET, "id=%u Wspp client work thread end", id); }); hr = S_OK; } - catch (std::system_error err) + catch (std::system_error const& err) { - HC_TRACE_ERROR(WEBSOCKET, "Websocket: couldn't create background websocket thread (%d)", err.code().value()); - hr = E_FAIL; + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: couldn't create background websocket thread (%d)", TO_ULL(m_hcWebsocketHandle->websocket->id), err.code().value()); + complete_connect_start_failure(async, websocketpp::error::make_error_code(websocketpp::error::general)); + return S_OK; + } + catch (std::exception const& err) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: websocket thread startup failed: %s", TO_ULL(m_hcWebsocketHandle->websocket->id), err.what()); + complete_connect_start_failure(async, websocketpp::error::make_error_code(websocketpp::error::general)); + return S_OK; + } + catch (...) + { + HC_TRACE_ERROR(WEBSOCKET, "Websocket [ID %llu]: websocket thread startup failed with unknown exception", TO_ULL(m_hcWebsocketHandle->websocket->id)); + complete_connect_start_failure(async, websocketpp::error::make_error_code(websocketpp::error::general)); + return S_OK; } } @@ -722,14 +1304,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared { if (message.payloadBinary.size() > 0) { - if (m_client->is_tls_client()) - { - m_client->impl().send(m_con, message.payloadBinary.data(), message.payloadBinary.size(), websocketpp::frame::opcode::binary, message.error); - } - else + invoke_wspp_client([this, &message](auto& client) { - m_client->impl().send(m_con, message.payloadBinary.data(), message.payloadBinary.size(), websocketpp::frame::opcode::binary, message.error); - } + client.send(m_con, message.payloadBinary.data(), message.payloadBinary.size(), websocketpp::frame::opcode::binary, message.error); + }); } else { @@ -738,14 +1316,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared } else { - if (m_client->is_tls_client()) - { - m_client->impl().send(m_con, message.payload.data(), message.payload.length(), websocketpp::frame::opcode::text, message.error); - } - else + invoke_wspp_client([this, &message](auto& client) { - m_client->impl().send(m_con, message.payload.data(), message.payload.length(), websocketpp::frame::opcode::text, message.error); - } + client.send(m_con, message.payload.data(), message.payload.length(), websocketpp::frame::opcode::text, message.error); + }); } } @@ -868,14 +1442,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared if (sharedThis->m_state == CONNECTED) { - if (sharedThis->m_client->is_tls_client()) - { - sharedThis->m_client->impl().ping(sharedThis->m_con, std::string{}); - } - else + sharedThis->invoke_wspp_client([sharedThis](auto& client) { - sharedThis->m_client->impl().ping(sharedThis->m_con, std::string{}); - } + client.ping(sharedThis->m_con, std::string{}); + }); sharedThis->send_ping(); } @@ -891,41 +1461,219 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared ); } + void complete_wspp_shutdown(AsyncWork const& shutdownCompleteCallback) + { + { + std::lock_guard lock{ m_wsppClientLock }; + + // Delete client to make sure Websocketpp cleans up all Boost.Asio portions. + m_client.reset(); + m_state = DISCONNECTED; + } + + shutdownCompleteCallback(); + } + + struct wspp_shutdown_completion_context + { + wspp_shutdown_completion_context( + std::shared_ptr websocket, + AsyncWork shutdownCompleteCallback, + XTaskQueueHandle backgroundQueue) : + websocket{ std::move(websocket) }, + shutdownCompleteCallback{ std::move(shutdownCompleteCallback) }, + backgroundQueue{ backgroundQueue } + { + } + + ~wspp_shutdown_completion_context() + { + if (backgroundQueue) + { + XTaskQueueCloseHandle(backgroundQueue); + } + } + + std::shared_ptr websocket; + AsyncWork shutdownCompleteCallback; + XTaskQueueHandle backgroundQueue{ nullptr }; + }; + + HRESULT create_wspp_shutdown_completion_context( + std::shared_ptr const& sharedThis, + AsyncWork const& shutdownCompleteCallback, + std::shared_ptr& shutdownContext) + { + XTaskQueueHandle backgroundQueue{ nullptr }; + RETURN_IF_FAILED(XTaskQueueDuplicateHandle(sharedThis->m_backgroundQueue, &backgroundQueue)); + + shutdownContext = http_allocate_shared( + sharedThis, + shutdownCompleteCallback, + backgroundQueue); + if (!shutdownContext) + { + XTaskQueueCloseHandle(backgroundQueue); + return E_OUTOFMEMORY; + } + + return S_OK; + } + + void schedule_wspp_shutdown_completion( + std::shared_ptr const& shutdownContext) + { + auto hr = RunAsync( + [ + shutdownContext + ] + { + shutdownContext->websocket->complete_wspp_shutdown(shutdownContext->shutdownCompleteCallback); + }, + shutdownContext->backgroundQueue, + 0); + if (FAILED(hr)) + { + HC_TRACE_WARNING_HR(WEBSOCKET, hr, "Failed to queue websocketpp shutdown completion; running inline"); + shutdownContext->websocket->complete_wspp_shutdown(shutdownContext->shutdownCompleteCallback); + } + } + + void schedule_wspp_shutdown_completion_when_thread_exits( + std::shared_ptr const& shutdownContext) + { + auto hr = RunAsync( + [ + shutdownContext + ] + { + bool websocketThreadExited{ false }; + { + std::lock_guard lock{ shutdownContext->websocket->m_websocketThreadStateMutex }; + websocketThreadExited = shutdownContext->websocket->m_websocketThreadExited; + } + + if (!websocketThreadExited) + { + shutdownContext->websocket->schedule_wspp_shutdown_completion_when_thread_exits(shutdownContext); + return; + } + + if (shutdownContext->websocket->m_websocketThread.joinable()) + { + shutdownContext->websocket->m_websocketThread.join(); + } + + shutdownContext->websocket->complete_wspp_shutdown(shutdownContext->shutdownCompleteCallback); + }, + shutdownContext->backgroundQueue, + WSPP_SHUTDOWN_POLL_INTERVAL_MS); + if (FAILED(hr)) + { + HC_TRACE_WARNING_HR(WEBSOCKET, hr, "Failed to queue websocketpp shutdown retry; waiting synchronously for thread completion."); + if (shutdownContext->websocket->m_websocketThread.joinable()) + { + shutdownContext->websocket->m_websocketThread.join(); + } + + shutdownContext->websocket->complete_wspp_shutdown(shutdownContext->shutdownCompleteCallback); + } + } + template void shutdown_wspp_impl(std::function shutdownCompleteCallback) { auto &client = m_client->impl(); const auto &connection = client.get_con_from_hdl(m_con); - m_closeCode = connection->get_local_close_code(); + auto const localCloseCode = connection->get_local_close_code(); + auto const remoteCloseCode = connection->get_remote_close_code(); + m_closeCode = localCloseCode != websocketpp::close::status::blank ? localCloseCode : remoteCloseCode; client.stop_perpetual(); // Yield and wait for background thread to finish RunAsync( [ sharedThis{ shared_from_this() }, - shutdownCompleteCallback + shutdownCompleteCallback = AsyncWork{ std::move(shutdownCompleteCallback) } ] { + auto joinWebsocketThread = [&sharedThis]() + { + if (sharedThis->m_websocketThread.joinable()) + { + sharedThis->m_websocketThread.join(); + } + }; + // Wait for background thread to finish if (sharedThis->m_websocketThread.joinable()) { - auto future = std::async(std::launch::async, &std::thread::join, &sharedThis->m_websocketThread); - if (future.wait_for(std::chrono::milliseconds(WSPP_SHUTDOWN_TIMEOUT_MS)) == std::future_status::timeout) + auto waitForThreadExit = [&sharedThis](std::chrono::milliseconds timeout) + { + std::unique_lock lock{ sharedThis->m_websocketThreadStateMutex }; + return sharedThis->m_websocketThreadStateCondition.wait_for( + lock, + timeout, + [&sharedThis]() + { + return sharedThis->m_websocketThreadExited; + }); + }; + + if (!waitForThreadExit(std::chrono::milliseconds(WSPP_SHUTDOWN_TIMEOUT_MS))) { HC_TRACE_WARNING(WEBSOCKET, "Warning: WSPP client thread didn't complete execution within the expected timeout. Force stopping processing loop."); - sharedThis->m_client->impl().stop(); - } - } + { + std::lock_guard lock{ sharedThis->m_wsppClientLock }; + if (sharedThis->m_client != nullptr) + { + sharedThis->m_client->impl().stop(); + } + } - { - std::lock_guard lock{ sharedThis->m_wsppClientLock }; + if (!waitForThreadExit(std::chrono::milliseconds(WSPP_SHUTDOWN_TIMEOUT_MS))) + { + HC_TRACE_WARNING(WEBSOCKET, "Warning: WSPP client thread did not exit within the post-stop timeout. Completing shutdown asynchronously after the thread exits."); + + std::shared_ptr shutdownContext; + auto hr = sharedThis->create_wspp_shutdown_completion_context(sharedThis, shutdownCompleteCallback, shutdownContext); + if (FAILED(hr)) + { + HC_TRACE_WARNING_HR(WEBSOCKET, hr, "Failed to capture websocketpp shutdown context. Waiting synchronously for thread completion."); + joinWebsocketThread(); + sharedThis->complete_wspp_shutdown(shutdownCompleteCallback); + return; + } + + try + { + std::thread( + [ + shutdownContext + ] + { + if (shutdownContext->websocket->m_websocketThread.joinable()) + { + shutdownContext->websocket->m_websocketThread.join(); + } + + shutdownContext->websocket->schedule_wspp_shutdown_completion(shutdownContext); + }).detach(); + } + catch (std::system_error const& err) + { + HC_TRACE_WARNING(WEBSOCKET, "Warning: WSPP shutdown couldn't create a joiner thread (%d). Waiting asynchronously for thread completion.", err.code().value()); + sharedThis->schedule_wspp_shutdown_completion_when_thread_exits(shutdownContext); + } - // Delete client to make sure Websocketpp cleans up all Boost.Asio portions. - sharedThis->m_client.reset(); - sharedThis->m_state = DISCONNECTED; + return; + } + } + + joinWebsocketThread(); } - shutdownCompleteCallback(); + sharedThis->complete_wspp_shutdown(shutdownCompleteCallback); }, m_backgroundQueue, 0 @@ -937,7 +1685,19 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared { auto &client = m_client->impl(); const auto &connection = client.get_con_from_hdl(m_con); - m_connectError = connection->get_ec(); + auto const connectError = connection->get_ec(); + auto const transportError = connection->get_transport_ec(); + + if (transportError && + (connectError == websocketpp::transport::asio::error::make_error_code(websocketpp::transport::asio::error::general) || + connectError == websocketpp::transport::asio::error::make_error_code(websocketpp::transport::asio::error::pass_through) || + connectError == websocketpp::error::make_error_code(websocketpp::error::general))) + { + m_connectError = transportError; + return; + } + + m_connectError = connectError; } template @@ -948,60 +1708,70 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared m_connectStatusCode = connection->get_response_code(); } + template + inline void set_response_headers(websocketpp::connection_hdl hdl) + { + auto& client = m_client->impl(); + websocketpp::lib::error_code ec; + auto connection = client.get_con_from_hdl(hdl, ec); + if (ec || !connection) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: failed to get websocketpp connection for response headers", TO_ULL(m_hcWebsocketHandle->websocket->id)); + return; + } + + HttpHeaders responseHeaders; + for (auto const& header : connection->get_response().get_headers()) + { + http_internal_string name{ header.first.data(), header.first.size() }; + http_internal_string value{ header.second.data(), header.second.size() }; + responseHeaders[std::move(name)] = std::move(value); + } + + HRESULT hr = m_hcWebsocketHandle->websocket->SetResponseHeaders(std::move(responseHeaders)); + if (FAILED(hr)) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: failed to cache upgrade response headers 0x%0.8x", TO_ULL(m_hcWebsocketHandle->websocket->id), hr); + } + } + // Wrappers for the different types of websocketpp clients. // Perform type erasure to set the websocketpp client in use at runtime - // after construction based on the URI. + // at connect time based on the URI and compression options. struct websocketpp_client_base { websocketpp_client_base() noexcept = default; virtual ~websocketpp_client_base() noexcept = default; template - websocketpp::client & impl() + websocketpp::client& impl() { - if (is_tls_client()) - { - return reinterpret_cast &>(tls_client()); - } - else - { - return reinterpret_cast &>(non_tls_client()); - } + return *reinterpret_cast*>(client_storage()); } - virtual websocketpp::client & non_tls_client() - { - throw std::bad_cast(); - } - virtual websocketpp::client & tls_client() - { - throw std::bad_cast(); - } + virtual void* client_storage() noexcept = 0; virtual bool is_tls_client() const = 0; + virtual bool uses_compression() const = 0; }; - struct websocketpp_client : websocketpp_client_base + template + struct websocketpp_client_impl : websocketpp_client_base { - websocketpp::client & non_tls_client() override + void* client_storage() noexcept override { - return m_client; + return &m_client; } - bool is_tls_client() const override { return false; } - websocketpp::client m_client; - }; - struct websocketpp_tls_client : websocketpp_client_base - { - websocketpp::client & tls_client() override - { - return m_client; - } - bool is_tls_client() const override { return true; } - websocketpp::client m_client; + bool is_tls_client() const override { return IsTlsClient; } + bool uses_compression() const override { return UsesCompression; } + websocketpp::client m_client; }; // Asio client has a long running "run" task that we need to provide a thread for std::thread m_websocketThread; + std::mutex m_websocketThreadStateMutex; + std::condition_variable m_websocketThreadStateCondition; + bool m_websocketThreadExited{ true }; XTaskQueueHandle m_backgroundQueue = nullptr; websocketpp::connection_hdl m_con; @@ -1039,6 +1809,11 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared NAMESPACE_XBOX_HTTP_CLIENT_BEGIN +bool IsWebSocketppConnection(std::shared_ptr const& connection) noexcept +{ + return std::dynamic_pointer_cast(connection) != nullptr; +} + HRESULT WebSocketppProvider::ConnectAsync( String const& uri, String const& subprotocol, @@ -1046,12 +1821,17 @@ HRESULT WebSocketppProvider::ConnectAsync( XAsyncBlock* async ) noexcept { +#if HC_PLATFORM == HC_PLATFORM_GDK + RETURN_HR_IF(E_HC_NETWORK_NOT_INITIALIZED, m_isSuspended.load()); +#endif + auto wsppSocket{ std::dynamic_pointer_cast(websocketHandle->websocket->impl) }; if (!wsppSocket) { wsppSocket = http_allocate_shared(websocketHandle, uri.data(), subprotocol.data()); websocketHandle->websocket->impl = wsppSocket; + TrackConnection(wsppSocket); } return wsppSocket->connect(async); @@ -1106,6 +1886,86 @@ HRESULT WebSocketppProvider::Disconnect( return wsppSocket->close(closeStatus); } +HRESULT WebSocketppProvider::OptionsResult(HCWebSocketOptions options) const noexcept +{ +#if !defined(HC_ENABLE_WEBSOCKET_COMPRESSION) + return options == HCWebSocketOptions::None ? S_OK : E_NOT_SUPPORTED; +#else + if (HasUnsupportedWebSocketOptions(options) || RequestsLegacyWebSocketSemantics(options)) + { + return E_NOT_SUPPORTED; + } + + if (options != HCWebSocketOptions::None && !RequestsWebSocketCompression(options)) + { + return E_NOT_SUPPORTED; + } + + return S_OK; +#endif +} + +void WebSocketppProvider::OnSuspending() noexcept +{ +#if HC_PLATFORM == HC_PLATFORM_GDK + m_isSuspended.store(true); + + std::vector> activeConnections; + { + std::lock_guard lock{ m_connectionsMutex }; + auto it = m_connections.begin(); + while (it != m_connections.end()) + { + auto connection = it->lock(); + if (!connection) + { + it = m_connections.erase(it); + continue; + } + + auto wsppConnection = std::dynamic_pointer_cast(connection); + if (wsppConnection) + { + activeConnections.push_back(std::move(wsppConnection)); + } + + ++it; + } + } + + for (auto const& connection : activeConnections) + { + auto hr = connection->close(HCWebSocketCloseStatus::GoingAway); + if (FAILED(hr) && hr != E_UNEXPECTED) + { + HC_TRACE_WARNING_HR(WEBSOCKET, hr, "WebSocketppProvider suspend close failed"); + } + } +#endif +} + +void WebSocketppProvider::OnResuming() noexcept +{ +#if HC_PLATFORM == HC_PLATFORM_GDK + m_isSuspended.store(false); +#endif +} + +void WebSocketppProvider::TrackConnection(std::shared_ptr connection) noexcept +{ + std::lock_guard lock{ m_connectionsMutex }; + m_connections.erase( + std::remove_if( + m_connections.begin(), + m_connections.end(), + [](std::weak_ptr const& candidate) + { + return candidate.expired(); + }), + m_connections.end()); + m_connections.push_back(std::move(connection)); +} + NAMESPACE_XBOX_HTTP_CLIENT_END #endif // !HC_NOWEBSOCKETS diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.h b/Source/WebSocket/Websocketpp/websocketpp_websocket.h index 7c3a0702..6c71ea9f 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.h +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.h @@ -1,12 +1,28 @@ #pragma once +#include +#include +#include + #include "WebSocket/hcwebsocket.h" #include "Platform/IWebSocketProvider.h" +#include "utils.h" NAMESPACE_XBOX_HTTP_CLIENT_BEGIN #ifndef HC_NOWEBSOCKETS -class WebSocketppProvider : public IWebSocketProvider +bool IsWebSocketppConnection(std::shared_ptr const& connection) noexcept; +inline bool ShouldForceTlsValidationForGdkSandbox(HRESULT sandboxQueryHr, const char* sandbox) noexcept +{ + return FAILED(sandboxQueryHr) || (sandbox != nullptr && 0 == str_icmp(sandbox, "RETAIL")); +} + +inline bool ApplyTlsValidationBackstopForGdkConsole(bool allowProxyToDecryptHttps, bool isDevicePc) noexcept +{ + return isDevicePc ? allowProxyToDecryptHttps : false; +} + +class WebSocketppProvider : public IWebSocketProvider, public IProviderLifecycle { public: HRESULT ConnectAsync( @@ -33,6 +49,20 @@ class WebSocketppProvider : public IWebSocketProvider HCWebsocketHandle websocketHandle, HCWebSocketCloseStatus closeStatus ) noexcept override; + + HRESULT OptionsResult(HCWebSocketOptions options) const noexcept override; + + void OnSuspending() noexcept override; + void OnResuming() noexcept override; + +private: + void TrackConnection(std::shared_ptr connection) noexcept; + + std::mutex m_connectionsMutex; + std::vector> m_connections; +#if HC_PLATFORM == HC_PLATFORM_GDK + std::atomic m_isSuspended{ false }; +#endif }; #endif diff --git a/Source/WebSocket/Websocketpp/wintls_socket.hpp b/Source/WebSocket/Websocketpp/wintls_socket.hpp new file mode 100644 index 00000000..5ded7944 --- /dev/null +++ b/Source/WebSocket/Websocketpp/wintls_socket.hpp @@ -0,0 +1,289 @@ +/* + * Copyright (c) Microsoft Corporation + * Licensed under the MIT license. See LICENSE file in the project root for full license information. + */ + +#pragma once + +#ifndef WINTLS_USE_STANDALONE_ASIO +#define WINTLS_USE_STANDALONE_ASIO +#endif + +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include + +namespace websocketpp { +namespace transport { +namespace asio { +namespace wintls_socket { + +typedef lib::function&)> + socket_init_handler; + +typedef lib::function(connection_hdl)> + tls_init_handler; + +class connection : public lib::enable_shared_from_this { +public: + typedef connection type; + typedef lib::shared_ptr ptr; + typedef wintls::stream socket_type; + typedef lib::shared_ptr socket_ptr; + typedef lib::asio::io_service* io_service_ptr; + typedef lib::shared_ptr strand_ptr; + typedef lib::shared_ptr context_ptr; + + explicit connection() = default; + + ptr get_shared() + { + return shared_from_this(); + } + + bool is_secure() const + { + return true; + } + + socket_type::next_layer_type& get_raw_socket() + { + return m_socket->next_layer(); + } + + socket_type::next_layer_type& get_next_layer() + { + return m_socket->next_layer(); + } + + socket_type& get_socket() + { + return *m_socket; + } + + void set_socket_init_handler(socket_init_handler h) + { + m_socket_init_handler = h; + } + + void set_tls_init_handler(tls_init_handler h) + { + m_tls_init_handler = h; + } + + void set_certificate_revocation_check(bool check) + { + m_certificateRevocationCheck = check; + } + + std::string get_remote_endpoint(lib::error_code& ec) const + { + std::stringstream s; + + lib::asio::error_code aec; + lib::asio::ip::tcp::endpoint ep = m_socket->next_layer().remote_endpoint(aec); + + if (aec) + { + ec = socket::make_error_code(socket::error::pass_through); + s << "Error getting remote endpoint: " << aec << " (" << aec.message() << ")"; + return s.str(); + } + + ec = lib::error_code(); + s << ep; + return s.str(); + } + +protected: + lib::error_code init_asio(io_service_ptr service, strand_ptr strand, bool is_server) + { + if (!m_tls_init_handler) + { + return socket::make_error_code(socket::error::missing_tls_init_handler); + } + + m_context = m_tls_init_handler(m_hdl); + if (!m_context) + { + return socket::make_error_code(socket::error::invalid_tls_context); + } + + m_socket.reset(new socket_type(lib::asio::ip::tcp::socket(*service), *m_context)); + + if (m_socket_init_handler) + { + m_socket_init_handler(m_hdl, get_socket()); + } + + m_strand = strand; + m_is_server = is_server; + + return lib::error_code(); + } + + void set_uri(uri_ptr u) + { + m_uri = u; + } + + void pre_init(socket::init_handler callback) + { + if (!m_is_server && m_uri) + { + m_socket->set_server_hostname(m_uri->get_host()); + m_socket->set_certificate_revocation_check(m_certificateRevocationCheck); + } + + callback(lib::error_code()); + } + + void post_init(socket::init_handler callback) + { + m_ec = socket::make_error_code(socket::error::tls_handshake_timeout); + + if (m_strand) + { + m_socket->async_handshake( + get_handshake_type(), + m_strand->wrap(lib::bind(&type::handle_init, get_shared(), callback, lib::placeholders::_1)) + ); + } + else + { + m_socket->async_handshake( + get_handshake_type(), + lib::bind(&type::handle_init, get_shared(), callback, lib::placeholders::_1) + ); + } + } + + void set_handle(connection_hdl hdl) + { + m_hdl = hdl; + } + + void handle_init(socket::init_handler callback, wintls::error_code const& ec) + { + if (ec) + { + m_ec = translate_ec(ec); + } + else + { + m_ec = lib::error_code(); + } + + callback(m_ec); + } + + lib::error_code get_ec() const + { + return m_ec; + } + + lib::asio::error_code cancel_socket() + { + lib::asio::error_code ec; + get_raw_socket().cancel(ec); + return ec; + } + + void async_shutdown(socket::shutdown_handler callback) + { + if (m_strand) + { + m_socket->async_shutdown(m_strand->wrap(callback)); + } + else + { + m_socket->async_shutdown(callback); + } + } + +public: + template + static lib::error_code translate_ec(ErrorCodeType) + { + return make_error_code(transport::error::pass_through); + } + + static lib::error_code translate_ec(lib::error_code ec) + { + return ec; + } + +private: + wintls::handshake_type get_handshake_type() const + { + return m_is_server ? wintls::handshake_type::server : wintls::handshake_type::client; + } + + strand_ptr m_strand; + context_ptr m_context; + socket_ptr m_socket; + uri_ptr m_uri; + bool m_is_server{ false }; + lib::error_code m_ec; + connection_hdl m_hdl; + socket_init_handler m_socket_init_handler; + tls_init_handler m_tls_init_handler; + bool m_certificateRevocationCheck{ true }; +}; + +class endpoint { +public: + typedef endpoint type; + typedef connection socket_con_type; + typedef socket_con_type::ptr socket_con_ptr; + + explicit endpoint() = default; + + bool is_secure() const + { + return true; + } + + void set_socket_init_handler(socket_init_handler h) + { + m_socket_init_handler = h; + } + + void set_tls_init_handler(tls_init_handler h) + { + m_tls_init_handler = h; + } + + void set_certificate_revocation_check(bool check) + { + m_certificateRevocationCheck = check; + } + +protected: + lib::error_code init(socket_con_ptr scon) + { + scon->set_socket_init_handler(m_socket_init_handler); + scon->set_tls_init_handler(m_tls_init_handler); + scon->set_certificate_revocation_check(m_certificateRevocationCheck); + return lib::error_code(); + } + +private: + socket_init_handler m_socket_init_handler; + tls_init_handler m_tls_init_handler; + bool m_certificateRevocationCheck{ true }; +}; + +} // namespace wintls_socket +} // namespace asio +} // namespace transport +} // namespace websocketpp diff --git a/Source/WebSocket/WinRT/winrt_websocket.cpp b/Source/WebSocket/WinRT/winrt_websocket.cpp index 02cad726..0970a68d 100644 --- a/Source/WebSocket/WinRT/winrt_websocket.cpp +++ b/Source/WebSocket/WinRT/winrt_websocket.cpp @@ -176,6 +176,42 @@ http_internal_vector parse_subprotocols(const http_intern return values; } +void TrySetWinRTResponseHeaders( + _In_ std::shared_ptr const& websocket, + _In_ MessageWebSocket^ messageWebSocket +) +{ + if (!websocket || messageWebSocket == nullptr) + { + return; + } + + try + { + HttpHeaders responseHeaders; + auto information = messageWebSocket->Information; + if (information != nullptr && information->Protocol != nullptr && information->Protocol->Length() > 0) + { + responseHeaders[http_internal_string{ "Sec-WebSocket-Protocol" }] = + utf8_from_utf16(http_internal_wstring{ information->Protocol->Data() }); + } + + auto hr = websocket->SetResponseHeaders(std::move(responseHeaders)); + if (FAILED(hr)) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: failed to cache WinRT upgrade response headers 0x%0.8x", TO_ULL(websocket->id), hr); + } + } + catch (Platform::Exception^ e) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: failed to read WinRT upgrade response info 0x%0.8x", TO_ULL(websocket->id), e->HResult); + } + catch (...) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: failed to read WinRT upgrade response info", TO_ULL(websocket->id)); + } +} + HRESULT WebsocketConnectDoWork( _Inout_ XAsyncBlock* asyncBlock, _In_opt_ void* executionRoutineContext @@ -196,6 +232,7 @@ try HCWebSocketGetNumHeaders(websocketHandle, &numHeaders); http_internal_string protocolHeader("Sec-WebSocket-Protocol"); + http_internal_string requestedProtocolHeaderValue; for (uint32_t i = 0; i < numHeaders; i++) { const char* headerName; @@ -203,8 +240,16 @@ try HCWebSocketGetHeaderAtIndex(websocketHandle, i, &headerName, &headerValue); // The MessageWebSocket API throws a COMException if you try to set the - // 'Sec-WebSocket-Protocol' header here. It requires you to go through their API instead. + // 'Sec-WebSocket-Protocol' header through SetRequestHeader. Capture the + // requested value here and route it through SupportedProtocols below instead. if (headerName != nullptr && headerValue != nullptr && !str_icmp(headerName, protocolHeader.c_str())) + { + requestedProtocolHeaderValue = headerValue; + HC_TRACE_INFORMATION(WEBSOCKET, "Websocket [ID %llu]: Deferring [%s: %s] to SupportedProtocols", TO_ULL(websocket->id), headerName, headerValue); + continue; + } + + if (headerName != nullptr && headerValue != nullptr) { http_internal_wstring wHeaderName = utf16_from_utf8(headerName); http_internal_wstring wHeaderValue = utf16_from_utf8(headerValue); @@ -215,7 +260,17 @@ try } } - auto protocols = parse_subprotocols(websocket->SubProtocol()); + auto requestedProtocols = websocket->SubProtocol(); + if (requestedProtocols.empty()) + { + requestedProtocols = requestedProtocolHeaderValue; + } + else if (!requestedProtocolHeaderValue.empty() && str_icmp(requestedProtocols.c_str(), requestedProtocolHeaderValue.c_str()) != 0) + { + HC_TRACE_WARNING(WEBSOCKET, "Websocket [ID %llu]: Ignoring duplicate Sec-WebSocket-Protocol header because the connect subprotocol parameter is already set", TO_ULL(websocket->id)); + } + + auto protocols = parse_subprotocols(requestedProtocols); for (const auto& value : protocols) { websocketTask->m_messageWebSocket->Control->SupportedProtocols->Append(Platform::StringReference(value.c_str())); @@ -251,6 +306,7 @@ try else { websocketTask->m_connectAsyncOpResult = S_OK; + TrySetWinRTResponseHeaders(websocket, websocketTask->m_messageWebSocket); } } catch (Platform::Exception^ e) @@ -673,4 +729,4 @@ HRESULT WinRTWebSocketProvider::Disconnect( NAMESPACE_XBOX_HTTP_CLIENT_END -#endif // !HC_NOWEBSOCKETS \ No newline at end of file +#endif // !HC_NOWEBSOCKETS diff --git a/Source/WebSocket/iOS/ios_websocket.cpp b/Source/WebSocket/iOS/ios_websocket.cpp deleted file mode 100644 index a899c534..00000000 --- a/Source/WebSocket/iOS/ios_websocket.cpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) Microsoft Corporation -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - #include "pch.h" - #include "../hcwebsocket.h" - using namespace xbox::httpclient; - HRESULT Internal_HCWebSocketConnectAsync( - _In_z_ const char* uri, - _In_z_ const char* subProtocol, - _In_ HCWebsocketHandle websocket, - _Inout_ AsyncBlock* async - ) -{ - // TODO - return S_OK; -} - HRESULT Internal_HCWebSocketSendMessageAsync( - _In_ HCWebsocketHandle websocket, - _In_z_ const char* message, - _Inout_ AsyncBlock* async - ) -{ - // TODO - return S_OK; -} - HRESULT Internal_HCWebSocketDisconnect( - _In_ HCWebsocketHandle websocket, - _In_ HCWebSocketCloseStatus closeStatus -) -{ - // TODO - return S_OK; -} \ No newline at end of file From 5698ddb3e16824ce18b5d162880760d257c1de0e Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 19:10:55 -0700 Subject: [PATCH 03/24] Replace compression flags with explicit WebSocket options Rename the compression-only control surface to HCWebSocketSetOptions and HCWebSocketOptions, add the LegacySemantics escape hatch, and make deterministic behavior explicit on the handle. Update the Android-facing surface, public headers, exports, and documentation so callers can reason about fragment-handler support, max receive buffer behavior, and compression negotiation through one options contract. --- .../xbox/httpclient/HttpClientWebSocket.java | 37 ++- Build/libHttpClient.GDK/libHttpClient.GDK.def | 7 +- Build/libHttpClient.Linux/README.md | 21 +- .../libHttpClient.Win32.def | 7 +- Include/httpClient/httpClient.h | 154 ++++++++--- Include/httpClient/httpProvider.h | 43 +++ Include/httpClient/pal.h | 1 + README.md | 60 ++++- SECURITY_AUDIT_BOOST-WINTLS.md | 255 ++++++++++++++++++ Source/Platform/IWebSocketProvider.h | 23 ++ .../Android/AndroidWebSocketProvider.cpp | 99 ++++++- Source/WebSocket/hcwebsocket.cpp | 177 +++++++++++- Source/WebSocket/hcwebsocket.h | 29 +- Source/WebSocket/websocket_options.h | 57 ++++ Source/WebSocket/websocket_publics.cpp | 66 ++++- Utilities/FrameworkResources/exports.exp | 5 + 16 files changed, 971 insertions(+), 70 deletions(-) create mode 100644 SECURITY_AUDIT_BOOST-WINTLS.md create mode 100644 Source/WebSocket/websocket_options.h diff --git a/Build/libHttpClient.Android/src/main/java/com/xbox/httpclient/HttpClientWebSocket.java b/Build/libHttpClient.Android/src/main/java/com/xbox/httpclient/HttpClientWebSocket.java index fe047234..e0ddaa32 100644 --- a/Build/libHttpClient.Android/src/main/java/com/xbox/httpclient/HttpClientWebSocket.java +++ b/Build/libHttpClient.Android/src/main/java/com/xbox/httpclient/HttpClientWebSocket.java @@ -60,12 +60,17 @@ public void disconnect(int closeStatus) { @Override public void onOpen(WebSocket webSocket, Response response) { - onOpen(); + Headers responseHeaders = response != null ? response.headers() : null; + onOpen(getHeaderNames(responseHeaders), getHeaderValues(responseHeaders)); } @Override public void onFailure(WebSocket webSocket, Throwable t, Response response) { - onFailure(response != null ? response.code() : -1); + Headers responseHeaders = response != null ? response.headers() : null; + onFailure( + response != null ? response.code() : -1, + getHeaderNames(responseHeaders), + getHeaderValues(responseHeaders)); } @Override @@ -93,8 +98,8 @@ public void onMessage(WebSocket webSocket, okio.ByteString bytes) { onBinaryMessage(buffer); } - public native void onOpen(); - public native void onFailure(int statusCode); + public native void onOpen(String[] headerNames, String[] headerValues); + public native void onFailure(int statusCode, String[] headerNames, String[] headerValues); public native void onClose(int code); public native void onMessage(String text); public native void onBinaryMessage(ByteBuffer data); @@ -109,4 +114,28 @@ protected void finalize() private long pingInterval; private WebSocket socket; + + private static String[] getHeaderNames(Headers headers) { + if (headers == null) { + return null; + } + + String[] names = new String[headers.size()]; + for (int i = 0; i < headers.size(); ++i) { + names[i] = headers.name(i); + } + return names; + } + + private static String[] getHeaderValues(Headers headers) { + if (headers == null) { + return null; + } + + String[] values = new String[headers.size()]; + for (int i = 0; i < headers.size(); ++i) { + values[i] = headers.value(i); + } + return values; + } } diff --git a/Build/libHttpClient.GDK/libHttpClient.GDK.def b/Build/libHttpClient.GDK/libHttpClient.GDK.def index eafd5b75..086ff7c1 100644 --- a/Build/libHttpClient.GDK/libHttpClient.GDK.def +++ b/Build/libHttpClient.GDK/libHttpClient.GDK.def @@ -102,6 +102,7 @@ EXPORTS HCWebSocketConnectAsync HCWebSocketCreate HCWebSocketDisconnect + HCWebSocketDisconnectWithStatus HCWebSocketDuplicateHandle HCWebSocketGetBinaryMessageFragmentEventFunction HCWebSocketGetEventFunctions @@ -109,9 +110,13 @@ EXPORTS HCWebSocketGetHeaderAtIndex HCWebSocketGetNumHeaders HCWebSocketGetProxyUri + HCWebSocketGetResponseHeader + HCWebSocketGetResponseHeaderAtIndex + HCWebSocketGetNumResponseHeaders HCWebSocketSendBinaryMessageAsync HCWebSocketSendMessageAsync HCWebSocketSetBinaryMessageFragmentEventFunction + HCWebSocketSetOptions HCWebSocketSetHeader HCWebSocketSetMaxReceiveBufferSize HCWebSocketSetProxyUri @@ -123,4 +128,4 @@ EXPORTS HCWebSocketSetPingInterval HCHttpCallRequestGetMaxReceiveBufferSize HCHttpCallRequestSetMaxReceiveBufferSize - \ No newline at end of file + diff --git a/Build/libHttpClient.Linux/README.md b/Build/libHttpClient.Linux/README.md index 87a92d64..2f8f83c2 100644 --- a/Build/libHttpClient.Linux/README.md +++ b/Build/libHttpClient.Linux/README.md @@ -40,6 +40,25 @@ Running the build script with the `-nc|--nocurl` will **not** generate a binary Running the build script with the `-ns|--nossl` will **not** generate a binary of `libssl.a` and `libcrypto.a`. Use this flag if you wish to bring your own version of OpenSSL. +``` +./libHttpClient_Linux.bash [<-wc|--websocket-compression>] [<-nwc|--no-websocket-compression>] +``` + +Linux builds enable `HC_ENABLE_WEBSOCKET_COMPRESSION` by default. This compiles in the +compression-capable `websocketpp` provider path and enables the local Linux WebSocket compression +integration test target. Compression is still only negotiated at runtime when the caller explicitly +requests it via `HCWebSocketSetOptions()`. + +Use `-nwc|--no-websocket-compression` to compile the Linux build without websocket compression +support. `-wc|--websocket-compression` remains available for explicit enablement. + +After configuring/building with `HC_ENABLE_WEBSOCKET_COMPRESSION=ON`, you can run it from the CMake +build directory with: + +``` +ctest --output-on-failure -R websocket-compression-linux +``` + If the bash script fails to run and produces the error: ``` /bin/bash^M: bad interpreter @@ -107,4 +126,4 @@ If the bash script fails to run and produces the error: running the following command and re-running the script should fix it. ``` sed -i -e 's/\r$//' openssl_Linux.bash -``` \ No newline at end of file +``` diff --git a/Build/libHttpClient.Win32/libHttpClient.Win32.def b/Build/libHttpClient.Win32/libHttpClient.Win32.def index 03d8ffcb..46f16bc5 100644 --- a/Build/libHttpClient.Win32/libHttpClient.Win32.def +++ b/Build/libHttpClient.Win32/libHttpClient.Win32.def @@ -101,6 +101,7 @@ EXPORTS HCWebSocketConnectAsync HCWebSocketCreate HCWebSocketDisconnect + HCWebSocketDisconnectWithStatus HCWebSocketDuplicateHandle HCWebSocketGetBinaryMessageFragmentEventFunction HCWebSocketGetEventFunctions @@ -108,9 +109,13 @@ EXPORTS HCWebSocketGetHeaderAtIndex HCWebSocketGetNumHeaders HCWebSocketGetProxyUri + HCWebSocketGetResponseHeader + HCWebSocketGetResponseHeaderAtIndex + HCWebSocketGetNumResponseHeaders HCWebSocketSendBinaryMessageAsync HCWebSocketSendMessageAsync HCWebSocketSetBinaryMessageFragmentEventFunction + HCWebSocketSetOptions HCWebSocketSetHeader HCWebSocketSetMaxReceiveBufferSize HCWebSocketSetProxyDecryptsHttps @@ -143,4 +148,4 @@ EXPORTS HCWebSocketGetPingInterval HCWebSocketSetPingInterval HCHttpCallRequestGetMaxReceiveBufferSize - HCHttpCallRequestSetMaxReceiveBufferSize \ No newline at end of file + HCHttpCallRequestSetMaxReceiveBufferSize diff --git a/Include/httpClient/httpClient.h b/Include/httpClient/httpClient.h index b5361d70..744caaf7 100644 --- a/Include/httpClient/httpClient.h +++ b/Include/httpClient/httpClient.h @@ -929,16 +929,18 @@ typedef void ); /// -/// A callback invoked every time a WebSocket receives an incoming binary message that is larger than -/// the WebSocket receive buffer (default 20KB, configurable using HCWebSocketSetMaxReceiveBufferSize). -/// Large messages are automatically fragmented and passed to clients in chunks. -/// -/// IMPORTANT: You must set this callback using HCWebSocketSetBinaryMessageFragmentEventFunction() to properly -/// handle binary messages larger than the receive buffer. Without this callback, large messages will be -/// delivered as separate messages via HCWebSocketBinaryMessageFunction with no way to determine -/// they are fragments of a single message. -/// -/// Typical usage: Accumulate fragments in a buffer until isLastFragment is true, then process the complete message. +/// A callback invoked on Win32 and GDK when the built-in WebSocket transport surfaces an oversized +/// incoming payload as raw fragments to honor the configured receive buffer size. +/// +/// The callback receives raw bytes. On current Win32 / GDK behavior, oversized UTF-8 payloads use +/// this same fragment path. +/// +/// IMPORTANT: If you expect incoming payloads larger than the receive buffer, set this callback or +/// raise the receive buffer with HCWebSocketSetMaxReceiveBufferSize(). Without this callback, +/// oversized payloads are not surfaced through the public whole-message callbacks. +/// +/// Typical usage: Accumulate fragments in a buffer until isLastFragment is true, then process the +/// complete payload. /// /// Handle to the WebSocket that this message was sent to /// Binary message fragment payload. @@ -967,6 +969,33 @@ typedef void _In_ void* functionContext ); +/// +/// Options controlling pre-connect WebSocket behavior. +/// Reserved bits must be zero. +/// +enum class HCWebSocketOptions : uint32_t +{ + None = 0x00000000, + /// + /// Explicitly preserve the platform's existing legacy WebSocket semantics. + /// This flag is mutually exclusive with every other option. + /// + LegacySemantics = 0x80000000, + /// + /// Request permessage-deflate. Without no-context-takeover flags, compression + /// context is reused in both directions. + /// + RequestCompression = 0x00000001, + /// + /// Request that server-to-client compressed messages disable context reuse. + /// + CompressionServerNoContextTakeover = 0x00000002, + /// + /// Request that client-to-server compressed messages disable context reuse. + /// + CompressionClientNoContextTakeover = 0x00000004 +}; + /// /// Creates an WebSocket handle. /// @@ -982,7 +1011,7 @@ typedef void /// Call HCWebSocketSetProxyUri(), HCWebSocketSetHeader(), or HCWebSocketSetPingInterval() to prepare the HCWebsocketHandle
/// Call HCWebSocketConnectAsync() to connect the WebSocket using the HCWebsocketHandle.
/// Call HCWebSocketSendMessageAsync() to send a message to the WebSocket using the HCWebsocketHandle.
-/// Call HCWebSocketDisconnect() to disconnect the WebSocket using the HCWebsocketHandle.
+/// Call HCWebSocketDisconnect() or HCWebSocketDisconnectWithStatus() to disconnect the WebSocket using the HCWebsocketHandle.
/// Call HCWebSocketCloseHandle() when done with the HCWebsocketHandle to free the associated memory
/// STDAPI HCWebSocketCreate( @@ -999,17 +1028,20 @@ STDAPI HCWebSocketCreate( /// /// The handle of the websocket. /// A pointer to the binary message fragment handling callback to use, or a null pointer to remove. -/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_NOT_SUPPORTED, or E_FAIL. /// -/// IMPORTANT: Binary messages larger than the WebSocket receive buffer (default 20KB) are automatically fragmented. -/// Without this handler, large messages are broken into chunks and passed to HCWebSocketBinaryMessageFunction with NO indication -/// they are fragments, making message reconstruction impossible. -/// -/// For applications expecting large binary messages, you MUST either: +/// On Win32 and GDK, the built-in transport uses this callback when an incoming payload exceeds the +/// configured receive buffer (default 20KB). Current oversized UTF-8 overflow behavior also uses +/// this raw-byte fragment path. +/// +/// For applications expecting oversized incoming payloads, you MUST either: /// 1. Set this fragment handler to properly reconstruct messages, OR /// 2. Increase the receive buffer size with HCWebSocketSetMaxReceiveBufferSize() to accommodate your largest expected message -/// -/// The fragment handler receives each chunk with an isLastFragment flag to indicate message completion. +/// +/// Without this handler, oversized incoming payloads are not delivered through HCWebSocketMessageFunction +/// or HCWebSocketBinaryMessageFunction. +/// +/// Fragment callbacks are not supported after selecting deterministic behavior with HCWebSocketSetOptions(). /// STDAPI HCWebSocketSetBinaryMessageFragmentEventFunction( _In_ HCWebsocketHandle websocket, @@ -1029,14 +1061,18 @@ STDAPI HCWebSocketSetProxyUri( _In_z_ const char* proxyUri ) noexcept; -#if HC_PLATFORM == HC_PLATFORM_WIN32 && !HC_WINHTTP_WEBSOCKETS +#if HC_PLATFORM_IS_MICROSOFT && (HC_PLATFORM != HC_PLATFORM_UWP) && (HC_PLATFORM != HC_PLATFORM_XDK) /// -/// Allows proxy server to decrypt and inspect traffic; should be used only for debugging purposes +/// Disables TLS server certificate validation for this WebSocket connection. +/// This is a debugging-only setting for use with HTTPS-intercepting proxies (Fiddler, Charles, etc.) +/// and should never be enabled in production. /// This must be called after calling HCWebSocketSetProxyUri. -/// Only applies to Win32 non-GDK builds +/// This must be called prior to calling HCWebSocketConnectAsync. +/// Available on Win32 and GDK. On GDK console, TLS validation is always enforced +/// regardless of this setting, matching the built-in WinHTTP WebSocket path. /// /// The handle of the WebSocket -/// true is proxy can decrypt, false is not allowed to decrypt +/// true to disable TLS server certificate validation, false to keep validation enabled /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_OUTOFMEMORY, or E_FAIL. STDAPI HCWebSocketSetProxyDecryptsHttps( _In_ HCWebsocketHandle websocket, @@ -1069,6 +1105,32 @@ STDAPI HCWebSocketSetPingInterval( _In_ uint32_t pingIntervalSeconds ) noexcept; +/// +/// Set pre-connect WebSocket behavior options. +/// +/// The handle of the WebSocket. +/// Options to apply. Reserved bits must be zero. +/// LegacySemantics is mutually exclusive with every other flag. CompressionServerNoContextTakeover +/// and CompressionClientNoContextTakeover require RequestCompression. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_HC_CONNECT_ALREADY_CALLED, or E_NOT_SUPPORTED. +/// This must be called prior to calling HCWebSocketConnectAsync. +/// +/// If this API is never called, the socket uses the platform's legacy behavior. +/// Calling HCWebSocketSetOptions(HCWebSocketOptions::LegacySemantics) explicitly preserves that same legacy behavior. +/// Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior. +/// +/// In deterministic behavior, fragment callbacks are not supported. On websocketpp-backed paths, the inbound +/// message-size limit becomes a hard cap: the value passed to HCWebSocketSetMaxReceiveBufferSize() if set before connect, +/// otherwise the provider default of 32,000,000 bytes. +/// +/// RequestCompression alone reuses compression context in both directions by default. +#if HC_PLATFORM != HC_PLATFORM_ANDROID +STDAPI HCWebSocketSetOptions( + _In_ HCWebsocketHandle websocket, + _In_ HCWebSocketOptions options + ) noexcept; +#endif + /// /// Gets the WebSocket functions to allow callers to respond to incoming messages and WebSocket close events. /// @@ -1109,7 +1171,7 @@ typedef struct WebSocketCompletionResult /// The handle of the HTTP call. HCWebsocketHandle websocket; - /// The error code of the call. Possible values are S_OK, or E_FAIL. + /// The result of the call. S_OK indicates success; failures return a descriptive HRESULT. HRESULT errorCode; /// The platform specific network error code of the call to be used for tracing / debugging. @@ -1161,6 +1223,9 @@ STDAPI HCGetWebSocketConnectResult( /// /// To get the result, first call HCGetWebSocketSendMessageResult /// inside the AsyncBlock callback or after the AsyncBlock is complete. +/// +/// When built-in WebSocket compression is enabled, messages larger than UINT32_MAX bytes +/// are rejected with E_INVALIDARG. /// STDAPI HCWebSocketSendMessageAsync( _In_ HCWebsocketHandle websocket, @@ -1173,7 +1238,7 @@ STDAPI HCWebSocketSendMessageAsync( /// /// Handle to the WebSocket. /// Binary data to send in byte buffer. -/// Size of byte buffer. +/// Size of byte buffer. Maximum supported value is UINT32_MAX bytes. /// The AsyncBlock that defines the async operation. /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. /// @@ -1199,7 +1264,7 @@ STDAPI HCGetWebSocketSendMessageResult( ) noexcept; /// -/// Disconnects / closes the WebSocket. +/// Disconnects / closes the WebSocket with the Normal (1000) close status. /// /// Handle to the WebSocket. /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. @@ -1207,23 +1272,32 @@ STDAPI HCWebSocketDisconnect( _In_ HCWebsocketHandle websocket ) noexcept; -#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK /// -/// Configures how large the WebSocket receive buffer is allowed to grow before messages are fragmented. -/// Binary messages exceeding this buffer size are automatically broken into fragments and delivered via -/// HCWebSocketBinaryMessageFragmentFunction (if set) or as separate messages via HCWebSocketBinaryMessageFunction. -/// -/// The default value is 20KB (20,480 bytes). -/// -/// IMPORTANT: For applications expecting large binary messages, you should either: -/// 1. Set this buffer size large enough for your largest expected message, OR -/// 2. Use HCWebSocketSetBinaryMessageFragmentEventFunction() to properly handle message fragments -/// -/// Text messages exceeding the buffer size are handled differently and may be passed via multiple calls to HCWebSocketMessageFunction. +/// Disconnects / closes the WebSocket using an explicit close status such as GoingAway (1001). /// -/// The handle of the WebSocket -/// Maximum size (in bytes) for the WebSocket receive buffer. +/// Handle to the WebSocket. +/// The close status to send to the peer. /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. +STDAPI HCWebSocketDisconnectWithStatus( + _In_ HCWebsocketHandle websocket, + _In_ HCWebSocketCloseStatus closeStatus + ) noexcept; + +#if HC_PLATFORM != HC_PLATFORM_ANDROID +/// +/// Configures the pre-connect inbound message-size limit behavior for built-in WebSocket transports. +/// +/// In deterministic websocketpp-backed behavior, this becomes the hard inbound message-size cap. +/// If you do not call this API before connect, that deterministic cap defaults to 32,000,000 bytes. +/// +/// In legacy Win32 / GDK behavior, the configured receive buffer still controls when oversized incoming +/// payloads are routed through HCWebSocketSetBinaryMessageFragmentEventFunction(). The legacy default value +/// remains 20KB (20,480 bytes). +/// +/// The handle of the WebSocket +/// Maximum size (in bytes) for the WebSocket receive buffer. Values larger than UINT32_MAX are rejected with E_INVALIDARG. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_HC_CONNECT_ALREADY_CALLED. +/// This must be called prior to calling HCWebSocketConnectAsync. STDAPI HCWebSocketSetMaxReceiveBufferSize( _In_ HCWebsocketHandle websocket, _In_ size_t bufferSizeInBytes diff --git a/Include/httpClient/httpProvider.h b/Include/httpClient/httpProvider.h index 2a793f40..449ffa1a 100644 --- a/Include/httpClient/httpProvider.h +++ b/Include/httpClient/httpProvider.h @@ -646,6 +646,49 @@ HCWebSocketGetHeaderAtIndex( _Out_ const char** headerValue ) noexcept; +/// +/// Get a response header from the WebSocket upgrade response. +/// +/// The handle of the WebSocket. +/// UTF-8 encoded header name from the upgrade response. +/// UTF-8 encoded header value from the upgrade response. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_OUTOFMEMORY, or E_FAIL. +STDAPI +HCWebSocketGetResponseHeader( + _In_ HCWebsocketHandle websocket, + _In_z_ const char* headerName, + _Out_ const char** headerValue + ) noexcept; + +/// +/// Gets the number of headers in the WebSocket upgrade response. +/// +/// The handle of the WebSocket. +/// The number of upgrade response headers on the WebSocket. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. +STDAPI +HCWebSocketGetNumResponseHeaders( + _In_ HCWebsocketHandle websocket, + _Out_ uint32_t* numHeaders + ) noexcept; + +/// +/// Gets the upgrade response header at a specific zero based index in the WebSocket. +/// +/// The handle of the WebSocket. +/// Specific zero based index of the header. +/// UTF-8 encoded header name from the upgrade response. +/// UTF-8 encoded header value from the upgrade response. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_FAIL. +/// Use HCWebSocketGetNumResponseHeaders() to know how many upgrade response headers there are in the WebSocket. +STDAPI +HCWebSocketGetResponseHeaderAtIndex( + _In_ HCWebsocketHandle websocket, + _In_ uint32_t headerIndex, + _Out_ const char** headerName, + _Out_ const char** headerValue +) noexcept; + /// /// Gets the ping interval for this WebSocket. /// diff --git a/Include/httpClient/pal.h b/Include/httpClient/pal.h index 7919636b..7875e8e2 100644 --- a/Include/httpClient/pal.h +++ b/Include/httpClient/pal.h @@ -140,6 +140,7 @@ typedef void* HANDLE; #define __HRESULT_FROM_WIN32(x) ((HRESULT)(x) <= 0 ? ((HRESULT)(x)) : ((HRESULT) (((x) & 0x0000FFFF) | (FACILITY_WIN32 << 16) | 0x80000000))) #define S_OK ((HRESULT)0L) +#define S_FALSE ((HRESULT)1L) #define E_NOTIMPL _HRESULTYPEDEF_(0x80004001L) #define E_OUTOFMEMORY _HRESULTYPEDEF_(0x8007000EL) #define E_INVALIDARG _HRESULTYPEDEF_(0x80070057L) diff --git a/README.md b/README.md index fa01a05d..71bec100 100644 --- a/README.md +++ b/README.md @@ -49,27 +49,68 @@ libHttpClient provides a platform abstraction layer for HTTP and WebSocket, and 1. Follow steps 1-3 from HTTP API setup above 1. Call HCWebSocketCreate() to create a new HCWebsocketHandle with message/binary message/close event callbacks -1. **For large binary messages (>20KB)**: Call HCWebSocketSetBinaryMessageFragmentEventFunction() to handle message fragments -1. Optionally call HCWebSocketSetMaxReceiveBufferSize() to adjust the default 20KB receive buffer +1. Optionally call HCWebSocketSetOptions() before connect to explicitly preserve legacy behavior or select deterministic behavior and compression requests +1. **On Win32 and GDK legacy behavior, for payloads that may exceed the receive buffer (>20KB by default)**: Call HCWebSocketSetBinaryMessageFragmentEventFunction() to handle oversized incoming payloads +1. Optionally call HCWebSocketSetMaxReceiveBufferSize() and HCWebSocketSetPingInterval() to adjust receive buffering and keepalive behavior 1. Call HCWebSocketConnectAsync() to connect to the WebSocket server 1. Call HCWebSocketSendMessageAsync() or HCWebSocketSendBinaryMessageAsync() to send messages 1. Handle incoming messages via your registered callbacks -1. Call HCWebSocketDisconnect() when done +1. Call HCWebSocketDisconnect() or HCWebSocketDisconnectWithStatus() when done 1. Call HCWebSocketCloseHandle() to cleanup 1. Call HCCleanup() at shutdown ### Important WebSocket Notes -- **Default buffer size**: WebSocket receive buffer defaults to 20KB (20,480 bytes) -- **Message fragmentation**: Binary messages larger than the buffer size are automatically fragmented -- **Fragment handling**: Without setting HCWebSocketSetBinaryMessageFragmentEventFunction(), large messages will be broken into chunks passed to your binary message handler with no indication they are fragments -- **Best practice**: Either set a fragment handler OR increase buffer size with HCWebSocketSetMaxReceiveBufferSize() for your expected message sizes +- **No call to HCWebSocketSetOptions()**: The socket uses the platform's legacy behavior. +- **Legacy Win32 / GDK default buffer**: The legacy receive buffer defaults to 20KB (20,480 bytes). +- **Legacy Win32 / GDK oversized payload path**: When an incoming payload exceeds the configured receive buffer, the built-in transport surfaces it through HCWebSocketSetBinaryMessageFragmentEventFunction() as raw bytes. +- **Legacy Win32 / GDK text overflow behavior**: Oversized UTF-8 payloads use that same raw-byte fragment callback path. +- **Legacy Win32 / GDK without a fragment handler**: Oversized incoming payloads are not surfaced through the public whole-message callbacks unless a fragment handler is installed. +- **Deterministic websocketpp-backed behavior**: Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior on supported websocketpp-backed paths. Fragment callbacks are not supported there. +- **Deterministic inbound limit**: HCWebSocketSetMaxReceiveBufferSize() becomes a hard inbound message-size cap for deterministic websocketpp-backed behavior. If not set before connect, that path uses the provider default `32,000,000` byte limit. + +### WebSocket backend selection + +libHttpClient selects a built-in WebSocket backend based on platform: + +- **Win32**: WinHTTP +- **GDK**: WinHTTP +- **Linux / macOS / iOS**: `websocketpp` +- **Android**: OkHttp +- **UWP / WinRT**: `MessageWebSocket` + +To replace the built-in backend entirely, call `HCSetWebSocketFunctions()`. + +#### Compression + +When compression is requested, the compression-capable backend depends on platform: + +- **Win32**: `websocketpp` +- **GDK PC**: `websocketpp` +- **GDK Console**: disabled by default; `websocketpp` with optional build flag +- **Linux / macOS / iOS**: `websocketpp` +- **Android**: managed by OkHttp; libHttpClient does not expose `HCWebSocketSetOptions()` +- **UWP / WinRT**: not supported + +Compression support can be compiled out entirely by omitting the `HC_ENABLE_WEBSOCKET_COMPRESSION` build flag. +Compression for GDK Console can be enabled with the `HC_ENABLE_GDK_XBOX_WEBSOCKET_COMPRESSION` build flag. + +Call `HCWebSocketSetOptions()` on a handle before `HCWebSocketConnectAsync()` to control the built-in WebSocket behavior for that connection. `LegacySemantics` explicitly preserves the existing legacy behavior. `None` selects deterministic behavior without requesting compression. `RequestCompression` selects deterministic behavior and requests `permessage-deflate` compression. Combine `RequestCompression` with `CompressionServerNoContextTakeover` and/or `CompressionClientNoContextTakeover` to request fresh zlib state per message in the corresponding direction. These flags require `RequestCompression`; setting them alone returns `E_INVALIDARG`. + +In deterministic websocketpp-backed behavior, fragment callbacks are not supported and the inbound message-size limit becomes a hard cap. `HCWebSocketSetMaxReceiveBufferSize()` overrides that cap if called before connect; otherwise the websocketpp path uses its default `32,000,000` byte limit. On Win32 and GDK, explicit non-legacy options switch the built-in transport to the websocketpp-backed deterministic path. Legacy WinHTTP behavior, including the current oversized-payload fragment callback behavior, remains the default when `HCWebSocketSetOptions()` is not called. + +#### WinHTTP proxy and TLS notes + +On the built-in WinHTTP WebSocket path, `HCWebSocketSetProxyUri()` applies the explicit proxy URI to the WebSocket request. Embedded proxy credentials are passed through but not pre-authenticated. + +`HCWebSocketSetProxyDecryptsHttps()` disables TLS server certificate validation for a WebSocket connection. It is a debugging-only setting for use with HTTPS-intercepting proxies (Fiddler, Charles, etc.) and should never be enabled in production. Available on Win32 and GDK. On GDK console, TLS validation is always enforced regardless of this setting, matching the built-in WinHTTP WebSocket path. ## Behavior control * On GDK, XDK ERA, UWP, iOS, and Android, HCHttpCallPerform() will call native platform APIs * Optionally call HCSetHttpCallPerformFunction() to do your own HTTP handling using HCHttpCallRequestGet*(), HCHttpCallResponseSet*(), and HCSettingsGet*() * See sample CustomHttpImplWithCurl for an example of how to use this callback to make your own HTTP implementation. +* Optionally call HCSetWebSocketFunctions() to replace the built-in WebSocket backend selection with your own connect / send / disconnect callbacks. ## Build customization @@ -77,6 +118,11 @@ If you are building libHttpClient from source, you can provide an hc_settings.pr * Defining HCNoWebSockets will exclude WebSocket APIs (and all their dependencies) from the libHttpClient library * Defining HCNoZlib will exclude compression APIs and prevent libHttpClient from defining Zlib symbols within libHttpClient * Defining HCExternalOpenSSL will prevent libHttpClient from referencing our private OpenSSL projects. If this is defined, you will need to manually include your own (compatible) version of OpenSSL when linking. +* Setting `HCEnableWebSocketCompression` to `true` or `false` controls whether the optional compression-capable WebSocket provider is compiled into the Win32 and GDK MSBuild builds. When enabled and the required WinTLS dependency is present, the build defines `HC_ENABLE_WEBSOCKET_COMPRESSION` and registers the alternate `websocketpp`-based compression provider. This property defaults to `true`. +* Setting `HCEnableGDKXboxWebSocketCompression` to `true` enables that alternate `websocketpp`-based provider on GDK Xbox console builds. This property defaults to `false`, so GDK console builds keep the WinHTTP WebSocket path unless they are explicitly opted in. +* The Win32 certificate-validation integration tests are compiled only in `Tests\WebSocketCompression\WebSocketCompressionIntegrationTests.Win32.vcxproj`, which defines `HC_ENABLE_WSS_CERT_STORE_TESTS=1`. Running that integration binary may prompt for Windows confirmation when it adds or removes the temporary test certificate from `CurrentUser\Root`, so it is intended for manual/integration use rather than CI. The default `Tests\WebSocketCompression\WebSocketCompressionTests.Win32.vcxproj` intentionally omits that define and remains popup-free. + +For Linux CMake builds, `HC_ENABLE_WEBSOCKET_COMPRESSION` is enabled by default. The helper script in `Build\libHttpClient.Linux\libHttpClient_Linux.bash` keeps that default and exposes `-nwc|--no-websocket-compression` as an opt-out, while still accepting `-wc|--websocket-compression` for explicit enablement. An example customization file hc_settings.props.example can be found at the root of the repository. diff --git a/SECURITY_AUDIT_BOOST-WINTLS.md b/SECURITY_AUDIT_BOOST-WINTLS.md new file mode 100644 index 00000000..d7e7ab46 --- /dev/null +++ b/SECURITY_AUDIT_BOOST-WINTLS.md @@ -0,0 +1,255 @@ +# Security Audit: `libHttpClient` websocket compression backend (`websocketpp` + `boost-wintls`) + +Repository: `libHttpClient` +Audited branch: `websocket-compression` +Audited revision: `69f735d9bfff82b5317eceb92d081c1a6bd69f90` + +## Executive Summary + +This audit evaluates the optional compression-capable websocket backend implemented in `libHttpClient` for Win32 and GDK. WinHTTP remains the default websocket provider. When compression is requested, eligible connections can instead be routed through a `websocketpp` provider. On Microsoft `wss` connections, that provider uses `boost-wintls`, which delegates TLS protocol and trust decisions to SSPI/Schannel rather than shipping a separate crypto stack.[^1][^2][^3][^4] + +The branch has three clear security strengths: + +- backend selection is explicit and scoped to the compression-capable path rather than replacing all websocket traffic by default;[^1][^2] +- the Microsoft `wss` adapter centralizes secure TLS setup by enabling system trust, certificate verification, hostname/SNI propagation, and revocation policy in the transport layer;[^3][^4] +- the repo includes real tests for compression negotiation, upgrade response headers, and fragment behavior on the new websocket path.[^8] + +At the time of the original review, the principal concerns were not about bespoke cryptography. They were about backend parity, platform gating, diagnostics, lifecycle hardening, and validation depth: + +- selecting the websocketpp backend also changes proxy behavior and the effect of existing proxy-related websocket APIs relative to the default WinHTTP websocket path;[^2][^5][^6] +- the compression-capable provider is broadly available on GDK, including the Xbox console runtime-detection path, without a separate console-specific build flag, runtime gate, or similar control in the code reviewed here;[^1] +- TLS failure reporting is more generic than ideal for approval review or production incident triage;[^7] +- disconnect and shutdown handling still have open TODOs and a not-fully-bounded join/stop sequence;[^8] +- checked-in tests do not yet provide a Microsoft `wss` failure matrix for the actual WinTLS path.[^9] + +Bottom line: + +- **Windows desktop, websocket-only use, current branch state**: a strong candidate, with the originally identified branch-local hardening items now addressed in code, configuration, and tests. +- **GDK/Xbox approval argument, current branch state**: materially stronger than the audited revision because console exposure is now default-off, RETAIL validation remains fail-closed, and the Microsoft `wss` path now has direct validation evidence. Platform-owner review is still required, but the audit no longer leaves an obvious branch-local hardening gap open in this repo. + +## Remediation Update (current branch state) + +Since the audited revision above, this branch has landed follow-on hardening specifically aimed at the approval-sensitive gaps called out in this report. + +- GDK/Xbox exposure of the compression-capable websocket backend is now default-off and gated separately from the desktop path. +- Proxy handling and validation policy on the websocketpp + WinTLS path have been tightened, with GDK console RETAIL remaining fail-closed for certificate validation. +- Microsoft `wss` diagnostics and websocket shutdown behavior have been hardened in the `libHttpClient` integration layer. +- A dedicated Win32 integration-test project now carries the local WinTLS-backed Microsoft `wss` certificate-validation matrix for trusted `wss://localhost` success plus wrong-host `CERT_E_CN_NO_MATCH` and untrusted-root `CERT_E_UNTRUSTEDROOT` failures. The default `WebSocketCompressionTests.Win32` binary retains popup-free refused-connect diagnostics, while the trust-store-manipulation scenarios stay out of the CI-facing path because `CurrentUser\Root` updates can trigger confirmation UI on Windows; local verification confirmed the regular test binary stayed popup-free and the integration binary prompted as expected. + +The findings below remain useful as the rationale for that remediation work, but the branch should now be evaluated together with these follow-on changes rather than only as a snapshot of the original audited revision. + +### Current status of the original concerns + +- **Backend parity and policy**: addressed to the intended branch policy. The websocketpp path now uses HTTPS proxy discovery, WinHTTP websocket calls now honor explicit websocket proxy URIs, and the security-sensitive `ProxyDecryptsHttps()` behavior is implemented/documented intentionally rather than left as accidental backend drift.[^2][^5][^6] +- **Platform gating**: addressed for GDK/Xbox. The compression-capable provider is now default-off on GDK console builds unless `HCEnableGDKXboxWebSocketCompression` is explicitly enabled.[^1] +- **TLS diagnostics**: addressed for branch scope. The Microsoft `wss` path now preserves more meaningful transport and certificate failure detail instead of flattening everything to generic failure reporting.[^7] +- **Shutdown and disconnect hardening**: addressed for branch scope. The websocketpp shutdown path is now materially more bounded and better covered by regressions.[^8] +- **Microsoft `wss` validation depth**: addressed for branch scope. The repo now has a popup-free regular test path plus an integration-only Win32 certificate-validation matrix covering trusted localhost success, wrong-host, untrusted-root, and refused-connect diagnostics.[^9][^11] + +No unresolved **branch-local** audit blocker remains in this repo. The remaining work is optional evidence expansion or intentionally deferred inherited/vendor follow-up. + +## Scope and Method + +This is a branch-focused review of the `libHttpClient` integration, not a generic Schannel or `boost-wintls` review in isolation. Because the comparison point is current `main`, the practical focus is the scoped backend-selection, compression-capable provider registration, Microsoft WinTLS transport setup, and associated tests introduced or exercised by this work. The review covered: + +- platform registration and provider-selection logic, +- the `HCWebSocket*` option surface that feeds provider behavior, +- the Microsoft `wss` transport setup in the `websocketpp` backend, +- proxy and certificate-policy behavior, +- shutdown, disconnect, and thread-lifetime handling, +- checked-in tests for compression and websocket behavior, +- upstream `boost-wintls` concerns only to the extent they remain inherited risk for this integration. + +This review did **not** include a full Win32/GDK/Xbox interoperability matrix in this environment. Where the report discusses coverage, it is describing checked-in code and tests, not claiming full end-to-end execution proof on every target. + +## Architecture Overview + +At a high level, the relevant design is: + +```text +libHttpClient websocket handle + | + v +SelectorWebSocketProvider + | | + | default path | compression requested + v v +WinHTTP provider websocketpp provider + | + v + ws:// or wss:// client + | + v + Microsoft wss -> boost-wintls -> SSPI/Schannel +``` + +This structure matters for the audit in two ways. + +First, the change is scoped. The new backend is not globally replacing WinHTTP for all websocket traffic; it is selected per connection when the compression-capable path is available and `RequestCompression` is set on the websocket handle.[^1][^2] + +Second, once that routing decision is made, the security boundary includes more than just the TLS library: + +- provider selection and active-provider lifetime, +- compression option validation, +- transport initialization and proxy handling, +- TLS setup in the Microsoft `wss` path, +- close, timeout, and cancellation behavior. + +That is why this report focuses on the integration as a system rather than treating `boost-wintls` as the whole story. + +## Findings + +### Finding 1 (Positive): provider routing is explicit and constrained + +`NetworkState` wraps the default websocket provider and the optional compression-capable provider in `SelectorWebSocketProvider`. Connect-time routing selects a provider based on the websocket handle's compression options, stores that choice as the active provider for the handle, and send/disconnect operations continue through that already-selected provider.[^1] + +`HCWebSocketSetOptions` is also rejected after connect has started, which reduces the chance of mid-connection backend changes or ambiguous fallback behavior.[^2] + +From a security-review perspective, this is a good design choice. The integration does not appear to "slip" between providers after connect begins. + +### Finding 2 (Positive): the Microsoft `wss` adapter applies secure TLS defaults centrally + +On Microsoft `wss`, the `websocketpp` provider creates a `wintls::context` with `wintls::method::system_default`, enables default certificates, enables server-certificate verification by default, and configures revocation checking on the client transport.[^3] + +The custom `wintls_socket` transport then sets the verification hostname/SNI from the websocket URI before the TLS handshake begins.[^4] + +This is the kind of centralization expected in a security-sensitive adapter layer: callers are not left to remember the critical trust and identity settings for every connection. For the Microsoft `wss` path, those settings are applied centrally and predictably in one place.[^3][^4] + +### Finding 3 (Medium-High at audited revision): backend selection changed proxy semantics and API behavior relative to WinHTTP + +At the audited revision, the most significant backend-parity issue in the reviewed code was proxy handling. + +The websocketpp backend actively consumes per-websocket proxy state: + +- it parses and applies `ProxyUri()` when explicitly configured; +- it gives `ProxyDecryptsHttps()` concrete effect in the Microsoft `wss` path; +- if no explicit proxy is set, it performs its own Windows proxy lookup using `get_ie_proxy_info(proxy_protocol::websocket, ...)`.[^2][^3][^6] + +By contrast, the audited revision did not show corresponding WinHTTP websocket-provider reads of `ProxyUri()` or `ProxyDecryptsHttps()`. The WinHTTP websocket provider is a thin pass-through into the shared WinHTTP implementation, and repository-wide searches under `Source\HTTP\WinHttp` for those websocket-handle proxy fields returned no matches.[^5] + +As a result, backend selection changed not only the transport implementation but also the meaning and effect of existing websocket proxy-related knobs. + +Two specific concerns sit inside that larger parity gap. + +First, `HCWebSocketSetProxyDecryptsHttps` is a preexisting `libHttpClient` API, not something introduced solely for this branch. The concern here is **not** that the branch added a new dangerous knob. The concern is that the websocketpp+WinTLS path gives that API concrete per-connection effect on Microsoft `wss`, while the existing WinHTTP websocket path does not appear to implement equivalent behavior. On the new path, enabling it disables both certificate verification and revocation checking for that connection.[^2][^3][^5] + +Second, automatic proxy lookup differs between the backends. The websocketpp path requests `proxy_protocol::websocket`, and the Windows helper maps that to the `socks` slot while parsing IE proxy configuration. The WinHTTP path, on the secure side of the stack, uses `proxy_protocol::https` when establishing proxy policy for secure traffic.[^6] + +If both differences were intentional, they needed to be documented as deliberate backend-specific behavior. If not, they represented a meaningful parity gap between the default websocket backend and the compression backend. This finding is preserved as the rationale for the follow-on proxy/policy remediation described above. + +### Finding 4 (Medium-High at audited revision): GDK/Xbox enablement was broader than ideal for an initial approval case + +On Win32, the compression-capable provider is registered when `HC_ENABLE_WEBSOCKET_COMPRESSION` is defined. On GDK, the same compression provider is also registered in both the non-console and Xbox console runtime-detection paths, while WinHTTP remains the default websocket provider.[^1] + +The repo README also documents `HCEnableWebSocketCompression` as a Win32/GDK MSBuild property that defaults to `true` when the dependency is present.[^1] + +That meant the alternate websocket stack was readily available on GDK builds whenever compression was requested. I did not find a separate console-specific build flag, runtime gate, policy hook, or rollback control in the code reviewed at that time.[^1] + +For a Windows desktop rollout this may have been acceptable. For an Xbox/platform approval argument, the console scope was easier to review once the later default-off kill switch was added. + +### Finding 5 (Medium at audited revision): TLS diagnostics were coarser than ideal for support and review + +At the audited revision, the custom WinTLS websocketpp transport mapped handshake failures to a generic `tls_handshake_failed` error, and the higher layer typically reduced connect failures to `E_FAIL` except for invalid HTTP status responses. The websocketpp numeric error was retained as `platformErrorCode`, but the resulting surface made it harder than necessary to distinguish wrong-host, untrusted-root, revocation, EOF, and protocol failures.[^7] + +This was primarily a diagnosability issue rather than a fail-open vulnerability. It is preserved here as the reason the follow-on branch work tightened HRESULT and WinTLS failure preservation. + +### Finding 6 (Medium at audited revision): shutdown and disconnect handling needed additional hardening + +At the audited revision, the websocketpp implementation still carried TODOs for wiring unexpected disconnects into close behavior and for verifying behavior when the client websocket handle was closed.[^8] + +The shutdown path also used a timed `wait_for` around an asynchronously launched `std::thread::join`. On timeout it logged a warning and called `stop()`, but it did not perform a second explicit bounded wait or otherwise demonstrate that teardown had actually completed before the client object was torn down.[^8] + +That did not prove a memory-safety defect, but it did leave meaningful uncertainty around close semantics, time-bounded cleanup, and callback coherence under timeout, cancellation, or abrupt peer loss. The finding is retained as the motivation for the later shutdown/join hardening. + +### Finding 7 (Medium at audited revision): checked-in tests were valuable, but they did not yet validate the Microsoft `wss` path + +The test story is better than "no coverage": + +- there is a dedicated compression integration suite; +- it verifies negotiation behavior and upgrade response headers; +- it validates the synthetic fragment-delivery logic used on Win32/GDK; +- there are unit tests for the compression-options API and for proxy formatting helpers.[^8] + +That is a good sign for maintainability and regression resistance. + +However, at the audited revision the checked-in websocket compression suite used `websocketpp/config/asio_no_tls.hpp`, so it exercised a local `ws://` path rather than the Microsoft `wss` transport that was most security-sensitive in this review.[^9] + +No corresponding checked-in Microsoft `wss` suite was identified at that time for: + +- trusted vs untrusted certificates, +- wrong-host validation, +- revocation failure or revocation-offline behavior, +- backend parity around explicit proxy vs automatic proxy handling, +- the effect of `ProxyDecryptsHttps` on the new backend, +- TLS 1.2 vs TLS 1.3 async websocket flows, +- abrupt close, timeout, and cancellation behavior. + +That left the most approval-sensitive parts of the implementation primarily supported by static review rather than targeted integration evidence. The later regular/integration test split was added specifically to close that gap without making the CI-facing binary popup-prone. + +## Overall Assessment + +Taken as a whole, the current branch now provides a **reasonable and security-conscious engineering foundation** for a compression-capable websocket backend on Windows-family platforms: + +- the routing model is explicit, +- the Microsoft `wss` path preserves a Schannel-based trust and crypto story, +- secure TLS setup is centralized in the adapter rather than left to callers, +- GDK/Xbox exposure is fail-closed by default, +- TLS failure and shutdown behavior have been hardened in the integration layer, +- there is now direct Microsoft `wss` validation evidence, with the popup-prone trust-store cases isolated to an explicit integration-only test binary.[^1][^3][^4][^8][^9] + +For the scope of this repository work, the originally identified audit remediation items are complete. In other words: there is no remaining branch-local audit finding here that still requires code, configuration, or test work before review of this branch. + +That does **not** mean every conceivable approval question is permanently closed. It means the remaining questions are no longer about missing hardening work in this branch; they are about optional confidence-building evidence, platform-owner rollout decisions, or inherited upstream behavior that the user intentionally chose not to patch in vendored code. + +## Optional Follow-On Work + +1. **Expand the Microsoft `wss` evidence matrix only if more approval evidence is desired.** + The current branch already covers the baseline path needed for the audit remediation. Additional cases such as revocation-specific failures, proxy-specific `wss` behavior, TLS 1.2/TLS 1.3 variants, cancellation, and more shutdown edge cases would be confidence-building follow-up rather than unfinished remediation.[^9][^11] + +2. **Keep `ProxyDecryptsHttps()` treated as a security-sensitive backend-specific setting.** + The branch now handles and documents this intentionally. Future changes should preserve the fail-closed GDK console RETAIL behavior and avoid silently broadening relaxed-validation behavior.[^2][^3][^5] + +3. **Revisit vendored `boost-wintls` only if future testing proves it necessary.** + The remaining upstream concerns are around async/TLS 1.3/post-handshake handling and deeper shutdown behavior. Those are real inherited-risk topics, but in this branch they are intentionally documented and deferred rather than left half-remediated.[^11] + +4. **Use the new default-off console gate as the rollout control point.** + The branch now has the kill switch the audit called for. Any future platform rollout should keep that gate explicit and easy to reverse if platform guidance changes.[^1] + +## Appendix A: comparison to generic `boost-wintls` use + +This report is intentionally centered on the `libHttpClient` integration rather than on upstream `boost-wintls` in isolation. That said, one comparison is still worth preserving because it explains why the adapter design matters. + +Upstream `boost-wintls` is a thin wrapper over SSPI/Schannel, which is a good foundation from a crypto/provider perspective. But upstream is **not secure by default for generic client callers**: + +- server-certificate verification is opt-in, +- default system trust stores are opt-in, +- hostname verification only happens if the caller explicitly sets the server hostname.[^10] + +The `libHttpClient` integration improves that story significantly on the Microsoft `wss` path by centralizing those settings in the adapter. That is one of the strongest positive outcomes of this branch and a key reason the integration itself is the right audit focus.[^3][^4][^10] + +## Appendix B: inherited `boost-wintls` concerns that still matter here + +Some upstream `boost-wintls` concerns still remain relevant because the Microsoft `wss` path rides on those internals. + +Importantly, the upstream audit did **not** uncover a concrete fail-open handshake flaw, obvious certificate-validation bypass, or bespoke cryptographic weakness in `boost-wintls` itself. The residual upstream concern is instead about **robustness and security posture**: generic callers must opt into safe client settings, and the async TLS 1.3 / post-handshake / shutdown paths deserve more direct validation than the current upstream evidence provides. + +The two most important inherited concerns are: + +1. **async/TLS 1.3/post-handshake handling deserves direct validation**, because upstream async paths are thinner than the synchronous paths in their handling of renegotiation/post-handshake states;[^11] +2. **shutdown behavior deserves direct validation**, because upstream close handling is one of the reasons the downstream websocket teardown path should be tested aggressively.[^11] + +Those are not reasons to reject the integration outright. They are reasons to make Microsoft `wss` integration testing and lifecycle hardening part of the next engineering pass rather than assuming the transport layer is already fully proven. + +## Footnotes + +[^1]: `README.md:90-96`; `Source\Global\NetworkState.cpp:14-103,107-118,269-293`; `Source\Platform\Win32\PlatformComponents_Win32.cpp:13-25`; `Source\Platform\GDK\PlatformComponents_GDK.cpp:35-47,57-70,87-91`. +[^2]: `Source\WebSocket\websocket_publics.cpp:59-78,104-128`; `Source\WebSocket\hcwebsocket.cpp:432-477`. +[^3]: `Source\WebSocket\Websocketpp\websocketpp_websocket.cpp:419-463`. +[^4]: `Source\WebSocket\Websocketpp\wintls_socket.hpp:139-145,175-180,266-276`. +[^5]: Repository-wide searches on 2026-03-24 under `Source\HTTP\WinHttp` for `ProxyDecryptsHttps` and `ProxyUri\(` returned no matches; see also `Source\HTTP\WinHttp\winhttp_provider.h:160-186`; `Source\HTTP\WinHttp\winhttp_provider.cpp:696-735`. +[^6]: `Source\WebSocket\Websocketpp\websocketpp_websocket.cpp:88-204,913-944`; `Source\Common\Win\utils_win.cpp:152-177`; `Source\HTTP\WinHttp\winhttp_provider.cpp:374-378,501-514`; `Source\HTTP\WinHttp\winhttp_connection.cpp:1448-1472`. +[^7]: `Source\WebSocket\Websocketpp\wintls_socket.hpp:150-180`; `Source\WebSocket\Websocketpp\websocketpp_websocket.cpp:969-980`. +[^8]: `Source\WebSocket\Websocketpp\websocketpp_websocket.cpp:807-808,1304-1338`; `Tests\WebSocketCompression\WebSocketCompressionTests.cpp:575-695,729-801`; `Tests\UnitTests\Tests\WebsocketTests.cpp:345-362`; `Tests\UnitTests\Tests\ProxyTests.cpp:21-77`. +[^9]: `Tests\WebSocketCompression\WebSocketCompressionTests.cpp:21`; repository-wide search on 2026-03-24 under `Tests` for `wss|tls|certificate|revocation|ProxyDecryptsHttps|wintls|schannel`. +[^10]: `External\boost-wintls\include\wintls\context.hpp:33-36,47-49,77-96,140-156`; `External\boost-wintls\include\wintls\detail\context_certificates.hpp:54-83,116-168`; `External\boost-wintls\include\wintls\stream.hpp:104-129,148-179`. +[^11]: `External\boost-wintls\include\wintls\detail\async_read.hpp:39-62`; `External\boost-wintls\include\wintls\detail\async_handshake.hpp:55-95`; `External\boost-wintls\include\wintls\detail\sspi_shutdown.hpp:30-57`; `External\boost-wintls\include\wintls\detail\async_shutdown.hpp:26-56`. diff --git a/Source/Platform/IWebSocketProvider.h b/Source/Platform/IWebSocketProvider.h index 047af846..97585a60 100644 --- a/Source/Platform/IWebSocketProvider.h +++ b/Source/Platform/IWebSocketProvider.h @@ -39,7 +39,30 @@ class IWebSocketProvider HCWebsocketHandle websocketHandle, HCWebSocketCloseStatus closeStatus ) noexcept = 0; + + virtual HRESULT OptionsResult(HCWebSocketOptions options) const noexcept + { + UNREFERENCED_PARAMETER(options); + return E_NOT_SUPPORTED; + } +}; + +class IProviderLifecycle +{ +public: + IProviderLifecycle() = default; + IProviderLifecycle(IProviderLifecycle const&) = delete; + IProviderLifecycle& operator=(IProviderLifecycle const&) = delete; + virtual ~IProviderLifecycle() = default; + + virtual void OnSuspending() noexcept = 0; + virtual void OnResuming() noexcept = 0; }; + +inline IProviderLifecycle* GetProviderLifecycle(IWebSocketProvider* provider) noexcept +{ + return dynamic_cast(provider); +} #endif // !HC_NOWEBSOCKETS NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/WebSocket/Android/AndroidWebSocketProvider.cpp b/Source/WebSocket/Android/AndroidWebSocketProvider.cpp index fc9fdcf9..f89fbba5 100644 --- a/Source/WebSocket/Android/AndroidWebSocketProvider.cpp +++ b/Source/WebSocket/Android/AndroidWebSocketProvider.cpp @@ -10,8 +10,8 @@ extern "C" { - void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv*, jobject); - void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv*, jobject, jint); + void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv*, jobject, jobjectArray, jobjectArray); + void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv*, jobject, jint, jobjectArray, jobjectArray); void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onClose(JNIEnv*, jobject, jint); void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onMessage(JNIEnv*, jobject, jstring); void JNICALL Java_com_xbox_httpclient_HttpClientWebSocket_onBinaryMessage(JNIEnv*, jobject, jobject); @@ -111,6 +111,7 @@ struct HttpClientWebSocket { env->DeleteLocalRef(headerValue); } + env->DeleteLocalRef(headerName); return E_UNEXPECTED; } @@ -126,6 +127,90 @@ struct HttpClientWebSocket return S_OK; } + static HttpHeaders ReadHeaders(JNIEnv* env, jobjectArray headerNames, jobjectArray headerValues) + { + HttpHeaders headers; + if (!env || !headerNames || !headerValues) + { + return headers; + } + + const jsize nameCount = env->GetArrayLength(headerNames); + if (HadException(env)) + { + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read failed while reading names."); + return headers; + } + + const jsize valueCount = env->GetArrayLength(headerValues); + if (HadException(env)) + { + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read failed while reading values."); + return headers; + } + + if (nameCount != valueCount) + { + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read saw mismatched header array sizes (%d vs %d).", static_cast(nameCount), static_cast(valueCount)); + } + + const jsize count = std::min(nameCount, valueCount); + for (jsize i = 0; i < count; ++i) + { + auto name = static_cast(env->GetObjectArrayElement(headerNames, i)); + if (HadException(env) || !name) + { + if (name) + { + env->DeleteLocalRef(name); + } + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read failed to get header name."); + return headers; + } + + auto value = static_cast(env->GetObjectArrayElement(headerValues, i)); + if (HadException(env) || !value) + { + env->DeleteLocalRef(name); + if (value) + { + env->DeleteLocalRef(value); + } + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read failed to get header value."); + return headers; + } + + const char* rawName = env->GetStringUTFChars(name, 0); + const char* rawValue = env->GetStringUTFChars(value, 0); + if (HadException(env) || !rawName || !rawValue) + { + if (rawName) + { + env->ReleaseStringUTFChars(name, rawName); + } + if (rawValue) + { + env->ReleaseStringUTFChars(value, rawValue); + } + env->DeleteLocalRef(name); + env->DeleteLocalRef(value); + HC_TRACE_WARNING(WEBSOCKET, "Android websocket response-header read failed to marshal UTF-8 strings."); + return headers; + } + + http_internal_string headerName{ rawName }; + http_internal_string headerValue{ rawValue }; + headers[std::move(headerName)] = std::move(headerValue); + + env->ReleaseStringUTFChars(name, rawName); + env->ReleaseStringUTFChars(value, rawValue); + env->DeleteLocalRef(name); + env->DeleteLocalRef(value); + } + + return headers; + } + HRESULT Connect(const std::string& uri, const std::string subProtocol) const { if (uri.empty()) @@ -434,8 +519,8 @@ struct HttpClientWebSocket struct okhttp_websocket_impl : hc_websocket_impl, std::enable_shared_from_this { - friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv*, jobject); - friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv*, jobject, jint); + friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv*, jobject, jobjectArray, jobjectArray); + friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv*, jobject, jint, jobjectArray, jobjectArray); friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onClose(JNIEnv*, jobject, jint); friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onMessage(JNIEnv*, jobject, jstring); friend void JNICALL ::Java_com_xbox_httpclient_HttpClientWebSocket_onBinaryMessage(JNIEnv*, jobject, jobject); @@ -1020,7 +1105,7 @@ extern "C" { JNIEXPORT void JNICALL -Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv *env, jobject thiz) +Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv *env, jobject thiz, jobjectArray headerNames, jobjectArray headerValues) { using namespace xbox::httpclient; @@ -1030,11 +1115,12 @@ Java_com_xbox_httpclient_HttpClientWebSocket_onOpen(JNIEnv *env, jobject thiz) return; } + owner->GetHandle()->websocket->SetResponseHeaders(HttpClientWebSocket::ReadHeaders(env, headerNames, headerValues)); owner->OnOpen(owner->Lock()); } JNIEXPORT void JNICALL -Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv *env, jobject thiz, jint statusCode) +Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv *env, jobject thiz, jint statusCode, jobjectArray headerNames, jobjectArray headerValues) { using namespace xbox::httpclient; @@ -1044,6 +1130,7 @@ Java_com_xbox_httpclient_HttpClientWebSocket_onFailure(JNIEnv *env, jobject thiz return; } + owner->GetHandle()->websocket->SetResponseHeaders(HttpClientWebSocket::ReadHeaders(env, headerNames, headerValues)); owner->OnFailure(owner->Lock(), statusCode); } diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 9674c443..00ca6cd3 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -3,6 +3,7 @@ #include "pch.h" #include "hcwebsocket.h" +#include "websocket_options.h" #ifndef HC_NOWEBSOCKETS @@ -64,9 +65,16 @@ xbox::httpclient::ObserverPtr HC_WEBSOCKET_OBSERVER::Initialize( return observer; } -void HC_WEBSOCKET_OBSERVER::SetBinaryMessageFragmentEventFunction(HCWebSocketBinaryMessageFragmentFunction binaryFragmentFunc) +HRESULT HC_WEBSOCKET_OBSERVER::SetBinaryMessageFragmentEventFunction(HCWebSocketBinaryMessageFragmentFunction binaryFragmentFunc) noexcept { + RETURN_HR_IF(E_NOT_SUPPORTED, binaryFragmentFunc != nullptr && websocket->UsesDeterministicSemantics()); m_binaryFragmentFunc = binaryFragmentFunc; + return S_OK; +} + +bool HC_WEBSOCKET_OBSERVER::HasBinaryMessageFragmentEventFunction() const noexcept +{ + return m_binaryFragmentFunc != nullptr; } void CALLBACK HC_WEBSOCKET_OBSERVER::MessageFunc(HCWebsocketHandle internalHandle, const char* message, void* context) @@ -212,9 +220,10 @@ HRESULT CALLBACK WebSocket::ConnectAsyncProvider(XAsyncOp op, XAsyncProviderData { assert(context->observer); auto& ws{ context->observer->websocket }; - std::unique_lock lock{ ws->m_stateMutex }; + std::unique_lock lock{ ws->m_stateMutex }; RETURN_HR_IF(E_UNEXPECTED, ws->m_state != State::Initial); + ws->ClearResponseHeadersLockHeld(); XTaskQueuePortHandle workPort{ nullptr }; XTaskQueueGetPort(data->async->queue, XTaskQueuePort::Work, &workPort); @@ -259,6 +268,7 @@ void CALLBACK WebSocket::ConnectComplete(XAsyncBlock* async) // We can be put into the Disconnected state if a spontaneous error occurs between the connection process completing and this callback being invoked. // We need to be able to handle that scenario here. HRESULT hr = HCGetWebSocketConnectResult(&context->internalAsyncBlock, &context->result); +<<<<<<< HEAD std::unique_lock lock{ ws->m_stateMutex }; const bool bIsDisconnected = (ws->m_state == State::Disconnected); if (bIsDisconnected && !FAILED(hr)) @@ -269,6 +279,10 @@ void CALLBACK WebSocket::ConnectComplete(XAsyncBlock* async) assert(ws->m_state == State::Connecting || bIsDisconnected); assert(context->observer.get() == context->result.websocket || FAILED(hr) || bIsDisconnected); +======= + + std::unique_lock lock{ ws->m_stateMutex }; +>>>>>>> c7283d7 (Replace compression flags with explicit WebSocket options) if (SUCCEEDED(hr) && SUCCEEDED(context->result.errorCode)) { // Connect was sucessful. Allocate ProviderContext to ensure WebSocket lifetime until it is reclaimed in WebSocket::CloseFunc @@ -279,6 +293,7 @@ void CALLBACK WebSocket::ConnectComplete(XAsyncBlock* async) } else { + ws->ClearResponseHeadersLockHeld(); ws->m_state = State::Disconnected; } lock.unlock(); @@ -328,10 +343,15 @@ HRESULT WebSocket::SendBinaryAsync( } HRESULT WebSocket::Disconnect() +{ + return Disconnect(HCWebSocketCloseStatus::Normal); +} + +HRESULT WebSocket::Disconnect(HCWebSocketCloseStatus closeStatus) { RETURN_HR_IF(E_UNEXPECTED, !m_providerContext); - std::unique_lock lock{ m_stateMutex }; + std::unique_lock lock{ m_stateMutex }; RETURN_HR_IF(S_OK, m_state == State::Disconnecting); RETURN_HR_IF(E_UNEXPECTED, m_state != State::Connected); @@ -340,7 +360,7 @@ HRESULT WebSocket::Disconnect() try { - return m_provider.Disconnect(m_providerContext->observer.get(), HCWebSocketCloseStatus::Normal); + return m_provider.Disconnect(m_providerContext->observer.get(), closeStatus); } catch (...) { @@ -364,6 +384,58 @@ const HttpHeaders& WebSocket::Headers() const noexcept return m_connectHeaders; } +HRESULT WebSocket::GetResponseHeader( + _In_z_ const char* headerName, + _Out_ const char** headerValue +) const noexcept +{ + std::lock_guard lock{ m_stateMutex }; + + auto it = m_connectResponseHeaders.find(headerName); + if (it != m_connectResponseHeaders.end()) + { + *headerValue = it->second.c_str(); + } + else + { + *headerValue = nullptr; + } + + return S_OK; +} + +uint32_t WebSocket::GetNumResponseHeaders() const noexcept +{ + std::lock_guard lock{ m_stateMutex }; + return static_cast(m_connectResponseHeaders.size()); +} + +HRESULT WebSocket::GetResponseHeaderAtIndex( + _In_ uint32_t headerIndex, + _Out_ const char** headerName, + _Out_ const char** headerValue +) const noexcept +{ + std::lock_guard lock{ m_stateMutex }; + + uint32_t index = 0; + for (auto it = m_connectResponseHeaders.cbegin(); it != m_connectResponseHeaders.cend(); ++it) + { + if (index == headerIndex) + { + *headerName = it->first.c_str(); + *headerValue = it->second.c_str(); + return S_OK; + } + + ++index; + } + + *headerName = nullptr; + *headerValue = nullptr; + return S_OK; +} + const http_internal_string& WebSocket::ProxyUri() const noexcept { return m_proxyUri; @@ -374,16 +446,41 @@ const bool WebSocket::ProxyDecryptsHttps() const noexcept return m_allowProxyToDecryptHttps; } +HCWebSocketOptions WebSocket::Options() const noexcept +{ + return m_options; +} + size_t WebSocket::MaxReceiveBufferSize() const noexcept { return m_maxReceiveBufferSize; } +bool WebSocket::MaxReceiveBufferSizeExplicitlySet() const noexcept +{ + return m_maxReceiveBufferSizeExplicitlySet; +} + uint32_t WebSocket::PingInterval() const noexcept { return m_pingInterval; } +bool WebSocket::OptionsExplicitlySet() const noexcept +{ + return m_optionsExplicitlySet; +} + +bool WebSocket::UsesLegacySemantics() const noexcept +{ + return !m_optionsExplicitlySet || RequestsLegacyWebSocketSemantics(m_options); +} + +bool WebSocket::UsesDeterministicSemantics() const noexcept +{ + return m_optionsExplicitlySet && !RequestsLegacyWebSocketSemantics(m_options); +} + HRESULT WebSocket::SetHeader( http_internal_string&& headerName, http_internal_string&& headerValue @@ -412,13 +509,20 @@ HRESULT WebSocket::SetProxyDecryptsHttps( { return E_UNEXPECTED; } + RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); m_allowProxyToDecryptHttps = allowProxyToDecryptHttps; return S_OK; } HRESULT WebSocket::SetMaxReceiveBufferSize(size_t maxReceiveBufferSizeBytes) noexcept { + RETURN_HR_IF(E_INVALIDARG, maxReceiveBufferSizeBytes > static_cast((std::numeric_limits::max)())); + + std::lock_guard lock{ m_stateMutex }; + RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); + m_maxReceiveBufferSize = maxReceiveBufferSizeBytes; + m_maxReceiveBufferSizeExplicitlySet = true; return S_OK; } @@ -428,6 +532,69 @@ HRESULT WebSocket::SetPingInterval(uint32_t pingInterval) noexcept return S_OK; } +HRESULT WebSocket::SetOptions(HCWebSocketOptions options) noexcept +{ + RETURN_HR_IF(E_INVALIDARG, HasUnsupportedWebSocketOptions(options)); + RETURN_HR_IF( + E_INVALIDARG, + RequestsLegacyWebSocketSemantics(options) && + RawWebSocketOptions(options) != static_cast(HCWebSocketOptions::LegacySemantics)); + RETURN_HR_IF( + E_INVALIDARG, + HasNoContextTakeoverWebSocketOptions(options) && !RequestsWebSocketCompression(options)); + RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); + if (RequestsLegacyWebSocketSemantics(options)) + { + m_options = options; + m_optionsExplicitlySet = true; + return S_OK; + } + + RETURN_HR_IF(E_NOT_SUPPORTED, HasBinaryMessageFragmentHandlers()); + + HRESULT hr = m_provider.OptionsResult(options); + RETURN_IF_FAILED(hr); + + m_options = options; + m_optionsExplicitlySet = true; + return hr; +} + +bool WebSocket::HasBinaryMessageFragmentHandlers() const noexcept +{ + std::lock_guard lock{ m_eventCallbacksMutex }; + for (auto const& pair : m_eventCallbacks) + { + auto observer = static_cast(pair.second.context); + if (observer != nullptr && observer->HasBinaryMessageFragmentEventFunction()) + { + return true; + } + } + + return false; +} + +void WebSocket::ClearResponseHeadersLockHeld() noexcept +{ + m_connectResponseHeaders.clear(); +} + +void WebSocket::ClearResponseHeaders() noexcept +{ + std::lock_guard lock{ m_stateMutex }; + ClearResponseHeadersLockHeld(); +} + +HRESULT WebSocket::SetResponseHeaders(HttpHeaders&& headers) noexcept +try +{ + std::lock_guard lock{ m_stateMutex }; + m_connectResponseHeaders = std::move(headers); + return S_OK; +} +CATCH_RETURN() + void CALLBACK WebSocket::MessageFunc( HCWebsocketHandle handle, const char* message, @@ -534,7 +701,7 @@ void CALLBACK WebSocket::CloseFunc( auto websocket{ handle->websocket }; - std::unique_lock stateLock{ websocket->m_stateMutex }; + std::unique_lock stateLock{ websocket->m_stateMutex }; if (!websocket->m_providerContext) { // It's possible for our websocket to get closed before we finish connecting. m_providerContext only gets populated when the connection process is 100% completed. diff --git a/Source/WebSocket/hcwebsocket.h b/Source/WebSocket/hcwebsocket.h index daee11c0..fc490f07 100644 --- a/Source/WebSocket/hcwebsocket.h +++ b/Source/WebSocket/hcwebsocket.h @@ -3,6 +3,8 @@ #pragma once +#include + #include #include "HTTP/httpcall.h" #include "Platform/IWebSocketProvider.h" @@ -52,7 +54,8 @@ struct HC_WEBSOCKET_OBSERVER _In_opt_ void* callbackContext = nullptr ); - void SetBinaryMessageFragmentEventFunction(HCWebSocketBinaryMessageFragmentFunction binaryFragmentFunc); + HRESULT SetBinaryMessageFragmentEventFunction(HCWebSocketBinaryMessageFragmentFunction binaryFragmentFunc) noexcept; + bool HasBinaryMessageFragmentEventFunction() const noexcept; std::shared_ptr const websocket; @@ -122,6 +125,7 @@ class WebSocket : public std::enable_shared_from_this ) noexcept; HRESULT Disconnect(); + HRESULT Disconnect(HCWebSocketCloseStatus closeStatus); // Unique ID for logging uint64_t const id; @@ -132,13 +136,24 @@ class WebSocket : public std::enable_shared_from_this const http_internal_string& ProxyUri() const noexcept; const bool ProxyDecryptsHttps() const noexcept; size_t MaxReceiveBufferSize() const noexcept; + bool MaxReceiveBufferSizeExplicitlySet() const noexcept; uint32_t PingInterval() const noexcept; + HCWebSocketOptions Options() const noexcept; + bool OptionsExplicitlySet() const noexcept; + bool UsesLegacySemantics() const noexcept; + bool UsesDeterministicSemantics() const noexcept; + HRESULT GetResponseHeader(_In_z_ const char* headerName, _Out_ const char** headerValue) const noexcept; + uint32_t GetNumResponseHeaders() const noexcept; + HRESULT GetResponseHeaderAtIndex(_In_ uint32_t headerIndex, _Out_ const char** headerName, _Out_ const char** headerValue) const noexcept; HRESULT SetHeader(http_internal_string&& headerName, http_internal_string&& headerValue) noexcept; HRESULT SetProxyUri(http_internal_string&& proxyUri) noexcept; HRESULT SetProxyDecryptsHttps(bool allowProxyToDecryptHttps) noexcept; HRESULT SetMaxReceiveBufferSize(size_t maxReceiveBufferSizeBytes) noexcept; HRESULT SetPingInterval(uint32_t pingInterval) noexcept; + HRESULT SetOptions(HCWebSocketOptions options) noexcept; + void ClearResponseHeaders() noexcept; + HRESULT SetResponseHeaders(xbox::httpclient::HttpHeaders&& headers) noexcept; // Event functions static void CALLBACK MessageFunc(HCWebsocketHandle handle, const char* message, void* context); @@ -152,6 +167,8 @@ class WebSocket : public std::enable_shared_from_this private: static HRESULT CALLBACK ConnectAsyncProvider(XAsyncOp op, XAsyncProviderData const* data); static void CALLBACK ConnectComplete(XAsyncBlock* async); + void ClearResponseHeadersLockHeld() noexcept; + bool HasBinaryMessageFragmentHandlers() const noexcept; static void NotifyWebSocketRoutedHandlers( _In_ HCWebsocketHandle websocket, @@ -162,6 +179,7 @@ class WebSocket : public std::enable_shared_from_this ); xbox::httpclient::HttpHeaders m_connectHeaders; + xbox::httpclient::HttpHeaders m_connectResponseHeaders; bool m_allowProxyToDecryptHttps{ false }; http_internal_string m_proxyUri; http_internal_string m_uri; @@ -181,7 +199,7 @@ class WebSocket : public std::enable_shared_from_this void* context{ nullptr }; }; - DefaultUnnamedMutex m_stateMutex; + mutable DefaultUnnamedMutex m_stateMutex; enum class State { Initial, @@ -191,13 +209,18 @@ class WebSocket : public std::enable_shared_from_this Disconnected } m_state{ State::Initial }; - std::recursive_mutex m_eventCallbacksMutex; + mutable std::recursive_mutex m_eventCallbacksMutex; http_internal_map m_eventCallbacks{}; uint32_t m_nextToken{ 1 }; ProviderContext* m_providerContext{ nullptr }; + // Stable routing provider for this WebSocket. Hybrid providers may delegate internally, + // but the WebSocket does not keep per-connection active-provider state. IWebSocketProvider& m_provider; + HCWebSocketOptions m_options{ HCWebSocketOptions::None }; + bool m_optionsExplicitlySet{ false }; + bool m_maxReceiveBufferSizeExplicitlySet{ false }; }; } // namespace httpclient diff --git a/Source/WebSocket/websocket_options.h b/Source/WebSocket/websocket_options.h new file mode 100644 index 00000000..08ab468d --- /dev/null +++ b/Source/WebSocket/websocket_options.h @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#pragma once + +#include + +NAMESPACE_XBOX_HTTP_CLIENT_BEGIN + +constexpr uint32_t WebSocketLegacySemanticsMask() noexcept +{ + return static_cast(HCWebSocketOptions::LegacySemantics); +} + +constexpr uint32_t WebSocketRequestCompressionMask() noexcept +{ + return static_cast(HCWebSocketOptions::RequestCompression); +} + +constexpr uint32_t WebSocketCompressionNoContextTakeoverMask() noexcept +{ + return + static_cast(HCWebSocketOptions::CompressionServerNoContextTakeover) | + static_cast(HCWebSocketOptions::CompressionClientNoContextTakeover); +} + +constexpr uint32_t WebSocketSupportedOptionsMask() noexcept +{ + return WebSocketLegacySemanticsMask() | WebSocketRequestCompressionMask() | WebSocketCompressionNoContextTakeoverMask(); +} + +constexpr uint32_t RawWebSocketOptions(HCWebSocketOptions options) noexcept +{ + return static_cast(options); +} + +constexpr bool HasUnsupportedWebSocketOptions(HCWebSocketOptions options) noexcept +{ + return (RawWebSocketOptions(options) & ~WebSocketSupportedOptionsMask()) != 0; +} + +constexpr bool RequestsLegacyWebSocketSemantics(HCWebSocketOptions options) noexcept +{ + return (RawWebSocketOptions(options) & WebSocketLegacySemanticsMask()) != 0; +} + +constexpr bool RequestsWebSocketCompression(HCWebSocketOptions options) noexcept +{ + return (RawWebSocketOptions(options) & WebSocketRequestCompressionMask()) != 0; +} + +constexpr bool HasNoContextTakeoverWebSocketOptions(HCWebSocketOptions options) noexcept +{ + return (RawWebSocketOptions(options) & WebSocketCompressionNoContextTakeoverMask()) != 0; +} + +NAMESPACE_XBOX_HTTP_CLIENT_END diff --git a/Source/WebSocket/websocket_publics.cpp b/Source/WebSocket/websocket_publics.cpp index 173c309a..cab8ee76 100644 --- a/Source/WebSocket/websocket_publics.cpp +++ b/Source/WebSocket/websocket_publics.cpp @@ -39,8 +39,7 @@ STDAPI HCWebSocketSetBinaryMessageFragmentEventFunction( try { RETURN_HR_IF(E_INVALIDARG, !handle); - handle->SetBinaryMessageFragmentEventFunction(binaryMessageFragmentFunc); - return S_OK; + return handle->SetBinaryMessageFragmentEventFunction(binaryMessageFragmentFunc); } CATCH_RETURN() @@ -89,6 +88,19 @@ try } CATCH_RETURN() +#if HC_PLATFORM != HC_PLATFORM_ANDROID +STDAPI HCWebSocketSetOptions( + _In_ HCWebsocketHandle handle, + _In_ HCWebSocketOptions options +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle); + return handle->websocket->SetOptions(options); +} +CATCH_RETURN() +#endif + STDAPI HCWebSocketConnectAsync( _In_z_ const char* uri, _In_z_ const char* subProtocol, @@ -141,6 +153,18 @@ try } CATCH_RETURN() +STDAPI HCWebSocketDisconnectWithStatus( + _In_ HCWebsocketHandle handle, + _In_ HCWebSocketCloseStatus closeStatus +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle); + return handle->websocket->Disconnect(closeStatus); +} +CATCH_RETURN() + +#if HC_PLATFORM != HC_PLATFORM_ANDROID STDAPI HCWebSocketSetMaxReceiveBufferSize( _In_ HCWebsocketHandle handle, _In_ size_t bufferSizeInBytes @@ -151,6 +175,7 @@ try return handle->websocket->SetMaxReceiveBufferSize(bufferSizeInBytes); } CATCH_RETURN() +#endif STDAPI_(HCWebsocketHandle) HCWebSocketDuplicateHandle( _In_ HCWebsocketHandle handle @@ -292,6 +317,43 @@ try } CATCH_RETURN() +STDAPI HCWebSocketGetResponseHeader( + _In_ HCWebsocketHandle handle, + _In_z_ const char* headerName, + _Out_ const char** headerValue +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle || !headerName || !headerValue); + return handle->websocket->GetResponseHeader(headerName, headerValue); +} +CATCH_RETURN() + +STDAPI HCWebSocketGetNumResponseHeaders( + _In_ HCWebsocketHandle handle, + _Out_ uint32_t* numHeaders +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle || !numHeaders); + *numHeaders = handle->websocket->GetNumResponseHeaders(); + return S_OK; +} +CATCH_RETURN() + +STDAPI HCWebSocketGetResponseHeaderAtIndex( + _In_ HCWebsocketHandle handle, + _In_ uint32_t headerIndex, + _Out_ const char** headerName, + _Out_ const char** headerValue +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle || !headerName || !headerValue); + return handle->websocket->GetResponseHeaderAtIndex(headerIndex, headerName, headerValue); +} +CATCH_RETURN() + STDAPI HCWebSocketGetPingInterval( _In_ HCWebsocketHandle handle, _Out_ uint32_t* pingIntervalSeconds diff --git a/Utilities/FrameworkResources/exports.exp b/Utilities/FrameworkResources/exports.exp index d107f9b3..8c7ba717 100644 --- a/Utilities/FrameworkResources/exports.exp +++ b/Utilities/FrameworkResources/exports.exp @@ -54,6 +54,7 @@ _HCWebSocketCreate _HCWebSocketSetPingInterval _HCWebSocketSetProxyUri _HCWebSocketSetHeader +_HCWebSocketSetOptions _HCWebSocketGetEventFunctions _HCWebSocketGetPingInterval _HCWebSocketConnectAsync @@ -61,7 +62,11 @@ _HCGetWebSocketConnectResult _HCWebSocketSendMessageAsync _HCWebSocketSendBinaryMessageAsync _HCGetWebSocketSendMessageResult +_HCWebSocketGetResponseHeader +_HCWebSocketGetResponseHeaderAtIndex +_HCWebSocketGetNumResponseHeaders _HCWebSocketDisconnect +_HCWebSocketDisconnectWithStatus _HCWebSocketDuplicateHandle _HCWebSocketCloseHandle From 818751b0ba4871202037efcefe01912411605fa7 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 19:11:04 -0700 Subject: [PATCH 04/24] Add WebSocket samples and compression validation coverage Add the local echo-server sample, the focused Win32 compression test projects, and the unit-test updates that lock down the final options contract. Keep the review surface centered on compression, certificate validation, and explicit options behavior by dropping the now-redundant standalone semantics harness from the branch. --- .gitignore | 5 +- .../WebSocketEchoServer.cpp | 64 +- .../WebSocketEchoServer.vcxproj | 28 +- .../WebSocketEchoServer.vcxproj.filters | 26 +- Tests/UnitTests/Tests/WebsocketTests.cpp | 476 +++++ ...tCompressionIntegrationTests.Win32.vcxproj | 72 + .../WebSocketCompressionTests.Win32.vcxproj | 71 + .../WebSocketCompressionTests.cpp | 1781 +++++++++++++++++ libHttpClient.vs2019.sln | 21 - libHttpClient.vs2022.sln | 21 - 10 files changed, 2518 insertions(+), 47 deletions(-) create mode 100644 Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj create mode 100644 Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj create mode 100644 Tests/WebSocketCompression/WebSocketCompressionTests.cpp diff --git a/.gitignore b/.gitignore index 5e6e31f5..55d4c2f4 100644 --- a/.gitignore +++ b/.gitignore @@ -157,4 +157,7 @@ Build/**/*.log packages/ #Exclude copied headers -Samples/GDK-Http/httpClient \ No newline at end of file +Samples/GDK-Http/httpClient + +# Keep working planning / handoff docs out of commits +.github/working/ diff --git a/Samples/WebSocketEchoServer/WebSocketEchoServer.cpp b/Samples/WebSocketEchoServer/WebSocketEchoServer.cpp index ede6a462..c57c08c5 100644 --- a/Samples/WebSocketEchoServer/WebSocketEchoServer.cpp +++ b/Samples/WebSocketEchoServer/WebSocketEchoServer.cpp @@ -9,11 +9,50 @@ #define ASIO_ERROR_CATEGORY_NOEXCEPT noexcept(true) #endif // (_MSC_VER >= 1900) +#include + #include +#include #include #include -typedef websocketpp::server server; +struct echo_server_config : public websocketpp::config::asio +{ + typedef echo_server_config type; + typedef websocketpp::config::asio base; + + typedef base::concurrency_type concurrency_type; + typedef base::request_type request_type; + typedef base::response_type response_type; + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config + { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::basic_socket::endpoint socket_type; + }; + + typedef websocketpp::transport::asio::endpoint transport_type; + + struct permessage_deflate_config + { + static const bool allow_disabling_context_takeover = true; + static const uint8_t minimum_outgoing_window_bits = 8; + }; + + typedef websocketpp::extensions::permessage_deflate::enabled permessage_deflate_type; +}; + +typedef websocketpp::server server; using websocketpp::lib::placeholders::_1; using websocketpp::lib::placeholders::_2; @@ -48,6 +87,26 @@ void on_message(server* s, websocketpp::connection_hdl hdl, message_ptr msg) } } +void on_open(server* s, websocketpp::connection_hdl hdl) +{ + try + { + auto connection = s->get_con_from_hdl(hdl); + auto const& requestedExtensions = connection->get_request_header("Sec-WebSocket-Extensions"); + auto const& negotiatedExtensions = connection->get_response_header("Sec-WebSocket-Extensions"); + + std::cout << "client connected; requested extensions: " + << (requestedExtensions.empty() ? "" : requestedExtensions) + << "; negotiated extensions: " + << (negotiatedExtensions.empty() ? "" : negotiatedExtensions) + << std::endl; + } + catch (websocketpp::exception const& e) + { + std::cout << "Failed to inspect negotiated extensions: (" << e.what() << ")" << std::endl; + } +} + int main() { // Create a server endpoint @@ -64,6 +123,7 @@ int main() // Register our message handler echo_server.set_message_handler(bind(&on_message, &echo_server, ::_1, ::_2)); + echo_server.set_open_handler(bind(&on_open, &echo_server, ::_1)); // Listen on port 9002 echo_server.listen(9002); @@ -71,6 +131,8 @@ int main() // Start the server accept loop echo_server.start_accept(); + std::cout << "Listening on ws://127.0.0.1:9002 with permessage-deflate negotiation enabled." << std::endl; + // Start the ASIO io_service run loop echo_server.run(); } diff --git a/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj b/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj index d5a25c04..cc797afe 100644 --- a/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj +++ b/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj @@ -22,7 +22,7 @@ Level3 WIN32;_CONSOLE;ASIO_STANDALONE;%(PreprocessorDefinitions) - $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp + $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp;$(HCRoot)\External\zlib;%(AdditionalIncludeDirectories) NotUsing @@ -42,6 +42,30 @@
+ + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + - \ No newline at end of file + diff --git a/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj.filters b/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj.filters index 0e009382..d4c37c2a 100644 --- a/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj.filters +++ b/Samples/WebSocketEchoServer/WebSocketEchoServer.vcxproj.filters @@ -18,5 +18,29 @@ Source Files + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + - \ No newline at end of file + diff --git a/Tests/UnitTests/Tests/WebsocketTests.cpp b/Tests/UnitTests/Tests/WebsocketTests.cpp index 5a789650..3fed010f 100644 --- a/Tests/UnitTests/Tests/WebsocketTests.cpp +++ b/Tests/UnitTests/Tests/WebsocketTests.cpp @@ -8,6 +8,7 @@ #include "Utils.h" #include "../global/global.h" #include "WebSocket/hcwebsocket.h" +#include "../../Source/WebSocket/Websocketpp/websocketpp_websocket.h" #include #pragma warning(disable:4389) @@ -87,6 +88,21 @@ void CALLBACK Internal_HCWebSocketBinaryMessage( UNREFERENCED_PARAMETER(context); } +void CALLBACK Internal_HCWebSocketBinaryMessageFragment( + _In_ HCWebsocketHandle websocket, + _In_reads_bytes_(payloadSize) const uint8_t* payloadBytes, + _In_ uint32_t payloadSize, + _In_ bool isLastFragment, + _In_ void* context + ) +{ + UNREFERENCED_PARAMETER(websocket); + UNREFERENCED_PARAMETER(payloadBytes); + UNREFERENCED_PARAMETER(payloadSize); + UNREFERENCED_PARAMETER(isLastFragment); + UNREFERENCED_PARAMETER(context); +} + void CALLBACK Internal_HCWebSocketCloseEvent( _In_ HCWebsocketHandle websocket, _In_ HCWebSocketCloseStatus closeStatus, @@ -363,6 +379,7 @@ HRESULT CALLBACK Test_Internal_HCWebSocketSendBinaryMessageAsync( } bool g_HCWebSocketDisconnect_Called = false; +HCWebSocketCloseStatus g_HCWebSocketDisconnect_CloseStatus = HCWebSocketCloseStatus::Normal; HRESULT CALLBACK Test_Internal_HCWebSocketDisconnect( _In_ HCWebsocketHandle websocket, _In_ HCWebSocketCloseStatus closeStatus, @@ -372,6 +389,7 @@ HRESULT CALLBACK Test_Internal_HCWebSocketDisconnect( UNREFERENCED_PARAMETER(context); g_HCWebSocketDisconnect_Called = true; + g_HCWebSocketDisconnect_CloseStatus = closeStatus; // Simulate proper disconnect by calling the close callback // This is needed for cleanup to work properly @@ -386,6 +404,148 @@ HRESULT CALLBACK Test_Internal_HCWebSocketDisconnect( return S_OK; } +constexpr HCWebSocketOptions CombineCompressionOptions( + HCWebSocketOptions lhs, + HCWebSocketOptions rhs) noexcept +{ + return static_cast( + static_cast(lhs) | + static_cast(rhs)); +} + +class UnsupportedOptionsTestProvider final : public IWebSocketProvider +{ +public: + HRESULT ConnectAsync( + xbox::httpclient::String const& uri, + xbox::httpclient::String const& subprotocol, + HCWebsocketHandle websocketHandle, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(uri); + UNREFERENCED_PARAMETER(subprotocol); + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT SendAsync( + HCWebsocketHandle websocketHandle, + const char* message, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(message); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT SendBinaryAsync( + HCWebsocketHandle websocketHandle, + const uint8_t* payloadBytes, + uint32_t payloadSize, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(payloadBytes); + UNREFERENCED_PARAMETER(payloadSize); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT Disconnect( + HCWebsocketHandle websocketHandle, + HCWebSocketCloseStatus closeStatus) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(closeStatus); + return E_NOTIMPL; + } + + HRESULT OptionsResult(HCWebSocketOptions options) const noexcept override + { + UNREFERENCED_PARAMETER(options); + return E_NOT_SUPPORTED; + } +}; + +class ConfigurableCompressionTestProvider final : public IWebSocketProvider +{ +public: + HRESULT ConnectAsync( + xbox::httpclient::String const& uri, + xbox::httpclient::String const& subprotocol, + HCWebsocketHandle websocketHandle, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(uri); + UNREFERENCED_PARAMETER(subprotocol); + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT SendAsync( + HCWebsocketHandle websocketHandle, + const char* message, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(message); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT SendBinaryAsync( + HCWebsocketHandle websocketHandle, + const uint8_t* payloadBytes, + uint32_t payloadSize, + XAsyncBlock* async) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(payloadBytes); + UNREFERENCED_PARAMETER(payloadSize); + UNREFERENCED_PARAMETER(async); + return E_NOTIMPL; + } + + HRESULT Disconnect( + HCWebsocketHandle websocketHandle, + HCWebSocketCloseStatus closeStatus) noexcept override + { + UNREFERENCED_PARAMETER(websocketHandle); + UNREFERENCED_PARAMETER(closeStatus); + return E_NOTIMPL; + } + + HRESULT OptionsResult(HCWebSocketOptions options) const noexcept override + { + constexpr uint32_t requestCompression = static_cast(HCWebSocketOptions::RequestCompression); + constexpr uint32_t noContextTakeoverMask = + static_cast(HCWebSocketOptions::CompressionServerNoContextTakeover) | + static_cast(HCWebSocketOptions::CompressionClientNoContextTakeover); + constexpr uint32_t legacySemantics = static_cast(HCWebSocketOptions::LegacySemantics); + auto const rawOptions = static_cast(options); + + if ((rawOptions & legacySemantics) != 0) + { + return S_OK; + } + + if ((rawOptions & ~(requestCompression | noContextTakeoverMask)) != 0) + { + return E_NOT_SUPPORTED; + } + + if ((rawOptions & noContextTakeoverMask) != 0 && (rawOptions & requestCompression) == 0) + { + return E_NOT_SUPPORTED; + } + + return S_OK; + } +}; + DEFINE_TEST_CLASS(WebsocketTests) { public: @@ -484,8 +644,10 @@ DEFINE_TEST_CLASS(WebsocketTests) VERIFY_ARE_EQUAL(true, g_HCWebSocketSendMessage_Called); VERIFY_ARE_EQUAL(false, g_HCWebSocketDisconnect_Called); + g_HCWebSocketDisconnect_CloseStatus = HCWebSocketCloseStatus::UnknownError; VERIFY_ARE_EQUAL(S_OK, HCWebSocketDisconnect(websocket)); VERIFY_ARE_EQUAL(true, g_HCWebSocketDisconnect_Called); + VERIFY_ARE_EQUAL(static_cast(HCWebSocketCloseStatus::Normal), static_cast(g_HCWebSocketDisconnect_CloseStatus)); VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(websocket)); HCCleanup(); @@ -547,6 +709,251 @@ DEFINE_TEST_CLASS(WebsocketTests) #endif } + DEFINE_TEST_CASE(TestDisconnectWithStatus) + { + VERIFY_ARE_EQUAL(S_OK, HCSetWebSocketFunctions(Test_Internal_HCWebSocketConnectAsync, Test_Internal_HCWebSocketSendMessageAsync, Test_Internal_HCWebSocketSendBinaryMessageAsync, Test_Internal_HCWebSocketDisconnect, nullptr)); + VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); + + HCWebsocketHandle websocket = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCreate(&websocket, nullptr, nullptr, nullptr, nullptr)); + VERIFY_IS_NOT_NULL(websocket); + + XAsyncBlock asyncBlock{}; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketConnectAsync("test", "subProtoTest", websocket, &asyncBlock)); + VERIFY_SUCCEEDED(XAsyncGetStatus(&asyncBlock, true)); + + g_HCWebSocketDisconnect_Called = false; + g_HCWebSocketDisconnect_CloseStatus = HCWebSocketCloseStatus::Normal; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketDisconnectWithStatus(websocket, HCWebSocketCloseStatus::GoingAway)); + VERIFY_ARE_EQUAL(true, g_HCWebSocketDisconnect_Called); + VERIFY_ARE_EQUAL(static_cast(HCWebSocketCloseStatus::GoingAway), static_cast(g_HCWebSocketDisconnect_CloseStatus)); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(websocket)); + HCCleanup(); + } + + DEFINE_TEST_CASE(TestCompressionOptions) + { + auto const legacySemantics = HCWebSocketOptions::LegacySemantics; + auto const requestCompression = HCWebSocketOptions::RequestCompression; + auto const serverNoContextTakeover = HCWebSocketOptions::CompressionServerNoContextTakeover; + auto const clientNoContextTakeover = HCWebSocketOptions::CompressionClientNoContextTakeover; + auto const requestWithServerNoContextTakeover = CombineCompressionOptions(requestCompression, serverNoContextTakeover); + auto const requestWithClientNoContextTakeover = CombineCompressionOptions(requestCompression, clientNoContextTakeover); + auto const requestWithBothNoContextTakeover = CombineCompressionOptions(requestWithServerNoContextTakeover, clientNoContextTakeover); + + VERIFY_ARE_EQUAL(S_OK, HCSetWebSocketFunctions(Test_Internal_HCWebSocketConnectAsync, Test_Internal_HCWebSocketSendMessageAsync, Test_Internal_HCWebSocketSendBinaryMessageAsync, Test_Internal_HCWebSocketDisconnect, nullptr)); + VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); + + HCWebsocketHandle websocket = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCreate(&websocket, nullptr, nullptr, nullptr, nullptr)); + VERIFY_IS_NOT_NULL(websocket); + VERIFY_IS_TRUE(websocket->websocket->UsesLegacySemantics()); + VERIFY_IS_FALSE(websocket->websocket->OptionsExplicitlySet()); + + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(nullptr, HCWebSocketOptions::None)); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, static_cast(0x8))); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, CombineCompressionOptions(legacySemantics, requestCompression))); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, serverNoContextTakeover)); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, clientNoContextTakeover)); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, CombineCompressionOptions(serverNoContextTakeover, clientNoContextTakeover))); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, legacySemantics)); + VERIFY_ARE_EQUAL(static_cast(legacySemantics), static_cast(websocket->websocket->Options())); + VERIFY_IS_TRUE(websocket->websocket->UsesLegacySemantics()); + VERIFY_IS_TRUE(websocket->websocket->OptionsExplicitlySet()); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, HCWebSocketOptions::None)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, requestCompression)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, requestWithServerNoContextTakeover)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, requestWithClientNoContextTakeover)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, requestWithBothNoContextTakeover)); + + XAsyncBlock asyncBlock{}; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketConnectAsync("test", "subProtoTest", websocket, &asyncBlock)); + VERIFY_SUCCEEDED(XAsyncGetStatus(&asyncBlock, true)); + VERIFY_ARE_EQUAL(E_HC_CONNECT_ALREADY_CALLED, HCWebSocketSetOptions(websocket, requestWithBothNoContextTakeover)); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketDisconnect(websocket)); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(websocket)); + HCCleanup(); + } + + DEFINE_TEST_CASE(TestLegacySemanticsNoOp) + { + auto const requestCompression = HCWebSocketOptions::RequestCompression; + auto const legacySemantics = HCWebSocketOptions::LegacySemantics; + + UnsupportedOptionsTestProvider provider; + auto observer = HC_WEBSOCKET_OBSERVER::Initialize( + std::make_shared(1, provider), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + HCWebsocketHandle websocket = observer.get(); + + VERIFY_IS_TRUE(websocket->websocket->UsesLegacySemantics()); + VERIFY_IS_FALSE(websocket->websocket->OptionsExplicitlySet()); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, legacySemantics)); + VERIFY_ARE_EQUAL( + static_cast(legacySemantics), + static_cast(websocket->websocket->Options())); + VERIFY_IS_TRUE(websocket->websocket->UsesLegacySemantics()); + VERIFY_IS_TRUE(websocket->websocket->OptionsExplicitlySet()); + + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocket, requestCompression)); + VERIFY_ARE_EQUAL( + static_cast(legacySemantics), + static_cast(websocket->websocket->Options())); + } + + DEFINE_TEST_CASE(TestCompressionOptionsConfigurableProvider) + { + auto const legacySemantics = HCWebSocketOptions::LegacySemantics; + auto const requestCompression = HCWebSocketOptions::RequestCompression; + auto const requestWithServerNoContextTakeover = CombineCompressionOptions(requestCompression, HCWebSocketOptions::CompressionServerNoContextTakeover); + auto const requestWithClientNoContextTakeover = CombineCompressionOptions(requestCompression, HCWebSocketOptions::CompressionClientNoContextTakeover); + auto const requestWithBothNoContextTakeover = CombineCompressionOptions(requestWithServerNoContextTakeover, HCWebSocketOptions::CompressionClientNoContextTakeover); + + ConfigurableCompressionTestProvider provider; + auto observer = HC_WEBSOCKET_OBSERVER::Initialize( + std::make_shared(1, provider), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + HCWebsocketHandle websocket = observer.get(); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, legacySemantics)); + VERIFY_ARE_EQUAL( + static_cast(legacySemantics), + static_cast(websocket->websocket->Options())); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, HCWebSocketOptions::None)); + VERIFY_ARE_EQUAL( + static_cast(HCWebSocketOptions::None), + static_cast(websocket->websocket->Options())); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, requestCompression)); + VERIFY_ARE_EQUAL( + static_cast(requestCompression), + static_cast(websocket->websocket->Options())); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, requestWithServerNoContextTakeover)); + VERIFY_ARE_EQUAL( + static_cast(requestWithServerNoContextTakeover), + static_cast(websocket->websocket->Options())); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, requestWithClientNoContextTakeover)); + VERIFY_ARE_EQUAL( + static_cast(requestWithClientNoContextTakeover), + static_cast(websocket->websocket->Options())); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, requestWithBothNoContextTakeover)); + VERIFY_ARE_EQUAL( + static_cast(requestWithBothNoContextTakeover), + static_cast(websocket->websocket->Options())); + VERIFY_IS_TRUE(websocket->websocket->OptionsExplicitlySet()); + } + + DEFINE_TEST_CASE(TestWebSocketppGdkProxyDecryptsHttpsPolicyHelpers) + { + VERIFY_IS_TRUE(ShouldForceTlsValidationForGdkSandbox(E_FAIL, "CERT")); + VERIFY_IS_TRUE(ShouldForceTlsValidationForGdkSandbox(S_OK, "RETAIL")); + VERIFY_IS_FALSE(ShouldForceTlsValidationForGdkSandbox(S_OK, "CERT")); + + VERIFY_IS_TRUE(ApplyTlsValidationBackstopForGdkConsole(true, true)); + VERIFY_IS_FALSE(ApplyTlsValidationBackstopForGdkConsole(false, true)); + VERIFY_IS_FALSE(ApplyTlsValidationBackstopForGdkConsole(true, false)); + VERIFY_IS_FALSE(ApplyTlsValidationBackstopForGdkConsole(false, false)); + } + +#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK + DEFINE_TEST_CASE(TestDeterministicOptionsRejectFragmentHandlers) + { + ConfigurableCompressionTestProvider provider; + + auto observer = HC_WEBSOCKET_OBSERVER::Initialize( + std::make_shared(1, provider), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + HCWebsocketHandle websocket = observer.get(); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, HCWebSocketOptions::None)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetBinaryMessageFragmentEventFunction(websocket, Internal_HCWebSocketBinaryMessageFragment)); + + auto observerWithFragment = HC_WEBSOCKET_OBSERVER::Initialize( + std::make_shared(2, provider), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + HCWebsocketHandle websocketWithFragment = observerWithFragment.get(); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetBinaryMessageFragmentEventFunction(websocketWithFragment, Internal_HCWebSocketBinaryMessageFragment)); + VERIFY_ARE_EQUAL(E_NOT_SUPPORTED, HCWebSocketSetOptions(websocketWithFragment, HCWebSocketOptions::None)); + } + + DEFINE_TEST_CASE(TestMaxReceiveBufferSizePreservedAcrossSetOptions) + { + ConfigurableCompressionTestProvider provider; + auto observer = HC_WEBSOCKET_OBSERVER::Initialize( + std::make_shared(1, provider), + nullptr, + nullptr, + nullptr, + nullptr, + nullptr); + HCWebsocketHandle websocket = observer.get(); + + constexpr size_t configuredReceiveBufferSize{ 4096 }; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetMaxReceiveBufferSize(websocket, configuredReceiveBufferSize)); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, HCWebSocketOptions::None)); + VERIFY_ARE_EQUAL(configuredReceiveBufferSize, websocket->websocket->MaxReceiveBufferSize()); + VERIFY_IS_TRUE(websocket->websocket->MaxReceiveBufferSizeExplicitlySet()); + } + + DEFINE_TEST_CASE(TestMaxReceiveBufferSizeValidation) + { + VERIFY_ARE_EQUAL(S_OK, HCSetWebSocketFunctions(Test_Internal_HCWebSocketConnectAsync, Test_Internal_HCWebSocketSendMessageAsync, Test_Internal_HCWebSocketSendBinaryMessageAsync, Test_Internal_HCWebSocketDisconnect, nullptr)); + VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); + + HCWebsocketHandle websocket = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCreate(&websocket, nullptr, nullptr, nullptr, nullptr)); + VERIFY_IS_NOT_NULL(websocket); + + constexpr size_t initialReceiveBufferSize{ 4096 }; + constexpr size_t updatedReceiveBufferSize{ 8192 }; + + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetMaxReceiveBufferSize(nullptr, initialReceiveBufferSize)); + + if (sizeof(size_t) > sizeof(uint32_t)) + { + size_t const oversizedReceiveBufferSize = static_cast(UINT32_MAX) + 1u; + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetMaxReceiveBufferSize(websocket, oversizedReceiveBufferSize)); + } + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetMaxReceiveBufferSize(websocket, initialReceiveBufferSize)); + VERIFY_ARE_EQUAL(initialReceiveBufferSize, websocket->websocket->MaxReceiveBufferSize()); + + XAsyncBlock asyncBlock{}; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketConnectAsync("test", "subProtoTest", websocket, &asyncBlock)); + VERIFY_SUCCEEDED(XAsyncGetStatus(&asyncBlock, true)); + + VERIFY_ARE_EQUAL(E_HC_CONNECT_ALREADY_CALLED, HCWebSocketSetMaxReceiveBufferSize(websocket, updatedReceiveBufferSize)); + VERIFY_ARE_EQUAL(initialReceiveBufferSize, websocket->websocket->MaxReceiveBufferSize()); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketDisconnect(websocket)); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(websocket)); + HCCleanup(); + } +#endif DEFINE_TEST_CASE(TestRequestHeaders) { @@ -595,6 +1002,75 @@ DEFINE_TEST_CLASS(WebsocketTests) HCCleanup(); } + DEFINE_TEST_CASE(TestResponseHeaders) + { + DEFINE_TEST_CASE_PROPERTIES(TestResponseHeaders); + VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); + HCWebsocketHandle call = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCreate(&call, nullptr, nullptr, nullptr, nullptr)); + + uint32_t numHeaders = 1; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetNumResponseHeaders(call, &numHeaders)); + VERIFY_ARE_EQUAL(0, numHeaders); + + const CHAR* headerValue = "sentinel"; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeader(call, "testHeader", &headerValue)); + VERIFY_IS_NULL(headerValue); + + const CHAR* headerName = "sentinel"; + headerValue = "sentinel"; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeaderAtIndex(call, 0, &headerName, &headerValue)); + VERIFY_IS_NULL(headerName); + VERIFY_IS_NULL(headerValue); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(call)); + HCCleanup(); + } + + DEFINE_TEST_CASE(TestResponseHeadersAccessors) + { + DEFINE_TEST_CASE_PROPERTIES(TestResponseHeadersAccessors); + VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); + HCWebsocketHandle call = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCreate(&call, nullptr, nullptr, nullptr, nullptr)); + + HttpHeaders responseHeaders; + responseHeaders["aHeader"] = "aValue"; + responseHeaders["bHeader"] = "bValue"; + VERIFY_ARE_EQUAL(S_OK, call->websocket->SetResponseHeaders(std::move(responseHeaders))); + + uint32_t numHeaders = 0; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetNumResponseHeaders(call, &numHeaders)); + VERIFY_ARE_EQUAL(2, numHeaders); + + const CHAR* headerValue = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeader(call, "aHeader", &headerValue)); + VERIFY_ARE_EQUAL_STR("aValue", headerValue); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeader(call, "bHeader", &headerValue)); + VERIFY_ARE_EQUAL_STR("bValue", headerValue); + + const CHAR* headerName = nullptr; + const CHAR* indexedHeaderValue = nullptr; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeaderAtIndex(call, 0, &headerName, &indexedHeaderValue)); + VERIFY_ARE_EQUAL_STR("aHeader", headerName); + VERIFY_ARE_EQUAL_STR("aValue", indexedHeaderValue); + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeaderAtIndex(call, 1, &headerName, &indexedHeaderValue)); + VERIFY_ARE_EQUAL_STR("bHeader", headerName); + VERIFY_ARE_EQUAL_STR("bValue", indexedHeaderValue); + + call->websocket->ClearResponseHeaders(); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetNumResponseHeaders(call, &numHeaders)); + VERIFY_ARE_EQUAL(0, numHeaders); + + headerValue = "sentinel"; + VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeader(call, "aHeader", &headerValue)); + VERIFY_IS_NULL(headerValue); + + VERIFY_ARE_EQUAL(S_OK, HCWebSocketCloseHandle(call)); + HCCleanup(); + } + }; NAMESPACE_XBOX_HTTP_CLIENT_TEST_END diff --git a/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj b/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj new file mode 100644 index 00000000..ac6eba9e --- /dev/null +++ b/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj @@ -0,0 +1,72 @@ + + + + {F5D76018-3E61-4C5E-9C26-6A49A9C24E73} + Application + + + Spectre + + + Spectre + + + Spectre + + + Spectre + + + + + + Level3 + WIN32;_CONSOLE;ASIO_STANDALONE;HC_ENABLE_WSS_CERT_STORE_TESTS=1;%(PreprocessorDefinitions) + $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp;$(HCRoot)\External\boost-wintls\include;$(HCRoot)\External\zlib;%(AdditionalIncludeDirectories) + NotUsing + + + Console + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + false + + + + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + + + diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj b/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj new file mode 100644 index 00000000..a5fb97e0 --- /dev/null +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj @@ -0,0 +1,71 @@ + + + + {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1} + Application + + + Spectre + + + Spectre + + + Spectre + + + Spectre + + + + + + Level3 + WIN32;_CONSOLE;ASIO_STANDALONE;%(PreprocessorDefinitions) + $(HCRoot)\External\asio\asio\include;$(HCRoot)\External\websocketpp;$(HCRoot)\External\zlib;%(AdditionalIncludeDirectories) + NotUsing + + + Console + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + false + + + + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + false + + + + diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp new file mode 100644 index 00000000..8ddc176a --- /dev/null +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp @@ -0,0 +1,1781 @@ +// Copyright (c) Microsoft Corporation +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#define _WEBSOCKETPP_CPP11_SYSTEM_ERROR_ + +#pragma warning( push ) +#pragma warning( disable : 4100 4127 4512 4996 4701 4267 ) +#define _WEBSOCKETPP_CPP11_STL_ +#define _WEBSOCKETPP_CONSTEXPR_TOKEN_ +#define _SCL_SECURE_NO_WARNINGS + +#if defined(_MSC_VER) && (_MSC_VER >= 1900) +#define ASIO_ERROR_CATEGORY_NOEXCEPT noexcept(true) +#endif + +#include +#include +#include +#include + +#include + +// Trust-store manipulation can trigger Windows confirmation UI, so those cases stay in the +// dedicated Win32 integration-test binary rather than the default CI-facing test binary. +#if defined(HC_ENABLE_WSS_CERT_STORE_TESTS) && HC_PLATFORM == HC_PLATFORM_WIN32 +#define HC_RUN_WSS_CERT_STORE_TESTS 1 +#else +#define HC_RUN_WSS_CERT_STORE_TESTS 0 +#endif + +#if HC_RUN_WSS_CERT_STORE_TESTS +#include "../../External/boost-wintls/test/certificate.hpp" +#include "../../Source/WebSocket/Websocketpp/wintls_socket.hpp" +#endif + +#include "../../Source/WebSocket/Websocketpp/websocketpp_disabled_permessage_deflate.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +constexpr int SkipReturnCode = 125; +constexpr uint16_t TestPort = 39002; +#if HC_RUN_WSS_CERT_STORE_TESTS +constexpr uint16_t WssTestPort = 39004; +#endif +constexpr std::chrono::seconds Timeout{ 10 }; + +enum class CompressionExpectation +{ + Negotiated, + Unsupported +}; + +constexpr HCWebSocketOptions CombineCompressionOptions( + HCWebSocketOptions lhs, + HCWebSocketOptions rhs) noexcept +{ + return static_cast( + static_cast(lhs) | + static_cast(rhs)); +} + +constexpr size_t ReceiveBufferSize = 4096; + +void PrintHr(char const* operation, HRESULT hr) +{ + std::printf("%s: 0x%08x\n", operation, static_cast(hr)); +} + +bool HeaderContainsToken(std::string const& headerValue, char const* token) +{ + return headerValue.find(token) != std::string::npos; +} + +bool ValidateRequestedCompressionExtensionHeader( + std::string const& headerValue, + char const* headerLabel, + char const* scenarioLabel, + bool expectServerNoContextTakeover, + bool expectClientNoContextTakeover) +{ + if (!HeaderContainsToken(headerValue, "permessage-deflate")) + { + std::printf("[FAIL] %s did not advertise permessage-deflate for %s.\n", headerLabel, scenarioLabel); + return false; + } + + bool const hasServerNoContextTakeover = HeaderContainsToken(headerValue, "server_no_context_takeover"); + if (hasServerNoContextTakeover != expectServerNoContextTakeover) + { + std::printf( + "[FAIL] %s %s server_no_context_takeover for %s.\n", + headerLabel, + expectServerNoContextTakeover ? "did not advertise" : "unexpectedly advertised", + scenarioLabel); + return false; + } + + bool const hasClientNoContextTakeover = HeaderContainsToken(headerValue, "client_no_context_takeover"); + if (hasClientNoContextTakeover != expectClientNoContextTakeover) + { + std::printf( + "[FAIL] %s %s client_no_context_takeover for %s.\n", + headerLabel, + expectClientNoContextTakeover ? "did not advertise" : "unexpectedly advertised", + scenarioLabel); + return false; + } + + return true; +} + +bool ValidateNegotiatedCompressionExtensionHeader( + std::string const& headerValue, + char const* headerLabel, + char const* scenarioLabel) +{ + if (!HeaderContainsToken(headerValue, "permessage-deflate")) + { + std::printf("[FAIL] %s did not advertise permessage-deflate for %s.\n", headerLabel, scenarioLabel); + return false; + } + + return true; +} + +struct ScopeGuard +{ + explicit ScopeGuard(std::function cleanup) : m_cleanup(std::move(cleanup)) {} + + ~ScopeGuard() + { + if (m_cleanup) + { + m_cleanup(); + } + } + + ScopeGuard(ScopeGuard const&) = delete; + ScopeGuard& operator=(ScopeGuard const&) = delete; + +private: + std::function m_cleanup; +}; + +struct CapturedTraceMessage +{ + std::string area; + HCTraceLevel level; + std::string message; +}; + +class ScopedTraceCapture +{ +public: + ScopedTraceCapture() + { + m_restoreTraceLevel = SUCCEEDED(HCSettingsGetTraceLevel(&m_previousTraceLevel)); + + { + std::lock_guard lock(s_callbackMutex); + s_activeCapture = this; + HCTraceSetClientCallback(&ScopedTraceCapture::TraceCallback); + } + + HCSettingsSetTraceLevel(HCTraceLevel::Verbose); + } + + ~ScopedTraceCapture() + { + { + std::lock_guard lock(s_callbackMutex); + if (s_activeCapture == this) + { + s_activeCapture = nullptr; + HCTraceSetClientCallback(nullptr); + } + } + + if (m_restoreTraceLevel) + { + HCSettingsSetTraceLevel(m_previousTraceLevel); + } + } + + ScopedTraceCapture(ScopedTraceCapture const&) = delete; + ScopedTraceCapture& operator=(ScopedTraceCapture const&) = delete; + + bool ContainsWebsocketConnectError(uint32_t expectedPlatformErrorCode) const + { + auto const expectedCode = std::to_string(expectedPlatformErrorCode); + std::lock_guard lock(m_mutex); + return std::any_of(m_messages.begin(), m_messages.end(), [&expectedCode](CapturedTraceMessage const& message) + { + return message.area == "WEBSOCKET" && + message.message.find("asio async_connect error:") != std::string::npos && + message.message.find(expectedCode) != std::string::npos; + }); + } + + void DumpWebsocketMessages() const + { + std::lock_guard lock(m_mutex); + for (auto const& message : m_messages) + { + if (message.area == "WEBSOCKET") + { + std::printf("[INFO] captured trace (%d): %s\n", static_cast(message.level), message.message.c_str()); + } + } + } + +private: + static void CALLBACK TraceCallback( + _In_z_ char const* areaName, + _In_ HCTraceLevel level, + _In_ uint64_t, + _In_ uint64_t, + _In_z_ char const* message + ) noexcept + { + std::lock_guard lock(s_callbackMutex); + if (s_activeCapture == nullptr) + { + return; + } + + s_activeCapture->Append(areaName, level, message); + } + + void Append(char const* areaName, HCTraceLevel level, char const* message) + { + std::lock_guard lock(m_mutex); + m_messages.push_back(CapturedTraceMessage + { + areaName != nullptr ? areaName : "", + level, + message != nullptr ? message : "" + }); + } + + inline static std::mutex s_callbackMutex{}; + inline static ScopedTraceCapture* s_activeCapture{ nullptr }; + + mutable std::mutex m_mutex; + std::vector m_messages; + HCTraceLevel m_previousTraceLevel{}; + bool m_restoreTraceLevel{ false }; +}; + +struct CompressionServerConfig : public websocketpp::config::asio +{ + typedef CompressionServerConfig type; + typedef websocketpp::config::asio base; + + typedef base::concurrency_type concurrency_type; + typedef base::request_type request_type; + typedef base::response_type response_type; + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config + { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::basic_socket::endpoint socket_type; + }; + + typedef websocketpp::transport::asio::endpoint transport_type; + + struct permessage_deflate_config + { + static const bool allow_disabling_context_takeover = true; + static const uint8_t minimum_outgoing_window_bits = 8; + }; + + typedef websocketpp::extensions::permessage_deflate::enabled permessage_deflate_type; +}; + +#if HC_RUN_WSS_CERT_STORE_TESTS +struct WinTlsServerConfig : public websocketpp::config::core +{ + typedef WinTlsServerConfig type; + typedef websocketpp::config::core base; + + typedef base::concurrency_type concurrency_type; + typedef base::request_type request_type; + typedef base::response_type response_type; + typedef base::message_type message_type; + typedef base::con_msg_manager_type con_msg_manager_type; + typedef base::endpoint_msg_manager_type endpoint_msg_manager_type; + typedef base::alog_type alog_type; + typedef base::elog_type elog_type; + typedef base::rng_type rng_type; + + struct transport_config : public base::transport_config + { + typedef type::concurrency_type concurrency_type; + typedef type::alog_type alog_type; + typedef type::elog_type elog_type; + typedef type::request_type request_type; + typedef type::response_type response_type; + typedef websocketpp::transport::asio::wintls_socket::endpoint socket_type; + }; + + typedef websocketpp::transport::asio::endpoint transport_type; +}; +#endif + +class CompressionEchoServer +{ +public: + using server = websocketpp::server; + using message_ptr = server::message_ptr; + + bool Start(uint16_t port) + { + m_server.clear_access_channels(websocketpp::log::alevel::all); + m_server.clear_error_channels(websocketpp::log::elevel::all); + m_server.init_asio(); + m_server.set_reuse_addr(true); + m_server.set_open_handler([this](websocketpp::connection_hdl hdl) + { + auto connection = m_server.get_con_from_hdl(hdl); + { + std::lock_guard lock(m_mutex); + m_connection = hdl; + m_connected = true; + m_requestedExtensions = connection->get_request_header("Sec-WebSocket-Extensions"); + m_negotiatedExtensions = connection->get_response_header("Sec-WebSocket-Extensions"); + } + m_cv.notify_all(); + }); + m_server.set_fail_handler([this](websocketpp::connection_hdl hdl) + { + std::lock_guard lock(m_mutex); + try + { + m_error = m_server.get_con_from_hdl(hdl)->get_ec().message(); + } + catch (...) + { + m_error = "connection failed"; + } + m_cv.notify_all(); + }); + m_server.set_message_handler([this](websocketpp::connection_hdl hdl, message_ptr const& msg) + { + websocketpp::lib::error_code ec; + m_server.send(hdl, msg->get_payload(), msg->get_opcode(), ec); + if (ec) + { + std::lock_guard lock(m_mutex); + m_error = ec.message(); + m_cv.notify_all(); + } + }); + + websocketpp::lib::error_code ec; + m_server.listen(port, ec); + if (ec) + { + std::printf("[FAIL] listen failed: %s\n", ec.message().c_str()); + return false; + } + + m_server.start_accept(ec); + if (ec) + { + std::printf("[FAIL] start_accept failed: %s\n", ec.message().c_str()); + return false; + } + + m_thread = std::thread([this]() + { + try + { + m_server.run(); + } + catch (std::exception const& e) + { + std::lock_guard lock(m_mutex); + m_error = e.what(); + m_cv.notify_all(); + } + }); + + return true; + } + + void Stop() + { + websocketpp::lib::error_code ec; + m_server.stop_listening(ec); + if (m_connected) + { + m_server.close(m_connection, websocketpp::close::status::normal, "test complete", ec); + } + m_server.stop(); + + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + bool WaitForConnection() const + { + std::unique_lock lock(m_mutex); + return m_cv.wait_for(lock, Timeout, [this]() + { + return m_connected || !m_error.empty(); + }); + } + + std::string RequestedExtensions() const + { + std::lock_guard lock(m_mutex); + return m_requestedExtensions; + } + + std::string NegotiatedExtensions() const + { + std::lock_guard lock(m_mutex); + return m_negotiatedExtensions; + } + + std::string Error() const + { + std::lock_guard lock(m_mutex); + return m_error; + } + + void ResetObservedConnection() + { + std::lock_guard lock(m_mutex); + m_connected = false; + m_requestedExtensions.clear(); + m_negotiatedExtensions.clear(); + m_error.clear(); + } + +private: + mutable std::mutex m_mutex; + mutable std::condition_variable m_cv; + server m_server; + std::thread m_thread; + websocketpp::connection_hdl m_connection; + bool m_connected{ false }; + std::string m_requestedExtensions; + std::string m_negotiatedExtensions; + std::string m_error; +}; + +#if HC_RUN_WSS_CERT_STORE_TESTS +struct StoreCloser +{ + void operator()(void* store) const noexcept + { + if (store != nullptr) + { + CertCloseStore(reinterpret_cast(store), 0); + } + } +}; + +using store_ptr = std::unique_ptr; + +store_ptr OpenCurrentUserRootStore() +{ + auto store = store_ptr( + CertOpenStore( + CERT_STORE_PROV_SYSTEM_A, + 0, + 0, + CERT_SYSTEM_STORE_CURRENT_USER, + "ROOT"), + StoreCloser{}); + + if (!store) + { + throw std::runtime_error("CertOpenStore(ROOT) failed"); + } + + return store; +} + +bool CertificatesMatch(CERT_CONTEXT const* lhs, CERT_CONTEXT const* rhs) +{ + return lhs != nullptr && + rhs != nullptr && + lhs->cbCertEncoded == rhs->cbCertEncoded && + std::memcmp(lhs->pbCertEncoded, rhs->pbCertEncoded, lhs->cbCertEncoded) == 0; +} + +void RemoveMatchingCertificatesFromStore(HCERTSTORE store, CERT_CONTEXT const* cert) +{ + PCCERT_CONTEXT current = nullptr; + while ((current = CertEnumCertificatesInStore(store, current)) != nullptr) + { + if (!CertificatesMatch(current, cert)) + { + continue; + } + + PCCERT_CONTEXT duplicate = CertDuplicateCertificateContext(current); + if (duplicate == nullptr || !CertDeleteCertificateFromStore(duplicate)) + { + throw std::runtime_error("CertDeleteCertificateFromStore failed"); + } + } +} + +class CurrentUserRootCertificateScope +{ +public: + CurrentUserRootCertificateScope() = default; + + void Install(CERT_CONTEXT const* cert) + { + if (cert == nullptr) + { + throw std::runtime_error("Attempted to trust a null certificate"); + } + + auto store = OpenCurrentUserRootStore(); + RemoveMatchingCertificatesFromStore(reinterpret_cast(store.get()), cert); + if (!CertAddCertificateContextToStore(reinterpret_cast(store.get()), cert, CERT_STORE_ADD_REPLACE_EXISTING, nullptr)) + { + throw std::runtime_error("CertAddCertificateContextToStore failed"); + } + + m_installed = true; + m_cert.reset(CertDuplicateCertificateContext(cert)); + if (!m_cert) + { + throw std::runtime_error("CertDuplicateCertificateContext failed"); + } + } + + void Uninstall() + { + if (!m_installed || !m_cert) + { + return; + } + + auto store = OpenCurrentUserRootStore(); + RemoveMatchingCertificatesFromStore(reinterpret_cast(store.get()), m_cert.get()); + m_installed = false; + m_cert.reset(); + } + + ~CurrentUserRootCertificateScope() + { + try + { + Uninstall(); + } + catch (...) + { + } + } + + CurrentUserRootCertificateScope(CurrentUserRootCertificateScope const&) = delete; + CurrentUserRootCertificateScope& operator=(CurrentUserRootCertificateScope const&) = delete; + +private: + struct CertDeleter + { + void operator()(CERT_CONTEXT const* cert) const noexcept + { + if (cert != nullptr) + { + CertFreeCertificateContext(cert); + } + } + }; + + bool m_installed{ false }; + std::unique_ptr m_cert; +}; + +class ImportedServerCertificate +{ +public: + ImportedServerCertificate() + : m_keyName("libHttpClient-wss-test-key-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(GetTickCount64())) + { + wintls::error_code ignored{}; + wintls::delete_private_key(m_keyName, ignored); + + m_cert = wintls::x509_to_cert_context(wintls::net::buffer(test_certificate), wintls::file_format::pem); + wintls::import_private_key(wintls::net::buffer(test_key), wintls::file_format::pem, m_keyName); + m_privateKeyImported = true; + wintls::assign_private_key(m_cert.get(), m_keyName); + } + + ~ImportedServerCertificate() + { + if (m_privateKeyImported) + { + wintls::error_code ignored{}; + wintls::delete_private_key(m_keyName, ignored); + } + } + + ImportedServerCertificate(ImportedServerCertificate const&) = delete; + ImportedServerCertificate& operator=(ImportedServerCertificate const&) = delete; + + CERT_CONTEXT const* Context() const noexcept + { + return m_cert.get(); + } + +private: + std::string m_keyName; + bool m_privateKeyImported{ false }; + wintls::cert_context_ptr m_cert; +}; + +class WssEchoServer +{ +public: + using server = websocketpp::server; + using message_ptr = server::message_ptr; + + explicit WssEchoServer(CERT_CONTEXT const* certificate) + : m_certificate(certificate) + { + } + + bool Start(uint16_t port) + { + m_server.clear_access_channels(websocketpp::log::alevel::all); + m_server.clear_error_channels(websocketpp::log::elevel::all); + m_server.init_asio(); + m_server.set_reuse_addr(true); + m_server.set_tls_init_handler([this](websocketpp::connection_hdl) + { + auto context = websocketpp::lib::shared_ptr(new wintls::context(wintls::method::system_default)); + context->use_certificate(m_certificate); + return context; + }); + m_server.set_open_handler([this](websocketpp::connection_hdl hdl) + { + { + std::lock_guard lock(m_mutex); + m_connection = hdl; + m_connected = true; + ++m_openCount; + } + m_cv.notify_all(); + }); + m_server.set_fail_handler([this](websocketpp::connection_hdl hdl) + { + std::lock_guard lock(m_mutex); + try + { + m_error = m_server.get_con_from_hdl(hdl)->get_ec().message(); + } + catch (...) + { + m_error = "connection failed"; + } + m_cv.notify_all(); + }); + m_server.set_message_handler([this](websocketpp::connection_hdl hdl, message_ptr const& msg) + { + websocketpp::lib::error_code ec; + m_server.send(hdl, msg->get_payload(), msg->get_opcode(), ec); + if (ec) + { + std::lock_guard lock(m_mutex); + m_error = ec.message(); + m_cv.notify_all(); + } + }); + + websocketpp::lib::error_code ec; + m_server.listen(port, ec); + if (ec) + { + std::printf("[FAIL] WSS listen failed: %s\n", ec.message().c_str()); + return false; + } + + m_server.start_accept(ec); + if (ec) + { + std::printf("[FAIL] WSS start_accept failed: %s\n", ec.message().c_str()); + return false; + } + + m_thread = std::thread([this]() + { + try + { + m_server.run(); + } + catch (std::exception const& e) + { + std::lock_guard lock(m_mutex); + m_error = e.what(); + m_cv.notify_all(); + } + }); + + return true; + } + + void Stop() + { + websocketpp::lib::error_code ec; + m_server.stop_listening(ec); + if (m_connected) + { + m_server.close(m_connection, websocketpp::close::status::normal, "test complete", ec); + } + m_server.stop(); + + if (m_thread.joinable()) + { + m_thread.join(); + } + } + + bool WaitForOpenCount(size_t expectedOpenCount) const + { + std::unique_lock lock(m_mutex); + return m_cv.wait_for(lock, Timeout, [this, expectedOpenCount]() + { + return m_openCount >= expectedOpenCount || !m_error.empty(); + }); + } + + std::string Error() const + { + std::lock_guard lock(m_mutex); + return m_error; + } + +private: + mutable std::mutex m_mutex; + mutable std::condition_variable m_cv; + server m_server; + std::thread m_thread; + websocketpp::connection_hdl m_connection; + CERT_CONTEXT const* m_certificate{ nullptr }; + bool m_connected{ false }; + size_t m_openCount{ 0 }; + std::string m_error; +}; +#endif + +struct ClientState +{ + std::mutex mutex; + std::condition_variable cv; + size_t textMessagesReceived{ 0 }; + std::string lastMessage; + size_t closeEventsReceived{ 0 }; + HCWebSocketCloseStatus lastCloseStatus{}; +}; + +void CALLBACK OnTextMessage( + _In_ HCWebsocketHandle, + _In_z_ char const* incomingBodyString, + _In_ void* context +) noexcept +{ + auto* state = static_cast(context); + { + std::lock_guard lock(state->mutex); + ++state->textMessagesReceived; + state->lastMessage = incomingBodyString != nullptr ? incomingBodyString : ""; + } + state->cv.notify_all(); +} + +std::string BuildLargePayload() +{ + std::string payload(ReceiveBufferSize - 1, 'A'); + payload += "\xF0\x9F\x98\x80"; + payload += std::string(128, 'B'); + return payload; +} + +void CALLBACK OnClose( + _In_ HCWebsocketHandle, + _In_ HCWebSocketCloseStatus status, + _In_ void* context +) noexcept +{ + auto* state = static_cast(context); + { + std::lock_guard lock(state->mutex); + ++state->closeEventsReceived; + state->lastCloseStatus = status; + } + state->cv.notify_all(); +} + +bool WaitForEcho(ClientState& state, size_t expectedTextMessagesReceived, std::string const& expected) +{ + std::unique_lock lock(state.mutex); + bool signaled = state.cv.wait_for(lock, Timeout, [&state, expectedTextMessagesReceived]() + { + return state.textMessagesReceived >= expectedTextMessagesReceived; + }); + + return signaled && state.lastMessage == expected; +} + +bool WaitForClose(ClientState& state, size_t expectedCloseEventsReceived, HCWebSocketCloseStatus expectedStatus) +{ + std::unique_lock lock(state.mutex); + return state.cv.wait_for(lock, Timeout, [&state, expectedCloseEventsReceived, expectedStatus]() + { + return state.closeEventsReceived >= expectedCloseEventsReceived && state.lastCloseStatus == expectedStatus; + }); +} + +HRESULT ConfigureTestWebSocket(HCWebsocketHandle websocket, ClientState& state) +{ + (void)websocket; + (void)state; + + return S_OK; +} + +#if HC_RUN_WSS_CERT_STORE_TESTS +HRESULT ConfigureCompressionWebSocket(HCWebsocketHandle websocket, ClientState& state) +{ + HRESULT hr = ConfigureTestWebSocket(websocket, state); + if (FAILED(hr)) + { + return hr; + } + + return HCWebSocketSetOptions(websocket, HCWebSocketOptions::RequestCompression); +} + +bool CreateCompressionRequestedWebSocket(HCWebsocketHandle& websocket, ClientState& state) +{ + HRESULT hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &state); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate", hr); + return false; + } + + hr = ConfigureCompressionWebSocket(websocket, state); + if (hr != S_OK) + { + PrintHr("[FAIL] Expected compression-capable websocket provider for WSS validation", hr); + return false; + } + + return true; +} + +bool ConnectWebSocketAndGetResult( + XTaskQueueHandle queue, + char const* uri, + HCWebsocketHandle websocket, + WebSocketCompletionResult& connectResult) +{ + XAsyncBlock connectAsync{}; + connectAsync.queue = queue; + + HRESULT hr = HCWebSocketConnectAsync(uri, "", websocket, &connectAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketConnectAsync", hr); + return false; + } + + hr = XAsyncGetStatus(&connectAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(connect)", hr); + return false; + } + + hr = HCGetWebSocketConnectResult(&connectAsync, &connectResult); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCGetWebSocketConnectResult", hr); + return false; + } + + return true; +} + +bool SendWebSocketMessageAndValidateEcho(HCWebsocketHandle websocket, XTaskQueueHandle queue, ClientState& state, char const* message) +{ + XAsyncBlock sendAsync{}; + sendAsync.queue = queue; + + HRESULT hr = HCWebSocketSendMessageAsync(websocket, message, &sendAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSendMessageAsync", hr); + return false; + } + + hr = XAsyncGetStatus(&sendAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(send)", hr); + return false; + } + + WebSocketCompletionResult sendResult{}; + hr = HCGetWebSocketSendMessageResult(&sendAsync, &sendResult); + if (FAILED(hr) || FAILED(sendResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketSendMessageResult", hr); + PrintHr("[FAIL] Send result", sendResult.errorCode); + return false; + } + + if (!WaitForEcho(state, 1, message)) + { + std::printf("[FAIL] Timed out waiting for echoed WSS message.\n"); + return false; + } + + return true; +} + +bool DisconnectWebSocketAndValidateClose(HCWebsocketHandle websocket, ClientState& state) +{ + HRESULT hr = HCWebSocketDisconnect(websocket); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketDisconnect", hr); + return false; + } + + if (!WaitForClose(state, 1, HCWebSocketCloseStatus::Normal)) + { + std::printf("[FAIL] Timed out waiting for WSS close callback after disconnect.\n"); + return false; + } + + return true; +} + +bool DisconnectWebSocket(HCWebsocketHandle websocket) +{ + HRESULT hr = HCWebSocketDisconnect(websocket); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketDisconnect", hr); + return false; + } + + return true; +} + +bool ValidateWssCertificateValidation(XTaskQueueHandle queue) +{ + ImportedServerCertificate certificate; + CurrentUserRootCertificateScope trustedRoot; + WssEchoServer server{ certificate.Context() }; + if (!server.Start(WssTestPort)) + { + return false; + } + + ScopeGuard serverGuard([&server]() + { + server.Stop(); + }); + + trustedRoot.Install(certificate.Context()); + + { + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + if (!CreateCompressionRequestedWebSocket(websocket, state)) + { + return false; + } + + WebSocketCompletionResult connectResult{}; + if (!ConnectWebSocketAndGetResult(queue, "wss://localhost:39004", websocket, connectResult)) + { + return false; + } + + if (FAILED(connectResult.errorCode)) + { + PrintHr("[FAIL] Trusted localhost WSS connect result", connectResult.errorCode); + return false; + } + + if (!server.WaitForOpenCount(1)) + { + std::printf("[FAIL] Timed out waiting for trusted WSS server connection.\n"); + if (!server.Error().empty()) + { + std::printf("[FAIL] WSS server error: %s\n", server.Error().c_str()); + } + return false; + } + + if (!SendWebSocketMessageAndValidateEcho(websocket, queue, state, "secure-hello")) + { + return false; + } + + if (!DisconnectWebSocket(websocket)) + { + return false; + } + + std::printf("[INFO] Trusted localhost WSS validation passed.\n"); + } + + { + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + if (!CreateCompressionRequestedWebSocket(websocket, state)) + { + return false; + } + + WebSocketCompletionResult connectResult{}; + if (!ConnectWebSocketAndGetResult(queue, "wss://127.0.0.1:39004", websocket, connectResult)) + { + return false; + } + + if (connectResult.errorCode != CERT_E_CN_NO_MATCH) + { + std::printf("[FAIL] Expected wrong-host WSS failure CERT_E_CN_NO_MATCH.\n"); + PrintHr("[FAIL] Wrong-host WSS connect result", connectResult.errorCode); + return false; + } + + std::printf("[INFO] Wrong-host WSS validation failed as expected with CERT_E_CN_NO_MATCH.\n"); + } + + trustedRoot.Uninstall(); + + { + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + if (!CreateCompressionRequestedWebSocket(websocket, state)) + { + return false; + } + + WebSocketCompletionResult connectResult{}; + if (!ConnectWebSocketAndGetResult(queue, "wss://localhost:39004", websocket, connectResult)) + { + return false; + } + + if (connectResult.errorCode != CERT_E_UNTRUSTEDROOT) + { + std::printf("[FAIL] Expected untrusted-root WSS failure CERT_E_UNTRUSTEDROOT.\n"); + PrintHr("[FAIL] Untrusted-root WSS connect result", connectResult.errorCode); + return false; + } + + std::printf("[INFO] Untrusted-root WSS validation failed as expected with CERT_E_UNTRUSTEDROOT.\n"); + } + + return true; +} +#endif + +bool ValidateWssFailureDiagnostics(XTaskQueueHandle queue) +{ +#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK + constexpr uint32_t ConnectionRefusedError = static_cast(WSAECONNREFUSED); +#else + constexpr uint32_t ConnectionRefusedError = static_cast(ECONNREFUSED); +#endif + + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + HRESULT hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &state); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate(wss failure diagnostics)", hr); + return false; + } + + hr = HCWebSocketSetOptions(websocket, HCWebSocketOptions::RequestCompression); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSetOptions(wss failure diagnostics)", hr); + return false; + } + + ScopedTraceCapture traceCapture; + + XAsyncBlock connectAsync{}; + connectAsync.queue = queue; + + hr = HCWebSocketConnectAsync("wss://127.0.0.1:39003", "", websocket, &connectAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketConnectAsync(wss failure diagnostics)", hr); + return false; + } + + hr = XAsyncGetStatus(&connectAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(wss failure diagnostics)", hr); + return false; + } + + WebSocketCompletionResult connectResult{}; + hr = HCGetWebSocketConnectResult(&connectAsync, &connectResult); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCGetWebSocketConnectResult(wss failure diagnostics)", hr); + return false; + } + + if (SUCCEEDED(connectResult.errorCode)) + { + std::printf("[FAIL] Expected WSS connect diagnostics test to fail against an unused localhost port.\n"); + return false; + } + + bool const hasExactPublicDiagnostic = connectResult.platformErrorCode == ConnectionRefusedError; + bool const hasExactTraceDiagnostic = traceCapture.ContainsWebsocketConnectError(ConnectionRefusedError); + if (!hasExactPublicDiagnostic && !hasExactTraceDiagnostic) + { + std::printf("[FAIL] Expected either an exact refused-connect platform error or a websocket trace with the refused-connect error.\n"); + std::printf("[INFO] WSS connect result HRESULT: 0x%08x (platform=%u)\n", + static_cast(connectResult.errorCode), + static_cast(connectResult.platformErrorCode)); + traceCapture.DumpWebsocketMessages(); + return false; + } + + if (hasExactPublicDiagnostic) + { + std::printf("[INFO] WSS connect failure exposed exact platform error %u through the public result surface.\n", + static_cast(connectResult.platformErrorCode)); + } + else + { + std::printf("[INFO] WSS connect returned HRESULT 0x%08x (platform=%u); verified exact refused-connect detail via websocket trace.\n", + static_cast(connectResult.errorCode), + static_cast(connectResult.platformErrorCode)); + } + + return true; +} + +bool ValidateUpgradeResponseHeaders( + HCWebsocketHandle websocket, + CompressionExpectation compressionExpectation, + char const* scenarioLabel) +{ + uint32_t numHeaders{}; + HRESULT hr = HCWebSocketGetNumResponseHeaders(websocket, &numHeaders); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketGetNumResponseHeaders", hr); + return false; + } + + if (numHeaders == 0) + { + std::printf("[FAIL] Expected websocket upgrade response headers but found none.\n"); + return false; + } + + char const* headerName{}; + char const* headerValue{}; + hr = HCWebSocketGetResponseHeaderAtIndex(websocket, 0, &headerName, &headerValue); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketGetResponseHeaderAtIndex", hr); + return false; + } + + if (headerName == nullptr || headerValue == nullptr) + { + std::printf("[FAIL] Expected a response header at index 0.\n"); + return false; + } + + char const* acceptHeader{}; + hr = HCWebSocketGetResponseHeader(websocket, "Sec-WebSocket-Accept", &acceptHeader); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketGetResponseHeader(Sec-WebSocket-Accept)", hr); + return false; + } + + if (acceptHeader == nullptr || acceptHeader[0] == '\0') + { + std::printf("[FAIL] Expected Sec-WebSocket-Accept in the upgrade response.\n"); + return false; + } + + std::printf("[INFO] Upgrade response headers: %u\n", numHeaders); + std::printf("[INFO] Sec-WebSocket-Accept: %s\n", acceptHeader); + + if (compressionExpectation == CompressionExpectation::Negotiated) + { + char const* extensionsHeader{}; + hr = HCWebSocketGetResponseHeader(websocket, "Sec-WebSocket-Extensions", &extensionsHeader); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketGetResponseHeader(Sec-WebSocket-Extensions)", hr); + return false; + } + + auto const extensions = extensionsHeader != nullptr ? std::string{ extensionsHeader } : std::string{}; + if (!ValidateNegotiatedCompressionExtensionHeader( + extensions, + "upgrade response headers", + scenarioLabel)) + { + return false; + } + } + + return true; +} + +bool ValidateDefaultCompressionOptionsBehavior() +{ + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + HRESULT hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &state); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate(default compression options validation)", hr); + return false; + } + + hr = ConfigureTestWebSocket(websocket, state); + if (FAILED(hr)) + { + PrintHr("[FAIL] ConfigureTestWebSocket(default compression options validation)", hr); + return false; + } + + hr = HCWebSocketSetOptions(websocket, HCWebSocketOptions::None); + if (hr != S_OK) + { + PrintHr("[FAIL] Expected S_OK for HCWebSocketSetOptions(None)", hr); + return false; + } + + return true; +} + +bool ValidateCompressionNegotiationScenario( + CompressionEchoServer& server, + XTaskQueueHandle queue, + char const* scenarioLabel, + HCWebSocketOptions options, + bool expectServerNoContextTakeover, + bool expectClientNoContextTakeover) +{ + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + HRESULT hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &state); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate(negotiation scenario)", hr); + return false; + } + + hr = ConfigureTestWebSocket(websocket, state); + if (FAILED(hr)) + { + PrintHr("[FAIL] ConfigureTestWebSocket(negotiation scenario)", hr); + return false; + } + + hr = HCWebSocketSetOptions(websocket, options); + if (hr != S_OK) + { + PrintHr("[FAIL] Expected S_OK from HCWebSocketSetOptions(negotiation scenario)", hr); + return false; + } + + server.ResetObservedConnection(); + + XAsyncBlock connectAsync{}; + connectAsync.queue = queue; + hr = HCWebSocketConnectAsync("ws://127.0.0.1:39002", "", websocket, &connectAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketConnectAsync(negotiation scenario)", hr); + return false; + } + + hr = XAsyncGetStatus(&connectAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(connect negotiation scenario)", hr); + return false; + } + + WebSocketCompletionResult connectResult{}; + hr = HCGetWebSocketConnectResult(&connectAsync, &connectResult); + if (FAILED(hr) || FAILED(connectResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketConnectResult(negotiation scenario)", hr); + PrintHr("[FAIL] Connect result(negotiation scenario)", connectResult.errorCode); + return false; + } + + if (!server.WaitForConnection()) + { + std::printf("[FAIL] Timed out waiting for server connection state for %s.\n", scenarioLabel); + if (!server.Error().empty()) + { + std::printf("[FAIL] Server error: %s\n", server.Error().c_str()); + } + return false; + } + + if (!server.Error().empty()) + { + std::printf("[FAIL] Server error for %s: %s\n", scenarioLabel, server.Error().c_str()); + return false; + } + + auto const requestedExtensions = server.RequestedExtensions(); + auto const negotiatedExtensions = server.NegotiatedExtensions(); + + std::printf("[INFO] %s requested extensions: %s\n", scenarioLabel, requestedExtensions.empty() ? "" : requestedExtensions.c_str()); + std::printf("[INFO] %s negotiated extensions: %s\n", scenarioLabel, negotiatedExtensions.empty() ? "" : negotiatedExtensions.c_str()); + + if (!ValidateRequestedCompressionExtensionHeader( + requestedExtensions, + "requested extensions", + scenarioLabel, + expectServerNoContextTakeover, + expectClientNoContextTakeover)) + { + return false; + } + + if (!ValidateNegotiatedCompressionExtensionHeader( + negotiatedExtensions, + "negotiated extensions", + scenarioLabel)) + { + return false; + } + + if (!ValidateUpgradeResponseHeaders( + websocket, + CompressionExpectation::Negotiated, + scenarioLabel)) + { + return false; + } + + hr = HCWebSocketDisconnect(websocket); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketDisconnect(negotiation scenario)", hr); + return false; + } + + if (!WaitForClose(state, 1, HCWebSocketCloseStatus::Normal)) + { + std::printf("[FAIL] Timed out waiting for WebSocket close callback after %s.\n", scenarioLabel); + return false; + } + + return true; +} + +} // namespace + +int main() +{ + HRESULT hr = HCInitialize(nullptr); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCInitialize", hr); + return 1; + } + + HCWebsocketHandle websocket{ nullptr }; + XTaskQueueHandle queue{ nullptr }; + + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + + if (queue != nullptr) + { + XTaskQueueCloseHandle(queue); + } + + HCCleanup(); + }); + + hr = XTaskQueueCreate( + XTaskQueueDispatchMode::ThreadPool, + XTaskQueueDispatchMode::ThreadPool, + &queue + ); + if (FAILED(hr)) + { + PrintHr("[FAIL] XTaskQueueCreate", hr); + return 1; + } + + ClientState clientState; + hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &clientState); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate", hr); + return 1; + } + +#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK + hr = ConfigureTestWebSocket(websocket, clientState); + if (FAILED(hr)) + { + PrintHr("[FAIL] ConfigureTestWebSocket", hr); + return 1; + } +#endif + + CompressionExpectation compressionExpectation{}; + hr = HCWebSocketSetOptions(websocket, HCWebSocketOptions::RequestCompression); + if (hr == S_OK) + { + compressionExpectation = CompressionExpectation::Negotiated; + PrintHr("[INFO] HCWebSocketSetOptions", hr); + } + else if (hr == E_NOT_SUPPORTED) + { +#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK + compressionExpectation = CompressionExpectation::Unsupported; + PrintHr("[INFO] HCWebSocketSetOptions", hr); +#else + std::printf("[SKIP] Compression request not supported in this build/backend.\n"); + PrintHr("[SKIP] HCWebSocketSetOptions", hr); + return SkipReturnCode; +#endif + } + else if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSetOptions", hr); + return 1; + } + else + { + PrintHr("[FAIL] Unexpected HCWebSocketSetOptions result", hr); + return 1; + } + + if (compressionExpectation == CompressionExpectation::Negotiated) + { +#if HC_RUN_WSS_CERT_STORE_TESTS + if (!ValidateWssCertificateValidation(queue)) + { + return 1; + } +#endif + + if (!ValidateWssFailureDiagnostics(queue)) + { + return 1; + } + + if (!ValidateDefaultCompressionOptionsBehavior()) + { + return 1; + } + } + + CompressionEchoServer server; + if (!server.Start(TestPort)) + { + return 1; + } + + ScopeGuard serverGuard([&server]() + { + server.Stop(); + }); + + char const* uri = "ws://127.0.0.1:39002"; + XAsyncBlock connectAsync{}; + connectAsync.queue = queue; + + hr = HCWebSocketConnectAsync(uri, "", websocket, &connectAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketConnectAsync", hr); + return 1; + } + + hr = XAsyncGetStatus(&connectAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(connect)", hr); + return 1; + } + + WebSocketCompletionResult connectResult{}; + hr = HCGetWebSocketConnectResult(&connectAsync, &connectResult); + if (FAILED(hr) || FAILED(connectResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketConnectResult", hr); + PrintHr("[FAIL] Connect result", connectResult.errorCode); + return 1; + } + + if (!server.WaitForConnection()) + { + std::printf("[FAIL] Timed out waiting for server connection state.\n"); + if (!server.Error().empty()) + { + std::printf("[FAIL] Server error: %s\n", server.Error().c_str()); + } + return 1; + } + + if (!server.Error().empty()) + { + std::printf("[FAIL] Server error: %s\n", server.Error().c_str()); + return 1; + } + + auto const requestedExtensions = server.RequestedExtensions(); + auto const negotiatedExtensions = server.NegotiatedExtensions(); + + std::printf("[INFO] Requested extensions: %s\n", requestedExtensions.empty() ? "" : requestedExtensions.c_str()); + std::printf("[INFO] Negotiated extensions: %s\n", negotiatedExtensions.empty() ? "" : negotiatedExtensions.c_str()); + + if (!ValidateUpgradeResponseHeaders( + websocket, + compressionExpectation, + "RequestCompression")) + { + return 1; + } + + if (compressionExpectation == CompressionExpectation::Negotiated) + { + if (!ValidateRequestedCompressionExtensionHeader( + requestedExtensions, + "requested extensions", + "RequestCompression", + false, + false)) + { + return 1; + } + + if (!ValidateNegotiatedCompressionExtensionHeader( + negotiatedExtensions, + "negotiated extensions", + "RequestCompression")) + { + return 1; + } + + std::printf("[INFO] RequestCompression negotiated permessage-deflate without no-context-takeover flags.\n"); + } + else + { + std::printf("[INFO] Compression request unsupported on this backend; continuing with non-compression validation.\n"); + } + + XAsyncBlock sendAsync{}; + sendAsync.queue = queue; + + hr = HCWebSocketSendMessageAsync(websocket, "hello", &sendAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSendMessageAsync", hr); + return 1; + } + + hr = XAsyncGetStatus(&sendAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(send)", hr); + return 1; + } + + WebSocketCompletionResult sendResult{}; + hr = HCGetWebSocketSendMessageResult(&sendAsync, &sendResult); + if (FAILED(hr) || FAILED(sendResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketSendMessageResult", hr); + PrintHr("[FAIL] Send result", sendResult.errorCode); + return 1; + } + + if (!WaitForEcho(clientState, 1, "hello")) + { + std::printf("[FAIL] Timed out waiting for echoed message.\n"); + return 1; + } + + std::string const largePayload = BuildLargePayload(); + sendAsync = {}; + sendAsync.queue = queue; + + hr = HCWebSocketSendMessageAsync(websocket, largePayload.c_str(), &sendAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSendMessageAsync(large)", hr); + return 1; + } + + hr = XAsyncGetStatus(&sendAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(send large)", hr); + return 1; + } + + sendResult = {}; + hr = HCGetWebSocketSendMessageResult(&sendAsync, &sendResult); + if (FAILED(hr) || FAILED(sendResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketSendMessageResult(large)", hr); + PrintHr("[FAIL] Send large result", sendResult.errorCode); + return 1; + } + + if (!WaitForEcho(clientState, 2, largePayload)) + { + std::printf("[FAIL] Timed out waiting for oversized text echo.\n"); + return 1; + } + + hr = HCWebSocketDisconnect(websocket); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketDisconnect", hr); + return 1; + } + + if (!WaitForClose(clientState, 1, HCWebSocketCloseStatus::Normal)) + { + std::printf("[FAIL] Timed out waiting for WebSocket close callback after disconnect.\n"); + return 1; + } + + if (compressionExpectation == CompressionExpectation::Negotiated) + { + if (!ValidateCompressionNegotiationScenario( + server, + queue, + "RequestCompression|CompressionServerNoContextTakeover", + CombineCompressionOptions( + HCWebSocketOptions::RequestCompression, + HCWebSocketOptions::CompressionServerNoContextTakeover), + true, + false)) + { + return 1; + } + + if (!ValidateCompressionNegotiationScenario( + server, + queue, + "RequestCompression|CompressionClientNoContextTakeover", + CombineCompressionOptions( + HCWebSocketOptions::RequestCompression, + HCWebSocketOptions::CompressionClientNoContextTakeover), + false, + true)) + { + return 1; + } + + if (!ValidateCompressionNegotiationScenario( + server, + queue, + "RequestCompression|CompressionServerNoContextTakeover|CompressionClientNoContextTakeover", + CombineCompressionOptions( + CombineCompressionOptions( + HCWebSocketOptions::RequestCompression, + HCWebSocketOptions::CompressionServerNoContextTakeover), + HCWebSocketOptions::CompressionClientNoContextTakeover), + true, + true)) + { + return 1; + } + } + + std::printf("[PASS] WebSocket integration behavior validated successfully.\n"); + return 0; +} diff --git a/libHttpClient.vs2019.sln b/libHttpClient.vs2019.sln index 9e450ee8..c6b167e8 100644 --- a/libHttpClient.vs2019.sln +++ b/libHttpClient.vs2019.sln @@ -31,8 +31,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TE", EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest", "Build\libHttpClient.UnitTest\libHttpClient.UnitTest.vcxitems", "{8EF7009A-36CF-4D82-9FB7-6D69154893CF}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketSemanticsTests.Win32", "Tests\WebSocketSemantics\WebSocketSemanticsTests.Win32.vcxproj", "{6E9D9094-EC44-4E0C-B271-25E8CB5C9734}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.UnitTest.TAEF", "Build\libHttpClient.UnitTest.TAEF\libHttpClient.UnitTest.TAEF.vcxproj", "{E885BB30-F51E-4BAB-9300-4B303144BB49}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionTests.Win32.vcxproj", "{4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1}" @@ -271,24 +269,6 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x64.Build.0 = Release|x64 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.ActiveCfg = Release|Win32 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.Build.0 = Release|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.ActiveCfg = Debug|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.Build.0 = Debug|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.Build.0 = Debug|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.ActiveCfg = Debug|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.Build.0 = Debug|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.ActiveCfg = Debug|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.Build.0 = Debug|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.ActiveCfg = Release|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.Build.0 = Release|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.ActiveCfg = Release|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.Build.0 = Release|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|Gaming.Desktop.x64.ActiveCfg = Release|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.ActiveCfg = Release|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.Build.0 = Release|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.ActiveCfg = Release|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.Build.0 = Release|Win32 {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.ActiveCfg = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM.Build.0 = Debug|ARM {E885BB30-F51E-4BAB-9300-4B303144BB49}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -454,7 +434,6 @@ Global {E885BB30-F51E-4BAB-9300-4B303144BB49} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {F5D76018-3E61-4C5E-9C26-6A49A9C24E73} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734} = {A7CDB1FA-01AE-4507-B54A-BA54F2080A11} {47564F2B-9ED7-4527-997A-5D76C36998D1} = {6FB21B4B-86D1-4883-85A2-B51771884ED8} {2DFBBF3A-6D4B-4FF8-BD01-C9527A1FE0AC} = {5B7AC447-943C-45D3-AD4E-3A61DA208FD8} {99542110-3CCD-4525-B607-DB8F6E76AEAC} = {5B7AC447-943C-45D3-AD4E-3A61DA208FD8} diff --git a/libHttpClient.vs2022.sln b/libHttpClient.vs2022.sln index b319effc..e9148c79 100644 --- a/libHttpClient.vs2022.sln +++ b/libHttpClient.vs2022.sln @@ -66,8 +66,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionTests.W EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketCompressionIntegrationTests.Win32", "Tests\WebSocketCompression\WebSocketCompressionIntegrationTests.Win32.vcxproj", "{F5D76018-3E61-4C5E-9C26-6A49A9C24E73}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WebSocketSemanticsTests.Win32", "Tests\WebSocketSemantics\WebSocketSemanticsTests.Win32.vcxproj", "{6E9D9094-EC44-4E0C-B271-25E8CB5C9734}" -EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.XAsync", "Build\libHttpClient.XAsync\libHttpClient.XAsync.vcxitems", "{B5118956-53CC-4B24-8807-DE8D7D226B40}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libHttpClient.Zlib", "Build\libHttpClient.Zlib\libHttpClient.Zlib.vcxitems", "{F9061DCA-255B-4D5E-8DF5-4AFBFB4B98EF}" @@ -368,24 +366,6 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x64.Build.0 = Release|x64 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.ActiveCfg = Release|Win32 {9DD2BA60-6505-493A-8C41-8085C44E9F1F}.Release|x86.Build.0 = Release|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.ActiveCfg = Debug|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM.Build.0 = Debug|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.ActiveCfg = Debug|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|ARM64.Build.0 = Debug|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|Gaming.Desktop.x64.ActiveCfg = Debug|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.ActiveCfg = Debug|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x64.Build.0 = Debug|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.ActiveCfg = Debug|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Debug|x86.Build.0 = Debug|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.ActiveCfg = Release|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM.Build.0 = Release|ARM - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.ActiveCfg = Release|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|ARM64.Build.0 = Release|ARM64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|Gaming.Desktop.x64.ActiveCfg = Release|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.ActiveCfg = Release|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x64.Build.0 = Release|x64 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.ActiveCfg = Release|Win32 - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734}.Release|x86.Build.0 = Release|Win32 {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM.ActiveCfg = Debug|ARM {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM.Build.0 = Debug|ARM {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8}.Debug|ARM64.ActiveCfg = Debug|ARM64 @@ -461,7 +441,6 @@ Global {9DD2BA60-6505-493A-8C41-8085C44E9F1F} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {4F1D3BB2-0D96-4B1C-9E0D-8E3E20D856B1} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {F5D76018-3E61-4C5E-9C26-6A49A9C24E73} = {6BBCD917-A163-48D8-9385-3F6135E031FE} - {6E9D9094-EC44-4E0C-B271-25E8CB5C9734} = {6BBCD917-A163-48D8-9385-3F6135E031FE} {D980F91B-92BF-4CC4-89FD-1CD99DBA870F} = {118840A6-8EB2-4D70-B0EE-65EE13E2FEAB} {E35BA8A1-AE7B-4FB5-8200-469B98BC1CA8} = {118840A6-8EB2-4D70-B0EE-65EE13E2FEAB} {8CA3B500-0D89-4DB1-BA8B-98AEB468CA13} = {348C2EBE-5E0D-4008-8E9C-BD2ECF40F4BC} From a593c5bc77764e08beaf234cf2bee10f81a6a97b Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 22:15:25 -0700 Subject: [PATCH 05/24] Unify deterministic WebSocket behavior on WinHTTP --- Include/httpClient/httpClient.h | 25 +-- README.md | 45 ++--- Source/HTTP/WinHttp/winhttp_connection.cpp | 72 ++++++- Source/HTTP/WinHttp/winhttp_connection.h | 2 + .../HTTP/WinHttp/winhttp_websocket_hybrid.cpp | 9 +- .../Websocketpp/websocketpp_websocket.cpp | 8 +- Source/WebSocket/hcwebsocket.cpp | 7 + Source/WebSocket/hcwebsocket.h | 3 + .../WebSocketCompressionTests.cpp | 176 ++++++++++++++++++ 9 files changed, 298 insertions(+), 49 deletions(-) diff --git a/Include/httpClient/httpClient.h b/Include/httpClient/httpClient.h index 744caaf7..fd9ce9b2 100644 --- a/Include/httpClient/httpClient.h +++ b/Include/httpClient/httpClient.h @@ -929,10 +929,10 @@ typedef void ); /// -/// A callback invoked on Win32 and GDK when the built-in WebSocket transport surfaces an oversized -/// incoming payload as raw fragments to honor the configured receive buffer size. +/// A callback invoked on Win32 and GDK legacy behavior when an oversized incoming payload is surfaced +/// as raw fragments to honor the configured receive buffer size. /// -/// The callback receives raw bytes. On current Win32 / GDK behavior, oversized UTF-8 payloads use +/// The callback receives raw bytes. In legacy Win32 / GDK behavior, oversized UTF-8 payloads use /// this same fragment path. /// /// IMPORTANT: If you expect incoming payloads larger than the receive buffer, set this callback or @@ -1030,7 +1030,7 @@ STDAPI HCWebSocketCreate( /// A pointer to the binary message fragment handling callback to use, or a null pointer to remove. /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_NOT_SUPPORTED, or E_FAIL. /// -/// On Win32 and GDK, the built-in transport uses this callback when an incoming payload exceeds the +/// On Win32 and GDK legacy behavior, this callback is used when an incoming payload exceeds the /// configured receive buffer (default 20KB). Current oversized UTF-8 overflow behavior also uses /// this raw-byte fragment path. /// @@ -1069,7 +1069,7 @@ STDAPI HCWebSocketSetProxyUri( /// This must be called after calling HCWebSocketSetProxyUri. /// This must be called prior to calling HCWebSocketConnectAsync. /// Available on Win32 and GDK. On GDK console, TLS validation is always enforced -/// regardless of this setting, matching the built-in WinHTTP WebSocket path. +/// regardless of this setting. /// /// The handle of the WebSocket /// true to disable TLS server certificate validation, false to keep validation enabled @@ -1119,9 +1119,10 @@ STDAPI HCWebSocketSetPingInterval( /// Calling HCWebSocketSetOptions(HCWebSocketOptions::LegacySemantics) explicitly preserves that same legacy behavior. /// Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior. /// -/// In deterministic behavior, fragment callbacks are not supported. On websocketpp-backed paths, the inbound -/// message-size limit becomes a hard cap: the value passed to HCWebSocketSetMaxReceiveBufferSize() if set before connect, -/// otherwise the provider default of 32,000,000 bytes. +/// In deterministic behavior, fragment callbacks are not supported. The inbound message-size limit becomes a hard cap: +/// the value passed to HCWebSocketSetMaxReceiveBufferSize() if set before connect, otherwise the default +/// deterministic limit of 32,000,000 bytes. When an incoming message exceeds that cap, the socket closes with +/// HCWebSocketCloseStatus::TooLarge. /// /// RequestCompression alone reuses compression context in both directions by default. #if HC_PLATFORM != HC_PLATFORM_ANDROID @@ -1188,9 +1189,9 @@ typedef struct WebSocketCompletionResult /// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_OUTOFMEMORY, or E_FAIL. /// /// To get the result, first call HCGetWebSocketConnectResult inside the AsyncBlock callback or after the AsyncBlock is complete. -/// On GDK and Win32 (Win 8+) the background work is scheduled to threads owned by WinHttp run in async mode. -/// On UWP and XDK, the connection thread is owned and controlled by Windows::Networking::Sockets::MessageWebSocket. -/// On Win32 (Win 7+), iOS, and Android, all background work (including initial connection process) will be added to the queue +/// On GDK and on Win32 running on Windows 8 or later, some background work is scheduled to OS-owned async threads. +/// On UWP and XDK, the connection thread is owned and controlled by the platform WebSocket implementation. +/// On Win32 running on Windows 7, iOS, and Android, all background work (including initial connection process) will be added to the queue /// in the provided XAsyncBlock. LibHttpClient will create a reference to that queue but it is the responsibility of the /// caller to dispatch that queue for as long as the websocket connection is active. Note that work for /// HCWebSocketSendMessageAsync calls can be assigned to a separate queue if desired. @@ -1287,7 +1288,7 @@ STDAPI HCWebSocketDisconnectWithStatus( /// /// Configures the pre-connect inbound message-size limit behavior for built-in WebSocket transports. /// -/// In deterministic websocketpp-backed behavior, this becomes the hard inbound message-size cap. +/// In deterministic behavior, this becomes the hard inbound message-size cap. /// If you do not call this API before connect, that deterministic cap defaults to 32,000,000 bytes. /// /// In legacy Win32 / GDK behavior, the configured receive buffer still controls when oversized incoming diff --git a/README.md b/README.md index 71bec100..2a10904a 100644 --- a/README.md +++ b/README.md @@ -63,54 +63,41 @@ libHttpClient provides a platform abstraction layer for HTTP and WebSocket, and - **No call to HCWebSocketSetOptions()**: The socket uses the platform's legacy behavior. - **Legacy Win32 / GDK default buffer**: The legacy receive buffer defaults to 20KB (20,480 bytes). -- **Legacy Win32 / GDK oversized payload path**: When an incoming payload exceeds the configured receive buffer, the built-in transport surfaces it through HCWebSocketSetBinaryMessageFragmentEventFunction() as raw bytes. +- **Legacy Win32 / GDK oversized payload path**: When an incoming payload exceeds the configured receive buffer, it is surfaced through HCWebSocketSetBinaryMessageFragmentEventFunction() as raw bytes. - **Legacy Win32 / GDK text overflow behavior**: Oversized UTF-8 payloads use that same raw-byte fragment callback path. - **Legacy Win32 / GDK without a fragment handler**: Oversized incoming payloads are not surfaced through the public whole-message callbacks unless a fragment handler is installed. -- **Deterministic websocketpp-backed behavior**: Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior on supported websocketpp-backed paths. Fragment callbacks are not supported there. -- **Deterministic inbound limit**: HCWebSocketSetMaxReceiveBufferSize() becomes a hard inbound message-size cap for deterministic websocketpp-backed behavior. If not set before connect, that path uses the provider default `32,000,000` byte limit. +- **Deterministic behavior**: Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior on supported built-in implementations. Fragment callbacks are not supported there. +- **Deterministic inbound limit**: HCWebSocketSetMaxReceiveBufferSize() becomes a hard inbound message-size cap for deterministic behavior. If not set before connect, the deterministic default is `32,000,000` bytes, and oversized messages close the socket with `HCWebSocketCloseStatus::TooLarge`. -### WebSocket backend selection - -libHttpClient selects a built-in WebSocket backend based on platform: - -- **Win32**: WinHTTP -- **GDK**: WinHTTP -- **Linux / macOS / iOS**: `websocketpp` -- **Android**: OkHttp -- **UWP / WinRT**: `MessageWebSocket` - -To replace the built-in backend entirely, call `HCSetWebSocketFunctions()`. +To replace the built-in WebSocket implementation entirely, call `HCSetWebSocketFunctions()`. #### Compression -When compression is requested, the compression-capable backend depends on platform: +When compression is requested, built-in compression support is available on Linux, macOS, iOS, Win32, GDK PC, and optionally GDK console when enabled with `HC_ENABLE_GDK_XBOX_WEBSOCKET_COMPRESSION`. -- **Win32**: `websocketpp` -- **GDK PC**: `websocketpp` -- **GDK Console**: disabled by default; `websocketpp` with optional build flag -- **Linux / macOS / iOS**: `websocketpp` -- **Android**: managed by OkHttp; libHttpClient does not expose `HCWebSocketSetOptions()` -- **UWP / WinRT**: not supported +Android manages compression internally and does not expose `HCWebSocketSetOptions()`. + +UWP / WinRT does not support built-in compression negotiation. Compression support can be compiled out entirely by omitting the `HC_ENABLE_WEBSOCKET_COMPRESSION` build flag. Compression for GDK Console can be enabled with the `HC_ENABLE_GDK_XBOX_WEBSOCKET_COMPRESSION` build flag. Call `HCWebSocketSetOptions()` on a handle before `HCWebSocketConnectAsync()` to control the built-in WebSocket behavior for that connection. `LegacySemantics` explicitly preserves the existing legacy behavior. `None` selects deterministic behavior without requesting compression. `RequestCompression` selects deterministic behavior and requests `permessage-deflate` compression. Combine `RequestCompression` with `CompressionServerNoContextTakeover` and/or `CompressionClientNoContextTakeover` to request fresh zlib state per message in the corresponding direction. These flags require `RequestCompression`; setting them alone returns `E_INVALIDARG`. -In deterministic websocketpp-backed behavior, fragment callbacks are not supported and the inbound message-size limit becomes a hard cap. `HCWebSocketSetMaxReceiveBufferSize()` overrides that cap if called before connect; otherwise the websocketpp path uses its default `32,000,000` byte limit. On Win32 and GDK, explicit non-legacy options switch the built-in transport to the websocketpp-backed deterministic path. Legacy WinHTTP behavior, including the current oversized-payload fragment callback behavior, remains the default when `HCWebSocketSetOptions()` is not called. +In deterministic behavior, fragment callbacks are not supported and the inbound message-size limit becomes a hard cap. `HCWebSocketSetMaxReceiveBufferSize()` overrides that cap if called before connect; otherwise the deterministic default is `32,000,000` bytes. Legacy Win32 and GDK behavior, including oversized-payload fragment callbacks, remains the default when `HCWebSocketSetOptions()` is not called. -#### WinHTTP proxy and TLS notes +#### Windows proxy and TLS notes -On the built-in WinHTTP WebSocket path, `HCWebSocketSetProxyUri()` applies the explicit proxy URI to the WebSocket request. Embedded proxy credentials are passed through but not pre-authenticated. +On Win32 and GDK, `HCWebSocketSetProxyUri()` applies the explicit proxy URI to built-in WebSocket requests. Embedded proxy credentials are passed through but not pre-authenticated. -`HCWebSocketSetProxyDecryptsHttps()` disables TLS server certificate validation for a WebSocket connection. It is a debugging-only setting for use with HTTPS-intercepting proxies (Fiddler, Charles, etc.) and should never be enabled in production. Available on Win32 and GDK. On GDK console, TLS validation is always enforced regardless of this setting, matching the built-in WinHTTP WebSocket path. +`HCWebSocketSetProxyDecryptsHttps()` disables TLS server certificate validation for a WebSocket connection. It is a debugging-only setting for use with HTTPS-intercepting proxies (Fiddler, Charles, etc.) and should never be enabled in production. Available on Win32 and GDK. On GDK console, TLS validation is always enforced regardless of this setting. ## Behavior control * On GDK, XDK ERA, UWP, iOS, and Android, HCHttpCallPerform() will call native platform APIs * Optionally call HCSetHttpCallPerformFunction() to do your own HTTP handling using HCHttpCallRequestGet*(), HCHttpCallResponseSet*(), and HCSettingsGet*() * See sample CustomHttpImplWithCurl for an example of how to use this callback to make your own HTTP implementation. -* Optionally call HCSetWebSocketFunctions() to replace the built-in WebSocket backend selection with your own connect / send / disconnect callbacks. +* Optionally call HCSetWebSocketFunctions() to replace the built-in WebSocket implementation with your own connect / send / disconnect callbacks. ## Build customization @@ -118,12 +105,14 @@ If you are building libHttpClient from source, you can provide an hc_settings.pr * Defining HCNoWebSockets will exclude WebSocket APIs (and all their dependencies) from the libHttpClient library * Defining HCNoZlib will exclude compression APIs and prevent libHttpClient from defining Zlib symbols within libHttpClient * Defining HCExternalOpenSSL will prevent libHttpClient from referencing our private OpenSSL projects. If this is defined, you will need to manually include your own (compatible) version of OpenSSL when linking. -* Setting `HCEnableWebSocketCompression` to `true` or `false` controls whether the optional compression-capable WebSocket provider is compiled into the Win32 and GDK MSBuild builds. When enabled and the required WinTLS dependency is present, the build defines `HC_ENABLE_WEBSOCKET_COMPRESSION` and registers the alternate `websocketpp`-based compression provider. This property defaults to `true`. -* Setting `HCEnableGDKXboxWebSocketCompression` to `true` enables that alternate `websocketpp`-based provider on GDK Xbox console builds. This property defaults to `false`, so GDK console builds keep the WinHTTP WebSocket path unless they are explicitly opted in. +* Setting `HCEnableWebSocketCompression` to `true` or `false` controls whether optional built-in WebSocket compression support is compiled into the Win32 and GDK MSBuild builds. When enabled and the required WinTLS dependency is present, the build defines `HC_ENABLE_WEBSOCKET_COMPRESSION`. This property defaults to `true`. +* Setting `HCEnableGDKXboxWebSocketCompression` to `true` enables that same built-in compression support on GDK Xbox console builds. This property defaults to `false`. * The Win32 certificate-validation integration tests are compiled only in `Tests\WebSocketCompression\WebSocketCompressionIntegrationTests.Win32.vcxproj`, which defines `HC_ENABLE_WSS_CERT_STORE_TESTS=1`. Running that integration binary may prompt for Windows confirmation when it adds or removes the temporary test certificate from `CurrentUser\Root`, so it is intended for manual/integration use rather than CI. The default `Tests\WebSocketCompression\WebSocketCompressionTests.Win32.vcxproj` intentionally omits that define and remains popup-free. For Linux CMake builds, `HC_ENABLE_WEBSOCKET_COMPRESSION` is enabled by default. The helper script in `Build\libHttpClient.Linux\libHttpClient_Linux.bash` keeps that default and exposes `-nwc|--no-websocket-compression` as an opt-out, while still accepting `-wc|--websocket-compression` for explicit enablement. +For Apple Xcode builds, the websocket-enabled `libHttpClient_iOS` and `libHttpClient_macOS` targets define `HC_ENABLE_WEBSOCKET_COMPRESSION` and include the vendored `External/zlib` headers by default, so built-in compression support is enabled on iOS and macOS as well. + An example customization file hc_settings.props.example can be found at the root of the repository. ## How to clone repo diff --git a/Source/HTTP/WinHttp/winhttp_connection.cpp b/Source/HTTP/WinHttp/winhttp_connection.cpp index 100d64e9..6fdcb1c9 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.cpp +++ b/Source/HTTP/WinHttp/winhttp_connection.cpp @@ -1642,6 +1642,49 @@ void WinHttpConnection::StartWinHttpClose() // Flow continues from this point via WinHttp calling completion_callback } +HRESULT WinHttpConnection::StartWebSocketClose(HCWebSocketCloseStatus closeStatus) noexcept +{ +#ifndef HC_NOWEBSOCKETS + assert(m_winHttpWebSocketExports.close); + + { + win32_cs_autolock autoCriticalSection(&m_lock); + + if (m_state == ConnectionState::WebSocketClosing || m_state == ConnectionState::WinHttpClosing || m_state == ConnectionState::Closed) + { + return S_OK; + } + + m_state = ConnectionState::WebSocketClosing; + } + + DWORD dwError = m_winHttpWebSocketExports.close(m_hRequest, static_cast(closeStatus), nullptr, 0); + return HRESULT_FROM_WIN32(dwError); +#else + UNREFERENCED_PARAMETER(closeStatus); + assert(false); + return E_FAIL; +#endif +} + +size_t WinHttpConnection::EffectiveReceiveBufferLimit() const noexcept +{ +#ifndef HC_NOWEBSOCKETS + if (!m_websocketHandle || !m_websocketHandle->websocket) + { + return 0; + } + + auto const& websocket = m_websocketHandle->websocket; + return websocket->UsesDeterministicSemantics() ? + websocket->DeterministicMaxReceiveBufferSize() : + websocket->MaxReceiveBufferSize(); +#else + assert(false); + return 0; +#endif +} + void WinHttpConnection::WebSocketSendMessage(const WebSocketSendContext& sendContext) { #ifndef HC_NOWEBSOCKETS @@ -1763,16 +1806,30 @@ void WinHttpConnection::callback_websocket_status_read_complete( else if (wsStatus->eBufferType == WINHTTP_WEB_SOCKET_UTF8_FRAGMENT_BUFFER_TYPE || wsStatus->eBufferType == WINHTTP_WEB_SOCKET_BINARY_FRAGMENT_BUFFER_TYPE) { bool readBufferFull{ false }; + bool deterministicOverflow{ false }; { win32_cs_autolock autoCriticalSection(&pRequestContext->m_lock); pRequestContext->m_websocketReceiveBuffer.FinishWriteData(wsStatus->dwBytesTransferred); // If the receive buffer is full & at max size, invoke client fragment handler with partial message - readBufferFull = pRequestContext->m_websocketReceiveBuffer.GetBufferByteCount() >= pRequestContext->m_websocketHandle->websocket->MaxReceiveBufferSize(); + auto const receiveBufferLimit = pRequestContext->EffectiveReceiveBufferLimit(); + readBufferFull = pRequestContext->m_websocketReceiveBuffer.GetBufferByteCount() >= receiveBufferLimit; + deterministicOverflow = readBufferFull && pRequestContext->m_websocketHandle->websocket->UsesDeterministicSemantics(); } if (readBufferFull) { + if (deterministicOverflow) + { + HRESULT closeHr = pRequestContext->StartWebSocketClose(HCWebSocketCloseStatus::TooLarge); + if (FAILED(closeHr)) + { + HC_TRACE_ERROR(WEBSOCKET, "[WinHttp] failed to close oversized deterministic message with TooLarge: hr=0x%0.8x", closeHr); + pRequestContext->StartWinHttpClose(); + } + return; + } + // Treat all message fragments as binary as they may not be null terminated pRequestContext->WebSocketReadComplete(true, false); } @@ -1796,19 +1853,26 @@ HRESULT WinHttpConnection::WebSocketReadAsync() { #ifndef HC_NOWEBSOCKETS win32_cs_autolock autoCriticalSection(&m_lock); + auto const receiveBufferLimit = EffectiveReceiveBufferLimit(); if (m_websocketReceiveBuffer.GetBuffer() == nullptr) { // Initialize buffer with default size of WINHTTP_WEBSOCKET_RECVBUFFER_SIZE - RETURN_IF_FAILED(m_websocketReceiveBuffer.Resize(WINHTTP_WEBSOCKET_RECVBUFFER_INITIAL_SIZE)); + size_t initialSize = (std::min)(WINHTTP_WEBSOCKET_RECVBUFFER_INITIAL_SIZE, receiveBufferLimit); + if (initialSize == 0) + { + initialSize = 1; + } + + RETURN_IF_FAILED(m_websocketReceiveBuffer.Resize((uint32_t)initialSize)); } else if (m_websocketReceiveBuffer.GetRemainingCapacity() == 0) { // Expand buffer size_t newSize = (size_t)m_websocketReceiveBuffer.GetBufferByteCount() * 2; - if (newSize > m_websocketHandle->websocket->MaxReceiveBufferSize()) + if (newSize > receiveBufferLimit) { - newSize = m_websocketHandle->websocket->MaxReceiveBufferSize(); + newSize = receiveBufferLimit; } RETURN_IF_FAILED(m_websocketReceiveBuffer.Resize((uint32_t)newSize)); diff --git a/Source/HTTP/WinHttp/winhttp_connection.h b/Source/HTTP/WinHttp/winhttp_connection.h index be17818d..444e8600 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.h +++ b/Source/HTTP/WinHttp/winhttp_connection.h @@ -274,6 +274,8 @@ class WinHttpConnection : public std::enable_shared_from_this void SendRequest(); void StartWinHttpClose(); + HRESULT StartWebSocketClose(HCWebSocketCloseStatus closeStatus) noexcept; + size_t EffectiveReceiveBufferLimit() const noexcept; #if HC_PLATFORM != HC_PLATFORM_GDK HRESULT set_autodiscover_proxy(); diff --git a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp index 6bae5a9d..58087980 100644 --- a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp +++ b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp @@ -70,7 +70,12 @@ HRESULT WinHttpHybrid_WebSocketProvider::OptionsResult(HCWebSocketOptions option return S_OK; } - return m_wsppProvider->OptionsResult(options); + if (RequestsWebSocketCompression(options)) + { + return m_wsppProvider->OptionsResult(options); + } + + return S_OK; } void WinHttpHybrid_WebSocketProvider::OnSuspending() noexcept @@ -101,7 +106,7 @@ void WinHttpHybrid_WebSocketProvider::OnResuming() noexcept IWebSocketProvider& WinHttpHybrid_WebSocketProvider::ConnectProvider(HCWebsocketHandle websocketHandle) noexcept { - if (websocketHandle->websocket->UsesDeterministicSemantics()) + if (RequestsWebSocketCompression(websocketHandle->websocket->Options())) { return *m_wsppProvider; } diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index a53c1026..e9d8a29d 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -74,6 +74,10 @@ constexpr size_t WsppConfiguredMaxMessageSize = websocketpp::config::core_client constexpr size_t WsppMaxZlibInputSize = static_cast((std::numeric_limits::max)()); constexpr size_t WebSocketCallbackPayloadSizeLimit = static_cast((std::numeric_limits::max)()); +static_assert( + WsppConfiguredMaxMessageSize == WEBSOCKET_RECVBUFFER_MAXSIZE_DETERMINISTIC_DEFAULT, + "deterministic default max message size must stay aligned with websocketpp's configured default"); + static_assert( WsppConfiguredMaxMessageSize <= WsppMaxZlibInputSize, "websocketpp max message size must fit in zlib's uInt input width"); @@ -161,9 +165,7 @@ size_t ResolveWsppMaxMessageSize(HCWebsocketHandle websocketHandle) noexcept return WsppConfiguredMaxMessageSize; } - return websocket->MaxReceiveBufferSizeExplicitlySet() ? - websocket->MaxReceiveBufferSize() : - WsppConfiguredMaxMessageSize; + return websocket->DeterministicMaxReceiveBufferSize(); } bool TryParseProxyUri( diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 00ca6cd3..0a700afb 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -456,6 +456,13 @@ size_t WebSocket::MaxReceiveBufferSize() const noexcept return m_maxReceiveBufferSize; } +size_t WebSocket::DeterministicMaxReceiveBufferSize() const noexcept +{ + return m_maxReceiveBufferSizeExplicitlySet ? + m_maxReceiveBufferSize : + WEBSOCKET_RECVBUFFER_MAXSIZE_DETERMINISTIC_DEFAULT; +} + bool WebSocket::MaxReceiveBufferSizeExplicitlySet() const noexcept { return m_maxReceiveBufferSizeExplicitlySet; diff --git a/Source/WebSocket/hcwebsocket.h b/Source/WebSocket/hcwebsocket.h index fc490f07..96f17c15 100644 --- a/Source/WebSocket/hcwebsocket.h +++ b/Source/WebSocket/hcwebsocket.h @@ -88,6 +88,8 @@ void ObserverDeleter::operator()(HC_WEBSOCKET_OBSERVER* ptr) noexcept ptr->Release(); } +constexpr size_t WEBSOCKET_RECVBUFFER_MAXSIZE_DETERMINISTIC_DEFAULT = 32000000; + class WebSocket : public std::enable_shared_from_this { public: @@ -136,6 +138,7 @@ class WebSocket : public std::enable_shared_from_this const http_internal_string& ProxyUri() const noexcept; const bool ProxyDecryptsHttps() const noexcept; size_t MaxReceiveBufferSize() const noexcept; + size_t DeterministicMaxReceiveBufferSize() const noexcept; bool MaxReceiveBufferSizeExplicitlySet() const noexcept; uint32_t PingInterval() const noexcept; HCWebSocketOptions Options() const noexcept; diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp index 8ddc176a..bda18fca 100644 --- a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp @@ -1323,6 +1323,177 @@ bool ValidateDefaultCompressionOptionsBehavior() return true; } +bool ValidateDeterministicReceiveLimit( + CompressionEchoServer& server, + XTaskQueueHandle queue) +{ + ClientState state; + HCWebsocketHandle websocket{ nullptr }; + ScopeGuard cleanup([&]() + { + if (websocket != nullptr) + { + HCWebSocketCloseHandle(websocket); + } + }); + + HRESULT hr = HCWebSocketCreate(&websocket, OnTextMessage, nullptr, OnClose, &state); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketCreate(deterministic receive limit)", hr); + return false; + } + + hr = ConfigureTestWebSocket(websocket, state); + if (FAILED(hr)) + { + PrintHr("[FAIL] ConfigureTestWebSocket(deterministic receive limit)", hr); + return false; + } + + hr = HCWebSocketSetMaxReceiveBufferSize(websocket, ReceiveBufferSize); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSetMaxReceiveBufferSize(deterministic receive limit)", hr); + return false; + } + + hr = HCWebSocketSetOptions(websocket, HCWebSocketOptions::None); + if (hr == E_NOT_SUPPORTED) + { + PrintHr("[INFO] HCWebSocketSetOptions(None)", hr); + return true; + } + + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSetOptions(None)", hr); + return false; + } + + server.ResetObservedConnection(); + + XAsyncBlock connectAsync{}; + connectAsync.queue = queue; + + hr = HCWebSocketConnectAsync("ws://127.0.0.1:39002", "", websocket, &connectAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketConnectAsync(deterministic receive limit)", hr); + return false; + } + + hr = XAsyncGetStatus(&connectAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(connect deterministic receive limit)", hr); + return false; + } + + WebSocketCompletionResult connectResult{}; + hr = HCGetWebSocketConnectResult(&connectAsync, &connectResult); + if (FAILED(hr) || FAILED(connectResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketConnectResult(deterministic receive limit)", hr); + PrintHr("[FAIL] Connect result(deterministic receive limit)", connectResult.errorCode); + return false; + } + + if (!server.WaitForConnection()) + { + std::printf("[FAIL] Timed out waiting for deterministic receive-limit connection.\n"); + if (!server.Error().empty()) + { + std::printf("[FAIL] Server error: %s\n", server.Error().c_str()); + } + return false; + } + + if (!ValidateUpgradeResponseHeaders( + websocket, + CompressionExpectation::Unsupported, + "DeterministicReceiveLimit")) + { + return false; + } + + XAsyncBlock sendAsync{}; + sendAsync.queue = queue; + + hr = HCWebSocketSendMessageAsync(websocket, "hello", &sendAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSendMessageAsync(deterministic receive limit)", hr); + return false; + } + + hr = XAsyncGetStatus(&sendAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(send deterministic receive limit)", hr); + return false; + } + + WebSocketCompletionResult sendResult{}; + hr = HCGetWebSocketSendMessageResult(&sendAsync, &sendResult); + if (FAILED(hr) || FAILED(sendResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketSendMessageResult(deterministic receive limit)", hr); + PrintHr("[FAIL] Send result(deterministic receive limit)", sendResult.errorCode); + return false; + } + + if (!WaitForEcho(state, 1, "hello")) + { + std::printf("[FAIL] Timed out waiting for deterministic receive-limit baseline echo.\n"); + return false; + } + + std::string const largePayload = BuildLargePayload(); + sendAsync = {}; + sendAsync.queue = queue; + + hr = HCWebSocketSendMessageAsync(websocket, largePayload.c_str(), &sendAsync); + if (FAILED(hr)) + { + PrintHr("[FAIL] HCWebSocketSendMessageAsync(large deterministic receive limit)", hr); + return false; + } + + hr = XAsyncGetStatus(&sendAsync, true); + if (FAILED(hr)) + { + PrintHr("[FAIL] XAsyncGetStatus(send large deterministic receive limit)", hr); + return false; + } + + sendResult = {}; + hr = HCGetWebSocketSendMessageResult(&sendAsync, &sendResult); + if (FAILED(hr) || FAILED(sendResult.errorCode)) + { + PrintHr("[FAIL] HCGetWebSocketSendMessageResult(large deterministic receive limit)", hr); + PrintHr("[FAIL] Send large result(deterministic receive limit)", sendResult.errorCode); + return false; + } + + if (!WaitForClose(state, 1, HCWebSocketCloseStatus::TooLarge)) + { + std::printf("[FAIL] Timed out waiting for deterministic receive-limit close.\n"); + return false; + } + + { + std::lock_guard lock(state.mutex); + if (state.textMessagesReceived != 1) + { + std::printf("[FAIL] Oversized deterministic receive unexpectedly surfaced a full-message callback.\n"); + return false; + } + } + + return true; +} + bool ValidateCompressionNegotiationScenario( CompressionEchoServer& server, XTaskQueueHandle queue, @@ -1570,6 +1741,11 @@ int main() server.Stop(); }); + if (!ValidateDeterministicReceiveLimit(server, queue)) + { + return 1; + } + char const* uri = "ws://127.0.0.1:39002"; XAsyncBlock connectAsync{}; connectAsync.queue = queue; From 0fa4a01c9079bd686f00579214bc53afe4302fd6 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 23:21:39 -0700 Subject: [PATCH 06/24] Update third-party compliance metadata (AI Review) - Add boost-wintls (BSL-1.0) to NOTICE.txt and cgmanifest.json - Update asio commit hash from 22afb86 to 03ae834 (asio-1-32-0) in both files to match the current submodule pointer --- NOTICE.txt | 19 ++++++++++++++++++- cgmanifest.json | 12 +++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/NOTICE.txt b/NOTICE.txt index dcee89f5..17c45dec 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -313,7 +313,7 @@ SOFTWARE. --------------------------------------------------------- -chriskohlhoff/asio 22afb86087a77037cd296d27134756c9b0d2cb75 - BSL-1.0 +chriskohlhoff/asio 03ae834edbace31a96157b89bf50e5ee464e5ef9 - BSL-1.0 Copyright (c) 2004 @@ -343,6 +343,23 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI --------------------------------------------------------- +laudrup/boost-wintls 5a16a857e03b8d75d0aed8a5ad918bca442dbe27 - BSL-1.0 + + +Copyright (c) 2020-2024 Kasper Laudrup (laudrup at stacktrace dot dk) + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization obtaining a copy of the software and accompanying documentation covered by this license (the "Software") to use, reproduce, display, distribute, execute, and transmit the Software, and to prepare derivative works of the Software, and to permit third-parties to whom the Software is furnished to do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including the above license grant, this restriction and the following disclaimer, must be included in all copies of the Software, in whole or in part, and all derivative works of the Software, unless such copies or derivative works are solely in the form of machine-executable object code generated by a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------------------------- + +--------------------------------------------------------- + curl/curl 801bd5138ce31aa0d906fa4e2eabfc599d74e793 - curl diff --git a/cgmanifest.json b/cgmanifest.json index 32ee9ecf..ccdee42d 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -27,7 +27,17 @@ "Type": "git", "git": { "RepositoryUrl": "https://github.com/chriskohlhoff/asio", - "CommitHash": "22afb86087a77037cd296d27134756c9b0d2cb75" + "CommitHash": "03ae834edbace31a96157b89bf50e5ee464e5ef9" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "git", + "git": { + "RepositoryUrl": "https://github.com/laudrup/boost-wintls", + "CommitHash": "5a16a857e03b8d75d0aed8a5ad918bca442dbe27" } }, "DevelopmentDependency": false From 0a76816d198a9438a7dd838d6af94b688bd5d9a3 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 23:22:19 -0700 Subject: [PATCH 07/24] Fix WebSocket API issues (AI Review) - Add DEFINE_ENUM_FLAG_OPERATORS(HCWebSocketOptions) so callers can combine flags with | without casting through uint32_t - Add m_stateMutex lock to SetOptions() to match the locking pattern already used in SetMaxReceiveBufferSize() - Add m_stateMutex lock and pre-connect guard to SetPingInterval() for consistency with all other pre-connect setters - Return E_INVALIDARG from GetResponseHeaderAtIndex() when headerIndex is out of range instead of returning S_OK with null output pointers --- Include/httpClient/httpClient.h | 2 ++ Source/WebSocket/hcwebsocket.cpp | 7 ++++++- Tests/UnitTests/Tests/WebsocketTests.cpp | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Include/httpClient/httpClient.h b/Include/httpClient/httpClient.h index fd9ce9b2..1b80d8ce 100644 --- a/Include/httpClient/httpClient.h +++ b/Include/httpClient/httpClient.h @@ -996,6 +996,8 @@ enum class HCWebSocketOptions : uint32_t CompressionClientNoContextTakeover = 0x00000004 }; +DEFINE_ENUM_FLAG_OPERATORS(HCWebSocketOptions) + /// /// Creates an WebSocket handle. /// diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 0a700afb..2662016b 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -433,7 +433,7 @@ HRESULT WebSocket::GetResponseHeaderAtIndex( *headerName = nullptr; *headerValue = nullptr; - return S_OK; + return E_INVALIDARG; } const http_internal_string& WebSocket::ProxyUri() const noexcept @@ -535,6 +535,9 @@ HRESULT WebSocket::SetMaxReceiveBufferSize(size_t maxReceiveBufferSizeBytes) noe HRESULT WebSocket::SetPingInterval(uint32_t pingInterval) noexcept { + std::lock_guard lock{ m_stateMutex }; + RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); + m_pingInterval = pingInterval; return S_OK; } @@ -549,6 +552,8 @@ HRESULT WebSocket::SetOptions(HCWebSocketOptions options) noexcept RETURN_HR_IF( E_INVALIDARG, HasNoContextTakeoverWebSocketOptions(options) && !RequestsWebSocketCompression(options)); + + std::lock_guard lock{ m_stateMutex }; RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); if (RequestsLegacyWebSocketSemantics(options)) { diff --git a/Tests/UnitTests/Tests/WebsocketTests.cpp b/Tests/UnitTests/Tests/WebsocketTests.cpp index 3fed010f..efddd98f 100644 --- a/Tests/UnitTests/Tests/WebsocketTests.cpp +++ b/Tests/UnitTests/Tests/WebsocketTests.cpp @@ -1019,7 +1019,7 @@ DEFINE_TEST_CLASS(WebsocketTests) const CHAR* headerName = "sentinel"; headerValue = "sentinel"; - VERIFY_ARE_EQUAL(S_OK, HCWebSocketGetResponseHeaderAtIndex(call, 0, &headerName, &headerValue)); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketGetResponseHeaderAtIndex(call, 0, &headerName, &headerValue)); VERIFY_IS_NULL(headerName); VERIFY_IS_NULL(headerValue); From 81436e88f78e0f5efe0c675fb1c8fd987165e8c1 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 23:25:34 -0700 Subject: [PATCH 08/24] Build hygiene and documentation comments (AI Review) - Add Websocketpp and WinHttp Hybrid filter entries to the Win32.Shared vcxitems.filters so the new files appear under proper Solution Explorer folders (matching the existing GDK.Shared pattern) - Document the intentional raw new in wintls_socket.hpp where websocketpp's transport layer requires std::shared_ptr - Document the leak-rather-than-deadlock design choice in the detached joiner thread shutdown fallback - Note the intentional proxy URI parser duplication between websocketpp_websocket.cpp and winhttp_connection.cpp --- ...ibHttpClient.Win32.Shared.vcxitems.filters | 21 +++++++++++++++++++ Source/HTTP/WinHttp/winhttp_connection.cpp | 2 ++ .../Websocketpp/websocketpp_websocket.cpp | 6 ++++++ .../WebSocket/Websocketpp/wintls_socket.hpp | 3 +++ 4 files changed, 32 insertions(+) diff --git a/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems.filters b/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems.filters index f9d099ba..1975d5d7 100644 --- a/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems.filters +++ b/Build/libHttpClient.Win32.Shared/libHttpClient.Win32.Shared.vcxitems.filters @@ -28,6 +28,12 @@ {a33dcb7f-3435-401d-8b9c-4c30fd992feb} + + {7e1a42c8-f2b0-4d1a-9c3e-5f8a6b7d0e12} + + + {8d2b53f9-a1c0-4e7f-b6d4-3f9e5c8a1b23} + @@ -52,6 +58,12 @@ Source\Platform\Windows + + Source\HTTP\WinHttp\Hybrid + + + Source\WebSocket\Websocketpp + @@ -63,5 +75,14 @@ Source\HTTP\WinHttp + + Source\HTTP\WinHttp\Hybrid + + + Source\WebSocket\Websocketpp + + + Source\WebSocket\Websocketpp + \ No newline at end of file diff --git a/Source/HTTP/WinHttp/winhttp_connection.cpp b/Source/HTTP/WinHttp/winhttp_connection.cpp index 6fdcb1c9..e0351d5a 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.cpp +++ b/Source/HTTP/WinHttp/winhttp_connection.cpp @@ -32,6 +32,8 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN namespace { +// Note: this logic is intentionally duplicated from TryParseProxyUri in +// websocketpp_websocket.cpp to keep compilation units independent. bool TryParseWebSocketProxyUri( http_internal_string const& rawProxyUri, Uri& proxyUri diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index e9d8a29d..5440408c 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -168,6 +168,8 @@ size_t ResolveWsppMaxMessageSize(HCWebsocketHandle websocketHandle) noexcept return websocket->DeterministicMaxReceiveBufferSize(); } +// Note: this logic is intentionally duplicated from TryParseWebSocketProxyUri in +// winhttp_connection.cpp to keep compilation units independent. bool TryParseProxyUri( http_internal_string const& rawProxyUri, Uri& proxyUri @@ -1649,6 +1651,10 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared try { + // Intentional: the detached thread will block on join() until the + // ASIO thread exits, then complete shutdown asynchronously. If the + // ASIO thread is genuinely hung, this leaks the impl rather than + // deadlocking the caller — that is the intended tradeoff. std::thread( [ shutdownContext diff --git a/Source/WebSocket/Websocketpp/wintls_socket.hpp b/Source/WebSocket/Websocketpp/wintls_socket.hpp index 5ded7944..758406ea 100644 --- a/Source/WebSocket/Websocketpp/wintls_socket.hpp +++ b/Source/WebSocket/Websocketpp/wintls_socket.hpp @@ -118,6 +118,9 @@ class connection : public lib::enable_shared_from_this { return socket::make_error_code(socket::error::invalid_tls_context); } + // Uses raw new because websocketpp's transport layer manages this via + // lib::shared_ptr (std::shared_ptr), which cannot use the project's + // custom allocator. m_socket.reset(new socket_type(lib::asio::ip::tcp::socket(*service), *m_context)); if (m_socket_init_handler) From d1e0f903cb5fa6c2d9ad0999060cccf883d63144 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 15 Apr 2026 23:55:55 -0700 Subject: [PATCH 09/24] Simplify WebSocket option flag handling (AI Review) --- .../Websocketpp/websocketpp_websocket.cpp | 10 ++--- Source/WebSocket/hcwebsocket.cpp | 2 +- Tests/UnitTests/Tests/WebsocketTests.cpp | 43 +++++++------------ .../WebSocketCompressionTests.cpp | 27 +++--------- 4 files changed, 29 insertions(+), 53 deletions(-) diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index 5440408c..51b6b41b 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -85,9 +85,9 @@ static_assert( WsppConfiguredMaxMessageSize <= WebSocketCallbackPayloadSizeLimit, "websocketpp max message size must fit in websocket callback size fields"); -constexpr uint32_t RequestCompressionOptionMask = static_cast(HCWebSocketOptions::RequestCompression); -constexpr uint32_t ServerNoContextTakeoverOptionMask = static_cast(HCWebSocketOptions::CompressionServerNoContextTakeover); -constexpr uint32_t ClientNoContextTakeoverOptionMask = static_cast(HCWebSocketOptions::CompressionClientNoContextTakeover); +constexpr HCWebSocketOptions RequestCompressionOptionMask = HCWebSocketOptions::RequestCompression; +constexpr HCWebSocketOptions ServerNoContextTakeoverOptionMask = HCWebSocketOptions::CompressionServerNoContextTakeover; +constexpr HCWebSocketOptions ClientNoContextTakeoverOptionMask = HCWebSocketOptions::CompressionClientNoContextTakeover; enum class CompressionClientPolicy { @@ -97,9 +97,9 @@ enum class CompressionClientPolicy ServerAndClientNoContextTakeover }; -bool HasCompressionOption(HCWebSocketOptions options, uint32_t optionMask) noexcept +bool HasCompressionOption(HCWebSocketOptions options, HCWebSocketOptions optionMask) noexcept { - return (static_cast(options) & optionMask) != 0; + return (options & optionMask) != HCWebSocketOptions::None; } bool ShouldUseCompression(HCWebsocketHandle websocketHandle) noexcept diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 2662016b..9320ee14 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -548,7 +548,7 @@ HRESULT WebSocket::SetOptions(HCWebSocketOptions options) noexcept RETURN_HR_IF( E_INVALIDARG, RequestsLegacyWebSocketSemantics(options) && - RawWebSocketOptions(options) != static_cast(HCWebSocketOptions::LegacySemantics)); + options != HCWebSocketOptions::LegacySemantics); RETURN_HR_IF( E_INVALIDARG, HasNoContextTakeoverWebSocketOptions(options) && !RequestsWebSocketCompression(options)); diff --git a/Tests/UnitTests/Tests/WebsocketTests.cpp b/Tests/UnitTests/Tests/WebsocketTests.cpp index efddd98f..5f5dc9a1 100644 --- a/Tests/UnitTests/Tests/WebsocketTests.cpp +++ b/Tests/UnitTests/Tests/WebsocketTests.cpp @@ -404,15 +404,6 @@ HRESULT CALLBACK Test_Internal_HCWebSocketDisconnect( return S_OK; } -constexpr HCWebSocketOptions CombineCompressionOptions( - HCWebSocketOptions lhs, - HCWebSocketOptions rhs) noexcept -{ - return static_cast( - static_cast(lhs) | - static_cast(rhs)); -} - class UnsupportedOptionsTestProvider final : public IWebSocketProvider { public: @@ -520,24 +511,22 @@ class ConfigurableCompressionTestProvider final : public IWebSocketProvider HRESULT OptionsResult(HCWebSocketOptions options) const noexcept override { - constexpr uint32_t requestCompression = static_cast(HCWebSocketOptions::RequestCompression); - constexpr uint32_t noContextTakeoverMask = - static_cast(HCWebSocketOptions::CompressionServerNoContextTakeover) | - static_cast(HCWebSocketOptions::CompressionClientNoContextTakeover); - constexpr uint32_t legacySemantics = static_cast(HCWebSocketOptions::LegacySemantics); - auto const rawOptions = static_cast(options); - - if ((rawOptions & legacySemantics) != 0) + auto const noContextTakeoverMask = + HCWebSocketOptions::CompressionServerNoContextTakeover | + HCWebSocketOptions::CompressionClientNoContextTakeover; + + if ((options & HCWebSocketOptions::LegacySemantics) != HCWebSocketOptions::None) { return S_OK; } - if ((rawOptions & ~(requestCompression | noContextTakeoverMask)) != 0) + if ((options & ~(HCWebSocketOptions::RequestCompression | noContextTakeoverMask)) != HCWebSocketOptions::None) { return E_NOT_SUPPORTED; } - if ((rawOptions & noContextTakeoverMask) != 0 && (rawOptions & requestCompression) == 0) + if ((options & noContextTakeoverMask) != HCWebSocketOptions::None && + (options & HCWebSocketOptions::RequestCompression) == HCWebSocketOptions::None) { return E_NOT_SUPPORTED; } @@ -738,9 +727,9 @@ DEFINE_TEST_CLASS(WebsocketTests) auto const requestCompression = HCWebSocketOptions::RequestCompression; auto const serverNoContextTakeover = HCWebSocketOptions::CompressionServerNoContextTakeover; auto const clientNoContextTakeover = HCWebSocketOptions::CompressionClientNoContextTakeover; - auto const requestWithServerNoContextTakeover = CombineCompressionOptions(requestCompression, serverNoContextTakeover); - auto const requestWithClientNoContextTakeover = CombineCompressionOptions(requestCompression, clientNoContextTakeover); - auto const requestWithBothNoContextTakeover = CombineCompressionOptions(requestWithServerNoContextTakeover, clientNoContextTakeover); + auto const requestWithServerNoContextTakeover = requestCompression | serverNoContextTakeover; + auto const requestWithClientNoContextTakeover = requestCompression | clientNoContextTakeover; + auto const requestWithBothNoContextTakeover = requestWithServerNoContextTakeover | clientNoContextTakeover; VERIFY_ARE_EQUAL(S_OK, HCSetWebSocketFunctions(Test_Internal_HCWebSocketConnectAsync, Test_Internal_HCWebSocketSendMessageAsync, Test_Internal_HCWebSocketSendBinaryMessageAsync, Test_Internal_HCWebSocketDisconnect, nullptr)); VERIFY_ARE_EQUAL(S_OK, HCInitialize(nullptr)); @@ -753,10 +742,10 @@ DEFINE_TEST_CLASS(WebsocketTests) VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(nullptr, HCWebSocketOptions::None)); VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, static_cast(0x8))); - VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, CombineCompressionOptions(legacySemantics, requestCompression))); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, legacySemantics | requestCompression)); VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, serverNoContextTakeover)); VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, clientNoContextTakeover)); - VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, CombineCompressionOptions(serverNoContextTakeover, clientNoContextTakeover))); + VERIFY_ARE_EQUAL(E_INVALIDARG, HCWebSocketSetOptions(websocket, serverNoContextTakeover | clientNoContextTakeover)); VERIFY_ARE_EQUAL(S_OK, HCWebSocketSetOptions(websocket, legacySemantics)); VERIFY_ARE_EQUAL(static_cast(legacySemantics), static_cast(websocket->websocket->Options())); VERIFY_IS_TRUE(websocket->websocket->UsesLegacySemantics()); @@ -812,9 +801,9 @@ DEFINE_TEST_CLASS(WebsocketTests) { auto const legacySemantics = HCWebSocketOptions::LegacySemantics; auto const requestCompression = HCWebSocketOptions::RequestCompression; - auto const requestWithServerNoContextTakeover = CombineCompressionOptions(requestCompression, HCWebSocketOptions::CompressionServerNoContextTakeover); - auto const requestWithClientNoContextTakeover = CombineCompressionOptions(requestCompression, HCWebSocketOptions::CompressionClientNoContextTakeover); - auto const requestWithBothNoContextTakeover = CombineCompressionOptions(requestWithServerNoContextTakeover, HCWebSocketOptions::CompressionClientNoContextTakeover); + auto const requestWithServerNoContextTakeover = requestCompression | HCWebSocketOptions::CompressionServerNoContextTakeover; + auto const requestWithClientNoContextTakeover = requestCompression | HCWebSocketOptions::CompressionClientNoContextTakeover; + auto const requestWithBothNoContextTakeover = requestWithServerNoContextTakeover | HCWebSocketOptions::CompressionClientNoContextTakeover; ConfigurableCompressionTestProvider provider; auto observer = HC_WEBSOCKET_OBSERVER::Initialize( diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp index bda18fca..92c025a7 100644 --- a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp @@ -67,15 +67,6 @@ enum class CompressionExpectation Unsupported }; -constexpr HCWebSocketOptions CombineCompressionOptions( - HCWebSocketOptions lhs, - HCWebSocketOptions rhs) noexcept -{ - return static_cast( - static_cast(lhs) | - static_cast(rhs)); -} - constexpr size_t ReceiveBufferSize = 4096; void PrintHr(char const* operation, HRESULT hr) @@ -1914,9 +1905,8 @@ int main() server, queue, "RequestCompression|CompressionServerNoContextTakeover", - CombineCompressionOptions( - HCWebSocketOptions::RequestCompression, - HCWebSocketOptions::CompressionServerNoContextTakeover), + HCWebSocketOptions::RequestCompression | + HCWebSocketOptions::CompressionServerNoContextTakeover, true, false)) { @@ -1927,9 +1917,8 @@ int main() server, queue, "RequestCompression|CompressionClientNoContextTakeover", - CombineCompressionOptions( - HCWebSocketOptions::RequestCompression, - HCWebSocketOptions::CompressionClientNoContextTakeover), + HCWebSocketOptions::RequestCompression | + HCWebSocketOptions::CompressionClientNoContextTakeover, false, true)) { @@ -1940,11 +1929,9 @@ int main() server, queue, "RequestCompression|CompressionServerNoContextTakeover|CompressionClientNoContextTakeover", - CombineCompressionOptions( - CombineCompressionOptions( - HCWebSocketOptions::RequestCompression, - HCWebSocketOptions::CompressionServerNoContextTakeover), - HCWebSocketOptions::CompressionClientNoContextTakeover), + HCWebSocketOptions::RequestCompression | + HCWebSocketOptions::CompressionServerNoContextTakeover | + HCWebSocketOptions::CompressionClientNoContextTakeover, true, true)) { From b1b8d97a307cc2251450bbddb30c9b6ed439edec Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 22 Apr 2026 09:54:56 -0700 Subject: [PATCH 10/24] Handle websocketpp close status consistently --- .../Websocketpp/websocketpp_websocket.cpp | 40 ++++++++++++++++++- .../WebSocketCompressionTests.cpp | 20 +++++++++- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index 51b6b41b..1a7a1ed8 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #if HC_PLATFORM == HC_PLATFORM_ANDROID #include "../HTTP/Android/android_platform_context.h" #endif @@ -189,6 +190,25 @@ bool TryParseProxyUri( return proxyUri.IsValid(); } +websocketpp::close::status::value ResolveObservedCloseCode( + websocketpp::close::status::value localCloseCode, + websocketpp::close::status::value remoteCloseCode, + websocketpp::lib::error_code const& ec) noexcept +{ + auto closeCode = localCloseCode != websocketpp::close::status::blank ? localCloseCode : remoteCloseCode; + if (closeCode == websocketpp::close::status::blank || + closeCode == websocketpp::close::status::abnormal_close) + { + auto const mappedCloseCode = websocketpp::processor::error::to_ws(ec); + if (mappedCloseCode != websocketpp::close::status::blank) + { + closeCode = mappedCloseCode; + } + } + + return closeCode; +} + http_internal_string BuildProxyEndpointUri(Uri const& proxyUri) { http_internal_string proxyEndpointUri{ proxyUri.Scheme() }; @@ -207,7 +227,6 @@ bool TryPercentDecodeUserInfo(http_internal_string const& value, http_internal_s { decoded.clear(); decoded.reserve(value.size()); - for (size_t i = 0; i < value.size(); ++i) { if (value[i] == '%') @@ -1048,6 +1067,23 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared client.set_close_handler([sharedThis](websocketpp::connection_hdl) { ASSERT(sharedThis->m_state == CONNECTED || sharedThis->m_state == DISCONNECTING); + { + std::lock_guard lock{ sharedThis->m_wsppClientLock }; + if (sharedThis->m_client != nullptr) + { + auto& closeClient = sharedThis->m_client->impl(); + websocketpp::lib::error_code connectionEc{}; + auto connection = closeClient.get_con_from_hdl(sharedThis->m_con, connectionEc); + if (!connectionEc && connection) + { + sharedThis->m_closeCode = ResolveObservedCloseCode( + connection->get_local_close_code(), + connection->get_remote_close_code(), + connection->get_ec()); + } + } + } + sharedThis->shutdown_wspp_impl([sharedThis]() { HCWebSocketCloseEventFunction closeFunc{ nullptr }; @@ -1591,7 +1627,7 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared const auto &connection = client.get_con_from_hdl(m_con); auto const localCloseCode = connection->get_local_close_code(); auto const remoteCloseCode = connection->get_remote_close_code(); - m_closeCode = localCloseCode != websocketpp::close::status::blank ? localCloseCode : remoteCloseCode; + m_closeCode = ResolveObservedCloseCode(localCloseCode, remoteCloseCode, connection->get_ec()); client.stop_perpetual(); // Yield and wait for background thread to finish diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp index 92c025a7..e5d30a95 100644 --- a/Tests/WebSocketCompression/WebSocketCompressionTests.cpp +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.cpp @@ -829,10 +829,26 @@ bool WaitForEcho(ClientState& state, size_t expectedTextMessagesReceived, std::s bool WaitForClose(ClientState& state, size_t expectedCloseEventsReceived, HCWebSocketCloseStatus expectedStatus) { std::unique_lock lock(state.mutex); - return state.cv.wait_for(lock, Timeout, [&state, expectedCloseEventsReceived, expectedStatus]() + bool const signaled = state.cv.wait_for(lock, Timeout, [&state, expectedCloseEventsReceived]() { - return state.closeEventsReceived >= expectedCloseEventsReceived && state.lastCloseStatus == expectedStatus; + return state.closeEventsReceived >= expectedCloseEventsReceived; }); + + if (!signaled) + { + return false; + } + + if (state.lastCloseStatus != expectedStatus) + { + std::printf( + "[INFO] Observed close status %u but expected %u.\n", + static_cast(state.lastCloseStatus), + static_cast(expectedStatus)); + return false; + } + + return true; } HRESULT ConfigureTestWebSocket(HCWebsocketHandle websocket, ClientState& state) From c4753395e264d51d642ec0071816062aa158e935 Mon Sep 17 00:00:00 2001 From: jhugard Date: Wed, 22 Apr 2026 17:25:21 -0700 Subject: [PATCH 11/24] Fix websocket async queue topology --- Source/Global/NetworkState.cpp | 6 +++--- Source/WebSocket/Websocketpp/websocketpp_websocket.cpp | 6 +++--- Source/WebSocket/Websocketpp/websocketpp_websocket.h | 2 -- Source/WebSocket/hcwebsocket.cpp | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Source/Global/NetworkState.cpp b/Source/Global/NetworkState.cpp index a2391e5a..f09bc707 100644 --- a/Source/Global/NetworkState.cpp +++ b/Source/Global/NetworkState.cpp @@ -360,10 +360,10 @@ HRESULT CALLBACK NetworkState::WebSocketConnectAsyncProvider(XAsyncOp op, const { case XAsyncOp::Begin: { - XTaskQueuePortHandle workPort{}; assert(data->async->queue); // Queue should never be null here - RETURN_IF_FAILED(XTaskQueueGetPort(data->async->queue, XTaskQueuePort::Work, &workPort)); - RETURN_IF_FAILED(XTaskQueueCreateComposite(workPort, workPort, &context->internalAsyncBlock.queue)); + RETURN_IF_FAILED(XTaskQueueDuplicateHandle( + data->async->queue, + &context->internalAsyncBlock.queue)); std::unique_lock lock{ state.m_mutex }; state.m_connectingWebSockets.insert(context->clientAsyncBlock); diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index 1a7a1ed8..935784f5 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -995,9 +995,9 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared { if (async->queue) { - XTaskQueuePortHandle worker{ nullptr }; - RETURN_IF_FAILED(XTaskQueueGetPort(async->queue, XTaskQueuePort::Work, &worker)); - RETURN_IF_FAILED(XTaskQueueCreateComposite(worker, worker, &m_backgroundQueue)); + RETURN_IF_FAILED(XTaskQueueDuplicateHandle( + async->queue, + &m_backgroundQueue)); } auto &client = m_client->impl(); diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.h b/Source/WebSocket/Websocketpp/websocketpp_websocket.h index 6c71ea9f..6ff34fc2 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.h +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.h @@ -60,9 +60,7 @@ class WebSocketppProvider : public IWebSocketProvider, public IProviderLifecycle std::mutex m_connectionsMutex; std::vector> m_connections; -#if HC_PLATFORM == HC_PLATFORM_GDK std::atomic m_isSuspended{ false }; -#endif }; #endif diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 9320ee14..7f3beb2c 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -225,9 +225,9 @@ HRESULT CALLBACK WebSocket::ConnectAsyncProvider(XAsyncOp op, XAsyncProviderData RETURN_HR_IF(E_UNEXPECTED, ws->m_state != State::Initial); ws->ClearResponseHeadersLockHeld(); - XTaskQueuePortHandle workPort{ nullptr }; - XTaskQueueGetPort(data->async->queue, XTaskQueuePort::Work, &workPort); - XTaskQueueCreateComposite(workPort, workPort, &context->internalAsyncBlock.queue); + RETURN_IF_FAILED(XTaskQueueDuplicateHandle( + data->async->queue, + &context->internalAsyncBlock.queue)); ws->m_state = State::Connecting; lock.unlock(); From 10aed2daf911cecc74faba03012d36d8548645a6 Mon Sep 17 00:00:00 2001 From: jhugard Date: Fri, 24 Apr 2026 13:49:23 -0700 Subject: [PATCH 12/24] Address PR feedback on WebSocket semantics docs - clarify deterministic vs legacy WebSocket behavior across platforms - document receive buffer, fragment callback, and compression behavior in the public header and README - reorder pre-connect WebSocket APIs in the header to match the intended setup flow - scope WinHTTP websocket-only proxy helpers under HC_NOWEBSOCKETS Co-authored-by: Copilot --- Include/httpClient/httpClient.h | 152 ++++++++++++--------- README.md | 6 +- Source/HTTP/WinHttp/winhttp_connection.cpp | 5 +- 3 files changed, 94 insertions(+), 69 deletions(-) diff --git a/Include/httpClient/httpClient.h b/Include/httpClient/httpClient.h index 1b80d8ce..da64abd0 100644 --- a/Include/httpClient/httpClient.h +++ b/Include/httpClient/httpClient.h @@ -975,11 +975,26 @@ typedef void /// enum class HCWebSocketOptions : uint32_t { + /// + /// Select deterministic WebSocket behavior without requesting compression. + /// + /// + /// Deterministic behavior includes no fragment callbacks and a hard cap on + /// inbound message size which can be set with HCWebSocketSetMaxReceiveBufferSize() + /// or defaults to 32,000,000 bytes. + /// None = 0x00000000, /// /// Explicitly preserve the platform's existing legacy WebSocket semantics. /// This flag is mutually exclusive with every other option. /// + /// + /// Legacy behavior preserves the platform's existing defaults. On Win32 and GDK, + /// that includes oversized-payload fragment callbacks rather than the deterministic + /// hard inbound message-size cap. On macOS / iOS and Linux, legacy behavior continues + /// using the provider's configured 32,000,000-byte maximum instead of honoring + /// HCWebSocketSetMaxReceiveBufferSize() as a hard cap. + /// LegacySemantics = 0x80000000, /// /// Request permessage-deflate. Without no-context-takeover flags, compression @@ -1010,7 +1025,10 @@ DEFINE_ENUM_FLAG_OPERATORS(HCWebSocketOptions) /// /// WebSocket usage:
/// Create a WebSocket handle using HCWebSocketCreate()
+/// Optionally call HCWebSocketSetOptions() to select deterministic behavior and/or request compression
+/// Optionally call HCWebSocketSetMaxReceiveBufferSize() to configure the deterministic inbound message-size limit before connect, or the Win32 / GDK legacy fragment threshold
/// Call HCWebSocketSetProxyUri(), HCWebSocketSetHeader(), or HCWebSocketSetPingInterval() to prepare the HCWebsocketHandle
+/// On Win32 / GDK legacy behavior, call HCWebSocketSetBinaryMessageFragmentEventFunction() if oversized incoming payloads must be reconstructed from fragments
/// Call HCWebSocketConnectAsync() to connect the WebSocket using the HCWebsocketHandle.
/// Call HCWebSocketSendMessageAsync() to send a message to the WebSocket using the HCWebsocketHandle.
/// Call HCWebSocketDisconnect() or HCWebSocketDisconnectWithStatus() to disconnect the WebSocket using the HCWebsocketHandle.
@@ -1024,30 +1042,59 @@ STDAPI HCWebSocketCreate( _In_opt_ void* functionContext ) noexcept; -#if HC_PLATFORM == HC_PLATFORM_WIN32 || HC_PLATFORM == HC_PLATFORM_GDK /// -/// Set the binary message fragment handler. The client functionContext passed to HCWebSocketCreate will also be passed to this handler. +/// Set pre-connect WebSocket behavior options. /// -/// The handle of the websocket. -/// A pointer to the binary message fragment handling callback to use, or a null pointer to remove. -/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_NOT_SUPPORTED, or E_FAIL. +/// The handle of the WebSocket. +/// Options to apply. Reserved bits must be zero. +/// LegacySemantics is mutually exclusive with every other flag. CompressionServerNoContextTakeover +/// and CompressionClientNoContextTakeover require RequestCompression. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_HC_CONNECT_ALREADY_CALLED, or E_NOT_SUPPORTED. /// -/// On Win32 and GDK legacy behavior, this callback is used when an incoming payload exceeds the -/// configured receive buffer (default 20KB). Current oversized UTF-8 overflow behavior also uses -/// this raw-byte fragment path. +/// This must be called prior to calling HCWebSocketConnectAsync. /// -/// For applications expecting oversized incoming payloads, you MUST either: -/// 1. Set this fragment handler to properly reconstruct messages, OR -/// 2. Increase the receive buffer size with HCWebSocketSetMaxReceiveBufferSize() to accommodate your largest expected message +/// If this API is never called, the socket uses the platform's legacy behavior. +/// On macOS / iOS and Linux legacy behavior, HCWebSocketSetMaxReceiveBufferSize() does not override +/// the provider's configured 32,000,000-byte maximum. +/// Calling HCWebSocketSetOptions(HCWebSocketOptions::LegacySemantics) explicitly preserves that same legacy behavior. +/// Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior. /// -/// Without this handler, oversized incoming payloads are not delivered through HCWebSocketMessageFunction -/// or HCWebSocketBinaryMessageFunction. +/// In deterministic behavior, fragment callbacks are not supported. The inbound message-size limit becomes a hard cap: +/// the value passed to HCWebSocketSetMaxReceiveBufferSize() if set before connect, otherwise the default +/// deterministic limit of 32,000,000 bytes. When an incoming message exceeds that cap, the socket closes with +/// HCWebSocketCloseStatus::TooLarge. /// -/// Fragment callbacks are not supported after selecting deterministic behavior with HCWebSocketSetOptions(). +/// RequestCompression alone reuses compression context in both directions by default. /// -STDAPI HCWebSocketSetBinaryMessageFragmentEventFunction( +#if HC_PLATFORM != HC_PLATFORM_ANDROID +STDAPI HCWebSocketSetOptions( _In_ HCWebsocketHandle websocket, - _In_ HCWebSocketBinaryMessageFragmentFunction binaryMessageFragmentFunc + _In_ HCWebSocketOptions options + ) noexcept; +#endif + +#if HC_PLATFORM != HC_PLATFORM_ANDROID +/// +/// Configures the pre-connect inbound message-size limit behavior for built-in WebSocket transports. +/// +/// The handle of the WebSocket +/// Maximum size (in bytes) for the WebSocket receive buffer. Values larger than UINT32_MAX are rejected with E_INVALIDARG. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_HC_CONNECT_ALREADY_CALLED. +/// +/// This must be called prior to calling HCWebSocketConnectAsync. +/// +/// In deterministic behavior, this becomes the hard inbound message-size cap. +/// If you do not call this API before connect, that deterministic cap defaults to 32,000,000 bytes. +/// +/// In legacy Win32 / GDK behavior, the configured receive buffer still controls when oversized incoming +/// payloads are routed through HCWebSocketSetBinaryMessageFragmentEventFunction(). The legacy default value +/// remains 20KB (20,480 bytes). +/// On macOS / iOS and Linux legacy behavior, this API does not change the provider's configured +/// 32,000,000-byte maximum. +/// +STDAPI HCWebSocketSetMaxReceiveBufferSize( + _In_ HCWebsocketHandle websocket, + _In_ size_t bufferSizeInBytes ) noexcept; #endif @@ -1107,33 +1154,6 @@ STDAPI HCWebSocketSetPingInterval( _In_ uint32_t pingIntervalSeconds ) noexcept; -/// -/// Set pre-connect WebSocket behavior options. -/// -/// The handle of the WebSocket. -/// Options to apply. Reserved bits must be zero. -/// LegacySemantics is mutually exclusive with every other flag. CompressionServerNoContextTakeover -/// and CompressionClientNoContextTakeover require RequestCompression. -/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_HC_CONNECT_ALREADY_CALLED, or E_NOT_SUPPORTED. -/// This must be called prior to calling HCWebSocketConnectAsync. -/// -/// If this API is never called, the socket uses the platform's legacy behavior. -/// Calling HCWebSocketSetOptions(HCWebSocketOptions::LegacySemantics) explicitly preserves that same legacy behavior. -/// Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior. -/// -/// In deterministic behavior, fragment callbacks are not supported. The inbound message-size limit becomes a hard cap: -/// the value passed to HCWebSocketSetMaxReceiveBufferSize() if set before connect, otherwise the default -/// deterministic limit of 32,000,000 bytes. When an incoming message exceeds that cap, the socket closes with -/// HCWebSocketCloseStatus::TooLarge. -/// -/// RequestCompression alone reuses compression context in both directions by default. -#if HC_PLATFORM != HC_PLATFORM_ANDROID -STDAPI HCWebSocketSetOptions( - _In_ HCWebsocketHandle websocket, - _In_ HCWebSocketOptions options - ) noexcept; -#endif - /// /// Gets the WebSocket functions to allow callers to respond to incoming messages and WebSocket close events. /// @@ -1164,6 +1184,31 @@ STDAPI HCWebSocketGetBinaryMessageFragmentEventFunction( _Out_ HCWebSocketBinaryMessageFragmentFunction* binaryMessageFragmentFunc, _Out_ void** functionContext ) noexcept; + +/// +/// Set the binary message fragment handler. The client functionContext passed to HCWebSocketCreate will also be passed to this handler. +/// +/// The handle of the websocket. +/// A pointer to the binary message fragment handling callback to use, or a null pointer to remove. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, E_NOT_SUPPORTED, or E_FAIL. +/// +/// On Win32 and GDK legacy behavior, this callback is used when an incoming payload exceeds the +/// configured receive buffer (default 20KB). Current oversized UTF-8 overflow behavior also uses +/// this raw-byte fragment path. +/// +/// For applications expecting oversized incoming payloads, you MUST either: +/// 1. Set this fragment handler to properly reconstruct messages, OR +/// 2. Increase the receive buffer size with HCWebSocketSetMaxReceiveBufferSize() to accommodate your largest expected message +/// +/// Without this handler, oversized incoming payloads are not delivered through HCWebSocketMessageFunction +/// or HCWebSocketBinaryMessageFunction. +/// +/// Fragment callbacks are only supported when using HCWebSocketOptions::LegacySemantics. +/// +STDAPI HCWebSocketSetBinaryMessageFragmentEventFunction( + _In_ HCWebsocketHandle websocket, + _In_ HCWebSocketBinaryMessageFragmentFunction binaryMessageFragmentFunc +) noexcept; #endif /// @@ -1286,27 +1331,6 @@ STDAPI HCWebSocketDisconnectWithStatus( _In_ HCWebSocketCloseStatus closeStatus ) noexcept; -#if HC_PLATFORM != HC_PLATFORM_ANDROID -/// -/// Configures the pre-connect inbound message-size limit behavior for built-in WebSocket transports. -/// -/// In deterministic behavior, this becomes the hard inbound message-size cap. -/// If you do not call this API before connect, that deterministic cap defaults to 32,000,000 bytes. -/// -/// In legacy Win32 / GDK behavior, the configured receive buffer still controls when oversized incoming -/// payloads are routed through HCWebSocketSetBinaryMessageFragmentEventFunction(). The legacy default value -/// remains 20KB (20,480 bytes). -/// -/// The handle of the WebSocket -/// Maximum size (in bytes) for the WebSocket receive buffer. Values larger than UINT32_MAX are rejected with E_INVALIDARG. -/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_HC_CONNECT_ALREADY_CALLED. -/// This must be called prior to calling HCWebSocketConnectAsync. -STDAPI HCWebSocketSetMaxReceiveBufferSize( - _In_ HCWebsocketHandle websocket, - _In_ size_t bufferSizeInBytes -) noexcept; -#endif - /// /// Increments the reference count on the call object. /// diff --git a/README.md b/README.md index 2a10904a..6457f5bd 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ libHttpClient provides a platform abstraction layer for HTTP and WebSocket, and 1. Call HCWebSocketCreate() to create a new HCWebsocketHandle with message/binary message/close event callbacks 1. Optionally call HCWebSocketSetOptions() before connect to explicitly preserve legacy behavior or select deterministic behavior and compression requests 1. **On Win32 and GDK legacy behavior, for payloads that may exceed the receive buffer (>20KB by default)**: Call HCWebSocketSetBinaryMessageFragmentEventFunction() to handle oversized incoming payloads -1. Optionally call HCWebSocketSetMaxReceiveBufferSize() and HCWebSocketSetPingInterval() to adjust receive buffering and keepalive behavior +1. Optionally call HCWebSocketSetMaxReceiveBufferSize() before connect to adjust the deterministic inbound message-size limit, or on Win32 / GDK legacy behavior the fragment threshold +1. Optionally call HCWebSocketSetPingInterval() to adjust keepalive behavior 1. Call HCWebSocketConnectAsync() to connect to the WebSocket server 1. Call HCWebSocketSendMessageAsync() or HCWebSocketSendBinaryMessageAsync() to send messages 1. Handle incoming messages via your registered callbacks @@ -66,6 +67,7 @@ libHttpClient provides a platform abstraction layer for HTTP and WebSocket, and - **Legacy Win32 / GDK oversized payload path**: When an incoming payload exceeds the configured receive buffer, it is surfaced through HCWebSocketSetBinaryMessageFragmentEventFunction() as raw bytes. - **Legacy Win32 / GDK text overflow behavior**: Oversized UTF-8 payloads use that same raw-byte fragment callback path. - **Legacy Win32 / GDK without a fragment handler**: Oversized incoming payloads are not surfaced through the public whole-message callbacks unless a fragment handler is installed. +- **Legacy macOS / iOS and Linux max**: On macOS / iOS and Linux legacy behavior, the provider continues using its configured `32,000,000`-byte maximum; `HCWebSocketSetMaxReceiveBufferSize()` does not become a caller-controlled hard cap there. - **Deterministic behavior**: Calling HCWebSocketSetOptions(HCWebSocketOptions::None) or any non-legacy compression flag selects deterministic behavior on supported built-in implementations. Fragment callbacks are not supported there. - **Deterministic inbound limit**: HCWebSocketSetMaxReceiveBufferSize() becomes a hard inbound message-size cap for deterministic behavior. If not set before connect, the deterministic default is `32,000,000` bytes, and oversized messages close the socket with `HCWebSocketCloseStatus::TooLarge`. @@ -84,7 +86,7 @@ Compression for GDK Console can be enabled with the `HC_ENABLE_GDK_XBOX_WEBSOCKE Call `HCWebSocketSetOptions()` on a handle before `HCWebSocketConnectAsync()` to control the built-in WebSocket behavior for that connection. `LegacySemantics` explicitly preserves the existing legacy behavior. `None` selects deterministic behavior without requesting compression. `RequestCompression` selects deterministic behavior and requests `permessage-deflate` compression. Combine `RequestCompression` with `CompressionServerNoContextTakeover` and/or `CompressionClientNoContextTakeover` to request fresh zlib state per message in the corresponding direction. These flags require `RequestCompression`; setting them alone returns `E_INVALIDARG`. -In deterministic behavior, fragment callbacks are not supported and the inbound message-size limit becomes a hard cap. `HCWebSocketSetMaxReceiveBufferSize()` overrides that cap if called before connect; otherwise the deterministic default is `32,000,000` bytes. Legacy Win32 and GDK behavior, including oversized-payload fragment callbacks, remains the default when `HCWebSocketSetOptions()` is not called. +In deterministic behavior, fragment callbacks are not supported and the inbound message-size limit becomes a hard cap. `HCWebSocketSetMaxReceiveBufferSize()` overrides that cap if called before connect; otherwise the deterministic default is `32,000,000` bytes. When `HCWebSocketSetOptions()` is not called, Win32 and GDK remain on legacy fragment-callback behavior, while macOS, iOS, and Linux remain on the provider's configured `32,000,000`-byte maximum. #### Windows proxy and TLS notes diff --git a/Source/HTTP/WinHttp/winhttp_connection.cpp b/Source/HTTP/WinHttp/winhttp_connection.cpp index e0351d5a..91f87707 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.cpp +++ b/Source/HTTP/WinHttp/winhttp_connection.cpp @@ -29,9 +29,9 @@ using namespace xbox::httpclient; NAMESPACE_XBOX_HTTP_CLIENT_BEGIN +#ifndef HC_NOWEBSOCKETS namespace { - // Note: this logic is intentionally duplicated from TryParseProxyUri in // websocketpp_websocket.cpp to keep compilation units independent. bool TryParseWebSocketProxyUri( @@ -53,7 +53,6 @@ bool TryParseWebSocketProxyUri( return proxyUri.IsValid(); } -#ifndef HC_NOWEBSOCKETS HRESULT ApplyExplicitWebSocketProxy( HINTERNET hRequest, HCWebsocketHandle websocketHandle, @@ -92,9 +91,9 @@ HRESULT ApplyExplicitWebSocketProxy( return S_OK; } -#endif } +#endif WinHttpConnection::WinHttpConnection( HINTERNET hSession, From 5ba013481cc9924a2c2cdb233f5fb437fee7a104 Mon Sep 17 00:00:00 2001 From: jhugard Date: Fri, 24 Apr 2026 14:19:54 -0700 Subject: [PATCH 13/24] Fix websocket connect completion merge --- Source/WebSocket/hcwebsocket.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 7f3beb2c..a3931a5c 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -268,8 +268,8 @@ void CALLBACK WebSocket::ConnectComplete(XAsyncBlock* async) // We can be put into the Disconnected state if a spontaneous error occurs between the connection process completing and this callback being invoked. // We need to be able to handle that scenario here. HRESULT hr = HCGetWebSocketConnectResult(&context->internalAsyncBlock, &context->result); -<<<<<<< HEAD - std::unique_lock lock{ ws->m_stateMutex }; + + std::unique_lock lock{ ws->m_stateMutex }; const bool bIsDisconnected = (ws->m_state == State::Disconnected); if (bIsDisconnected && !FAILED(hr)) { @@ -279,10 +279,6 @@ void CALLBACK WebSocket::ConnectComplete(XAsyncBlock* async) assert(ws->m_state == State::Connecting || bIsDisconnected); assert(context->observer.get() == context->result.websocket || FAILED(hr) || bIsDisconnected); -======= - - std::unique_lock lock{ ws->m_stateMutex }; ->>>>>>> c7283d7 (Replace compression flags with explicit WebSocket options) if (SUCCEEDED(hr) && SUCCEEDED(context->result.errorCode)) { // Connect was sucessful. Allocate ProviderContext to ensure WebSocket lifetime until it is reclaimed in WebSocket::CloseFunc From 6b8a6126e186074fb648c11dc6c916984eefde23 Mon Sep 17 00:00:00 2001 From: jhugard Date: Fri, 24 Apr 2026 15:19:44 -0700 Subject: [PATCH 14/24] Fix no-websockets build variants --- Build/libHttpClient.CMake/GetLibHCFlags.cmake | 2 +- .../libHttpClient.GDK.NoWebSockets.def | 100 +++++++++++++++ .../libHttpClient.GDK.vcxproj | 2 + Build/libHttpClient.Linux/CMakeLists.txt | 3 +- .../libHttpClient.Win32.NoWebSockets.def | 120 ++++++++++++++++++ .../libHttpClient.Win32.vcxproj | 2 + Source/WebSocket/websocket_options.h | 4 + 7 files changed, 231 insertions(+), 2 deletions(-) create mode 100644 Build/libHttpClient.GDK/libHttpClient.GDK.NoWebSockets.def create mode 100644 Build/libHttpClient.Win32/libHttpClient.Win32.NoWebSockets.def diff --git a/Build/libHttpClient.CMake/GetLibHCFlags.cmake b/Build/libHttpClient.CMake/GetLibHCFlags.cmake index cb68d32d..4830e659 100644 --- a/Build/libHttpClient.CMake/GetLibHCFlags.cmake +++ b/Build/libHttpClient.CMake/GetLibHCFlags.cmake @@ -14,7 +14,7 @@ function(GET_LIBHC_FLAGS OUT_FLAGS OUT_FLAGS_DEBUG OUT_FLAGS_RELEASE) PARENT_SCOPE ) - if (DEFINED HC_NOWEBSOCKETS) + if (HC_NOWEBSOCKETS) list(APPEND FLAGS "-DHC_NOWEBSOCKETS=1") endif() diff --git a/Build/libHttpClient.GDK/libHttpClient.GDK.NoWebSockets.def b/Build/libHttpClient.GDK/libHttpClient.GDK.NoWebSockets.def new file mode 100644 index 00000000..ba63f8d5 --- /dev/null +++ b/Build/libHttpClient.GDK/libHttpClient.GDK.NoWebSockets.def @@ -0,0 +1,100 @@ +EXPORTS + HCAddCallRoutedHandler + HCCleanup + HCCleanupAsync + HCDisableAssertsForSSLValidationInDevSandboxes + HCGetHttpCallPerformFunction + HCGetLibVersion + HCHttpCallCloseHandle + HCHttpCallCreate + HCHttpCallDuplicateHandle + HCHttpCallGetContext + HCHttpCallGetId + HCHttpCallGetPerformCount + HCHttpCallGetRequestUrl + HCHttpCallPerformAsync + HCHttpCallRequestAddDynamicBytesWritten + HCHttpCallRequestEnableGzipCompression + HCHttpCallRequestGetDynamicBytesWritten + HCHttpCallRequestGetHeader + HCHttpCallRequestGetHeaderAtIndex + HCHttpCallRequestGetMaxReceiveBufferSize + HCHttpCallRequestGetNumHeaders + HCHttpCallRequestGetRequestBodyBytes + HCHttpCallRequestGetRequestBodyReadFunction + HCHttpCallRequestGetRequestBodyString + HCHttpCallRequestGetRetryAllowed + HCHttpCallRequestGetRetryCacheId + HCHttpCallRequestGetRetryDelay + HCHttpCallRequestGetTimeout + HCHttpCallRequestGetTimeoutWindow + HCHttpCallRequestGetUrl + HCHttpCallRequestSetDynamicSize + HCHttpCallRequestSetHeader + HCHttpCallRequestSetMaxReceiveBufferSize + HCHttpCallRequestSetProgressReportFunction + HCHttpCallRequestSetRequestBodyBytes + HCHttpCallRequestSetRequestBodyReadFunction + HCHttpCallRequestSetRequestBodyString + HCHttpCallRequestSetRetryAllowed + HCHttpCallRequestSetRetryCacheId + HCHttpCallRequestSetRetryDelay + HCHttpCallRequestSetSSLValidation + HCHttpCallRequestSetTimeout + HCHttpCallRequestSetTimeoutWindow + HCHttpCallRequestSetUrl + HCHttpCallResponseAddDynamicBytesWritten + HCHttpCallResponseAppendResponseBodyBytes + HCHttpCallResponseGetDynamicBytesWritten + HCHttpCallResponseGetHeader + HCHttpCallResponseGetHeaderAtIndex + HCHttpCallResponseGetNetworkErrorCode + HCHttpCallResponseGetNumHeaders + HCHttpCallResponseGetPlatformNetworkErrorMessage + HCHttpCallResponseGetResponseBodyBytes + HCHttpCallResponseGetResponseBodyBytesSize + HCHttpCallResponseGetResponseBodyWriteFunction + HCHttpCallResponseGetResponseString + HCHttpCallResponseGetStatusCode + HCHttpCallResponseSetDynamicSize + HCHttpCallResponseSetGzipCompressed + HCHttpCallResponseSetHeader + HCHttpCallResponseSetHeaderWithLength + HCHttpCallResponseSetNetworkErrorCode + HCHttpCallResponseSetPlatformNetworkErrorMessage + HCHttpCallResponseSetResponseBodyBytes + HCHttpCallResponseSetResponseBodyWriteFunction + HCHttpCallResponseSetStatusCode + HCHttpCallSetContext + HCHttpCallSetTracing + HCInitialize + HCIsInitialized + HCMemGetFunctions + HCMemSetFunctions + HCMockAddMock + HCMockCallCloseHandle + HCMockCallCreate + HCMockCallDuplicateHandle + HCMockClearMocks + HCMockRemoveMock + HCMockResponseSetHeader + HCMockResponseSetNetworkErrorCode + HCMockResponseSetResponseBodyBytes + HCMockResponseSetStatusCode + HCMockSetMockMatchedCallback + HCRemoveCallRoutedHandler + HCSetGlobalProxy + HCSetHttpCallPerformFunction + HCSettingsGetTraceLevel + HCSettingsSetTraceLevel + HCTraceCleanup + HCTraceImplMessage + HCTraceImplMessage_v + HCTraceImplScopeId + HCTraceInit + HCTraceSetClientCallback + HCTraceSetEtwEnabled + HCTraceSetPlatformCallbacks + HCTraceSetTraceToDebugger + HCWinHttpResume + HCWinHttpSuspend \ No newline at end of file diff --git a/Build/libHttpClient.GDK/libHttpClient.GDK.vcxproj b/Build/libHttpClient.GDK/libHttpClient.GDK.vcxproj index af9bb5dc..ae1f8a66 100644 --- a/Build/libHttpClient.GDK/libHttpClient.GDK.vcxproj +++ b/Build/libHttpClient.GDK/libHttpClient.GDK.vcxproj @@ -16,6 +16,7 @@ + @@ -32,6 +33,7 @@ $(HCBuildRoot)\$(ProjectName)\$(ProjectName).def + $(HCBuildRoot)\$(ProjectName)\$(ProjectName).NoWebSockets.def %(AdditionalDependencies);Appnotify.lib;winhttp.lib;crypt32.lib /profile /opt:ref /opt:icf /guard:cf /DYNAMICBASE %(AdditionalOptions) /profile /opt:ref /opt:icf /guard:cf /DYNAMICBASE %(AdditionalOptions) diff --git a/Build/libHttpClient.Linux/CMakeLists.txt b/Build/libHttpClient.Linux/CMakeLists.txt index 03215df8..0ef8b447 100644 --- a/Build/libHttpClient.Linux/CMakeLists.txt +++ b/Build/libHttpClient.Linux/CMakeLists.txt @@ -7,6 +7,7 @@ project("libHttpClient.Linux") include(CTest) enable_testing() +option(HC_NOWEBSOCKETS "Disable WebSocket support" OFF) option(HC_ENABLE_WEBSOCKET_COMPRESSION "Enable compression-capable WebSocket provider support" ON) set(CMAKE_C_COMPILER clang) @@ -190,7 +191,7 @@ target_set_flags( set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") -if (HC_ENABLE_WEBSOCKET_COMPRESSION) +if (HC_ENABLE_WEBSOCKET_COMPRESSION AND NOT HC_NOWEBSOCKETS) if (DEFINED HC_NOZLIB) message(FATAL_ERROR "HC_ENABLE_WEBSOCKET_COMPRESSION requires zlib support") endif() diff --git a/Build/libHttpClient.Win32/libHttpClient.Win32.NoWebSockets.def b/Build/libHttpClient.Win32/libHttpClient.Win32.NoWebSockets.def new file mode 100644 index 00000000..3b323290 --- /dev/null +++ b/Build/libHttpClient.Win32/libHttpClient.Win32.NoWebSockets.def @@ -0,0 +1,120 @@ +EXPORTS + HCAddCallRoutedHandler + HCCleanup + HCCleanupAsync + HCGetHttpCallPerformFunction + HCGetLibVersion + HCHttpCallCloseHandle + HCHttpCallCreate + HCHttpCallDuplicateHandle + HCHttpCallGetContext + HCHttpCallGetId + HCHttpCallGetRequestUrl + HCHttpCallGetPerformCount + HCHttpCallPerformAsync + HCHttpCallRequestEnableGzipCompression + HCHttpCallRequestGetHeader + HCHttpCallRequestGetHeaderAtIndex + HCHttpCallRequestGetNumHeaders + HCHttpCallRequestGetRequestBodyBytes + HCHttpCallRequestGetRequestBodyReadFunction + HCHttpCallRequestGetRequestBodyString + HCHttpCallRequestGetRetryAllowed + HCHttpCallRequestGetRetryCacheId + HCHttpCallRequestGetRetryDelay + HCHttpCallRequestGetTimeout + HCHttpCallRequestGetTimeoutWindow + HCHttpCallRequestGetUrl + HCHttpCallRequestSetHeader + HCHttpCallRequestSetRequestBodyBytes + HCHttpCallRequestSetRequestBodyReadFunction + HCHttpCallRequestSetRequestBodyString + HCHttpCallRequestSetRetryAllowed + HCHttpCallRequestSetRetryCacheId + HCHttpCallRequestSetRetryDelay + HCHttpCallRequestSetSSLValidation + HCHttpCallRequestSetTimeout + HCHttpCallRequestSetTimeoutWindow + HCHttpCallRequestSetUrl + HCHttpCallResponseAppendResponseBodyBytes + HCHttpCallResponseGetHeader + HCHttpCallResponseGetHeaderAtIndex + HCHttpCallResponseGetNetworkErrorCode + HCHttpCallResponseGetNumHeaders + HCHttpCallResponseGetPlatformNetworkErrorMessage + HCHttpCallResponseGetResponseBodyBytes + HCHttpCallResponseGetResponseBodyBytesSize + HCHttpCallResponseGetResponseBodyWriteFunction + HCHttpCallResponseGetResponseString + HCHttpCallResponseGetStatusCode + HCHttpCallResponseSetHeader + HCHttpCallResponseSetHeaderWithLength + HCHttpCallResponseSetNetworkErrorCode + HCHttpCallResponseSetPlatformNetworkErrorMessage + HCHttpCallResponseSetResponseBodyBytes + HCHttpCallResponseSetResponseBodyWriteFunction + HCHttpCallResponseSetStatusCode + HCHttpCallRequestSetDynamicSize + HCHttpCallRequestAddDynamicBytesWritten + HCHttpCallRequestGetDynamicBytesWritten + HCHttpCallResponseSetDynamicSize + HCHttpCallResponseAddDynamicBytesWritten + HCHttpCallResponseGetDynamicBytesWritten + HCHttpCallSetContext + HCHttpCallSetTracing + HCInitialize + HCIsInitialized + HCMemGetFunctions + HCMemSetFunctions + HCMockAddMock + HCMockCallCloseHandle + HCMockCallCreate + HCMockCallDuplicateHandle + HCMockClearMocks + HCMockRemoveMock + HCMockResponseSetHeader + HCMockResponseSetNetworkErrorCode + HCMockResponseSetResponseBodyBytes + HCMockResponseSetStatusCode + HCMockSetMockMatchedCallback + HCRemoveCallRoutedHandler + HCSetGlobalProxy + HCSetHttpCallPerformFunction + HCSettingsGetTraceLevel + HCSettingsSetTraceLevel + HCTraceCleanup + HCTraceImplMessage + HCTraceImplMessage_v + HCTraceImplScopeId + HCTraceInit + HCTraceSetClientCallback + HCTraceSetEtwEnabled + HCTraceSetPlatformCallbacks + HCTraceSetTraceToDebugger + XAsyncBegin + XAsyncCancel + XAsyncComplete + XAsyncGetResult + XAsyncGetResultSize + XAsyncGetStatus + XAsyncRun + XAsyncSchedule + XTaskQueueCloseHandle + XTaskQueueCreate + XTaskQueueCreateComposite + XTaskQueueDispatch + XTaskQueueDuplicateHandle + XTaskQueueGetCurrentProcessTaskQueue + XTaskQueueGetPort + XTaskQueueRegisterMonitor + XTaskQueueRegisterWaiter + XTaskQueueSetCurrentProcessTaskQueue + XTaskQueueSubmitCallback + XTaskQueueSubmitDelayedCallback + XTaskQueueTerminate + XTaskQueueUnregisterMonitor + XTaskQueueUnregisterWaiter + HCHttpCallResponseSetGzipCompressed + HCHttpCallRequestSetProgressReportFunction + HCHttpCallRequestGetMaxReceiveBufferSize + HCHttpCallRequestSetMaxReceiveBufferSize \ No newline at end of file diff --git a/Build/libHttpClient.Win32/libHttpClient.Win32.vcxproj b/Build/libHttpClient.Win32/libHttpClient.Win32.vcxproj index 04d500f6..f319e2f0 100644 --- a/Build/libHttpClient.Win32/libHttpClient.Win32.vcxproj +++ b/Build/libHttpClient.Win32/libHttpClient.Win32.vcxproj @@ -22,12 +22,14 @@ + %(AdditionalDependencies);Appnotify.lib;winhttp.lib;crypt32.lib $(HCBuildRoot)\$(ProjectName)\$(ProjectName).def + $(HCBuildRoot)\$(ProjectName)\$(ProjectName).NoWebSockets.def /profile /opt:ref /opt:icf /guard:cf /DYNAMICBASE %(AdditionalOptions) /profile /opt:ref /opt:icf /guard:cf /DYNAMICBASE %(AdditionalOptions) /profile /opt:ref /opt:icf /guard:cf /DYNAMICBASE %(AdditionalOptions) diff --git a/Source/WebSocket/websocket_options.h b/Source/WebSocket/websocket_options.h index 08ab468d..b00cf01f 100644 --- a/Source/WebSocket/websocket_options.h +++ b/Source/WebSocket/websocket_options.h @@ -7,6 +7,8 @@ NAMESPACE_XBOX_HTTP_CLIENT_BEGIN +#ifndef HC_NOWEBSOCKETS + constexpr uint32_t WebSocketLegacySemanticsMask() noexcept { return static_cast(HCWebSocketOptions::LegacySemantics); @@ -54,4 +56,6 @@ constexpr bool HasNoContextTakeoverWebSocketOptions(HCWebSocketOptions options) return (RawWebSocketOptions(options) & WebSocketCompressionNoContextTakeoverMask()) != 0; } +#endif // !HC_NOWEBSOCKETS + NAMESPACE_XBOX_HTTP_CLIENT_END From 9fbc9c184dbc69b8d0f771e2d5bd19b80704530f Mon Sep 17 00:00:00 2001 From: jhugard Date: Fri, 24 Apr 2026 15:51:26 -0700 Subject: [PATCH 15/24] Make hybrid websocket helpers const --- Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp | 4 ++-- Source/HTTP/WinHttp/winhttp_websocket_hybrid.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp index 58087980..7c179cb4 100644 --- a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp +++ b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.cpp @@ -104,7 +104,7 @@ void WinHttpHybrid_WebSocketProvider::OnResuming() noexcept } } -IWebSocketProvider& WinHttpHybrid_WebSocketProvider::ConnectProvider(HCWebsocketHandle websocketHandle) noexcept +IWebSocketProvider& WinHttpHybrid_WebSocketProvider::ConnectProvider(HCWebsocketHandle websocketHandle) const noexcept { if (RequestsWebSocketCompression(websocketHandle->websocket->Options())) { @@ -114,7 +114,7 @@ IWebSocketProvider& WinHttpHybrid_WebSocketProvider::ConnectProvider(HCWebsocket return *m_winHttpProvider; } -IWebSocketProvider* WinHttpHybrid_WebSocketProvider::ActiveProvider(HCWebsocketHandle websocketHandle) noexcept +IWebSocketProvider* WinHttpHybrid_WebSocketProvider::ActiveProvider(HCWebsocketHandle websocketHandle) const noexcept { if (websocketHandle == nullptr || !websocketHandle->websocket) { diff --git a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h index 7b9a240d..50c52f90 100644 --- a/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h +++ b/Source/HTTP/WinHttp/winhttp_websocket_hybrid.h @@ -41,8 +41,8 @@ class WinHttpHybrid_WebSocketProvider final : public IWebSocketProvider, public void OnResuming() noexcept override; private: - IWebSocketProvider& ConnectProvider(HCWebsocketHandle websocketHandle) noexcept; - IWebSocketProvider* ActiveProvider(HCWebsocketHandle websocketHandle) noexcept; + IWebSocketProvider& ConnectProvider(HCWebsocketHandle websocketHandle) const noexcept; + IWebSocketProvider* ActiveProvider(HCWebsocketHandle websocketHandle) const noexcept; UniquePtr m_winHttpProvider; UniquePtr m_wsppProvider; From cdaebe68b7d2b97e9feb47ea2f5dc2f83c7e342b Mon Sep 17 00:00:00 2001 From: jhugard Date: Fri, 24 Apr 2026 17:41:10 -0700 Subject: [PATCH 16/24] Fix strict-mode WinTLS websocket build --- Source/WebSocket/Websocketpp/wintls_socket.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/WebSocket/Websocketpp/wintls_socket.hpp b/Source/WebSocket/Websocketpp/wintls_socket.hpp index 758406ea..639aa8bb 100644 --- a/Source/WebSocket/Websocketpp/wintls_socket.hpp +++ b/Source/WebSocket/Websocketpp/wintls_socket.hpp @@ -11,6 +11,7 @@ #include +#include #include #include @@ -217,7 +218,8 @@ class connection : public lib::enable_shared_from_this { template static lib::error_code translate_ec(ErrorCodeType) { - return make_error_code(transport::error::pass_through); + return websocketpp::transport::asio::error::make_error_code( + websocketpp::transport::asio::error::pass_through); } static lib::error_code translate_ec(lib::error_code ec) From c90949aab802a9b3c1874bcc04a5f0922f9c7f0c Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 14:15:51 -0700 Subject: [PATCH 17/24] Harden Linux helper scripts for container builds --- Build/libHttpClient.Linux/curl_Linux.bash | 5 ++++- Build/libHttpClient.Linux/openssl_Linux.bash | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Build/libHttpClient.Linux/curl_Linux.bash b/Build/libHttpClient.Linux/curl_Linux.bash index befc3f88..8a36874c 100755 --- a/Build/libHttpClient.Linux/curl_Linux.bash +++ b/Build/libHttpClient.Linux/curl_Linux.bash @@ -57,7 +57,10 @@ make $MAKE_PARALLELISM # copies binaries to final directory mkdir -p "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux -cp -R "$PWD"/lib/.libs/* "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux +rm -f "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux/libcurl.a +rm -f "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux/libcurl.la +rm -f "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux/libcurl.lai +cp "$PWD"/lib/.libs/libcurl.a "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcurl.Linux/libcurl.a make clean popd diff --git a/Build/libHttpClient.Linux/openssl_Linux.bash b/Build/libHttpClient.Linux/openssl_Linux.bash index 126f8abd..1bf9093e 100755 --- a/Build/libHttpClient.Linux/openssl_Linux.bash +++ b/Build/libHttpClient.Linux/openssl_Linux.bash @@ -33,7 +33,6 @@ fi OPENSSL_INSTALL_DIR="$SCRIPT_DIR/../../Int/x64/$CONFIGURATION/openssl.Linux/" -rm -rf "$OPENSSL_INSTALL_DIR" mkdir -p "$OPENSSL_INSTALL_DIR" mkdir -p "$OPENSSL_INSTALL_DIR/lib" mkdir -p "$OPENSSL_INSTALL_DIR/include" @@ -76,7 +75,7 @@ fi MAKE_PARALLELISM="-j$(nproc)" # run Make in parallel to speed up the build process make $MAKE_PARALLELISM CFLAGS="-fvisibility=hidden" CXXFLAGS="-fvisibility=hidden" -make install +make install_sw # copies binaries to final directory mkdir -p "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcrypto.Linux cp -R "$PWD"/libcrypto.a "$SCRIPT_DIR"/../../Out/x64/"$CONFIGURATION"/libcrypto.Linux From 6cdd6396a9e68a0cb50c17ad35d7d3a2727c3852 Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 14:15:51 -0700 Subject: [PATCH 18/24] Silence non-GDK curl multi poll warning --- Source/HTTP/Curl/CurlMulti.cpp | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Source/HTTP/Curl/CurlMulti.cpp b/Source/HTTP/Curl/CurlMulti.cpp index de615cc8..4e6754a0 100644 --- a/Source/HTTP/Curl/CurlMulti.cpp +++ b/Source/HTTP/Curl/CurlMulti.cpp @@ -268,16 +268,7 @@ HRESULT CurlMulti::Perform() noexcept result = CURL_CALL(curl_multi_wait)(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); } #elif defined(CURL_AT_LEAST_VERSION) && CURL_AT_LEAST_VERSION(7,69,0) - // Try curl_multi_poll first, fall back to curl_multi_wait if not available - // For non-GDK, CURL_CALL expands directly to the symbol - if (CURL_CALL(curl_multi_poll)) - { - result = CURL_CALL(curl_multi_poll)(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); - } - else - { - result = CURL_CALL(curl_multi_wait)(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); - } + result = CURL_CALL(curl_multi_poll)(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); #else result = CURL_CALL(curl_multi_wait)(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); #endif From cc5fa18678a646dfe0d5858263ad21c76281133a Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 14:15:52 -0700 Subject: [PATCH 19/24] Align Apple minizip Xcode wiring --- .../libHttpClient.xcodeproj/project.pbxproj | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj b/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj index 9b45947a..f0fb617f 100644 --- a/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj +++ b/Build/libHttpClient.Apple.C/libHttpClient.xcodeproj/project.pbxproj @@ -141,11 +141,15 @@ 828CED582AFC484D00CA08B1 /* gzread.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED252AFC484D00CA08B1 /* gzread.c */; settings = {COMPILER_FLAGS = "-Wno-error=implicit-int-conversion -Wno-error=shorten-64-to-32"; }; }; 828CED5C2AFC484D00CA08B1 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED262AFC484D00CA08B1 /* adler32.c */; settings = {COMPILER_FLAGS = "-Wno-error=macro-redefined"; }; }; 828CED602AFC484D00CA08B1 /* trees.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED272AFC484D00CA08B1 /* trees.c */; settings = {COMPILER_FLAGS = "-Wno-error=comma -Wno-error=macro-redefined"; }; }; + 828CED612AFC486100CA08B1 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED632AFC486100CA08B1 /* ioapi.c */; }; 828CED652AFC486000CA08B1 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED642AFC486000CA08B1 /* unzip.c */; }; 828CED662AFC486000CA08B1 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED642AFC486000CA08B1 /* unzip.c */; }; 828CED672AFC486000CA08B1 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED642AFC486000CA08B1 /* unzip.c */; }; 828CED682AFC486000CA08B1 /* unzip.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED642AFC486000CA08B1 /* unzip.c */; }; + 828CED692AFC486100CA08B1 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED632AFC486100CA08B1 /* ioapi.c */; }; 828CED6C2AFD885100CA08B1 /* adler32.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED262AFC484D00CA08B1 /* adler32.c */; settings = {COMPILER_FLAGS = "-Wno-error=macro-redefined"; }; }; + 828CED6A2AFC486100CA08B1 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED632AFC486100CA08B1 /* ioapi.c */; }; + 828CED6B2AFC486100CA08B1 /* ioapi.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED632AFC486100CA08B1 /* ioapi.c */; }; 828CED6D2AFD885100CA08B1 /* crc32.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED1E2AFC484D00CA08B1 /* crc32.c */; settings = {COMPILER_FLAGS = "-Wno-error=implicit-int-conversion -Wno-error=shorten-64-to-32 -Wno-error=macro-redefined"; }; }; 828CED6E2AFD885100CA08B1 /* deflate.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED1D2AFC484D00CA08B1 /* deflate.c */; settings = {COMPILER_FLAGS = "-Wno-error=implicit-int-conversion -Wno-error=shorten-64-to-32 -Wno-error=comma -Wno-error=macro-redefined"; }; }; 828CED6F2AFD885100CA08B1 /* gzread.c in Sources */ = {isa = PBXBuildFile; fileRef = 828CED252AFC484D00CA08B1 /* gzread.c */; settings = {COMPILER_FLAGS = "-Wno-error=implicit-int-conversion -Wno-error=shorten-64-to-32"; }; }; @@ -446,6 +450,7 @@ 828CED252AFC484D00CA08B1 /* gzread.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = gzread.c; path = ../External/zlib/gzread.c; sourceTree = ""; }; 828CED262AFC484D00CA08B1 /* adler32.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = adler32.c; path = ../External/zlib/adler32.c; sourceTree = ""; }; 828CED272AFC484D00CA08B1 /* trees.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = trees.c; path = ../External/zlib/trees.c; sourceTree = ""; }; + 828CED632AFC486100CA08B1 /* ioapi.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = ioapi.c; path = ../External/zlib/contrib/minizip/ioapi.c; sourceTree = ""; }; 828CED642AFC486000CA08B1 /* unzip.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = unzip.c; path = ../External/zlib/contrib/minizip/unzip.c; sourceTree = ""; }; 9C3B253E212F29CF0080AEC6 /* websocketpp_websocket.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = websocketpp_websocket.cpp; sourceTree = ""; }; 9C3B253F212F29CF0080AEC6 /* x509_cert_utilities.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = x509_cert_utilities.hpp; sourceTree = ""; }; @@ -868,6 +873,7 @@ 828CED182AFC47DD00CA08B1 /* Zlib */ = { isa = PBXGroup; children = ( + 828CED632AFC486100CA08B1 /* ioapi.c */, 828CED642AFC486000CA08B1 /* unzip.c */, 828CED262AFC484D00CA08B1 /* adler32.c */, 828CED1B2AFC484D00CA08B1 /* compress.c */, @@ -1345,6 +1351,7 @@ buildActionMask = 2147483647; files = ( 58A7E9ED209ADEB100CC6774 /* global_publics.cpp in Sources */, + 828CED612AFC486100CA08B1 /* ioapi.c in Sources */, 828CED652AFC486000CA08B1 /* unzip.c in Sources */, 58A7E9EF209ADEB100CC6774 /* global.cpp in Sources */, A23D0BA62B925EC900624E90 /* utils_apple.mm in Sources */, @@ -1426,6 +1433,7 @@ 828CED752AFD885100CA08B1 /* trees.c in Sources */, 828CED762AFD885100CA08B1 /* zutil.c in Sources */, 7DB100DE2119F91B00AE22F5 /* pch.cpp in Sources */, + 828CED692AFC486100CA08B1 /* ioapi.c in Sources */, 828CED662AFC486000CA08B1 /* unzip.c in Sources */, 7DB100D1211927DF00AE22F5 /* uri.cpp in Sources */, 7DB100D2211927DF00AE22F5 /* utils.cpp in Sources */, @@ -1489,6 +1497,7 @@ 828CED802AFD886600CA08B1 /* trees.c in Sources */, 828CED812AFD886600CA08B1 /* zutil.c in Sources */, D9EF882725A522BC005C4BDF /* global_publics.cpp in Sources */, + 828CED6A2AFC486100CA08B1 /* ioapi.c in Sources */, 828CED672AFC486000CA08B1 /* unzip.c in Sources */, D9EF882925A522BC005C4BDF /* global.cpp in Sources */, 727979122A4E32A800EB7079 /* ExternalWebSocketProvider.cpp in Sources */, @@ -1550,6 +1559,7 @@ 828CED8B2AFD887000CA08B1 /* trees.c in Sources */, 828CED8C2AFD887000CA08B1 /* zutil.c in Sources */, D9FF0A5E25A5366A0061B717 /* global_publics.cpp in Sources */, + 828CED6B2AFC486100CA08B1 /* ioapi.c in Sources */, 828CED682AFC486000CA08B1 /* unzip.c in Sources */, D9FF0A6025A5366A0061B717 /* global.cpp in Sources */, 727979132A4E32A800EB7079 /* ExternalWebSocketProvider.cpp in Sources */, @@ -1837,6 +1847,7 @@ "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", "$(SRCROOT)/../../External/zlib", + "$(SRCROOT)/../../External/zlib/contrib/minizip", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -1871,6 +1882,7 @@ "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", "$(SRCROOT)/../../External/zlib", + "$(SRCROOT)/../../External/zlib/contrib/minizip", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -2019,6 +2031,7 @@ "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", "$(SRCROOT)/../../External/zlib", + "$(SRCROOT)/../../External/zlib/contrib/minizip", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", @@ -2056,6 +2069,7 @@ "$(SRCROOT)/../../Include", "$(SRCROOT)/../../Source/Common", "$(SRCROOT)/../../External/zlib", + "$(SRCROOT)/../../External/zlib/contrib/minizip", "$(SRCROOT)/../../External/websocketpp", "$(SRCROOT)/../../External/asio/asio/include", "$(BUILT_PRODUCTS_DIR)/openssl/include", From 5f8ec91ea3a4c69c7d7ca01afaeaa583e7f85149 Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 14:19:16 -0700 Subject: [PATCH 20/24] Fix Win32 compression test linker settings --- .../WebSocketCompressionIntegrationTests.Win32.vcxproj | 5 ++++- .../WebSocketCompressionTests.Win32.vcxproj | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj b/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj index ac6eba9e..5db0ec95 100644 --- a/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj +++ b/Tests/WebSocketCompression/WebSocketCompressionIntegrationTests.Win32.vcxproj @@ -37,8 +37,11 @@ NDEBUG;%(PreprocessorDefinitions) - false + + UseLinkTimeCodeGeneration + false + diff --git a/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj b/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj index a5fb97e0..02e9efba 100644 --- a/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj +++ b/Tests/WebSocketCompression/WebSocketCompressionTests.Win32.vcxproj @@ -37,8 +37,11 @@ NDEBUG;%(PreprocessorDefinitions) - false + + UseLinkTimeCodeGeneration + false + From fba869da75f8008119343f5b7ba94a9750a8678b Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 14:19:16 -0700 Subject: [PATCH 21/24] Reject zero WebSocket receive buffer size --- Source/WebSocket/hcwebsocket.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index a3931a5c..43563c3c 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -519,6 +519,7 @@ HRESULT WebSocket::SetProxyDecryptsHttps( HRESULT WebSocket::SetMaxReceiveBufferSize(size_t maxReceiveBufferSizeBytes) noexcept { + RETURN_HR_IF(E_INVALIDARG, maxReceiveBufferSizeBytes == 0); RETURN_HR_IF(E_INVALIDARG, maxReceiveBufferSizeBytes > static_cast((std::numeric_limits::max)())); std::lock_guard lock{ m_stateMutex }; From bae2321308cf02fe0c7a2ffd8d762e738e4afe4e Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 15:03:16 -0700 Subject: [PATCH 22/24] Disable zstd in Linux curl helper --- Build/libHttpClient.Linux/curl_Linux.bash | 1 + 1 file changed, 1 insertion(+) diff --git a/Build/libHttpClient.Linux/curl_Linux.bash b/Build/libHttpClient.Linux/curl_Linux.bash index 8a36874c..35f169dd 100755 --- a/Build/libHttpClient.Linux/curl_Linux.bash +++ b/Build/libHttpClient.Linux/curl_Linux.bash @@ -42,6 +42,7 @@ CONFIGURE_ARGS=( --with-openssl=$OPENSSL_INSTALL_DIR --enable-symbol-hiding --without-brotli + --without-zstd ) if [ "$CONFIGURATION" = "Debug" ]; then CONFIGURE_ARGS+=(--enable-debug) From 7dad3d9a252c41d7efac281bd6de3260a042d296 Mon Sep 17 00:00:00 2001 From: jhugard Date: Tue, 28 Apr 2026 15:27:59 -0700 Subject: [PATCH 23/24] Silence Apple websocketpp documentation warning --- .../Websocketpp/websocketpp_configured_permessage_deflate.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp b/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp index 7c6613a3..a3a9d3b0 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp +++ b/Source/WebSocket/Websocketpp/websocketpp_configured_permessage_deflate.hpp @@ -7,6 +7,7 @@ #if defined(__clang__) // Keep this third-party warning suppression local to the wrapper instead of patching the submodule. #pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdocumentation" #pragma clang diagnostic ignored "-Wshorten-64-to-32" #endif #include From ea05d34c428545de8687e830f07ef854d5503780 Mon Sep 17 00:00:00 2001 From: jhugard Date: Thu, 30 Apr 2026 12:59:21 -0700 Subject: [PATCH 24/24] Address PR feedback: lock ordering, translate_ec comment, type-erasure assert 1. SetOptions: move HasBinaryMessageFragmentHandlers() before m_stateMutex acquisition to eliminate latent m_stateMutex -> m_eventCallbacksMutex nesting hazard. 2. wintls_socket translate_ec: add comment explaining that wintls::error_code resolves to the lib::error_code identity overload (same type via WINTLS_USE_STANDALONE_ASIO), so TLS errors propagate correctly. 3. websocketpp_client_base::impl(): add debug-only type tag (typeid hash) set at construction and asserted on access, catching config-type dispatch mismatches at zero release cost. --- Source/WebSocket/Websocketpp/websocketpp_websocket.cpp | 10 ++++++++++ Source/WebSocket/Websocketpp/wintls_socket.hpp | 6 ++++++ Source/WebSocket/hcwebsocket.cpp | 7 ++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp index 935784f5..242b415d 100644 --- a/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp +++ b/Source/WebSocket/Websocketpp/websocketpp_websocket.cpp @@ -1790,17 +1790,27 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared template websocketpp::client& impl() { + ASSERT(m_typeTag == typeid(websocketpp::client).hash_code() + && "Config type mismatch in wspp client type erasure"); return *reinterpret_cast*>(client_storage()); } virtual void* client_storage() noexcept = 0; virtual bool is_tls_client() const = 0; virtual bool uses_compression() const = 0; + + protected: + size_t m_typeTag{ 0 }; }; template struct websocketpp_client_impl : websocketpp_client_base { + websocketpp_client_impl() + { + m_typeTag = typeid(websocketpp::client).hash_code(); + } + void* client_storage() noexcept override { return &m_client; diff --git a/Source/WebSocket/Websocketpp/wintls_socket.hpp b/Source/WebSocket/Websocketpp/wintls_socket.hpp index 639aa8bb..ffcc7b9c 100644 --- a/Source/WebSocket/Websocketpp/wintls_socket.hpp +++ b/Source/WebSocket/Websocketpp/wintls_socket.hpp @@ -215,6 +215,12 @@ class connection : public lib::enable_shared_from_this { } public: + // Overload resolution note: wintls::error_code is a typedef for std::error_code + // (via WINTLS_USE_STANDALONE_ASIO), which is the same type as lib::error_code. + // TLS handshake failures therefore resolve to the identity overload below, preserving + // the original SECURITY_STATUS value in system_category through to HResultFromConnectError. + // The generic template only fires for non-std::error_code types (e.g. boost error codes + // in non-standalone configurations). template static lib::error_code translate_ec(ErrorCodeType) { diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 43563c3c..7289a917 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -550,6 +550,11 @@ HRESULT WebSocket::SetOptions(HCWebSocketOptions options) noexcept E_INVALIDARG, HasNoContextTakeoverWebSocketOptions(options) && !RequestsWebSocketCompression(options)); + // Check fragment handlers before acquiring m_stateMutex to avoid nesting + // m_stateMutex -> m_eventCallbacksMutex (which HasBinaryMessageFragmentHandlers acquires). + // Safe because SetOptions is only valid in State::Initial, before callbacks can fire. + bool const hasBinaryFragmentHandlers = HasBinaryMessageFragmentHandlers(); + std::lock_guard lock{ m_stateMutex }; RETURN_HR_IF(E_HC_CONNECT_ALREADY_CALLED, m_state != State::Initial); if (RequestsLegacyWebSocketSemantics(options)) @@ -559,7 +564,7 @@ HRESULT WebSocket::SetOptions(HCWebSocketOptions options) noexcept return S_OK; } - RETURN_HR_IF(E_NOT_SUPPORTED, HasBinaryMessageFragmentHandlers()); + RETURN_HR_IF(E_NOT_SUPPORTED, hasBinaryFragmentHandlers); HRESULT hr = m_provider.OptionsResult(options); RETURN_IF_FAILED(hr);