Add config and checks for trusted ID servers (#206)

* Add config for trusted ID servers

* Add new error

* Implement check for trusted ID server

* Complete unfinished comment

* Make comment more explicit in the config file

* Use go standard errors in membership.go

* Use standard errors instead of JSON responses in threepid

* Doc errors

* Remove unused parameter
main
Brendan Abolivier 2017-09-11 19:18:19 +01:00 committed by GitHub
parent 28346b39e8
commit f1fce55697
8 changed files with 175 additions and 80 deletions

View File

@ -12,6 +12,12 @@ matrix:
private_key: "/etc/dendrite/matrix_key.pem" private_key: "/etc/dendrite/matrix_key.pem"
# The x509 certificates used by the federation listeners for this server # The x509 certificates used by the federation listeners for this server
federation_certificates: ["/etc/dendrite/server.pem"] federation_certificates: ["/etc/dendrite/server.pem"]
# The list of identity servers trusted to verify third party identifiers by this server.
# Defaults to no trusted servers.
trusted_third_party_id_servers:
- vector.im
- matrix.org
- riot.im
# The media repository config # The media repository config
media: media:

View File

@ -104,3 +104,12 @@ func LimitExceeded(msg string, retryAfterMS int64) *LimitExceededError {
RetryAfterMS: retryAfterMS, RetryAfterMS: retryAfterMS,
} }
} }
// NotTrusted is an error which is returned when the client asks the server to
// proxy a request (e.g. 3PID association) to a server that isn't trusted
func NotTrusted(serverName string) *MatrixError {
return &MatrixError{
ErrCode: "M_SERVER_NOT_TRUSTED",
Err: fmt.Sprintf("Untrusted server '%s'", serverName),
}
}

View File

@ -22,6 +22,7 @@ import (
"github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/threepid" "github.com/matrix-org/dendrite/clientapi/threepid"
"github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util" "github.com/matrix-org/util"
@ -38,7 +39,7 @@ type threePIDsResponse struct {
// RequestEmailToken implements: // RequestEmailToken implements:
// POST /account/3pid/email/requestToken // POST /account/3pid/email/requestToken
// POST /register/email/requestToken // POST /register/email/requestToken
func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSONResponse { func RequestEmailToken(req *http.Request, accountDB *accounts.Database, cfg config.Dendrite) util.JSONResponse {
var body threepid.EmailAssociationRequest var body threepid.EmailAssociationRequest
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
return *reqErr return *reqErr
@ -63,8 +64,13 @@ func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSO
} }
} }
resp.SID, err = threepid.CreateSession(body) resp.SID, err = threepid.CreateSession(body, cfg)
if err != nil { if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.NotTrusted(body.IDServer),
}
} else if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
@ -77,6 +83,7 @@ func RequestEmailToken(req *http.Request, accountDB *accounts.Database) util.JSO
// CheckAndSave3PIDAssociation implements POST /account/3pid // CheckAndSave3PIDAssociation implements POST /account/3pid
func CheckAndSave3PIDAssociation( func CheckAndSave3PIDAssociation(
req *http.Request, accountDB *accounts.Database, device *authtypes.Device, req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
cfg config.Dendrite,
) util.JSONResponse { ) util.JSONResponse {
var body threepid.EmailAssociationCheckRequest var body threepid.EmailAssociationCheckRequest
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
@ -84,8 +91,13 @@ func CheckAndSave3PIDAssociation(
} }
// Check if the association has been validated // Check if the association has been validated
verified, address, medium, err := threepid.CheckAssociation(body.Creds) verified, address, medium, err := threepid.CheckAssociation(body.Creds, cfg)
if err != nil { if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.NotTrusted(body.Creds.IDServer),
}
} else if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
@ -101,7 +113,13 @@ func CheckAndSave3PIDAssociation(
if body.Bind { if body.Bind {
// Publish the association on the identity server if requested // Publish the association on the identity server if requested
if err = threepid.PublishAssociation(body.Creds, device.UserID); err != nil { err = threepid.PublishAssociation(body.Creds, device.UserID, cfg)
if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.NotTrusted(body.Creds.IDServer),
}
} else if err != nil {
return httputil.LogThenError(req, err) return httputil.LogThenError(req, err)
} }
} }

View File

@ -241,7 +241,7 @@ func Setup(
r0mux.Handle("/account/3pid", r0mux.Handle("/account/3pid",
common.MakeAuthAPI("account_3pid", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse { common.MakeAuthAPI("account_3pid", deviceDB, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
return readers.CheckAndSave3PIDAssociation(req, accountDB, device) return readers.CheckAndSave3PIDAssociation(req, accountDB, device, cfg)
}), }),
).Methods("POST", "OPTIONS") ).Methods("POST", "OPTIONS")
@ -253,7 +253,7 @@ func Setup(
r0mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken", r0mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken",
common.MakeAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse { common.MakeAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse {
return readers.RequestEmailToken(req, accountDB) return readers.RequestEmailToken(req, accountDB, cfg)
}), }),
).Methods("POST", "OPTIONS") ).Methods("POST", "OPTIONS")

View File

@ -26,15 +26,11 @@ import (
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts" "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/events" "github.com/matrix-org/dendrite/clientapi/events"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers" "github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config" "github.com/matrix-org/dendrite/common/config"
"github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
) )
// MembershipRequest represents the body of an incoming POST request // MembershipRequest represents the body of an incoming POST request
@ -66,6 +62,15 @@ type idServerStoreInviteResponse struct {
PublicKeys []common.PublicKey `json:"public_keys"` PublicKeys []common.PublicKey `json:"public_keys"`
} }
var (
// ErrMissingParameter is the error raised if a request for 3PID invite has
// an incomplete body
ErrMissingParameter = errors.New("'address', 'id_server' and 'medium' must all be supplied")
// ErrNotTrusted is the error raised if an identity server isn't in the list
// of trusted servers in the configuration file.
ErrNotTrusted = errors.New("untrusted server")
)
// CheckAndProcessInvite analyses the body of an incoming membership request. // CheckAndProcessInvite analyses the body of an incoming membership request.
// If the fields relative to a third-party-invite are all supplied, lookups the // If the fields relative to a third-party-invite are all supplied, lookups the
// matching Matrix ID from the given identity server. If no Matrix ID is // matching Matrix ID from the given identity server. If no Matrix ID is
@ -80,27 +85,24 @@ type idServerStoreInviteResponse struct {
// fills the Matrix ID in the request body so a normal invite membership event // fills the Matrix ID in the request body so a normal invite membership event
// can be emitted. // can be emitted.
func CheckAndProcessInvite( func CheckAndProcessInvite(
req *http.Request, device *authtypes.Device, body *MembershipRequest, device *authtypes.Device, body *MembershipRequest, cfg config.Dendrite,
cfg config.Dendrite, queryAPI api.RoomserverQueryAPI, db *accounts.Database, queryAPI api.RoomserverQueryAPI, db *accounts.Database,
producer *producers.RoomserverProducer, membership string, roomID string, producer *producers.RoomserverProducer, membership string, roomID string,
) *util.JSONResponse { ) (inviteStoredOnIDServer bool, err error) {
if membership != "invite" || (body.Address == "" && body.IDServer == "" && body.Medium == "") { if membership != "invite" || (body.Address == "" && body.IDServer == "" && body.Medium == "") {
// If none of the 3PID-specific fields are supplied, it's a standard invite // If none of the 3PID-specific fields are supplied, it's a standard invite
// so return nil for it to be processed as such // so return nil for it to be processed as such
return nil return
} else if body.Address == "" || body.IDServer == "" || body.Medium == "" { } else if body.Address == "" || body.IDServer == "" || body.Medium == "" {
// If at least one of the 3PID-specific fields is supplied but not all // If at least one of the 3PID-specific fields is supplied but not all
// of them, return an error // of them, return an error
return &util.JSONResponse{ err = ErrMissingParameter
Code: 400, return
JSON: jsonerror.BadJSON("'address', 'id_server' and 'medium' must all be supplied"),
}
} }
lookupRes, storeInviteRes, err := queryIDServer(db, cfg, device, body, roomID) lookupRes, storeInviteRes, err := queryIDServer(db, cfg, device, body, roomID)
if err != nil { if err != nil {
resErr := httputil.LogThenError(req, err) return
return &resErr
} }
if lookupRes.MXID == "" { if lookupRes.MXID == "" {
@ -108,28 +110,16 @@ func CheckAndProcessInvite(
// "m.room.third_party_invite" have to be emitted from the data in // "m.room.third_party_invite" have to be emitted from the data in
// storeInviteRes. // storeInviteRes.
err = emit3PIDInviteEvent(body, storeInviteRes, device, roomID, cfg, queryAPI, producer) err = emit3PIDInviteEvent(body, storeInviteRes, device, roomID, cfg, queryAPI, producer)
if err == events.ErrRoomNoExists { inviteStoredOnIDServer = err == nil
return &util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound(err.Error()),
}
} else if err != nil {
resErr := httputil.LogThenError(req, err)
return &resErr
}
// If everything went well, returns with an empty response. return
return &util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
} }
// A Matrix ID have been found: set it in the body request and let the process // A Matrix ID have been found: set it in the body request and let the process
// continue to create a "m.room.member" event with an "invite" membership // continue to create a "m.room.member" event with an "invite" membership
body.UserID = lookupRes.MXID body.UserID = lookupRes.MXID
return nil return
} }
// queryIDServer handles all the requests to the identity server, starting by // queryIDServer handles all the requests to the identity server, starting by
@ -142,9 +132,13 @@ func CheckAndProcessInvite(
// Returns a representation of the response for both cases. // Returns a representation of the response for both cases.
// Returns an error if a check or a request failed. // Returns an error if a check or a request failed.
func queryIDServer( func queryIDServer(
db *accounts.Database, cfg config.Dendrite, db *accounts.Database, cfg config.Dendrite, device *authtypes.Device,
device *authtypes.Device, body *MembershipRequest, roomID string, body *MembershipRequest, roomID string,
) (lookupRes *idServerLookupResponse, storeInviteRes *idServerStoreInviteResponse, err error) { ) (lookupRes *idServerLookupResponse, storeInviteRes *idServerStoreInviteResponse, err error) {
if err = isTrusted(body.IDServer, cfg); err != nil {
return
}
// Lookup the 3PID // Lookup the 3PID
lookupRes, err = queryIDServerLookup(body) lookupRes, err = queryIDServerLookup(body)
if err != nil { if err != nil {
@ -249,7 +243,6 @@ func queryIDServerStoreInvite(
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// TODO: Log the error supplied with the identity server?
errMsg := fmt.Sprintf("Identity server %s responded with a %d error code", body.IDServer, resp.StatusCode) errMsg := fmt.Sprintf("Identity server %s responded with a %d error code", body.IDServer, resp.StatusCode)
return nil, errors.New(errMsg) return nil, errors.New(errMsg)
} }
@ -275,7 +268,6 @@ func queryIDServerPubKey(idServerName string, keyID string) ([]byte, error) {
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
// TODO: Log the error supplied with the identity server?
errMsg := fmt.Sprintf("Couldn't retrieve key %s from server %s", keyID, idServerName) errMsg := fmt.Sprintf("Couldn't retrieve key %s from server %s", keyID, idServerName)
return nil, errors.New(errMsg) return nil, errors.New(errMsg)
} }
@ -297,7 +289,6 @@ func checkIDServerSignatures(body *MembershipRequest, res *idServerLookupRespons
return err return err
} }
// TODO: Check if the domain is part of a list of trusted ID servers
signatures, ok := res.Signatures[body.IDServer] signatures, ok := res.Signatures[body.IDServer]
if !ok { if !ok {
return errors.New("No signature for domain " + body.IDServer) return errors.New("No signature for domain " + body.IDServer)

View File

@ -22,6 +22,8 @@ import (
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"github.com/matrix-org/dendrite/common/config"
) )
// EmailAssociationRequest represents the request defined at https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register-email-requesttoken // EmailAssociationRequest represents the request defined at https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-register-email-requesttoken
@ -49,8 +51,10 @@ type Credentials struct {
// Returns the session's ID. // Returns the session's ID.
// Returns an error if there was a problem sending the request or decoding the // Returns an error if there was a problem sending the request or decoding the
// response, or if the identity server responded with a non-OK status. // response, or if the identity server responded with a non-OK status.
func CreateSession(req EmailAssociationRequest) (string, error) { func CreateSession(req EmailAssociationRequest, cfg config.Dendrite) (string, error) {
// TODO: Check if the ID server is trusted if err := isTrusted(req.IDServer, cfg); err != nil {
return "", err
}
// Create a session on the ID server // Create a session on the ID server
postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/validate/email/requestToken", req.IDServer) postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/validate/email/requestToken", req.IDServer)
@ -93,8 +97,11 @@ func CreateSession(req EmailAssociationRequest) (string, error) {
// identifier and its medium. // identifier and its medium.
// Returns an error if there was a problem sending the request or decoding the // Returns an error if there was a problem sending the request or decoding the
// response, or if the identity server responded with a non-OK status. // response, or if the identity server responded with a non-OK status.
func CheckAssociation(creds Credentials) (bool, string, string, error) { func CheckAssociation(creds Credentials, cfg config.Dendrite) (bool, string, string, error) {
// TODO: Check if the ID server is trusted if err := isTrusted(creds.IDServer, cfg); err != nil {
return false, "", "", err
}
url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/getValidated3pid?sid=%s&client_secret=%s", creds.IDServer, creds.SID, creds.Secret) url := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/getValidated3pid?sid=%s&client_secret=%s", creds.IDServer, creds.SID, creds.Secret)
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
@ -126,8 +133,11 @@ func CheckAssociation(creds Credentials) (bool, string, string, error) {
// identifier and a Matrix ID. // identifier and a Matrix ID.
// Returns an error if there was a problem sending the request or decoding the // Returns an error if there was a problem sending the request or decoding the
// response, or if the identity server responded with a non-OK status. // response, or if the identity server responded with a non-OK status.
func PublishAssociation(creds Credentials, userID string) error { func PublishAssociation(creds Credentials, userID string, cfg config.Dendrite) error {
// TODO: Check if the ID server is trusted if err := isTrusted(creds.IDServer, cfg); err != nil {
return err
}
postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/bind", creds.IDServer) postURL := fmt.Sprintf("https://%s/_matrix/identity/api/v1/3pid/bind", creds.IDServer)
data := url.Values{} data := url.Values{}
@ -154,3 +164,15 @@ func PublishAssociation(creds Credentials, userID string) error {
return nil return nil
} }
// isTrusted checks if a given identity server is part of the list of trusted
// identity servers in the configuration file.
// Returns an error if the server isn't trusted.
func isTrusted(idServer string, cfg config.Dendrite) error {
for _, server := range cfg.Matrix.TrustedIDServers {
if idServer == server {
return nil
}
}
return ErrNotTrusted
}

View File

@ -15,6 +15,7 @@
package writers package writers
import ( import (
"errors"
"net/http" "net/http"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
@ -32,6 +33,8 @@ import (
"github.com/matrix-org/util" "github.com/matrix-org/util"
) )
var errMissingUserID = errors.New("'user_id' must be supplied")
// SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite) // SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite)
// by building a m.room.member event then sending it to the room server // by building a m.room.member event then sending it to the room server
func SendMembership( func SendMembership(
@ -44,20 +47,78 @@ func SendMembership(
return *reqErr return *reqErr
} }
if res := threepid.CheckAndProcessInvite( inviteStored, err := threepid.CheckAndProcessInvite(
req, device, &body, cfg, queryAPI, accountDB, producer, membership, roomID, device, &body, cfg, queryAPI, accountDB, producer, membership, roomID,
); res != nil { )
return *res if err == threepid.ErrMissingParameter {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON(err.Error()),
}
} else if err == threepid.ErrNotTrusted {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.NotTrusted(body.IDServer),
}
} else if err == events.ErrRoomNoExists {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound(err.Error()),
}
} else if err != nil {
return httputil.LogThenError(req, err)
} }
stateKey, reason, reqErr := getMembershipStateKey(body, device, membership) // If an invite has been stored on an identity server, it means that a
if reqErr != nil { // m.room.third_party_invite event has been emitted and that we shouldn't
return *reqErr // emit a m.room.member one.
if inviteStored {
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
event, err := buildMembershipEvent(
body, accountDB, device, membership, roomID, cfg, queryAPI,
)
if err == errMissingUserID {
return util.JSONResponse{
Code: 400,
JSON: jsonerror.BadJSON(err.Error()),
}
} else if err == events.ErrRoomNoExists {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound(err.Error()),
}
} else if err != nil {
return httputil.LogThenError(req, err)
}
if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
}
func buildMembershipEvent(
body threepid.MembershipRequest, accountDB *accounts.Database,
device *authtypes.Device, membership string, roomID string, cfg config.Dendrite,
queryAPI api.RoomserverQueryAPI,
) (*gomatrixserverlib.Event, error) {
stateKey, reason, err := getMembershipStateKey(body, device, membership)
if err != nil {
return nil, err
} }
profile, err := loadProfile(stateKey, cfg, accountDB) profile, err := loadProfile(stateKey, cfg, accountDB)
if err != nil { if err != nil {
return httputil.LogThenError(req, err) return nil, err
} }
builder := gomatrixserverlib.EventBuilder{ builder := gomatrixserverlib.EventBuilder{
@ -80,27 +141,10 @@ func SendMembership(
} }
if err = builder.SetContent(content); err != nil { if err = builder.SetContent(content); err != nil {
return httputil.LogThenError(req, err) return nil, err
} }
event, err := events.BuildEvent(&builder, cfg, queryAPI, nil) return events.BuildEvent(&builder, cfg, queryAPI, nil)
if err == events.ErrRoomNoExists {
return util.JSONResponse{
Code: 404,
JSON: jsonerror.NotFound(err.Error()),
}
} else if err != nil {
return httputil.LogThenError(req, err)
}
if err := producer.SendEvents([]gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName); err != nil {
return httputil.LogThenError(req, err)
}
return util.JSONResponse{
Code: 200,
JSON: struct{}{},
}
} }
// loadProfile lookups the profile of a given user from the database and returns // loadProfile lookups the profile of a given user from the database and returns
@ -130,16 +174,13 @@ func loadProfile(userID string, cfg config.Dendrite, accountDB *accounts.Databas
// returns a JSONResponse with a corresponding error code and message. // returns a JSONResponse with a corresponding error code and message.
func getMembershipStateKey( func getMembershipStateKey(
body threepid.MembershipRequest, device *authtypes.Device, membership string, body threepid.MembershipRequest, device *authtypes.Device, membership string,
) (stateKey string, reason string, response *util.JSONResponse) { ) (stateKey string, reason string, err error) {
if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" { if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" {
// If we're in this case, the state key is contained in the request body, // If we're in this case, the state key is contained in the request body,
// possibly along with a reason (for "kick" and "ban") so we need to parse // possibly along with a reason (for "kick" and "ban") so we need to parse
// it // it
if body.UserID == "" { if body.UserID == "" {
response = &util.JSONResponse{ err = errMissingUserID
Code: 400,
JSON: jsonerror.BadJSON("'user_id' must be supplied."),
}
return return
} }

View File

@ -70,6 +70,10 @@ type Dendrite struct {
// by remote servers. // by remote servers.
// Defaults to 24 hours. // Defaults to 24 hours.
KeyValidityPeriod time.Duration `yaml:"key_validity_period"` KeyValidityPeriod time.Duration `yaml:"key_validity_period"`
// List of domains that the server will trust as identity servers to
// verify third-party identifiers.
// Defaults to an empty array.
TrustedIDServers []string `yaml:"trusted_third_party_id_servers"`
} `yaml:"matrix"` } `yaml:"matrix"`
// The configuration specific to the media repostitory. // The configuration specific to the media repostitory.
@ -273,6 +277,10 @@ func (config *Dendrite) setDefaults() {
config.Matrix.KeyValidityPeriod = 24 * time.Hour config.Matrix.KeyValidityPeriod = 24 * time.Hour
} }
if config.Matrix.TrustedIDServers == nil {
config.Matrix.TrustedIDServers = []string{}
}
if config.Media.MaxThumbnailGenerators == 0 { if config.Media.MaxThumbnailGenerators == 0 {
config.Media.MaxThumbnailGenerators = 10 config.Media.MaxThumbnailGenerators = 10
} }