| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- // Copyright (c) 2021 Tulir Asokan
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this
- // file, You can obtain one at http://mozilla.org/MPL/2.0/.
- package whatsmeow
- import (
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "encoding/binary"
- "fmt"
- "time"
- "go.mau.fi/libsignal/ecc"
- "go.mau.fi/libsignal/groups"
- "go.mau.fi/libsignal/keys/prekey"
- "go.mau.fi/libsignal/protocol"
- "google.golang.org/protobuf/proto"
- waBinary "go.mau.fi/whatsmeow/binary"
- "go.mau.fi/whatsmeow/proto/waCommon"
- "go.mau.fi/whatsmeow/proto/waConsumerApplication"
- "go.mau.fi/whatsmeow/proto/waE2E"
- "go.mau.fi/whatsmeow/proto/waMsgApplication"
- "go.mau.fi/whatsmeow/proto/waMsgTransport"
- "go.mau.fi/whatsmeow/types"
- "go.mau.fi/whatsmeow/types/events"
- )
- // Number of sent messages to cache in memory for handling retry receipts.
- const recentMessagesSize = 256
- type recentMessageKey struct {
- To types.JID
- ID types.MessageID
- }
- type RecentMessage struct {
- wa *waE2E.Message
- fb *waMsgApplication.MessageApplication
- }
- func (rm RecentMessage) IsEmpty() bool {
- return rm.wa == nil && rm.fb == nil
- }
- func (cli *Client) addRecentMessage(to types.JID, id types.MessageID, wa *waE2E.Message, fb *waMsgApplication.MessageApplication) {
- cli.recentMessagesLock.Lock()
- key := recentMessageKey{to, id}
- if cli.recentMessagesList[cli.recentMessagesPtr].ID != "" {
- delete(cli.recentMessagesMap, cli.recentMessagesList[cli.recentMessagesPtr])
- }
- cli.recentMessagesMap[key] = RecentMessage{wa: wa, fb: fb}
- cli.recentMessagesList[cli.recentMessagesPtr] = key
- cli.recentMessagesPtr++
- if cli.recentMessagesPtr >= len(cli.recentMessagesList) {
- cli.recentMessagesPtr = 0
- }
- cli.recentMessagesLock.Unlock()
- }
- func (cli *Client) getRecentMessage(to types.JID, id types.MessageID) RecentMessage {
- cli.recentMessagesLock.RLock()
- msg, _ := cli.recentMessagesMap[recentMessageKey{to, id}]
- cli.recentMessagesLock.RUnlock()
- return msg
- }
- func (cli *Client) getMessageForRetry(ctx context.Context, receipt *events.Receipt, messageID types.MessageID) (RecentMessage, error) {
- msg := cli.getRecentMessage(receipt.Chat, messageID)
- if msg.IsEmpty() {
- waMsg := cli.GetMessageForRetry(receipt.Sender, receipt.Chat, messageID)
- if waMsg == nil {
- return RecentMessage{}, fmt.Errorf("couldn't find message %s", messageID)
- } else {
- cli.Log.Debugf("Found message in GetMessageForRetry to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
- }
- msg = RecentMessage{wa: waMsg}
- } else {
- cli.Log.Debugf("Found message in local cache to accept retry receipt for %s/%s from %s", receipt.Chat, messageID, receipt.Sender)
- }
- return msg, nil
- }
- const recreateSessionTimeout = 1 * time.Hour
- func (cli *Client) shouldRecreateSession(ctx context.Context, retryCount int, jid types.JID) (reason string, recreate bool) {
- cli.sessionRecreateHistoryLock.Lock()
- defer cli.sessionRecreateHistoryLock.Unlock()
- if contains, err := cli.Store.ContainsSession(ctx, jid.SignalAddress()); err != nil {
- return "", false
- } else if !contains {
- cli.sessionRecreateHistory[jid] = time.Now()
- return "we don't have a Signal session with them", true
- } else if retryCount < 2 {
- return "", false
- }
- prevTime, ok := cli.sessionRecreateHistory[jid]
- if !ok || prevTime.Add(recreateSessionTimeout).Before(time.Now()) {
- cli.sessionRecreateHistory[jid] = time.Now()
- return "retry count > 1 and over an hour since last recreation", true
- }
- return "", false
- }
- type incomingRetryKey struct {
- jid types.JID
- messageID types.MessageID
- }
- // handleRetryReceipt handles an incoming retry receipt for an outgoing message.
- func (cli *Client) handleRetryReceipt(ctx context.Context, receipt *events.Receipt, node *waBinary.Node) error {
- retryChild, ok := node.GetOptionalChildByTag("retry")
- if !ok {
- return &ElementMissingError{Tag: "retry", In: "retry receipt"}
- }
- ag := retryChild.AttrGetter()
- messageID := ag.String("id")
- timestamp := ag.UnixTime("t")
- retryCount := ag.Int("count")
- if !ag.OK() {
- return ag.Error()
- }
- msg, err := cli.getMessageForRetry(ctx, receipt, messageID)
- if err != nil {
- return err
- }
- var fbConsumerMsg *waConsumerApplication.ConsumerApplication
- if msg.fb != nil {
- subProto, ok := msg.fb.GetPayload().GetSubProtocol().GetSubProtocol().(*waMsgApplication.MessageApplication_SubProtocolPayload_ConsumerMessage)
- if ok {
- fbConsumerMsg, err = subProto.Decode()
- if err != nil {
- return fmt.Errorf("failed to decode consumer message for retry: %w", err)
- }
- }
- }
- retryKey := incomingRetryKey{receipt.Sender, messageID}
- cli.incomingRetryRequestCounterLock.Lock()
- cli.incomingRetryRequestCounter[retryKey]++
- internalCounter := cli.incomingRetryRequestCounter[retryKey]
- cli.incomingRetryRequestCounterLock.Unlock()
- if internalCounter >= 10 {
- cli.Log.Warnf("Dropping retry request from %s for %s: internal retry counter is %d", messageID, receipt.Sender, internalCounter)
- return nil
- }
- var fbSKDM *waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage
- var fbDSM *waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage
- if receipt.IsGroup {
- builder := groups.NewGroupSessionBuilder(cli.Store, pbSerializer)
- senderKeyName := protocol.NewSenderKeyName(receipt.Chat.String(), cli.getOwnLID().SignalAddress())
- signalSKDMessage, err := builder.Create(ctx, senderKeyName)
- if err != nil {
- cli.Log.Warnf("Failed to create sender key distribution message to include in retry of %s in %s to %s: %v", messageID, receipt.Chat, receipt.Sender, err)
- } else if msg.wa != nil {
- msg.wa.SenderKeyDistributionMessage = &waE2E.SenderKeyDistributionMessage{
- GroupID: proto.String(receipt.Chat.String()),
- AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
- }
- } else {
- fbSKDM = &waMsgTransport.MessageTransport_Protocol_Ancillary_SenderKeyDistributionMessage{
- GroupID: proto.String(receipt.Chat.String()),
- AxolotlSenderKeyDistributionMessage: signalSKDMessage.Serialize(),
- }
- }
- } else if receipt.IsFromMe {
- if msg.wa != nil {
- msg.wa = &waE2E.Message{
- DeviceSentMessage: &waE2E.DeviceSentMessage{
- DestinationJID: proto.String(receipt.Chat.String()),
- Message: msg.wa,
- },
- }
- } else {
- fbDSM = &waMsgTransport.MessageTransport_Protocol_Integral_DeviceSentMessage{
- DestinationJID: proto.String(receipt.Chat.String()),
- }
- }
- }
- // TODO pre-retry callback for fb
- if cli.PreRetryCallback != nil && !cli.PreRetryCallback(receipt, messageID, retryCount, msg.wa) {
- cli.Log.Debugf("Cancelled retry receipt in PreRetryCallback")
- return nil
- }
- var plaintext, frankingTag []byte
- if msg.wa != nil {
- plaintext, err = proto.Marshal(msg.wa)
- if err != nil {
- return fmt.Errorf("failed to marshal message: %w", err)
- }
- } else {
- plaintext, err = proto.Marshal(msg.fb)
- if err != nil {
- return fmt.Errorf("failed to marshal consumer message: %w", err)
- }
- frankingHash := hmac.New(sha256.New, msg.fb.GetMetadata().GetFrankingKey())
- frankingHash.Write(plaintext)
- frankingTag = frankingHash.Sum(nil)
- }
- _, hasKeys := node.GetOptionalChildByTag("keys")
- var bundle *prekey.Bundle
- if hasKeys {
- bundle, err = nodeToPreKeyBundle(uint32(receipt.Sender.Device), *node)
- if err != nil {
- return fmt.Errorf("failed to read prekey bundle in retry receipt: %w", err)
- }
- } else if reason, recreate := cli.shouldRecreateSession(ctx, retryCount, receipt.Sender); recreate {
- cli.Log.Debugf("Fetching prekeys for %s for handling retry receipt with no prekey bundle because %s", receipt.Sender, reason)
- var keys map[types.JID]preKeyResp
- keys, err = cli.fetchPreKeys(ctx, []types.JID{receipt.Sender})
- if err != nil {
- return err
- }
- bundle, err = keys[receipt.Sender].bundle, keys[receipt.Sender].err
- if err != nil {
- return fmt.Errorf("failed to fetch prekeys: %w", err)
- } else if bundle == nil {
- return fmt.Errorf("didn't get prekey bundle for %s (response size: %d)", receipt.Sender, len(keys))
- }
- }
- encAttrs := waBinary.Attrs{}
- var msgAttrs messageAttrs
- if msg.wa != nil {
- msgAttrs.MediaType = getMediaTypeFromMessage(msg.wa)
- msgAttrs.Type = getTypeFromMessage(msg.wa)
- } else if fbConsumerMsg != nil {
- msgAttrs = getAttrsFromFBMessage(fbConsumerMsg)
- } else {
- msgAttrs.Type = "text"
- }
- if msgAttrs.MediaType != "" {
- encAttrs["mediatype"] = msgAttrs.MediaType
- }
- var encrypted *waBinary.Node
- var includeDeviceIdentity bool
- if msg.wa != nil {
- encryptionIdentity := receipt.Sender
- if receipt.Sender.Server == types.DefaultUserServer {
- lidForPN, err := cli.Store.LIDs.GetLIDForPN(ctx, receipt.Sender)
- if err != nil {
- cli.Log.Warnf("Failed to get LID for %s: %v", receipt.Sender, err)
- } else if !lidForPN.IsEmpty() {
- cli.migrateSessionStore(ctx, receipt.Sender, lidForPN)
- encryptionIdentity = lidForPN
- }
- }
- encrypted, includeDeviceIdentity, err = cli.encryptMessageForDevice(ctx, plaintext, encryptionIdentity, bundle, encAttrs, nil)
- } else {
- encrypted, err = cli.encryptMessageForDeviceV3(ctx, &waMsgTransport.MessageTransport_Payload{
- ApplicationPayload: &waCommon.SubProtocol{
- Payload: plaintext,
- Version: proto.Int32(FBMessageApplicationVersion),
- },
- FutureProof: waCommon.FutureProofBehavior_PLACEHOLDER.Enum(),
- }, fbSKDM, fbDSM, receipt.Sender, bundle, encAttrs)
- }
- if err != nil {
- return fmt.Errorf("failed to encrypt message for retry: %w", err)
- }
- encrypted.Attrs["count"] = retryCount
- attrs := waBinary.Attrs{
- "to": node.Attrs["from"],
- "type": msgAttrs.Type,
- "id": messageID,
- "t": timestamp.Unix(),
- }
- if !receipt.IsGroup {
- attrs["device_fanout"] = false
- }
- if participant, ok := node.Attrs["participant"]; ok {
- attrs["participant"] = participant
- }
- if recipient, ok := node.Attrs["recipient"]; ok {
- attrs["recipient"] = recipient
- }
- if edit, ok := node.Attrs["edit"]; ok {
- attrs["edit"] = edit
- }
- var content []waBinary.Node
- if msg.wa != nil {
- content = cli.getMessageContent(
- *encrypted, msg.wa, attrs, includeDeviceIdentity, nodeExtraParams{},
- )
- } else {
- content = []waBinary.Node{
- *encrypted,
- {Tag: "franking", Content: []waBinary.Node{{Tag: "franking_tag", Content: frankingTag}}},
- }
- }
- err = cli.sendNode(ctx, waBinary.Node{
- Tag: "message",
- Attrs: attrs,
- Content: content,
- })
- if err != nil {
- return fmt.Errorf("failed to send retry message: %w", err)
- }
- cli.Log.Debugf("Sent retry #%d for %s/%s to %s", retryCount, receipt.Chat, messageID, receipt.Sender)
- return nil
- }
- func (cli *Client) cancelDelayedRequestFromPhone(msgID types.MessageID) {
- if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
- return
- }
- cli.pendingPhoneRerequestsLock.RLock()
- cancelPendingRequest, ok := cli.pendingPhoneRerequests[msgID]
- if ok {
- cancelPendingRequest()
- }
- cli.pendingPhoneRerequestsLock.RUnlock()
- }
- // RequestFromPhoneDelay specifies how long to wait for the sender to resend the message before requesting from your phone.
- // This is only used if Client.AutomaticMessageRerequestFromPhone is true.
- var RequestFromPhoneDelay = 5 * time.Second
- func (cli *Client) delayedRequestMessageFromPhone(info *types.MessageInfo) {
- if !cli.AutomaticMessageRerequestFromPhone || cli.MessengerConfig != nil {
- return
- }
- cli.pendingPhoneRerequestsLock.Lock()
- _, alreadyRequesting := cli.pendingPhoneRerequests[info.ID]
- if alreadyRequesting {
- cli.pendingPhoneRerequestsLock.Unlock()
- return
- }
- ctx, cancel := context.WithCancel(cli.BackgroundEventCtx)
- defer cancel()
- cli.pendingPhoneRerequests[info.ID] = cancel
- cli.pendingPhoneRerequestsLock.Unlock()
- defer func() {
- cli.pendingPhoneRerequestsLock.Lock()
- delete(cli.pendingPhoneRerequests, info.ID)
- cli.pendingPhoneRerequestsLock.Unlock()
- }()
- select {
- case <-time.After(RequestFromPhoneDelay):
- case <-ctx.Done():
- cli.Log.Debugf("Cancelled delayed request for message %s from phone", info.ID)
- return
- }
- cli.immediateRequestMessageFromPhone(ctx, info)
- }
- func (cli *Client) immediateRequestMessageFromPhone(ctx context.Context, info *types.MessageInfo) {
- _, err := cli.SendMessage(
- ctx,
- cli.getOwnID().ToNonAD(),
- cli.BuildUnavailableMessageRequest(info.Chat, info.Sender, info.ID),
- SendRequestExtra{Peer: true},
- )
- if err != nil {
- cli.Log.Warnf("Failed to send request for unavailable message %s to phone: %v", info.ID, err)
- } else {
- cli.Log.Debugf("Requested message %s from phone", info.ID)
- }
- return
- }
- func (cli *Client) clearDelayedMessageRequests() {
- cli.pendingPhoneRerequestsLock.Lock()
- defer cli.pendingPhoneRerequestsLock.Unlock()
- for _, cancel := range cli.pendingPhoneRerequests {
- cancel()
- }
- }
- // sendRetryReceipt sends a retry receipt for an incoming message.
- func (cli *Client) sendRetryReceipt(ctx context.Context, node *waBinary.Node, info *types.MessageInfo, forceIncludeIdentity bool) {
- id, _ := node.Attrs["id"].(string)
- children := node.GetChildren()
- var retryCountInMsg int
- if len(children) == 1 && children[0].Tag == "enc" {
- retryCountInMsg = children[0].AttrGetter().OptionalInt("count")
- }
- cli.messageRetriesLock.Lock()
- cli.messageRetries[id]++
- retryCount := cli.messageRetries[id]
- // In case the message is a retry response, and we restarted in between, find the count from the message
- if retryCount == 1 && retryCountInMsg > 0 {
- retryCount = retryCountInMsg + 1
- cli.messageRetries[id] = retryCount
- }
- cli.messageRetriesLock.Unlock()
- if retryCount >= 5 {
- cli.Log.Warnf("Not sending any more retry receipts for %s", id)
- return
- }
- if retryCount == 1 {
- if cli.SynchronousAck {
- cli.immediateRequestMessageFromPhone(ctx, info)
- } else {
- go cli.delayedRequestMessageFromPhone(info)
- }
- }
- var registrationIDBytes [4]byte
- binary.BigEndian.PutUint32(registrationIDBytes[:], cli.Store.RegistrationID)
- attrs := buildBaseReceipt(info.ID, node)
- attrs["type"] = "retry"
- if info.Type == "peer_msg" && info.IsFromMe {
- attrs["category"] = "peer"
- }
- payload := waBinary.Node{
- Tag: "receipt",
- Attrs: attrs,
- Content: []waBinary.Node{
- {Tag: "retry", Attrs: waBinary.Attrs{
- "count": retryCount,
- "id": id,
- "t": node.Attrs["t"],
- "v": 1,
- }},
- {Tag: "registration", Content: registrationIDBytes[:]},
- },
- }
- if retryCount > 1 || forceIncludeIdentity {
- if key, err := cli.Store.PreKeys.GenOnePreKey(ctx); err != nil {
- cli.Log.Errorf("Failed to get prekey for retry receipt: %v", err)
- } else if deviceIdentity, err := proto.Marshal(cli.Store.Account); err != nil {
- cli.Log.Errorf("Failed to marshal account info: %v", err)
- return
- } else {
- payload.Content = append(payload.GetChildren(), waBinary.Node{
- Tag: "keys",
- Content: []waBinary.Node{
- {Tag: "type", Content: []byte{ecc.DjbType}},
- {Tag: "identity", Content: cli.Store.IdentityKey.Pub[:]},
- preKeyToNode(key),
- preKeyToNode(cli.Store.SignedPreKey),
- {Tag: "device-identity", Content: deviceIdentity},
- },
- })
- }
- }
- err := cli.sendNode(ctx, payload)
- if err != nil {
- cli.Log.Errorf("Failed to send retry receipt for %s: %v", id, err)
- }
- }
|