gmessages/libgm/client.go

270 lines
7.3 KiB
Go
Raw Normal View History

2023-06-30 11:05:33 +00:00
package libgm
2023-06-30 09:54:08 +00:00
import (
"crypto/ecdsa"
"crypto/rand"
"crypto/sha256"
"fmt"
2023-06-30 09:54:08 +00:00
"io"
"net/http"
"net/url"
2023-07-16 11:36:13 +00:00
"strconv"
2023-06-30 09:54:08 +00:00
"time"
2023-07-16 10:23:44 +00:00
"github.com/google/uuid"
2023-06-30 09:54:08 +00:00
"github.com/rs/zerolog"
"go.mau.fi/mautrix-gmessages/libgm/crypto"
"go.mau.fi/mautrix-gmessages/libgm/events"
2023-07-17 13:51:31 +00:00
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
2023-06-30 09:54:08 +00:00
"go.mau.fi/mautrix-gmessages/libgm/util"
)
type AuthData struct {
2023-07-16 11:36:13 +00:00
// Keys used to encrypt communication with the phone
RequestCrypto *crypto.AESCTRHelper `json:"request_crypto,omitempty"`
// Key used to sign requests to refresh the tachyon auth token from the server
RefreshKey *crypto.JWK `json:"refresh_key,omitempty"`
// Identity of the paired phone and browser
2023-07-17 13:51:31 +00:00
Browser *gmproto.Device `json:"browser,omitempty"`
Mobile *gmproto.Device `json:"mobile,omitempty"`
2023-07-16 11:36:13 +00:00
// Key used to authenticate with the server
TachyonAuthToken []byte `json:"tachyon_token,omitempty"`
TachyonExpiry time.Time `json:"tachyon_expiry,omitempty"`
TachyonTTL int64 `json:"tachyon_ttl,omitempty"`
// Unknown encryption key, not used for anything
WebEncryptionKey []byte `json:"web_encryption_key,omitempty"`
2023-06-30 09:54:08 +00:00
}
2023-07-16 11:36:13 +00:00
const RefreshTachyonBuffer = 1 * time.Hour
2023-06-30 09:54:08 +00:00
type Proxy func(*http.Request) (*url.URL, error)
2023-07-15 22:45:19 +00:00
type EventHandler func(evt any)
2023-07-16 11:36:13 +00:00
2023-06-30 09:54:08 +00:00
type Client struct {
Logger zerolog.Logger
evHandler EventHandler
sessionHandler *SessionHandler
2023-07-19 11:12:23 +00:00
longPollingConn io.Closer
listenID int
skipCount int
disconnecting bool
recentUpdates [8][32]byte
recentUpdatesPtr int
2023-07-18 21:59:51 +00:00
conversationsFetchedOnce bool
2023-07-16 12:55:30 +00:00
AuthData *AuthData
2023-06-30 09:54:08 +00:00
proxy Proxy
http *http.Client
}
2023-07-16 11:36:13 +00:00
func NewAuthData() *AuthData {
return &AuthData{
RequestCrypto: crypto.NewAESCTRHelper(),
RefreshKey: crypto.GenerateECDSAKey(),
}
}
func NewClient(authData *AuthData, logger zerolog.Logger) *Client {
2023-06-30 09:54:08 +00:00
sessionHandler := &SessionHandler{
responseWaiters: make(map[string]chan<- *IncomingRPCMessage),
2023-06-30 09:54:08 +00:00
responseTimeout: time.Duration(5000) * time.Millisecond,
}
cli := &Client{
2023-07-16 12:55:30 +00:00
AuthData: authData,
2023-06-30 09:54:08 +00:00
Logger: logger,
sessionHandler: sessionHandler,
http: &http.Client{},
2023-06-30 09:54:08 +00:00
}
sessionHandler.client = cli
cli.FetchConfigVersion()
2023-06-30 09:54:08 +00:00
return cli
}
func (c *Client) SetEventHandler(eventHandler EventHandler) {
c.evHandler = eventHandler
}
func (c *Client) SetProxy(proxy string) error {
proxyParsed, err := url.Parse(proxy)
if err != nil {
c.Logger.Fatal().Err(err).Msg("Failed to set proxy")
}
proxyUrl := http.ProxyURL(proxyParsed)
c.http.Transport = &http.Transport{
Proxy: proxyUrl,
}
c.proxy = proxyUrl
c.Logger.Debug().Any("proxy", proxyParsed.Host).Msg("SetProxy")
return nil
}
2023-07-16 12:55:30 +00:00
func (c *Client) Connect() error {
2023-07-16 12:55:30 +00:00
if c.AuthData.TachyonAuthToken == nil {
return fmt.Errorf("no auth token")
} else if c.AuthData.Browser == nil {
return fmt.Errorf("not logged in")
}
2023-07-16 13:18:45 +00:00
err := c.refreshAuthToken()
if err != nil {
return fmt.Errorf("failed to refresh auth token: %w", err)
2023-07-16 12:55:30 +00:00
}
2023-07-16 13:18:45 +00:00
webEncryptionKeyResponse, err := c.GetWebEncryptionKey()
if err != nil {
return fmt.Errorf("failed to get web encryption key: %w", err)
2023-07-16 12:55:30 +00:00
}
c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey())
go c.doLongPoll(true)
2023-07-16 12:55:30 +00:00
c.sessionHandler.startAckInterval()
bugleRes, bugleErr := c.IsBugleDefault()
if bugleErr != nil {
2023-07-16 13:18:45 +00:00
return fmt.Errorf("failed to check bugle default: %w", err)
2023-07-16 12:55:30 +00:00
}
2023-07-17 13:43:34 +00:00
c.Logger.Debug().Bool("bugle_default", bugleRes.Success).Msg("Got is bugle default response on connect")
2023-07-16 12:55:30 +00:00
sessionErr := c.SetActiveSession()
if sessionErr != nil {
2023-07-16 13:18:45 +00:00
return fmt.Errorf("failed to set active session: %w", err)
2023-07-16 12:55:30 +00:00
}
return nil
}
2023-07-01 15:19:57 +00:00
func (c *Client) Disconnect() {
2023-07-19 11:12:23 +00:00
c.closeLongPolling()
2023-07-01 15:19:57 +00:00
c.http.CloseIdleConnections()
}
func (c *Client) IsConnected() bool {
2023-07-19 11:12:23 +00:00
// TODO add better check (longPollingConn is set to nil while the polling reconnects)
return c.longPollingConn != nil
2023-07-01 15:19:57 +00:00
}
func (c *Client) IsLoggedIn() bool {
2023-07-16 12:55:30 +00:00
return c.AuthData != nil && c.AuthData.Browser != nil
}
func (c *Client) Reconnect() error {
2023-07-19 11:12:23 +00:00
c.closeLongPolling()
err := c.Connect()
if err != nil {
2023-07-17 13:43:34 +00:00
c.Logger.Err(err).Msg("Failed to reconnect")
return err
}
2023-07-17 13:43:34 +00:00
c.Logger.Debug().Msg("Successfully reconnected to server")
return nil
2023-07-01 15:19:57 +00:00
}
2023-06-30 09:54:08 +00:00
func (c *Client) triggerEvent(evt interface{}) {
if c.evHandler != nil {
c.evHandler(evt)
}
}
func (c *Client) FetchConfigVersion() {
2023-07-15 23:21:53 +00:00
req, bErr := http.NewRequest("GET", util.ConfigUrl, nil)
if bErr != nil {
2023-07-09 20:32:19 +00:00
panic(bErr)
}
configRes, requestErr := c.http.Do(req)
if requestErr != nil {
2023-07-09 20:32:19 +00:00
panic(requestErr)
}
responseBody, readErr := io.ReadAll(configRes.Body)
if readErr != nil {
2023-07-09 20:32:19 +00:00
panic(readErr)
}
version, parseErr := util.ParseConfigVersion(responseBody)
if parseErr != nil {
2023-07-09 20:32:19 +00:00
panic(parseErr)
}
2023-07-16 12:55:30 +00:00
currVersion := util.ConfigMessage
2023-07-15 13:25:54 +00:00
if version.Year != currVersion.Year || version.Month != currVersion.Month || version.Day != currVersion.Day {
toLog := c.diffVersionFormat(currVersion, version)
c.Logger.Info().Any("version", toLog).Msg("There's a new version available!")
} else {
c.Logger.Info().Any("version", currVersion).Msg("You are running on the latest version.")
}
}
2023-07-17 13:51:31 +00:00
func (c *Client) diffVersionFormat(curr *gmproto.ConfigVersion, latest *gmproto.ConfigVersion) string {
2023-07-15 13:25:54 +00:00
return fmt.Sprintf("%d.%d.%d -> %d.%d.%d", curr.Year, curr.Month, curr.Day, latest.Year, latest.Month, latest.Day)
}
func (c *Client) updateWebEncryptionKey(key []byte) {
2023-07-17 13:43:34 +00:00
c.Logger.Debug().Msg("Updated WebEncryptionKey")
2023-07-16 12:55:30 +00:00
c.AuthData.WebEncryptionKey = key
}
2023-07-16 11:36:13 +00:00
func (c *Client) updateTachyonAuthToken(t []byte, validFor int64) {
2023-07-16 12:55:30 +00:00
c.AuthData.TachyonAuthToken = t
2023-07-16 11:36:13 +00:00
validForDuration := time.Duration(validFor) * time.Microsecond
if validForDuration == 0 {
validForDuration = 24 * time.Hour
}
2023-07-16 12:55:30 +00:00
c.AuthData.TachyonExpiry = time.Now().UTC().Add(time.Microsecond * time.Duration(validFor))
c.AuthData.TachyonTTL = validForDuration.Microseconds()
2023-07-17 13:43:34 +00:00
c.Logger.Debug().
Time("tachyon_expiry", c.AuthData.TachyonExpiry).
Int64("valid_for", validFor).
Msg("Updated tachyon token")
}
func (c *Client) refreshAuthToken() error {
2023-07-16 12:55:30 +00:00
if c.AuthData.Browser == nil || time.Until(c.AuthData.TachyonExpiry) > RefreshTachyonBuffer {
2023-07-16 11:36:13 +00:00
return nil
}
2023-07-17 13:43:34 +00:00
c.Logger.Debug().
Time("tachyon_expiry", c.AuthData.TachyonExpiry).
Msg("Refreshing auth token")
2023-07-16 12:55:30 +00:00
jwk := c.AuthData.RefreshKey
2023-07-16 10:23:44 +00:00
requestID := uuid.NewString()
timestamp := time.Now().UnixMilli() * 1000
signBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", requestID, timestamp)))
sig, err := ecdsa.SignASN1(rand.Reader, jwk.GetPrivateKey(), signBytes[:])
if err != nil {
return err
}
payload := &gmproto.RegisterRefreshRequest{
2023-07-17 13:51:31 +00:00
MessageAuth: &gmproto.AuthMessage{
2023-07-16 11:36:13 +00:00
RequestID: requestID,
2023-07-16 12:55:30 +00:00
TachyonAuthToken: c.AuthData.TachyonAuthToken,
ConfigVersion: util.ConfigMessage,
2023-07-16 11:36:13 +00:00
},
2023-07-16 12:55:30 +00:00
CurrBrowserDevice: c.AuthData.Browser,
2023-07-16 11:36:13 +00:00
UnixTimestamp: timestamp,
Signature: sig,
2023-07-17 23:57:20 +00:00
EmptyRefreshArr: &gmproto.RegisterRefreshRequest_NestedEmptyArr{EmptyArr: &gmproto.EmptyArr{}},
2023-07-16 11:36:13 +00:00
MessageType: 2, // hmm
}
resp, err := typedHTTPResponse[*gmproto.RegisterRefreshResponse](
c.makeProtobufHTTPRequest(util.RegisterRefreshURL, payload, ContentTypePBLite),
)
if err != nil {
return err
}
token := resp.GetTokenData().GetTachyonAuthToken()
if token == nil {
return fmt.Errorf("no tachyon auth token in refresh response")
}
2023-07-16 11:36:13 +00:00
validFor, _ := strconv.ParseInt(resp.GetTokenData().GetValidFor(), 10, 64)
c.updateTachyonAuthToken(token, validFor)
2023-07-16 12:55:30 +00:00
c.triggerEvent(&events.AuthTokenRefreshed{})
return nil
}