gmessages/libgm/session_handler.go

313 lines
8 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 (
"encoding/base64"
2023-06-30 09:54:08 +00:00
"fmt"
2023-07-15 12:57:07 +00:00
"sync"
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
"golang.org/x/exp/slices"
"google.golang.org/protobuf/proto"
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"
)
2023-07-15 22:45:57 +00:00
type SessionHandler struct {
client *Client
2023-06-30 09:54:08 +00:00
responseWaiters map[string]chan<- *IncomingRPCMessage
2023-07-15 22:45:57 +00:00
responseWaitersLock sync.Mutex
2023-06-30 09:54:08 +00:00
2023-07-15 12:57:07 +00:00
ackMapLock sync.Mutex
ackMap []string
ackTicker *time.Ticker
2023-06-30 09:54:08 +00:00
2023-07-15 22:45:57 +00:00
sessionID string
2023-06-30 09:54:08 +00:00
}
2023-07-15 22:45:57 +00:00
func (s *SessionHandler) ResetSessionID() {
2023-07-16 10:23:44 +00:00
s.sessionID = uuid.NewString()
2023-06-30 09:54:08 +00:00
}
2023-07-18 21:59:51 +00:00
func (s *SessionHandler) sendMessageNoResponse(params SendMessageParams) error {
_, payload, err := s.buildMessage(params)
2023-07-15 22:45:57 +00:00
if err != nil {
return err
}
url := util.SendMessageURL
if s.client.AuthData.Cookies != nil {
url = util.SendMessageURLGoogle
}
_, err = typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
2023-07-15 22:45:57 +00:00
return err
2023-06-30 09:54:08 +00:00
}
2023-07-18 21:59:51 +00:00
func (s *SessionHandler) sendAsyncMessage(params SendMessageParams) (<-chan *IncomingRPCMessage, error) {
requestID, payload, err := s.buildMessage(params)
2023-07-18 00:08:01 +00:00
if err != nil {
return nil, err
2023-06-30 09:54:08 +00:00
}
2023-07-15 22:45:57 +00:00
ch := s.waitResponse(requestID)
url := util.SendMessageURL
if s.client.AuthData.Cookies != nil {
url = util.SendMessageURLGoogle
}
_, err = typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
if err != nil {
2023-07-15 22:45:57 +00:00
s.cancelResponse(requestID, ch)
return nil, err
2023-07-15 22:45:57 +00:00
}
return ch, nil
}
2023-07-17 23:11:43 +00:00
func typedResponse[T proto.Message](resp *IncomingRPCMessage, err error) (casted T, retErr error) {
if err != nil {
retErr = err
return
}
var ok bool
casted, ok = resp.DecryptedMessage.(T)
if !ok {
retErr = fmt.Errorf("unexpected response type %T for %s, expected %T", resp.DecryptedMessage, resp.ResponseID, casted)
2023-07-17 23:11:43 +00:00
}
return
}
func (s *SessionHandler) waitResponse(requestID string) chan *IncomingRPCMessage {
ch := make(chan *IncomingRPCMessage, 1)
s.responseWaitersLock.Lock()
s.responseWaiters[requestID] = ch
s.responseWaitersLock.Unlock()
return ch
}
func (s *SessionHandler) cancelResponse(requestID string, ch chan *IncomingRPCMessage) {
s.responseWaitersLock.Lock()
close(ch)
delete(s.responseWaiters, requestID)
s.responseWaitersLock.Unlock()
}
func (s *SessionHandler) receiveResponse(msg *IncomingRPCMessage) bool {
if msg.Message == nil {
return false
}
if s.client.AuthData.Cookies != nil {
switch msg.Message.Action {
case gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_INIT, gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_FINISHED:
default:
// Very hacky way to ignore weird messages that come before real responses
// TODO figure out how to properly handle these
if msg.Message.UnencryptedData != nil && msg.Message.EncryptedData == nil {
return false
}
}
}
requestID := msg.Message.SessionID
s.responseWaitersLock.Lock()
ch, ok := s.responseWaiters[requestID]
if !ok {
s.responseWaitersLock.Unlock()
return false
}
delete(s.responseWaiters, requestID)
s.responseWaitersLock.Unlock()
evt := s.client.Logger.Trace().
Str("request_id", requestID)
if evt.Enabled() {
if msg.DecryptedData != nil {
evt.Str("data", base64.StdEncoding.EncodeToString(msg.DecryptedData))
}
if msg.DecryptedMessage != nil {
evt.Str("proto_name", string(msg.DecryptedMessage.ProtoReflect().Descriptor().FullName()))
}
}
evt.Msg("Received response")
ch <- msg
return true
}
2023-07-18 21:59:51 +00:00
func (s *SessionHandler) sendMessageWithParams(params SendMessageParams) (*IncomingRPCMessage, error) {
ch, err := s.sendAsyncMessage(params)
2023-07-15 22:45:57 +00:00
if err != nil {
return nil, err
2023-06-30 09:54:08 +00:00
}
2023-07-15 22:45:57 +00:00
if params.NoPingOnTimeout {
return <-ch, nil
}
select {
case resp := <-ch:
return resp, nil
case <-time.After(5 * time.Second):
// Notify the pinger in order to trigger an event that the phone isn't responding
select {
case s.client.pingShortCircuit <- struct{}{}:
default:
}
}
// TODO hard timeout?
2023-07-15 22:45:57 +00:00
return <-ch, nil
}
2023-06-30 09:54:08 +00:00
2023-07-18 21:59:51 +00:00
func (s *SessionHandler) sendMessage(actionType gmproto.ActionType, encryptedData proto.Message) (*IncomingRPCMessage, error) {
return s.sendMessageWithParams(SendMessageParams{
Action: actionType,
Data: encryptedData,
})
}
type SendMessageParams struct {
Action gmproto.ActionType
Data proto.Message
RequestID string
OmitTTL bool
CustomTTL int64
DontEncrypt bool
MessageType gmproto.MessageType
2024-02-23 17:26:49 +00:00
NoPingOnTimeout bool
2023-07-18 21:59:51 +00:00
}
func (s *SessionHandler) buildMessage(params SendMessageParams) (string, proto.Message, error) {
2023-07-18 00:08:01 +00:00
var err error
sessionID := s.client.sessionHandler.sessionID
2023-06-30 09:54:08 +00:00
requestID := params.RequestID
if requestID == "" {
2023-07-16 10:23:44 +00:00
requestID = uuid.NewString()
2023-06-30 09:54:08 +00:00
}
2023-07-18 21:59:51 +00:00
if params.MessageType == 0 {
params.MessageType = gmproto.MessageType_BUGLE_MESSAGE
}
2023-07-18 00:08:01 +00:00
message := &gmproto.OutgoingRPCMessage{
Mobile: s.client.AuthData.Mobile,
Data: &gmproto.OutgoingRPCMessage_Data{
RequestID: requestID,
2023-07-18 21:59:51 +00:00
BugleRoute: gmproto.BugleRoute_DataEvent,
2023-07-18 00:08:01 +00:00
MessageTypeData: &gmproto.OutgoingRPCMessage_Data_Type{
EmptyArr: &gmproto.EmptyArr{},
2023-07-18 21:59:51 +00:00
MessageType: params.MessageType,
2023-07-18 00:08:01 +00:00
},
},
Auth: &gmproto.OutgoingRPCMessage_Auth{
RequestID: requestID,
TachyonAuthToken: s.client.AuthData.TachyonAuthToken,
ConfigVersion: util.ConfigMessage,
},
2024-02-23 17:26:49 +00:00
DestRegistrationIDs: []string{},
}
if s.client.AuthData != nil && s.client.AuthData.DestRegID != uuid.Nil {
message.DestRegistrationIDs = append(message.DestRegistrationIDs, s.client.AuthData.DestRegID.String())
2023-06-30 09:54:08 +00:00
}
if params.CustomTTL != 0 {
message.TTL = params.CustomTTL
} else if !params.OmitTTL {
2023-07-18 21:59:51 +00:00
message.TTL = s.client.AuthData.TachyonTTL
}
var encryptedData, unencryptedData []byte
2023-07-18 21:59:51 +00:00
if params.Data != nil {
2023-07-18 00:08:01 +00:00
var serializedData []byte
2023-07-18 21:59:51 +00:00
serializedData, err = proto.Marshal(params.Data)
2023-07-18 00:08:01 +00:00
if err != nil {
return "", nil, err
}
if params.DontEncrypt {
unencryptedData = serializedData
} else {
encryptedData, err = s.client.AuthData.RequestCrypto.Encrypt(serializedData)
if err != nil {
return "", nil, err
}
2023-07-18 00:08:01 +00:00
}
2023-06-30 09:54:08 +00:00
}
2023-07-18 00:08:01 +00:00
message.Data.MessageData, err = proto.Marshal(&gmproto.OutgoingRPCData{
RequestID: requestID,
Action: params.Action,
UnencryptedProtoData: unencryptedData,
EncryptedProtoData: encryptedData,
SessionID: sessionID,
2023-07-18 00:08:01 +00:00
})
if err != nil {
return "", nil, err
2023-06-30 09:54:08 +00:00
}
return requestID, message, err
2023-06-30 09:54:08 +00:00
}
2023-07-15 22:45:57 +00:00
func (s *SessionHandler) queueMessageAck(messageID string) {
2023-07-15 12:57:07 +00:00
s.ackMapLock.Lock()
defer s.ackMapLock.Unlock()
2023-07-15 22:45:57 +00:00
if !slices.Contains(s.ackMap, messageID) {
s.ackMap = append(s.ackMap, messageID)
s.client.Logger.Trace().Any("message_id", messageID).Msg("Queued ack for message")
} else {
s.client.Logger.Trace().Any("message_id", messageID).Msg("Ack for message was already queued")
2023-06-30 09:54:08 +00:00
}
}
func (s *SessionHandler) startAckInterval() {
if s.ackTicker != nil {
s.ackTicker.Stop()
}
ticker := time.NewTicker(5 * time.Second)
s.ackTicker = ticker
go func() {
for range ticker.C {
s.sendAckRequest()
}
}()
}
func (s *SessionHandler) sendAckRequest() {
2023-07-15 12:57:07 +00:00
s.ackMapLock.Lock()
dataToAck := s.ackMap
s.ackMap = nil
2023-07-15 12:57:07 +00:00
s.ackMapLock.Unlock()
if len(dataToAck) == 0 {
return
}
2023-07-17 23:57:20 +00:00
ackMessages := make([]*gmproto.AckMessageRequest_Message, len(dataToAck))
for i, reqID := range dataToAck {
2023-07-17 23:57:20 +00:00
ackMessages[i] = &gmproto.AckMessageRequest_Message{
RequestID: reqID,
2023-07-16 12:55:30 +00:00
Device: s.client.AuthData.Browser,
}
}
payload := &gmproto.AckMessageRequest{
2023-07-17 13:51:31 +00:00
AuthData: &gmproto.AuthMessage{
2023-07-16 10:23:44 +00:00
RequestID: uuid.NewString(),
2023-07-16 12:55:30 +00:00
TachyonAuthToken: s.client.AuthData.TachyonAuthToken,
2024-02-23 12:53:19 +00:00
Network: s.client.AuthData.AuthNetwork(),
2023-07-16 12:55:30 +00:00
ConfigVersion: util.ConfigMessage,
2023-06-30 09:54:08 +00:00
},
2023-07-17 13:51:31 +00:00
EmptyArr: &gmproto.EmptyArr{},
Acks: ackMessages,
2023-06-30 09:54:08 +00:00
}
url := util.AckMessagesURL
if s.client.AuthData.Cookies != nil {
url = util.AckMessagesURLGoogle
}
_, err := typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
2023-06-30 09:54:08 +00:00
if err != nil {
// TODO retry?
s.client.Logger.Err(err).Strs("message_ids", dataToAck).Msg("Failed to send acks")
} else {
s.client.Logger.Debug().Strs("message_ids", dataToAck).Msg("Sent acks")
2023-06-30 09:54:08 +00:00
}
}