diff --git a/doc/CHANNELING-API.txt b/doc/CHANNELING-API.txt index 2b6a527b..5a0ac709 100644 --- a/doc/CHANNELING-API.txt +++ b/doc/CHANNELING-API.txt @@ -157,7 +157,8 @@ Special purpose documents for channling Hello: { Version: "1.0.0", Ua: "Test client 1.0", - Id: "" + Id: "", + "Credentials": {...} } } @@ -169,13 +170,29 @@ Special purpose documents for channling Keys under Hello: - Version : Channel protocol version (string). - Ua : User agent description (string). - Id : Room id. The default Room has the empty string Id ("") (string). + Version : Channel protocol version (string). + Ua : User agent description (string). + Id : Room id. The default Room has the empty string Id ("") (string). + Credentials : An optional RoomCredentials document containing room + authentication information. See the Room document for + information on how such credentials should be handled after + a Welcome is received for the requested room. Note that + providing credentials while joining an existing room which + does not require them is an error, such requests should be + retried without credentials. In contrast, joining a + non-existent room with credentials will create the room + using the given credentials. Note that an error with a code + of authorization_not_required or invalid_credentials shall + cause the client to discard any cached room credentials. Error codes: - default_room_disabled : Joining the room "" is not allowed by this server. + default_room_disabled : Joining the room "" is not allowed by this + server. + authorization_required : Joining the given room requires credentials. + authorization_not_required : No credentials should be provided for this + room. + invalid_credentials : The provided credentials are incorrect. Welcome @@ -197,11 +214,27 @@ Special purpose documents for channling Users: Contains the user list for the room, see the description of the Users document for more details. + RoomCredentials + + { + "PIN": "my-super-sekrit-code" + } + + RoomCredentials contains room authentication information, and is used as a + child document when joining or updating a room. + + Keys under RoomCredentials: + + PIN : A password string which may be used by clients to authenticate + themselves. Note that acceptable characters for this field may be + constrained by the server based upon its configuration. + Room { "Type": "Room", "Name": "room-name-here" + "Credentials": {...} } Clients may send a Room document in order to update all room properties @@ -214,7 +247,19 @@ Special purpose documents for channling Keys under Room: - Name: The human readable ID of the room, currently must be globally unique. + Name : The human readable ID of the room, currently must be globally + unique. + Credentials : Optional authentication information for the room, see the + documentation of the RoomCredentials document for more + details. This field shall only be present when sending or + receiving an update which alters room authentication data. + It should only be inferred that authentication is not + required if joining a room succeeds without credentials and + no updates containing credentials has been received. + Authentication may be disabled by sending a Room document + containing a RoomCredentials document with only empty + fields. Clients shall discard any cached authentication + information upon receiving such an update. Error codes: diff --git a/src/app/spreed-webrtc-server/channeling.go b/src/app/spreed-webrtc-server/channeling.go index dd4e8eb1..7d3c516b 100644 --- a/src/app/spreed-webrtc-server/channeling.go +++ b/src/app/spreed-webrtc-server/channeling.go @@ -31,10 +31,15 @@ func (err *DataError) Error() string { return err.Message } +type DataRoomCredentials struct { + PIN string +} + type DataHello struct { - Version string - Ua string - Id string + Version string + Ua string + Id string + Credentials *DataRoomCredentials } type DataWelcome struct { @@ -44,8 +49,9 @@ type DataWelcome struct { } type DataRoom struct { - Type string - Name string + Type string + Name string + Credentials *DataRoomCredentials } type DataOffer struct { diff --git a/src/app/spreed-webrtc-server/channelling_api.go b/src/app/spreed-webrtc-server/channelling_api.go index 2f4c6ded..832a79ae 100644 --- a/src/app/spreed-webrtc-server/channelling_api.go +++ b/src/app/spreed-webrtc-server/channelling_api.go @@ -87,14 +87,13 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D // NOTE(lcooper): Iid filtered for compatibility's sake. // Evaluate sending unconditionally when supported by all clients. - if api.CanJoinRoom(msg.Hello.Id) { + if room, err := api.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, session, c); err == nil { session.Hello = true session.Roomid = msg.Hello.Id - api.JoinRoom(session, c) if msg.Iid != "" { c.Reply(msg.Iid, &DataWelcome{ Type: "Welcome", - Room: &DataRoom{Name: msg.Hello.Id}, + Room: room, Users: api.RoomUsers(session), }) } @@ -102,7 +101,7 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D } else { session.Hello = false if msg.Iid != "" { - c.Reply(msg.Iid, &DataError{Type: "Error", Code: "default_room_disabled", Message: "The default room is not enabled"}) + c.Reply(msg.Iid, err) } } case "Offer": diff --git a/src/app/spreed-webrtc-server/channelling_api_test.go b/src/app/spreed-webrtc-server/channelling_api_test.go index 0c1dff2a..c6a5e6dc 100644 --- a/src/app/spreed-webrtc-server/channelling_api_test.go +++ b/src/app/spreed-webrtc-server/channelling_api_test.go @@ -22,6 +22,7 @@ package main import ( + "errors" "testing" ) @@ -45,27 +46,24 @@ func (fake *fakeClient) Reply(iid string, msg interface{}) { } type fakeRoomManager struct { - disallowJoin bool joinedRoomID string leftRoomID string roomUsers []*DataSession joinedID string + joinError error leftID string broadcasts []interface{} updatedRoom *DataRoom updateError error } -func (fake *fakeRoomManager) CanJoinRoom(roomID string) bool { - return !fake.disallowJoin -} - func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession { return fake.roomUsers } -func (fake *fakeRoomManager) JoinRoom(session *Session, _ Sender) { - fake.joinedID = session.Roomid +func (fake *fakeRoomManager) JoinRoom(id string, _ *DataRoomCredentials, session *Session, _ Sender) (*DataRoom, error) { + fake.joinedID = id + return &DataRoom{Name: id}, fake.joinError } func (fake *fakeRoomManager) LeaveRoom(session *Session) { @@ -159,7 +157,7 @@ func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms( func Test_ChannellingAPI_OnIncoming_HelloMessage_DoesNotJoinIfNotPermitted(t *testing.T) { api, client, session, roomManager := NewTestChannellingAPI() - roomManager.disallowJoin = true + roomManager.joinError = errors.New("Can't enter this room") api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{}}) @@ -201,11 +199,11 @@ func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) { iid := "foo" api, client, session, roomManager := NewTestChannellingAPI() - roomManager.disallowJoin = true + roomManager.joinError = &DataError{Type: "Error", Code: "bad_join"} api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{}}) - assertErrorReply(t, client, iid, "default_room_disabled") + assertErrorReply(t, client, iid, "bad_join") } func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpdatedRoom(t *testing.T) { diff --git a/src/app/spreed-webrtc-server/common_test.go b/src/app/spreed-webrtc-server/common_test.go new file mode 100644 index 00000000..395e2a9c --- /dev/null +++ b/src/app/spreed-webrtc-server/common_test.go @@ -0,0 +1,43 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "testing" +) + +func assertDataError(t *testing.T, err error, code string) { + if err == nil { + t.Error("Expected an error, but none was returned") + return + } + + dataError, ok := err.(*DataError) + if !ok { + t.Errorf("Expected error %#v to be a *DataError", err) + return + } + + if code != dataError.Code { + t.Errorf("Expected error code to be %v, but was %v", code, dataError.Code) + } +} diff --git a/src/app/spreed-webrtc-server/room_manager.go b/src/app/spreed-webrtc-server/room_manager.go index c57d8c75..9efc5420 100644 --- a/src/app/spreed-webrtc-server/room_manager.go +++ b/src/app/spreed-webrtc-server/room_manager.go @@ -27,9 +27,8 @@ import ( ) type RoomStatusManager interface { - CanJoinRoom(id string) bool RoomUsers(*Session) []*DataSession - JoinRoom(*Session, Sender) + JoinRoom(string, *DataRoomCredentials, *Session, Sender) (*DataRoom, error) LeaveRoom(*Session) UpdateRoom(*Session, *DataRoom) (*DataRoom, error) } @@ -66,20 +65,24 @@ func NewRoomManager(config *Config, encoder OutgoingEncoder) RoomManager { } } -func (rooms *roomManager) CanJoinRoom(id string) bool { - return id != "" || rooms.defaultRoomEnabled -} - func (rooms *roomManager) RoomUsers(session *Session) []*DataSession { - return <-rooms.GetOrCreate(session).GetUsers() + if room, ok := rooms.Get(session.Roomid); ok { + return room.GetUsers() + } + // TODO(lcooper): This should return an error. + return []*DataSession{} } -func (rooms *roomManager) JoinRoom(session *Session, sender Sender) { - rooms.GetOrCreate(session).Join(session, sender) +func (rooms *roomManager) JoinRoom(id string, credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) { + if id == "" && !rooms.defaultRoomEnabled { + return nil, &DataError{Type: "Error", Code: "default_room_disabled", Message: "The default room is not enabled"} + } + + return rooms.GetOrCreate(id, credentials).Join(credentials, session, sender) } func (rooms *roomManager) LeaveRoom(session *Session) { - if room, ok := rooms.Get(session); ok { + if room, ok := rooms.Get(session.Roomid); ok { room.Leave(session) } } @@ -91,6 +94,10 @@ func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoo // XXX(lcooper): We'll process and send documents without this field // correctly, however clients cannot not handle it currently. room.Type = "Room" + if roomWorker, ok := rooms.Get(session.Roomid); ok { + return room, roomWorker.Update(room) + } + // TODO(lcooper): We should almost certainly return an error in this case. return room, nil } @@ -113,8 +120,10 @@ func (rooms *roomManager) Broadcast(session *Session, m interface{}) { room.Broadcast(session, message) } rooms.RUnlock() + } else if room, ok := rooms.Get(id); ok { + room.Broadcast(session, message) } else { - rooms.GetOrCreate(session).Broadcast(session, message) + log.Printf("No room named %s found for broadcast message %#v", id, m) } message.Decref() } @@ -133,23 +142,22 @@ func (rooms *roomManager) RoomInfo(includeSessions bool) (count int, sessionInfo return } -func (rooms *roomManager) Get(session *Session) (room RoomWorker, ok bool) { +func (rooms *roomManager) Get(id string) (room RoomWorker, ok bool) { rooms.RLock() - room, ok = rooms.roomTable[session.Roomid] + room, ok = rooms.roomTable[id] rooms.RUnlock() return } -func (rooms *roomManager) GetOrCreate(session *Session) RoomWorker { - room, ok := rooms.Get(session) +func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredentials) RoomWorker { + room, ok := rooms.Get(id) if !ok { - id := session.Roomid rooms.Lock() // Need to re-check, another thread might have created the room // while we waited for the lock. room, ok = rooms.roomTable[id] if !ok { - room = NewRoomWorker(rooms, id) + room = NewRoomWorker(rooms, id, credentials) rooms.roomTable[id] = room rooms.Unlock() go func() { diff --git a/src/app/spreed-webrtc-server/room_manager_test.go b/src/app/spreed-webrtc-server/room_manager_test.go index f7f2d3df..55ceb2e4 100644 --- a/src/app/spreed-webrtc-server/room_manager_test.go +++ b/src/app/spreed-webrtc-server/room_manager_test.go @@ -29,18 +29,6 @@ func NewTestRoomManager() RoomManager { return NewRoomManager(&Config{}, nil) } -func assertDataError(t *testing.T, err error, code string) { - dataError, ok := err.(*DataError) - if !ok { - t.Errorf("Expected error %#v to be a *DataError", err) - return - } - - if code != dataError.Code { - t.Errorf("Expected error code to be %v, but was %v", code, dataError.Code) - } -} - func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) { roomManager := NewTestRoomManager() _, err := roomManager.UpdateRoom(&Session{}, nil) diff --git a/src/app/spreed-webrtc-server/roomworker.go b/src/app/spreed-webrtc-server/roomworker.go index df7e1feb..2e52fefa 100644 --- a/src/app/spreed-webrtc-server/roomworker.go +++ b/src/app/spreed-webrtc-server/roomworker.go @@ -22,6 +22,7 @@ package main import ( + "crypto/subtle" "log" "sync" "time" @@ -36,9 +37,10 @@ type RoomWorker interface { Start() SessionIDs() []string Users() []*roomUser - GetUsers() <-chan []*DataSession + Update(*DataRoom) error + GetUsers() []*DataSession Broadcast(*Session, Buffer) - Join(*Session, Sender) + Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error) Leave(*Session) } @@ -54,7 +56,8 @@ type roomWorker struct { mutex sync.RWMutex // Metadata. - Id string + Id string + credentials *DataRoomCredentials } type roomUser struct { @@ -62,7 +65,7 @@ type roomUser struct { Sender } -func NewRoomWorker(manager *roomManager, id string) RoomWorker { +func NewRoomWorker(manager *roomManager, id string, credentials *DataRoomCredentials) RoomWorker { log.Printf("Creating worker for room '%s'\n", id) @@ -74,6 +77,10 @@ func NewRoomWorker(manager *roomManager, id string) RoomWorker { users: make(map[string]*roomUser), } + if credentials != nil && len(credentials.PIN) > 0 { + r.credentials = credentials + } + // Create expire timer. r.timer = time.AfterFunc(roomExpiryDuration, func() { r.expired <- true @@ -148,7 +155,25 @@ func (r *roomWorker) Run(f func()) bool { } -func (r *roomWorker) GetUsers() <-chan []*DataSession { +func (r *roomWorker) Update(room *DataRoom) error { + fault := make(chan error, 1) + worker := func() { + r.mutex.Lock() + if room.Credentials != nil { + if len(room.Credentials.PIN) > 0 { + r.credentials = room.Credentials + } else { + r.credentials = nil + } + } + r.mutex.Unlock() + fault <- nil + } + r.Run(worker) + return <-fault +} + +func (r *roomWorker) GetUsers() []*DataSession { out := make(chan []*DataSession, 1) worker := func() { var sl []*DataSession @@ -185,7 +210,7 @@ func (r *roomWorker) GetUsers() <-chan []*DataSession { } r.Run(worker) - return out + return <-out } func (r *roomWorker) Broadcast(session *Session, message Buffer) { @@ -209,13 +234,43 @@ func (r *roomWorker) Broadcast(session *Session, message Buffer) { } -func (r *roomWorker) Join(session *Session, sender Sender) { +type joinResult struct { + *DataRoom + error +} + +func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) { + results := make(chan joinResult, 1) worker := func() { r.mutex.Lock() - defer r.mutex.Unlock() + if r.credentials == nil && credentials != nil { + results <- joinResult{nil, &DataError{"Error", "authorization_not_required", "No credentials may be provided for this room"}} + r.mutex.Unlock() + return + } else if r.credentials != nil { + if credentials == nil { + results <- joinResult{nil, &DataError{"Error", "authorization_required", "Valid credentials are required to join this room"}} + r.mutex.Unlock() + return + } + + if len(r.credentials.PIN) != len(credentials.PIN) || subtle.ConstantTimeCompare([]byte(r.credentials.PIN), []byte(credentials.PIN)) != 1 { + results <- joinResult{nil, &DataError{"Error", "invalid_credentials", "The provided credentials are incorrect"}} + r.mutex.Unlock() + return + } + } + r.users[session.Id] = &roomUser{session, sender} + // NOTE(lcooper): Needs to be a copy, else we risk races with + // a subsequent modification of room properties. + result := joinResult{&DataRoom{Name: r.Id}, nil} + r.mutex.Unlock() + results <- result } r.Run(worker) + result := <-results + return result.DataRoom, result.error } func (r *roomWorker) Leave(session *Session) { diff --git a/src/app/spreed-webrtc-server/roomworker_test.go b/src/app/spreed-webrtc-server/roomworker_test.go new file mode 100644 index 00000000..ad785440 --- /dev/null +++ b/src/app/spreed-webrtc-server/roomworker_test.go @@ -0,0 +1,124 @@ +/* + * Spreed WebRTC. + * Copyright (C) 2013-2014 struktur AG + * + * This file is part of Spreed WebRTC. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "testing" +) + +const ( + testRoomName string = "a-room-name" +) + +func NewTestRoomWorker() RoomWorker { + worker := NewRoomWorker(&roomManager{}, testRoomName, nil) + go worker.Start() + return worker +} + +func NewTestRoomWorkerWithPIN(t *testing.T) (RoomWorker, string) { + pin := "asdf" + worker := NewRoomWorker(&roomManager{}, testRoomName, &DataRoomCredentials{PIN: pin}) + go worker.Start() + return worker, pin +} + +func Test_RoomWorker_Join_SucceedsWhenNoCredentialsAreRequired(t *testing.T) { + worker := NewTestRoomWorker() + + _, err := worker.Join(nil, &Session{}, nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if userCount := len(worker.GetUsers()); userCount != 1 { + t.Errorf("Expected join to have been accepted but room contains %d users", userCount) + } +} + +func Test_RoomWorker_Join_FailsIfCredentialsAreGivenWhenUnneeded(t *testing.T) { + worker := NewTestRoomWorker() + + _, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil) + + assertDataError(t, err, "authorization_not_required") + if userCount := len(worker.GetUsers()); userCount != 0 { + t.Errorf("Expected join to have been rejected but room contains %d users", userCount) + } +} + +func Test_RoomWorker_Join_FailsIfNoCredentialsAreGiven(t *testing.T) { + worker, _ := NewTestRoomWorkerWithPIN(t) + + _, err := worker.Join(nil, &Session{}, nil) + + assertDataError(t, err, "authorization_required") + if userCount := len(worker.GetUsers()); userCount != 0 { + t.Errorf("Expected join to have been rejected but room contains %d users", userCount) + } +} + +func Test_RoomWorker_Join_FailsIfIncorrectCredentialsAreGiven(t *testing.T) { + worker, _ := NewTestRoomWorkerWithPIN(t) + + _, err := worker.Join(&DataRoomCredentials{PIN: "adfs"}, &Session{}, nil) + + assertDataError(t, err, "invalid_credentials") + if userCount := len(worker.GetUsers()); userCount != 0 { + t.Errorf("Expected join to have been rejected but room contains %d users", userCount) + } +} + +func Test_RoomWorker_Join_SucceedsWhenTheCorrectPINIsGiven(t *testing.T) { + worker, pin := NewTestRoomWorkerWithPIN(t) + + if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil { + t.Fatalf("Unexpected error %v", err) + } + + if len(worker.GetUsers()) < 1 { + t.Error("Expected join to have been accepted but room contains no users") + } +} + +func Test_RoomWorker_Update_AllowsClearingCredentials(t *testing.T) { + worker, _ := NewTestRoomWorkerWithPIN(t) + + if err := worker.Update(&DataRoom{Credentials: &DataRoomCredentials{PIN: ""}}); err != nil { + t.Fatalf("Failed to update room: %v", err) + } + + _, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil) + assertDataError(t, err, "authorization_not_required") +} + +func Test_RoomWorker_Update_RetainsCredentialsWhenOtherPropertiesAreUpdated(t *testing.T) { + worker, pin := NewTestRoomWorkerWithPIN(t) + + if err := worker.Update(&DataRoom{}); err != nil { + t.Fatalf("Failed to update room: %v", err) + } + + if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil { + t.Fatalf("Unexpected error joining room %v", err) + } +}