Browse Source
* support-room-pin: Add basic support for pin locked rooms to the web application. Add support for PIN locking rooms to the server. Support handling room updates in web client. Support receiving and broadcasting room update events. Use room document from Welcome rather than synthesizing a response. Add Room document and send it back in response to room joins. Send Hello with Iid from Javascript client, and use the data from the Welcome. Add server support for responding with Welcome to a Hello. Major refactoring of server side code to allow isolated unit tests. Refactor web app room logic into separate service.pull/112/head
50 changed files with 2726 additions and 1423 deletions
@ -0,0 +1,286 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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