mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
bridgev2/provisioning: add session transfer support
For connector logins that support it this will expose an API to transfer credentials between bridge instances. Currently does not do any extra validation beyond the usual provisioning API request validation (so shared secret or matrix token). One future improvement would be to require clients to sign incoming requests, and to then validate a) the signature and b) the device is verified.
This commit is contained in:
parent
a205a77db4
commit
487fc699fe
3 changed files with 128 additions and 3 deletions
|
|
@ -95,9 +95,10 @@ type AnalyticsConfig struct {
|
|||
}
|
||||
|
||||
type ProvisioningConfig struct {
|
||||
Prefix string `yaml:"prefix"`
|
||||
SharedSecret string `yaml:"shared_secret"`
|
||||
DebugEndpoints bool `yaml:"debug_endpoints"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
SharedSecret string `yaml:"shared_secret"`
|
||||
DebugEndpoints bool `yaml:"debug_endpoints"`
|
||||
EnableSessionTransfers bool `yaml:"enable_session_transfers"`
|
||||
}
|
||||
|
||||
type DirectMediaConfig struct {
|
||||
|
|
|
|||
|
|
@ -252,6 +252,9 @@ provisioning:
|
|||
allow_matrix_auth: true
|
||||
# Enable debug API at /debug with provisioning authentication.
|
||||
debug_endpoints: false
|
||||
# Enable session transfers between bridges. Note that this only validates Matrix or shared secret
|
||||
# auth before passing live network client credentials down in the response.
|
||||
enable_session_transfers: false
|
||||
|
||||
# Some networks require publicly accessible media download links (e.g. for user avatars when using Discord webhooks).
|
||||
# These settings control whether the bridge will provide such public media access.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,11 @@ type ProvisioningAPI struct {
|
|||
matrixAuthCache map[string]matrixAuthCacheEntry
|
||||
matrixAuthCacheLock sync.Mutex
|
||||
|
||||
// Set for a given login once credentials have been exported, once in this state the finish
|
||||
// API is available which will call logout on the client in question.
|
||||
sessionTransfers map[networkid.UserLoginID]struct{}
|
||||
sessionTransfersLock sync.Mutex
|
||||
|
||||
// GetAuthFromRequest is a custom function for getting the auth token from
|
||||
// the request if the Authorization header is not present.
|
||||
GetAuthFromRequest func(r *http.Request) string
|
||||
|
|
@ -101,6 +106,7 @@ func (br *Connector) GetProvisioning() IProvisioningAPI {
|
|||
func (prov *ProvisioningAPI) Init() {
|
||||
prov.matrixAuthCache = make(map[string]matrixAuthCacheEntry)
|
||||
prov.logins = make(map[string]*ProvLogin)
|
||||
prov.sessionTransfers = make(map[networkid.UserLoginID]struct{})
|
||||
prov.net = prov.br.Bridge.Network
|
||||
prov.log = prov.br.Log.With().Str("component", "provisioning").Logger()
|
||||
prov.fedClient = federation.NewClient("", nil, nil)
|
||||
|
|
@ -128,6 +134,12 @@ func (prov *ProvisioningAPI) Init() {
|
|||
prov.Router.Path("/v3/create_dm/{identifier}").Methods(http.MethodPost, http.MethodOptions).HandlerFunc(prov.PostCreateDM)
|
||||
prov.Router.Path("/v3/create_group").Methods(http.MethodPost, http.MethodOptions).HandlerFunc(prov.PostCreateGroup)
|
||||
|
||||
if prov.br.Config.Provisioning.EnableSessionTransfers {
|
||||
prov.log.Debug().Msg("Enabling session transfer API")
|
||||
prov.Router.Path("/v3/session_transfer/init").Methods(http.MethodPost, http.MethodOptions).HandlerFunc(prov.PostInitSessionTransfer)
|
||||
prov.Router.Path("/v3/session_transfer/finish").Methods(http.MethodPost, http.MethodOptions).HandlerFunc(prov.PostFinishSessionTransfer)
|
||||
}
|
||||
|
||||
if prov.br.Config.Provisioning.DebugEndpoints {
|
||||
prov.log.Debug().Msg("Enabling debug API at /debug")
|
||||
r := prov.br.AS.Router.PathPrefix("/debug").Subrouter()
|
||||
|
|
@ -791,3 +803,112 @@ func (prov *ProvisioningAPI) PostCreateGroup(w http.ResponseWriter, r *http.Requ
|
|||
ErrCode: mautrix.MUnrecognized.ErrCode,
|
||||
})
|
||||
}
|
||||
|
||||
type ReqExportCredentials struct {
|
||||
RemoteID networkid.UserLoginID `json:"remote_name"`
|
||||
}
|
||||
|
||||
type RespExportCredentials struct {
|
||||
Credentials any `json:"credentials"`
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) PostInitSessionTransfer(w http.ResponseWriter, r *http.Request) {
|
||||
prov.sessionTransfersLock.Lock()
|
||||
defer prov.sessionTransfersLock.Unlock()
|
||||
|
||||
var req ReqExportCredentials
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to decode request body")
|
||||
jsonResponse(w, http.StatusBadRequest, &mautrix.RespError{
|
||||
Err: "Failed to decode request body",
|
||||
ErrCode: mautrix.MNotJSON.ErrCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user := prov.GetUser(r)
|
||||
logins := user.GetUserLogins()
|
||||
var loginToExport *bridgev2.UserLogin
|
||||
for _, login := range logins {
|
||||
if login.ID == req.RemoteID {
|
||||
loginToExport = login
|
||||
break
|
||||
}
|
||||
}
|
||||
if loginToExport == nil {
|
||||
jsonResponse(w, http.StatusNotFound, &mautrix.RespError{
|
||||
Err: "No matching user login found",
|
||||
ErrCode: mautrix.MNotFound.ErrCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
client, ok := loginToExport.Client.(bridgev2.CredentialExportingNetworkAPI)
|
||||
if !ok {
|
||||
jsonResponse(w, http.StatusBadRequest, &mautrix.RespError{
|
||||
Err: "Client does not support credential exporting",
|
||||
ErrCode: mautrix.MInvalidParam.ErrCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := prov.sessionTransfers[loginToExport.ID]; ok {
|
||||
// Warn, but allow, double exports. This might happen if a client crashes handling creds,
|
||||
// and should be safe to call multiple times.
|
||||
zerolog.Ctx(r.Context()).Warn().Msg("Exporting already exported credentials")
|
||||
}
|
||||
|
||||
resp := RespExportCredentials{
|
||||
Credentials: client.ExportCredentials(r.Context()),
|
||||
}
|
||||
jsonResponse(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (prov *ProvisioningAPI) PostFinishSessionTransfer(w http.ResponseWriter, r *http.Request) {
|
||||
prov.sessionTransfersLock.Lock()
|
||||
defer prov.sessionTransfersLock.Unlock()
|
||||
|
||||
var req ReqExportCredentials
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
zerolog.Ctx(r.Context()).Err(err).Msg("Failed to decode request body")
|
||||
jsonResponse(w, http.StatusBadRequest, &mautrix.RespError{
|
||||
Err: "Failed to decode request body",
|
||||
ErrCode: mautrix.MNotJSON.ErrCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user := prov.GetUser(r)
|
||||
logins := user.GetUserLogins()
|
||||
var loginToExport *bridgev2.UserLogin
|
||||
for _, login := range logins {
|
||||
if login.ID == req.RemoteID {
|
||||
loginToExport = login
|
||||
break
|
||||
}
|
||||
}
|
||||
if loginToExport == nil {
|
||||
jsonResponse(w, http.StatusNotFound, &mautrix.RespError{
|
||||
Err: "No matching user login found",
|
||||
ErrCode: mautrix.MNotFound.ErrCode,
|
||||
})
|
||||
return
|
||||
} else if _, ok := prov.sessionTransfers[loginToExport.ID]; !ok {
|
||||
jsonResponse(w, http.StatusBadRequest, &mautrix.RespError{
|
||||
Err: "No matching credential export found",
|
||||
ErrCode: mautrix.MNotJSON.ErrCode,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
zerolog.Ctx(r.Context()).Info().
|
||||
Str("remote_name", string(req.RemoteID)).
|
||||
Msg("Logging out remote after finishing credential export")
|
||||
|
||||
loginToExport.Client.LogoutRemote(r.Context())
|
||||
delete(prov.sessionTransfers, req.RemoteID)
|
||||
|
||||
jsonResponse(w, http.StatusOK, struct{}{})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue