Add canonical alias support (#2236)

* Add canonical support

* Add test

* Check that the send event is actually an m.room.canonical_alias
Check that we got an event from the database

* Update to get correct required events

* Add flakey test to blacklist
This commit is contained in:
S7evinK 2022-03-07 10:37:04 +01:00 committed by GitHub
parent 86d4eef9f1
commit 9fbaa1194b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 6 deletions

View File

@ -58,6 +58,11 @@ func BadJSON(msg string) *MatrixError {
return &MatrixError{"M_BAD_JSON", msg} return &MatrixError{"M_BAD_JSON", msg}
} }
// BadAlias is an error when the client supplies a bad alias.
func BadAlias(msg string) *MatrixError {
return &MatrixError{"M_BAD_ALIAS", msg}
}
// NotJSON is an error when the client supplies something that is not JSON // NotJSON is an error when the client supplies something that is not JSON
// to a JSON endpoint. // to a JSON endpoint.
func NotJSON(msg string) *MatrixError { func NotJSON(msg string) *MatrixError {

View File

@ -16,6 +16,8 @@ package routing
import ( import (
"context" "context"
"encoding/json"
"fmt"
"net/http" "net/http"
"sync" "sync"
"time" "time"
@ -120,6 +122,40 @@ func SendEvent(
} }
timeToGenerateEvent := time.Since(startedGeneratingEvent) timeToGenerateEvent := time.Since(startedGeneratingEvent)
// validate that the aliases exists
if eventType == gomatrixserverlib.MRoomCanonicalAlias && stateKey != nil && *stateKey == "" {
aliasReq := api.AliasEvent{}
if err = json.Unmarshal(e.Content(), &aliasReq); err != nil {
return util.ErrorResponse(fmt.Errorf("unable to parse alias event: %w", err))
}
if !aliasReq.Valid() {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.InvalidParam("Request contains invalid aliases."),
}
}
aliasRes := &api.GetAliasesForRoomIDResponse{}
if err = rsAPI.GetAliasesForRoomID(req.Context(), &api.GetAliasesForRoomIDRequest{RoomID: roomID}, aliasRes); err != nil {
return jsonerror.InternalServerError()
}
var found int
requestAliases := append(aliasReq.AltAliases, aliasReq.Alias)
for _, alias := range aliasRes.Aliases {
for _, altAlias := range requestAliases {
if altAlias == alias {
found++
}
}
}
// check that we found at least the same amount of existing aliases as are in the request
if aliasReq.Alias != "" && found < len(requestAliases) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadAlias("No matching alias found."),
}
}
}
var txnAndSessionID *api.TransactionID var txnAndSessionID *api.TransactionID
if txnID != nil { if txnID != nil {
txnAndSessionID = &api.TransactionID{ txnAndSessionID = &api.TransactionID{

View File

@ -14,6 +14,8 @@
package api package api
import "regexp"
// SetRoomAliasRequest is a request to SetRoomAlias // SetRoomAliasRequest is a request to SetRoomAlias
type SetRoomAliasRequest struct { type SetRoomAliasRequest struct {
// ID of the user setting the alias // ID of the user setting the alias
@ -84,3 +86,20 @@ type RemoveRoomAliasResponse struct {
// Did we remove it? // Did we remove it?
Removed bool `json:"removed"` Removed bool `json:"removed"`
} }
type AliasEvent struct {
Alias string `json:"alias"`
AltAliases []string `json:"alt_aliases"`
}
var validateAliasRegex = regexp.MustCompile("^#.*:.+$")
func (a AliasEvent) Valid() bool {
for _, alias := range a.AltAliases {
if !validateAliasRegex.MatchString(alias) {
return false
}
}
return a.Alias == "" || validateAliasRegex.MatchString(a.Alias)
}

View File

@ -0,0 +1,62 @@
package api
import "testing"
func TestAliasEvent_Valid(t *testing.T) {
type fields struct {
Alias string
AltAliases []string
}
tests := []struct {
name string
fields fields
want bool
}{
{
name: "empty alias",
fields: fields{
Alias: "",
},
want: true,
},
{
name: "empty alias, invalid alt aliases",
fields: fields{
Alias: "",
AltAliases: []string{ "%not:valid.local"},
},
},
{
name: "valid alias, invalid alt aliases",
fields: fields{
Alias: "#valid:test.local",
AltAliases: []string{ "%not:valid.local"},
},
},
{
name: "empty alias, invalid alt aliases",
fields: fields{
Alias: "",
AltAliases: []string{ "%not:valid.local"},
},
},
{
name: "invalid alias",
fields: fields{
Alias: "%not:valid.local",
AltAliases: []string{ },
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := AliasEvent{
Alias: tt.fields.Alias,
AltAliases: tt.fields.AltAliases,
}
if got := a.Valid(); got != tt.want {
t.Errorf("Valid() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -16,12 +16,18 @@ package internal
import ( import (
"context" "context"
"database/sql"
"errors"
"fmt" "fmt"
"time"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
asAPI "github.com/matrix-org/dendrite/appservice/api" asAPI "github.com/matrix-org/dendrite/appservice/api"
"github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/dendrite/roomserver/internal/helpers"
"github.com/matrix-org/gomatrixserverlib"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
) )
// RoomserverInternalAPIDatabase has the storage APIs needed to implement the alias API. // RoomserverInternalAPIDatabase has the storage APIs needed to implement the alias API.
@ -183,6 +189,57 @@ func (r *RoomserverInternalAPI) RemoveRoomAlias(
} }
} }
ev, err := r.DB.GetStateEvent(ctx, roomID, gomatrixserverlib.MRoomCanonicalAlias, "")
if err != nil && err != sql.ErrNoRows {
return err
} else if ev != nil {
stateAlias := gjson.GetBytes(ev.Content(), "alias").Str
// the alias to remove is currently set as the canonical alias, remove it
if stateAlias == request.Alias {
res, err := sjson.DeleteBytes(ev.Content(), "alias")
if err != nil {
return err
}
sender := request.UserID
if request.UserID != ev.Sender() {
sender = ev.Sender()
}
builder := &gomatrixserverlib.EventBuilder{
Sender: sender,
RoomID: ev.RoomID(),
Type: ev.Type(),
StateKey: ev.StateKey(),
Content: res,
}
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
if err != nil {
return fmt.Errorf("gomatrixserverlib.StateNeededForEventBuilder: %w", err)
}
if len(eventsNeeded.Tuples()) == 0 {
return errors.New("expecting state tuples for event builder, got none")
}
stateRes := &api.QueryLatestEventsAndStateResponse{}
if err := helpers.QueryLatestEventsAndState(ctx, r.DB, &api.QueryLatestEventsAndStateRequest{RoomID: roomID, StateToFetch: eventsNeeded.Tuples()}, stateRes); err != nil {
return err
}
newEvent, err := eventutil.BuildEvent(ctx, builder, r.Cfg.Matrix, time.Now(), &eventsNeeded, stateRes)
if err != nil {
return err
}
err = api.SendEvents(ctx, r.RSAPI, api.KindNew, []*gomatrixserverlib.HeaderedEvent{newEvent}, r.ServerName, r.ServerName, nil, false)
if err != nil {
return err
}
}
}
// Remove the alias from the database // Remove the alias from the database
if err := r.DB.RemoveRoomAlias(ctx, request.Alias); err != nil { if err := r.DB.RemoveRoomAlias(ctx, request.Alias); err != nil {
return err return err

View File

@ -31,6 +31,7 @@ Remove group role
# Flakey # Flakey
AS-ghosted users can use rooms themselves AS-ghosted users can use rooms themselves
/context/ with lazy_load_members filter works
# Flakey, need additional investigation # Flakey, need additional investigation
Messages that notify from another user increment notification_count Messages that notify from another user increment notification_count

View File

@ -648,7 +648,7 @@ Device list doesn't change if remote server is down
/context/ on joined room works /context/ on joined room works
/context/ on non world readable room does not work /context/ on non world readable room does not work
/context/ returns correct number of events /context/ returns correct number of events
/context/ with lazy_load_members filter works
GET /rooms/:room_id/messages lazy loads members correctly GET /rooms/:room_id/messages lazy loads members correctly
Can query remote device keys using POST after notification Can query remote device keys using POST after notification
Device deletion propagates over federation Device deletion propagates over federation
@ -659,4 +659,8 @@ registration accepts non-ascii passwords
registration with inhibit_login inhibits login registration with inhibit_login inhibits login
The operation must be consistent through an interactive authentication session The operation must be consistent through an interactive authentication session
Multiple calls to /sync should not cause 500 errors Multiple calls to /sync should not cause 500 errors
/context/ with lazy_load_members filter works
Canonical alias can be set
Canonical alias can include alt_aliases
Can delete canonical alias
Multiple calls to /sync should not cause 500 errors