Implement /_synapse/admin/v1/register (#1911)
* Implement /_synapse/admin/v1/register This is implemented identically to Synapse, so scripts which work with Synapse should work with Dendrite. ``` Test 27 POST /_synapse/admin/v1/register with shared secret... OK Test 28 POST /_synapse/admin/v1/register admin with shared secret... OK Test 29 POST /_synapse/admin/v1/register with shared secret downcases capitals... OK Test 30 POST /_synapse/admin/v1/register with shared secret disallows symbols... OK ``` Sytest however has `implementation_specific => "synapse"` which stops these tests from running. * Add missing muxes to gobind * Lintingmain
parent
c8408a6387
commit
1ed732cc78
|
@ -334,6 +334,7 @@ func (m *DendriteMonolith) Start() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||
|
|
|
@ -173,6 +173,7 @@ func (m *DendriteMonolith) Start() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
httpRouter := mux.NewRouter()
|
||||
|
|
|
@ -35,6 +35,7 @@ import (
|
|||
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
||||
func AddPublicRoutes(
|
||||
router *mux.Router,
|
||||
synapseAdminRouter *mux.Router,
|
||||
cfg *config.ClientAPI,
|
||||
accountsDB accounts.Database,
|
||||
federation *gomatrixserverlib.FederationClient,
|
||||
|
@ -56,7 +57,7 @@ func AddPublicRoutes(
|
|||
}
|
||||
|
||||
routing.Setup(
|
||||
router, cfg, eduInputAPI, rsAPI, asAPI,
|
||||
router, synapseAdminRouter, cfg, eduInputAPI, rsAPI, asAPI,
|
||||
accountsDB, userAPI, federation,
|
||||
syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
|
||||
)
|
||||
|
|
|
@ -17,10 +17,7 @@ package routing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
@ -594,7 +591,6 @@ func handleRegistrationFlow(
|
|||
accessToken string,
|
||||
accessTokenErr error,
|
||||
) util.JSONResponse {
|
||||
// TODO: Shared secret registration (create new user scripts)
|
||||
// TODO: Enable registration config flag
|
||||
// TODO: Guest account upgrading
|
||||
|
||||
|
@ -643,20 +639,6 @@ func handleRegistrationFlow(
|
|||
// Add Recaptcha to the list of completed registration stages
|
||||
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||
|
||||
case authtypes.LoginTypeSharedSecret:
|
||||
// Check shared secret against config
|
||||
valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac)
|
||||
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("isValidMacLogin failed")
|
||||
return jsonerror.InternalServerError()
|
||||
} else if !valid {
|
||||
return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
|
||||
}
|
||||
|
||||
// Add SharedSecret to the list of completed registration stages
|
||||
AddCompletedSessionStage(sessionID, authtypes.LoginTypeSharedSecret)
|
||||
|
||||
case authtypes.LoginTypeDummy:
|
||||
// there is nothing to do
|
||||
// Add Dummy to the list of completed registration stages
|
||||
|
@ -849,49 +831,6 @@ func completeRegistration(
|
|||
}
|
||||
}
|
||||
|
||||
// Used for shared secret registration.
|
||||
// Checks if the username, password and isAdmin flag matches the given mac.
|
||||
func isValidMacLogin(
|
||||
cfg *config.ClientAPI,
|
||||
username, password string,
|
||||
isAdmin bool,
|
||||
givenMac []byte,
|
||||
) (bool, error) {
|
||||
sharedSecret := cfg.RegistrationSharedSecret
|
||||
|
||||
// Check that shared secret registration isn't disabled.
|
||||
if cfg.RegistrationSharedSecret == "" {
|
||||
return false, errors.New("Shared secret registration is disabled")
|
||||
}
|
||||
|
||||
// Double check that username/password don't contain the HMAC delimiters. We should have
|
||||
// already checked this.
|
||||
if strings.Contains(username, "\x00") {
|
||||
return false, errors.New("Username contains invalid character")
|
||||
}
|
||||
if strings.Contains(password, "\x00") {
|
||||
return false, errors.New("Password contains invalid character")
|
||||
}
|
||||
if sharedSecret == "" {
|
||||
return false, errors.New("Shared secret registration is disabled")
|
||||
}
|
||||
|
||||
adminString := "notadmin"
|
||||
if isAdmin {
|
||||
adminString = "admin"
|
||||
}
|
||||
joined := strings.Join([]string{username, password, adminString}, "\x00")
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(sharedSecret))
|
||||
_, err := mac.Write([]byte(joined))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expectedMAC := mac.Sum(nil)
|
||||
|
||||
return hmac.Equal(givenMac, expectedMAC), nil
|
||||
}
|
||||
|
||||
// checkFlows checks a single completed flow against another required one. If
|
||||
// one contains at least all of the stages that the other does, checkFlows
|
||||
// returns true.
|
||||
|
@ -995,3 +934,34 @@ func RegisterAvailable(
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func handleSharedSecretRegistration(userAPI userapi.UserInternalAPI, sr *SharedSecretRegistration, req *http.Request) util.JSONResponse {
|
||||
ssrr, err := NewSharedSecretRegistrationRequest(req.Body)
|
||||
if err != nil {
|
||||
return util.JSONResponse{
|
||||
Code: 400,
|
||||
JSON: jsonerror.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
||||
}
|
||||
}
|
||||
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
||||
if err != nil {
|
||||
return util.ErrorResponse(err)
|
||||
}
|
||||
if !valid {
|
||||
return util.JSONResponse{
|
||||
Code: 403,
|
||||
JSON: jsonerror.Forbidden("bad mac"),
|
||||
}
|
||||
}
|
||||
// downcase capitals
|
||||
ssrr.User = strings.ToLower(ssrr.User)
|
||||
|
||||
if resErr := validateUsername(ssrr.User); resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
if resErr := validatePassword(ssrr.Password); resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
deviceID := "shared_secret_registration"
|
||||
return completeRegistration(req.Context(), userAPI, ssrr.User, ssrr.Password, "", req.RemoteAddr, req.UserAgent(), false, &ssrr.User, &deviceID)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/dendrite/internal"
|
||||
"github.com/matrix-org/util"
|
||||
cache "github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type SharedSecretRegistrationRequest struct {
|
||||
User string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Nonce string `json:"nonce"`
|
||||
MacBytes []byte
|
||||
MacStr string `json:"mac"`
|
||||
Admin bool `json:"admin"`
|
||||
}
|
||||
|
||||
func NewSharedSecretRegistrationRequest(reader io.ReadCloser) (*SharedSecretRegistrationRequest, error) {
|
||||
defer internal.CloseAndLogIfError(context.Background(), reader, "NewSharedSecretRegistrationRequest: failed to close request body")
|
||||
var ssrr SharedSecretRegistrationRequest
|
||||
err := json.NewDecoder(reader).Decode(&ssrr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ssrr.MacBytes, err = hex.DecodeString(ssrr.MacStr)
|
||||
return &ssrr, err
|
||||
}
|
||||
|
||||
type SharedSecretRegistration struct {
|
||||
sharedSecret string
|
||||
nonces *cache.Cache
|
||||
}
|
||||
|
||||
func NewSharedSecretRegistration(sharedSecret string) *SharedSecretRegistration {
|
||||
return &SharedSecretRegistration{
|
||||
sharedSecret: sharedSecret,
|
||||
// nonces live for 5mins, purge every 10mins
|
||||
nonces: cache.New(5*time.Minute, 10*time.Minute),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SharedSecretRegistration) GenerateNonce() string {
|
||||
nonce := util.RandomString(16)
|
||||
r.nonces.Set(nonce, true, cache.DefaultExpiration)
|
||||
return nonce
|
||||
}
|
||||
|
||||
func (r *SharedSecretRegistration) validNonce(nonce string) bool {
|
||||
_, exists := r.nonces.Get(nonce)
|
||||
return exists
|
||||
}
|
||||
|
||||
func (r *SharedSecretRegistration) IsValidMacLogin(
|
||||
nonce, username, password string,
|
||||
isAdmin bool,
|
||||
givenMac []byte,
|
||||
) (bool, error) {
|
||||
// Check that shared secret registration isn't disabled.
|
||||
if r.sharedSecret == "" {
|
||||
return false, errors.New("Shared secret registration is disabled")
|
||||
}
|
||||
if !r.validNonce(nonce) {
|
||||
return false, fmt.Errorf("Incorrect or expired nonce: %s", nonce)
|
||||
}
|
||||
|
||||
// Check that username/password don't contain the HMAC delimiters.
|
||||
if strings.Contains(username, "\x00") {
|
||||
return false, errors.New("Username contains invalid character")
|
||||
}
|
||||
if strings.Contains(password, "\x00") {
|
||||
return false, errors.New("Password contains invalid character")
|
||||
}
|
||||
|
||||
adminString := "notadmin"
|
||||
if isAdmin {
|
||||
adminString = "admin"
|
||||
}
|
||||
joined := strings.Join([]string{nonce, username, password, adminString}, "\x00")
|
||||
|
||||
mac := hmac.New(sha1.New, []byte(r.sharedSecret))
|
||||
_, err := mac.Write([]byte(joined))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
expectedMAC := mac.Sum(nil)
|
||||
|
||||
return hmac.Equal(givenMac, expectedMAC), nil
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
func TestSharedSecretRegister(t *testing.T) {
|
||||
// these values have come from a local synapse instance to ensure compatibility
|
||||
jsonStr := []byte(`{"admin":false,"mac":"f1ba8d37123866fd659b40de4bad9b0f8965c565","nonce":"759f047f312b99ff428b21d581256f8592b8976e58bc1b543972dc6147e529a79657605b52d7becd160ff5137f3de11975684319187e06901955f79e5a6c5a79","password":"wonderland","username":"alice"}`)
|
||||
sharedSecret := "dendritetest"
|
||||
|
||||
req, err := NewSharedSecretRegistrationRequest(ioutil.NopCloser(bytes.NewBuffer(jsonStr)))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read request: %s", err)
|
||||
}
|
||||
|
||||
r := NewSharedSecretRegistration(sharedSecret)
|
||||
|
||||
// force the nonce to be known
|
||||
r.nonces.Set(req.Nonce, true, cache.DefaultExpiration)
|
||||
|
||||
valid, err := r.IsValidMacLogin(req.Nonce, req.User, req.Password, req.Admin, req.MacBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check for valid mac: %s", err)
|
||||
}
|
||||
if !valid {
|
||||
t.Errorf("mac login failed, wanted success")
|
||||
}
|
||||
|
||||
// modify the mac so it fails
|
||||
req.MacBytes[0] = 0xff
|
||||
valid, err = r.IsValidMacLogin(req.Nonce, req.User, req.Password, req.Admin, req.MacBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to check for valid mac: %s", err)
|
||||
}
|
||||
if valid {
|
||||
t.Errorf("mac login succeeded, wanted failure")
|
||||
}
|
||||
}
|
|
@ -37,6 +37,7 @@ import (
|
|||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
||||
|
@ -46,7 +47,7 @@ import (
|
|||
// applied:
|
||||
// nolint: gocyclo
|
||||
func Setup(
|
||||
publicAPIMux *mux.Router, cfg *config.ClientAPI,
|
||||
publicAPIMux, synapseAdminRouter *mux.Router, cfg *config.ClientAPI,
|
||||
eduAPI eduServerAPI.EDUServerInputAPI,
|
||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||
|
@ -88,6 +89,32 @@ func Setup(
|
|||
}),
|
||||
).Methods(http.MethodGet, http.MethodOptions)
|
||||
|
||||
if cfg.RegistrationSharedSecret != "" {
|
||||
logrus.Info("Enabling shared secret registration at /_synapse/admin/v1/register")
|
||||
sr := NewSharedSecretRegistration(cfg.RegistrationSharedSecret)
|
||||
synapseAdminRouter.Handle("/admin/v1/register",
|
||||
httputil.MakeExternalAPI("shared_secret_registration", func(req *http.Request) util.JSONResponse {
|
||||
if req.Method == http.MethodGet {
|
||||
return util.JSONResponse{
|
||||
Code: 200,
|
||||
JSON: struct {
|
||||
Nonce string `json:"nonce"`
|
||||
}{
|
||||
Nonce: sr.GenerateNonce(),
|
||||
},
|
||||
}
|
||||
}
|
||||
if req.Method == http.MethodPost {
|
||||
return handleSharedSecretRegistration(userAPI, sr, req)
|
||||
}
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
JSON: jsonerror.NotFound("unknown method"),
|
||||
}
|
||||
}),
|
||||
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||
}
|
||||
|
||||
r0mux := publicAPIMux.PathPrefix("/r0").Subrouter()
|
||||
unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter()
|
||||
|
||||
|
|
|
@ -197,6 +197,7 @@ func main() {
|
|||
base.Base.PublicFederationAPIMux,
|
||||
base.Base.PublicKeyAPIMux,
|
||||
base.Base.PublicMediaAPIMux,
|
||||
base.Base.SynapseAdminMux,
|
||||
)
|
||||
if err := mscs.Enable(&base.Base, &monolith); err != nil {
|
||||
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
||||
|
|
|
@ -210,6 +210,7 @@ func main() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
wsUpgrader := websocket.Upgrader{
|
||||
|
|
|
@ -154,6 +154,7 @@ func main() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
if err := mscs.Enable(base, &monolith); err != nil {
|
||||
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
||||
|
|
|
@ -149,6 +149,7 @@ func main() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
if len(base.Cfg.MSCs.MSCs) > 0 {
|
||||
|
|
|
@ -33,7 +33,7 @@ func ClientAPI(base *setup.BaseDendrite, cfg *config.Dendrite) {
|
|||
keyAPI := base.KeyServerHTTPClient()
|
||||
|
||||
clientapi.AddPublicRoutes(
|
||||
base.PublicClientAPIMux, &base.Cfg.ClientAPI, accountDB, federation,
|
||||
base.PublicClientAPIMux, base.SynapseAdminMux, &base.Cfg.ClientAPI, accountDB, federation,
|
||||
rsAPI, eduInputAPI, asQuery, transactions.New(), fsAPI, userAPI, keyAPI, nil,
|
||||
&cfg.MSCs,
|
||||
)
|
||||
|
|
|
@ -215,6 +215,7 @@ func main() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||
|
|
|
@ -236,6 +236,7 @@ func main() {
|
|||
base.PublicFederationAPIMux,
|
||||
base.PublicKeyAPIMux,
|
||||
base.PublicMediaAPIMux,
|
||||
base.SynapseAdminMux,
|
||||
)
|
||||
|
||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||
|
|
1
go.mod
1
go.mod
|
@ -39,6 +39,7 @@ require (
|
|||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||
github.com/ngrok/sqlmw v0.0.0-20200129213757-d5c93a81bec6
|
||||
github.com/opentracing/opentracing-go v1.2.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pressly/goose v2.7.0+incompatible
|
||||
github.com/prometheus/client_golang v1.9.0
|
||||
|
|
2
go.sum
2
go.sum
|
@ -1256,6 +1256,8 @@ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
|
|||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
|
|
|
@ -77,6 +77,7 @@ type BaseDendrite struct {
|
|||
PublicKeyAPIMux *mux.Router
|
||||
PublicMediaAPIMux *mux.Router
|
||||
InternalAPIMux *mux.Router
|
||||
SynapseAdminMux *mux.Router
|
||||
UseHTTPAPIs bool
|
||||
apiHttpClient *http.Client
|
||||
httpClient *http.Client
|
||||
|
@ -199,6 +200,7 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, useHTTPAPIs boo
|
|||
PublicKeyAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicKeyPathPrefix).Subrouter().UseEncodedPath(),
|
||||
PublicMediaAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicMediaPathPrefix).Subrouter().UseEncodedPath(),
|
||||
InternalAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.InternalPathPrefix).Subrouter().UseEncodedPath(),
|
||||
SynapseAdminMux: mux.NewRouter().SkipClean(true).PathPrefix("/_synapse/").Subrouter().UseEncodedPath(),
|
||||
apiHttpClient: &apiClient,
|
||||
httpClient: &client,
|
||||
}
|
||||
|
@ -391,6 +393,7 @@ func (b *BaseDendrite) SetupAndServeHTTP(
|
|||
externalRouter.PathPrefix(httputil.PublicKeyPathPrefix).Handler(b.PublicKeyAPIMux)
|
||||
externalRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(federationHandler)
|
||||
}
|
||||
externalRouter.PathPrefix("/_synapse/").Handler(b.SynapseAdminMux)
|
||||
externalRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(b.PublicMediaAPIMux)
|
||||
|
||||
if internalAddr != NoListener && internalAddr != externalAddr {
|
||||
|
|
|
@ -57,9 +57,9 @@ type Monolith struct {
|
|||
}
|
||||
|
||||
// AddAllPublicRoutes attaches all public paths to the given router
|
||||
func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, mediaMux *mux.Router) {
|
||||
func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, mediaMux, synapseMux *mux.Router) {
|
||||
clientapi.AddPublicRoutes(
|
||||
csMux, &m.Config.ClientAPI, m.AccountDB,
|
||||
csMux, synapseMux, &m.Config.ClientAPI, m.AccountDB,
|
||||
m.FedClient, m.RoomserverAPI,
|
||||
m.EDUInternalAPI, m.AppserviceAPI, transactions.New(),
|
||||
m.FederationSenderAPI, m.UserAPI, m.KeyAPI, m.ExtPublicRoomsProvider,
|
||||
|
|
Loading…
Reference in New Issue