From e5d3f180dc6e8e9dc3663dbd77e7dca36b14558b Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Thu, 15 Jan 2026 12:53:45 +0500 Subject: [PATCH] engine: client: implement steam broker client side --- engine/client/cl_main.c | 103 +++++++++++++++++++++---------- engine/client/cl_steam.c | 129 +++++++++++++++++++++++++++++++++++++++ engine/client/client.h | 16 +++++ 3 files changed, 215 insertions(+), 33 deletions(-) create mode 100644 engine/client/cl_steam.c diff --git a/engine/client/cl_main.c b/engine/client/cl_main.c index 1cc40aed..65faf006 100644 --- a/engine/client/cl_main.c +++ b/engine/client/cl_main.c @@ -1056,13 +1056,6 @@ static void CL_WriteSteamTicket( sizebuf_t *send ) return; } - //if( !Q_strcmp( cl_ticket_generator.string, "steam" ) - //{ - // i = SteamBroker_InitiateGameConnection( buf, sizeof( buf )); - // MSG_WriteBytes( send, buf, i ); - // return; - //} - s = ID_GetMD5(); CRC32_Init( &crc ); CRC32_ProcessBuffer( &crc, s, Q_strlen( s )); @@ -1075,6 +1068,40 @@ static void CL_WriteSteamTicket( sizebuf_t *send ) *(uint32_t*)(cls.steamid + 4) = LittleLong( ((uint32_t*)buf)[5] ); } +void CL_SendGoldSrcConnectPacket( netadr_t adr, int challenge, const void *ticket, size_t ticketlen ) +{ + const char *name; + sizebuf_t send; + byte send_buf[2048]; + char protinfo[MAX_INFO_STRING]; + + protinfo[0] = 0; + + Info_SetValueForKey( protinfo, "prot", "3", sizeof( protinfo )); // steam auth type + Info_SetValueForKeyf( protinfo, "unique", sizeof( protinfo ), "%i", 0xffffffff ); + Info_SetValueForKey( protinfo, "raw", "steam", sizeof( protinfo )); + CL_GetCDKey( protinfo, sizeof( protinfo )); + name = Info_ValueForKey( cls.userinfo, "name" ); + if( cl_advertise_engine_in_name.value && Q_strnicmp( name, "[Xash3D]", 8 )) + Info_SetValueForKeyf( cls.userinfo, "name", sizeof( cls.userinfo ), "[Xash3D]%s", name ); + + MSG_Init( &send, "GoldSrcConnect", send_buf, sizeof( send_buf )); + MSG_WriteLong( &send, NET_HEADER_OUTOFBANDPACKET ); + MSG_WriteStringf( &send, C2S_CONNECT" %i %i \"%s\" \"%s\"\n", + PROTOCOL_GOLDSRC_VERSION, challenge, protinfo, cls.userinfo ); + MSG_SeekToBit( &send, -8, SEEK_CUR ); // rewrite null terminator + if( ticket == NULL ) + CL_WriteSteamTicket( &send ); + else + MSG_WriteBytes( &send, ticket, ticketlen ); + + if( MSG_CheckOverflow( &send )) + Con_Printf( S_ERROR "%s: %s overflow!\n", __func__, MSG_GetName( &send ) ); + + NET_SendPacket( NS_CLIENT, MSG_GetNumBytesWritten( &send ), MSG_GetData( &send ), adr ); + Con_Printf( "Trying to connect with GoldSrc 48 protocol\n" ); +} + /* ======================= CL_SendConnectPacket @@ -1117,33 +1144,24 @@ static void CL_SendConnectPacket( connprotocol_t proto, int challenge ) Info_SetValueForKey( protinfo, "a", Q_buildarch(), sizeof( protinfo ) ); } + cls.broker_wait = false; + if( proto == PROTO_GOLDSRC ) { - const char *name; - sizebuf_t send; - byte send_buf[2048]; + // if the cl_ticket_generator is set to "steam" we need to get ticket + // from steam broker, which is asynchronous process by nature + if( !Q_stricmp( cl_ticket_generator.string, "steam" )) + { + if( SteamBroker_InitiateGameConnection( adr, challenge )) + { + // we are waiting for the broker response... + cls.broker_wait = true; + cls.timestart = Platform_DoubleTime(); + return; + } + } - Info_SetValueForKey( protinfo, "prot", "3", sizeof( protinfo )); // steam auth type - Info_SetValueForKeyf( protinfo, "unique", sizeof( protinfo ), "%i", 0xffffffff ); - Info_SetValueForKey( protinfo, "raw", "steam", sizeof( protinfo )); - CL_GetCDKey( protinfo, sizeof( protinfo )); - - name = Info_ValueForKey( cls.userinfo, "name" ); - if( cl_advertise_engine_in_name.value && Q_strnicmp( name, "[Xash3D]", 8 )) - Info_SetValueForKeyf( cls.userinfo, "name", sizeof( cls.userinfo ), "[Xash3D]%s", name ); - - MSG_Init( &send, "GoldSrcConnect", send_buf, sizeof( send_buf )); - MSG_WriteLong( &send, NET_HEADER_OUTOFBANDPACKET ); - MSG_WriteStringf( &send, C2S_CONNECT" %i %i \"%s\" \"%s\"\n", - PROTOCOL_GOLDSRC_VERSION, challenge, protinfo, cls.userinfo ); - MSG_SeekToBit( &send, -8, SEEK_CUR ); // rewrite null terminator - CL_WriteSteamTicket( &send ); - - if( MSG_CheckOverflow( &send )) - Con_Printf( S_ERROR "%s: %s overflow!\n", __func__, MSG_GetName( &send ) ); - - NET_SendPacket( NS_CLIENT, MSG_GetNumBytesWritten( &send ), MSG_GetData( &send ), adr ); - Con_Printf( "Trying to connect with GoldSrc 48 protocol\n" ); + CL_SendGoldSrcConnectPacket( adr, challenge, NULL, 0 ); } else { @@ -1320,7 +1338,7 @@ static void CL_CheckForResend( void ) else CL_SendBandwidthTest( adr, false ); } - else + else if( !cls.broker_wait ) { Con_Printf( "Connecting to %s... (retry #%i)\n", cls.servername, cls.connect_retry ); CL_SendGetChallenge( adr ); @@ -1686,6 +1704,7 @@ void CL_Disconnect( void ) // send a disconnect message to the server CL_SendDisconnectMessage( cls.legacymode ); + SteamBroker_TerminateGameConnection(); CL_ClearState (); S_StopBackgroundTrack (); @@ -2373,8 +2392,21 @@ static void CL_Challenge( const char *c, netadr_t from ) // try to autodetect protocol by challenge response if( !Q_strcmp( c, S2C_GOLDSRC_CHALLENGE )) + { cls.legacymode = PROTO_GOLDSRC; + cls.steam_auth = Q_atoi( Cmd_Argv( 2 )) == 3; + + if( Cmd_Argc( ) == 5 && cls.steam_auth ) + { + // arg 2 auth protocol, we only support steam + // arg 3 if steam is server's steam id + // arg 4 if steam is server's VAC status + cls.server_steamid = strtoull( Cmd_Argv( 3 ), NULL, 10 ); + cls.vac2_secure = Q_atoi( Cmd_Argv( 4 )); + } + } + cls.bandwidth_test.challenge = Q_atoi( Cmd_Argv( 1 )); if( cls.legacymode == PROTO_CURRENT && cl_test_bandwidth.value && !cls.bandwidth_test.passed ) @@ -2537,6 +2569,10 @@ static void CL_ConnectionlessPacket( netadr_t from, sizebuf_t *msg ) Con_Reportf( "%s: %s : %s\n", __func__, NET_AdrToString( from ), c ); // server connection + if( !Q_strcmp( c, "sb_connect" )) + { + SteamBroker_HandlePacket( from, msg ); + } if( !Q_strcmp( c, S2C_GOLDSRC_CONNECTION ) || !Q_strcmp( c, S2C_CONNECTION )) { CL_ClientConnect( cls.legacymode, c, from ); @@ -3637,6 +3673,7 @@ void CL_Init( void ) Host_Error( "can't initialize %s: %s\n", libpath, COM_GetLibraryError( )); ID_Init(); + SteamBroker_Init(); cls.build_num = 0; cls.initialized = true; @@ -3668,6 +3705,7 @@ void CL_Shutdown( void ) Mobile_Shutdown (); SCR_Shutdown (); CL_UnloadProgs (); + SteamBroker_Shutdown(); cls.initialized = false; // for client-side VGUI support we use other order @@ -3681,5 +3719,4 @@ void CL_Shutdown( void ) R_Shutdown (); Con_Shutdown (); - } diff --git a/engine/client/cl_steam.c b/engine/client/cl_steam.c new file mode 100644 index 00000000..6eca6ca2 --- /dev/null +++ b/engine/client/cl_steam.c @@ -0,0 +1,129 @@ +/* +cl_steam.c - steam(tm) broker implementation +Copyright (C) 2026 Xash3D FWGS contributors + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. +*/ + +#include +#include "common.h" +#include "client.h" + +// What is a broker? +// From Wikipedia, the free encyclopedia: +// "The broker pattern is an architecture pattern that involves the use of an +// intermediary software entity, called a "broker", to facilitate communication +// between two or more software components. The broker acts as a "middleman" +// between the components, allowing them to communicate without being aware of +// each other's existence. +// +// Due to proprietary nature of Steamworks SDK, it cannot be run on same amount +// of platforms supported by Xash3D FWGS, neither we can link directly due to +// GNU GPLv3 license. However, here comes the broker, by running it (in trusted +// network, preferrably) on a machine that has Steam client installed, the +// engine can communicate with it, acquiring needed information to log-in into +// Steam protected multiplayer servers. +static CVAR_DEFINE_AUTO( cl_steam_broker_addr, "127.0.0.1:27420", FCVAR_ARCHIVE, "address of steam broker instance" ); + +static struct +{ + netadr_t adr; + + int challenge; + netadr_t serveradr; +} broker; + +qboolean SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge ) +{ + if( NET_NetadrType( &broker.adr ) == NA_UNDEFINED ) + { + if( !NET_StringToAdr( cl_steam_broker_addr.string, &broker.adr )) + { + Con_Printf( "%s: NET_StringToAdr( %s ) failed\n", __func__, cl_steam_broker_addr.string ); + return false; + } + } + + // only ipv4 supported + if( NET_NetadrType( &serveradr ) != NA_IP ) + return false; + + broker.challenge = challenge; + broker.serveradr = serveradr; + + // sb_connect + // the message calls + char buf[512]; + int len = Q_snprintf( buf, sizeof( buf ), "sb_connect %s %"PRIu64" %s %d", NET_AdrToString( serveradr ), cls.server_steamid, cls.vac2_secure ? "true" : "false", broker.challenge ); + + NET_SendPacket( NS_CLIENT, len, buf, broker.adr ); + + return true; +} + +void SteamBroker_TerminateGameConnection( void ) +{ + if( NET_NetadrType( &broker.adr ) == NA_UNDEFINED ) + return; + + if( NET_NetadrType( &cls.serveradr ) != NA_IP ) + return; + + if( cls.legacymode != PROTO_GOLDSRC ) + return; + + // sb_terminate quitйгше + char buf[512]; + int len = Q_snprintf( buf, sizeof( buf ), "sb_terminate %s %d", NET_AdrToString( cls.serveradr ), broker.challenge ); + + NET_SendPacket( NS_CLIENT, len, buf, broker.adr ); +} + +void SteamBroker_HandlePacket( netadr_t from, sizebuf_t *msg ) +{ + // message format + // sb_connect\n<4 byte challenge><8 byte steamid> + int challenge; + uint32_t len; + uint8_t ticket[2048]; // 2048 bytes according to SDK docs + + if( !NET_CompareAdr( from, broker.adr )) + return; + + challenge = MSG_ReadLong( msg ); + + if( broker.challenge != challenge ) // TODO: print error + return; + + MSG_ReadBytes( msg, cls.steamid, sizeof( cls.steamid )); + + len = MSG_ReadDword( msg ); + if( len > sizeof( ticket )) // TODO: print error, proceed without ticket? + return; + + MSG_ReadBytes( msg, ticket, len ); + + Con_Printf( "%s: SteamID: %"PRIu64", ticket: [%d, %d, %d, %d...]\n", __func__, *(uint64_t *)cls.steamid, ticket[0], ticket[1], ticket[2], ticket[3] ); + + CL_SendGoldSrcConnectPacket( broker.serveradr, challenge, ticket, len ); + + cls.broker_wait = false; +} + +void SteamBroker_Init( void ) +{ + Cvar_RegisterVariable( &cl_steam_broker_addr ); + NET_NetadrSetType( &broker.adr, NA_UNDEFINED ); +} + +void SteamBroker_Shutdown( void ) +{ +} diff --git a/engine/client/client.h b/engine/client/client.h index c57603a8..22bb6bfe 100644 --- a/engine/client/client.h +++ b/engine/client/client.h @@ -649,10 +649,16 @@ typedef struct // server's build number (might be zero) int build_num; uint8_t steamid[8]; + uint64_t server_steamid; // whether server allows cheats or not // set differently depending on protocol and extensions qboolean allow_cheats; + + // if server declares steam auth method, we can use our broker software to get the ticket + qboolean steam_auth; + qboolean broker_wait; + qboolean vac2_secure; } client_static_t; #ifdef __cplusplus @@ -777,6 +783,7 @@ int CL_IsDevOverviewMode( void ); void CL_SignonReply( connprotocol_t proto ); void CL_ClearState( void ); void CL_SetCheatState( qboolean multiplayer, qboolean allow_cheats ); +void CL_SendGoldSrcConnectPacket( netadr_t adr, int challenge, const void *ticket, size_t ticketlen ); // // cl_demo.c @@ -1194,6 +1201,15 @@ void Mobile_Shutdown( void ); // void CL_GetSecuredClientAPI( CL_EXPORT_FUNCS F ); +// +// cl_steam.c +// +void SteamBroker_Init( void ); +void SteamBroker_Shutdown( void ); +void SteamBroker_HandlePacket( netadr_t from, sizebuf_t *msg ); +int SteamBroker_InitiateGameConnection( netadr_t serveradr, int challenge ); +void SteamBroker_TerminateGameConnection( void ); + // // cl_video.c //