191 lines
6.5 KiB
Go
191 lines
6.5 KiB
Go
// Copyright 2024 New Vector Ltd.
|
|
// Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
// Please see LICENSE files in the repository root for full details.
|
|
|
|
package acls
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/element-hq/dendrite/roomserver/storage/tables"
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const MRoomServerACL = "m.room.server_acl"
|
|
|
|
type ServerACLDatabase interface {
|
|
// RoomsWithACLs returns all room IDs for rooms with ACLs
|
|
RoomsWithACLs(ctx context.Context) ([]string, error)
|
|
|
|
// GetBulkStateContent returns all state events which match a given room ID and a given state key tuple. Both must be satisfied for a match.
|
|
// If a tuple has the StateKey of '*' and allowWildcards=true then all state events with the EventType should be returned.
|
|
GetBulkStateContent(ctx context.Context, roomIDs []string, tuples []gomatrixserverlib.StateKeyTuple, allowWildcards bool) ([]tables.StrippedEvent, error)
|
|
}
|
|
|
|
type ServerACLs struct {
|
|
acls map[string]*serverACL // room ID -> ACL
|
|
aclsMutex sync.RWMutex // protects the above
|
|
aclRegexCache map[string]**regexp.Regexp // Cache from "serverName" -> pointer to a regex
|
|
aclRegexCacheMutex sync.RWMutex // protects the above
|
|
}
|
|
|
|
func NewServerACLs(db ServerACLDatabase) *ServerACLs {
|
|
ctx := context.TODO()
|
|
acls := &ServerACLs{
|
|
acls: make(map[string]*serverACL),
|
|
// Be generous when creating the cache, as in reality
|
|
// there are hundreds of servers in an ACL.
|
|
aclRegexCache: make(map[string]**regexp.Regexp, 100),
|
|
}
|
|
|
|
// Look up all of the rooms that the current state server knows about.
|
|
rooms, err := db.RoomsWithACLs(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.
|
|
|
|
events, err := db.GetBulkStateContent(ctx, rooms, []gomatrixserverlib.StateKeyTuple{{EventType: MRoomServerACL, StateKey: ""}}, false)
|
|
if err != nil {
|
|
logrus.WithError(err).Errorf("Failed to get server ACLs for all rooms: %q", err)
|
|
}
|
|
|
|
for _, event := range events {
|
|
acls.OnServerACLUpdate(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)
|
|
}
|
|
|
|
// cachedCompileACLRegex is a wrapper around compileACLRegex with added caching
|
|
func (s *ServerACLs) cachedCompileACLRegex(orig string) (**regexp.Regexp, error) {
|
|
s.aclRegexCacheMutex.RLock()
|
|
re, ok := s.aclRegexCache[orig]
|
|
if ok {
|
|
s.aclRegexCacheMutex.RUnlock()
|
|
return re, nil
|
|
}
|
|
s.aclRegexCacheMutex.RUnlock()
|
|
compiled, err := compileACLRegex(orig)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s.aclRegexCacheMutex.Lock()
|
|
defer s.aclRegexCacheMutex.Unlock()
|
|
s.aclRegexCache[orig] = &compiled
|
|
return &compiled, nil
|
|
}
|
|
|
|
func (s *ServerACLs) OnServerACLUpdate(strippedEvent tables.StrippedEvent) {
|
|
acls := &serverACL{}
|
|
if err := json.Unmarshal([]byte(strippedEvent.ContentValue), &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 := s.cachedCompileACLRegex(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 := s.cachedCompileACLRegex(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", strippedEvent.RoomID)
|
|
|
|
// Clear out Denied and Allowed, now that we have the compiled regexes.
|
|
// They are not needed anymore from this point on.
|
|
acls.Denied = nil
|
|
acls.Allowed = nil
|
|
s.aclsMutex.Lock()
|
|
defer s.aclsMutex.Unlock()
|
|
s.acls[strippedEvent.RoomID] = acls
|
|
}
|
|
|
|
func (s *ServerACLs) IsServerBannedFromRoom(serverName spec.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 = spec.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
|
|
}
|