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

Add ability for auth session ticket generation #127

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
56 changes: 56 additions & 0 deletions appticket/ticket.go
Original file line number Diff line number Diff line change
@@ -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])
}
2 changes: 2 additions & 0 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
181 changes: 179 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -380,14 +399,172 @@ 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()

if len(c.gcTokens) == 0 {
return nil
}
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, 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()

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, nil
}

func readIp(ip uint32) net.IP {
r := make(net.IP, 4)
r[3] = byte(ip)
Expand Down
9 changes: 9 additions & 0 deletions client_events.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package steam

import (
"github.com/Philipp15b/go-steam/v3/appticket"
"github.com/Philipp15b/go-steam/v3/netutil"
)

Expand All @@ -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
}