From 362e918cb66c2ba55da1a7a76cab735af5e302f2 Mon Sep 17 00:00:00 2001 From: zhaoYangguang <1163765691@qq.com> Date: Thu, 9 Dec 2021 17:27:06 +0800 Subject: [PATCH] update double puppet related logic, fix the issue user will rejion matrix room even if user has logout skype(bridge) --- commands.go | 7 ++++ config/bridge.go | 9 +++-- config/config.go | 8 ++++ crypto.go | 79 ++++++++++++++++-------------------- custompuppet.go | 99 +++++++++++++++++++++++++++++++-------------- example-config.yaml | 9 ++++- main.go | 2 +- portal.go | 15 +++---- puppet.go | 3 ++ user.go | 34 ++++++++-------- 10 files changed, 159 insertions(+), 106 deletions(-) diff --git a/commands.go b/commands.go index 424f7b0..59671fe 100644 --- a/commands.go +++ b/commands.go @@ -262,6 +262,13 @@ func (handler *CommandHandler) CommandLogout(ce *CommandEvent) { Contacts: make(map[string]skype.Contact), Chats: make(map[string]skype.Conversation), } + puppet := handler.bridge.GetPuppetByJID(ce.User.JID) + if puppet.CustomMXID != "" { + err := puppet.SwitchCustomMXID("", "") + if err != nil { + ce.User.log.Warnln("Failed to logout-matrix while logging out of WhatsApp:", err) + } + } ce.Reply("Logged out successfully.") if ce.User.Conn.Refresh != nil { ce.User.Conn.Refresh <- -1 diff --git a/config/bridge.go b/config/bridge.go index 31b08ac..06e0029 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -2,11 +2,12 @@ package config import ( "bytes" - skype "github.com/kelaresg/go-skypeapi" "strconv" "strings" "text/template" + skype "github.com/kelaresg/go-skypeapi" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -41,11 +42,14 @@ type BridgeConfig struct { SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` - LoginSharedSecret string `yaml:"login_shared_secret"` InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` + DoublePuppetServerMap map[string]string `yaml:"double_puppet_server_map"` + DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"` + LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"` + WhatsappThumbnail bool `yaml:"whatsapp_thumbnail"` AllowUserInvite bool `yaml:"allow_user_invite"` @@ -99,7 +103,6 @@ func (bc *BridgeConfig) setDefaults() { bc.SyncChatMaxAge = 259200 bc.SyncWithCustomPuppets = true - bc.LoginSharedSecret = "" bc.InviteOwnPuppetForBackfilling = true bc.PrivateChatPortalMeta = false diff --git a/config/config.go b/config/config.go index 36ea561..8e11bc1 100644 --- a/config/config.go +++ b/config/config.go @@ -18,6 +18,8 @@ package config import ( "io/ioutil" + + "maunium.net/go/mautrix/id" "maunium.net/go/mautrix/patch" "gopkg.in/yaml.v2" @@ -68,6 +70,12 @@ type Config struct { Logging appservice.LogConfig `yaml:"logging"` } +func (config *Config) CanAutoDoublePuppet(userID id.UserID) bool { + _, homeserver, _ := userID.Parse() + _, hasSecret := config.Bridge.LoginSharedSecretMap[homeserver] + return hasSecret +} + func (config *Config) setDefaults() { config.AppService.Database.MaxOpenConns = 20 config.AppService.Database.MaxIdleConns = 2 diff --git a/crypto.go b/crypto.go index 14ecc97..4718d74 100644 --- a/crypto.go +++ b/crypto.go @@ -11,18 +11,18 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +//go:build cgo && !nocrypto // +build cgo,!nocrypto package main import ( - "crypto/hmac" - "crypto/sha512" - "encoding/hex" "fmt" + "runtime/debug" "time" - "github.com/pkg/errors" + "github.com/lib/pq" + "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -48,13 +48,14 @@ type CryptoHelper struct { baseLog maulogger.Logger } +func init() { + crypto.PostgresArrayWrapper = pq.Array +} + func NewCryptoHelper(bridge *Bridge) Crypto { if !bridge.Config.Bridge.Encryption.Allow { bridge.Log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config") return nil - } else if bridge.Config.Bridge.LoginSharedSecret == "" { - bridge.Log.Warnln("End-to-bridge encryption enabled, but login_shared_secret not set") - return nil } baseLog := bridge.Log.Sub("Crypto") return &CryptoHelper{ @@ -101,7 +102,8 @@ func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info ev return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"} } user := helper.bridge.GetUserByMXID(device.UserID) - if !user.IsInPortal(portal.Key) { + // FIXME reimplement IsInPortal + if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ { helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID) return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"} } @@ -128,14 +130,15 @@ func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { if err != nil { return nil, fmt.Errorf("failed to get supported login flows: %w", err) } - if !flows.HasFlow(mautrix.AuthTypeHalfyAppservice) { + flow := flows.FirstFlowOfType(mautrix.AuthTypeAppservice, mautrix.AuthTypeHalfyAppservice) + if flow == nil { return nil, fmt.Errorf("homeserver does not support appservice login") } // We set the API token to the AS token here to authenticate the appservice login // It'll get overridden after the login client.AccessToken = helper.bridge.AS.Registration.AppToken resp, err := client.Login(&mautrix.ReqLogin{ - Type: mautrix.AuthTypeHalfyAppservice, + Type: flow.Type, Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, DeviceID: deviceID, InitialDeviceDisplayName: "Skype Bridge", @@ -144,37 +147,7 @@ func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { if err != nil { return nil, fmt.Errorf("failed to log in as bridge bot: %w", err) } - if len(deviceID) == 0 { - helper.store.DeviceID = resp.DeviceID - } - return client, nil -} - -func (helper *CryptoHelper) loginBotOld() (*mautrix.Client, error) { - deviceID := helper.store.FindDeviceID() - if len(deviceID) > 0 { - helper.log.Debugln("Found existing device ID for bot in database:", deviceID) - } - mac := hmac.New(sha512.New, []byte(helper.bridge.Config.Bridge.LoginSharedSecret)) - mac.Write([]byte(helper.bridge.AS.BotMXID())) - client, err := mautrix.NewClient(helper.bridge.AS.HomeserverURL, "", "") - if err != nil { - return nil, err - } - resp, err := client.Login(&mautrix.ReqLogin{ - Type: mautrix.AuthTypePassword, - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.AS.BotMXID())}, - Password: hex.EncodeToString(mac.Sum(nil)), - DeviceID: deviceID, - InitialDeviceDisplayName: "Skype Bridge", - StoreCredentials: true, - }) - if err != nil { - return nil, err - } - if len(deviceID) == 0 { - helper.store.DeviceID = resp.DeviceID - } + helper.store.DeviceID = resp.DeviceID return client, nil } @@ -203,15 +176,15 @@ func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, conten helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID) users, err := helper.store.GetRoomMembers(roomID) if err != nil { - return nil, errors.Wrap(err, "failed to get room member list") + return nil, fmt.Errorf("failed to get room member list: %w", err) } err = helper.mach.ShareGroupSession(roomID, users) if err != nil { - return nil, errors.Wrap(err, "failed to share group session") + return nil, fmt.Errorf("failed to share group session: %w", err) } encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content) if err != nil { - return nil, errors.Wrap(err, "failed to encrypt event after re-sharing group session") + return nil, fmt.Errorf("failed to encrypt event after re-sharing group session: %w", err) } } return encrypted, nil @@ -226,7 +199,23 @@ type cryptoSyncer struct { } func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error { - syncer.ProcessSyncResponse(resp, since) + done := make(chan struct{}) + go func() { + defer func() { + if err := recover(); err != nil { + syncer.Log.Error("Processing sync response (%s) panicked: %v\n%s", since, err, debug.Stack()) + } + done <- struct{}{} + }() + syncer.Log.Trace("Starting sync response handling (%s)", since) + syncer.ProcessSyncResponse(resp, since) + syncer.Log.Trace("Successfully handled sync response (%s)", since) + }() + select { + case <-done: + case <-time.After(30 * time.Second): + syncer.Log.Warn("Handling sync response (%s) is taking unusually long", since) + } return nil } diff --git a/custompuppet.go b/custompuppet.go index 84e8ab0..b301c24 100644 --- a/custompuppet.go +++ b/custompuppet.go @@ -4,7 +4,7 @@ import ( "crypto/hmac" "crypto/sha512" "encoding/hex" - "strings" + "fmt" "time" "github.com/pkg/errors" @@ -28,7 +28,7 @@ func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error puppet.CustomMXID = mxid puppet.AccessToken = accessToken - err := puppet.StartCustomMXID() + err := puppet.StartCustomMXID(false) if err != nil { return err } @@ -46,11 +46,17 @@ func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error } func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { - mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) + _, homeserver, _ := mxid.Parse() + puppet.log.Debugfln("Logging into %s with shared secret", mxid) + mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecretMap[homeserver])) mac.Write([]byte(mxid)) - resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ - Type: "m.login.password", - Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)}, + client, err := puppet.bridge.newDoublePuppetClient(mxid, "") + if err != nil { + return "", fmt.Errorf("failed to create mautrix client to log in: %v", err) + } + resp, err := client.Login(&mautrix.ReqLogin{ + Type: mautrix.AuthTypePassword, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, Password: hex.EncodeToString(mac.Sum(nil)), DeviceID: "Skype Bridge", InitialDeviceDisplayName: "Skype Bridge", @@ -61,6 +67,36 @@ func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) { return resp.AccessToken, nil } +func (bridge *Bridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) { + _, homeserver, err := mxid.Parse() + if err != nil { + return nil, err + } + homeserverURL, found := bridge.Config.Bridge.DoublePuppetServerMap[homeserver] + if !found { + if homeserver == bridge.AS.HomeserverDomain { + homeserverURL = bridge.AS.HomeserverURL + } else if bridge.Config.Bridge.DoublePuppetAllowDiscovery { + resp, err := mautrix.DiscoverClientAPI(homeserver) + if err != nil { + return nil, fmt.Errorf("failed to find homeserver URL for %s: %v", homeserver, err) + } + homeserverURL = resp.Homeserver.BaseURL + bridge.Log.Debugfln("Discovered URL %s for %s to enable double puppeting for %s", homeserverURL, homeserver, mxid) + } else { + return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver) + } + } + client, err := mautrix.NewClient(homeserverURL, mxid, accessToken) + if err != nil { + return nil, err + } + client.Logger = bridge.AS.Log.Sub(mxid.String()) + client.Client = bridge.AS.HTTPClient + client.DefaultHTTPRetries = bridge.AS.DefaultHTTPRetries + return client, nil +} + func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) { if len(puppet.CustomMXID) == 0 { return nil, ErrNoCustomMXID @@ -89,7 +125,7 @@ func (puppet *Puppet) clearCustomMXID() { puppet.customUser = nil } -func (puppet *Puppet) StartCustomMXID() error { +func (puppet *Puppet) StartCustomMXID(reloginOnFail bool) error { if len(puppet.CustomMXID) == 0 { puppet.clearCustomMXID() return nil @@ -101,21 +137,12 @@ func (puppet *Puppet) StartCustomMXID() error { } resp, err := intent.Whoami() if err != nil { - // if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { - // puppet.log.Debugln("StartCustomMXID UpdateAccessToken: ", puppet.MXID) - // if puppet.customUser != nil { - // err, _ = puppet.customUser.UpdateAccessToken(puppet) - // } - // } - if puppet.customUser != nil { - err, _ = puppet.updateCustomAccessTokenIfExpired(err, puppet.customUser) - } - if err != nil { + if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !puppet.tryRelogin(err, "initializing double puppeting")) { puppet.clearCustomMXID() return err } - } - if resp.UserID != puppet.CustomMXID { + intent.AccessToken = puppet.AccessToken + } else if resp.UserID != puppet.CustomMXID { puppet.clearCustomMXID() return ErrMismatchingMXID } @@ -126,17 +153,6 @@ func (puppet *Puppet) StartCustomMXID() error { return nil } -func (puppet *Puppet) updateCustomAccessTokenIfExpired(err error, user *User) (newErr error, accessToken string) { - if user == nil { - return err, "" - } - puppet.log.Debugln("StartCustomMXID UpdateAccessToken: ", puppet.MXID) - if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { - return user.UpdateAccessToken(puppet) - } - return err, "" -} - func (puppet *Puppet) startSyncing() { if !puppet.bridge.Config.Bridge.SyncWithCustomPuppets { return @@ -249,9 +265,30 @@ func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) { //} } +func (puppet *Puppet) tryRelogin(cause error, action string) bool { + if !puppet.bridge.Config.CanAutoDoublePuppet(puppet.CustomMXID) { + return false + } + puppet.log.Debugfln("Trying to relogin after '%v' while %s", cause, action) + accessToken, err := puppet.loginWithSharedSecret(puppet.CustomMXID) + if err != nil { + puppet.log.Errorfln("Failed to relogin after '%v' while %s: %v", cause, action, err) + return false + } + puppet.log.Infofln("Successfully relogined after '%v' while %s", cause, action) + puppet.AccessToken = accessToken + return true +} + func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Duration, error) { puppet.log.Warnln("Sync error:", err) - err, _ = puppet.updateCustomAccessTokenIfExpired(err, puppet.customUser) + if errors.Is(err, mautrix.MUnknownToken) { + if !puppet.tryRelogin(err, "syncing") { + return 0, err + } + puppet.customIntent.AccessToken = puppet.AccessToken + return 0, nil + } return 10 * time.Second, err } diff --git a/example-config.yaml b/example-config.yaml index 720b88c..47efdff 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -129,12 +129,19 @@ bridge: # Whether or not to sync with custom puppets to receive EDUs that # are not normally sent to appservices. sync_with_custom_puppets: true + + # Servers to always allow double puppeting from + double_puppet_server_map: + example.com: https://example.com + # Allow using double puppeting from any server with a valid client .well-known file. + double_puppet_allow_discovery: false # Shared secret for https://github.com/devture/matrix-synapse-shared-secret-auth # # If set, custom puppets will be enabled automatically for local users # instead of users having to find an access token and run `login-matrix` # manually. - login_shared_secret: null + login_shared_secret_map: + example.com: foobar # Whether or not to invite own Skype user's Matrix puppet into private # chat portals when backfilling if needed. diff --git a/main.go b/main.go index 2d2c050..fcc4f72 100644 --- a/main.go +++ b/main.go @@ -323,7 +323,7 @@ func (bridge *Bridge) StartUsers() { for _, loopuppet := range bridge.GetAllPuppetsWithCustomMXID() { go func(puppet *Puppet) { puppet.log.Debugln("Starting custom puppet", puppet.CustomMXID) - err := puppet.StartCustomMXID() + err := puppet.StartCustomMXID(true) if err != nil { puppet.log.Errorln("Failed to start custom puppet:", err) } diff --git a/portal.go b/portal.go index 8e49030..3387895 100644 --- a/portal.go +++ b/portal.go @@ -346,8 +346,9 @@ func (portal *Portal) getMessageIntentSkype(user *User, info skype.Resource) *ap return portal.bridge.GetPuppetByJID(info.SendId + skypeExt.NewUserSuffix).IntentFor(portal) } -func (portal *Portal) handlePrivateChatFromMe(fromMe bool) func() { - if portal.IsPrivateChat() && fromMe && len(portal.bridge.Config.Bridge.LoginSharedSecret) == 0 { +func (portal *Portal) handlePrivateChatFromMe(user *User, fromMe bool) func() { + _, homeserver, _ := user.MXID.Parse() + if portal.IsPrivateChat() && fromMe && len(portal.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]) == 0 { var privateChatPuppet *Puppet var privateChatPuppetInvited bool privateChatPuppet = portal.bridge.GetPuppetByJID(portal.Key.Receiver) @@ -378,10 +379,9 @@ func (portal *Portal) startHandlingSkype(source *User, info skype.Resource) (*ap } else { portal.log.Debugfln("Starting handling of %s (ts: %d)", info.Id, info.Timestamp) portal.lastMessageTs = uint64(info.Timestamp) - return portal.getMessageIntentSkype(source, info), portal.handlePrivateChatFromMe(info.GetFromMe(source.Conn.Conn)) + return portal.getMessageIntentSkype(source, info), portal.handlePrivateChatFromMe(source, info.GetFromMe(source.Conn.Conn)) } - fmt.Println() - fmt.Printf("portal startHandling: %+v", "but nil") + portal.log.Debugfln("startHandlingSkype: %+v", "but nil") return nil, nil } @@ -945,7 +945,8 @@ func (portal *Portal) beginBackfill() func() { portal.privateChatBackfillInvitePuppet = nil portal.backfillLock.Unlock() if privateChatPuppet != nil && privateChatPuppetInvited { - if len(portal.bridge.Config.Bridge.LoginSharedSecret) > 0 { + _, homeserver, _ := privateChatPuppet.MXID.Parse() + if len(portal.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]) > 0 { _, _ = privateChatPuppet.DefaultIntent().LeaveRoom(portal.MXID) } } @@ -1512,7 +1513,7 @@ func (portal *Portal) trySendMessage(intent *appservice.IntentAPI, eventType eve resp, err = portal.sendMessage(intent, eventType, content, message.Timestamp*1000) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Id, err) - if strings.Index(err.Error(), "M_UNKNOWN_TOKEN (HTTP 401)") > -1 { + if errors.Is(err, mautrix.MUnknownToken) { puppet := source.bridge.GetPuppetByJID(source.JID) err, accessToken := source.UpdateAccessToken(puppet) if err == nil && accessToken != "" { diff --git a/puppet.go b/puppet.go index 995eadc..7da4fc4 100644 --- a/puppet.go +++ b/puppet.go @@ -6,6 +6,7 @@ import ( skypeExt "github.com/kelaresg/matrix-skype/skype-ext" "net/http" "regexp" + "sync" "strings" log "maunium.net/go/maulogger/v2" @@ -159,6 +160,8 @@ type Puppet struct { customIntent *appservice.IntentAPI customTypingIn map[id.RoomID]bool customUser *User + + syncLock sync.Mutex } func (puppet *Puppet) PhoneNumber() string { diff --git a/user.go b/user.go index dbeb68c..e624628 100644 --- a/user.go +++ b/user.go @@ -445,34 +445,32 @@ func (user *User) PostLogin() { } func (user *User) tryAutomaticDoublePuppeting() { - fmt.Println("tryAutomaticDoublePuppeting") - if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 { - fmt.Println("tryAutomaticDoublePuppeting-1", user.bridge.Config.Bridge.LoginSharedSecret) - // Automatic login not enabled - return - } else if _, homeserver, _ := user.MXID.Parse(); homeserver != user.bridge.Config.Homeserver.Domain { - fmt.Println("tryAutomaticDoublePuppeting-2", user.MXID) - fmt.Println("tryAutomaticDoublePuppeting-21", homeserver) - fmt.Println("tryAutomaticDoublePuppeting--3", user.bridge.Config.Homeserver.Domain) - // user is on another homeserver + if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) { return } - fmt.Println("tryAutomaticDoublePuppeting1") + user.log.Debugln("Checking if double puppeting needs to be enabled") puppet := user.bridge.GetPuppetByJID(user.JID) if len(puppet.CustomMXID) > 0 { + user.log.Debugln("User already has double-puppeting enabled") // Custom puppet already enabled return } - fmt.Println("tryAutomaticDoublePuppeting2", user.MXID) - _, _ = user.UpdateAccessToken(puppet) + accessToken, err := puppet.loginWithSharedSecret(user.MXID) + if err != nil { + user.log.Warnln("Failed to login with shared secret:", err) + return + } + err = puppet.SwitchCustomMXID(accessToken, user.MXID) + if err != nil { + puppet.log.Warnln("Failed to switch to auto-logined custom puppet:", err) + return + } + user.log.Infoln("Successfully automatically enabled custom puppet") } func (user *User) UpdateAccessToken(puppet *Puppet) (err error, accessToken string) { - if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 { - return errors.New("you didn't set LoginSharedSecret"), "" - } else if _, homeserver, _ := user.MXID.Parse(); homeserver != user.bridge.Config.Homeserver.Domain { - // user is on another homeserver - return errors.New("user is on another homeServer"), "" + if !puppet.bridge.Config.CanAutoDoublePuppet(user.MXID) { + return errors.New("you didn't set LoginSharedSecret or user is on another homeServer"), "" } accessToken, err = puppet.loginWithSharedSecret(user.MXID) if err != nil {