gmessages/custompuppet.go
2023-07-02 17:52:28 +03:00

168 lines
5.1 KiB
Go

// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
// Copyright (C) 2023 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"context"
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/id"
)
var (
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
)
var _ bridge.DoublePuppet = (*User)(nil)
func (user *User) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
if mxid != user.MXID {
return errors.New("mismatching mxid")
}
user.AccessToken = accessToken
return user.startCustomMXID(false)
}
func (user *User) CustomIntent() *appservice.IntentAPI {
return user.DoublePuppetIntent
}
func (user *User) loginWithSharedSecret() error {
_, homeserver, _ := user.MXID.Parse()
user.zlog.Debug().Msg("Logging into double puppet with shared secret")
loginSecret := user.bridge.Config.Bridge.LoginSharedSecretMap[homeserver]
client, err := user.bridge.newDoublePuppetClient(user.MXID, "")
if err != nil {
return err
}
req := mautrix.ReqLogin{
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(user.MXID)},
DeviceID: "Google Messages Bridge",
InitialDeviceDisplayName: "Google Messages Bridge",
}
if loginSecret == "appservice" {
client.AccessToken = user.bridge.AS.Registration.AppToken
req.Type = mautrix.AuthTypeAppservice
} else {
mac := hmac.New(sha512.New, []byte(loginSecret))
mac.Write([]byte(user.MXID))
req.Password = hex.EncodeToString(mac.Sum(nil))
req.Type = mautrix.AuthTypePassword
}
resp, err := client.Login(&req)
if err != nil {
return fmt.Errorf("failed to log in with shared secret: %w", err)
}
user.AccessToken = resp.AccessToken
err = user.Update(context.TODO())
if err != nil {
return fmt.Errorf("failed to save access token: %w", err)
}
return nil
}
func (br *GMBridge) newDoublePuppetClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
_, homeserver, err := mxid.Parse()
if err != nil {
return nil, err
}
homeserverURL, found := br.Config.Bridge.DoublePuppetServerMap[homeserver]
if !found {
if homeserver == br.AS.HomeserverDomain {
homeserverURL = ""
} else if br.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
br.ZLog.Debug().
Str("server_name", homeserver).
Str("base_url", homeserverURL).
Str("user_id", mxid.String()).
Msg("Discovered homeserver URL to enable double puppeting for external user")
} else {
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
}
}
return br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
}
func (user *User) newDoublePuppetIntent() (*appservice.IntentAPI, error) {
client, err := user.bridge.newDoublePuppetClient(user.MXID, user.AccessToken)
if err != nil {
return nil, err
}
ia := user.bridge.AS.NewIntentAPI("custom")
ia.Client = client
ia.Localpart, _, _ = user.MXID.Parse()
ia.UserID = user.MXID
ia.IsCustomPuppet = true
return ia, nil
}
func (user *User) clearCustomMXID() {
user.AccessToken = ""
user.DoublePuppetIntent = nil
}
func (user *User) startCustomMXID(reloginOnFail bool) error {
if len(user.AccessToken) == 0 {
return nil
}
intent, err := user.newDoublePuppetIntent()
if err != nil {
user.clearCustomMXID()
return fmt.Errorf("failed to create double puppet intent: %w", err)
}
resp, err := intent.Whoami()
if err != nil {
if !reloginOnFail || (errors.Is(err, mautrix.MUnknownToken) && !user.tryRelogin(err)) {
user.clearCustomMXID()
return fmt.Errorf("failed to ensure double puppet token is valid: %w", err)
}
intent.AccessToken = user.AccessToken
}
if resp.UserID != user.MXID {
user.clearCustomMXID()
return ErrMismatchingMXID
}
user.DoublePuppetIntent = intent
return nil
}
func (user *User) tryRelogin(err error) bool {
if !user.bridge.Config.CanAutoDoublePuppet(user.MXID) {
return false
}
user.zlog.Debug().Err(err).Msg("Trying to relogin after error in double puppet")
err = user.loginWithSharedSecret()
if err != nil {
user.zlog.Err(err).Msg("Failed to relogin after error in double puppet")
return false
}
user.zlog.Info().Msg("Successfully relogined after error in double puppet")
return true
}