mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
Add bridge double puppeting utility
This commit is contained in:
parent
cacf5a943b
commit
ac5c2c2210
5 changed files with 187 additions and 5 deletions
|
|
@ -102,6 +102,7 @@ type User interface {
|
|||
type DoublePuppet interface {
|
||||
CustomIntent() *appservice.IntentAPI
|
||||
SwitchCustomMXID(accessToken string, userID id.UserID) error
|
||||
ClearCustomMXID()
|
||||
}
|
||||
|
||||
type Ghost interface {
|
||||
|
|
@ -171,6 +172,8 @@ type Bridge struct {
|
|||
|
||||
PublicHSAddress *url.URL
|
||||
|
||||
DoublePuppet *doublePuppetUtil
|
||||
|
||||
AS *appservice.AppService
|
||||
EventProcessor *appservice.EventProcessor
|
||||
CommandProcessor CommandProcessor
|
||||
|
|
@ -504,6 +507,8 @@ func (br *Bridge) init() {
|
|||
zerolog.DefaultContextLogger = &defaultCtxLog
|
||||
br.Log = maulogadapt.ZeroAsMau(br.ZLog)
|
||||
|
||||
br.DoublePuppet = &doublePuppetUtil{br: br, log: br.ZLog.With().Str("component", "double puppet").Logger()}
|
||||
|
||||
err = br.validateConfig()
|
||||
if err != nil {
|
||||
br.ZLog.WithLevel(zerolog.FatalLevel).Err(err).Msg("Configuration error")
|
||||
|
|
|
|||
|
|
@ -162,12 +162,19 @@ type BridgeConfig interface {
|
|||
GetEncryptionConfig() EncryptionConfig
|
||||
GetCommandPrefix() string
|
||||
GetManagementRoomTexts() ManagementRoomTexts
|
||||
GetDoublePuppetConfig() DoublePuppetConfig
|
||||
GetResendBridgeInfo() bool
|
||||
EnableMessageStatusEvents() bool
|
||||
EnableMessageErrorNotices() bool
|
||||
Validate() error
|
||||
}
|
||||
|
||||
type DoublePuppetConfig struct {
|
||||
ServerMap map[string]string `yaml:"double_puppet_server_map"`
|
||||
AllowDiscovery bool `yaml:"double_puppet_allow_discovery"`
|
||||
SharedSecretMap map[string]string `yaml:"login_shared_secret_map"`
|
||||
}
|
||||
|
||||
type EncryptionConfig struct {
|
||||
Allow bool `yaml:"allow"`
|
||||
Default bool `yaml:"default"`
|
||||
|
|
|
|||
|
|
@ -78,10 +78,6 @@ func fnLogoutMatrix(ce *Event) {
|
|||
ce.Reply("You don't have double puppeting enabled.")
|
||||
return
|
||||
}
|
||||
err := puppet.SwitchCustomMXID("", "")
|
||||
if err != nil {
|
||||
ce.Reply("Failed to disable double puppeting: %v", err)
|
||||
return
|
||||
}
|
||||
puppet.ClearCustomMXID()
|
||||
ce.Reply("Successfully disabled double puppeting.")
|
||||
}
|
||||
|
|
|
|||
172
bridge/doublepuppet.go
Normal file
172
bridge/doublepuppet.go
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
// Copyright (c) 2023 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 bridge
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type doublePuppetUtil struct {
|
||||
br *Bridge
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func (dp *doublePuppetUtil) newClient(mxid id.UserID, accessToken string) (*mautrix.Client, error) {
|
||||
_, homeserver, err := mxid.Parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
homeserverURL, found := dp.br.Config.Bridge.GetDoublePuppetConfig().ServerMap[homeserver]
|
||||
if !found {
|
||||
if homeserver == dp.br.AS.HomeserverDomain {
|
||||
homeserverURL = ""
|
||||
} else if dp.br.Config.Bridge.GetDoublePuppetConfig().AllowDiscovery {
|
||||
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
|
||||
dp.log.Debug().
|
||||
Str("homeserver", homeserver).
|
||||
Str("url", homeserverURL).
|
||||
Str("user_id", mxid.String()).
|
||||
Msg("Discovered URL to enable double puppeting for user")
|
||||
} else {
|
||||
return nil, fmt.Errorf("double puppeting from %s is not allowed", homeserver)
|
||||
}
|
||||
}
|
||||
return dp.br.AS.NewExternalMautrixClient(mxid, accessToken, homeserverURL)
|
||||
}
|
||||
|
||||
func (dp *doublePuppetUtil) newIntent(mxid id.UserID, accessToken string) (*appservice.IntentAPI, error) {
|
||||
client, err := dp.newClient(mxid, accessToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ia := dp.br.AS.NewIntentAPI("custom")
|
||||
ia.Client = client
|
||||
ia.Localpart, _, _ = mxid.Parse()
|
||||
ia.UserID = mxid
|
||||
ia.IsCustomPuppet = true
|
||||
return ia, nil
|
||||
}
|
||||
|
||||
func (dp *doublePuppetUtil) autoLogin(mxid id.UserID, loginSecret string) (string, error) {
|
||||
dp.log.Debug().Str("user_id", mxid.String()).Msg("Logging into user account with shared secret")
|
||||
client, err := dp.newClient(mxid, "")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create mautrix client to log in: %v", err)
|
||||
}
|
||||
bridgeName := fmt.Sprintf("%s Bridge", dp.br.ProtocolName)
|
||||
req := mautrix.ReqLogin{
|
||||
Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)},
|
||||
DeviceID: id.DeviceID(bridgeName),
|
||||
InitialDeviceDisplayName: bridgeName,
|
||||
}
|
||||
if loginSecret == "appservice" {
|
||||
client.AccessToken = dp.br.AS.Registration.AppToken
|
||||
req.Type = mautrix.AuthTypeAppservice
|
||||
} else {
|
||||
loginFlows, err := client.GetLoginFlows()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get supported login flows: %w", err)
|
||||
}
|
||||
mac := hmac.New(sha512.New, []byte(loginSecret))
|
||||
mac.Write([]byte(mxid))
|
||||
token := hex.EncodeToString(mac.Sum(nil))
|
||||
switch {
|
||||
case loginFlows.HasFlow(mautrix.AuthTypeDevtureSharedSecret):
|
||||
req.Type = mautrix.AuthTypeDevtureSharedSecret
|
||||
req.Token = token
|
||||
case loginFlows.HasFlow(mautrix.AuthTypePassword):
|
||||
req.Type = mautrix.AuthTypePassword
|
||||
req.Password = token
|
||||
default:
|
||||
return "", fmt.Errorf("no supported auth types for shared secret auth found")
|
||||
}
|
||||
}
|
||||
resp, err := client.Login(&req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.AccessToken, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
|
||||
ErrNoAccessToken = errors.New("no access token provided")
|
||||
ErrNoMXID = errors.New("no mxid provided")
|
||||
)
|
||||
|
||||
const useConfigASToken = "appservice-config"
|
||||
const asTokenModePrefix = "as_token:"
|
||||
|
||||
func (dp *doublePuppetUtil) Setup(mxid id.UserID, savedAccessToken string, reloginOnFail bool) (intent *appservice.IntentAPI, newAccessToken string, err error) {
|
||||
if len(mxid) == 0 {
|
||||
err = ErrNoMXID
|
||||
return
|
||||
}
|
||||
_, homeserver, _ := mxid.Parse()
|
||||
loginSecret, hasSecret := dp.br.Config.Bridge.GetDoublePuppetConfig().SharedSecretMap[homeserver]
|
||||
// Special case appservice: prefix to not login and use it as an as_token directly.
|
||||
if hasSecret && strings.HasPrefix(loginSecret, asTokenModePrefix) {
|
||||
intent, err = dp.newIntent(mxid, strings.TrimPrefix(loginSecret, asTokenModePrefix))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
intent.SetAppServiceUserID = true
|
||||
if savedAccessToken != useConfigASToken {
|
||||
var resp *mautrix.RespWhoami
|
||||
resp, err = intent.Whoami()
|
||||
if err == nil && resp.UserID != mxid {
|
||||
err = ErrMismatchingMXID
|
||||
}
|
||||
}
|
||||
return intent, useConfigASToken, err
|
||||
}
|
||||
if savedAccessToken == "" || savedAccessToken == useConfigASToken {
|
||||
if reloginOnFail && hasSecret {
|
||||
savedAccessToken, err = dp.autoLogin(mxid, loginSecret)
|
||||
} else {
|
||||
err = ErrNoAccessToken
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
intent, err = dp.newIntent(mxid, savedAccessToken)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var resp *mautrix.RespWhoami
|
||||
resp, err = intent.Whoami()
|
||||
if err != nil {
|
||||
if reloginOnFail && hasSecret && errors.Is(err, mautrix.MUnknownToken) {
|
||||
intent.AccessToken, err = dp.autoLogin(mxid, loginSecret)
|
||||
if err == nil {
|
||||
newAccessToken = intent.AccessToken
|
||||
}
|
||||
}
|
||||
} else if resp.UserID != mxid {
|
||||
err = ErrMismatchingMXID
|
||||
} else {
|
||||
newAccessToken = savedAccessToken
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -23,6 +23,8 @@ const (
|
|||
AuthTypeAppservice AuthType = "m.login.application_service"
|
||||
|
||||
AuthTypeSynapseJWT AuthType = "org.matrix.login.jwt"
|
||||
|
||||
AuthTypeDevtureSharedSecret AuthType = "com.devture.shared_secret_auth"
|
||||
)
|
||||
|
||||
type IdentifierType string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue