Browse Source

Add support for PIN locking rooms to the server.

pull/112/head
Lance Cooper 11 years ago committed by Simon Eisenmann
parent
commit
31e4f2c0c0
  1. 51
      doc/CHANNELING-API.txt
  2. 6
      src/app/spreed-webrtc-server/channeling.go
  3. 7
      src/app/spreed-webrtc-server/channelling_api.go
  4. 18
      src/app/spreed-webrtc-server/channelling_api_test.go
  5. 43
      src/app/spreed-webrtc-server/common_test.go
  6. 40
      src/app/spreed-webrtc-server/room_manager.go
  7. 12
      src/app/spreed-webrtc-server/room_manager_test.go
  8. 69
      src/app/spreed-webrtc-server/roomworker.go
  9. 124
      src/app/spreed-webrtc-server/roomworker_test.go

51
doc/CHANNELING-API.txt

@ -157,7 +157,8 @@ Special purpose documents for channling @@ -157,7 +157,8 @@ Special purpose documents for channling
Hello: {
Version: "1.0.0",
Ua: "Test client 1.0",
Id: ""
Id: "",
"Credentials": {...}
}
}
@ -172,10 +173,26 @@ Special purpose documents for channling @@ -172,10 +173,26 @@ Special purpose documents for channling
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 @@ -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 @@ -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:

6
src/app/spreed-webrtc-server/channeling.go

@ -31,10 +31,15 @@ func (err *DataError) Error() string { @@ -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
Credentials *DataRoomCredentials
}
type DataWelcome struct {
@ -46,6 +51,7 @@ type DataWelcome struct { @@ -46,6 +51,7 @@ type DataWelcome struct {
type DataRoom struct {
Type string
Name string
Credentials *DataRoomCredentials
}
type DataOffer struct {

7
src/app/spreed-webrtc-server/channelling_api.go

@ -87,14 +87,13 @@ func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *D @@ -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 @@ -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":

18
src/app/spreed-webrtc-server/channelling_api_test.go

@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
package main
import (
"errors"
"testing"
)
@ -45,27 +46,24 @@ func (fake *fakeClient) Reply(iid string, msg interface{}) { @@ -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( @@ -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 @@ -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) {

43
src/app/spreed-webrtc-server/common_test.go

@ -0,0 +1,43 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}

40
src/app/spreed-webrtc-server/room_manager.go

@ -27,9 +27,8 @@ import ( @@ -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 { @@ -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 {
if room, ok := rooms.Get(session.Roomid); ok {
return room.GetUsers()
}
// TODO(lcooper): This should return an error.
return []*DataSession{}
}
func (rooms *roomManager) RoomUsers(session *Session) []*DataSession {
return <-rooms.GetOrCreate(session).GetUsers()
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"}
}
func (rooms *roomManager) JoinRoom(session *Session, sender Sender) {
rooms.GetOrCreate(session).Join(session, sender)
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 @@ -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{}) { @@ -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 @@ -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() {

12
src/app/spreed-webrtc-server/room_manager_test.go

@ -29,18 +29,6 @@ func NewTestRoomManager() RoomManager { @@ -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)

69
src/app/spreed-webrtc-server/roomworker.go

@ -22,6 +22,7 @@ @@ -22,6 +22,7 @@
package main
import (
"crypto/subtle"
"log"
"sync"
"time"
@ -36,9 +37,10 @@ type RoomWorker interface { @@ -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)
}
@ -55,6 +57,7 @@ type roomWorker struct { @@ -55,6 +57,7 @@ type roomWorker struct {
// Metadata.
Id string
credentials *DataRoomCredentials
}
type roomUser struct {
@ -62,7 +65,7 @@ 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 { @@ -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 { @@ -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 { @@ -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) { @@ -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) {

124
src/app/spreed-webrtc-server/roomworker_test.go

@ -0,0 +1,124 @@ @@ -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 <http://www.gnu.org/licenses/>.
*
*/
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)
}
}
Loading…
Cancel
Save