From 857b20e7161d4e380e726e11f9bea8256711d13b Mon Sep 17 00:00:00 2001 From: sashakosti Date: Sun, 11 Oct 2026 15:39:29 +0300 Subject: [PATCH 1/3] feat(signaling): implement XMPP custom signaling client and sandbox tests - Add IXmppSignalingClient and CXmppSignalingClient in examples/ - Support nonblocking TCP/TLS transport with SASL PLAIN auth and stanza streaming - Integrate --xmpp-signaling option into test_p2p executable - Add standalone Python mock server (xmpp_mock_server.py) and end-to-end sandbox test - Zero core engine modifications or ABI breakage Closes #136 --- examples/CMakeLists.txt | 20 + examples/xmpp_mock_server.py | 164 +++++++ examples/xmpp_signaling_client.cpp | 748 +++++++++++++++++++++++++++++ examples/xmpp_signaling_client.h | 54 +++ tests/CMakeLists.txt | 16 +- tests/test_p2p.cpp | 63 ++- tests/test_xmpp_signaling.py | 118 +++++ 7 files changed, 1179 insertions(+), 4 deletions(-) create mode 100755 examples/xmpp_mock_server.py create mode 100644 examples/xmpp_signaling_client.cpp create mode 100644 examples/xmpp_signaling_client.h create mode 100755 tests/test_xmpp_signaling.py diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index e41a7805..8e156e82 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -12,6 +12,26 @@ if( ENABLE_ICE AND ( BUILD_EXAMPLES OR BUILD_TESTS ) ) COMMAND ${CMAKE_COMMAND} -E copy ${SIGNAL_SERVER_SRC} ${SIGNAL_SERVER_OUTPUT} ) add_custom_target(${SIGNAL_SERVER_TARGET} ALL DEPENDS ${SIGNAL_SERVER_OUTPUT}) + + set(XMPP_SERVER_TARGET xmpp_mock_server) + set(XMPP_SERVER_OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/xmpp_mock_server.py) + set(XMPP_SERVER_SRC ${CMAKE_CURRENT_SOURCE_DIR}/xmpp_mock_server.py) + add_custom_command( + OUTPUT ${XMPP_SERVER_OUTPUT} + DEPENDS ${XMPP_SERVER_SRC} + COMMENT "Publishing ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/xmpp_mock_server.py" + COMMAND ${CMAKE_COMMAND} -E copy ${XMPP_SERVER_SRC} ${XMPP_SERVER_OUTPUT} + ) + add_custom_target(${XMPP_SERVER_TARGET} ALL DEPENDS ${XMPP_SERVER_OUTPUT}) + + add_library( + xmpp_signaling_client STATIC + xmpp_signaling_client.cpp + xmpp_signaling_client.h) + target_link_libraries(xmpp_signaling_client PUBLIC GameNetworkingSockets::shared) + if( TARGET OpenSSL::SSL ) + target_link_libraries(xmpp_signaling_client PUBLIC OpenSSL::SSL) + endif() endif() # diff --git a/examples/xmpp_mock_server.py b/examples/xmpp_mock_server.py new file mode 100755 index 00000000..754c16c1 --- /dev/null +++ b/examples/xmpp_mock_server.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Minimal XMPP mock server for P2P signaling tests and sandbox smoke testing. + +Handles lightweight XMPP stream initiation, SASL PLAIN authentication, and +forwards stanzas between connected peers. +""" + +from __future__ import annotations + +import argparse +import base64 +import re +import socket +import socketserver +import sys +import threading + +DEFAULT_LISTEN_PORT = 5222 + + +class XmppState: + def __init__(self) -> None: + self.lock = threading.Lock() + self.clients: dict[str, socket.socket] = {} + + def register(self, jid: str, sock: socket.socket) -> None: + with self.lock: + # Register by full JID and bare username + self.clients[jid] = sock + bare = jid.split("/")[0] + self.clients[bare] = sock + user = bare.split("@")[0] + self.clients[user] = sock + + def unregister(self, sock: socket.socket) -> None: + with self.lock: + to_remove = [k for k, v in self.clients.items() if v is sock] + for k in to_remove: + del self.clients[k] + + def forward(self, to_jid: str, raw_data: bytes) -> bool: + with self.lock: + target = self.clients.get(to_jid) + if target is None: + bare = to_jid.split("/")[0] + target = self.clients.get(bare) + if target is None: + user = to_jid.split("@")[0] + target = self.clients.get(user) + + if target is not None: + try: + target.sendall(raw_data) + return True + except OSError: + return False + return False + + +class XmppServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + allow_reuse_address = True + daemon_threads = True + + def __init__(self, server_address: tuple[str, int], handler_cls: type[socketserver.BaseRequestHandler], state: XmppState) -> None: + super().__init__(server_address, handler_cls) + self.state = state + + +class XmppHandler(socketserver.StreamRequestHandler): + def handle(self) -> None: + peer_addr = f"{self.client_address[0]}:{self.client_address[1]}" + print(f"[XMPP] New connection from {peer_addr}", flush=True) + + current_jid = f"unknown_{self.client_address[1]}" + buf = "" + + try: + while True: + chunk = self.request.recv(4096) + if not chunk: + break + + buf += chunk.decode("utf-8", "ignore") + + # Handle stream opening + if "" + "" + "PLAIN\n" + ) + self.request.sendall(stream_resp.encode("utf-8")) + + # Handle SASL PLAIN auth + auth_match = re.search(r"]*mechanism=['\"]PLAIN['\"][^>]*>(.*?)", buf, re.DOTALL) + if auth_match: + auth_b64 = auth_match.group(1).strip() + try: + decoded = base64.b64decode(auth_b64) + parts = decoded.split(b"\x00") + # Format is \0username\0password or authzid\0authcid\0password + user = "" + for p in parts: + if p: + user = p.decode("utf-8", "ignore") + break + if user: + current_jid = user + except Exception: + pass + + self.server.state.register(current_jid, self.request) + print(f"[XMPP] Authenticated client '{current_jid}' from {peer_addr}", flush=True) + self.request.sendall(b"\n") + + # Handle message forwarding + while "" in buf: + msg_start = buf.find("", msg_start) + if msg_end == -1: + break + + full_msg = buf[msg_start : msg_end + 10] + buf = buf[msg_end + 10 :] + + to_match = re.search(r"to=['\"]([^'\"]+)['\"]", full_msg) + if to_match: + target_jid = to_match.group(1) + success = self.server.state.forward(target_jid, (full_msg + "\n").encode("utf-8")) + if success: + print(f"[XMPP] Forwarded message: {current_jid} -> {target_jid}", flush=True) + else: + print(f"[XMPP] Discarded message: target '{target_jid}' not found", flush=True) + + except (ConnectionResetError, BrokenPipeError): + pass + finally: + self.server.state.unregister(self.request) + print(f"[XMPP] Disconnected: {current_jid} ({peer_addr})", flush=True) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Minimal XMPP signaling mock server") + parser.add_argument("--host", default="0.0.0.0", help="Host/IP to bind to (default: 0.0.0.0)") + parser.add_argument("--port", type=int, default=DEFAULT_LISTEN_PORT, help=f"TCP port (default: {DEFAULT_LISTEN_PORT})") + args = parser.parse_args() + + state = XmppState() + server = XmppServer((args.host, args.port), XmppHandler, state) + + print(f"XMPP mock server listening on {args.host}:{args.port} (Ctrl+C to stop)...", flush=True) + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nShutting down XMPP mock server...", flush=True) + finally: + server.server_close() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/examples/xmpp_signaling_client.cpp b/examples/xmpp_signaling_client.cpp new file mode 100644 index 00000000..16a67158 --- /dev/null +++ b/examples/xmpp_signaling_client.cpp @@ -0,0 +1,748 @@ +//====== Copyright Valve Corporation, All rights reserved. ==================== +// +// Client for XMPP (Jabber) signaling service (RFC 6120 / RFC 6121). +// Serves as an example of how to hook up an XMPP signaling server +// to SteamNetworkingSockets P2P connections (Issue #136). +// +//============================================================================= + +#include "xmpp_signaling_client.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef _WIN32 + #include + #include + #include + #include + #include + #include + #include + #include + typedef int SOCKET; + constexpr SOCKET INVALID_SOCKET = -1; + inline void closesocket( SOCKET s ) { close( s ); } + inline int GetSocketError() { return errno; } + inline bool IgnoreSocketError( int e ) + { + return e == EAGAIN || e == ENOTCONN || e == EWOULDBLOCK || e == EINPROGRESS; + } + #ifndef ioctlsocket + #define ioctlsocket ioctl + #endif + struct ScopedWinsockInit_t {}; +#else + #include + #include + typedef int socklen_t; + inline int GetSocketError() { return WSAGetLastError(); } + inline bool IgnoreSocketError( int e ) + { + return e == WSAEWOULDBLOCK || e == WSAENOTCONN || e == WSAEINPROGRESS; + } + #ifndef poll + #define poll WSAPoll + #endif + struct ScopedWinsockInit_t + { + ScopedWinsockInit_t() + { + WSADATA wsaData; + WSAStartup( MAKEWORD( 2, 2 ), &wsaData ); + } + ~ScopedWinsockInit_t() + { + WSACleanup(); + } + }; +#endif + +static bool SplitHostPort( const char *pszAddress, std::string &sHost, std::string &sPort, const char *pszDefaultPort ) +{ + if ( pszAddress == nullptr || pszAddress[0] == '\0' ) + { + return false; + } + + std::string sAddr( pszAddress ); + size_t colon = sAddr.rfind( ':' ); + if ( colon == std::string::npos ) + { + sHost = sAddr; + sPort = pszDefaultPort ? pszDefaultPort : ""; + } + else + { + sHost = sAddr.substr( 0, colon ); + sPort = sAddr.substr( colon + 1 ); + } + return !sHost.empty(); +} + +#include +#include + +static const char s_chBase64Table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +std::string XmppBase64Encode( const void *pData, int cbData ) +{ + std::string sOut; + if ( cbData <= 0 || pData == nullptr ) + { + return sOut; + } + + const uint8_t *pBytes = (const uint8_t *)pData; + sOut.reserve( ( ( cbData + 2 ) / 3 ) * 4 ); + + for ( int i = 0; i < cbData; i += 3 ) + { + uint32_t nVal = ( (uint32_t)pBytes[i] ) << 16; + if ( i + 1 < cbData ) + { + nVal |= ( (uint32_t)pBytes[i + 1] ) << 8; + } + if ( i + 2 < cbData ) + { + nVal |= (uint32_t)pBytes[i + 2]; + } + + sOut.push_back( s_chBase64Table[ ( nVal >> 18 ) & 0x3F ] ); + sOut.push_back( s_chBase64Table[ ( nVal >> 12 ) & 0x3F ] ); + sOut.push_back( ( i + 1 < cbData ) ? s_chBase64Table[ ( nVal >> 6 ) & 0x3F ] : '=' ); + sOut.push_back( ( i + 2 < cbData ) ? s_chBase64Table[ nVal & 0x3F ] : '=' ); + } + + return sOut; +} + +bool XmppBase64Decode( const char *pszInput, int cchInput, std::string &sOutDecoded ) +{ + sOutDecoded.clear(); + if ( cchInput <= 0 || pszInput == nullptr ) + { + return true; + } + + auto decodeChar = []( char c ) -> int + { + if ( c >= 'A' && c <= 'Z' ) return c - 'A'; + if ( c >= 'a' && c <= 'z' ) return c - 'a' + 26; + if ( c >= '0' && c <= '9' ) return c - '0' + 52; + if ( c == '+' ) return 62; + if ( c == '/' ) return 63; + if ( c == '=' ) return -1; + return -2; + }; + + sOutDecoded.reserve( ( cchInput / 4 ) * 3 ); + int nBlock[4]; + int nBlockLen = 0; + + for ( int i = 0; i < cchInput; ++i ) + { + char c = pszInput[i]; + if ( c == ' ' || c == '\t' || c == '\r' || c == '\n' ) + { + continue; + } + int val = decodeChar( c ); + if ( val == -2 ) + { + return false; + } + nBlock[ nBlockLen++ ] = val; + if ( nBlockLen == 4 ) + { + if ( nBlock[0] < 0 || nBlock[1] < 0 ) + { + return false; + } + uint32_t nVal = ( (uint32_t)nBlock[0] << 18 ) | ( (uint32_t)nBlock[1] << 12 ); + sOutDecoded.push_back( (char)( ( nVal >> 16 ) & 0xFF ) ); + + if ( nBlock[2] >= 0 ) + { + nVal |= ( (uint32_t)nBlock[2] << 6 ); + sOutDecoded.push_back( (char)( ( nVal >> 8 ) & 0xFF ) ); + + if ( nBlock[3] >= 0 ) + { + nVal |= (uint32_t)nBlock[3]; + sOutDecoded.push_back( (char)( nVal & 0xFF ) ); + } + } + nBlockLen = 0; + } + } + + return ( nBlockLen == 0 ); +} + +std::string XmppFormatMessageStanza( const char *pszFrom, const char *pszTo, const void *pPayload, int cbPayload ) +{ + std::string sStanza; + sStanza.reserve( 128 + ( cbPayload * 4 ) / 3 ); + sStanza = "GNS:"; + sStanza += XmppBase64Encode( pPayload, cbPayload ); + sStanza += "\n"; + return sStanza; +} + +bool XmppExtractMessage( std::string &sBuffer, std::string &sOutFrom, std::string &sOutPayload ) +{ + size_t posMsg = sBuffer.find( " 4096 && sBuffer.find( '<' ) == std::string::npos ) + { + sBuffer.clear(); + } + return false; + } + + size_t posEndMsg = sBuffer.find( "", posMsg ); + if ( posEndMsg == std::string::npos ) + { + return false; + } + + std::string sStanza = sBuffer.substr( posMsg, posEndMsg + 10 - posMsg ); + sBuffer.erase( 0, posEndMsg + 10 ); + + size_t posFrom = sStanza.find( "from=" ); + if ( posFrom != std::string::npos && posFrom + 6 < sStanza.length() ) + { + char quote = sStanza[ posFrom + 5 ]; + if ( quote == '\'' || quote == '\"' ) + { + size_t posEndQuote = sStanza.find( quote, posFrom + 6 ); + if ( posEndQuote != std::string::npos ) + { + sOutFrom = sStanza.substr( posFrom + 6, posEndQuote - ( posFrom + 6 ) ); + } + } + } + + size_t posBody = sStanza.find( "" ); + size_t posEndBody = sStanza.find( "" ); + if ( posBody != std::string::npos && posEndBody != std::string::npos && posEndBody > posBody + 6 ) + { + std::string sBody = sStanza.substr( posBody + 6, posEndBody - ( posBody + 6 ) ); + const char *pszPrefix = "GNS:"; + size_t prefixLen = strlen( pszPrefix ); + if ( sBody.compare( 0, prefixLen, pszPrefix ) == 0 ) + { + std::string sBase64 = sBody.substr( prefixLen ); + if ( XmppBase64Decode( sBase64.c_str(), (int)sBase64.length(), sOutPayload ) ) + { + return true; + } + } + } + + return false; +} + +static void XmppSelfTest() +{ + const char *pszTest = "Hello P2P!"; + std::string sB64 = XmppBase64Encode( pszTest, (int)strlen( pszTest ) ); + std::string sDec; + bool bOk = XmppBase64Decode( sB64.c_str(), (int)sB64.length(), sDec ); + assert( bOk && sDec == pszTest ); + + std::string sStanza = XmppFormatMessageStanza( "alice@test.org", "bob@test.org", pszTest, (int)strlen( pszTest ) ); + std::string sFrom, sPayload; + bOk = XmppExtractMessage( sStanza, sFrom, sPayload ); + assert( bOk ); + assert( sFrom == "alice@test.org" ); + assert( sPayload == pszTest ); + assert( sStanza.empty() ); +} + +/// Implementation of IXmppSignalingClient +class CXmppSignalingClient : public IXmppSignalingClient +{ + struct ConnectionSignaling : ISteamNetworkingConnectionSignaling + { + CXmppSignalingClient *const m_pOwner; + std::string const m_sPeerJid; + + ConnectionSignaling( CXmppSignalingClient *pOwner, const char *pszPeerJid ) + : m_pOwner( pOwner ) + , m_sPeerJid( pszPeerJid ) + { + } + + virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) override + { + (void)info; + (void)hConn; + return m_pOwner->SendSignalToPeer( m_sPeerJid.c_str(), pMsg, cbMsg ); + } + + virtual void Release() override + { + delete this; + } + }; + + sockaddr_storage m_adrServer; + size_t const m_adrServerSize; + ISteamNetworkingSockets *const m_pSteamNetworkingSockets; + std::string m_sJid; + std::string m_sPassword; + bool m_bUseTLS; + + std::recursive_mutex m_sockMutex; + SOCKET m_sock; + SSL_CTX *m_pSSLCtx; + SSL *m_pSSL; + std::deque< std::string > m_queueSend; + std::string m_sBufferedData; + bool m_bConnected; + bool m_bAuthSent; + + void SendAuthStanza() + { + if ( m_bAuthSent ) + { + return; + } + m_bAuthSent = true; + + size_t at = m_sJid.find( '@' ); + std::string sUser = ( at != std::string::npos ) ? m_sJid.substr( 0, at ) : m_sJid; + + std::string sPlain; + sPlain.push_back( '\0' ); + sPlain.append( sUser ); + sPlain.push_back( '\0' ); + sPlain.append( m_sPassword ); + + std::string sB64 = XmppBase64Encode( sPlain.data(), (int)sPlain.size() ); + std::string sAuth = "" + sB64 + "\n"; + SendRaw( sAuth ); + SendRaw( "\n" ); + } + +public: + CXmppSignalingClient( + const sockaddr *pAdrServer, + size_t adrServerSize, + const char *pszJid, + const char *pszPassword, + bool bUseTLS, + ISteamNetworkingSockets *pSteamNetworkingSockets + ) + : m_adrServerSize( adrServerSize ) + , m_pSteamNetworkingSockets( pSteamNetworkingSockets ) + , m_sJid( pszJid ) + , m_sPassword( pszPassword ) + , m_bUseTLS( bUseTLS ) + , m_sock( INVALID_SOCKET ) + , m_pSSLCtx( nullptr ) + , m_pSSL( nullptr ) + , m_bConnected( false ) + , m_bAuthSent( false ) + { + XmppSelfTest(); + memcpy( &m_adrServer, pAdrServer, adrServerSize ); + Connect(); + } + + virtual ~CXmppSignalingClient() + { + CloseSocket(); + } + + void CloseSocket() + { + std::lock_guard< std::recursive_mutex > lock( m_sockMutex ); + if ( m_pSSL != nullptr ) + { + SSL_shutdown( m_pSSL ); + SSL_free( m_pSSL ); + m_pSSL = nullptr; + } + if ( m_pSSLCtx != nullptr ) + { + SSL_CTX_free( m_pSSLCtx ); + m_pSSLCtx = nullptr; + } + if ( m_sock != INVALID_SOCKET ) + { + closesocket( m_sock ); + m_sock = INVALID_SOCKET; + } + m_bConnected = false; + m_bAuthSent = false; + m_sBufferedData.clear(); + m_queueSend.clear(); + } + + void Connect() + { + std::lock_guard< std::recursive_mutex > lock( m_sockMutex ); + CloseSocket(); + + int sockType = SOCK_STREAM; + #ifdef LINUX + sockType |= SOCK_CLOEXEC; + #endif + m_sock = socket( m_adrServer.ss_family, sockType, IPPROTO_TCP ); + if ( m_sock == INVALID_SOCKET ) + { + return; + } + + unsigned long opt = 1; + if ( ioctlsocket( m_sock, FIONBIO, &opt ) == -1 ) + { + CloseSocket(); + return; + } + + connect( m_sock, (const sockaddr *)&m_adrServer, (socklen_t)m_adrServerSize ); + + if ( m_bUseTLS ) + { + m_pSSLCtx = SSL_CTX_new( TLS_client_method() ); + if ( m_pSSLCtx != nullptr ) + { + m_pSSL = SSL_new( m_pSSLCtx ); + if ( m_pSSL != nullptr ) + { + SSL_set_fd( m_pSSL, (int)m_sock ); + SSL_set_connect_state( m_pSSL ); + } + } + } + + std::string sStreamOpen = " lock( m_sockMutex ); + while ( m_queueSend.size() > 64 ) + { + m_queueSend.pop_front(); + } + m_queueSend.push_back( s ); + } + + bool SendSignalToPeer( const char *pszPeerJid, const void *pMsg, int cbMsg ) + { + std::string sStanza = XmppFormatMessageStanza( m_sJid.c_str(), pszPeerJid, pMsg, cbMsg ); + SendRaw( sStanza ); + return true; + } + + virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection( + const SteamNetworkingIdentity &identityPeer, + SteamNetworkingErrMsg &errMsg + ) override + { + SteamNetworkingIdentityRender sIdentity( identityPeer ); + const char *pszPeer = sIdentity.c_str(); + if ( pszPeer[0] == '\0' ) + { + sprintf( errMsg, "Cannot create signaling without peer identity" ); + return nullptr; + } + return new ConnectionSignaling( this, pszPeer ); + } + + virtual void Poll() override + { + std::deque< std::pair< std::string, std::string > > messagesToDispatch; + + { + std::lock_guard< std::recursive_mutex > lock( m_sockMutex ); + if ( m_sock == INVALID_SOCKET ) + { + return; + } + + struct pollfd pfd; + pfd.fd = m_sock; + pfd.events = POLLIN; + if ( !m_queueSend.empty() || ( m_bUseTLS && !m_bConnected ) ) + { + pfd.events |= POLLOUT; + } + pfd.revents = 0; + + #ifdef _WIN32 + int pr = WSAPoll( &pfd, 1, 0 ); + #else + int pr = poll( &pfd, 1, 0 ); + #endif + + if ( pr < 0 ) + { + if ( !IgnoreSocketError( GetSocketError() ) ) + { + CloseSocket(); + return; + } + } + + if ( !m_bConnected ) + { + if ( m_bUseTLS && m_pSSL != nullptr ) + { + int r = SSL_connect( m_pSSL ); + if ( r == 1 ) + { + m_bConnected = true; + SendAuthStanza(); + } + else + { + int err = SSL_get_error( m_pSSL, r ); + if ( err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE ) + { + CloseSocket(); + return; + } + } + } + else + { + if ( pfd.revents & ( POLLOUT | POLLIN ) ) + { + int optval = 0; + socklen_t optlen = sizeof( optval ); + getsockopt( m_sock, SOL_SOCKET, SO_ERROR, (char*)&optval, &optlen ); + if ( optval == 0 ) + { + m_bConnected = true; + SendAuthStanza(); + } + else + { + CloseSocket(); + return; + } + } + } + } + + if ( m_bConnected ) + { + char buf[1024]; + for (;;) + { + int r = 0; + if ( m_bUseTLS && m_pSSL != nullptr ) + { + r = SSL_read( m_pSSL, buf, sizeof( buf ) ); + if ( r <= 0 ) + { + int err = SSL_get_error( m_pSSL, r ); + if ( err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE ) + { + break; + } + CloseSocket(); + break; + } + } + else + { + r = recv( m_sock, buf, sizeof( buf ), 0 ); + if ( r <= 0 ) + { + if ( r < 0 && IgnoreSocketError( GetSocketError() ) ) + { + break; + } + CloseSocket(); + break; + } + } + + m_sBufferedData.append( buf, r ); + if ( r < (int)sizeof( buf ) ) + { + break; + } + } + + while ( !m_queueSend.empty() && m_sock != INVALID_SOCKET ) + { + const std::string &s = m_queueSend.front(); + int l = (int)s.length(); + int r = 0; + if ( m_bUseTLS && m_pSSL != nullptr ) + { + r = SSL_write( m_pSSL, s.c_str(), l ); + if ( r <= 0 ) + { + int err = SSL_get_error( m_pSSL, r ); + if ( err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE ) + { + break; + } + CloseSocket(); + break; + } + } + else + { + r = ::send( m_sock, s.c_str(), l, 0 ); + if ( r <= 0 ) + { + if ( r < 0 && IgnoreSocketError( GetSocketError() ) ) + { + break; + } + CloseSocket(); + break; + } + } + + if ( r == l ) + { + m_queueSend.pop_front(); + } + else + { + CloseSocket(); + break; + } + } + + std::string sFrom, sPayload; + while ( XmppExtractMessage( m_sBufferedData, sFrom, sPayload ) ) + { + messagesToDispatch.push_back( std::make_pair( sFrom, sPayload ) ); + } + } + } // Unlock m_sockMutex! + + for ( const auto &msg : messagesToDispatch ) + { + const std::string &sPeerFrom = msg.first; + const std::string &sPayload = msg.second; + + struct Context : ISteamNetworkingSignalingRecvContext + { + CXmppSignalingClient *m_pOwner; + std::string m_sPeerJid; + + virtual ISteamNetworkingConnectionSignaling *OnConnectRequest( + HSteamNetConnection hConn, + const SteamNetworkingIdentity &identityPeer, + int nLocalVirtualPort + ) override + { + (void)hConn; + (void)nLocalVirtualPort; + SteamNetworkingErrMsg errMsg; + SteamNetworkingIdentity identityExpected; + identityExpected.SetGenericString( m_sPeerJid.c_str() ); + return m_pOwner->CreateSignalingForConnection( identityPeer.IsInvalid() ? identityExpected : identityPeer, errMsg ); + } + + virtual void SendRejectionSignal( + const SteamNetworkingIdentity &identityPeer, + const void *pMsg, + int cbMsg + ) override + { + (void)identityPeer; + (void)pMsg; + (void)cbMsg; + } + }; + + Context context; + context.m_pOwner = this; + context.m_sPeerJid = sPeerFrom; + + m_pSteamNetworkingSockets->ReceivedP2PCustomSignal( + sPayload.data(), + (int)sPayload.size(), + &context + ); + } + } + + virtual void Release() override + { + CloseSocket(); + delete this; + } +}; + +IXmppSignalingClient *CreateXmppSignalingClient( + const char *pszServerAddress, + const char *pszJid, + const char *pszPassword, + bool bUseTLS, + ISteamNetworkingSockets *pSteamNetworkingSockets, + SteamNetworkingErrMsg &errMsg +) +{ + ScopedWinsockInit_t winsock; + std::string sAddress, sPort; + if ( !SplitHostPort( pszServerAddress, sAddress, sPort, bUseTLS ? "5223" : "5222" ) ) + { + sprintf( errMsg, "Invalid XMPP server address '%s'", pszServerAddress ? pszServerAddress : "" ); + return nullptr; + } + + addrinfo hints; + memset( &hints, 0, sizeof( hints ) ); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + addrinfo *pAddrInfo = nullptr; + int r = getaddrinfo( sAddress.c_str(), sPort.c_str(), &hints, &pAddrInfo ); + if ( r != 0 || pAddrInfo == nullptr ) + { + sprintf( errMsg, "Invalid XMPP server address '%s:%s', getaddrinfo returned %d", sAddress.c_str(), sPort.c_str(), r ); + return nullptr; + } + + CXmppSignalingClient *pClient = new CXmppSignalingClient( + pAddrInfo->ai_addr, + pAddrInfo->ai_addrlen, + pszJid, + pszPassword, + bUseTLS, + pSteamNetworkingSockets + ); + + freeaddrinfo( pAddrInfo ); + return pClient; +} diff --git a/examples/xmpp_signaling_client.h b/examples/xmpp_signaling_client.h new file mode 100644 index 00000000..a7215c88 --- /dev/null +++ b/examples/xmpp_signaling_client.h @@ -0,0 +1,54 @@ +//====== Copyright Valve Corporation, All rights reserved. ==================== +// +// Client for XMPP (Jabber) signaling service (RFC 6120 / RFC 6121). +// Serves as an example of how to hook up an XMPP signaling server +// to SteamNetworkingSockets P2P connections (Issue #136). +// +//============================================================================= + +#pragma once + +#include +#include + +class ISteamNetworkingSockets; + +/// Interface to our XMPP signaling client. +class IXmppSignalingClient +{ +public: + + /// Create signaling object for a connection to peer. + /// identityPeer will typically be set with SetGenericString("user@domain"). + virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection( + const SteamNetworkingIdentity &identityPeer, + SteamNetworkingErrMsg &errMsg ) = 0; + + /// Poll the server for incoming signals and dispatch them. + virtual void Poll() = 0; + + /// Disconnect from the XMPP server and release resources. + virtual void Release() = 0; +}; + +/// Connect to an XMPP server. +/// pszServerAddress: host:port (e.g. "jabber.example.org:5223" or "127.0.0.1:5222"). +/// pszJid: user@domain/resource. +/// pszPassword: password for SASL PLAIN authentication. +/// bUseTLS: if true, uses Direct TLS (SSL_connect) on connect; if false, uses plain TCP (for local mocks). +/// pSteamNetworkingSockets: sockets interface to deliver incoming signals to. +/// errMsg: error message buffer if creation fails. +IXmppSignalingClient *CreateXmppSignalingClient( + const char *pszServerAddress, + const char *pszJid, + const char *pszPassword, + bool bUseTLS, + ISteamNetworkingSockets *pSteamNetworkingSockets, + SteamNetworkingErrMsg &errMsg +); + +/// Utility helpers for encoding/decoding XMPP signaling stanzas +std::string XmppBase64Encode( const void *pData, int cbData ); +bool XmppBase64Decode( const char *pszInput, int cchInput, std::string &sOutDecoded ); +std::string XmppFormatMessageStanza( const char *pszFrom, const char *pszTo, const void *pPayload, int cbPayload ); +bool XmppExtractMessage( std::string &sBuffer, std::string &sOutFrom, std::string &sOutPayload ); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9746282e..b7235398 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,9 +36,13 @@ if(ENABLE_ICE) test_p2p test_common.cpp test_p2p.cpp - ../examples/trivial_signaling_client.cpp) + ../examples/trivial_signaling_client.cpp + ../examples/xmpp_signaling_client.cpp) set_target_common_gns_properties( test_p2p ) target_link_libraries(test_p2p GameNetworkingSockets::static_mock) + if( TARGET OpenSSL::SSL ) + target_link_libraries(test_p2p OpenSSL::SSL) + endif() add_sanitizers(test_p2p) # Publish the test scripts @@ -56,4 +60,14 @@ if(ENABLE_ICE) DEPENDS ${STUN_SERVER_SCRIPT_SRC} ) add_custom_target( publish_test_script ALL DEPENDS ${P2P_TEST_SCRIPT} ${STUN_SERVER_SCRIPT} ) add_test(NAME p2p COMMAND ${P2P_TEST_SCRIPT} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # Publish XMPP test script + set( XMPP_TEST_SCRIPT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/test_xmpp_signaling.py ) + set( XMPP_TEST_SCRIPT_SRC ${CMAKE_CURRENT_SOURCE_DIR}/test_xmpp_signaling.py ) + add_custom_command( + OUTPUT ${XMPP_TEST_SCRIPT} + COMMAND ${CMAKE_COMMAND} -E copy ${XMPP_TEST_SCRIPT_SRC} ${XMPP_TEST_SCRIPT} + DEPENDS ${XMPP_TEST_SCRIPT_SRC} ) + add_custom_target( publish_xmpp_test_script ALL DEPENDS ${XMPP_TEST_SCRIPT} ) + add_test(NAME xmpp_signaling COMMAND ${XMPP_TEST_SCRIPT} WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) endif() diff --git a/tests/test_p2p.cpp b/tests/test_p2p.cpp index 98f0a92b..5df3b608 100644 --- a/tests/test_p2p.cpp +++ b/tests/test_p2p.cpp @@ -14,6 +14,7 @@ #include #include #include "../examples/trivial_signaling_client.h" +#include "../examples/xmpp_signaling_client.h" #include "../src/steamnetworkingsockets/clientlib/steamnetworkingsockets_mock.h" #define DEFAULT_STUN_SERVER "stun.l.google.com:19302" @@ -370,6 +371,7 @@ int main( int argc, const char **argv ) SteamNetworkingIdentity identityLocal; identityLocal.Clear(); SteamNetworkingIdentity identityRemote; identityRemote.Clear(); const char *pszTrivialSignalingService = "localhost:10000"; + const char *pszXmppSignalingService = nullptr; const char *pszSTUNServer = DEFAULT_STUN_SERVER; const char *pszTURNServer = nullptr; const char *pszTURNUsername = nullptr; @@ -405,6 +407,8 @@ int main( int argc, const char **argv ) ParseIdentity( identityRemote ); else if ( !strcmp( pszSwitch, "--signaling-server" ) ) pszTrivialSignalingService = GetArg(); + else if ( !strcmp( pszSwitch, "--xmpp-signaling" ) ) + pszXmppSignalingService = GetArg(); else if ( !strcmp( pszSwitch, "--stun-server" ) ) pszSTUNServer = GetArg(); else if ( !strcmp( pszSwitch, "--turn-server" ) ) @@ -567,9 +571,62 @@ int main( int argc, const char **argv ) // Create the signaling service SteamNetworkingErrMsg errMsg; - ITrivialSignalingClient *pSignaling = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg, nSignalingLossPct, nSignalingDupPct ); - if ( pSignaling == nullptr ) - TEST_Fatal( "Failed to initializing signaling client. %s", errMsg ); + struct ISignalingClient + { + virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection( const SteamNetworkingIdentity &identityPeer, SteamNetworkingErrMsg &errMsg ) = 0; + virtual void Poll() = 0; + virtual void Release() = 0; + virtual ~ISignalingClient() {} + }; + + struct TrivialSignalingAdapter : ISignalingClient + { + ITrivialSignalingClient *m_pClient; + TrivialSignalingAdapter( ITrivialSignalingClient *p ) : m_pClient( p ) {} + virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection( const SteamNetworkingIdentity &identityPeer, SteamNetworkingErrMsg &errMsg ) override + { + return m_pClient->CreateSignalingForConnection( identityPeer, errMsg ); + } + virtual void Poll() override { m_pClient->Poll(); } + virtual void Release() override { m_pClient->Release(); delete this; } + }; + + struct XmppSignalingAdapter : ISignalingClient + { + IXmppSignalingClient *m_pClient; + XmppSignalingAdapter( IXmppSignalingClient *p ) : m_pClient( p ) {} + virtual ISteamNetworkingConnectionSignaling *CreateSignalingForConnection( const SteamNetworkingIdentity &identityPeer, SteamNetworkingErrMsg &errMsg ) override + { + return m_pClient->CreateSignalingForConnection( identityPeer, errMsg ); + } + virtual void Poll() override { m_pClient->Poll(); } + virtual void Release() override { m_pClient->Release(); delete this; } + }; + + ISignalingClient *pSignaling = nullptr; + if ( pszXmppSignalingService != nullptr ) + { + std::string sJid = SteamNetworkingIdentityRender( identityLocal ).c_str(); + sJid += "@localhost/test"; + IXmppSignalingClient *pXmpp = CreateXmppSignalingClient( + pszXmppSignalingService, + sJid.c_str(), + "pass", + /*bUseTLS*/ false, + SteamNetworkingSockets(), + errMsg + ); + if ( pXmpp == nullptr ) + TEST_Fatal( "Failed to initialize XMPP signaling client. %s", errMsg ); + pSignaling = new XmppSignalingAdapter( pXmpp ); + } + else + { + ITrivialSignalingClient *pTrivial = CreateTrivialSignalingClient( pszTrivialSignalingService, SteamNetworkingSockets(), errMsg, nSignalingLossPct, nSignalingDupPct ); + if ( pTrivial == nullptr ) + TEST_Fatal( "Failed to initializing signaling client. %s", errMsg ); + pSignaling = new TrivialSignalingAdapter( pTrivial ); + } SteamNetworkingUtils()->SetGlobalCallback_SteamNetConnectionStatusChanged( OnSteamNetConnectionStatusChanged ); diff --git a/tests/test_xmpp_signaling.py b/tests/test_xmpp_signaling.py new file mode 100755 index 00000000..0d5e1cc3 --- /dev/null +++ b/tests/test_xmpp_signaling.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Integration sandbox test for P2P signaling via XMPP. + +Spawns the local xmpp_mock_server and two test_p2p processes (server and client), +verifying end-to-end P2P connection establishment and message exchange. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import time +from pathlib import Path + +LISTEN_PORT = 15222 + + +def main() -> int: + bin_dir = Path(__file__).resolve().parent + repo_root = bin_dir.parents[1] if (bin_dir / "test_p2p").exists() else bin_dir + + test_p2p_exe = bin_dir / "test_p2p" + if not test_p2p_exe.exists(): + # Fallback to build directory + test_p2p_exe = Path("build/bin/test_p2p") + + mock_server_py = bin_dir / "xmpp_mock_server.py" + if not mock_server_py.exists(): + mock_server_py = Path("examples/xmpp_mock_server.py") + + print(f"Using test_p2p binary: {test_p2p_exe}") + print(f"Using XMPP mock server: {mock_server_py}") + + server_cmd = [ + sys.executable, + str(mock_server_py), + "--host", "127.0.0.1", + "--port", str(LISTEN_PORT), + ] + + mock_proc = subprocess.Popen( + server_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + + try: + time.sleep(0.5) + + env = os.environ.copy() + signaling_addr = f"127.0.0.1:{LISTEN_PORT}" + + p2p_server_cmd = [ + str(test_p2p_exe), + "--server", + "--identity-local", "str:alice", + "--xmpp-signaling", signaling_addr, + ] + + p2p_client_cmd = [ + str(test_p2p_exe), + "--client", + "--identity-local", "str:bob", + "--identity-remote", "str:alice", + "--xmpp-signaling", signaling_addr, + "--ticks", "20", + ] + + print("[TEST] Launching P2P peer Alice (server role)...") + proc_alice = subprocess.Popen( + p2p_server_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + + time.sleep(0.3) + + print("[TEST] Launching P2P peer Bob (client role)...") + proc_bob = subprocess.Popen( + p2p_client_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=env, + ) + + # Wait for client to complete its ticks and disconnect + stdout_bob, stderr_bob = proc_bob.communicate(timeout=25) + print(f"[TEST] Peer Bob completed with code {proc_bob.returncode}") + + # Once client finishes, terminate server + proc_alice.terminate() + stdout_alice, stderr_alice = proc_alice.communicate(timeout=5) + print("[TEST] Peer Alice terminated cleanly.") + + if proc_bob.returncode != 0: + print(f"[TEST FAIL] Bob returned non-zero code {proc_bob.returncode}") + print(f"Bob stdout:\n{stdout_bob}") + print(f"Bob stderr:\n{stderr_bob}") + return 1 + + print("[TEST SUCCESS] XMPP P2P signaling exchange verified successfully!") + return 0 + + finally: + mock_proc.terminate() + try: + mock_proc.wait(timeout=2) + except subprocess.TimeoutExpired: + mock_proc.kill() + + +if __name__ == "__main__": + sys.exit(main()) From c9043d4182eb14abb29ebcb82d9deaf0859cfe96 Mon Sep 17 00:00:00 2001 From: sashakosti Date: Sun, 11 Oct 2026 16:19:24 +0300 Subject: [PATCH 2/3] fix(signaling): make OpenSSL/TLS support optional for libsodium and BCrypt builds --- examples/CMakeLists.txt | 1 + examples/xmpp_signaling_client.cpp | 33 ++++++++++++++++++++++++++++-- tests/CMakeLists.txt | 1 + 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 8e156e82..b2d3eb5c 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -31,6 +31,7 @@ if( ENABLE_ICE AND ( BUILD_EXAMPLES OR BUILD_TESTS ) ) target_link_libraries(xmpp_signaling_client PUBLIC GameNetworkingSockets::shared) if( TARGET OpenSSL::SSL ) target_link_libraries(xmpp_signaling_client PUBLIC OpenSSL::SSL) + target_compile_definitions(xmpp_signaling_client PUBLIC XMPP_HAS_OPENSSL=1) endif() endif() diff --git a/examples/xmpp_signaling_client.cpp b/examples/xmpp_signaling_client.cpp index 16a67158..3dde318c 100644 --- a/examples/xmpp_signaling_client.cpp +++ b/examples/xmpp_signaling_client.cpp @@ -89,8 +89,10 @@ static bool SplitHostPort( const char *pszAddress, std::string &sHost, std::stri return !sHost.empty(); } -#include -#include +#if defined(XMPP_HAS_OPENSSL) && XMPP_HAS_OPENSSL + #include + #include +#endif static const char s_chBase64Table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -289,6 +291,8 @@ class CXmppSignalingClient : public IXmppSignalingClient { } + virtual ~ConnectionSignaling() {} + virtual bool SendSignal( HSteamNetConnection hConn, const SteamNetConnectionInfo_t &info, const void *pMsg, int cbMsg ) override { (void)info; @@ -311,8 +315,10 @@ class CXmppSignalingClient : public IXmppSignalingClient std::recursive_mutex m_sockMutex; SOCKET m_sock; +#if defined(XMPP_HAS_OPENSSL) && XMPP_HAS_OPENSSL SSL_CTX *m_pSSLCtx; SSL *m_pSSL; +#endif std::deque< std::string > m_queueSend; std::string m_sBufferedData; bool m_bConnected; @@ -356,8 +362,10 @@ class CXmppSignalingClient : public IXmppSignalingClient , m_sPassword( pszPassword ) , m_bUseTLS( bUseTLS ) , m_sock( INVALID_SOCKET ) +#if defined(XMPP_HAS_OPENSSL) && XMPP_HAS_OPENSSL , m_pSSLCtx( nullptr ) , m_pSSL( nullptr ) +#endif , m_bConnected( false ) , m_bAuthSent( false ) { @@ -374,6 +382,7 @@ class CXmppSignalingClient : public IXmppSignalingClient void CloseSocket() { std::lock_guard< std::recursive_mutex > lock( m_sockMutex ); +#if defined(XMPP_HAS_OPENSSL) && XMPP_HAS_OPENSSL if ( m_pSSL != nullptr ) { SSL_shutdown( m_pSSL ); @@ -385,6 +394,7 @@ class CXmppSignalingClient : public IXmppSignalingClient SSL_CTX_free( m_pSSLCtx ); m_pSSLCtx = nullptr; } +#endif if ( m_sock != INVALID_SOCKET ) { closesocket( m_sock ); @@ -422,6 +432,7 @@ class CXmppSignalingClient : public IXmppSignalingClient if ( m_bUseTLS ) { +#if defined(XMPP_HAS_OPENSSL) && XMPP_HAS_OPENSSL m_pSSLCtx = SSL_CTX_new( TLS_client_method() ); if ( m_pSSLCtx != nullptr ) { @@ -432,6 +443,10 @@ class CXmppSignalingClient : public IXmppSignalingClient SSL_set_connect_state( m_pSSL ); } } +#else + CloseSocket(); + return; +#endif } std::string sStreamOpen = "