diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index 620246d28..24558cc92 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -16,6 +16,7 @@ package routing import ( "encoding/json" + "errors" "fmt" "net/http" "strings" @@ -137,10 +138,35 @@ func CreateRoom( cfg config.Dendrite, producer *producers.RoomserverProducer, accountDB *accounts.Database, aliasAPI roomserverAPI.RoomserverAliasAPI, asAPI appserviceAPI.AppServiceQueryAPI, + queryAPI roomserverAPI.RoomserverQueryAPI, ) util.JSONResponse { - // TODO (#267): Check room ID doesn't clash with an existing one, and we - // probably shouldn't be using pseudo-random strings, maybe GUIDs? - roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName) + + // Generate the room ID for our new room and reserve it from the roomserver. + // Keep trying until we have a roomID which is unused, give up after 10 tries. + var roomID string + var try int + for ; roomID == "" && try <= 10; try++ { + checkRoomID := util.RandomString(16) + checkRoomID = fmt.Sprintf("!%s:%s", checkRoomID, cfg.Matrix.ServerName) + + queryReq := roomserverAPI.QueryReserveRoomIDRequest{RoomID: checkRoomID} + var queryResp roomserverAPI.QueryReserveRoomIDResponse + + err := queryAPI.QueryReserveRoomID(req.Context(), &queryReq, &queryResp) + if err != nil { + return httputil.LogThenError(req, err) + } + + if queryResp.Success { + roomID = checkRoomID + } + } + + if roomID == "" { + return httputil.LogThenError(req, errors.New( + "failed to determine a unused roomID for new room")) + } + return createRoom(req, device, cfg, roomID, producer, accountDB, aliasAPI, asAPI) } diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index d4b323a2d..6fc1558ec 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -89,7 +89,7 @@ func Setup( r0mux.Handle("/createRoom", common.MakeAuthAPI("createRoom", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse { - return CreateRoom(req, device, cfg, producer, accountDB, aliasAPI, asAPI) + return CreateRoom(req, device, cfg, producer, accountDB, aliasAPI, asAPI, queryAPI) }), ).Methods(http.MethodPost, http.MethodOptions) r0mux.Handle("/join/{roomIDOrAlias}", diff --git a/roomserver/api/query.go b/roomserver/api/query.go index a544f8aa2..88157bf8d 100644 --- a/roomserver/api/query.go +++ b/roomserver/api/query.go @@ -155,6 +155,19 @@ type QueryInvitesForUserResponse struct { InviteSenderUserIDs []string `json:"invite_sender_user_ids"` } +// QueryReserveRoomIDRequest is a request to QueryReserveRoomID +type QueryReserveRoomIDRequest struct { + // The room ID to check and reserve. + RoomID string `json:"room_id"` +} + +// QueryReserveRoomIDResponse is a response to QueryServerAllowedToSeeEvent +type QueryReserveRoomIDResponse struct { + // Whether the room ID has been reserved. + // False means that the room ID is already is use, or already reserved. + Success bool `json:"success"` +} + // QueryServerAllowedToSeeEventRequest is a request to QueryServerAllowedToSeeEvent type QueryServerAllowedToSeeEventRequest struct { // The event ID to look up invites in. @@ -303,6 +316,13 @@ type RoomserverQueryAPI interface { request *QueryBackfillRequest, response *QueryBackfillResponse, ) error + + // Query if a room ID is available and reserve it. + QueryReserveRoomID( + ctx context.Context, + request *QueryReserveRoomIDRequest, + response *QueryReserveRoomIDResponse, + ) error } // RoomserverQueryLatestEventsAndStatePath is the HTTP path for the QueryLatestEventsAndState API. @@ -335,6 +355,9 @@ const RoomserverQueryStateAndAuthChainPath = "/api/roomserver/queryStateAndAuthC // RoomserverQueryBackfillPath is the HTTP path for the QueryMissingEvents API const RoomserverQueryBackfillPath = "/api/roomserver/QueryBackfill" +// RoomserverQueryReserveRoomIDPath is the HTTP path for the QueryReserveRoomID API +const RoomserverQueryReserveRoomIDPath = "/api/roomserver/queryReserveRoomID" + // NewRoomserverQueryAPIHTTP creates a RoomserverQueryAPI implemented by talking to a HTTP POST API. // If httpClient is nil then it uses the http.DefaultClient func NewRoomserverQueryAPIHTTP(roomserverURL string, httpClient *http.Client) RoomserverQueryAPI { @@ -478,3 +501,16 @@ func (h *httpRoomserverQueryAPI) QueryBackfill( apiURL := h.roomserverURL + RoomserverQueryMissingEventsPath return commonHTTP.PostJSON(ctx, span, h.httpClient, apiURL, request, response) } + +// QueryReserveRoomID implements RoomserverQueryAPI +func (h *httpRoomserverQueryAPI) QueryReserveRoomID( + ctx context.Context, + request *QueryReserveRoomIDRequest, + response *QueryReserveRoomIDResponse, +) (err error) { + span, ctx := opentracing.StartSpanFromContext(ctx, "QueryReserveRoomID") + defer span.Finish() + + apiURL := h.roomserverURL + RoomserverQueryReserveRoomIDPath + return commonHTTP.PostJSON(ctx, span, h.httpClient, apiURL, request, response) +} diff --git a/roomserver/query/query.go b/roomserver/query/query.go index a62a1f706..cab989613 100644 --- a/roomserver/query/query.go +++ b/roomserver/query/query.go @@ -16,8 +16,10 @@ package query import ( "context" + "database/sql" "encoding/json" "net/http" + "time" "github.com/matrix-org/dendrite/common" "github.com/matrix-org/dendrite/roomserver/api" @@ -85,6 +87,10 @@ type RoomserverQueryAPIDatabase interface { EventStateKeys( context.Context, []types.EventStateKeyNID, ) (map[types.EventStateKeyNID]string, error) + // Lookup if the roomID has been reserved, and when that reservation was made. + GetRoomIDReserved(ctx context.Context, roomID string) (time.Time, error) + // Reserve the room ID. + ReserveRoomID(ctx context.Context, roomID string) (success bool, err error) } // RoomserverQueryAPI is an implementation of api.RoomserverQueryAPI @@ -656,6 +662,37 @@ func getAuthChain( return authEvents, nil } +// QueryReserveRoomID implements api.RoomserverQueryAPI +func (r *RoomserverQueryAPI) QueryReserveRoomID( + ctx context.Context, + request *api.QueryReserveRoomIDRequest, + response *api.QueryReserveRoomIDResponse, +) error { + // Check if room already exists. + // TODO: This is racey as some other request can create the + // room in between this check and the reservation below. + nid, err := r.DB.RoomNID(ctx, request.RoomID) + if err != nil && err != sql.ErrNoRows { + return err + } + + if nid != 0 { + response.Success = false + return nil + } + + // Try to reserve room. + success, err := r.DB.ReserveRoomID(ctx, request.RoomID) + if err != nil { + // TODO: handle errors + return err + } + + response.Success = success + + return nil +} + // SetupHTTP adds the RoomserverQueryAPI handlers to the http.ServeMux. // nolint: gocyclo func (r *RoomserverQueryAPI) SetupHTTP(servMux *http.ServeMux) { diff --git a/roomserver/storage/reserved_rooms.go b/roomserver/storage/reserved_rooms.go new file mode 100644 index 000000000..fc937dbac --- /dev/null +++ b/roomserver/storage/reserved_rooms.go @@ -0,0 +1,80 @@ +// Copyright 2017 Vector Creations Ltd +// +// 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 storage + +import ( + "context" + "database/sql" + "time" + + "github.com/matrix-org/dendrite/common" +) + +const reservedRoomSchema = ` +-- The events table holds metadata for each event, the actual JSON is stored +-- separately to keep the size of the rows small. +CREATE SEQUENCE IF NOT EXISTS roomserver_event_nid_seq; +CREATE TABLE IF NOT EXISTS roomserver_reserved_room_ids ( + --- The Room ID --- + room_id TEXT PRIMARY KEY, + reserved_since TIMESTAMP NOT NULL +); +` + +const reserveRoomIDSQL = "" + + "INSERT INTO roomserver_reserved_room_ids (room_id, reserved_since) select $1, $2" + //"WHERE NOT EXISTS (SELECT 1 FROM roomserver_rooms WHERE room_id = $1)" + +const selectReservedRoomIDSQL = "" + + "SELECT reserved_since FROM roomserver_reserved_room_ids WHERE room_id = $1" + +type reservedRoomStatements struct { + reserveRoomIDStmt *sql.Stmt + selectReservedRoomIDStmt *sql.Stmt +} + +func (s *reservedRoomStatements) prepare(db *sql.DB) (err error) { + _, err = db.Exec(reservedRoomSchema) + if err != nil { + return + } + + return statementList{ + {&s.reserveRoomIDStmt, reserveRoomIDSQL}, + {&s.selectReservedRoomIDStmt, selectReservedRoomIDSQL}, + }.prepare(db) +} + +func (s *reservedRoomStatements) reserveRoomID( + ctx context.Context, + roomID string, + reservedSince time.Time, +) (success bool, err error) { + _, err = s.reserveRoomIDStmt.ExecContext(ctx, roomID, reservedSince) + + if common.IsUniqueConstraintViolationErr(err) { + return false, nil + } + + return err == nil, err +} + +func (s *reservedRoomStatements) selectReservedRoomID( + ctx context.Context, roomID string, +) (time.Time, error) { + var reservedSince time.Time + err := s.selectReservedRoomIDStmt.QueryRowContext(ctx, roomID).Scan(&reservedSince) + return reservedSince, err +} diff --git a/roomserver/storage/sql.go b/roomserver/storage/sql.go index 05efa8dd4..479dadcce 100644 --- a/roomserver/storage/sql.go +++ b/roomserver/storage/sql.go @@ -31,6 +31,7 @@ type statements struct { inviteStatements membershipStatements transactionStatements + reservedRoomStatements } func (s *statements) prepare(db *sql.DB) error { @@ -49,6 +50,7 @@ func (s *statements) prepare(db *sql.DB) error { s.inviteStatements.prepare, s.membershipStatements.prepare, s.transactionStatements.prepare, + s.reservedRoomStatements.prepare, } { if err = prepare(db); err != nil { return err diff --git a/roomserver/storage/storage.go b/roomserver/storage/storage.go index 7e8eb98c9..bd892dab7 100644 --- a/roomserver/storage/storage.go +++ b/roomserver/storage/storage.go @@ -17,6 +17,7 @@ package storage import ( "context" "database/sql" + "time" // Import the postgres database driver. _ "github.com/lib/pq" @@ -696,6 +697,17 @@ func (d *Database) EventsFromIDs(ctx context.Context, eventIDs []string) ([]type return d.Events(ctx, nids) } +// RoomIDReserved implements query.RoomserverQueryAPIDB +func (d *Database) GetRoomIDReserved(ctx context.Context, roomID string) (time.Time, error) { + return d.statements.selectReservedRoomID(ctx, roomID) +} + +// ReserveRoomID implements query.RoomserverQueryAPIDB +// It saved the given room ID as reserved. +func (d *Database) ReserveRoomID(ctx context.Context, roomID string) (success bool, err error) { + return d.statements.reserveRoomID(ctx, roomID, time.Now()) +} + type transaction struct { ctx context.Context txn *sql.Tx