package roomserver import ( "context" "encoding/json" "fmt" "net" "regexp" "strings" "sync" "github.com/dariuszjaniak/warp/server/internal/federation" ) // TypeServerACL is the state event that says which servers may take part // in a room. const TypeServerACL = "m.room.server_acl" // Server ACLs. // // A room can ban whole servers. The ACL is ordinary room state, and what // it does is decided by every server in the room for itself: requests and // events from a server the ACL denies are refused, as if that server were // not there. It is the one defence a room has against a server that // creates users faster than moderators can ban them. // // It applies to what a server *sends*: its transactions' PDUs, its EDUs, // its joins and leaves, and its reads of the room's history. It does not // rewrite the past — events already in the room stay — and it does not // apply to the server enforcing it. // aclContent is the content of an m.room.server_acl event. type aclContent struct { Allow []string `json:"allow"` Deny []string `json:"deny"` AllowIPLiterals *bool `json:"allow_ip_literals"` } // globCache keeps compiled ACL globs: the same handful of patterns are // matched against every request a busy room sees. var globCache sync.Map // pattern -> *regexp.Regexp // globMatch matches a server name against an ACL glob, where '*' is any // run of characters and '?' is any one. The match ignores case, as every // implementation does: a DNS name is not case-sensitive, and an ACL that // "example.com" got around by writing "EXAMPLE.com" would be no ACL. func globMatch(pattern, name string) bool { if cached, ok := globCache.Load(pattern); ok { return cached.(*regexp.Regexp).MatchString(name) } var b strings.Builder b.WriteString(`(?i)^`) for _, r := range pattern { switch r { case '*': b.WriteString(`.*`) case '?': b.WriteString(`.`) default: b.WriteString(regexp.QuoteMeta(string(r))) } } b.WriteString(`$`) re, err := regexp.Compile(b.String()) if err != nil { return false } globCache.Store(pattern, re) return re.MatchString(name) } // aclAllows applies one ACL to a server name. The port is not part of // what an ACL matches. func aclAllows(acl aclContent, serverName string) bool { host := serverName if parsed, err := federation.ParseServerName(serverName); err == nil { host = parsed.Host if parsed.IsIP && acl.AllowIPLiterals != nil && !*acl.AllowIPLiterals { return false } } else if h, _, err := net.SplitHostPort(serverName); err == nil { host = h } for _, deny := range acl.Deny { if globMatch(deny, host) { return false } } for _, allow := range acl.Allow { if globMatch(allow, host) { return true } } // An ACL with an empty allow list allows nobody. That is what the // spec says, and the room's own server is protected from it only // because a server never applies a room's ACL to itself. return false } // ServerAllowed reports whether a room's ACL lets a server take part. A // room with no ACL allows everybody, and this server is always allowed. func (r *API) ServerAllowed(ctx context.Context, roomID, serverName string) (bool, error) { if serverName == r.serverName { return true, nil } stored, err := r.store.CurrentStateEvent(ctx, roomID, TypeServerACL, "") if isNotFound(err) { return true, nil } else if err != nil { return false, err } acl, ok := parseACL(contentOf(stored)) if !ok { // An ACL nobody can read restricts nobody; refusing everyone over // a malformed event would let one bad state event cut a room off. r.log.Warn("ignoring an unreadable server ACL", "room_id", roomID, "event_id", stored.EventID) return true, nil } return aclAllows(acl, serverName), nil } // parseACL decodes an ACL's content, reporting whether it could. func parseACL(content json.RawMessage) (aclContent, bool) { var acl aclContent return acl, json.Unmarshal(content, &acl) == nil } // requireAllowed refuses a server the room's ACL denies. func (r *API) requireAllowed(ctx context.Context, roomID, serverName string) error { ok, err := r.ServerAllowed(ctx, roomID, serverName) if err != nil { return err } if !ok { return fmt.Errorf("%w: %s is denied by the server ACL of %s", ErrForbidden, serverName, roomID) } return nil }