Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISteamUser.GetAuthSessionTicket implementation #789

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/Callbacks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Collections.Generic;
using SteamKit2.Internal;

namespace SteamKit2
{
public sealed partial class SteamAuthTicket
{
/// <summary>
/// This callback is fired when Steam accepts our auth ticket as valid.
/// </summary>
internal sealed class TicketAcceptedCallback : CallbackMsg
{
/// <summary>
/// <see cref="List{T}"/> of AppIDs of the games that have generated tickets.
/// </summary>
public List<uint> AppIDs { get; private set; }
/// <summary>
/// <see cref="List{T}"/> of CRC32 hashes of activated tickets.
/// </summary>
public List<uint> ActiveTicketsCRC { get; private set; }
/// <summary>
/// Number of message in sequence.
/// </summary>
public uint MessageSequence { get; private set; }

internal TicketAcceptedCallback( JobID jobId, CMsgClientAuthListAck body )
{
JobID = jobId;
AppIDs = body.app_ids;
ActiveTicketsCRC = body.ticket_crc;
MessageSequence = body.message_sequence;
}
}

/// <summary>
/// This callback is fired when generated ticket was successfully used to authenticate user.
/// </summary>
internal sealed class TicketAuthCompleteCallback : CallbackMsg
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make these callbacks public, as youre using Client.Post, otherwise consumers won't be able to use them.

{
/// <summary>
/// Steam response to authentication request.
/// </summary>
public EAuthSessionResponse AuthSessionResponse { get; }
/// <summary>
/// Authentication state.
/// </summary>
public uint State { get; }
/// <summary>
/// ID of the game the token was generated for.
/// </summary>
public GameID GameID { get; }
/// <summary>
/// <see cref="SteamKit2.SteamID"/> of the game owner.
/// </summary>
public SteamID OwnerSteamID { get; }
/// <summary>
/// <see cref="SteamKit2.SteamID"/> of the game server.
/// </summary>
public SteamID SteamID { get; }
/// <summary>
/// CRC of the ticket.
/// </summary>
public uint TicketCRC { get; }
/// <summary>
/// Sequence of the ticket.
/// </summary>
public uint TicketSequence { get; }

internal TicketAuthCompleteCallback( JobID targetJobID, CMsgClientTicketAuthComplete body )
{
JobID = targetJobID;
AuthSessionResponse = ( EAuthSessionResponse )body.eauth_session_response;
State = body.estate;
GameID = body.game_id;
OwnerSteamID = body.owner_steam_id;
SteamID = body.steam_id;
TicketCRC = body.ticket_crc;
TicketSequence = body.ticket_sequence;
}
}
}
}
210 changes: 210 additions & 0 deletions SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/SteamAuthTicket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SteamKit2.Internal;

namespace SteamKit2
{
/// <summary>
/// This handler generates auth session ticket and handles it's verification by steam.
/// </summary>
public sealed partial class SteamAuthTicket : ClientMsgHandler
{
private readonly Dictionary<EMsg, Action<IPacketMsg>> dispatchMap;
private readonly ConcurrentQueue<byte[]> gameConnectTokens = new ConcurrentQueue<byte[]>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this queue be cleared on a log off?

private readonly ConcurrentDictionary<uint, List<CMsgAuthTicket>> ticketsByGame = new ConcurrentDictionary<uint, List<CMsgAuthTicket>>();

internal SteamAuthTicket()
{
dispatchMap = new Dictionary<EMsg, Action<IPacketMsg>>
{
{ EMsg.ClientAuthListAck, HandleTicketAcknowledged },
{ EMsg.ClientTicketAuthComplete, HandleTicketAuthComplete },
{ EMsg.ClientGameConnectTokens, HandleGameConnectTokens },
{ EMsg.ClientLogOnResponse, HandleLogOnResponse }
};
}

/// <summary>
/// Generate session ticket, and verify it with steam servers.
/// </summary>
/// <param name="appid">The appid to request the ticket of.</param>
/// <returns><c>null</c> if user isn't fully logged in, doesn't own the game, or steam deemed ticket invalid; otherwise <see cref="TicketInfo" /> instance.</returns>
public async Task<TicketInfo?> GetAuthSessionTicket( uint appid )
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task returning method seems out of place for me looking at the rest of the codebase, but couldn't figure out how to do it better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async is fine.

{
// not logged in
if ( Client.CellID == null )
{
return null;
}

var apps = Client.GetHandler<SteamApps>()!;
var appTicket = await apps.GetAppOwnershipTicket( appid );
// user doesn't own the game
if ( appTicket.Result != EResult.OK )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this throw?

{
return null;
}

if ( gameConnectTokens.TryDequeue( out var token ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this seems like a silent failure, if there is no way to request new tokens, i think it should throw that there are no tokens available.

{
byte[] authToken = CreateAuthToken( token );
var ticketTask = await VerifyTicket( appid, authToken, out var crc );
// verify the ticket is on the list of accepted tickets
// didn't happen on my testing, but I don't think it hurts to check
if ( ticketTask.ActiveTicketsCRC.Any( x => x == crc ) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this throw?

{
return new TicketInfo( this, appid, crc, BuildTicket( authToken, appTicket.Ticket ) );
}
}
return null;
}
internal bool CancelAuthTicket( TicketInfo ticket )
{
if(ticketsByGame.TryGetValue(ticket.AppID, out var values))
{
if ( values.RemoveAll( x => x.ticket_crc == ticket.CRC ) > 0 )
{
SendTickets();
}
}
return false;
}

private byte[] BuildTicket( byte[] authToken, byte[] appTicket )
{
using ( var stream = new MemoryStream( authToken.Length + 4 + appTicket.Length ) )
{
using ( var writer = new BinaryWriter( stream ) )
{
writer.Write( authToken );
writer.Write( appTicket.Length );
writer.Write( appTicket );
}
return stream.ToArray();
}
}
private byte[] CreateAuthToken( byte[] gameConnectToken )
{
const int sessionSize =
4 + // unknown 1
4 + // unknown 2
4 + // external IP
4 + // padding
4 + // connection time
4; // connection count

// We checked that we're connected before calling this function
uint ipAddress = NetHelpers.GetIPAddress( Client.PublicIP! );
int connectionTime = ( int )( ( DateTime.UtcNow - serverTime ).TotalMilliseconds );
using ( var stream = new MemoryStream( 4 + gameConnectToken.Length + 4 + sessionSize ) )
{
using ( var writer = new BinaryWriter( stream ) )
{
writer.Write( gameConnectToken.Length );
writer.Write( gameConnectToken.ToArray() );

writer.Write( sessionSize );
writer.Write( 1 );
writer.Write( 2 );

writer.Write( ipAddress );
writer.Write( 0 ); // padding
writer.Write( connectionTime ); // in milliseconds
writer.Write( 1 ); // single client connected
}

return stream.ToArray();
}
}

private AsyncJob<TicketAcceptedCallback> VerifyTicket( uint appid, byte[] authToken, out uint crc )
{
crc = Crc32.Compute( authToken );
var items = ticketsByGame.GetOrAdd( appid, new List<CMsgAuthTicket>() );

// add ticket to specified games list
items.Add( new CMsgAuthTicket
{
gameid = appid,
ticket = authToken,
ticket_crc = crc
} );
return SendTickets();
}
private AsyncJob<TicketAcceptedCallback> SendTickets()
{
var auth = new ClientMsgProtobuf<CMsgClientAuthList>( EMsg.ClientAuthList );
auth.Body.tokens_left = ( uint )gameConnectTokens.Count;
// all registered games
auth.Body.app_ids.AddRange( ticketsByGame.Keys );
// flatten all registered per-game tickets
auth.Body.tickets.AddRange( ticketsByGame.Values.SelectMany( x => x ) );
auth.SourceJobID = Client.GetNextJobID();
Client.Send( auth );
return new AsyncJob<TicketAcceptedCallback>( Client, auth.SourceJobID );
}

/// <summary>
/// Handles a client message. This should not be called directly.
/// </summary>
/// <param name="packetMsg">The packet message that contains the data.</param>
public override void HandleMsg( IPacketMsg packetMsg )
{
if ( packetMsg == null )
{
throw new ArgumentNullException( nameof( packetMsg ) );
}

if ( dispatchMap.TryGetValue( packetMsg.MsgType, out var handlerFunc ) )
{
handlerFunc( packetMsg );
}
}

#region ClientMsg Handlers
private void HandleLogOnResponse( IPacketMsg packetMsg )
{
var body = new ClientMsgProtobuf<CMsgClientLogonResponse>( packetMsg ).Body;
// just grabbing server time
serverTime = DateUtils.DateTimeFromUnixTime( body.rtime32_server_time );
}
private void HandleGameConnectTokens( IPacketMsg packetMsg )
{
var body = new ClientMsgProtobuf<CMsgClientGameConnectTokens>( packetMsg ).Body;

// add tokens
foreach ( var tok in body.tokens )
{
gameConnectTokens.Enqueue( tok );
}

// keep only required amount, discard old entries
while ( gameConnectTokens.Count > body.max_tokens_to_keep )
{
gameConnectTokens.TryDequeue( out _ );
}
}
private void HandleTicketAuthComplete( IPacketMsg packetMsg )
{
// ticket successfully used to authorize user
var complete = new ClientMsgProtobuf<CMsgClientTicketAuthComplete>( packetMsg );
var inUse = new TicketAuthCompleteCallback( complete.TargetJobID, complete.Body );
Client.PostCallback( inUse );
}
private void HandleTicketAcknowledged( IPacketMsg packetMsg )
{
// ticket acknowledged as valid by steam
var authAck = new ClientMsgProtobuf<CMsgClientAuthListAck>( packetMsg );
var acknowledged = new TicketAcceptedCallback( authAck.TargetJobID, authAck.Body );
Client.PostCallback( acknowledged );
}
#endregion

private DateTime serverTime;

}
}
37 changes: 37 additions & 0 deletions SteamKit2/SteamKit2/Steam/Handlers/SteamAuthTicket/TicketInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace SteamKit2
{
/// <summary>
/// Represents a valid authorized session ticket.
/// </summary>
public class TicketInfo : IDisposable
{
internal uint AppID { get; }
internal uint CRC { get; }
/// <summary>
/// Bytes of the valid Session Ticket
/// </summary>
public byte[] Ticket { get; }

internal TicketInfo( SteamAuthTicket handler, uint appid, uint crc, byte[] ticket )
{
_handler = handler;
AppID = appid;
CRC = crc;
Ticket = ticket;
}

/// <summary>
/// Tell steam we no longer use the ticket.
/// </summary>
public void Dispose()
{
_handler.CancelAuthTicket( this );
}

private readonly SteamAuthTicket _handler;
}
}
1 change: 1 addition & 0 deletions SteamKit2/SteamKit2/Steam/SteamClient/SteamClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public SteamClient( SteamConfiguration configuration, string identifier )
this.AddHandler( new SteamScreenshots() );
this.AddHandler( new SteamMatchmaking() );
this.AddHandler( new SteamNetworking() );
this.AddHandler( new SteamAuthTicket() );

using ( var process = Process.GetCurrentProcess() )
{
Expand Down