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 7a91b688..fe047234 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 @@ -1,6 +1,7 @@ package com.xbox.httpclient; import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; import okhttp3.Headers; import okhttp3.OkHttpClient; @@ -19,6 +20,11 @@ public final class HttpClientWebSocket extends WebSocketListener { HttpClientWebSocket(long owner) { this.headers = new Headers.Builder(); this.owner = owner; + this.pingInterval = 0; + } + + public void setPingInterval(long pingInterval) { + this.pingInterval = pingInterval; } public void addHeader(String name, String value) { @@ -33,10 +39,14 @@ public void connect(String url, String subProtocol) { .headers(headers.build()) .build(); - socket = OK_CLIENT.newWebSocket(request, this); + OkHttpClient clientWithPing = OK_CLIENT.newBuilder() + .pingInterval(pingInterval, TimeUnit.SECONDS) // default is 0, which disables pings + .build(); + + socket = clientWithPing.newWebSocket(request, this); } - public boolean sendMessage(String message) { + public boolean sendMessage(String message) { return socket.send(message); } @@ -96,6 +106,7 @@ protected void finalize() private final Headers.Builder headers; private final long owner; + private long pingInterval; private WebSocket socket; } diff --git a/Build/libHttpClient.GDK/libHttpClient.GDK.def b/Build/libHttpClient.GDK/libHttpClient.GDK.def index c53d78dc..673a8d83 100644 --- a/Build/libHttpClient.GDK/libHttpClient.GDK.def +++ b/Build/libHttpClient.GDK/libHttpClient.GDK.def @@ -101,12 +101,14 @@ EXPORTS HCWebSocketGetHeader HCWebSocketGetHeaderAtIndex HCWebSocketGetNumHeaders + HCWebSocketGetPingInterval HCWebSocketGetProxyUri HCWebSocketSendBinaryMessageAsync HCWebSocketSendMessageAsync HCWebSocketSetBinaryMessageFragmentEventFunction HCWebSocketSetHeader HCWebSocketSetMaxReceiveBufferSize + HCWebSocketSetPingInterval HCWebSocketSetProxyUri HCWinHttpResume HCWinHttpSuspend diff --git a/Build/libHttpClient.Win32/libHttpClient.Win32.def b/Build/libHttpClient.Win32/libHttpClient.Win32.def index 2cc63586..5e35d324 100644 --- a/Build/libHttpClient.Win32/libHttpClient.Win32.def +++ b/Build/libHttpClient.Win32/libHttpClient.Win32.def @@ -100,6 +100,7 @@ EXPORTS HCWebSocketGetHeader HCWebSocketGetHeaderAtIndex HCWebSocketGetNumHeaders + HCWebSocketGetPingInterval HCWebSocketGetProxyUri HCWebSocketSendBinaryMessageAsync HCWebSocketSendMessageAsync @@ -107,6 +108,7 @@ EXPORTS HCWebSocketSetHeader HCWebSocketSetMaxReceiveBufferSize HCWebSocketSetProxyDecryptsHttps + HCWebSocketSetPingInterval HCWebSocketSetProxyUri XAsyncBegin XAsyncCancel diff --git a/Include/httpClient/httpClient.h b/Include/httpClient/httpClient.h index 7f224fbb..05e4e067 100644 --- a/Include/httpClient/httpClient.h +++ b/Include/httpClient/httpClient.h @@ -874,7 +874,7 @@ typedef void /// /// WebSocket usage:
/// Create a WebSocket handle using HCWebSocketCreate()
-/// Call HCWebSocketSetProxyUri() and HCWebSocketSetHeader() to prepare the HCWebsocketHandle
+/// 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.
@@ -947,6 +947,17 @@ STDAPI HCWebSocketSetHeader( _In_z_ const char* headerValue ) noexcept; +/// +/// Set the ping interval for the WebSocket. +/// The handle of the WebSocket. +/// The interval at which this websocket should send keepalive frames, in seconds. +/// Result code for this API operation. Possible values are S_OK, E_INVALIDARG, or E_UNEXPECTED. +/// +STDAPI HCWebSocketSetPingInterval( + _In_ HCWebsocketHandle websocket, + _In_ uint32_t pingIntervalSeconds + ) noexcept; + /// /// Gets the WebSocket functions to allow callers to respond to incoming messages and WebSocket close events. /// diff --git a/Include/httpClient/httpProvider.h b/Include/httpClient/httpProvider.h index b3a9d6f9..fec9ad64 100644 --- a/Include/httpClient/httpProvider.h +++ b/Include/httpClient/httpProvider.h @@ -569,6 +569,18 @@ HCWebSocketGetHeaderAtIndex( _Out_ const char** headerValue ) noexcept; +/// +/// Gets the ping interval for this WebSocket. +/// +/// The handle of the WebSocket. +/// The ping interval of this WebSocket. +/// Result code for this API operation. Possible values are S_OK, or E_INVALIDARG. +STDAPI +HCWebSocketGetPingInterval( + _In_ HCWebsocketHandle websocket, + _Out_ uint32_t* pingIntervalSeconds +) noexcept; + #endif // !HC_NOWEBSOCKETS } diff --git a/Source/HTTP/Curl/CurlMulti.cpp b/Source/HTTP/Curl/CurlMulti.cpp index 36d4a136..9236be72 100644 --- a/Source/HTTP/Curl/CurlMulti.cpp +++ b/Source/HTTP/Curl/CurlMulti.cpp @@ -247,7 +247,7 @@ HRESULT CurlMulti::Perform() noexcept { // Reschedule Perform if there are still running requests int workAvailable{ 0 }; -#if HC_PLATFORM == HC_PLATFORM_GDK || LIBCURL_VERSION_NUM >= 0x074201 +#if HC_PLATFORM == HC_PLATFORM_GDK || CURL_AT_LEAST_VERSION(7,66,0) result = curl_multi_poll(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); #else result = curl_multi_wait(m_curlMultiHandle, nullptr, 0, POLL_TIMEOUT_MS, &workAvailable); diff --git a/Source/HTTP/Curl/CurlProvider.cpp b/Source/HTTP/Curl/CurlProvider.cpp index b602d0df..287335bf 100644 --- a/Source/HTTP/Curl/CurlProvider.cpp +++ b/Source/HTTP/Curl/CurlProvider.cpp @@ -22,7 +22,7 @@ HRESULT HrFromCurlm(CURLMcode c) noexcept switch (c) { case CURLMcode::CURLM_OK: return S_OK; -#if HC_PLATFORM == HC_PLATFORM_GDK || LIBCURL_VERSION_NUM >= 0x074201 +#if HC_PLATFORM == HC_PLATFORM_GDK || CURL_AT_LEAST_VERSION(7,69,0) case CURLMcode::CURLM_BAD_FUNCTION_ARGUMENT: assert(false); return E_INVALIDARG; #endif default: return E_FAIL; diff --git a/Source/HTTP/WinHttp/winhttp_connection.cpp b/Source/HTTP/WinHttp/winhttp_connection.cpp index dce72460..66ab5ba8 100644 --- a/Source/HTTP/WinHttp/winhttp_connection.cpp +++ b/Source/HTTP/WinHttp/winhttp_connection.cpp @@ -1633,8 +1633,16 @@ void WinHttpConnection::callback_websocket_status_headers_available( return; } + 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); + } + constexpr DWORD closeTimeoutMs = 1000; - bool status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT, (LPVOID)&closeTimeoutMs, sizeof(DWORD)); + status = WinHttpSetOption(winHttpConnection->m_hRequest, WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT, (LPVOID)&closeTimeoutMs, sizeof(DWORD)); if (!status) { DWORD dwError = GetLastError(); diff --git a/Source/WebSocket/Android/AndroidWebSocketProvider.cpp b/Source/WebSocket/Android/AndroidWebSocketProvider.cpp index 8da425a5..409a56b4 100644 --- a/Source/WebSocket/Android/AndroidWebSocketProvider.cpp +++ b/Source/WebSocket/Android/AndroidWebSocketProvider.cpp @@ -48,6 +48,7 @@ struct HttpClientWebSocket HttpClientWebSocket(JavaVM* vm, jclass webSocketClass, okhttp_websocket_impl* owner) : m_vm(vm) + , m_setPingInterval(GetSetPingIntervalMethod(GetEnv(vm), webSocketClass)) , m_addHeader(GetAddHeaderMethod(GetEnv(vm), webSocketClass)) , m_connect(GetConnectMethod(GetEnv(vm), webSocketClass)) , m_sendMessage(GetSendMessageMethod(GetEnv(vm), webSocketClass)) @@ -63,6 +64,23 @@ struct HttpClientWebSocket } } + HRESULT SetPingInterval(uint32_t pingInterval) const + { + JNIEnv* env = GetEnv(m_vm); + if (!env || !m_webSocket || !m_setPingInterval) + { + return E_UNEXPECTED; + } + + env->CallVoidMethod(m_webSocket, m_setPingInterval, static_cast(pingInterval)); + if (HadException(env)) + { + return E_UNEXPECTED; + } + + return S_OK; + } + HRESULT AddHeader(const char* name, const char* value) const { if (!name || !value) @@ -223,6 +241,22 @@ struct HttpClientWebSocket return static_cast(env); } + static jmethodID GetSetPingIntervalMethod(JNIEnv* env, jclass webSocketClass) + { + if (!env || !webSocketClass) + { + return nullptr; + } + + const jmethodID setPingInterval = env->GetMethodID(webSocketClass, "setPingInterval", "(J)V"); + if (HadException(env) || !setPingInterval) + { + return nullptr; + } + + return setPingInterval; + } + static jmethodID GetAddHeaderMethod(JNIEnv* env, jclass webSocketClass) { if (!env || !webSocketClass) @@ -358,6 +392,7 @@ struct HttpClientWebSocket private: JavaVM* const m_vm; + const jmethodID m_setPingInterval; const jmethodID m_addHeader; const jmethodID m_connect; const jmethodID m_sendMessage; @@ -691,8 +726,21 @@ struct okhttp_websocket_impl : hc_websocket_impl, std::enable_shared_from_thisimpl(); + const long pingIntervalMs = m_hcWebsocketHandle->websocket->PingInterval() * 1000; + client.set_pong_timeout(pingIntervalMs); // default ping interval is 0, which disables the timeout + client.init_asio(); client.start_perpetual(); @@ -481,6 +484,11 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared }); }); + client.set_pong_timeout_handler([sharedThis](websocketpp::connection_hdl, std::string) + { + sharedThis->close(HCWebSocketCloseStatus::PolicyViolation); + }); + // Set User Agent specified by the user. This needs to happen before any connection is created const auto& headers = m_hcWebsocketHandle->websocket->Headers(); @@ -841,6 +849,11 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared // is terminated (i.e. by disconnecting the network cable). Sending periodic ping // allows us to detect this situation. See https://github.com/zaphoyd/websocketpp/issues/695. + // Preserving behavior: if client did not specify a ping interval, default to WSPP_PING_INTERVAL + const uint64_t pingDelayInMs = m_hcWebsocketHandle->websocket->PingInterval() + ? m_hcWebsocketHandle->websocket->PingInterval() * 1000 + : WSPP_PING_INTERVAL_MS; + RunAsync( [ weakThis = std::weak_ptr{ shared_from_this() } @@ -874,7 +887,7 @@ struct wspp_websocket_impl : public hc_websocket_impl, public std::enable_shared } }, m_backgroundQueue, - WSPP_PING_INTERVAL_MS + pingDelayInMs ); } diff --git a/Source/WebSocket/hcwebsocket.cpp b/Source/WebSocket/hcwebsocket.cpp index 00fdf2df..bc540d0b 100644 --- a/Source/WebSocket/hcwebsocket.cpp +++ b/Source/WebSocket/hcwebsocket.cpp @@ -370,6 +370,11 @@ size_t WebSocket::MaxReceiveBufferSize() const noexcept return m_maxReceiveBufferSize; } +uint32_t WebSocket::PingInterval() const noexcept +{ + return m_pingInterval; +} + HRESULT WebSocket::SetHeader( http_internal_string&& headerName, http_internal_string&& headerValue @@ -408,6 +413,12 @@ HRESULT WebSocket::SetMaxReceiveBufferSize(size_t maxReceiveBufferSizeBytes) noe return S_OK; } +HRESULT WebSocket::SetPingInterval(uint32_t pingInterval) noexcept +{ + m_pingInterval = pingInterval; + return S_OK; +} + void CALLBACK WebSocket::MessageFunc( HCWebsocketHandle handle, const char* message, diff --git a/Source/WebSocket/hcwebsocket.h b/Source/WebSocket/hcwebsocket.h index e8c6f31e..32522856 100644 --- a/Source/WebSocket/hcwebsocket.h +++ b/Source/WebSocket/hcwebsocket.h @@ -132,11 +132,13 @@ 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; + uint32_t PingInterval() 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; // Event functions static void CALLBACK MessageFunc(HCWebsocketHandle handle, const char* message, void* context); @@ -165,6 +167,7 @@ class WebSocket : public std::enable_shared_from_this http_internal_string m_uri; http_internal_string m_subProtocol; size_t m_maxReceiveBufferSize{ 0 }; + uint32_t m_pingInterval{ 0 }; struct ConnectContext; struct ProviderContext; diff --git a/Source/WebSocket/websocket_publics.cpp b/Source/WebSocket/websocket_publics.cpp index a5c61188..a94e5fb0 100644 --- a/Source/WebSocket/websocket_publics.cpp +++ b/Source/WebSocket/websocket_publics.cpp @@ -78,6 +78,16 @@ try } CATCH_RETURN() +STDAPI HCWebSocketSetPingInterval( + _In_ HCWebsocketHandle handle, + _In_ uint32_t pingIntervalSeconds +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle); + return handle->websocket->SetPingInterval(pingIntervalSeconds); +} +CATCH_RETURN() STDAPI HCWebSocketConnectAsync( _In_z_ const char* uri, @@ -282,6 +292,19 @@ try } CATCH_RETURN() +STDAPI HCWebSocketGetPingInterval( + _In_ HCWebsocketHandle handle, + _Out_ uint32_t* pingIntervalSeconds +) noexcept +try +{ + RETURN_HR_IF(E_INVALIDARG, !handle || !pingIntervalSeconds); + + *pingIntervalSeconds = handle->websocket->PingInterval(); + return S_OK; +} +CATCH_RETURN() + STDAPI HCWebSocketGetEventFunctions( _In_ HCWebsocketHandle websocket, _Out_opt_ HCWebSocketMessageFunction* messageFunc, diff --git a/Utilities/FrameworkResources/exports.exp b/Utilities/FrameworkResources/exports.exp index 234fe663..68b8d254 100644 --- a/Utilities/FrameworkResources/exports.exp +++ b/Utilities/FrameworkResources/exports.exp @@ -43,9 +43,11 @@ _HCHttpCallResponseGetResponseBodyWriteFunction _HCHttpCallResponseSetGzipCompressed _HCWebSocketCreate +_HCWebSocketSetPingInterval _HCWebSocketSetProxyUri _HCWebSocketSetHeader _HCWebSocketGetEventFunctions +_HCWebSocketGetPingInterval _HCWebSocketConnectAsync _HCGetWebSocketConnectResult _HCWebSocketSendMessageAsync