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)
+ }
+}