engine: client: implement steam broker client side

This commit is contained in:
Alibek Omarov
2026-01-15 12:53:45 +05:00
parent 5cb5e76872
commit e5d3f180dc
3 changed files with 215 additions and 33 deletions

View File

@@ -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 ();
}

129
engine/client/cl_steam.c Normal file
View File

@@ -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 <inttypes.h>
#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 <ip:port> <server's steam id> <secure> <challenge>
// 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 <ip:port> <challenge>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><unsigned 4 byte len><len bytes ticket>
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 )
{
}

View File

@@ -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
//