From 6225b83d2f1c378f785d3be5644a15f88432959f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 16 Jul 2023 15:55:30 +0300 Subject: [PATCH] Refactor login flow --- commands.go | 90 +++++++++++------ gmtest/main.go | 28 ++++-- libgm/client.go | 156 ++++++++++++------------------ libgm/event_handler.go | 3 +- libgm/events/ready.go | 10 +- libgm/media_processor.go | 7 +- libgm/pair.go | 134 ++++++++----------------- libgm/pairing_handler.go | 27 ++---- libgm/payload/sendMessage.go | 3 +- libgm/qr.go | 8 +- libgm/rpc.go | 7 +- libgm/session_handler.go | 14 +-- libgm/{payload => util}/config.go | 5 +- main.go | 6 +- user.go | 83 ++++++++++++---- 15 files changed, 276 insertions(+), 305 deletions(-) rename libgm/{payload => util}/config.go (78%) diff --git a/commands.go b/commands.go index 6f026dd..6d07c3c 100644 --- a/commands.go +++ b/commands.go @@ -18,6 +18,7 @@ package main import ( "context" + "fmt" "github.com/skip2/go-qrcode" "maunium.net/go/mautrix" @@ -82,57 +83,82 @@ func fnLogin(ce *WrappedCommandEvent) { ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?") } return - } - - ce.User.hackyLoginCommand = ce - ce.User.hackyLoginCommandPrevEvent = "" - _, err := ce.User.Login(context.Background()) - if err != nil { - ce.User.log.Errorf("Failed to log in:", err) - ce.Reply("Failed to log in: %v", err) + } else if ce.User.pairSuccessChan != nil { + ce.Reply("You already have a login in progress") return } + + ch, err := ce.User.Login(context.Background(), 6) + if err != nil { + ce.ZLog.Err(err).Msg("Failed to start login") + ce.Reply("Failed to start login: %v", err) + return + } + var prevEvent id.EventID + for item := range ch { + switch { + case item.qr != "": + ce.ZLog.Debug().Msg("Got code in QR channel") + prevEvent = ce.User.sendQR(ce, item.qr, prevEvent) + case item.err != nil: + ce.ZLog.Err(err).Msg("Error in QR channel") + prevEvent = ce.User.sendQREdit(ce, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: fmt.Sprintf("Failed to log in: %v", err), + }, prevEvent) + case item.success: + ce.ZLog.Debug().Msg("Got pair success in QR channel") + prevEvent = ce.User.sendQREdit(ce, &event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: "Successfully logged in", + }, prevEvent) + } + } + ce.ZLog.Trace().Msg("Login command finished") } -func (user *User) sendQR(ce *WrappedCommandEvent, code string, prevEvent id.EventID) id.EventID { - url, ok := user.uploadQR(ce, code) - if !ok { - return prevEvent - } - content := event.MessageEventContent{ - MsgType: event.MsgImage, - Body: code, - URL: url.CUString(), - } +func (user *User) sendQREdit(ce *WrappedCommandEvent, content *event.MessageEventContent, prevEvent id.EventID) id.EventID { if len(prevEvent) != 0 { content.SetEdit(prevEvent) } resp, err := ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &content) if err != nil { - user.log.Errorln("Failed to send edited QR code to user:", err) + ce.ZLog.Err(err).Msg("Failed to send edited QR code") } else if len(prevEvent) == 0 { prevEvent = resp.EventID } return prevEvent } -func (user *User) uploadQR(ce *WrappedCommandEvent, code string) (id.ContentURI, bool) { +func (user *User) sendQR(ce *WrappedCommandEvent, code string, prevEvent id.EventID) id.EventID { + var content event.MessageEventContent + url, err := user.uploadQR(code) + if err != nil { + ce.ZLog.Err(err).Msg("Failed to upload QR code") + content = event.MessageEventContent{ + MsgType: event.MsgNotice, + Body: fmt.Sprintf("Failed to upload QR code: %v", err), + } + } else { + content = event.MessageEventContent{ + MsgType: event.MsgImage, + Body: code, + URL: url.CUString(), + } + } + return user.sendQREdit(ce, &content, prevEvent) +} + +func (user *User) uploadQR(code string) (id.ContentURI, error) { qrCode, err := qrcode.Encode(code, qrcode.Low, 256) if err != nil { - user.log.Errorln("Failed to encode QR code:", err) - ce.Reply("Failed to encode QR code: %v", err) - return id.ContentURI{}, false + return id.ContentURI{}, err } - - bot := user.bridge.AS.BotClient() - - resp, err := bot.UploadBytes(qrCode, "image/png") + resp, err := user.bridge.Bot.UploadBytes(qrCode, "image/png") if err != nil { - user.log.Errorln("Failed to upload QR code:", err) - ce.Reply("Failed to upload QR code: %v", err) - return id.ContentURI{}, false + return id.ContentURI{}, err } - return resp.ContentURI, true + return resp.ContentURI, nil } var cmdLogout = &commands.FullHandler{ @@ -236,7 +262,7 @@ func fnPing(ce *WrappedCommandEvent) { ce.Reply("You're not logged into Google Messages.") } } else if ce.User.Client == nil || !ce.User.Client.IsConnected() { - ce.Reply("You're logged in as %s (device #%d), but you don't have a Google Messages connection.", ce.User.Phone) + ce.Reply("You're logged in as %s, but you don't have a Google Messages connection.", ce.User.Phone) } else { ce.Reply("Logged in as %s, connection to Google Messages may be OK", ce.User.Phone) } diff --git a/gmtest/main.go b/gmtest/main.go index 95b4939..65a7fa1 100644 --- a/gmtest/main.go +++ b/gmtest/main.go @@ -39,11 +39,13 @@ func main() { w.TimeFormat = time.Stamp })).With().Timestamp().Logger() file, err := os.Open("session.json") + var doLogin bool if err != nil { if !errors.Is(err, os.ErrNotExist) { panic(err) } sess = *libgm.NewAuthData() + doLogin = true } else { must(json.NewDecoder(file).Decode(&sess)) log.Info().Msg("Loaded session?") @@ -51,7 +53,23 @@ func main() { _ = file.Close() cli = libgm.NewClient(&sess, log) cli.SetEventHandler(evtHandler) - must(cli.Connect()) + if doLogin { + qr := mustReturn(cli.StartLogin()) + qrterminal.GenerateHalfBlock(qr, qrterminal.L, os.Stdout) + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for range ticker.C { + if sess.Browser != nil { + return + } + qr := mustReturn(cli.RefreshPhoneRelay()) + qrterminal.GenerateHalfBlock(qr, qrterminal.L, os.Stdout) + } + }() + } else { + must(cli.Connect()) + } c := make(chan os.Signal) input := make(chan string) @@ -97,20 +115,12 @@ func evtHandler(rawEvt any) { log.Debug().Any("data", evt).Msg("Client is ready!") case *events.PairSuccessful: log.Debug().Any("data", evt).Msg("Pair successful") - //kd := evt.Data.(*binary.AuthenticationContainer_KeyData) - //sess.DevicePair = &pblite.DevicePair{ - // Mobile: kd.KeyData.Mobile, - // Browser: kd.KeyData.Browser, - //} - //sess.TachyonAuthToken = evt.AuthMessage.TachyonAuthToken saveSession() log.Debug().Msg("Wrote session") case *binary.Message: log.Debug().Any("data", evt).Msg("Message event") case *binary.Conversation: log.Debug().Any("data", evt).Msg("Conversation event") - case *events.QR: - qrterminal.GenerateHalfBlock(evt.URL, qrterminal.L, os.Stdout) case *events.BrowserActive: log.Debug().Any("data", evt).Msg("Browser active") default: diff --git a/libgm/client.go b/libgm/client.go index f7a8c49..bf3002e 100644 --- a/libgm/client.go +++ b/libgm/client.go @@ -5,12 +5,10 @@ import ( "crypto/rand" "crypto/sha256" "encoding/base64" - "encoding/json" "fmt" "io" "net/http" "net/url" - "os" "strconv" "time" @@ -21,7 +19,6 @@ import ( "go.mau.fi/mautrix-gmessages/libgm/binary" "go.mau.fi/mautrix-gmessages/libgm/crypto" "go.mau.fi/mautrix-gmessages/libgm/events" - "go.mau.fi/mautrix-gmessages/libgm/payload" "go.mau.fi/mautrix-gmessages/libgm/pblite" "go.mau.fi/mautrix-gmessages/libgm/util" ) @@ -50,11 +47,10 @@ type EventHandler func(evt any) type Client struct { Logger zerolog.Logger rpc *RPC - pairer *Pairer evHandler EventHandler sessionHandler *SessionHandler - authData *AuthData + AuthData *AuthData proxy Proxy http *http.Client @@ -73,7 +69,7 @@ func NewClient(authData *AuthData, logger zerolog.Logger) *Client { responseTimeout: time.Duration(5000) * time.Millisecond, } cli := &Client{ - authData: authData, + AuthData: authData, Logger: logger, sessionHandler: sessionHandler, http: &http.Client{}, @@ -102,46 +98,52 @@ func (c *Client) SetProxy(proxy string) error { c.Logger.Debug().Any("proxy", proxyParsed.Host).Msg("SetProxy") return nil } + func (c *Client) Connect() error { - if c.authData.TachyonAuthToken != nil { - refreshErr := c.refreshAuthToken() - if refreshErr != nil { - panic(refreshErr) - } - - webEncryptionKeyResponse, webEncryptionKeyErr := c.GetWebEncryptionKey() - if webEncryptionKeyErr != nil { - c.Logger.Err(webEncryptionKeyErr).Any("response", webEncryptionKeyResponse).Msg("GetWebEncryptionKey request failed") - return webEncryptionKeyErr - } - c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey()) - go c.rpc.ListenReceiveMessages() - c.sessionHandler.startAckInterval() - - bugleRes, bugleErr := c.IsBugleDefault() - if bugleErr != nil { - panic(bugleErr) - } - c.Logger.Info().Any("isBugle", bugleRes.Success).Msg("IsBugleDefault") - sessionErr := c.SetActiveSession() - if sessionErr != nil { - panic(sessionErr) - } - return nil - } else { - pairer, err := c.NewPairer(c.authData.RefreshKey, 20) - if err != nil { - panic(err) - } - c.pairer = pairer - registered, err2 := c.pairer.RegisterPhoneRelay() - if err2 != nil { - return err2 - } - c.authData.TachyonAuthToken = registered.AuthKeyData.TachyonAuthToken - go c.rpc.ListenReceiveMessages() - return nil + if c.AuthData.TachyonAuthToken == nil { + return fmt.Errorf("no auth token") + } else if c.AuthData.Browser == nil { + return fmt.Errorf("not logged in") } + + refreshErr := c.refreshAuthToken() + if refreshErr != nil { + panic(refreshErr) + } + + webEncryptionKeyResponse, webEncryptionKeyErr := c.GetWebEncryptionKey() + if webEncryptionKeyErr != nil { + c.Logger.Err(webEncryptionKeyErr).Any("response", webEncryptionKeyResponse).Msg("GetWebEncryptionKey request failed") + return webEncryptionKeyErr + } + c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey()) + go c.rpc.ListenReceiveMessages() + c.sessionHandler.startAckInterval() + + bugleRes, bugleErr := c.IsBugleDefault() + if bugleErr != nil { + panic(bugleErr) + } + c.Logger.Info().Any("isBugle", bugleRes.Success).Msg("IsBugleDefault") + sessionErr := c.SetActiveSession() + if sessionErr != nil { + panic(sessionErr) + } + return nil +} + +func (c *Client) StartLogin() (string, error) { + registered, err := c.RegisterPhoneRelay() + if err != nil { + return "", err + } + c.AuthData.TachyonAuthToken = registered.AuthKeyData.TachyonAuthToken + go c.rpc.ListenReceiveMessages() + qr, err := c.GenerateQRCodeData(registered.GetPairingKey()) + if err != nil { + return "", fmt.Errorf("failed to generate QR code: %w", err) + } + return qr, nil } func (c *Client) Disconnect() { @@ -154,7 +156,7 @@ func (c *Client) IsConnected() bool { } func (c *Client) IsLoggedIn() bool { - return c.authData != nil && c.authData.Browser != nil + return c.AuthData != nil && c.AuthData.Browser != nil } func (c *Client) Reconnect() error { @@ -164,10 +166,10 @@ func (c *Client) Reconnect() error { } err := c.Connect() if err != nil { - c.Logger.Err(err).Any("tachyonAuthToken", c.authData.TachyonAuthToken).Msg("Failed to reconnect") + c.Logger.Err(err).Any("tachyonAuthToken", c.AuthData.TachyonAuthToken).Msg("Failed to reconnect") return err } - c.Logger.Debug().Any("tachyonAuthToken", c.authData.TachyonAuthToken).Msg("Successfully reconnected to server") + c.Logger.Debug().Any("tachyonAuthToken", c.AuthData.TachyonAuthToken).Msg("Successfully reconnected to server") return nil } @@ -185,8 +187,8 @@ func (c *Client) DownloadMedia(mediaID string, key []byte) ([]byte, error) { }, AuthData: &binary.AuthMessage{ RequestID: uuid.NewString(), - TachyonAuthToken: c.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, } downloadMetadataBytes, err2 := proto.Marshal(downloadMetadata) @@ -242,7 +244,7 @@ func (c *Client) FetchConfigVersion() { panic(parseErr) } - currVersion := payload.ConfigMessage + currVersion := util.ConfigMessage 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!") @@ -257,56 +259,26 @@ func (c *Client) diffVersionFormat(curr *binary.ConfigVersion, latest *binary.Co func (c *Client) updateWebEncryptionKey(key []byte) { c.Logger.Debug().Any("key", key).Msg("Updated WebEncryptionKey") - c.authData.WebEncryptionKey = key + c.AuthData.WebEncryptionKey = key } func (c *Client) updateTachyonAuthToken(t []byte, validFor int64) { - c.authData.TachyonAuthToken = t + c.AuthData.TachyonAuthToken = t validForDuration := time.Duration(validFor) * time.Microsecond if validForDuration == 0 { validForDuration = 24 * time.Hour } - c.authData.TachyonExpiry = time.Now().UTC().Add(time.Microsecond * time.Duration(validFor)) - c.authData.TachyonTTL = validForDuration.Microseconds() - c.Logger.Debug().Time("tachyon_expiry", c.authData.TachyonExpiry).Int64("valid_for", validFor).Msg("Updated tachyon token") -} - -func (c *Client) updateDevicePair(mobile, browser *binary.Device) { - c.authData.Mobile = mobile - c.authData.Browser = browser - c.Logger.Debug().Any("mobile", mobile).Any("browser", browser).Msg("Updated device pair") -} - -func (c *Client) SaveAuthSession(path string) error { - toSaveJson, jsonErr := json.Marshal(c.authData) - if jsonErr != nil { - return jsonErr - } - writeErr := os.WriteFile(path, toSaveJson, os.ModePerm) - return writeErr -} - -func LoadAuthSession(path string) (*AuthData, error) { - jsonData, readErr := os.ReadFile(path) - if readErr != nil { - return nil, readErr - } - - sessionData := &AuthData{} - marshalErr := json.Unmarshal(jsonData, sessionData) - if marshalErr != nil { - return nil, marshalErr - } - - return sessionData, nil + c.AuthData.TachyonExpiry = time.Now().UTC().Add(time.Microsecond * time.Duration(validFor)) + c.AuthData.TachyonTTL = validForDuration.Microseconds() + c.Logger.Debug().Time("tachyon_expiry", c.AuthData.TachyonExpiry).Int64("valid_for", validFor).Msg("Updated tachyon token") } func (c *Client) refreshAuthToken() error { - if c.authData.Browser == nil || time.Until(c.authData.TachyonExpiry) > RefreshTachyonBuffer { + if c.AuthData.Browser == nil || time.Until(c.AuthData.TachyonExpiry) > RefreshTachyonBuffer { return nil } - c.Logger.Debug().Time("tachyon_expiry", c.authData.TachyonExpiry).Msg("Refreshing auth token") - jwk := c.authData.RefreshKey + c.Logger.Debug().Time("tachyon_expiry", c.AuthData.TachyonExpiry).Msg("Refreshing auth token") + jwk := c.AuthData.RefreshKey requestID := uuid.NewString() timestamp := time.Now().UnixMilli() * 1000 @@ -319,10 +291,10 @@ func (c *Client) refreshAuthToken() error { payload, err := pblite.Marshal(&binary.RegisterRefreshPayload{ MessageAuth: &binary.AuthMessage{ RequestID: requestID, - TachyonAuthToken: c.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, - CurrBrowserDevice: c.authData.Browser, + CurrBrowserDevice: c.AuthData.Browser, UnixTimestamp: timestamp, Signature: sig, EmptyRefreshArr: &binary.EmptyRefreshArr{EmptyArr: &binary.EmptyArr{}}, @@ -363,6 +335,6 @@ func (c *Client) refreshAuthToken() error { validFor, _ := strconv.ParseInt(resp.GetTokenData().GetValidFor(), 10, 64) c.updateTachyonAuthToken(token, validFor) - c.triggerEvent(events.NewAuthTokenRefreshed(token)) + c.triggerEvent(&events.AuthTokenRefreshed{}) return nil } diff --git a/libgm/event_handler.go b/libgm/event_handler.go index abf8faf..70938fa 100644 --- a/libgm/event_handler.go +++ b/libgm/event_handler.go @@ -43,7 +43,7 @@ func (r *RPC) deduplicateUpdate(response *pblite.Response) bool { } func (r *RPC) HandleRPCMsg(msg *binary.InternalMessage) { - response, decodeErr := pblite.DecryptInternalMessage(msg, r.client.authData.RequestCrypto) + response, decodeErr := pblite.DecryptInternalMessage(msg, r.client.AuthData.RequestCrypto) if decodeErr != nil { r.client.Logger.Error().Err(decodeErr).Msg("rpc decrypt msg err") return @@ -55,7 +55,6 @@ func (r *RPC) HandleRPCMsg(msg *binary.InternalMessage) { r.client.sessionHandler.queueMessageAck(response.ResponseID) if r.client.sessionHandler.receiveResponse(response) { - r.client.Logger.Debug().Str("request_id", response.Data.RequestID).Msg("Received response") return } switch response.BugleRoute { diff --git a/libgm/events/ready.go b/libgm/events/ready.go index d81c854..1986ae0 100644 --- a/libgm/events/ready.go +++ b/libgm/events/ready.go @@ -19,15 +19,7 @@ func NewClientReady(sessionID string, conversationList *binary.Conversations) *C } } -type AuthTokenRefreshed struct { - Token []byte -} - -func NewAuthTokenRefreshed(token []byte) *AuthTokenRefreshed { - return &AuthTokenRefreshed{ - Token: token, - } -} +type AuthTokenRefreshed struct{} type HTTPError struct { Action string diff --git a/libgm/media_processor.go b/libgm/media_processor.go index 6c1c492..fab86b4 100644 --- a/libgm/media_processor.go +++ b/libgm/media_processor.go @@ -12,7 +12,6 @@ import ( "google.golang.org/protobuf/proto" "go.mau.fi/mautrix-gmessages/libgm/binary" - "go.mau.fi/mautrix-gmessages/libgm/payload" "go.mau.fi/mautrix-gmessages/libgm/util" ) @@ -131,10 +130,10 @@ func (c *Client) buildStartUploadPayload() (string, error) { ImageType: 1, AuthData: &binary.AuthMessage{ RequestID: uuid.NewString(), - TachyonAuthToken: c.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, - Mobile: c.authData.Mobile, + Mobile: c.AuthData.Mobile, } protoDataBytes, err := proto.Marshal(protoData) diff --git a/libgm/pair.go b/libgm/pair.go index 8deef46..4e0ad80 100644 --- a/libgm/pair.go +++ b/libgm/pair.go @@ -3,38 +3,16 @@ package libgm import ( "crypto/x509" "io" - "time" "github.com/google/uuid" "google.golang.org/protobuf/proto" "go.mau.fi/mautrix-gmessages/libgm/binary" - "go.mau.fi/mautrix-gmessages/libgm/crypto" - "go.mau.fi/mautrix-gmessages/libgm/events" - "go.mau.fi/mautrix-gmessages/libgm/payload" "go.mau.fi/mautrix-gmessages/libgm/util" ) -type Pairer struct { - client *Client - KeyData *crypto.JWK - ticker *time.Ticker - tickerTime time.Duration - pairingKey []byte -} - -func (c *Client) NewPairer(keyData *crypto.JWK, refreshQrCodeTime int) (*Pairer, error) { - p := &Pairer{ - client: c, - KeyData: keyData, - tickerTime: time.Duration(refreshQrCodeTime) * time.Second, - } - c.pairer = p - return p, nil -} - -func (p *Pairer) RegisterPhoneRelay() (*binary.RegisterPhoneRelayResponse, error) { - key, err := x509.MarshalPKIXPublicKey(p.KeyData.GetPublicKey()) +func (c *Client) RegisterPhoneRelay() (*binary.RegisterPhoneRelayResponse, error) { + key, err := x509.MarshalPKIXPublicKey(c.AuthData.RefreshKey.GetPublicKey()) if err != nil { return nil, err } @@ -42,10 +20,10 @@ func (p *Pairer) RegisterPhoneRelay() (*binary.RegisterPhoneRelayResponse, error body, err := proto.Marshal(&binary.AuthenticationContainer{ AuthMessage: &binary.AuthMessage{ RequestID: uuid.NewString(), - Network: &payload.Network, - ConfigVersion: payload.ConfigMessage, + Network: &util.Network, + ConfigVersion: util.ConfigMessage, }, - BrowserDetails: payload.BrowserDetailsMessage, + BrowserDetails: util.BrowserDetailsMessage, Data: &binary.AuthenticationContainer_KeyData{ KeyData: &binary.KeyData{ EcdsaKeys: &binary.ECDSAKeys{ @@ -56,91 +34,66 @@ func (p *Pairer) RegisterPhoneRelay() (*binary.RegisterPhoneRelayResponse, error }, }) if err != nil { - p.client.Logger.Err(err) - return &binary.RegisterPhoneRelayResponse{}, err - } - relayResponse, reqErr := p.client.MakeRelayRequest(util.RegisterPhoneRelayURL, body) - if reqErr != nil { - p.client.Logger.Err(reqErr) + c.Logger.Err(err) return nil, err } - responseBody, err2 := io.ReadAll(relayResponse.Body) - if err2 != nil { - return nil, err2 + relayResponse, reqErr := c.MakeRelayRequest(util.RegisterPhoneRelayURL, body) + if reqErr != nil { + c.Logger.Err(reqErr) + return nil, err + } + responseBody, err := io.ReadAll(relayResponse.Body) + if err != nil { + return nil, err } relayResponse.Body.Close() res := &binary.RegisterPhoneRelayResponse{} - err3 := proto.Unmarshal(responseBody, res) - if err3 != nil { - return nil, err3 + err = proto.Unmarshal(responseBody, res) + if err != nil { + return nil, err } - p.pairingKey = res.GetPairingKey() - p.client.Logger.Debug().Any("response", res).Msg("Registerphonerelay response") - url, qrErr := p.GenerateQRCodeData() - if qrErr != nil { - return nil, qrErr - } - p.client.triggerEvent(&events.QR{URL: url}) - p.startRefreshRelayTask() return res, err } -func (p *Pairer) startRefreshRelayTask() { - if p.ticker != nil { - p.ticker.Stop() - } - ticker := time.NewTicker(30 * time.Second) - p.ticker = ticker - go func() { - for range ticker.C { - p.RefreshPhoneRelay() - } - }() -} - -func (p *Pairer) RefreshPhoneRelay() { +func (c *Client) RefreshPhoneRelay() (string, error) { body, err := proto.Marshal(&binary.AuthenticationContainer{ AuthMessage: &binary.AuthMessage{ RequestID: uuid.NewString(), - Network: &payload.Network, - TachyonAuthToken: p.client.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + Network: &util.Network, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, }) if err != nil { - p.client.Logger.Err(err).Msg("refresh phone relay err") - return + return "", err } - relayResponse, reqErr := p.client.MakeRelayRequest(util.RefreshPhoneRelayURL, body) - if reqErr != nil { - p.client.Logger.Err(reqErr).Msg("refresh phone relay err") + relayResponse, err := c.MakeRelayRequest(util.RefreshPhoneRelayURL, body) + if err != nil { + return "", err } - responseBody, err2 := io.ReadAll(relayResponse.Body) + responseBody, err := io.ReadAll(relayResponse.Body) defer relayResponse.Body.Close() - if err2 != nil { - p.client.Logger.Err(err2).Msg("refresh phone relay err") + if err != nil { + return "", err } - p.client.Logger.Debug().Any("responseLength", len(responseBody)).Msg("Response Body Length") res := &binary.RefreshPhoneRelayResponse{} - err3 := proto.Unmarshal(responseBody, res) - if err3 != nil { - p.client.Logger.Err(err3) + err = proto.Unmarshal(responseBody, res) + if err != nil { + return "", err } - p.pairingKey = res.GetPairKey() - p.client.Logger.Debug().Any("res", res).Msg("RefreshPhoneRelayResponse") - url, qrErr := p.GenerateQRCodeData() - if qrErr != nil { - panic(qrErr) + qr, err := c.GenerateQRCodeData(res.GetPairKey()) + if err != nil { + return "", err } - p.client.triggerEvent(&events.QR{URL: url}) + return qr, nil } func (c *Client) GetWebEncryptionKey() (*binary.WebEncryptionKeyResponse, error) { body, err := proto.Marshal(&binary.AuthenticationContainer{ AuthMessage: &binary.AuthMessage{ RequestID: uuid.NewString(), - TachyonAuthToken: c.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, }) if err != nil { @@ -160,25 +113,20 @@ func (c *Client) GetWebEncryptionKey() (*binary.WebEncryptionKeyResponse, error) if err != nil { return nil, err } - if c.pairer != nil { - if c.pairer.ticker != nil { - c.pairer.ticker.Stop() - } - } return parsedResponse, nil } func (c *Client) Unpair() (*binary.RevokeRelayPairingResponse, error) { - if c.authData.TachyonAuthToken == nil || c.authData.Browser == nil { + if c.AuthData.TachyonAuthToken == nil || c.AuthData.Browser == nil { return nil, nil } payload, err := proto.Marshal(&binary.RevokeRelayPairing{ AuthMessage: &binary.AuthMessage{ RequestID: uuid.NewString(), - TachyonAuthToken: c.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: c.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, - Browser: c.authData.Browser, + Browser: c.AuthData.Browser, }) if err != nil { return nil, err diff --git a/libgm/pairing_handler.go b/libgm/pairing_handler.go index 54a5c7e..d29dcea 100644 --- a/libgm/pairing_handler.go +++ b/libgm/pairing_handler.go @@ -17,7 +17,7 @@ func (c *Client) handlePairingEvent(response *pblite.Response) { switch evt := pairEventData.Event.(type) { case *binary.PairEvents_Paired: - callbackErr := c.pairCallback(evt.Paired) + callbackErr := c.completePairing(evt.Paired) if callbackErr != nil { panic(callbackErr) } @@ -29,27 +29,12 @@ func (c *Client) handlePairingEvent(response *pblite.Response) { } } -func (c *Client) NewDevicePair(mobile, browser *binary.Device) *pblite.DevicePair { - return &pblite.DevicePair{ - Mobile: mobile, - Browser: browser, - } -} +func (c *Client) completePairing(data *binary.PairedData) error { + c.updateTachyonAuthToken(data.GetTokenData().GetTachyonAuthToken(), data.GetTokenData().GetTTL()) + c.AuthData.Mobile = data.Mobile + c.AuthData.Browser = data.Browser -func (c *Client) pairCallback(data *binary.PairedData) error { - - tokenData := data.GetTokenData() - c.updateTachyonAuthToken(tokenData.GetTachyonAuthToken(), tokenData.GetTTL()) - - c.updateDevicePair(data.Mobile, data.Browser) - - webEncryptionKeyResponse, webErr := c.GetWebEncryptionKey() - if webErr != nil { - return webErr - } - c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey()) - - c.triggerEvent(&events.PairSuccessful{data}) + c.triggerEvent(&events.PairSuccessful{PairedData: data}) reconnectErr := c.Reconnect() if reconnectErr != nil { diff --git a/libgm/payload/sendMessage.go b/libgm/payload/sendMessage.go index 63b826c..3a59c03 100644 --- a/libgm/payload/sendMessage.go +++ b/libgm/payload/sendMessage.go @@ -9,6 +9,7 @@ import ( "go.mau.fi/mautrix-gmessages/libgm/crypto" "go.mau.fi/mautrix-gmessages/libgm/pblite" "go.mau.fi/mautrix-gmessages/libgm/routes" + "go.mau.fi/mautrix-gmessages/libgm/util" ) type SendMessageBuilder struct { @@ -32,7 +33,7 @@ func NewSendMessageBuilder(tachyonAuthToken []byte, pairedDevice *binary.Device, MessageAuth: &binary.SendMessageAuth{ RequestID: requestId, TachyonAuthToken: tachyonAuthToken, - ConfigVersion: ConfigMessage, + ConfigVersion: util.ConfigMessage, }, EmptyArr: &binary.EmptyArr{}, }, diff --git a/libgm/qr.go b/libgm/qr.go index 7a17e51..a1c5ebf 100644 --- a/libgm/qr.go +++ b/libgm/qr.go @@ -9,11 +9,11 @@ import ( "go.mau.fi/mautrix-gmessages/libgm/util" ) -func (p *Pairer) GenerateQRCodeData() (string, error) { +func (c *Client) GenerateQRCodeData(pairingKey []byte) (string, error) { urlData := &binary.URLData{ - PairingKey: p.pairingKey, - AESKey: p.client.authData.RequestCrypto.AESKey, - HMACKey: p.client.authData.RequestCrypto.HMACKey, + PairingKey: pairingKey, + AESKey: c.AuthData.RequestCrypto.AESKey, + HMACKey: c.AuthData.RequestCrypto.HMACKey, } encodedURLData, err := proto.Marshal(urlData) if err != nil { diff --git a/libgm/rpc.go b/libgm/rpc.go index ff6d1db..4766814 100644 --- a/libgm/rpc.go +++ b/libgm/rpc.go @@ -15,7 +15,6 @@ import ( "github.com/rs/zerolog" "go.mau.fi/mautrix-gmessages/libgm/events" - "go.mau.fi/mautrix-gmessages/libgm/payload" "go.mau.fi/mautrix-gmessages/libgm/pblite" "go.mau.fi/mautrix-gmessages/libgm/binary" @@ -51,8 +50,8 @@ func (r *RPC) ListenReceiveMessages() { receivePayload, err := pblite.Marshal(&binary.ReceiveMessagesRequest{ Auth: &binary.AuthMessage{ RequestID: listenReqID, - TachyonAuthToken: r.client.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: r.client.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, Unknown: &binary.ReceiveMessagesRequest_UnknownEmptyObject2{ Unknown: &binary.ReceiveMessagesRequest_UnknownEmptyObject1{}, @@ -91,7 +90,7 @@ func (r *RPC) ListenReceiveMessages() { } r.client.Logger.Debug().Int("statusCode", resp.StatusCode).Msg("Long polling opened") r.conn = resp.Body - if r.client.authData.Browser != nil { + if r.client.AuthData.Browser != nil { go func() { err := r.client.NotifyDittoActivity() if err != nil { diff --git a/libgm/session_handler.go b/libgm/session_handler.go index 429a601..54d3d1b 100644 --- a/libgm/session_handler.go +++ b/libgm/session_handler.go @@ -73,9 +73,9 @@ func (s *SessionHandler) sendMessage(actionType binary.ActionType, encryptedData func (s *SessionHandler) buildMessage(actionType binary.ActionType, encryptedData proto.Message) (string, []byte, binary.ActionType, error) { var requestID string - pairedDevice := s.client.authData.Mobile + pairedDevice := s.client.AuthData.Mobile sessionId := s.client.sessionHandler.sessionID - token := s.client.authData.TachyonAuthToken + token := s.client.AuthData.TachyonAuthToken routeInfo, ok := routes.Routes[actionType] if !ok { @@ -91,11 +91,11 @@ func (s *SessionHandler) buildMessage(actionType binary.ActionType, encryptedDat tmpMessage := payload.NewSendMessageBuilder(token, pairedDevice, requestID, sessionId).SetRoute(routeInfo.Action).SetSessionId(s.sessionID) if encryptedData != nil { - tmpMessage.SetEncryptedProtoMessage(encryptedData, s.client.authData.RequestCrypto) + tmpMessage.SetEncryptedProtoMessage(encryptedData, s.client.AuthData.RequestCrypto) } if routeInfo.UseTTL { - tmpMessage.SetTTL(s.client.authData.TachyonTTL) + tmpMessage.SetTTL(s.client.AuthData.TachyonTTL) } message, buildErr := tmpMessage.Build() @@ -142,14 +142,14 @@ func (s *SessionHandler) sendAckRequest() { for i, reqID := range dataToAck { ackMessages[i] = &binary.AckMessageData{ RequestID: reqID, - Device: s.client.authData.Browser, + Device: s.client.AuthData.Browser, } } ackMessagePayload := &binary.AckMessagePayload{ AuthData: &binary.AuthMessage{ RequestID: uuid.NewString(), - TachyonAuthToken: s.client.authData.TachyonAuthToken, - ConfigVersion: payload.ConfigMessage, + TachyonAuthToken: s.client.AuthData.TachyonAuthToken, + ConfigVersion: util.ConfigMessage, }, EmptyArr: &binary.EmptyArr{}, Acks: ackMessages, diff --git a/libgm/payload/config.go b/libgm/util/config.go similarity index 78% rename from libgm/payload/config.go rename to libgm/util/config.go index 58d70f1..74825eb 100644 --- a/libgm/payload/config.go +++ b/libgm/util/config.go @@ -1,8 +1,7 @@ -package payload +package util import ( "go.mau.fi/mautrix-gmessages/libgm/binary" - "go.mau.fi/mautrix-gmessages/libgm/util" ) var ConfigMessage = &binary.ConfigVersion{ @@ -14,7 +13,7 @@ var ConfigMessage = &binary.ConfigVersion{ } var Network = "Bugle" var BrowserDetailsMessage = &binary.BrowserDetails{ - UserAgent: util.UserAgent, + UserAgent: UserAgent, BrowserType: binary.BrowserTypes_OTHER, Os: "libgm", SomeBool: true, diff --git a/main.go b/main.go index 99dde37..7bba7f1 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ import ( "go.mau.fi/mautrix-gmessages/config" "go.mau.fi/mautrix-gmessages/database" "go.mau.fi/mautrix-gmessages/libgm/binary" - "go.mau.fi/mautrix-gmessages/libgm/payload" + "go.mau.fi/mautrix-gmessages/libgm/util" ) // Information to find out exactly which commit the bridge was built from. @@ -67,12 +67,12 @@ func (br *GMBridge) Init() { br.CommandProcessor = commands.NewProcessor(&br.Bridge) br.RegisterCommands() - payload.BrowserDetailsMessage.Os = br.Config.GoogleMessages.OS + util.BrowserDetailsMessage.Os = br.Config.GoogleMessages.OS browserVal, ok := binary.BrowserTypes_value[br.Config.GoogleMessages.Browser] if !ok { br.ZLog.Error().Str("browser_value", br.Config.GoogleMessages.Browser).Msg("Invalid browser value") } else { - payload.BrowserDetailsMessage.BrowserType = binary.BrowserTypes(browserVal) + util.BrowserDetailsMessage.BrowserType = binary.BrowserTypes(browserVal) } Segment.log = br.ZLog.With().Str("component", "segment").Logger() diff --git a/user.go b/user.go index 6a49ed8..b77ed1f 100644 --- a/user.go +++ b/user.go @@ -71,10 +71,9 @@ type User struct { batteryLow bool mobileData bool - DoublePuppetIntent *appservice.IntentAPI + pairSuccessChan chan struct{} - hackyLoginCommand *WrappedCommandEvent - hackyLoginCommandPrevEvent id.EventID + DoublePuppetIntent *appservice.IntentAPI } func (br *GMBridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User { @@ -389,12 +388,11 @@ func (user *User) SetManagementRoom(roomID id.RoomID) { } var ErrAlreadyLoggedIn = errors.New("already logged in") +var ErrLoginInProgress = errors.New("login already in progress") +var ErrLoginTimeout = errors.New("login timed out") -func (user *User) createClient() { - if user.Session == nil { - user.Session = libgm.NewAuthData() - } - user.Client = libgm.NewClient(user.Session, user.zlog.With().Str("component", "libgm").Logger()) +func (user *User) createClient(sess *libgm.AuthData) { + user.Client = libgm.NewClient(sess, user.zlog.With().Str("component", "libgm").Logger()) user.Client.SetEventHandler(user.syncHandleEvent) } @@ -402,20 +400,66 @@ func (user *User) syncHandleEvent(ev any) { go user.HandleEvent(ev) } -func (user *User) Login(ctx context.Context) (<-chan string, error) { +type qrChannelItem struct { + success bool + qr string + err error +} + +func (user *User) Login(ctx context.Context, maxAttempts int) (<-chan qrChannelItem, error) { user.connLock.Lock() defer user.connLock.Unlock() if user.Session != nil { return nil, ErrAlreadyLoggedIn } else if user.Client != nil { user.unlockedDeleteConnection() + } else if user.pairSuccessChan != nil { + return nil, ErrLoginInProgress } - user.createClient() - err := user.Client.Connect() + pairSuccessChan := make(chan struct{}) + user.pairSuccessChan = pairSuccessChan + user.createClient(libgm.NewAuthData()) + qr, err := user.Client.StartLogin() if err != nil { + user.DeleteConnection() + user.pairSuccessChan = nil return nil, fmt.Errorf("failed to connect to Google Messages: %w", err) } - return nil, nil + ch := make(chan qrChannelItem, maxAttempts+2) + ch <- qrChannelItem{qr: qr} + go func() { + ticker := time.NewTicker(30 * time.Second) + success := false + defer func() { + ticker.Stop() + if !success { + user.zlog.Debug().Msg("Deleting connection as login wasn't successful") + user.DeleteConnection() + } + user.pairSuccessChan = nil + close(ch) + }() + for ; maxAttempts > 0; maxAttempts-- { + select { + case <-ctx.Done(): + user.zlog.Debug().Err(ctx.Err()).Msg("Login context cancelled") + return + case <-ticker.C: + qr, err := user.Client.RefreshPhoneRelay() + if err != nil { + ch <- qrChannelItem{err: fmt.Errorf("failed to refresh QR code: %w", err)} + return + } + ch <- qrChannelItem{qr: qr} + case <-pairSuccessChan: + ch <- qrChannelItem{success: true} + success = true + return + } + } + ch <- qrChannelItem{err: ErrLoginTimeout} + }() + return ch, nil } func (user *User) Connect() bool { @@ -431,7 +475,7 @@ func (user *User) Connect() bool { } user.zlog.Debug().Msg("Connecting to Google Messages") user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting, Error: GMConnecting}) - user.createClient() + user.createClient(user.Session) err := user.Client.Connect() if err != nil { user.zlog.Err(err).Msg("Error connecting to Google Messages") @@ -512,11 +556,6 @@ func (user *User) sendMarkdownBridgeAlert(formatString string, args ...interface func (user *User) HandleEvent(event interface{}) { switch v := event.(type) { - case *events.QR: - // This shouldn't be here - if user.hackyLoginCommand != nil { - user.hackyLoginCommandPrevEvent = user.sendQR(user.hackyLoginCommand, v.URL, user.hackyLoginCommandPrevEvent) - } case *events.ListenFatalError: user.Logout(status.BridgeState{ StateEvent: status.StateBadCredentials, @@ -534,15 +573,17 @@ func (user *User) HandleEvent(event interface{}) { user.longPollingError = nil user.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) case *events.PairSuccessful: - user.hackyLoginCommand = nil - user.hackyLoginCommandPrevEvent = "" - user.tryAutomaticDoublePuppeting() + user.Session = user.Client.AuthData user.Phone = v.GetMobile().GetSourceID() + user.tryAutomaticDoublePuppeting() user.addToPhoneMap() err := user.Update(context.TODO()) if err != nil { user.zlog.Err(err).Msg("Failed to update session in database") } + if ch := user.pairSuccessChan; ch != nil { + close(ch) + } case *binary.RevokePairData: user.zlog.Info().Any("revoked_device", v.GetRevokedDevice()).Msg("Got pair revoked event") user.Logout(status.BridgeState{