package steam
import (
"crypto/aes"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"github.com/Philipp15b/go-steam/cryptoutil"
. "github.com/Philipp15b/go-steam/protocol"
. "github.com/Philipp15b/go-steam/protocol/protobuf"
. "github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/golang/protobuf/proto"
"net/http"
"net/url"
"strconv"
"sync/atomic"
)
type Web struct {
// 64 bit alignment
relogOnNonce uint32
// The `sessionid` cookie required to use the steam website.
// This cookie may contain a characters that will need to be URL-escaped, otherwise
// Steam (probably) interprets is as a string.
// When used as an URL paramter this is automatically escaped by the Go HTTP package.
SessionId string
// The `steamLogin` cookie required to use the steam website. Already URL-escaped.
// This is only available after calling LogOn().
SteamLogin string
// The `steamLoginSecure` cookie required to use the steam website over HTTPs. Already URL-escaped.
// This is only availbile after calling LogOn().
SteamLoginSecure string
webLoginKey string
client *Client
}
func (w *Web) HandlePacket(packet *Packet) {
switch packet.EMsg {
case EMsg_ClientNewLoginKey:
w.handleNewLoginKey(packet)
case EMsg_ClientRequestWebAPIAuthenticateUserNonceResponse:
w.handleAuthNonceResponse(packet)
}
}
// Fetches the `steamLogin` cookie. This may only be called after the first
// WebSessionIdEvent or it will panic.
func (w *Web) LogOn() {
if w.webLoginKey == "" {
panic("Web: webLoginKey not initialized!")
}
go func() {
// retry three times. yes, I know about loops.
err := w.apiLogOn()
if err != nil {
err = w.apiLogOn()
if err != nil {
err = w.apiLogOn()
}
}
if err != nil {
w.client.Emit(WebLogOnErrorEvent(err))
return
}
}()
}
func (w *Web) apiLogOn() error {
sessionKey := make([]byte, 32)
rand.Read(sessionKey)
cryptedSessionKey := cryptoutil.RSAEncrypt(GetPublicKey(EUniverse_Public), sessionKey)
ciph, _ := aes.NewCipher(sessionKey)
cryptedLoginKey := cryptoutil.SymmetricEncrypt(ciph, []byte(w.webLoginKey))
data := make(url.Values)
data.Add("format", "json")
data.Add("steamid", strconv.FormatUint(w.client.SteamId().ToUint64(), 10))
data.Add("sessionkey", string(cryptedSessionKey))
data.Add("encrypted_loginkey", string(cryptedLoginKey))
resp, err := http.PostForm("https://api.steampowered.com/ISteamUserAuth/AuthenticateUser/v0001", data)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
// our web login key has expired, request a new one
atomic.StoreUint32(&w.relogOnNonce, 1)
w.client.Write(NewClientMsgProtobuf(EMsg_ClientRequestWebAPIAuthenticateUserNonce, new(CMsgClientRequestWebAPIAuthenticateUserNonce)))
return nil
} else if resp.StatusCode != 200 {
return errors.New("steam.Web.apiLogOn: request failed with status " + resp.Status)
}
result := new(struct {
Authenticateuser struct {
Token string
TokenSecure string
}
})
err = json.NewDecoder(resp.Body).Decode(result)
if err != nil {
return err
}
w.SteamLogin = result.Authenticateuser.Token
w.SteamLoginSecure = result.Authenticateuser.TokenSecure
w.client.Emit(new(WebLoggedOnEvent))
return nil
}
func (w *Web) handleNewLoginKey(packet *Packet) {
msg := new(CMsgClientNewLoginKey)
packet.ReadProtoMsg(msg)
w.client.Write(NewClientMsgProtobuf(EMsg_ClientNewLoginKeyAccepted, &CMsgClientNewLoginKeyAccepted{
UniqueId: proto.Uint32(msg.GetUniqueId()),
}))
// number -> string -> bytes -> base64
w.SessionId = base64.StdEncoding.EncodeToString([]byte(strconv.FormatUint(uint64(msg.GetUniqueId()), 10)))
w.client.Emit(new(WebSessionIdEvent))
}
func (w *Web) handleAuthNonceResponse(packet *Packet) {
// this has to be the best name for a message yet.
msg := new(CMsgClientRequestWebAPIAuthenticateUserNonceResponse)
packet.ReadProtoMsg(msg)
w.webLoginKey = msg.GetWebapiAuthenticateUserNonce()
// if the nonce was specifically requested in apiLogOn(),
// don't emit an event.
if atomic.CompareAndSwapUint32(&w.relogOnNonce, 1, 0) {
w.LogOn()
}
}