50 changed files with 2726 additions and 1423 deletions
@ -0,0 +1,286 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"log" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
maxConferenceSize = 100 |
||||||
|
) |
||||||
|
|
||||||
|
type ChannellingAPI interface { |
||||||
|
OnConnect(Client, *Session) |
||||||
|
OnIncoming(ResponseSender, *Session, *DataIncoming) |
||||||
|
OnDisconnect(*Session) |
||||||
|
} |
||||||
|
|
||||||
|
type channellingAPI struct { |
||||||
|
version string |
||||||
|
*Config |
||||||
|
RoomStatusManager |
||||||
|
SessionEncoder |
||||||
|
SessionManager |
||||||
|
StatsCounter |
||||||
|
ContactManager |
||||||
|
TurnDataCreator |
||||||
|
Unicaster |
||||||
|
Broadcaster |
||||||
|
buddyImages ImageCache |
||||||
|
} |
||||||
|
|
||||||
|
func NewChannellingAPI(version string, config *Config, roomStatus RoomStatusManager, sessionEncoder SessionEncoder, sessionManager SessionManager, statsCounter StatsCounter, contactManager ContactManager, turnDataCreator TurnDataCreator, unicaster Unicaster, broadcaster Broadcaster, buddyImages ImageCache) ChannellingAPI { |
||||||
|
return &channellingAPI{ |
||||||
|
version, |
||||||
|
config, |
||||||
|
roomStatus, |
||||||
|
sessionEncoder, |
||||||
|
sessionManager, |
||||||
|
statsCounter, |
||||||
|
contactManager, |
||||||
|
turnDataCreator, |
||||||
|
unicaster, |
||||||
|
broadcaster, |
||||||
|
buddyImages, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) OnConnect(client Client, session *Session) { |
||||||
|
api.Unicaster.OnConnect(client, session) |
||||||
|
api.SendSelf(client, session) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) OnIncoming(c ResponseSender, session *Session, msg *DataIncoming) { |
||||||
|
switch msg.Type { |
||||||
|
case "Self": |
||||||
|
api.SendSelf(c, session) |
||||||
|
case "Hello": |
||||||
|
//log.Println("Hello", msg.Hello, c.Index())
|
||||||
|
// TODO(longsleep): Filter room id and user agent.
|
||||||
|
api.UpdateSession(session, &SessionUpdate{Types: []string{"Ua"}, Ua: msg.Hello.Ua}) |
||||||
|
if session.Hello && session.Roomid != msg.Hello.Id { |
||||||
|
api.LeaveRoom(session) |
||||||
|
api.Broadcast(session, session.DataSessionLeft("soft")) |
||||||
|
} |
||||||
|
|
||||||
|
// NOTE(lcooper): Iid filtered for compatibility's sake.
|
||||||
|
// Evaluate sending unconditionally when supported by all clients.
|
||||||
|
if room, err := api.JoinRoom(msg.Hello.Id, msg.Hello.Credentials, session, c); err == nil { |
||||||
|
session.Hello = true |
||||||
|
session.Roomid = msg.Hello.Id |
||||||
|
if msg.Iid != "" { |
||||||
|
c.Reply(msg.Iid, &DataWelcome{ |
||||||
|
Type: "Welcome", |
||||||
|
Room: room, |
||||||
|
Users: api.RoomUsers(session), |
||||||
|
}) |
||||||
|
} |
||||||
|
api.Broadcast(session, session.DataSessionJoined()) |
||||||
|
} else { |
||||||
|
session.Hello = false |
||||||
|
if msg.Iid != "" { |
||||||
|
c.Reply(msg.Iid, err) |
||||||
|
} |
||||||
|
} |
||||||
|
case "Offer": |
||||||
|
// TODO(longsleep): Validate offer
|
||||||
|
api.Unicast(session, msg.Offer.To, msg.Offer) |
||||||
|
case "Candidate": |
||||||
|
// TODO(longsleep): Validate candidate
|
||||||
|
api.Unicast(session, msg.Candidate.To, msg.Candidate) |
||||||
|
case "Answer": |
||||||
|
// TODO(longsleep): Validate Answer
|
||||||
|
api.Unicast(session, msg.Answer.To, msg.Answer) |
||||||
|
case "Users": |
||||||
|
if session.Hello { |
||||||
|
sessions := &DataSessions{Type: "Users", Users: api.RoomUsers(session)} |
||||||
|
c.Reply(msg.Iid, sessions) |
||||||
|
} |
||||||
|
case "Authentication": |
||||||
|
st := msg.Authentication.Authentication |
||||||
|
if st == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if err := api.Authenticate(session, st, ""); err == nil { |
||||||
|
log.Println("Authentication success", session.Userid) |
||||||
|
api.SendSelf(c, session) |
||||||
|
api.BroadcastSessionStatus(session) |
||||||
|
} else { |
||||||
|
log.Println("Authentication failed", err, st.Userid, st.Nonce) |
||||||
|
} |
||||||
|
case "Bye": |
||||||
|
api.Unicast(session, msg.Bye.To, msg.Bye) |
||||||
|
case "Status": |
||||||
|
//log.Println("Status", msg.Status)
|
||||||
|
api.UpdateSession(session, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) |
||||||
|
api.BroadcastSessionStatus(session) |
||||||
|
case "Chat": |
||||||
|
// TODO(longsleep): Limit sent chat messages per incoming connection.
|
||||||
|
if !msg.Chat.Chat.NoEcho { |
||||||
|
api.Unicast(session, session.Id, msg.Chat) |
||||||
|
} |
||||||
|
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339) |
||||||
|
if msg.Chat.To == "" { |
||||||
|
// TODO(longsleep): Check if chat broadcast is allowed.
|
||||||
|
if session.Hello { |
||||||
|
api.CountBroadcastChat() |
||||||
|
api.Broadcast(session, msg.Chat) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { |
||||||
|
if err := api.contactrequestHandler(session, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest); err != nil { |
||||||
|
log.Println("Ignoring invalid contact request.", err) |
||||||
|
return |
||||||
|
} |
||||||
|
msg.Chat.Chat.Status.ContactRequest.Userid = session.Userid() |
||||||
|
} |
||||||
|
if msg.Chat.Chat.Status == nil { |
||||||
|
api.CountUnicastChat() |
||||||
|
} |
||||||
|
|
||||||
|
api.Unicast(session, msg.Chat.To, msg.Chat) |
||||||
|
if msg.Chat.Chat.Mid != "" { |
||||||
|
// Send out delivery confirmation status chat message.
|
||||||
|
api.Unicast(session, session.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}}) |
||||||
|
} |
||||||
|
} |
||||||
|
case "Conference": |
||||||
|
// Check conference maximum size.
|
||||||
|
if len(msg.Conference.Conference) > maxConferenceSize { |
||||||
|
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference)) |
||||||
|
} else { |
||||||
|
// Send conference update to anyone.
|
||||||
|
for _, id := range msg.Conference.Conference { |
||||||
|
if id != session.Id { |
||||||
|
api.Unicast(session, id, msg.Conference) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
case "Alive": |
||||||
|
c.Reply(msg.Iid, msg.Alive) |
||||||
|
case "Sessions": |
||||||
|
var users []*DataSession |
||||||
|
switch msg.Sessions.Sessions.Type { |
||||||
|
case "contact": |
||||||
|
if userID, err := api.getContactID(session, msg.Sessions.Sessions.Token); err == nil { |
||||||
|
users = api.GetUserSessions(session, userID) |
||||||
|
} else { |
||||||
|
log.Printf(err.Error()) |
||||||
|
} |
||||||
|
case "session": |
||||||
|
id, err := session.attestation.Decode(msg.Sessions.Sessions.Token) |
||||||
|
if err != nil { |
||||||
|
log.Printf("Failed to decode incoming attestation", err, msg.Sessions.Sessions.Token) |
||||||
|
break |
||||||
|
} |
||||||
|
session, ok := api.GetSession(id) |
||||||
|
if !ok { |
||||||
|
log.Printf("Cannot retrieve session for id %s", id) |
||||||
|
break |
||||||
|
} |
||||||
|
users = make([]*DataSession, 1, 1) |
||||||
|
users[0] = session.Data() |
||||||
|
default: |
||||||
|
log.Printf("Unkown incoming sessions request type %s", msg.Sessions.Sessions.Type) |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(lcooper): We ought to reply with a *DataError here if failed.
|
||||||
|
if users != nil { |
||||||
|
c.Reply(msg.Iid, &DataSessions{Type: "Sessions", Users: users, Sessions: msg.Sessions.Sessions}) |
||||||
|
} |
||||||
|
case "Room": |
||||||
|
if room, err := api.UpdateRoom(session, msg.Room); err == nil { |
||||||
|
api.Broadcast(session, room) |
||||||
|
c.Reply(msg.Iid, room) |
||||||
|
} else { |
||||||
|
c.Reply(msg.Iid, err) |
||||||
|
} |
||||||
|
default: |
||||||
|
log.Println("OnText unhandled message type", msg.Type) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) OnDisconnect(session *Session) { |
||||||
|
dsl := session.DataSessionLeft("hard") |
||||||
|
if session.Hello { |
||||||
|
api.LeaveRoom(session) |
||||||
|
api.Broadcast(session, dsl) |
||||||
|
} |
||||||
|
|
||||||
|
session.RunForAllSubscribers(func(session *Session) { |
||||||
|
log.Println("Notifying subscriber that we are gone", session.Id, session.Id) |
||||||
|
api.Unicast(session, session.Id, dsl) |
||||||
|
}) |
||||||
|
|
||||||
|
api.Unicaster.OnDisconnect(session) |
||||||
|
|
||||||
|
api.buddyImages.Delete(session.Id) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) SendSelf(c Responder, session *Session) { |
||||||
|
token, err := api.EncodeSessionToken(session) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error in OnRegister", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
log.Println("Created new session token", len(token), token) |
||||||
|
self := &DataSelf{ |
||||||
|
Type: "Self", |
||||||
|
Id: session.Id, |
||||||
|
Sid: session.Sid, |
||||||
|
Userid: session.Userid(), |
||||||
|
Suserid: api.EncodeSessionUserID(session), |
||||||
|
Token: token, |
||||||
|
Version: api.version, |
||||||
|
Turn: api.CreateTurnData(session), |
||||||
|
Stun: api.StunURIs, |
||||||
|
} |
||||||
|
c.Reply("", self) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) UpdateSession(session *Session, s *SessionUpdate) uint64 { |
||||||
|
if s.Status != nil { |
||||||
|
status, ok := s.Status.(map[string]interface{}) |
||||||
|
if ok && status["buddyPicture"] != nil { |
||||||
|
pic := status["buddyPicture"].(string) |
||||||
|
if strings.HasPrefix(pic, "data:") { |
||||||
|
imageId := api.buddyImages.Update(session.Id, pic[5:]) |
||||||
|
if imageId != "" { |
||||||
|
status["buddyPicture"] = "img:" + imageId |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return session.Update(s) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) BroadcastSessionStatus(session *Session) { |
||||||
|
if session.Hello { |
||||||
|
api.Broadcast(session, session.DataSessionStatus()) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,244 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"errors" |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
testAppVersion string = "0.0.0+unittests" |
||||||
|
) |
||||||
|
|
||||||
|
type fakeClient struct { |
||||||
|
replies map[string]interface{} |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeClient) Send(_ Buffer) { |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeClient) Reply(iid string, msg interface{}) { |
||||||
|
if fake.replies == nil { |
||||||
|
fake.replies = make(map[string]interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
fake.replies[iid] = msg |
||||||
|
} |
||||||
|
|
||||||
|
type fakeRoomManager struct { |
||||||
|
joinedRoomID string |
||||||
|
leftRoomID string |
||||||
|
roomUsers []*DataSession |
||||||
|
joinedID string |
||||||
|
joinError error |
||||||
|
leftID string |
||||||
|
broadcasts []interface{} |
||||||
|
updatedRoom *DataRoom |
||||||
|
updateError error |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession { |
||||||
|
return fake.roomUsers |
||||||
|
} |
||||||
|
|
||||||
|
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) { |
||||||
|
fake.leftID = session.Roomid |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) Broadcast(_ *Session, msg interface{}) { |
||||||
|
fake.broadcasts = append(fake.broadcasts, msg) |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) UpdateRoom(_ *Session, _ *DataRoom) (*DataRoom, error) { |
||||||
|
return fake.updatedRoom, fake.updateError |
||||||
|
} |
||||||
|
|
||||||
|
func assertReply(t *testing.T, client *fakeClient, iid string) interface{} { |
||||||
|
msg, ok := client.replies[iid] |
||||||
|
if !ok { |
||||||
|
t.Fatalf("No response received for Iid %v", iid) |
||||||
|
} |
||||||
|
return msg |
||||||
|
} |
||||||
|
|
||||||
|
func assertErrorReply(t *testing.T, client *fakeClient, iid, code string) { |
||||||
|
err, ok := assertReply(t, client, iid).(*DataError) |
||||||
|
if !ok { |
||||||
|
t.Fatalf("Expected response message to be an Error") |
||||||
|
} |
||||||
|
|
||||||
|
if err.Type != "Error" { |
||||||
|
t.Error("Message did not have the correct type") |
||||||
|
} |
||||||
|
|
||||||
|
if err.Code != code { |
||||||
|
t.Errorf("Expected error code to be %v, but was %v", code, err.Code) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func NewTestChannellingAPI() (ChannellingAPI, *fakeClient, *Session, *fakeRoomManager) { |
||||||
|
client, roomManager, session := &fakeClient{}, &fakeRoomManager{}, &Session{} |
||||||
|
return NewChannellingAPI(testAppVersion, nil, roomManager, nil, nil, nil, nil, nil, nil, roomManager, nil), client, session, roomManager |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_HelloMessage_JoinsTheSelectedRoom(t *testing.T) { |
||||||
|
roomID, ua := "foobar", "unit tests" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID, Ua: ua}}) |
||||||
|
|
||||||
|
if roomManager.joinedID != roomID { |
||||||
|
t.Errorf("Expected to have joined room %v, but got %v", roomID, roomManager.joinedID) |
||||||
|
} |
||||||
|
|
||||||
|
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 1 { |
||||||
|
t.Fatalf("Expected 1 broadcast, but got %d", broadcastCount) |
||||||
|
} |
||||||
|
|
||||||
|
dataSession, ok := roomManager.broadcasts[0].(*DataSession) |
||||||
|
if !ok { |
||||||
|
t.Fatal("Expected a session data broadcast") |
||||||
|
} |
||||||
|
|
||||||
|
if dataSession.Ua != ua { |
||||||
|
t.Errorf("Expected to have broadcasted a user agent of %v, but was %v", ua, dataSession.Ua) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_HelloMessage_LeavesAnyPreviouslyJoinedRooms(t *testing.T) { |
||||||
|
roomID := "foobar" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: roomID}}) |
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{Id: "baz"}}) |
||||||
|
|
||||||
|
if roomManager.leftID != roomID { |
||||||
|
t.Errorf("Expected to have left room %v, but got %v", roomID, roomManager.leftID) |
||||||
|
} |
||||||
|
|
||||||
|
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 3 { |
||||||
|
t.Fatalf("Expected 3 broadcasts, but got %d", broadcastCount) |
||||||
|
} |
||||||
|
|
||||||
|
dataSession, ok := roomManager.broadcasts[1].(*DataSession) |
||||||
|
if !ok { |
||||||
|
t.Fatal("Expected a session data broadcast") |
||||||
|
} |
||||||
|
|
||||||
|
if status := "soft"; dataSession.Status != status { |
||||||
|
t.Errorf("Expected to have broadcast a leave status of of %v, but was %v", status, dataSession.Status) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_HelloMessage_DoesNotJoinIfNotPermitted(t *testing.T) { |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
roomManager.joinError = errors.New("Can't enter this room") |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Hello: &DataHello{}}) |
||||||
|
|
||||||
|
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 0 { |
||||||
|
t.Fatalf("Expected no broadcasts, but got %d", broadcastCount) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAWelcome(t *testing.T) { |
||||||
|
iid, roomID := "foo", "a-room" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
roomManager.roomUsers = []*DataSession{&DataSession{}} |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{Id: roomID}}) |
||||||
|
|
||||||
|
msg, ok := client.replies[iid] |
||||||
|
if !ok { |
||||||
|
t.Fatalf("No response received for Iid %v", iid) |
||||||
|
} |
||||||
|
|
||||||
|
welcome, ok := msg.(*DataWelcome) |
||||||
|
if !ok { |
||||||
|
t.Fatalf("Expected response message %#v to be a Welcome", msg) |
||||||
|
} |
||||||
|
|
||||||
|
if welcome.Type != "Welcome" { |
||||||
|
t.Error("Message did not have the correct type") |
||||||
|
} |
||||||
|
|
||||||
|
if welcome.Room == nil || welcome.Room.Name != roomID { |
||||||
|
t.Errorf("Expected room with name %v, but got %#v", roomID, welcome.Room) |
||||||
|
} |
||||||
|
|
||||||
|
if len(welcome.Users) != len(roomManager.roomUsers) { |
||||||
|
t.Errorf("Expected to get users %#v, but was %#v", roomManager.roomUsers, welcome.Users) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_HelloMessageWithAnIid_RespondsWithAnErrorIfTheRoomCannotBeJoined(t *testing.T) { |
||||||
|
iid := "foo" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
roomManager.joinError = &DataError{Type: "Error", Code: "bad_join"} |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: iid, Hello: &DataHello{}}) |
||||||
|
|
||||||
|
assertErrorReply(t, client, iid, "bad_join") |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAndBroadcastsTheUpdatedRoom(t *testing.T) { |
||||||
|
iid, roomName := "123", "foo" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
roomManager.updatedRoom = &DataRoom{Name: "FOO"} |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}}) |
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}}) |
||||||
|
|
||||||
|
room, ok := assertReply(t, client, iid).(*DataRoom) |
||||||
|
if !ok { |
||||||
|
t.Fatalf("Expected response message to be a Room") |
||||||
|
} |
||||||
|
|
||||||
|
if room.Name != roomManager.updatedRoom.Name { |
||||||
|
t.Errorf("Expected updated room with name %v, but got %#v", roomManager.updatedRoom, room) |
||||||
|
} |
||||||
|
|
||||||
|
if broadcastCount := len(roomManager.broadcasts); broadcastCount != 2 { |
||||||
|
t.Fatalf("Expected 1 broadcasts, but got %d", broadcastCount) |
||||||
|
} |
||||||
|
|
||||||
|
if _, ok := roomManager.broadcasts[1].(*DataRoom); !ok { |
||||||
|
t.Fatal("Expected a room data broadcast") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func Test_ChannellingAPI_OnIncoming_RoomMessage_RespondsWithAnErrorIfUpdatingTheRoomFails(t *testing.T) { |
||||||
|
iid, roomName := "123", "foo" |
||||||
|
api, client, session, roomManager := NewTestChannellingAPI() |
||||||
|
roomManager.updateError = &DataError{Type: "Error", Code: "a_room_error", Message: ""} |
||||||
|
|
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Hello", Iid: "0", Hello: &DataHello{Id: roomName}}) |
||||||
|
api.OnIncoming(client, session, &DataIncoming{Type: "Room", Iid: iid, Room: &DataRoom{Name: roomName}}) |
||||||
|
|
||||||
|
assertErrorReply(t, client, iid, "a_room_error") |
||||||
|
} |
||||||
@ -0,0 +1,87 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"log" |
||||||
|
) |
||||||
|
|
||||||
|
type Sender interface { |
||||||
|
Send(Buffer) |
||||||
|
} |
||||||
|
|
||||||
|
type ResponseSender interface { |
||||||
|
Sender |
||||||
|
Responder |
||||||
|
} |
||||||
|
|
||||||
|
type Responder interface { |
||||||
|
Reply(iid string, m interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type Client interface { |
||||||
|
ResponseSender |
||||||
|
Session() *Session |
||||||
|
Index() uint64 |
||||||
|
Close(bool) |
||||||
|
} |
||||||
|
|
||||||
|
type client struct { |
||||||
|
Codec |
||||||
|
ChannellingAPI |
||||||
|
Connection |
||||||
|
session *Session |
||||||
|
} |
||||||
|
|
||||||
|
func NewClient(codec Codec, api ChannellingAPI, session *Session) *client { |
||||||
|
return &client{codec, api, nil, session} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnConnect(conn Connection) { |
||||||
|
client.Connection = conn |
||||||
|
client.ChannellingAPI.OnConnect(client, client.session) |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnText(b Buffer) { |
||||||
|
if incoming, err := client.DecodeIncoming(b); err == nil { |
||||||
|
client.OnIncoming(client, client.session, incoming) |
||||||
|
} else { |
||||||
|
log.Println("OnText error while decoding JSON", err) |
||||||
|
log.Printf("JSON:\n%s\n", b) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnDisconnect() { |
||||||
|
client.ChannellingAPI.OnDisconnect(client.session) |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) Reply(iid string, m interface{}) { |
||||||
|
outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m} |
||||||
|
if b, err := client.EncodeOutgoing(outgoing); err == nil { |
||||||
|
client.Send(b) |
||||||
|
b.Decref() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) Session() *Session { |
||||||
|
return client.session |
||||||
|
} |
||||||
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,69 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"log" |
||||||
|
) |
||||||
|
|
||||||
|
type IncomingDecoder interface { |
||||||
|
DecodeIncoming(Buffer) (*DataIncoming, error) |
||||||
|
} |
||||||
|
|
||||||
|
type OutgoingEncoder interface { |
||||||
|
EncodeOutgoing(*DataOutgoing) (Buffer, error) |
||||||
|
} |
||||||
|
|
||||||
|
type Codec interface { |
||||||
|
NewBuffer() Buffer |
||||||
|
IncomingDecoder |
||||||
|
OutgoingEncoder |
||||||
|
} |
||||||
|
|
||||||
|
type incomingCodec struct { |
||||||
|
buffers BufferCache |
||||||
|
} |
||||||
|
|
||||||
|
func NewCodec() Codec { |
||||||
|
return &incomingCodec{NewBufferCache(1024, bytes.MinRead)} |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) NewBuffer() Buffer { |
||||||
|
return codec.buffers.New() |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) DecodeIncoming(b Buffer) (*DataIncoming, error) { |
||||||
|
incoming := &DataIncoming{} |
||||||
|
return incoming, json.Unmarshal(b.Bytes(), incoming) |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) EncodeOutgoing(outgoing *DataOutgoing) (Buffer, error) { |
||||||
|
b := codec.NewBuffer() |
||||||
|
if err := json.NewEncoder(b).Encode(outgoing); err != nil { |
||||||
|
log.Println("Error while encoding JSON", err) |
||||||
|
b.Decref() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return b, nil |
||||||
|
} |
||||||
@ -0,0 +1,192 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"log" |
||||||
|
"sync" |
||||||
|
) |
||||||
|
|
||||||
|
type RoomStatusManager interface { |
||||||
|
RoomUsers(*Session) []*DataSession |
||||||
|
JoinRoom(string, *DataRoomCredentials, *Session, Sender) (*DataRoom, error) |
||||||
|
LeaveRoom(*Session) |
||||||
|
UpdateRoom(*Session, *DataRoom) (*DataRoom, error) |
||||||
|
} |
||||||
|
|
||||||
|
type Broadcaster interface { |
||||||
|
Broadcast(*Session, interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type RoomStats interface { |
||||||
|
RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string) |
||||||
|
} |
||||||
|
|
||||||
|
type RoomManager interface { |
||||||
|
RoomStatusManager |
||||||
|
Broadcaster |
||||||
|
RoomStats |
||||||
|
} |
||||||
|
|
||||||
|
type roomManager struct { |
||||||
|
sync.RWMutex |
||||||
|
OutgoingEncoder |
||||||
|
defaultRoomEnabled bool |
||||||
|
globalRoomID string |
||||||
|
roomTable map[string]RoomWorker |
||||||
|
} |
||||||
|
|
||||||
|
func NewRoomManager(config *Config, encoder OutgoingEncoder) RoomManager { |
||||||
|
return &roomManager{ |
||||||
|
sync.RWMutex{}, |
||||||
|
encoder, |
||||||
|
config.DefaultRoomEnabled, |
||||||
|
config.globalRoomid, |
||||||
|
make(map[string]RoomWorker), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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) 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.Roomid); ok { |
||||||
|
room.Leave(session) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) UpdateRoom(session *Session, room *DataRoom) (*DataRoom, error) { |
||||||
|
if !session.Hello || session.Roomid != room.Name { |
||||||
|
return nil, &DataError{Type: "Error", Code: "not_in_room", Message: "Cannot update other rooms"} |
||||||
|
} |
||||||
|
// 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 |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) Broadcast(session *Session, m interface{}) { |
||||||
|
outgoing := &DataOutgoing{ |
||||||
|
From: session.Id, |
||||||
|
A: session.Attestation(), |
||||||
|
Data: m, |
||||||
|
} |
||||||
|
|
||||||
|
message, err := rooms.EncodeOutgoing(outgoing) |
||||||
|
if err != nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
id := session.Roomid |
||||||
|
if id != "" && id == rooms.globalRoomID { |
||||||
|
rooms.RLock() |
||||||
|
for _, room := range rooms.roomTable { |
||||||
|
room.Broadcast(session, message) |
||||||
|
} |
||||||
|
rooms.RUnlock() |
||||||
|
} else if room, ok := rooms.Get(id); ok { |
||||||
|
room.Broadcast(session, message) |
||||||
|
} else { |
||||||
|
log.Printf("No room named %s found for broadcast message %#v", id, m) |
||||||
|
} |
||||||
|
message.Decref() |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) RoomInfo(includeSessions bool) (count int, sessionInfo map[string][]string) { |
||||||
|
rooms.RLock() |
||||||
|
defer rooms.RUnlock() |
||||||
|
|
||||||
|
count = len(rooms.roomTable) |
||||||
|
if includeSessions { |
||||||
|
sessionInfo := make(map[string][]string) |
||||||
|
for roomid, room := range rooms.roomTable { |
||||||
|
sessionInfo[roomid] = room.SessionIDs() |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) Get(id string) (room RoomWorker, ok bool) { |
||||||
|
rooms.RLock() |
||||||
|
room, ok = rooms.roomTable[id] |
||||||
|
rooms.RUnlock() |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) GetOrCreate(id string, credentials *DataRoomCredentials) RoomWorker { |
||||||
|
room, ok := rooms.Get(id) |
||||||
|
if !ok { |
||||||
|
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, credentials) |
||||||
|
rooms.roomTable[id] = room |
||||||
|
rooms.Unlock() |
||||||
|
go func() { |
||||||
|
// Start room, this blocks until room expired.
|
||||||
|
room.Start() |
||||||
|
// Cleanup room when we are done.
|
||||||
|
rooms.Lock() |
||||||
|
defer rooms.Unlock() |
||||||
|
delete(rooms.roomTable, id) |
||||||
|
log.Printf("Cleaned up room '%s'\n", id) |
||||||
|
}() |
||||||
|
} else { |
||||||
|
rooms.Unlock() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return room |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) GlobalUsers() []*roomUser { |
||||||
|
if rooms.globalRoomID == "" { |
||||||
|
return make([]*roomUser, 0) |
||||||
|
} |
||||||
|
rooms.RLock() |
||||||
|
if room, ok := rooms.roomTable[rooms.globalRoomID]; ok { |
||||||
|
rooms.RUnlock() |
||||||
|
return room.Users() |
||||||
|
} |
||||||
|
|
||||||
|
rooms.RUnlock() |
||||||
|
return make([]*roomUser, 0) |
||||||
|
} |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
/* |
||||||
|
* 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 NewTestRoomManager() RoomManager { |
||||||
|
return NewRoomManager(&Config{}, nil) |
||||||
|
} |
||||||
|
|
||||||
|
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfNoRoomHasBeenJoined(t *testing.T) { |
||||||
|
roomManager := NewTestRoomManager() |
||||||
|
_, err := roomManager.UpdateRoom(&Session{}, nil) |
||||||
|
|
||||||
|
assertDataError(t, err, "not_in_room") |
||||||
|
} |
||||||
|
|
||||||
|
func Test_RoomManager_UpdateRoom_ReturnsAnErrorIfUpdatingAnUnjoinedRoom(t *testing.T) { |
||||||
|
roomManager := NewTestRoomManager() |
||||||
|
session := &Session{Hello: true, Roomid: "foo"} |
||||||
|
_, err := roomManager.UpdateRoom(session, &DataRoom{Name: "bar"}) |
||||||
|
assertDataError(t, err, "not_in_room") |
||||||
|
} |
||||||
|
|
||||||
|
func Test_RoomManager_UpdateRoom_ReturnsACorrectlyTypedDocument(t *testing.T) { |
||||||
|
roomManager := NewTestRoomManager() |
||||||
|
session := &Session{Hello: true, Roomid: "foo"} |
||||||
|
room, err := roomManager.UpdateRoom(session, &DataRoom{Name: session.Roomid}) |
||||||
|
if err != nil { |
||||||
|
t.Fatalf("Unexpected error %v updating room", err) |
||||||
|
} |
||||||
|
|
||||||
|
if room.Type != "Room" { |
||||||
|
t.Errorf("Expected document type to be Room, but was %v", room.Type) |
||||||
|
} |
||||||
|
} |
||||||
@ -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) |
||||||
|
} |
||||||
|
} |
||||||
@ -1,280 +0,0 @@ |
|||||||
/* |
|
||||||
* 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 ( |
|
||||||
"encoding/json" |
|
||||||
"log" |
|
||||||
"sync/atomic" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
maxConferenceSize = 100 |
|
||||||
) |
|
||||||
|
|
||||||
type Server struct { |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnRegister(c *Connection) { |
|
||||||
//log.Println("OnRegister", c.id)
|
|
||||||
if token, err := c.h.EncodeSessionToken(c.Session.Token()); err == nil { |
|
||||||
log.Println("Created new session token", len(token), token) |
|
||||||
// Send stuff back.
|
|
||||||
s.Unicast(c, c.Id, &DataSelf{ |
|
||||||
Type: "Self", |
|
||||||
Id: c.Id, |
|
||||||
Sid: c.Session.Sid, |
|
||||||
Userid: c.Session.Userid(), |
|
||||||
Suserid: c.h.CreateSuserid(c.Session), |
|
||||||
Token: token, |
|
||||||
Version: c.h.version, |
|
||||||
Turn: c.h.CreateTurnData(c.Id), |
|
||||||
Stun: c.h.config.StunURIs, |
|
||||||
}) |
|
||||||
} else { |
|
||||||
log.Println("Error in OnRegister", c.Idx, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnUnregister(c *Connection) { |
|
||||||
//log.Println("OnUnregister", c.id)
|
|
||||||
dsl := c.Session.DataSessionLeft("hard") |
|
||||||
if c.Hello { |
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) |
|
||||||
s.Broadcast(c, dsl) |
|
||||||
} |
|
||||||
c.Session.RunForAllSubscribers(func(session *Session) { |
|
||||||
log.Println("Notifying subscriber that we are gone", c.Id, session.Id) |
|
||||||
s.Unicast(c, session.Id, dsl) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnText(c *Connection, b Buffer) { |
|
||||||
|
|
||||||
//log.Printf("OnText from %d: %s\n", c.Id, b)
|
|
||||||
var msg DataIncoming |
|
||||||
err := json.Unmarshal(b.Bytes(), &msg) |
|
||||||
if err != nil { |
|
||||||
log.Println("OnText error while decoding JSON", err) |
|
||||||
log.Printf("JSON:\n%s\n", b) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
switch msg.Type { |
|
||||||
case "Self": |
|
||||||
s.OnRegister(c) |
|
||||||
case "Hello": |
|
||||||
//log.Println("Hello", msg.Hello, c.Idx)
|
|
||||||
// TODO(longsleep): Filter room id and user agent.
|
|
||||||
s.UpdateSession(c, &SessionUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua}) |
|
||||||
if c.Hello && c.Roomid != msg.Hello.Id { |
|
||||||
// Room changed.
|
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) |
|
||||||
s.Broadcast(c, c.Session.DataSessionLeft("soft")) |
|
||||||
} |
|
||||||
c.Roomid = msg.Hello.Id |
|
||||||
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { |
|
||||||
c.Hello = true |
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true}) |
|
||||||
s.Broadcast(c, c.Session.DataSessionJoined()) |
|
||||||
} else { |
|
||||||
c.Hello = false |
|
||||||
} |
|
||||||
case "Offer": |
|
||||||
// TODO(longsleep): Validate offer
|
|
||||||
s.Unicast(c, msg.Offer.To, msg.Offer) |
|
||||||
case "Candidate": |
|
||||||
// TODO(longsleep): Validate candidate
|
|
||||||
s.Unicast(c, msg.Candidate.To, msg.Candidate) |
|
||||||
case "Answer": |
|
||||||
// TODO(longsleep): Validate Answer
|
|
||||||
s.Unicast(c, msg.Answer.To, msg.Answer) |
|
||||||
case "Users": |
|
||||||
if c.Hello { |
|
||||||
s.Users(c) |
|
||||||
} |
|
||||||
case "Authentication": |
|
||||||
if msg.Authentication.Authentication != nil && s.Authenticate(c, msg.Authentication.Authentication) { |
|
||||||
s.OnRegister(c) |
|
||||||
if c.Hello { |
|
||||||
s.Broadcast(c, c.Session.DataSessionStatus()) |
|
||||||
} |
|
||||||
} |
|
||||||
case "Bye": |
|
||||||
s.Unicast(c, msg.Bye.To, msg.Bye) |
|
||||||
case "Status": |
|
||||||
//log.Println("Status", msg.Status)
|
|
||||||
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) |
|
||||||
if c.Hello { |
|
||||||
s.Broadcast(c, c.Session.DataSessionStatus()) |
|
||||||
} |
|
||||||
case "Chat": |
|
||||||
// TODO(longsleep): Limit sent chat messages per incoming connection.
|
|
||||||
if !msg.Chat.Chat.NoEcho { |
|
||||||
s.Unicast(c, c.Id, msg.Chat) |
|
||||||
} |
|
||||||
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339) |
|
||||||
if msg.Chat.To == "" { |
|
||||||
// TODO(longsleep): Check if chat broadcast is allowed.
|
|
||||||
if c.Hello { |
|
||||||
atomic.AddUint64(&c.h.broadcastChatMessages, 1) |
|
||||||
s.Broadcast(c, msg.Chat) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { |
|
||||||
err = s.ContactRequest(c, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest) |
|
||||||
if err != nil { |
|
||||||
log.Println("Ignoring invalid contact request.", err) |
|
||||||
return |
|
||||||
} |
|
||||||
msg.Chat.Chat.Status.ContactRequest.Userid = c.Session.Userid() |
|
||||||
} |
|
||||||
atomic.AddUint64(&c.h.unicastChatMessages, 1) |
|
||||||
s.Unicast(c, msg.Chat.To, msg.Chat) |
|
||||||
if msg.Chat.Chat.Mid != "" { |
|
||||||
// Send out delivery confirmation status chat message.
|
|
||||||
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}}) |
|
||||||
} |
|
||||||
} |
|
||||||
case "Conference": |
|
||||||
// Check conference maximum size.
|
|
||||||
if len(msg.Conference.Conference) > maxConferenceSize { |
|
||||||
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference)) |
|
||||||
} else { |
|
||||||
// Send conference update to anyone.
|
|
||||||
for _, id := range msg.Conference.Conference { |
|
||||||
if id != c.Id { |
|
||||||
//log.Println("participant", id)
|
|
||||||
s.Unicast(c, id, msg.Conference) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
case "Alive": |
|
||||||
s.Alive(c, msg.Alive, msg.Iid) |
|
||||||
case "Sessions": |
|
||||||
s.Sessions(c, msg.Sessions.Sessions, msg.Iid) |
|
||||||
default: |
|
||||||
log.Println("OnText unhandled message type", msg.Type) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Unicast(c *Connection, to string, m interface{}) { |
|
||||||
|
|
||||||
outgoing := &DataOutgoing{From: c.Id, To: to, Data: m} |
|
||||||
if !c.isClosing && c.Id != to { |
|
||||||
outgoing.A = c.Session.Attestation() |
|
||||||
} |
|
||||||
b := c.h.buffers.New() |
|
||||||
encoder := json.NewEncoder(b) |
|
||||||
err := encoder.Encode(outgoing) |
|
||||||
if err != nil { |
|
||||||
b.Decref() |
|
||||||
log.Println("Unicast error while encoding JSON", err) |
|
||||||
return |
|
||||||
} |
|
||||||
//log.Println("Unicast", b)
|
|
||||||
|
|
||||||
var msg = &MessageRequest{From: c.Id, To: to, Message: b} |
|
||||||
c.h.unicastHandler(msg) |
|
||||||
b.Decref() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Broadcast(c *Connection, m interface{}) { |
|
||||||
|
|
||||||
b := c.h.buffers.New() |
|
||||||
encoder := json.NewEncoder(b) |
|
||||||
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: m, A: c.Session.Attestation()}) |
|
||||||
if err != nil { |
|
||||||
b.Decref() |
|
||||||
log.Println("Broadcast error while encoding JSON", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if c.h.isGlobalRoomid(c.Roomid) { |
|
||||||
c.h.RunForAllRooms(func(room *RoomWorker) { |
|
||||||
var msg = &MessageRequest{From: c.Id, Message: b, Id: room.Id} |
|
||||||
room.broadcastHandler(msg) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
var msg = &MessageRequest{From: c.Id, Message: b, Id: c.Roomid} |
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.broadcastHandler(msg) |
|
||||||
} |
|
||||||
b.Decref() |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) { |
|
||||||
|
|
||||||
c.h.aliveHandler(c, alive, iid) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) { |
|
||||||
|
|
||||||
c.h.sessionsHandler(c, srq, iid) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 { |
|
||||||
|
|
||||||
su.Id = c.Id |
|
||||||
return c.h.sessionupdateHandler(su) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) { |
|
||||||
|
|
||||||
return c.h.contactrequestHandler(c, to, cr) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Users(c *Connection) { |
|
||||||
|
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.usersHandler(c) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool { |
|
||||||
|
|
||||||
err := c.h.authenticateHandler(c.Session, st, "") |
|
||||||
if err == nil { |
|
||||||
log.Println("Authentication success", c.Id, c.Idx, c.Session.Userid) |
|
||||||
return true |
|
||||||
} else { |
|
||||||
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce) |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) { |
|
||||||
|
|
||||||
rcu.Sessionid = c.Id |
|
||||||
rcu.Connection = c |
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.connectionHandler(rcu) |
|
||||||
|
|
||||||
} |
|
||||||
@ -0,0 +1,173 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"crypto/sha256" |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" |
||||||
|
) |
||||||
|
|
||||||
|
type UserStats interface { |
||||||
|
UserInfo(bool) (int, map[string]*DataUser) |
||||||
|
} |
||||||
|
|
||||||
|
type SessionManager interface { |
||||||
|
UserStats |
||||||
|
RetrieveUsersWith(func(*http.Request) (string, error)) |
||||||
|
CreateSession(*http.Request) *Session |
||||||
|
DestroySession(*Session) |
||||||
|
Authenticate(*Session, *SessionToken, string) error |
||||||
|
GetUserSessions(session *Session, id string) []*DataSession |
||||||
|
} |
||||||
|
|
||||||
|
type sessionManager struct { |
||||||
|
Tickets |
||||||
|
sync.RWMutex |
||||||
|
config *Config |
||||||
|
userTable map[string]*User |
||||||
|
fakesessionTable map[string]*Session |
||||||
|
useridRetriever func(*http.Request) (string, error) |
||||||
|
attestations *securecookie.SecureCookie |
||||||
|
} |
||||||
|
|
||||||
|
func NewSessionManager(config *Config, tickets Tickets, sessionSecret []byte) SessionManager { |
||||||
|
sessionManager := &sessionManager{ |
||||||
|
tickets, |
||||||
|
sync.RWMutex{}, |
||||||
|
config, |
||||||
|
make(map[string]*User), |
||||||
|
make(map[string]*Session), |
||||||
|
nil, |
||||||
|
nil, |
||||||
|
} |
||||||
|
|
||||||
|
sessionManager.attestations = securecookie.New(sessionSecret, nil) |
||||||
|
sessionManager.attestations.MaxAge(300) // 5 minutes
|
||||||
|
sessionManager.attestations.HashFunc(sha256.New) |
||||||
|
|
||||||
|
return sessionManager |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) UserInfo(details bool) (userCount int, users map[string]*DataUser) { |
||||||
|
sessionManager.RLock() |
||||||
|
defer sessionManager.RUnlock() |
||||||
|
|
||||||
|
userCount = len(sessionManager.userTable) |
||||||
|
if details { |
||||||
|
users := make(map[string]*DataUser) |
||||||
|
for userid, user := range sessionManager.userTable { |
||||||
|
users[userid] = user.Data() |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) RetrieveUsersWith(retriever func(*http.Request) (string, error)) { |
||||||
|
sessionManager.useridRetriever = retriever |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) CreateSession(request *http.Request) *Session { |
||||||
|
request.ParseForm() |
||||||
|
token := request.FormValue("t") |
||||||
|
st := sessionManager.DecodeSessionToken(token) |
||||||
|
|
||||||
|
var userid string |
||||||
|
if sessionManager.config.UsersEnabled { |
||||||
|
if sessionManager.useridRetriever != nil { |
||||||
|
userid, _ = sessionManager.useridRetriever(request) |
||||||
|
if userid == "" { |
||||||
|
userid = st.Userid |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
session := NewSession(sessionManager.attestations, st.Id, st.Sid) |
||||||
|
|
||||||
|
if userid != "" { |
||||||
|
// XXX(lcooper): Should errors be handled here?
|
||||||
|
sessionManager.Authenticate(session, st, userid) |
||||||
|
} |
||||||
|
|
||||||
|
return session |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) DestroySession(session *Session) { |
||||||
|
session.Close() |
||||||
|
|
||||||
|
sessionManager.Lock() |
||||||
|
if suserid := session.Userid(); suserid != "" { |
||||||
|
user, ok := sessionManager.userTable[suserid] |
||||||
|
if ok && user.RemoveSession(session) { |
||||||
|
delete(sessionManager.userTable, suserid) |
||||||
|
} |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) Authenticate(session *Session, st *SessionToken, userid string) error { |
||||||
|
if err := session.Authenticate(sessionManager.Realm(), st, userid); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Authentication success.
|
||||||
|
suserid := session.Userid() |
||||||
|
sessionManager.Lock() |
||||||
|
user, ok := sessionManager.userTable[suserid] |
||||||
|
if !ok { |
||||||
|
user = NewUser(suserid) |
||||||
|
sessionManager.userTable[suserid] = user |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
user.AddSession(session) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) GetUserSessions(session *Session, userid string) (users []*DataSession) { |
||||||
|
var ( |
||||||
|
user *User |
||||||
|
ok bool |
||||||
|
) |
||||||
|
sessionManager.RLock() |
||||||
|
user, ok = sessionManager.userTable[userid] |
||||||
|
sessionManager.RUnlock() |
||||||
|
if !ok { |
||||||
|
// No user. Create fake session.
|
||||||
|
sessionManager.Lock() |
||||||
|
session, ok := sessionManager.fakesessionTable[userid] |
||||||
|
if !ok { |
||||||
|
st := sessionManager.FakeSessionToken(userid) |
||||||
|
session = NewSession(sessionManager.attestations, st.Id, st.Sid) |
||||||
|
session.SetUseridFake(st.Userid) |
||||||
|
sessionManager.fakesessionTable[userid] = session |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
users = make([]*DataSession, 1, 1) |
||||||
|
users[0] = session.Data() |
||||||
|
} else { |
||||||
|
// Add sessions for foreign user.
|
||||||
|
users = user.SubscribeSessions(session) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,104 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"sync/atomic" |
||||||
|
) |
||||||
|
|
||||||
|
type HubStat struct { |
||||||
|
Rooms int `json:"rooms"` |
||||||
|
Connections int `json:"connections"` |
||||||
|
Sessions int `json:"sessions"` |
||||||
|
Users int `json:"users"` |
||||||
|
Count uint64 `json:"count"` |
||||||
|
BroadcastChatMessages uint64 `json:"broadcastchatmessages"` |
||||||
|
UnicastChatMessages uint64 `json:"unicastchatmessages"` |
||||||
|
IdsInRoom map[string][]string `json:"idsinroom,omitempty"` |
||||||
|
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"` |
||||||
|
UsersById map[string]*DataUser `json:"usersbyid,omitempty"` |
||||||
|
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type ConnectionCounter interface { |
||||||
|
CountConnection() uint64 |
||||||
|
} |
||||||
|
|
||||||
|
type StatsCounter interface { |
||||||
|
CountBroadcastChat() |
||||||
|
CountUnicastChat() |
||||||
|
} |
||||||
|
|
||||||
|
type StatsGenerator interface { |
||||||
|
Stat(details bool) *HubStat |
||||||
|
} |
||||||
|
|
||||||
|
type StatsManager interface { |
||||||
|
ConnectionCounter |
||||||
|
StatsCounter |
||||||
|
StatsGenerator |
||||||
|
} |
||||||
|
|
||||||
|
type statsManager struct { |
||||||
|
ClientStats |
||||||
|
RoomStats |
||||||
|
UserStats |
||||||
|
connectionCount uint64 |
||||||
|
broadcastChatMessages uint64 |
||||||
|
unicastChatMessages uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewStatsManager(clientStats ClientStats, roomStats RoomStats, userStats UserStats) StatsManager { |
||||||
|
return &statsManager{clientStats, roomStats, userStats, 0, 0, 0} |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountConnection() uint64 { |
||||||
|
return atomic.AddUint64(&stats.connectionCount, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountBroadcastChat() { |
||||||
|
atomic.AddUint64(&stats.broadcastChatMessages, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountUnicastChat() { |
||||||
|
atomic.AddUint64(&stats.unicastChatMessages, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) Stat(details bool) *HubStat { |
||||||
|
roomCount, roomSessionInfo := stats.RoomInfo(details) |
||||||
|
clientCount, sessions, connections := stats.ClientInfo(details) |
||||||
|
userCount, users := stats.UserInfo(details) |
||||||
|
|
||||||
|
return &HubStat{ |
||||||
|
Rooms: roomCount, |
||||||
|
Connections: clientCount, |
||||||
|
Sessions: clientCount, |
||||||
|
Users: userCount, |
||||||
|
Count: atomic.LoadUint64(&stats.connectionCount), |
||||||
|
BroadcastChatMessages: atomic.LoadUint64(&stats.broadcastChatMessages), |
||||||
|
UnicastChatMessages: atomic.LoadUint64(&stats.unicastChatMessages), |
||||||
|
IdsInRoom: roomSessionInfo, |
||||||
|
SessionsById: sessions, |
||||||
|
UsersById: users, |
||||||
|
ConnectionsByIdx: connections, |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,130 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
"crypto/aes" |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" |
||||||
|
) |
||||||
|
|
||||||
|
type SessionValidator interface { |
||||||
|
Realm() string |
||||||
|
ValidateSession(string, string) bool |
||||||
|
} |
||||||
|
|
||||||
|
type SessionEncoder interface { |
||||||
|
EncodeSessionToken(*Session) (string, error) |
||||||
|
EncodeSessionUserID(*Session) string |
||||||
|
} |
||||||
|
|
||||||
|
type Tickets interface { |
||||||
|
SessionValidator |
||||||
|
SessionEncoder |
||||||
|
DecodeSessionToken(token string) (st *SessionToken) |
||||||
|
FakeSessionToken(userid string) *SessionToken |
||||||
|
} |
||||||
|
|
||||||
|
type tickets struct { |
||||||
|
*securecookie.SecureCookie |
||||||
|
realm string |
||||||
|
tokenName string |
||||||
|
encryptionSecret []byte |
||||||
|
} |
||||||
|
|
||||||
|
func NewTickets(sessionSecret, encryptionSecret []byte, realm string) Tickets { |
||||||
|
tickets := &tickets{ |
||||||
|
nil, |
||||||
|
realm, |
||||||
|
fmt.Sprintf("token@%s", realm), |
||||||
|
encryptionSecret, |
||||||
|
} |
||||||
|
tickets.SecureCookie = securecookie.New(sessionSecret, encryptionSecret) |
||||||
|
tickets.MaxAge(86400 * 30) // 30 days
|
||||||
|
tickets.HashFunc(sha256.New) |
||||||
|
tickets.BlockFunc(aes.NewCipher) |
||||||
|
|
||||||
|
return tickets |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) Realm() string { |
||||||
|
return tickets.realm |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) DecodeSessionToken(token string) (st *SessionToken) { |
||||||
|
var err error |
||||||
|
if token != "" { |
||||||
|
st = &SessionToken{} |
||||||
|
err = tickets.Decode(tickets.tokenName, token, st) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error while decoding session token", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if st == nil || err != nil { |
||||||
|
sid := NewRandomString(32) |
||||||
|
id, _ := tickets.Encode("id", sid) |
||||||
|
st = &SessionToken{Id: id, Sid: sid} |
||||||
|
log.Println("Created new session id", id) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) FakeSessionToken(userid string) *SessionToken { |
||||||
|
st := &SessionToken{} |
||||||
|
st.Sid = fmt.Sprintf("fake-%s", NewRandomString(27)) |
||||||
|
st.Id, _ = tickets.Encode("id", st.Sid) |
||||||
|
st.Userid = userid |
||||||
|
log.Println("Created new fake session id", st.Id) |
||||||
|
return st |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) ValidateSession(id, sid string) bool { |
||||||
|
var decoded string |
||||||
|
if err := tickets.Decode("id", id, &decoded); err != nil { |
||||||
|
log.Println("Session validation error", err, id, sid) |
||||||
|
return false |
||||||
|
} |
||||||
|
if decoded != sid { |
||||||
|
log.Println("Session validation failed", id, sid) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) EncodeSessionToken(session *Session) (string, error) { |
||||||
|
return tickets.Encode(tickets.tokenName, session.Token()) |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) EncodeSessionUserID(session *Session) (suserid string) { |
||||||
|
if userid := session.Userid(); userid != "" { |
||||||
|
m := hmac.New(sha256.New, tickets.encryptionSecret) |
||||||
|
m.Write([]byte(userid)) |
||||||
|
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil)) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([], function() { |
||||||
|
return [function() { |
||||||
|
var link = function($scope, $element, attrs) { |
||||||
|
var originalText = $element.text(); |
||||||
|
var updateTitle = function(roomName) { |
||||||
|
if (roomName) { |
||||||
|
$element.text(roomName+ " - " + originalText); |
||||||
|
} else { |
||||||
|
$element.text(originalText); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
$scope.$on("room.updated", function(ev, room) { |
||||||
|
updateTitle(room.Name); |
||||||
|
}); |
||||||
|
|
||||||
|
$scope.$on("room.left", function(ev) { |
||||||
|
updateTitle(); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
return { |
||||||
|
restrict: 'E', |
||||||
|
replace: false, |
||||||
|
link: link |
||||||
|
}; |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
'mediastream/api' |
||||||
|
], function(Api) { |
||||||
|
return ["globalContext", "connector", function(context, connector) { |
||||||
|
return new Api(context.Cfg.Version, connector); |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
'mediastream/connector' |
||||||
|
], function(Connector) { |
||||||
|
return [function() { |
||||||
|
return new Connector(); |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
], function() { |
||||||
|
|
||||||
|
return ["globalContext", "$window", function(context, $window) { |
||||||
|
return { |
||||||
|
room: function(id) { |
||||||
|
id = $window.encodeURIComponent(id); |
||||||
|
return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + id; |
||||||
|
}, |
||||||
|
buddy: function(id) { |
||||||
|
return $window.location.protocol + '//' + $window.location.host + context.Cfg.B + "static/img/buddy/s46/" + id; |
||||||
|
}, |
||||||
|
api: function(path) { |
||||||
|
return (context.Cfg.B || "/") + "api/v1/" + path; |
||||||
|
} |
||||||
|
}; |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
], function() { |
||||||
|
|
||||||
|
return ["$window", "$q", function($window, $q) { |
||||||
|
var pinCache = {}; |
||||||
|
var roompin = { |
||||||
|
get: function(roomName) { |
||||||
|
var cachedPIN = pinCache[roomName]; |
||||||
|
return cachedPIN ? cachedPIN : null; |
||||||
|
}, |
||||||
|
clear: function(roomName) { |
||||||
|
delete pinCache[roomName]; |
||||||
|
console.log("Cleared PIN for", roomName); |
||||||
|
}, |
||||||
|
update: function(roomName, pin) { |
||||||
|
if (pin) { |
||||||
|
pinCache[roomName] = pin; |
||||||
|
$window.alert("PIN for room " + roomName + " is now '" + pin + "'"); |
||||||
|
} else { |
||||||
|
roompin.clear(roomName); |
||||||
|
$window.alert("PIN lock has been removed from room " + roomName); |
||||||
|
} |
||||||
|
}, |
||||||
|
requestInteractively: function(roomName) { |
||||||
|
var deferred = $q.defer(); |
||||||
|
var pin = $window.prompt("Enter the PIN for " + roomName + " below"); |
||||||
|
if (pin) { |
||||||
|
pinCache[roomName] = pin; |
||||||
|
deferred.resolve(); |
||||||
|
} else { |
||||||
|
deferred.reject(); |
||||||
|
} |
||||||
|
return deferred.promise; |
||||||
|
} |
||||||
|
}; |
||||||
|
return roompin; |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,204 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
'angular', |
||||||
|
'jquery' |
||||||
|
], function(angular, $) { |
||||||
|
|
||||||
|
return ["$window", "$location", "$timeout", "$q", "$route", "$rootScope", "$http", "globalContext", "safeApply", "connector", "api", "restURL", "roompin", function($window, $location, $timeout, $q, $route, $rootScope, $http, globalContext, safeApply, connector, api, restURL, roompin) { |
||||||
|
var url = restURL.api("rooms"); |
||||||
|
var requestedRoomName = ""; |
||||||
|
var currentRoom = null; |
||||||
|
|
||||||
|
var joinFailed = function(error) { |
||||||
|
setCurrentRoom(null); |
||||||
|
|
||||||
|
switch(error.Code) { |
||||||
|
case "default_room_disabled": |
||||||
|
rooms.randomRoom(); |
||||||
|
break; |
||||||
|
case "invalid_credentials": |
||||||
|
roompin.clear(requestedRoomName); |
||||||
|
/* falls through */ |
||||||
|
case "authorization_required": |
||||||
|
roompin.requestInteractively(requestedRoomName).then(joinRequestedRoom, |
||||||
|
function() { |
||||||
|
console.log("Authentication cancelled, try a different room"); |
||||||
|
}); |
||||||
|
break; |
||||||
|
case "authorization_not_required": |
||||||
|
roompin.clear(requestedRoomName); |
||||||
|
joinRequestedRoom(); |
||||||
|
break; |
||||||
|
default: |
||||||
|
console.log("Unknown error", error, "while joining room ", requestedRoomName); |
||||||
|
break; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var joinRequestedRoom = function() { |
||||||
|
if ($rootScope.authorizing()) { |
||||||
|
// Do nothing while authorizing.
|
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!connector.connected || !currentRoom || requestedRoomName !== currentRoom.Name) { |
||||||
|
if (requestedRoomName !== "" || globalContext.Cfg.DefaultRoomEnabled) { |
||||||
|
console.log("Joining room", requestedRoomName); |
||||||
|
requestedRoomName = requestedRoomName ? requestedRoomName : ""; |
||||||
|
api.sendHello(requestedRoomName, roompin.get(requestedRoomName), setCurrentRoom, joinFailed); |
||||||
|
} else { |
||||||
|
console.log("Default room disabled, requesting a random room."); |
||||||
|
setCurrentRoom(null); |
||||||
|
rooms.randomRoom(); |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var setCurrentRoom = function(room) { |
||||||
|
if (room === currentRoom) { |
||||||
|
return; |
||||||
|
} |
||||||
|
var priorRoom = currentRoom; |
||||||
|
currentRoom = room; |
||||||
|
if (priorRoom) { |
||||||
|
console.log("Left room", priorRoom.Name); |
||||||
|
$rootScope.$broadcast("room.left", priorRoom.Name); |
||||||
|
} |
||||||
|
if (currentRoom) { |
||||||
|
console.log("Joined room", currentRoom.Name); |
||||||
|
$rootScope.$broadcast("room.joined", currentRoom.Name); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
var updateRoom = function(room) { |
||||||
|
var response = $q.defer(); |
||||||
|
api.requestRoomUpdate(room, response.resolve, response.reject); |
||||||
|
return response.promise.then(applyRoomUpdate); |
||||||
|
}; |
||||||
|
|
||||||
|
var applyRoomUpdate = function(room) { |
||||||
|
if (room.Credentials) { |
||||||
|
roompin.update(currentRoom.Name, room.Credentials.PIN); |
||||||
|
delete room.Credentials; |
||||||
|
} |
||||||
|
currentRoom = room; |
||||||
|
$rootScope.$broadcast("room.updated", currentRoom); |
||||||
|
return room; |
||||||
|
}; |
||||||
|
|
||||||
|
connector.e.on("close error", function() { |
||||||
|
setCurrentRoom(null); |
||||||
|
}); |
||||||
|
|
||||||
|
api.e.on("received.self", function(event, data) { |
||||||
|
joinRequestedRoom(); |
||||||
|
}); |
||||||
|
|
||||||
|
api.e.on("received.room", function(event, room) { |
||||||
|
applyRoomUpdate(room); |
||||||
|
}); |
||||||
|
|
||||||
|
$rootScope.$on("authorization.succeeded", function() { |
||||||
|
// NOTE(lcooper): This will have been skipped earlier, so try again.
|
||||||
|
joinRequestedRoom(); |
||||||
|
}); |
||||||
|
|
||||||
|
$rootScope.$on("$locationChangeSuccess", function(event) { |
||||||
|
var roomName; |
||||||
|
if ($route.current) { |
||||||
|
roomName = $route.current.params.room; |
||||||
|
roomName = $window.decodeURIComponent(roomName); |
||||||
|
} else { |
||||||
|
roomName = ""; |
||||||
|
} |
||||||
|
|
||||||
|
requestedRoomName = roomName; |
||||||
|
if (connector.connected) { |
||||||
|
joinRequestedRoom(); |
||||||
|
} else { |
||||||
|
$rootScope.$broadcast("rooms.ready"); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
var rooms = { |
||||||
|
inDefaultRoom: function() { |
||||||
|
return (currentRoom !== null ? currentRoom.Name : requestedRoomName) === ""; |
||||||
|
}, |
||||||
|
randomRoom: function() { |
||||||
|
$http({ |
||||||
|
method: "POST", |
||||||
|
url: url, |
||||||
|
data: $.param({}), |
||||||
|
headers: { |
||||||
|
'Content-Type': 'application/x-www-form-urlencoded' |
||||||
|
} |
||||||
|
}). |
||||||
|
success(function(data, status) { |
||||||
|
console.info("Retrieved random room data", data); |
||||||
|
if (!data.name) { |
||||||
|
data.name = ""; |
||||||
|
} |
||||||
|
$rootScope.$broadcast('room.random', {name: data.name}); |
||||||
|
}). |
||||||
|
error(function() { |
||||||
|
console.error("Failed to retrieve random room data."); |
||||||
|
$rootScope.$broadcast('room.random', {}); |
||||||
|
}); |
||||||
|
}, |
||||||
|
joinByName: function(name, replace) { |
||||||
|
name = $window.encodeURIComponent(name); |
||||||
|
name = name.replace(/^%40/, "@"); |
||||||
|
name = name.replace(/^%24/, "$"); |
||||||
|
name = name.replace(/^%2B/, "+"); |
||||||
|
|
||||||
|
safeApply($rootScope, function(scope) { |
||||||
|
$location.path("/" + name); |
||||||
|
if (replace) { |
||||||
|
$location.replace(); |
||||||
|
} |
||||||
|
}); |
||||||
|
return name; |
||||||
|
}, |
||||||
|
link: function(room) { |
||||||
|
var name = room ? room.Name : null; |
||||||
|
if (!name) { |
||||||
|
name = ""; |
||||||
|
} |
||||||
|
return restURL.room(name); |
||||||
|
}, |
||||||
|
setPIN: function(pin) { |
||||||
|
pin = "" + pin; |
||||||
|
var newRoom = angular.copy(currentRoom); |
||||||
|
newRoom.Credentials = {PIN: pin}; |
||||||
|
return updateRoom(newRoom).then(null, function(error) { |
||||||
|
console.log("Failed to set room PIN", error); |
||||||
|
return $q.reject(error); |
||||||
|
}); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// NOTE(lcooper): For debugging only, do not use this on production.
|
||||||
|
$window.setRoomPIN = rooms.setPIN; |
||||||
|
|
||||||
|
return rooms; |
||||||
|
}]; |
||||||
|
}); |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
/* |
||||||
|
* 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/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
define([ |
||||||
|
'mediastream/webrtc' |
||||||
|
], function(WebRTC) { |
||||||
|
return ["api", function(api) { |
||||||
|
return new WebRTC(api); |
||||||
|
}]; |
||||||
|
}); |
||||||
Loading…
Reference in new issue