From 0e72d8f13c6364a49165e4c3527261c54415cfd8 Mon Sep 17 00:00:00 2001 From: Aleksei Galkin Date: Mon, 25 Apr 2022 23:58:08 +0400 Subject: [PATCH 1/2] Add ability for auth session ticket generation --- appticket/ticket.go | 56 ++++++++++++++ auth.go | 2 + client.go | 175 +++++++++++++++++++++++++++++++++++++++++++- client_events.go | 9 +++ 4 files changed, 240 insertions(+), 2 deletions(-) create mode 100644 appticket/ticket.go diff --git a/appticket/ticket.go b/appticket/ticket.go new file mode 100644 index 00000000..2b0192d0 --- /dev/null +++ b/appticket/ticket.go @@ -0,0 +1,56 @@ +package appticket + +import ( + "encoding/binary" + "net" + "time" +) + +type AppTicket struct { + Version uint32 + SteamId uint64 + AppId uint32 + OwnershipTicketExternalIP net.IP + OwnershipTicketInternalIP net.IP + OwnershipFlags uint32 + OwnershipTicketGenerated time.Time + OwnershipTicketExpires time.Time + + originalBuffer []byte +} + +func (at *AppTicket) IsExpired(currentDate time.Time) bool { + return at.OwnershipTicketExpires.Before(currentDate) +} + +func (at *AppTicket) OriginalBuffer() []byte { + return at.originalBuffer +} + +func NewAppTicket(buffer []byte) (*AppTicket, error) { + at := &AppTicket{ + originalBuffer: make([]byte, len(buffer)), + } + copy(at.originalBuffer, buffer) + + initialLength := binary.LittleEndian.Uint32(buffer[0:]) + + if initialLength == 20 { + panic("Unipmlemented") + } + + at.Version = binary.LittleEndian.Uint32(buffer[4:]) + at.SteamId = binary.LittleEndian.Uint64(buffer[8:]) + at.AppId = binary.LittleEndian.Uint32(buffer[16:]) + at.OwnershipTicketExternalIP = bytesToIp4LE(buffer[20:]) + at.OwnershipTicketInternalIP = bytesToIp4LE(buffer[24:]) + at.OwnershipFlags = binary.LittleEndian.Uint32(buffer[28:]) + at.OwnershipTicketGenerated = time.Unix(int64(binary.LittleEndian.Uint32(buffer[32:])), 0) + at.OwnershipTicketExpires = time.Unix(int64(binary.LittleEndian.Uint32(buffer[36:])), 0) + + return at, nil +} + +func bytesToIp4LE(b []byte) net.IP { + return net.IPv4(b[3], b[2], b[1], b[0]) +} diff --git a/auth.go b/auth.go index 52bf8f64..9cefbca8 100644 --- a/auth.go +++ b/auth.go @@ -108,8 +108,10 @@ func (a *Auth) handleLogOnResponse(packet *protocol.Packet) { result := steamlang.EResult(body.GetEresult()) if result == steamlang.EResult_OK { + a.client.connectTime = time.Unix(int64(body.GetRtime32ServerTime()), 0) atomic.StoreInt32(&a.client.sessionId, msg.Header.Proto.GetClientSessionid()) atomic.StoreUint64(&a.client.steamId, msg.Header.Proto.GetSteamid()) + atomic.StoreUint32(&a.client.publicIp, body.GetPublicIp().GetV4()) a.client.Web.webLoginKey = *body.WebapiAuthenticateUserNonce go a.client.heartbeatLoop(time.Duration(body.GetOutOfGameHeartbeatSeconds())) diff --git a/client.go b/client.go index d135f504..54f3caaa 100644 --- a/client.go +++ b/client.go @@ -6,9 +6,13 @@ import ( "crypto/rand" "encoding/binary" "fmt" + "github.com/Philipp15b/go-steam/v3/appticket" + "google.golang.org/protobuf/proto" "hash/crc32" "io/ioutil" + "log" "net" + "os" "sync" "sync/atomic" "time" @@ -49,6 +53,12 @@ type Client struct { ConnectionTimeout time.Duration + connectTime time.Time + connectionCount int + gcTokens [][]byte + gcTokensMutex sync.Mutex + publicIp uint32 + mutex sync.RWMutex // guarding conn and writeChan conn connection writeChan chan protocol.IMsg @@ -64,6 +74,7 @@ func NewClient() *Client { client := &Client{ events: make(chan interface{}, 3), writeBuf: new(bytes.Buffer), + gcTokens: make([][]byte, 0), } client.Auth = &Auth{client: client} @@ -292,6 +303,14 @@ func (c *Client) handlePacket(packet *protocol.Packet) { c.handleMulti(packet) case steamlang.EMsg_ClientCMList: c.handleClientCMList(packet) + case steamlang.EMsg_ClientGameConnectTokens: + c.handleGameClientToken(packet) + case steamlang.EMsg_ClientGetAppOwnershipTicketResponse: + c.handleGetAppOwnershipTicketResponse(packet) + case steamlang.EMsg_ClientAuthListAck: + c.handleClientAuthListAck(packet) + case steamlang.EMsg_ClientTicketAuthComplete: + c.handleClientTicketAuthComplete(packet) } c.handlersMutex.RLock() @@ -380,14 +399,166 @@ func (c *Client) handleClientCMList(packet *protocol.Packet) { l := make([]*netutil.PortAddr, 0) for i, ip := range body.GetCmAddresses() { l = append(l, &netutil.PortAddr{ - readIp(ip), - uint16(body.GetCmPorts()[i]), + IP: readIp(ip), + Port: uint16(body.GetCmPorts()[i]), }) } c.Emit(&ClientCMListEvent{l}) } +func (c *Client) handleGameClientToken(packet *protocol.Packet) { + body := new(protobuf.CMsgClientGameConnectTokens) + packet.ReadProtoMsg(body) + + c.gcTokensMutex.Lock() + defer c.gcTokensMutex.Unlock() + + c.gcTokens = append(c.gcTokens, body.Tokens...) +} + +func (c *Client) pullGcToken() []byte { + c.gcTokensMutex.Lock() + defer c.gcTokensMutex.Unlock() + + token := c.gcTokens[0] + c.gcTokens = c.gcTokens[1:len(c.gcTokens)] + + return token +} + +func (c *Client) handleGetAppOwnershipTicketResponse(packet *protocol.Packet) { + body := new(protobuf.CMsgClientGetAppOwnershipTicketResponse) + packet.ReadProtoMsg(body) + if body.GetEresult() != uint32(steamlang.EResult_OK) { + log.Fatalf("CMsgClientGetAppOwnershipTicketResponse error: %+#v", body) + // TODO: Add event + return + } + + ioutil.WriteFile(c.generateAppTicketFileCacheName(body.GetAppId()), body.GetTicket(), 0744) + ticket, err := appticket.NewAppTicket(body.GetTicket()) + if err != nil { + log.Fatalf("CMsgClientGetAppOwnershipTicketResponse invalid ticket: %v\n %+#v", err, body) + // TODO: Add event? + return + } + + c.onVaildAppOwnershipTicket(ticket) +} + +func (c *Client) generateAppTicketFileCacheName(appId uint32) string { + return fmt.Sprintf("appOwnershipTicket_%d_%d.bin", c.steamId, appId) +} + +func (c *Client) GetAppOwnershipTicket(appId uint32) { + cachedTickedFileName := c.generateAppTicketFileCacheName(appId) + + appTicket, err := ioutil.ReadFile(cachedTickedFileName) + if err == nil { + parsedTicket, err := appticket.NewAppTicket(appTicket) + + if err == nil && parsedTicket.IsExpired(time.Now().Add(time.Minute)) == false { + go c.onVaildAppOwnershipTicket(parsedTicket) + return + } else { + os.Remove(cachedTickedFileName) + } + } + + c.Write(protocol.NewClientMsgProtobuf(steamlang.EMsg_ClientGetAppOwnershipTicket, &protobuf.CMsgClientGetAppOwnershipTicket{ + AppId: proto.Uint32(appId), + })) +} + +func (c *Client) onVaildAppOwnershipTicket(appticket *appticket.AppTicket) { + c.Emit(&AppOwnershipTicket{AppOwnershipTicket: appticket}) +} + +func (c *Client) createAuthToken(gameConnectToken []byte) []byte { + var buf1 [4]byte + var buf2 [28]byte + + timestamp := uint32(time.Now().Sub(c.connectTime).Milliseconds()) + + binary.LittleEndian.PutUint32(buf1[0:], uint32(len(gameConnectToken))) + + binary.LittleEndian.PutUint32(buf2[0:], 6*4) + binary.LittleEndian.PutUint32(buf2[4:], 1) + binary.LittleEndian.PutUint32(buf2[8:], 2) + binary.BigEndian.PutUint32(buf2[12:], c.publicIp) + binary.LittleEndian.PutUint32(buf2[16:], 0) + binary.LittleEndian.PutUint32(buf2[20:], timestamp) + binary.LittleEndian.PutUint32(buf2[24:], 1) + + result := make([]byte, 0) + result = append(result, buf1[:]...) + result = append(result, gameConnectToken...) + result = append(result, buf2[:]...) + + return result +} + +func (c *Client) handleClientAuthListAck(packet *protocol.Packet) { + c.Emit(&TicketAuthAck{}) +} + +func (c *Client) handleClientTicketAuthComplete(packet *protocol.Packet) { + c.Emit(&TicketAuthComplete{}) +} + +func (c *Client) AuthSessionTicket(ticket *appticket.AppTicket) []byte { + var buf1 [4]byte + var buf2 [32]byte + + gcToken := c.pullGcToken() + + bufTicket := ticket.OriginalBuffer() + + c.connectionCount++ + + timestamp := uint32(time.Now().Sub(c.connectTime).Milliseconds()) + + binary.LittleEndian.PutUint32(buf1[:], uint32(len(gcToken))) + + binary.LittleEndian.PutUint32(buf2[0:], 24) + binary.LittleEndian.PutUint32(buf2[4:], 1) + binary.LittleEndian.PutUint32(buf2[8:], 2) + binary.BigEndian.PutUint32(buf2[12:], c.publicIp) + binary.LittleEndian.PutUint32(buf2[16:], 0) + binary.LittleEndian.PutUint32(buf2[20:], timestamp) + binary.LittleEndian.PutUint32(buf2[24:], uint32(c.connectionCount)) + binary.LittleEndian.PutUint32(buf2[28:], uint32(len(bufTicket))) + + result := make([]byte, 0) + result = append(result, buf1[:]...) + result = append(result, gcToken...) + result = append(result, buf2[:]...) + result = append(result, bufTicket...) + + gameId := uint64(ticket.AppId) + + tokensLeft := uint32(len(c.gcTokens)) + + authToken := c.createAuthToken(gcToken) + ticketCrc := crc32.ChecksumIEEE(authToken) + + authList := new(protobuf.CMsgClientAuthList) + authList.TokensLeft = &tokensLeft + authList.AppIds = []uint32{ticket.AppId} + authList.Tickets = []*protobuf.CMsgAuthTicket{ + &protobuf.CMsgAuthTicket{ + Gameid: &gameId, + Ticket: gcToken, + TicketCrc: &ticketCrc, + }, + } + + c.Write(protocol.NewClientMsgProtobuf(steamlang.EMsg_ClientAuthList, authList)) + + return result +} + func readIp(ip uint32) net.IP { r := make(net.IP, 4) r[3] = byte(ip) diff --git a/client_events.go b/client_events.go index 6e149c79..a4424da8 100644 --- a/client_events.go +++ b/client_events.go @@ -1,6 +1,7 @@ package steam import ( + "github.com/Philipp15b/go-steam/v3/appticket" "github.com/Philipp15b/go-steam/v3/netutil" ) @@ -18,3 +19,11 @@ type DisconnectedEvent struct{} type ClientCMListEvent struct { Addresses []*netutil.PortAddr } + +type TicketAuthAck struct{} + +type TicketAuthComplete struct{} + +type AppOwnershipTicket struct { + AppOwnershipTicket *appticket.AppTicket +} From 442c35763275d3f1e50bba424612afc777b6f5ae Mon Sep 17 00:00:00 2001 From: Aleksei Galkin Date: Sun, 8 May 2022 03:33:56 +0400 Subject: [PATCH 2/2] Fix zero tokens processing --- client.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 54f3caaa..d541c00e 100644 --- a/client.go +++ b/client.go @@ -421,6 +421,9 @@ func (c *Client) pullGcToken() []byte { c.gcTokensMutex.Lock() defer c.gcTokensMutex.Unlock() + if len(c.gcTokens) == 0 { + return nil + } token := c.gcTokens[0] c.gcTokens = c.gcTokens[1:len(c.gcTokens)] @@ -507,11 +510,14 @@ func (c *Client) handleClientTicketAuthComplete(packet *protocol.Packet) { c.Emit(&TicketAuthComplete{}) } -func (c *Client) AuthSessionTicket(ticket *appticket.AppTicket) []byte { +func (c *Client) AuthSessionTicket(ticket *appticket.AppTicket) ([]byte, error) { var buf1 [4]byte var buf2 [32]byte gcToken := c.pullGcToken() + if gcToken == nil { + return gcToken, fmt.Errorf("empty gc token") + } bufTicket := ticket.OriginalBuffer() @@ -556,7 +562,7 @@ func (c *Client) AuthSessionTicket(ticket *appticket.AppTicket) []byte { c.Write(protocol.NewClientMsgProtobuf(steamlang.EMsg_ClientAuthList, authList)) - return result + return result, nil } func readIp(ip uint32) net.IP {