summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/Philipp15b/go-steam/client.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/Philipp15b/go-steam/client.go')
-rw-r--r--vendor/github.com/Philipp15b/go-steam/client.go383
1 files changed, 383 insertions, 0 deletions
diff --git a/vendor/github.com/Philipp15b/go-steam/client.go b/vendor/github.com/Philipp15b/go-steam/client.go
new file mode 100644
index 00000000..667ad354
--- /dev/null
+++ b/vendor/github.com/Philipp15b/go-steam/client.go
@@ -0,0 +1,383 @@
+package steam
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "hash/crc32"
+ "io/ioutil"
+ "net"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/Philipp15b/go-steam/cryptoutil"
+ "github.com/Philipp15b/go-steam/netutil"
+ . "github.com/Philipp15b/go-steam/protocol"
+ . "github.com/Philipp15b/go-steam/protocol/protobuf"
+ . "github.com/Philipp15b/go-steam/protocol/steamlang"
+ . "github.com/Philipp15b/go-steam/steamid"
+)
+
+// Represents a client to the Steam network.
+// Always poll events from the channel returned by Events() or receiving messages will stop.
+// All access, unless otherwise noted, should be threadsafe.
+//
+// When a FatalErrorEvent is emitted, the connection is automatically closed. The same client can be used to reconnect.
+// Other errors don't have any effect.
+type Client struct {
+ // these need to be 64 bit aligned for sync/atomic on 32bit
+ sessionId int32
+ _ uint32
+ steamId uint64
+ currentJobId uint64
+
+ Auth *Auth
+ Social *Social
+ Web *Web
+ Notifications *Notifications
+ Trading *Trading
+ GC *GameCoordinator
+
+ events chan interface{}
+ handlers []PacketHandler
+ handlersMutex sync.RWMutex
+
+ tempSessionKey []byte
+
+ ConnectionTimeout time.Duration
+
+ mutex sync.RWMutex // guarding conn and writeChan
+ conn connection
+ writeChan chan IMsg
+ writeBuf *bytes.Buffer
+ heartbeat *time.Ticker
+}
+
+type PacketHandler interface {
+ HandlePacket(*Packet)
+}
+
+func NewClient() *Client {
+ client := &Client{
+ events: make(chan interface{}, 3),
+ writeBuf: new(bytes.Buffer),
+ }
+ client.Auth = &Auth{client: client}
+ client.RegisterPacketHandler(client.Auth)
+ client.Social = newSocial(client)
+ client.RegisterPacketHandler(client.Social)
+ client.Web = &Web{client: client}
+ client.RegisterPacketHandler(client.Web)
+ client.Notifications = newNotifications(client)
+ client.RegisterPacketHandler(client.Notifications)
+ client.Trading = &Trading{client: client}
+ client.RegisterPacketHandler(client.Trading)
+ client.GC = newGC(client)
+ client.RegisterPacketHandler(client.GC)
+ return client
+}
+
+// Get the event channel. By convention all events are pointers, except for errors.
+// It is never closed.
+func (c *Client) Events() <-chan interface{} {
+ return c.events
+}
+
+func (c *Client) Emit(event interface{}) {
+ c.events <- event
+}
+
+// Emits a FatalErrorEvent formatted with fmt.Errorf and disconnects.
+func (c *Client) Fatalf(format string, a ...interface{}) {
+ c.Emit(FatalErrorEvent(fmt.Errorf(format, a...)))
+ c.Disconnect()
+}
+
+// Emits an error formatted with fmt.Errorf.
+func (c *Client) Errorf(format string, a ...interface{}) {
+ c.Emit(fmt.Errorf(format, a...))
+}
+
+// Registers a PacketHandler that receives all incoming packets.
+func (c *Client) RegisterPacketHandler(handler PacketHandler) {
+ c.handlersMutex.Lock()
+ defer c.handlersMutex.Unlock()
+ c.handlers = append(c.handlers, handler)
+}
+
+func (c *Client) GetNextJobId() JobId {
+ return JobId(atomic.AddUint64(&c.currentJobId, 1))
+}
+
+func (c *Client) SteamId() SteamId {
+ return SteamId(atomic.LoadUint64(&c.steamId))
+}
+
+func (c *Client) SessionId() int32 {
+ return atomic.LoadInt32(&c.sessionId)
+}
+
+func (c *Client) Connected() bool {
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+ return c.conn != nil
+}
+
+// Connects to a random Steam server and returns its address.
+// If this client is already connected, it is disconnected first.
+// This method tries to use an address from the Steam Directory and falls
+// back to the built-in server list if the Steam Directory can't be reached.
+// If you want to connect to a specific server, use `ConnectTo`.
+func (c *Client) Connect() *netutil.PortAddr {
+ var server *netutil.PortAddr
+ if steamDirectoryCache.IsInitialized() {
+ server = steamDirectoryCache.GetRandomCM()
+ } else {
+ server = GetRandomCM()
+ }
+ c.ConnectTo(server)
+ return server
+}
+
+// Connects to a specific server.
+// You may want to use one of the `GetRandom*CM()` functions in this package.
+// If this client is already connected, it is disconnected first.
+func (c *Client) ConnectTo(addr *netutil.PortAddr) {
+ c.ConnectToBind(addr, nil)
+}
+
+// Connects to a specific server, and binds to a specified local IP
+// If this client is already connected, it is disconnected first.
+func (c *Client) ConnectToBind(addr *netutil.PortAddr, local *net.TCPAddr) {
+ c.Disconnect()
+
+ conn, err := dialTCP(local, addr.ToTCPAddr())
+ if err != nil {
+ c.Fatalf("Connect failed: %v", err)
+ return
+ }
+ c.conn = conn
+ c.writeChan = make(chan IMsg, 5)
+
+ go c.readLoop()
+ go c.writeLoop()
+}
+
+func (c *Client) Disconnect() {
+ c.mutex.Lock()
+ defer c.mutex.Unlock()
+
+ if c.conn == nil {
+ return
+ }
+
+ c.conn.Close()
+ c.conn = nil
+ if c.heartbeat != nil {
+ c.heartbeat.Stop()
+ }
+ close(c.writeChan)
+ c.Emit(&DisconnectedEvent{})
+
+}
+
+// Adds a message to the send queue. Modifications to the given message after
+// writing are not allowed (possible race conditions).
+//
+// Writes to this client when not connected are ignored.
+func (c *Client) Write(msg IMsg) {
+ if cm, ok := msg.(IClientMsg); ok {
+ cm.SetSessionId(c.SessionId())
+ cm.SetSteamId(c.SteamId())
+ }
+ c.mutex.RLock()
+ defer c.mutex.RUnlock()
+ if c.conn == nil {
+ return
+ }
+ c.writeChan <- msg
+}
+
+func (c *Client) readLoop() {
+ for {
+ // This *should* be atomic on most platforms, but the Go spec doesn't guarantee it
+ c.mutex.RLock()
+ conn := c.conn
+ c.mutex.RUnlock()
+ if conn == nil {
+ return
+ }
+ packet, err := conn.Read()
+
+ if err != nil {
+ c.Fatalf("Error reading from the connection: %v", err)
+ return
+ }
+ c.handlePacket(packet)
+ }
+}
+
+func (c *Client) writeLoop() {
+ for {
+ c.mutex.RLock()
+ conn := c.conn
+ c.mutex.RUnlock()
+ if conn == nil {
+ return
+ }
+
+ msg, ok := <-c.writeChan
+ if !ok {
+ return
+ }
+
+ err := msg.Serialize(c.writeBuf)
+ if err != nil {
+ c.writeBuf.Reset()
+ c.Fatalf("Error serializing message %v: %v", msg, err)
+ return
+ }
+
+ err = conn.Write(c.writeBuf.Bytes())
+
+ c.writeBuf.Reset()
+
+ if err != nil {
+ c.Fatalf("Error writing message %v: %v", msg, err)
+ return
+ }
+ }
+}
+
+func (c *Client) heartbeatLoop(seconds time.Duration) {
+ if c.heartbeat != nil {
+ c.heartbeat.Stop()
+ }
+ c.heartbeat = time.NewTicker(seconds * time.Second)
+ for {
+ _, ok := <-c.heartbeat.C
+ if !ok {
+ break
+ }
+ c.Write(NewClientMsgProtobuf(EMsg_ClientHeartBeat, new(CMsgClientHeartBeat)))
+ }
+ c.heartbeat = nil
+}
+
+func (c *Client) handlePacket(packet *Packet) {
+ switch packet.EMsg {
+ case EMsg_ChannelEncryptRequest:
+ c.handleChannelEncryptRequest(packet)
+ case EMsg_ChannelEncryptResult:
+ c.handleChannelEncryptResult(packet)
+ case EMsg_Multi:
+ c.handleMulti(packet)
+ case EMsg_ClientCMList:
+ c.handleClientCMList(packet)
+ }
+
+ c.handlersMutex.RLock()
+ defer c.handlersMutex.RUnlock()
+ for _, handler := range c.handlers {
+ handler.HandlePacket(packet)
+ }
+}
+
+func (c *Client) handleChannelEncryptRequest(packet *Packet) {
+ body := NewMsgChannelEncryptRequest()
+ packet.ReadMsg(body)
+
+ if body.Universe != EUniverse_Public {
+ c.Fatalf("Invalid univserse %v!", body.Universe)
+ }
+
+ c.tempSessionKey = make([]byte, 32)
+ rand.Read(c.tempSessionKey)
+ encryptedKey := cryptoutil.RSAEncrypt(GetPublicKey(EUniverse_Public), c.tempSessionKey)
+
+ payload := new(bytes.Buffer)
+ payload.Write(encryptedKey)
+ binary.Write(payload, binary.LittleEndian, crc32.ChecksumIEEE(encryptedKey))
+ payload.WriteByte(0)
+ payload.WriteByte(0)
+ payload.WriteByte(0)
+ payload.WriteByte(0)
+
+ c.Write(NewMsg(NewMsgChannelEncryptResponse(), payload.Bytes()))
+}
+
+func (c *Client) handleChannelEncryptResult(packet *Packet) {
+ body := NewMsgChannelEncryptResult()
+ packet.ReadMsg(body)
+
+ if body.Result != EResult_OK {
+ c.Fatalf("Encryption failed: %v", body.Result)
+ return
+ }
+ c.conn.SetEncryptionKey(c.tempSessionKey)
+ c.tempSessionKey = nil
+
+ c.Emit(&ConnectedEvent{})
+}
+
+func (c *Client) handleMulti(packet *Packet) {
+ body := new(CMsgMulti)
+ packet.ReadProtoMsg(body)
+
+ payload := body.GetMessageBody()
+
+ if body.GetSizeUnzipped() > 0 {
+ r, err := gzip.NewReader(bytes.NewReader(payload))
+ if err != nil {
+ c.Errorf("handleMulti: Error while decompressing: %v", err)
+ return
+ }
+
+ payload, err = ioutil.ReadAll(r)
+ if err != nil {
+ c.Errorf("handleMulti: Error while decompressing: %v", err)
+ return
+ }
+ }
+
+ pr := bytes.NewReader(payload)
+ for pr.Len() > 0 {
+ var length uint32
+ binary.Read(pr, binary.LittleEndian, &length)
+ packetData := make([]byte, length)
+ pr.Read(packetData)
+ p, err := NewPacket(packetData)
+ if err != nil {
+ c.Errorf("Error reading packet in Multi msg %v: %v", packet, err)
+ continue
+ }
+ c.handlePacket(p)
+ }
+}
+
+func (c *Client) handleClientCMList(packet *Packet) {
+ body := new(CMsgClientCMList)
+ packet.ReadProtoMsg(body)
+
+ l := make([]*netutil.PortAddr, 0)
+ for i, ip := range body.GetCmAddresses() {
+ l = append(l, &netutil.PortAddr{
+ readIp(ip),
+ uint16(body.GetCmPorts()[i]),
+ })
+ }
+
+ c.Emit(&ClientCMListEvent{l})
+}
+
+func readIp(ip uint32) net.IP {
+ r := make(net.IP, 4)
+ r[3] = byte(ip)
+ r[2] = byte(ip >> 8)
+ r[1] = byte(ip >> 16)
+ r[0] = byte(ip >> 24)
+ return r
+}