diff --git a/currentstateserver/acls/acls.go b/currentstateserver/acls/acls.go new file mode 100644 index 00000000..12619f5f --- /dev/null +++ b/currentstateserver/acls/acls.go @@ -0,0 +1,156 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acls + +import ( + "context" + "encoding/json" + "fmt" + "net" + "regexp" + "strings" + "sync" + + "github.com/matrix-org/dendrite/currentstateserver/storage" + "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" +) + +type ServerACLs struct { + acls map[string]*serverACL // room ID -> ACL + aclsMutex sync.RWMutex // protects the above +} + +func NewServerACLs(db storage.Database) *ServerACLs { + ctx := context.TODO() + acls := &ServerACLs{ + acls: make(map[string]*serverACL), + } + // Look up all of the rooms that the current state server knows about. + rooms, err := db.GetKnownRooms(ctx) + if err != nil { + logrus.WithError(err).Fatalf("Failed to get known rooms") + } + // For each room, let's see if we have a server ACL state event. If we + // do then we'll process it into memory so that we have the regexes to + // hand. + for _, room := range rooms { + state, err := db.GetStateEvent(ctx, room, "m.room.server_acl", "") + if err != nil { + logrus.WithError(err).Errorf("Failed to get server ACLs for room %q", room) + continue + } + if state != nil { + acls.OnServerACLUpdate(&state.Event) + } + } + return acls +} + +type ServerACL struct { + Allowed []string `json:"allow"` + Denied []string `json:"deny"` + AllowIPLiterals bool `json:"allow_ip_literals"` +} + +type serverACL struct { + ServerACL + allowedRegexes []*regexp.Regexp + deniedRegexes []*regexp.Regexp +} + +func compileACLRegex(orig string) (*regexp.Regexp, error) { + escaped := regexp.QuoteMeta(orig) + escaped = strings.Replace(escaped, "\\?", ".", -1) + escaped = strings.Replace(escaped, "\\*", ".*", -1) + return regexp.Compile(escaped) +} + +func (s *ServerACLs) OnServerACLUpdate(state *gomatrixserverlib.Event) { + acls := &serverACL{} + if err := json.Unmarshal(state.Content(), &acls.ServerACL); err != nil { + logrus.WithError(err).Errorf("Failed to unmarshal state content for server ACLs") + return + } + // The spec calls only for * (zero or more chars) and ? (exactly one char) + // to be supported as wildcard components, so we will escape all of the regex + // special characters and then replace * and ? with their regex counterparts. + // https://matrix.org/docs/spec/client_server/r0.6.1#m-room-server-acl + for _, orig := range acls.Allowed { + if expr, err := compileACLRegex(orig); err != nil { + logrus.WithError(err).Errorf("Failed to compile allowed regex") + } else { + acls.allowedRegexes = append(acls.allowedRegexes, expr) + } + } + for _, orig := range acls.Denied { + if expr, err := compileACLRegex(orig); err != nil { + logrus.WithError(err).Errorf("Failed to compile denied regex") + } else { + acls.deniedRegexes = append(acls.deniedRegexes, expr) + } + } + logrus.WithFields(logrus.Fields{ + "allow_ip_literals": acls.AllowIPLiterals, + "num_allowed": len(acls.allowedRegexes), + "num_denied": len(acls.deniedRegexes), + }).Debugf("Updating server ACLs for %q", state.RoomID()) + s.aclsMutex.Lock() + defer s.aclsMutex.Unlock() + s.acls[state.RoomID()] = acls +} + +func (s *ServerACLs) IsServerBannedFromRoom(serverName gomatrixserverlib.ServerName, roomID string) bool { + s.aclsMutex.RLock() + // First of all check if we have an ACL for this room. If we don't then + // no servers are banned from the room. + acls, ok := s.acls[roomID] + if !ok { + s.aclsMutex.RUnlock() + return false + } + s.aclsMutex.RUnlock() + // Split the host and port apart. This is because the spec calls on us to + // validate the hostname only in cases where the port is also present. + if serverNameOnly, _, err := net.SplitHostPort(string(serverName)); err == nil { + serverName = gomatrixserverlib.ServerName(serverNameOnly) + } + // Check if the hostname is an IPv4 or IPv6 literal. We cheat here by adding + // a /0 prefix length just to trick ParseCIDR into working. If we find that + // the server is an IP literal and we don't allow those then stop straight + // away. + if _, _, err := net.ParseCIDR(fmt.Sprintf("%s/0", serverName)); err == nil { + if !acls.AllowIPLiterals { + return true + } + } + // Check if the hostname matches one of the denied regexes. If it does then + // the server is banned from the room. + for _, expr := range acls.deniedRegexes { + if expr.MatchString(string(serverName)) { + return true + } + } + // Check if the hostname matches one of the allowed regexes. If it does then + // the server is NOT banned from the room. + for _, expr := range acls.allowedRegexes { + if expr.MatchString(string(serverName)) { + return false + } + } + // If we've got to this point then we haven't matched any regexes or an IP + // hostname if disallowed. The spec calls for default-deny here. + return true +} diff --git a/currentstateserver/acls/acls_test.go b/currentstateserver/acls/acls_test.go new file mode 100644 index 00000000..9fb6a558 --- /dev/null +++ b/currentstateserver/acls/acls_test.go @@ -0,0 +1,105 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package acls + +import ( + "regexp" + "testing" +) + +func TestOpenACLsWithBlacklist(t *testing.T) { + roomID := "!test:test.com" + allowRegex, err := compileACLRegex("*") + if err != nil { + t.Fatalf(err.Error()) + } + denyRegex, err := compileACLRegex("foo.com") + if err != nil { + t.Fatalf(err.Error()) + } + + acls := ServerACLs{ + acls: make(map[string]*serverACL), + } + + acls.acls[roomID] = &serverACL{ + ServerACL: ServerACL{ + AllowIPLiterals: true, + }, + allowedRegexes: []*regexp.Regexp{allowRegex}, + deniedRegexes: []*regexp.Regexp{denyRegex}, + } + + if acls.IsServerBannedFromRoom("1.2.3.4", roomID) { + t.Fatal("Expected 1.2.3.4 to be allowed but wasn't") + } + if acls.IsServerBannedFromRoom("1.2.3.4:2345", roomID) { + t.Fatal("Expected 1.2.3.4:2345 to be allowed but wasn't") + } + if !acls.IsServerBannedFromRoom("foo.com", roomID) { + t.Fatal("Expected foo.com to be banned but wasn't") + } + if !acls.IsServerBannedFromRoom("foo.com:3456", roomID) { + t.Fatal("Expected foo.com:3456 to be banned but wasn't") + } + if acls.IsServerBannedFromRoom("bar.com", roomID) { + t.Fatal("Expected bar.com to be allowed but wasn't") + } + if acls.IsServerBannedFromRoom("bar.com:4567", roomID) { + t.Fatal("Expected bar.com:4567 to be allowed but wasn't") + } +} + +func TestDefaultACLsWithWhitelist(t *testing.T) { + roomID := "!test:test.com" + allowRegex, err := compileACLRegex("foo.com") + if err != nil { + t.Fatalf(err.Error()) + } + + acls := ServerACLs{ + acls: make(map[string]*serverACL), + } + + acls.acls[roomID] = &serverACL{ + ServerACL: ServerACL{ + AllowIPLiterals: false, + }, + allowedRegexes: []*regexp.Regexp{allowRegex}, + deniedRegexes: []*regexp.Regexp{}, + } + + if !acls.IsServerBannedFromRoom("1.2.3.4", roomID) { + t.Fatal("Expected 1.2.3.4 to be banned but wasn't") + } + if !acls.IsServerBannedFromRoom("1.2.3.4:2345", roomID) { + t.Fatal("Expected 1.2.3.4:2345 to be banned but wasn't") + } + if acls.IsServerBannedFromRoom("foo.com", roomID) { + t.Fatal("Expected foo.com to be allowed but wasn't") + } + if acls.IsServerBannedFromRoom("foo.com:3456", roomID) { + t.Fatal("Expected foo.com:3456 to be allowed but wasn't") + } + if !acls.IsServerBannedFromRoom("bar.com", roomID) { + t.Fatal("Expected bar.com to be allowed but wasn't") + } + if !acls.IsServerBannedFromRoom("baz.com", roomID) { + t.Fatal("Expected baz.com to be allowed but wasn't") + } + if !acls.IsServerBannedFromRoom("qux.com:4567", roomID) { + t.Fatal("Expected qux.com:4567 to be allowed but wasn't") + } +} diff --git a/currentstateserver/api/api.go b/currentstateserver/api/api.go index 4ebe2968..5ae57bb9 100644 --- a/currentstateserver/api/api.go +++ b/currentstateserver/api/api.go @@ -36,6 +36,8 @@ type CurrentStateInternalAPI interface { QuerySharedUsers(ctx context.Context, req *QuerySharedUsersRequest, res *QuerySharedUsersResponse) error // QueryKnownUsers returns a list of users that we know about from our joined rooms. QueryKnownUsers(ctx context.Context, req *QueryKnownUsersRequest, res *QueryKnownUsersResponse) error + // QueryServerBannedFromRoom returns whether a server is banned from a room by server ACLs. + QueryServerBannedFromRoom(ctx context.Context, req *QueryServerBannedFromRoomRequest, res *QueryServerBannedFromRoomResponse) error } type QuerySharedUsersRequest struct { @@ -101,6 +103,15 @@ type QueryKnownUsersResponse struct { Users []authtypes.FullyQualifiedProfile `json:"profiles"` } +type QueryServerBannedFromRoomRequest struct { + ServerName gomatrixserverlib.ServerName `json:"server_name"` + RoomID string `json:"room_id"` +} + +type QueryServerBannedFromRoomResponse struct { + Banned bool `json:"banned"` +} + // MarshalJSON stringifies the StateKeyTuple keys so they can be sent over the wire in HTTP API mode. func (r *QueryCurrentStateResponse) MarshalJSON() ([]byte, error) { se := make(map[string]*gomatrixserverlib.HeaderedEvent, len(r.StateEvents)) diff --git a/currentstateserver/api/wrapper.go b/currentstateserver/api/wrapper.go index c88740c0..317fea43 100644 --- a/currentstateserver/api/wrapper.go +++ b/currentstateserver/api/wrapper.go @@ -1,3 +1,17 @@ +// Copyright 2020 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package api import ( @@ -25,6 +39,20 @@ func GetEvent(ctx context.Context, stateAPI CurrentStateInternalAPI, roomID stri return nil } +// IsServerBannedFromRoom returns whether the server is banned from a room by server ACLs. +func IsServerBannedFromRoom(ctx context.Context, stateAPI CurrentStateInternalAPI, roomID string, serverName gomatrixserverlib.ServerName) bool { + req := &QueryServerBannedFromRoomRequest{ + ServerName: serverName, + RoomID: roomID, + } + res := &QueryServerBannedFromRoomResponse{} + if err := stateAPI.QueryServerBannedFromRoom(ctx, req, res); err != nil { + util.GetLogger(ctx).WithError(err).Error("Failed to QueryServerBannedFromRoom") + return true + } + return res.Banned +} + // PopulatePublicRooms extracts PublicRoom information for all the provided room IDs. The IDs are not checked to see if they are visible in the // published room directory. // due to lots of switches diff --git a/currentstateserver/consumers/roomserver.go b/currentstateserver/consumers/roomserver.go index 81878c6d..492cf621 100644 --- a/currentstateserver/consumers/roomserver.go +++ b/currentstateserver/consumers/roomserver.go @@ -19,6 +19,7 @@ import ( "encoding/json" "github.com/Shopify/sarama" + "github.com/matrix-org/dendrite/currentstateserver/acls" "github.com/matrix-org/dendrite/currentstateserver/storage" "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/roomserver/api" @@ -30,9 +31,10 @@ import ( type OutputRoomEventConsumer struct { rsConsumer *internal.ContinualConsumer db storage.Database + acls *acls.ServerACLs } -func NewOutputRoomEventConsumer(topicName string, kafkaConsumer sarama.Consumer, store storage.Database) *OutputRoomEventConsumer { +func NewOutputRoomEventConsumer(topicName string, kafkaConsumer sarama.Consumer, store storage.Database, acls *acls.ServerACLs) *OutputRoomEventConsumer { consumer := &internal.ContinualConsumer{ Topic: topicName, Consumer: kafkaConsumer, @@ -41,6 +43,7 @@ func NewOutputRoomEventConsumer(topicName string, kafkaConsumer sarama.Consumer, s := &OutputRoomEventConsumer{ rsConsumer: consumer, db: store, + acls: acls, } consumer.ProcessMessage = s.onMessage @@ -76,6 +79,10 @@ func (c *OutputRoomEventConsumer) onNewRoomEvent( ) error { ev := msg.Event + if ev.Type() == "m.room.server_acl" && ev.StateKeyEquals("") { + defer c.acls.OnServerACLUpdate(&ev.Event) + } + addsStateEvents := msg.AddsState() ev, err := c.updateStateEvent(ev) diff --git a/currentstateserver/currentstateserver.go b/currentstateserver/currentstateserver.go index f0dd4b88..196434eb 100644 --- a/currentstateserver/currentstateserver.go +++ b/currentstateserver/currentstateserver.go @@ -17,6 +17,7 @@ package currentstateserver import ( "github.com/Shopify/sarama" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/currentstateserver/acls" "github.com/matrix-org/dendrite/currentstateserver/api" "github.com/matrix-org/dendrite/currentstateserver/consumers" "github.com/matrix-org/dendrite/currentstateserver/internal" @@ -39,13 +40,15 @@ func NewInternalAPI(cfg *config.CurrentStateServer, consumer sarama.Consumer) ap if err != nil { logrus.WithError(err).Panicf("failed to open database") } + serverACLs := acls.NewServerACLs(csDB) roomConsumer := consumers.NewOutputRoomEventConsumer( - cfg.Matrix.Kafka.TopicFor(config.TopicOutputRoomEvent), consumer, csDB, + cfg.Matrix.Kafka.TopicFor(config.TopicOutputRoomEvent), consumer, csDB, serverACLs, ) if err = roomConsumer.Start(); err != nil { logrus.WithError(err).Panicf("failed to start room server consumer") } return &internal.CurrentStateInternalAPI{ - DB: csDB, + DB: csDB, + ServerACLs: serverACLs, } } diff --git a/currentstateserver/internal/api.go b/currentstateserver/internal/api.go index dc255412..0a7e025e 100644 --- a/currentstateserver/internal/api.go +++ b/currentstateserver/internal/api.go @@ -16,15 +16,18 @@ package internal import ( "context" + "errors" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/currentstateserver/acls" "github.com/matrix-org/dendrite/currentstateserver/api" "github.com/matrix-org/dendrite/currentstateserver/storage" "github.com/matrix-org/gomatrixserverlib" ) type CurrentStateInternalAPI struct { - DB storage.Database + DB storage.Database + ServerACLs *acls.ServerACLs } func (a *CurrentStateInternalAPI) QueryCurrentState(ctx context.Context, req *api.QueryCurrentStateRequest, res *api.QueryCurrentStateResponse) error { @@ -112,3 +115,11 @@ func (a *CurrentStateInternalAPI) QuerySharedUsers(ctx context.Context, req *api res.UserIDsToCount = users return nil } + +func (a *CurrentStateInternalAPI) QueryServerBannedFromRoom(ctx context.Context, req *api.QueryServerBannedFromRoomRequest, res *api.QueryServerBannedFromRoomResponse) error { + if a.ServerACLs == nil { + return errors.New("no server ACL tracking") + } + res.Banned = a.ServerACLs.IsServerBannedFromRoom(req.ServerName, req.RoomID) + return nil +} diff --git a/currentstateserver/inthttp/client.go b/currentstateserver/inthttp/client.go index 37d289ea..6d54f548 100644 --- a/currentstateserver/inthttp/client.go +++ b/currentstateserver/inthttp/client.go @@ -26,11 +26,12 @@ import ( // HTTP paths for the internal HTTP APIs const ( - QueryCurrentStatePath = "/currentstateserver/queryCurrentState" - QueryRoomsForUserPath = "/currentstateserver/queryRoomsForUser" - QueryBulkStateContentPath = "/currentstateserver/queryBulkStateContent" - QuerySharedUsersPath = "/currentstateserver/querySharedUsers" - QueryKnownUsersPath = "/currentstateserver/queryKnownUsers" + QueryCurrentStatePath = "/currentstateserver/queryCurrentState" + QueryRoomsForUserPath = "/currentstateserver/queryRoomsForUser" + QueryBulkStateContentPath = "/currentstateserver/queryBulkStateContent" + QuerySharedUsersPath = "/currentstateserver/querySharedUsers" + QueryKnownUsersPath = "/currentstateserver/queryKnownUsers" + QueryServerBannedFromRoomPath = "/currentstateserver/queryServerBannedFromRoom" ) // NewCurrentStateAPIClient creates a CurrentStateInternalAPI implemented by talking to a HTTP POST API. @@ -108,3 +109,13 @@ func (h *httpCurrentStateInternalAPI) QueryKnownUsers( apiURL := h.apiURL + QueryKnownUsersPath return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res) } + +func (h *httpCurrentStateInternalAPI) QueryServerBannedFromRoom( + ctx context.Context, req *api.QueryServerBannedFromRoomRequest, res *api.QueryServerBannedFromRoomResponse, +) error { + span, ctx := opentracing.StartSpanFromContext(ctx, "QueryServerBannedFromRoom") + defer span.Finish() + + apiURL := h.apiURL + QueryServerBannedFromRoomPath + return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res) +} diff --git a/currentstateserver/inthttp/server.go b/currentstateserver/inthttp/server.go index aee900e0..1cf8cd2a 100644 --- a/currentstateserver/inthttp/server.go +++ b/currentstateserver/inthttp/server.go @@ -90,4 +90,17 @@ func AddRoutes(internalAPIMux *mux.Router, intAPI api.CurrentStateInternalAPI) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + internalAPIMux.Handle(QueryServerBannedFromRoomPath, + httputil.MakeInternalAPI("queryServerBannedFromRoom", func(req *http.Request) util.JSONResponse { + request := api.QueryServerBannedFromRoomRequest{} + response := api.QueryServerBannedFromRoomResponse{} + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if err := intAPI.QueryServerBannedFromRoom(req.Context(), &request, &response); err != nil { + return util.ErrorResponse(err) + } + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) } diff --git a/currentstateserver/storage/interface.go b/currentstateserver/storage/interface.go index 5a754b9e..7c1c83b7 100644 --- a/currentstateserver/storage/interface.go +++ b/currentstateserver/storage/interface.go @@ -41,4 +41,6 @@ type Database interface { JoinedUsersSetInRooms(ctx context.Context, roomIDs []string) (map[string]int, error) // GetKnownUsers searches all users that userID knows about. GetKnownUsers(ctx context.Context, userID, searchString string, limit int) ([]string, error) + // GetKnownRooms returns a list of all rooms we know about. + GetKnownRooms(ctx context.Context) ([]string, error) } diff --git a/currentstateserver/storage/postgres/current_room_state_table.go b/currentstateserver/storage/postgres/current_room_state_table.go index e29fa703..7d205671 100644 --- a/currentstateserver/storage/postgres/current_room_state_table.go +++ b/currentstateserver/storage/postgres/current_room_state_table.go @@ -82,6 +82,9 @@ const selectJoinedUsersSetForRoomsSQL = "" + "SELECT state_key, COUNT(room_id) FROM currentstate_current_room_state WHERE room_id = ANY($1) AND" + " type = 'm.room.member' and content_value = 'join' GROUP BY state_key" +const selectKnownRoomsSQL = "" + + "SELECT DISTINCT room_id FROM currentstate_current_room_state" + // selectKnownUsersSQL uses a sub-select statement here to find rooms that the user is // joined to. Since this information is used to populate the user directory, we will // only return users that the user would ordinarily be able to see anyway. @@ -99,6 +102,7 @@ type currentRoomStateStatements struct { selectBulkStateContentStmt *sql.Stmt selectBulkStateContentWildStmt *sql.Stmt selectJoinedUsersSetForRoomsStmt *sql.Stmt + selectKnownRoomsStmt *sql.Stmt selectKnownUsersStmt *sql.Stmt } @@ -132,6 +136,9 @@ func NewPostgresCurrentRoomStateTable(db *sql.DB) (tables.CurrentRoomState, erro if s.selectJoinedUsersSetForRoomsStmt, err = db.Prepare(selectJoinedUsersSetForRoomsSQL); err != nil { return nil, err } + if s.selectKnownRoomsStmt, err = db.Prepare(selectKnownRoomsSQL); err != nil { + return nil, err + } if s.selectKnownUsersStmt, err = db.Prepare(selectKnownUsersSQL); err != nil { return nil, err } @@ -325,3 +332,20 @@ func (s *currentRoomStateStatements) SelectKnownUsers(ctx context.Context, userI } return result, rows.Err() } + +func (s *currentRoomStateStatements) SelectKnownRooms(ctx context.Context) ([]string, error) { + rows, err := s.selectKnownRoomsStmt.QueryContext(ctx) + if err != nil { + return nil, err + } + result := []string{} + defer internal.CloseAndLogIfError(ctx, rows, "SelectKnownRooms: rows.close() failed") + for rows.Next() { + var roomID string + if err := rows.Scan(&roomID); err != nil { + return nil, err + } + result = append(result, roomID) + } + return result, rows.Err() +} diff --git a/currentstateserver/storage/shared/storage.go b/currentstateserver/storage/shared/storage.go index bd4329a7..46ef9e6c 100644 --- a/currentstateserver/storage/shared/storage.go +++ b/currentstateserver/storage/shared/storage.go @@ -93,3 +93,7 @@ func (d *Database) JoinedUsersSetInRooms(ctx context.Context, roomIDs []string) func (d *Database) GetKnownUsers(ctx context.Context, userID, searchString string, limit int) ([]string, error) { return d.CurrentRoomState.SelectKnownUsers(ctx, userID, searchString, limit) } + +func (d *Database) GetKnownRooms(ctx context.Context) ([]string, error) { + return d.CurrentRoomState.SelectKnownRooms(ctx) +} diff --git a/currentstateserver/storage/sqlite3/current_room_state_table.go b/currentstateserver/storage/sqlite3/current_room_state_table.go index a2989364..5c7e8b0a 100644 --- a/currentstateserver/storage/sqlite3/current_room_state_table.go +++ b/currentstateserver/storage/sqlite3/current_room_state_table.go @@ -70,6 +70,9 @@ const selectBulkStateContentWildSQL = "" + const selectJoinedUsersSetForRoomsSQL = "" + "SELECT state_key, COUNT(room_id) FROM currentstate_current_room_state WHERE room_id IN ($1) AND type = 'm.room.member' and content_value = 'join' GROUP BY state_key" +const selectKnownRoomsSQL = "" + + "SELECT DISTINCT room_id FROM currentstate_current_room_state" + // selectKnownUsersSQL uses a sub-select statement here to find rooms that the user is // joined to. Since this information is used to populate the user directory, we will // only return users that the user would ordinarily be able to see anyway. @@ -86,6 +89,7 @@ type currentRoomStateStatements struct { selectRoomIDsWithMembershipStmt *sql.Stmt selectStateEventStmt *sql.Stmt selectJoinedUsersSetForRoomsStmt *sql.Stmt + selectKnownRoomsStmt *sql.Stmt selectKnownUsersStmt *sql.Stmt } @@ -113,6 +117,9 @@ func NewSqliteCurrentRoomStateTable(db *sql.DB) (tables.CurrentRoomState, error) if s.selectJoinedUsersSetForRoomsStmt, err = db.Prepare(selectJoinedUsersSetForRoomsSQL); err != nil { return nil, err } + if s.selectKnownRoomsStmt, err = db.Prepare(selectKnownRoomsSQL); err != nil { + return nil, err + } if s.selectKnownUsersStmt, err = db.Prepare(selectKnownUsersSQL); err != nil { return nil, err } @@ -345,3 +352,20 @@ func (s *currentRoomStateStatements) SelectKnownUsers(ctx context.Context, userI } return result, rows.Err() } + +func (s *currentRoomStateStatements) SelectKnownRooms(ctx context.Context) ([]string, error) { + rows, err := s.selectKnownRoomsStmt.QueryContext(ctx) + if err != nil { + return nil, err + } + result := []string{} + defer internal.CloseAndLogIfError(ctx, rows, "SelectKnownRooms: rows.close() failed") + for rows.Next() { + var roomID string + if err := rows.Scan(&roomID); err != nil { + return nil, err + } + result = append(result, roomID) + } + return result, rows.Err() +} diff --git a/currentstateserver/storage/tables/interface.go b/currentstateserver/storage/tables/interface.go index 6290e7b3..cc5c6e80 100644 --- a/currentstateserver/storage/tables/interface.go +++ b/currentstateserver/storage/tables/interface.go @@ -41,6 +41,8 @@ type CurrentRoomState interface { SelectJoinedUsersSetForRooms(ctx context.Context, roomIDs []string) (map[string]int, error) // SelectKnownUsers searches all users that userID knows about. SelectKnownUsers(ctx context.Context, userID, searchString string, limit int) ([]string, error) + // SelectKnownRooms returns all rooms that we know about. + SelectKnownRooms(ctx context.Context) ([]string, error) } // StrippedEvent represents a stripped event for returning extracted content values. diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index e9a8e40a..e838e8e3 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -41,7 +41,6 @@ func AddPublicRoutes( stateAPI currentstateAPI.CurrentStateInternalAPI, keyAPI keyserverAPI.KeyInternalAPI, ) { - routing.Setup( router, cfg, rsAPI, eduAPI, federationSenderAPI, keyRing, diff --git a/federationapi/routing/routing.go b/federationapi/routing/routing.go index 88fd8757..027827fa 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -82,7 +82,7 @@ func Setup( func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return Send( httpReq, request, gomatrixserverlib.TransactionID(vars["txnID"]), - cfg, rsAPI, eduAPI, keyAPI, keys, federation, + cfg, rsAPI, eduAPI, keyAPI, stateAPI, keys, federation, ) }, )).Methods(http.MethodPut, http.MethodOptions) @@ -90,6 +90,12 @@ func Setup( v1fedmux.Handle("/invite/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_invite", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } res := InviteV1( httpReq, request, vars["roomID"], vars["eventID"], cfg, rsAPI, keys, @@ -106,6 +112,12 @@ func Setup( v2fedmux.Handle("/invite/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_invite", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return InviteV2( httpReq, request, vars["roomID"], vars["eventID"], cfg, rsAPI, keys, @@ -140,6 +152,12 @@ func Setup( v1fedmux.Handle("/state/{roomID}", httputil.MakeFedAPI( "federation_get_state", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return GetState( httpReq.Context(), request, rsAPI, vars["roomID"], ) @@ -149,6 +167,12 @@ func Setup( v1fedmux.Handle("/state_ids/{roomID}", httputil.MakeFedAPI( "federation_get_state_ids", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return GetStateIDs( httpReq.Context(), request, rsAPI, vars["roomID"], ) @@ -158,6 +182,12 @@ func Setup( v1fedmux.Handle("/event_auth/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_get_event_auth", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return GetEventAuth( httpReq.Context(), request, rsAPI, vars["roomID"], vars["eventID"], ) @@ -194,6 +224,12 @@ func Setup( v1fedmux.Handle("/make_join/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_make_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } roomID := vars["roomID"] eventID := vars["eventID"] queryVars := httpReq.URL.Query() @@ -219,6 +255,12 @@ func Setup( v1fedmux.Handle("/send_join/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_send_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } roomID := vars["roomID"] eventID := vars["eventID"] res := SendJoin( @@ -245,6 +287,12 @@ func Setup( v2fedmux.Handle("/send_join/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_send_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } roomID := vars["roomID"] eventID := vars["eventID"] return SendJoin( @@ -256,6 +304,12 @@ func Setup( v1fedmux.Handle("/make_leave/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_make_leave", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } roomID := vars["roomID"] eventID := vars["eventID"] return MakeLeave( @@ -264,9 +318,47 @@ func Setup( }, )).Methods(http.MethodGet) + v1fedmux.Handle("/send_leave/{roomID}/{eventID}", httputil.MakeFedAPI( + "federation_send_leave", cfg.Matrix.ServerName, keys, wakeup, + func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } + roomID := vars["roomID"] + eventID := vars["eventID"] + res := SendLeave( + httpReq, request, cfg, rsAPI, keys, roomID, eventID, + ) + // not all responses get wrapped in [code, body] + var body interface{} + body = []interface{}{ + res.Code, res.JSON, + } + jerr, ok := res.JSON.(*jsonerror.MatrixError) + if ok { + body = jerr + } + + return util.JSONResponse{ + Headers: res.Headers, + Code: res.Code, + JSON: body, + } + }, + )).Methods(http.MethodPut) + v2fedmux.Handle("/send_leave/{roomID}/{eventID}", httputil.MakeFedAPI( "federation_send_leave", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } roomID := vars["roomID"] eventID := vars["eventID"] return SendLeave( @@ -285,6 +377,12 @@ func Setup( v1fedmux.Handle("/get_missing_events/{roomID}", httputil.MakeFedAPI( "federation_get_missing_events", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return GetMissingEvents(httpReq, request, rsAPI, vars["roomID"]) }, )).Methods(http.MethodPost) @@ -292,6 +390,12 @@ func Setup( v1fedmux.Handle("/backfill/{roomID}", httputil.MakeFedAPI( "federation_backfill", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { + if currentstateAPI.IsServerBannedFromRoom(httpReq.Context(), stateAPI, vars["roomID"], request.Origin()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("Forbidden by server ACLs"), + } + } return Backfill(httpReq, request, rsAPI, vars["roomID"], cfg) }, )).Methods(http.MethodGet) diff --git a/federationapi/routing/send.go b/federationapi/routing/send.go index d1aa728c..cad77921 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -21,6 +21,7 @@ import ( "net/http" "github.com/matrix-org/dendrite/clientapi/jsonerror" + currentstateAPI "github.com/matrix-org/dendrite/currentstateserver/api" eduserverAPI "github.com/matrix-org/dendrite/eduserver/api" "github.com/matrix-org/dendrite/internal/config" keyapi "github.com/matrix-org/dendrite/keyserver/api" @@ -39,6 +40,7 @@ func Send( rsAPI api.RoomserverInternalAPI, eduAPI eduserverAPI.EDUServerInputAPI, keyAPI keyapi.KeyInternalAPI, + stateAPI currentstateAPI.CurrentStateInternalAPI, keys gomatrixserverlib.JSONVerifier, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { @@ -46,6 +48,7 @@ func Send( context: httpReq.Context(), rsAPI: rsAPI, eduAPI: eduAPI, + stateAPI: stateAPI, keys: keys, federation: federation, haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent), @@ -104,6 +107,7 @@ type txnReq struct { rsAPI api.RoomserverInternalAPI eduAPI eduserverAPI.EDUServerInputAPI keyAPI keyapi.KeyInternalAPI + stateAPI currentstateAPI.CurrentStateInternalAPI keys gomatrixserverlib.JSONVerifier federation txnFederationClient // local cache of events for auth checks, etc - this may include events @@ -164,6 +168,12 @@ func (t *txnReq) processTransaction() (*gomatrixserverlib.RespSend, *util.JSONRe util.GetLogger(t.context).WithError(err).Warnf("Transaction: Failed to parse event JSON of event %s", string(pdu)) continue } + if currentstateAPI.IsServerBannedFromRoom(t.context, t.stateAPI, event.RoomID(), t.Origin) { + results[event.EventID()] = gomatrixserverlib.PDUResult{ + Error: "Forbidden by server ACLs", + } + continue + } if err = gomatrixserverlib.VerifyAllEventSignatures(t.context, []gomatrixserverlib.Event{event}, t.keys); err != nil { util.GetLogger(t.context).WithError(err).Warnf("Transaction: Couldn't validate signature of event %q", event.EventID()) results[event.EventID()] = gomatrixserverlib.PDUResult{ diff --git a/federationapi/routing/send_test.go b/federationapi/routing/send_test.go index bfbdaa5f..5d3ed230 100644 --- a/federationapi/routing/send_test.go +++ b/federationapi/routing/send_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + currentstateAPI "github.com/matrix-org/dendrite/currentstateserver/api" eduAPI "github.com/matrix-org/dendrite/eduserver/api" fsAPI "github.com/matrix-org/dendrite/federationsender/api" "github.com/matrix-org/dendrite/internal/test" @@ -294,6 +295,33 @@ func (t *testRoomserverAPI) RemoveRoomAlias( return fmt.Errorf("not implemented") } +type testStateAPI struct { +} + +func (t *testStateAPI) QueryCurrentState(ctx context.Context, req *currentstateAPI.QueryCurrentStateRequest, res *currentstateAPI.QueryCurrentStateResponse) error { + return nil +} + +func (t *testStateAPI) QueryRoomsForUser(ctx context.Context, req *currentstateAPI.QueryRoomsForUserRequest, res *currentstateAPI.QueryRoomsForUserResponse) error { + return fmt.Errorf("not implemented") +} + +func (t *testStateAPI) QueryBulkStateContent(ctx context.Context, req *currentstateAPI.QueryBulkStateContentRequest, res *currentstateAPI.QueryBulkStateContentResponse) error { + return fmt.Errorf("not implemented") +} + +func (t *testStateAPI) QuerySharedUsers(ctx context.Context, req *currentstateAPI.QuerySharedUsersRequest, res *currentstateAPI.QuerySharedUsersResponse) error { + return fmt.Errorf("not implemented") +} + +func (t *testStateAPI) QueryKnownUsers(ctx context.Context, req *currentstateAPI.QueryKnownUsersRequest, res *currentstateAPI.QueryKnownUsersResponse) error { + return fmt.Errorf("not implemented") +} + +func (t *testStateAPI) QueryServerBannedFromRoom(ctx context.Context, req *currentstateAPI.QueryServerBannedFromRoomRequest, res *currentstateAPI.QueryServerBannedFromRoomResponse) error { + return nil +} + type txnFedClient struct { state map[string]gomatrixserverlib.RespState // event_id to response stateIDs map[string]gomatrixserverlib.RespStateIDs // event_id to response @@ -338,11 +366,12 @@ func (c *txnFedClient) LookupMissingEvents(ctx context.Context, s gomatrixserver return c.getMissingEvents(missing) } -func mustCreateTransaction(rsAPI api.RoomserverInternalAPI, fedClient txnFederationClient, pdus []json.RawMessage) *txnReq { +func mustCreateTransaction(rsAPI api.RoomserverInternalAPI, stateAPI currentstateAPI.CurrentStateInternalAPI, fedClient txnFederationClient, pdus []json.RawMessage) *txnReq { t := &txnReq{ context: context.Background(), rsAPI: rsAPI, eduAPI: &testEDUProducer{}, + stateAPI: stateAPI, keys: &test.NopJSONVerifier{}, federation: fedClient, haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent), @@ -422,10 +451,11 @@ func TestBasicTransaction(t *testing.T) { } }, } + stateAPI := &testStateAPI{} pdus := []json.RawMessage{ testData[len(testData)-1], // a message event } - txn := mustCreateTransaction(rsAPI, &txnFedClient{}, pdus) + txn := mustCreateTransaction(rsAPI, stateAPI, &txnFedClient{}, pdus) mustProcessTransaction(t, txn, nil) assertInputRoomEvents(t, rsAPI.inputRoomEvents, []gomatrixserverlib.HeaderedEvent{testEvents[len(testEvents)-1]}) } @@ -444,10 +474,11 @@ func TestTransactionFailAuthChecks(t *testing.T) { } }, } + stateAPI := &testStateAPI{} pdus := []json.RawMessage{ testData[len(testData)-1], // a message event } - txn := mustCreateTransaction(rsAPI, &txnFedClient{}, pdus) + txn := mustCreateTransaction(rsAPI, stateAPI, &txnFedClient{}, pdus) mustProcessTransaction(t, txn, []string{ // expect the event to have an error testEvents[len(testEvents)-1].EventID(), @@ -502,6 +533,8 @@ func TestTransactionFetchMissingPrevEvents(t *testing.T) { }, } + stateAPI := &testStateAPI{} + cli := &txnFedClient{ getMissingEvents: func(missing gomatrixserverlib.MissingEvents) (res gomatrixserverlib.RespMissingEvents, err error) { if !reflect.DeepEqual(missing.EarliestEvents, []string{haveEvent.EventID()}) { @@ -521,7 +554,7 @@ func TestTransactionFetchMissingPrevEvents(t *testing.T) { pdus := []json.RawMessage{ inputEvent.JSON(), } - txn := mustCreateTransaction(rsAPI, cli, pdus) + txn := mustCreateTransaction(rsAPI, stateAPI, cli, pdus) mustProcessTransaction(t, txn, nil) assertInputRoomEvents(t, rsAPI.inputRoomEvents, []gomatrixserverlib.HeaderedEvent{prevEvent, inputEvent}) } @@ -671,10 +704,12 @@ func TestTransactionFetchMissingStateByStateIDs(t *testing.T) { }, } + stateAPI := &testStateAPI{} + pdus := []json.RawMessage{ eventD.JSON(), } - txn := mustCreateTransaction(rsAPI, cli, pdus) + txn := mustCreateTransaction(rsAPI, stateAPI, cli, pdus) mustProcessTransaction(t, txn, nil) assertInputRoomEvents(t, rsAPI.inputRoomEvents, []gomatrixserverlib.HeaderedEvent{eventB, eventC, eventD}) } diff --git a/syncapi/internal/keychange_test.go b/syncapi/internal/keychange_test.go index 6765fa65..baf60ef0 100644 --- a/syncapi/internal/keychange_test.go +++ b/syncapi/internal/keychange_test.go @@ -114,6 +114,10 @@ func (s *mockCurrentStateAPI) QuerySharedUsers(ctx context.Context, req *api.Que return nil } +func (t *mockCurrentStateAPI) QueryServerBannedFromRoom(ctx context.Context, req *api.QueryServerBannedFromRoomRequest, res *api.QueryServerBannedFromRoomResponse) error { + return nil +} + type wantCatchup struct { hasNew bool changed []string diff --git a/sytest-whitelist b/sytest-whitelist index 875cc092..e92e883c 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -436,3 +436,15 @@ User directory correctly update on display name change User in shared private room does appear in user directory User in dir while user still shares private rooms Can get 'm.room.name' state for a departed room (SPEC-216) +Banned servers cannot send events +Banned servers cannot /make_join +Banned servers cannot /send_join +Banned servers cannot /make_leave +Banned servers cannot /send_leave +Banned servers cannot /invite +Banned servers cannot get room state +Banned servers cannot /event_auth +Banned servers cannot get missing events +Banned servers cannot get room state ids +Banned servers cannot backfill +Inbound /v1/send_leave rejects leaves from other servers