Browse Source

Merge pull request #130 from deathwish/support-room-pin

* 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
Simon Eisenmann 11 years ago
parent
commit
6aa48acb9a
  1. 117
      doc/CHANNELING-API.txt
  2. 18
      src/app/spreed-webrtc-server/buffercache.go
  3. 34
      src/app/spreed-webrtc-server/channeling.go
  4. 286
      src/app/spreed-webrtc-server/channelling_api.go
  5. 244
      src/app/spreed-webrtc-server/channelling_api_test.go
  6. 87
      src/app/spreed-webrtc-server/client.go
  7. 43
      src/app/spreed-webrtc-server/common_test.go
  8. 164
      src/app/spreed-webrtc-server/connection.go
  9. 566
      src/app/spreed-webrtc-server/hub.go
  10. 69
      src/app/spreed-webrtc-server/incoming_codec.go
  11. 29
      src/app/spreed-webrtc-server/main.go
  12. 192
      src/app/spreed-webrtc-server/room_manager.go
  13. 57
      src/app/spreed-webrtc-server/room_manager_test.go
  14. 189
      src/app/spreed-webrtc-server/roomworker.go
  15. 124
      src/app/spreed-webrtc-server/roomworker_test.go
  16. 280
      src/app/spreed-webrtc-server/server.go
  17. 28
      src/app/spreed-webrtc-server/session.go
  18. 173
      src/app/spreed-webrtc-server/session_manager.go
  19. 13
      src/app/spreed-webrtc-server/sessions.go
  20. 8
      src/app/spreed-webrtc-server/stats.go
  21. 104
      src/app/spreed-webrtc-server/stats_manager.go
  22. 130
      src/app/spreed-webrtc-server/tickets.go
  23. 32
      src/app/spreed-webrtc-server/users.go
  24. 35
      src/app/spreed-webrtc-server/ws.go
  25. 3
      static/js/app.js
  26. 62
      static/js/controllers/mediastreamcontroller.js
  27. 69
      static/js/controllers/roomchangecontroller.js
  28. 63
      static/js/directives/buddylist.js
  29. 36
      static/js/directives/chat.js
  30. 8
      static/js/directives/directives.js
  31. 54
      static/js/directives/page.js
  32. 26
      static/js/directives/roombar.js
  33. 10
      static/js/directives/socialshare.js
  34. 48
      static/js/directives/title.js
  35. 40
      static/js/directives/usability.js
  36. 68
      static/js/mediastream/api.js
  37. 59
      static/js/mediastream/connector.js
  38. 27
      static/js/services/api.js
  39. 6
      static/js/services/buddypicture.js
  40. 27
      static/js/services/connector.js
  41. 143
      static/js/services/mediastream.js
  42. 38
      static/js/services/resturl.js
  43. 58
      static/js/services/roompin.js
  44. 204
      static/js/services/rooms.js
  45. 26
      static/js/services/services.js
  46. 27
      static/js/services/webrtc.js
  47. 12
      static/partials/chat.html
  48. 8
      static/partials/page/welcome.html
  49. 4
      static/partials/roombar.html
  50. 1
      static/partials/usability.html

117
doc/CHANNELING-API.txt

@ -87,6 +87,16 @@ Sending vs receiving document data encapsulation @@ -87,6 +87,16 @@ Sending vs receiving document data encapsulation
A : Session attestation token. Only available for incoming data
created by other sessions (optional).
Error returns
Calls providing an Iid which fail will receive an Error document with the
following format:
{
"Type": "Error",
"Code": "value_identifying_error",
"Message": "A description of the error condition"
}
Special purpose documents for channling
@ -147,18 +157,113 @@ Special purpose documents for channling @@ -147,18 +157,113 @@ Special purpose documents for channling
Hello: {
Version: "1.0.0",
Ua: "Test client 1.0",
Id: ""
Id: "",
"Credentials": {...}
}
}
Hello document is to be send by the client after connection was
established.
Hello document is to be send by the client after connection was established.
If an Iid is provided, a Welcome document will be returned if joining the
room with the given Id succeeds. Otherwise an Error document with one of the
error codes listed below will be returned. Note that any previous room will
have been left regardless of whether the response is successful.
Keys under Hello:
Version : Channel protocol version (string).
Ua : User agent description (string).
Id : Room id. The default Room has the empty string Id ("") (string).
Version : Channel protocol version (string).
Ua : User agent description (string).
Id : Room id. The default Room has the empty string Id ("") (string).
Credentials : An optional RoomCredentials document containing room
authentication information. See the Room document for
information on how such credentials should be handled after
a Welcome is received for the requested room. Note that
providing credentials while joining an existing room which
does not require them is an error, such requests should be
retried without credentials. In contrast, joining a
non-existent room with credentials will create the room
using the given credentials. Note that an error with a code
of authorization_not_required or invalid_credentials shall
cause the client to discard any cached room credentials.
Error codes:
default_room_disabled : Joining the room "" is not allowed by this
server.
authorization_required : Joining the given room requires credentials.
authorization_not_required : No credentials should be provided for this
room.
invalid_credentials : The provided credentials are incorrect.
Welcome
{
"Type": "Welcome",
"Welcome": {
"Room": {...},
"Users": []
}
}
Welcome is sent in reply to a successful Hello, and contains all data
needed to set up the initial room connection.
Keys under Welcome:
Room: Contains the current state of the room, see the description of
the Room document for more details.
Users: Contains the user list for the room, see the description of
the Users document for more details.
RoomCredentials
{
"PIN": "my-super-sekrit-code"
}
RoomCredentials contains room authentication information, and is used as a
child document when joining or updating a room.
Keys under RoomCredentials:
PIN : A password string which may be used by clients to authenticate
themselves. Note that acceptable characters for this field may be
constrained by the server based upon its configuration.
Room
{
"Type": "Room",
"Name": "room-name-here"
"Credentials": {...}
}
Clients may send a Room document in order to update all room properties
to the values given in the document. The room name must be given and match
the currently joined room. Successful updates will receive an updated Room
document as a reply, or an Error document if the update fails.
Addtionally, the Room document is included in responses to initial joins
and broadcast when room properties are updated.
Keys under Room:
Name : The human readable ID of the room, currently must be globally
unique.
Credentials : Optional authentication information for the room, see the
documentation of the RoomCredentials document for more
details. This field shall only be present when sending or
receiving an update which alters room authentication data.
It should only be inferred that authentication is not
required if joining a room succeeds without credentials and
no updates containing credentials has been received.
Authentication may be disabled by sending a Room document
containing a RoomCredentials document with only empty
fields. Clients shall discard any cached authentication
information upon receiving such an update.
Error codes:
not_in_room : Clients may only update rooms which they have joined.
Peer connection documents

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

@ -160,3 +160,21 @@ func (cache *bufferCache) New() Buffer { @@ -160,3 +160,21 @@ func (cache *bufferCache) New() Buffer {
func (cache *bufferCache) Wrap(data []byte) Buffer {
return &directBuffer{refcnt: 1, cache: cache, buf: bytes.NewBuffer(data)}
}
func readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
}
}()
_, err = dest.ReadFrom(r)
return err
}

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

@ -21,10 +21,37 @@ @@ -21,10 +21,37 @@
package main
type DataError struct {
Type string
Code string
Message string
}
func (err *DataError) Error() string {
return err.Message
}
type DataRoomCredentials struct {
PIN string
}
type DataHello struct {
Version string
Ua string
Id string
Version string
Ua string
Id string
Credentials *DataRoomCredentials
}
type DataWelcome struct {
Type string
Room *DataRoom
Users []*DataSession
}
type DataRoom struct {
Type string
Name string
Credentials *DataRoomCredentials
}
type DataOffer struct {
@ -159,6 +186,7 @@ type DataIncoming struct { @@ -159,6 +186,7 @@ type DataIncoming struct {
Alive *DataAlive
Authentication *DataAuthentication
Sessions *DataSessions
Room *DataRoom
Iid string `json:",omitempty"`
}

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

@ -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())
}
}

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

@ -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")
}

87
src/app/spreed-webrtc-server/client.go

@ -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
}

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

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"testing"
)
func assertDataError(t *testing.T, err error, code string) {
if err == nil {
t.Error("Expected an error, but none was returned")
return
}
dataError, ok := err.(*DataError)
if !ok {
t.Errorf("Expected error %#v to be a *DataError", err)
return
}
if code != dataError.Code {
t.Errorf("Expected error code to be %v, but was %v", code, dataError.Code)
}
}

164
src/app/spreed-webrtc-server/connection.go

@ -22,14 +22,13 @@ @@ -22,14 +22,13 @@
package main
import (
"bytes"
"container/list"
"github.com/gorilla/websocket"
"io"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
)
const (
@ -54,110 +53,77 @@ const ( @@ -54,110 +53,77 @@ const (
maxRatePerSecond = 20
)
type Connection struct {
type Connection interface {
Index() uint64
Send(Buffer)
Close(runCallbacks bool)
readPump()
writePump()
}
type ConnectionHandler interface {
NewBuffer() Buffer
OnConnect(Connection)
OnText(Buffer)
OnDisconnect()
}
type connection struct {
// References.
h *Hub
ws *websocket.Conn
request *http.Request
handler ConnectionHandler
// Data handling.
condition *sync.Cond
queue list.List
mutex sync.Mutex
isClosed bool
isClosing bool
// Metadata.
Id string
Roomid string // Keep Roomid here for quick acess without locking c.Session.
Idx uint64
Session *Session
IsRegistered bool
Hello bool
Version string
}
func NewConnection(h *Hub, ws *websocket.Conn, request *http.Request) *Connection {
// Debugging
Idx uint64
}
c := &Connection{
h: h,
func NewConnection(index uint64, ws *websocket.Conn, handler ConnectionHandler) Connection {
c := &connection{
ws: ws,
request: request,
handler: handler,
Idx: index,
}
c.condition = sync.NewCond(&c.mutex)
return c
}
func (c *Connection) close() {
func (c *connection) Index() uint64 {
return c.Idx
}
if !c.isClosed {
c.ws.Close()
c.Session.Close()
c.mutex.Lock()
c.Session = nil
c.isClosed = true
for {
head := c.queue.Front()
if head == nil {
break
}
c.queue.Remove(head)
message := head.Value.(Buffer)
message.Decref()
}
c.condition.Signal()
func (c *connection) Close(runCallbacks bool) {
c.mutex.Lock()
if c.isClosed {
c.mutex.Unlock()
return
}
}
func (c *Connection) register() error {
s := c.h.CreateSession(c.request, nil)
c.h.registerHandler(c, s)
return nil
}
func (c *Connection) reregister(token string) error {
if st, err := c.h.DecodeSessionToken(token); err == nil {
s := c.h.CreateSession(c.request, st)
c.h.registerHandler(c, s)
} else {
log.Println("Error while decoding session token", err)
c.register()
if runCallbacks {
c.handler.OnDisconnect()
}
return nil
}
func (c *Connection) unregister() {
c.isClosing = true
c.h.unregisterHandler(c)
}
func (c *Connection) readAll(dest Buffer, r io.Reader) error {
var err error
defer func() {
e := recover()
if e == nil {
return
}
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
err = panicErr
} else {
panic(e)
c.ws.Close()
c.isClosed = true
for {
head := c.queue.Front()
if head == nil {
break
}
}()
_, err = dest.ReadFrom(r)
return err
c.queue.Remove(head)
message := head.Value.(Buffer)
message.Decref()
}
c.condition.Signal()
c.mutex.Unlock()
}
// readPump pumps messages from the websocket connection to the hub.
func (c *Connection) readPump() {
func (c *connection) readPump() {
c.ws.SetReadLimit(maxMessageSize)
c.ws.SetReadDeadline(time.Now().Add(pongWait))
c.ws.SetPongHandler(func(string) error {
@ -165,6 +131,10 @@ func (c *Connection) readPump() { @@ -165,6 +131,10 @@ func (c *Connection) readPump() {
return nil
})
times := list.New()
// NOTE(lcooper): This more or less assumes that the write pump is started.
c.handler.OnConnect(c)
for {
//fmt.Println("readPump wait nextReader", c.Idx)
op, r, err := c.ws.NextReader()
@ -177,12 +147,6 @@ func (c *Connection) readPump() { @@ -177,12 +147,6 @@ func (c *Connection) readPump() {
}
switch op {
case websocket.TextMessage:
message := c.h.buffers.New()
err = c.readAll(message, r)
if err != nil {
message.Decref()
break
}
now := time.Now()
if times.Len() == maxRatePerSecond {
front := times.Front()
@ -194,18 +158,23 @@ func (c *Connection) readPump() { @@ -194,18 +158,23 @@ func (c *Connection) readPump() {
}
}
times.PushBack(now)
c.h.server.OnText(c, message)
message := c.handler.NewBuffer()
err = readAll(message, r)
if err != nil {
message.Decref()
break
}
c.handler.OnText(message)
message.Decref()
}
}
c.unregister()
c.ws.Close()
c.Close(true)
}
// Write message to outbound queue.
func (c *Connection) send(message Buffer) {
func (c *connection) Send(message Buffer) {
c.mutex.Lock()
defer c.mutex.Unlock()
if c.isClosed {
@ -223,8 +192,7 @@ func (c *Connection) send(message Buffer) { @@ -223,8 +192,7 @@ func (c *Connection) send(message Buffer) {
}
// writePump pumps messages from the queue to the websocket connection.
func (c *Connection) writePump() {
func (c *connection) writePump() {
var timer *time.Timer
ping := false
@ -301,16 +269,16 @@ func (c *Connection) writePump() { @@ -301,16 +269,16 @@ func (c *Connection) writePump() {
cleanup:
//fmt.Println("writePump done")
timer.Stop()
c.ws.Close()
c.Close(true)
}
// Write ping message.
func (c *Connection) ping() error {
func (c *connection) ping() error {
return c.write(websocket.PingMessage, []byte{})
}
// Write writes a message with the given opCode and payload.
func (c *Connection) write(opCode int, payload []byte) error {
func (c *connection) write(opCode int, payload []byte) error {
c.ws.SetWriteDeadline(time.Now().Add(writeWait))
return c.ws.WriteMessage(opCode, payload)
}

566
src/app/spreed-webrtc-server/hub.go

@ -22,21 +22,16 @@ @@ -22,21 +22,16 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"github.com/gorilla/securecookie"
"log"
"net/http"
"strings"
"sync"
"sync/atomic"
"time"
)
@ -46,85 +41,56 @@ const ( @@ -46,85 +41,56 @@ const (
maxUsersLength = 5000
)
// TODO(longsleep): Get rid of MessageRequest type.
type MessageRequest struct {
From string
To string
Message Buffer
Id string
type SessionStore interface {
GetSession(id string) (session *Session, ok bool)
}
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 Unicaster interface {
SessionStore
OnConnect(Client, *Session)
Unicast(session *Session, to string, m interface{})
OnDisconnect(*Session)
}
type Hub struct {
server *Server
connectionTable map[string]*Connection
sessionTable map[string]*Session
roomTable map[string]*RoomWorker
userTable map[string]*User
fakesessionTable map[string]*Session
version string
config *Config
sessionSecret []byte
encryptionSecret []byte
turnSecret []byte
tickets *securecookie.SecureCookie
attestations *securecookie.SecureCookie
count uint64
mutex sync.RWMutex
buffers BufferCache
broadcastChatMessages uint64
unicastChatMessages uint64
buddyImages ImageCache
realm string
tokenName string
useridRetriever func(*http.Request) (string, error)
contacts *securecookie.SecureCookie
type ContactManager interface {
contactrequestHandler(*Session, string, *DataContactRequest) error
getContactID(*Session, string) (string, error)
}
func NewHub(version string, config *Config, sessionSecret, encryptionSecret, turnSecret []byte, realm string) *Hub {
h := &Hub{
connectionTable: make(map[string]*Connection),
sessionTable: make(map[string]*Session),
roomTable: make(map[string]*RoomWorker),
userTable: make(map[string]*User),
fakesessionTable: make(map[string]*Session),
version: version,
config: config,
sessionSecret: sessionSecret,
encryptionSecret: encryptionSecret,
turnSecret: turnSecret,
realm: realm,
}
type TurnDataCreator interface {
CreateTurnData(*Session) *DataTurn
}
type ClientStats interface {
ClientInfo(details bool) (int, map[string]*DataSession, map[string]string)
}
type Hub interface {
ClientStats
Unicaster
TurnDataCreator
ContactManager
}
if len(h.sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(h.sessionSecret))
type hub struct {
OutgoingEncoder
clients map[string]Client
config *Config
turnSecret []byte
mutex sync.RWMutex
contacts *securecookie.SecureCookie
}
func NewHub(config *Config, sessionSecret, encryptionSecret, turnSecret []byte, encoder OutgoingEncoder) Hub {
h := &hub{
OutgoingEncoder: encoder,
clients: make(map[string]Client),
config: config,
turnSecret: turnSecret,
}
h.tickets = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.tickets.MaxAge(86400 * 30) // 30 days
h.tickets.HashFunc(sha256.New)
h.tickets.BlockFunc(aes.NewCipher)
h.attestations = securecookie.New(h.sessionSecret, nil)
h.attestations.MaxAge(300) // 5 minutes
h.tickets.HashFunc(sha256.New)
h.buffers = NewBufferCache(1024, bytes.MinRead)
h.buddyImages = NewImageCache()
h.tokenName = fmt.Sprintf("token@%s", h.realm)
h.contacts = securecookie.New(h.sessionSecret, h.encryptionSecret)
h.contacts = securecookie.New(sessionSecret, encryptionSecret)
h.contacts.MaxAge(0) // Forever
h.contacts.HashFunc(sha256.New)
h.contacts.BlockFunc(aes.NewCipher)
@ -132,48 +98,27 @@ func NewHub(version string, config *Config, sessionSecret, encryptionSecret, tur @@ -132,48 +98,27 @@ func NewHub(version string, config *Config, sessionSecret, encryptionSecret, tur
}
func (h *Hub) Stat(details bool) *HubStat {
func (h *hub) ClientInfo(details bool) (clientCount int, sessions map[string]*DataSession, connections map[string]string) {
h.mutex.RLock()
defer h.mutex.RUnlock()
stat := &HubStat{
Rooms: len(h.roomTable),
Connections: len(h.connectionTable),
Sessions: len(h.sessionTable),
Users: len(h.userTable),
Count: h.count,
BroadcastChatMessages: atomic.LoadUint64(&h.broadcastChatMessages),
UnicastChatMessages: atomic.LoadUint64(&h.unicastChatMessages),
}
clientCount = len(h.clients)
if details {
rooms := make(map[string][]string)
for roomid, room := range h.roomTable {
sessions := make([]string, 0, len(room.connections))
for id := range room.connections {
sessions = append(sessions, id)
}
rooms[roomid] = sessions
}
stat.IdsInRoom = rooms
sessions := make(map[string]*DataSession)
for sessionid, session := range h.sessionTable {
sessions[sessionid] = session.Data()
}
stat.SessionsById = sessions
users := make(map[string]*DataUser)
for userid, user := range h.userTable {
users[userid] = user.Data()
sessions = make(map[string]*DataSession)
for id, client := range h.clients {
sessions[id] = client.Session().Data()
}
stat.UsersById = users
connections := make(map[string]string)
for id, connection := range h.connectionTable {
connections[fmt.Sprintf("%d", connection.Idx)] = id
connections = make(map[string]string)
for id, client := range h.clients {
connections[fmt.Sprintf("%d", client.Index())] = id
}
stat.ConnectionsByIdx = connections
}
return stat
return
}
func (h *Hub) CreateTurnData(id string) *DataTurn {
func (h *hub) CreateTurnData(session *Session) *DataTurn {
// Create turn data credentials for shared secret auth with TURN
// server. See http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
@ -182,6 +127,7 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { @@ -182,6 +127,7 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
if len(h.turnSecret) == 0 {
return &DataTurn{}
}
id := session.Id
bar := sha256.New()
bar.Write([]byte(id))
id = base64.StdEncoding.EncodeToString(bar.Sum(nil))
@ -194,389 +140,85 @@ func (h *Hub) CreateTurnData(id string) *DataTurn { @@ -194,389 +140,85 @@ func (h *Hub) CreateTurnData(id string) *DataTurn {
}
func (h *Hub) CreateSuserid(session *Session) (suserid string) {
userid := session.Userid()
if userid != "" {
m := hmac.New(sha256.New, h.encryptionSecret)
m.Write([]byte(userid))
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil))
func (h *hub) GetSession(id string) (session *Session, ok bool) {
var client Client
client, ok = h.GetClient(id)
if ok {
session = client.Session()
}
return
}
func (h *Hub) CreateSession(request *http.Request, st *SessionToken) *Session {
var session *Session
var userid string
usersEnabled := h.config.UsersEnabled
if usersEnabled && h.useridRetriever != nil {
userid, _ = h.useridRetriever(request)
}
if st == nil {
sid := NewRandomString(32)
id, _ := h.tickets.Encode("id", sid)
session = NewSession(h, id, sid)
log.Println("Created new session id", id)
} else {
if userid == "" {
userid = st.Userid
}
if !usersEnabled {
userid = ""
}
session = NewSession(h, st.Id, st.Sid)
}
if userid != "" {
h.authenticateHandler(session, st, userid)
}
return session
}
func (h *Hub) CreateFakeSession(userid string) *Session {
h.mutex.Lock()
session, ok := h.fakesessionTable[userid]
if !ok {
sid := fmt.Sprintf("fake-%s", NewRandomString(27))
id, _ := h.tickets.Encode("id", sid)
log.Println("Created new fake session id", id)
session = NewSession(h, id, sid)
session.SetUseridFake(userid)
h.fakesessionTable[userid] = session
}
h.mutex.Unlock()
return session
}
func (h *Hub) ValidateSession(id, sid string) bool {
var decoded string
err := h.tickets.Decode("id", id, &decoded)
if 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 (h *Hub) EncodeSessionToken(st *SessionToken) (string, error) {
return h.tickets.Encode(h.tokenName, st)
}
func (h *Hub) DecodeSessionToken(token string) (*SessionToken, error) {
st := &SessionToken{}
err := h.tickets.Decode(h.tokenName, token, st)
return st, err
}
func (h *Hub) GetRoom(id string) *RoomWorker {
h.mutex.RLock()
room, ok := h.roomTable[id]
if !ok {
h.mutex.RUnlock()
h.mutex.Lock()
// Need to re-check, another thread might have created the room
// while we waited for the lock.
room, ok = h.roomTable[id]
if !ok {
room = NewRoomWorker(h, id)
h.roomTable[id] = room
h.mutex.Unlock()
go func() {
// Start room, this blocks until room expired.
room.Start()
// Cleanup room when we are done.
h.mutex.Lock()
defer h.mutex.Unlock()
delete(h.roomTable, id)
log.Printf("Cleaned up room '%s'\n", id)
}()
} else {
h.mutex.Unlock()
}
} else {
h.mutex.RUnlock()
}
return room
}
func (h *Hub) GetGlobalConnections() []*Connection {
if h.config.globalRoomid == "" {
return make([]*Connection, 0)
}
h.mutex.RLock()
if room, ok := h.roomTable[h.config.globalRoomid]; ok {
h.mutex.RUnlock()
return room.GetConnections()
}
h.mutex.RUnlock()
return make([]*Connection, 0)
}
func (h *Hub) RunForAllRooms(f func(room *RoomWorker)) {
h.mutex.RLock()
for _, room := range h.roomTable {
f(room)
}
h.mutex.RUnlock()
}
func (h *Hub) isGlobalRoomid(id string) bool {
return id != "" && (id == h.config.globalRoomid)
}
func (h *Hub) isDefaultRoomid(id string) bool {
return id == ""
}
func (h *Hub) registerHandler(c *Connection, s *Session) {
// Apply session to connection.
c.Id = s.Id
c.Session = s
func (h *hub) OnConnect(client Client, session *Session) {
// Set flags.
h.mutex.Lock()
// Set flags.
h.count++
c.Idx = h.count
c.IsRegistered = true
log.Printf("Created client with id %s", session.Id)
// Register connection or replace existing one.
if ec, ok := h.connectionTable[c.Id]; ok {
ec.IsRegistered = false
ec.close()
if ec, ok := h.clients[session.Id]; ok {
ec.Close(false)
//log.Printf("Register (%d) from %s: %s (existing)\n", c.Idx, c.Id)
}
h.connectionTable[c.Id] = c
h.sessionTable[c.Id] = s
h.clients[session.Id] = client
//fmt.Println("registered", c.Id)
h.mutex.Unlock()
//log.Printf("Register (%d) from %s: %s\n", c.Idx, c.Id)
h.server.OnRegister(c)
}
func (h *Hub) unregisterHandler(c *Connection) {
func (h *hub) OnDisconnect(session *Session) {
h.mutex.Lock()
if !c.IsRegistered {
h.mutex.Unlock()
return
}
suserid := c.Session.Userid()
delete(h.connectionTable, c.Id)
delete(h.sessionTable, c.Id)
if suserid != "" {
user, ok := h.userTable[suserid]
if ok {
empty := user.RemoveSession(c.Session)
if empty {
delete(h.userTable, suserid)
}
}
}
delete(h.clients, session.Id)
h.mutex.Unlock()
h.buddyImages.Delete(c.Id)
//log.Printf("Unregister (%d) from %s: %s\n", c.Idx, c.RemoteAddr, c.Id)
h.server.OnUnregister(c)
c.close()
}
func (h *Hub) unicastHandler(m *MessageRequest) {
func (h *hub) GetClient(id string) (client Client, ok bool) {
h.mutex.RLock()
out, ok := h.connectionTable[m.To]
client, ok = h.clients[id]
h.mutex.RUnlock()
if !ok {
log.Println("Unicast To not found", m.To)
return
}
out.send(m.Message)
return
}
func (h *Hub) aliveHandler(c *Connection, alive *DataAlive, iid string) {
aliveJson := h.buffers.New()
encoder := json.NewEncoder(aliveJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: alive, Iid: iid})
if err != nil {
log.Println("Alive error while encoding JSON", err)
aliveJson.Decref()
return
func (h *hub) Unicast(session *Session, to string, m interface{}) {
outgoing := &DataOutgoing{
From: session.Id,
To: to,
A: session.Attestation(),
Data: m,
}
c.send(aliveJson)
aliveJson.Decref()
}
func (h *Hub) sessionsHandler(c *Connection, srq *DataSessionsRequest, iid string) {
var users []*DataSession
switch srq.Type {
case "contact":
contact := &Contact{}
err := h.contacts.Decode("contact", srq.Token, contact)
if err != nil {
log.Println("Failed to decode incoming contact token", err, srq.Token)
return
}
// Use the userid which is not ours from the contact data.
var userid string
suserid := c.Session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
if userid == "" {
log.Println("Ignoring foreign contact token", contact.A, contact.B)
return
}
// Find foreign user.
h.mutex.RLock()
user, ok := h.userTable[userid]
h.mutex.RUnlock()
if message, err := h.EncodeOutgoing(outgoing); err == nil {
client, ok := h.GetClient(to)
if !ok {
// No user. Create fake session.
users = make([]*DataSession, 1, 1)
users[0] = h.CreateFakeSession(userid).Data()
} else {
// Add sessions for forein user.
users = user.SubscribeSessions(c.Session)
}
case "session":
id, err := c.Session.attestation.Decode(srq.Token)
if err != nil {
log.Println("Failed to decode incoming attestation", err, srq.Token)
log.Println("Unicast To not found", to)
return
}
h.mutex.RLock()
session, ok := h.sessionTable[id]
h.mutex.RUnlock()
if !ok {
return
}
users = make([]*DataSession, 1, 1)
users[0] = session.Data()
default:
log.Println("Unkown incoming sessions request type", srq.Type)
client.Send(message)
message.Decref()
}
if users != nil {
sessions := &DataSessions{Type: "Sessions", Users: users, Sessions: srq}
sessionsJson := h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions, Iid: iid})
if err != nil {
log.Println("Sessions error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
}
}
func (h *Hub) sessionupdateHandler(s *SessionUpdate) uint64 {
//fmt.Println("Userupdate", u)
h.mutex.RLock()
session, ok := h.sessionTable[s.Id]
h.mutex.RUnlock()
var rev uint64
if ok {
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 := h.buddyImages.Update(s.Id, pic[5:])
if imageId != "" {
status["buddyPicture"] = "img:" + imageId
}
}
}
}
rev = session.Update(s)
} else {
log.Printf("Update data for unknown user %s\n", s.Id)
}
return rev
}
func (h *Hub) sessiontokenHandler(st *SessionToken) (string, error) {
h.mutex.RLock()
c, ok := h.connectionTable[st.Id]
h.mutex.RUnlock()
if !ok {
return "", errors.New("no such connection")
}
nonce, err := c.Session.Authorize(h.realm, st)
func (h *hub) getContactID(session *Session, token string) (userid string, err error) {
contact := &Contact{}
err = h.contacts.Decode("contact", token, contact)
if err != nil {
return "", err
err = fmt.Errorf("Failed to decode incoming contact token", err, token)
return
}
return nonce, nil
}
func (h *Hub) authenticateHandler(session *Session, st *SessionToken, userid string) error {
err := session.Authenticate(h.realm, st, userid)
if err == nil {
// Authentication success.
suserid := session.Userid()
h.mutex.Lock()
user, ok := h.userTable[suserid]
if !ok {
user = NewUser(suserid)
h.userTable[suserid] = user
}
h.mutex.Unlock()
user.AddSession(session)
// Use the userid which is not ours from the contact data.
suserid := session.Userid()
if contact.A == suserid {
userid = contact.B
} else if contact.B == suserid {
userid = contact.A
}
return err
if userid == "" {
err = fmt.Errorf("Ignoring foreign contact token", contact.A, contact.B)
}
return
}
func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactRequest) error {
func (h *hub) contactrequestHandler(session *Session, to string, cr *DataContactRequest) error {
var err error
@ -588,13 +230,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq @@ -588,13 +230,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
if err != nil {
return err
}
suserid := c.Session.Userid()
suserid := session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
session, ok := h.GetSession(to)
if !ok {
return errors.New("unknown to session for confirm")
}
@ -616,13 +256,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq @@ -616,13 +256,11 @@ func (h *Hub) contactrequestHandler(c *Connection, to string, cr *DataContactReq
} else {
// New request.
// Create Token with flag and c.Session.Userid and the to Session.Userid.
suserid := c.Session.Userid()
suserid := session.Userid()
if suserid == "" {
return errors.New("no userid")
}
h.mutex.RLock()
session, ok := h.sessionTable[to]
h.mutex.RUnlock()
session, ok := h.GetSession(to)
if !ok {
return errors.New("unknown to session")
}

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

@ -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
}

29
src/app/spreed-webrtc-server/main.go

@ -91,12 +91,12 @@ func roomHandler(w http.ResponseWriter, r *http.Request) { @@ -91,12 +91,12 @@ func roomHandler(w http.ResponseWriter, r *http.Request) {
}
func makeImageHandler(hub *Hub, expires time.Duration) http.HandlerFunc {
func makeImageHandler(buddyImages ImageCache, expires time.Duration) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
image := hub.buddyImages.Get(vars["imageid"])
image := buddyImages.Get(vars["imageid"])
if image == nil {
http.Error(w, "Unknown image", http.StatusNotFound)
return
@ -223,6 +223,10 @@ func runner(runtime phoenix.Runtime) error { @@ -223,6 +223,10 @@ func runner(runtime phoenix.Runtime) error {
}
}
if len(sessionSecret) < 32 {
log.Printf("Weak sessionSecret (only %d bytes). It is recommended to use a key with 32 or 64 bytes.\n", len(sessionSecret))
}
var encryptionSecret []byte
encryptionSecretString, err := runtime.GetString("app", "encryptionSecret")
if err != nil {
@ -371,9 +375,6 @@ func runner(runtime phoenix.Runtime) error { @@ -371,9 +375,6 @@ func runner(runtime phoenix.Runtime) error {
// Create realm string from config.
computedRealm := fmt.Sprintf("%s.%s", serverRealm, serverToken)
// Create our hub instance.
hub := NewHub(runtimeVersion, config, sessionSecret, encryptionSecret, turnSecret, computedRealm)
// Set number of go routines if it is 1
if goruntime.GOMAXPROCS(0) == 1 {
nCPU := goruntime.NumCPU()
@ -426,12 +427,20 @@ func runner(runtime phoenix.Runtime) error { @@ -426,12 +427,20 @@ func runner(runtime phoenix.Runtime) error {
}
// Add handlers.
buddyImages := NewImageCache()
codec := NewCodec()
roomManager := NewRoomManager(config, codec)
hub := NewHub(config, sessionSecret, encryptionSecret, turnSecret, codec)
tickets := NewTickets(sessionSecret, encryptionSecret, computedRealm)
sessionManager := NewSessionManager(config, tickets, sessionSecret)
statsManager := NewStatsManager(hub, roomManager, sessionManager)
channellingAPI := NewChannellingAPI(runtimeVersion, config, roomManager, tickets, sessionManager, statsManager, hub, hub, hub, roomManager, buddyImages)
r.HandleFunc("/", httputils.MakeGzipHandler(mainHandler))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(hub, time.Duration(24)*time.Hour)))
r.Handle("/static/img/buddy/{flags}/{imageid}/{idx:.*}", http.StripPrefix(basePath, makeImageHandler(buddyImages, time.Duration(24)*time.Hour)))
r.Handle("/static/{path:.*}", http.StripPrefix(basePath, httputils.FileStaticServer(http.Dir(rootFolder))))
r.Handle("/robots.txt", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static")))))
r.Handle("/favicon.ico", http.StripPrefix(basePath, http.FileServer(http.Dir(path.Join(rootFolder, "static", "img")))))
r.Handle("/ws", makeWsHubHandler(hub))
r.Handle("/ws", makeWSHandler(statsManager, sessionManager, codec, channellingAPI))
r.HandleFunc("/{room}", httputils.MakeGzipHandler(roomHandler))
// Add API end points.
@ -442,14 +451,14 @@ func runner(runtime phoenix.Runtime) error { @@ -442,14 +451,14 @@ func runner(runtime phoenix.Runtime) error {
api.AddResourceWithWrapper(&Tokens{tokenProvider}, httputils.MakeGzipHandler, "/tokens")
if usersEnabled {
// Create Users handler.
users := NewUsers(hub, usersMode, serverRealm, runtime)
api.AddResource(&Sessions{hub: hub, users: users}, "/sessions/{id}/")
users := NewUsers(hub, tickets, sessionManager, usersMode, serverRealm, runtime)
api.AddResource(&Sessions{tickets, hub, users}, "/sessions/{id}/")
if usersAllowRegistration {
api.AddResource(users, "/users")
}
}
if statsEnabled {
api.AddResourceWithWrapper(&Stats{hub: hub}, httputils.MakeGzipHandler, "/stats")
api.AddResourceWithWrapper(&Stats{statsManager}, httputils.MakeGzipHandler, "/stats")
log.Println("Stats are enabled!")
}

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

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

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

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

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

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
package main
import (
"encoding/json"
"crypto/subtle"
"log"
"sync"
"time"
@ -33,39 +33,53 @@ const ( @@ -33,39 +33,53 @@ const (
roomExpiryDuration = 60 * time.Second
)
type RoomConnectionUpdate struct {
Id string
Sessionid string
Status bool
Connection *Connection
type RoomWorker interface {
Start()
SessionIDs() []string
Users() []*roomUser
Update(*DataRoom) error
GetUsers() []*DataSession
Broadcast(*Session, Buffer)
Join(*DataRoomCredentials, *Session, Sender) (*DataRoom, error)
Leave(*Session)
}
type RoomWorker struct {
type roomWorker struct {
// References.
h *Hub
manager *roomManager
// Data handling.
workers chan (func())
expired chan (bool)
connections map[string]*Connection
timer *time.Timer
mutex sync.RWMutex
workers chan (func())
expired chan (bool)
users map[string]*roomUser
timer *time.Timer
mutex sync.RWMutex
// Metadata.
Id string
Id string
credentials *DataRoomCredentials
}
func NewRoomWorker(h *Hub, id string) *RoomWorker {
type roomUser struct {
*Session
Sender
}
func NewRoomWorker(manager *roomManager, id string, credentials *DataRoomCredentials) RoomWorker {
log.Printf("Creating worker for room '%s'\n", id)
r := &RoomWorker{
h: h,
Id: id,
r := &roomWorker{
manager: manager,
Id: id,
workers: make(chan func(), roomMaxWorkers),
expired: make(chan bool),
users: make(map[string]*roomUser),
}
if credentials != nil && len(credentials.PIN) > 0 {
r.credentials = credentials
}
r.workers = make(chan func(), roomMaxWorkers)
r.expired = make(chan bool)
r.connections = make(map[string]*Connection)
// Create expire timer.
r.timer = time.AfterFunc(roomExpiryDuration, func() {
@ -76,7 +90,7 @@ func NewRoomWorker(h *Hub, id string) *RoomWorker { @@ -76,7 +90,7 @@ func NewRoomWorker(h *Hub, id string) *RoomWorker {
}
func (r *RoomWorker) Start() {
func (r *roomWorker) Start() {
// Main blocking worker.
L:
@ -90,7 +104,7 @@ L: @@ -90,7 +104,7 @@ L:
//fmt.Println("Work room expired", r.Id)
//fmt.Println("Work room expired", r.Id, len(r.connections))
r.mutex.RLock()
if len(r.connections) == 0 {
if len(r.users) == 0 {
// Cleanup room when it is empty.
r.mutex.RUnlock()
log.Printf("Room worker not in use - cleaning up '%s'\n", r.Id)
@ -107,19 +121,29 @@ L: @@ -107,19 +121,29 @@ L:
}
func (r *RoomWorker) GetConnections() []*Connection {
func (r *roomWorker) SessionIDs() []string {
r.mutex.RLock()
defer r.mutex.RUnlock()
sessions := make([]string, 0, len(r.users))
for id := range r.users {
sessions = append(sessions, id)
}
return sessions
}
func (r *roomWorker) Users() []*roomUser {
r.mutex.RLock()
defer r.mutex.RUnlock()
connections := make([]*Connection, 0, len(r.connections))
for _, connection := range r.connections {
connections = append(connections, connection)
users := make([]*roomUser, 0, len(r.users))
for _, user := range r.users {
users = append(users, user)
}
return connections
return users
}
func (r *RoomWorker) Run(f func()) bool {
func (r *roomWorker) Run(f func()) bool {
select {
case r.workers <- f:
@ -131,13 +155,30 @@ func (r *RoomWorker) Run(f func()) bool { @@ -131,13 +155,30 @@ func (r *RoomWorker) Run(f func()) bool {
}
func (r *RoomWorker) usersHandler(c *Connection) {
func (r *roomWorker) Update(room *DataRoom) error {
fault := make(chan error, 1)
worker := func() {
r.mutex.Lock()
if room.Credentials != nil {
if len(room.Credentials.PIN) > 0 {
r.credentials = room.Credentials
} else {
r.credentials = nil
}
}
r.mutex.Unlock()
fault <- nil
}
r.Run(worker)
return <-fault
}
func (r *roomWorker) GetUsers() []*DataSession {
out := make(chan []*DataSession, 1)
worker := func() {
sessions := &DataSessions{Type: "Users"}
var sl []*DataSession
appender := func(ec *Connection) bool {
ecsession := ec.Session
appender := func(user *roomUser) bool {
ecsession := user.Session
if ecsession != nil {
session := ecsession.Data()
session.Type = "Online"
@ -150,73 +191,95 @@ func (r *RoomWorker) usersHandler(c *Connection) { @@ -150,73 +191,95 @@ func (r *RoomWorker) usersHandler(c *Connection) {
return true
}
r.mutex.RLock()
sl = make([]*DataSession, 0, len(r.connections))
sl = make([]*DataSession, 0, len(r.users))
// Include connections in this room.
for _, ec := range r.connections {
if !appender(ec) {
for _, user := range r.users {
if !appender(user) {
break
}
}
r.mutex.RUnlock()
// Include connections to global room.
for _, ec := range c.h.GetGlobalConnections() {
for _, ec := range r.manager.GlobalUsers() {
if !appender(ec) {
break
}
}
sessions.Users = sl
sessionsJson := c.h.buffers.New()
encoder := json.NewEncoder(sessionsJson)
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: sessions})
if err != nil {
log.Println("Users error while encoding JSON", err)
sessionsJson.Decref()
return
}
c.send(sessionsJson)
sessionsJson.Decref()
out <- sl
}
r.Run(worker)
return <-out
}
func (r *RoomWorker) broadcastHandler(m *MessageRequest) {
func (r *roomWorker) Broadcast(session *Session, message Buffer) {
worker := func() {
r.mutex.RLock()
defer r.mutex.RUnlock()
for id, ec := range r.connections {
if id == m.From {
for id, user := range r.users {
if id == session.Id {
// Skip broadcast to self.
continue
}
//fmt.Printf("%s\n", m.Message)
ec.send(m.Message)
user.Send(message)
}
m.Message.Decref()
message.Decref()
}
m.Message.Incref()
message.Incref()
r.Run(worker)
}
func (r *RoomWorker) connectionHandler(rcu *RoomConnectionUpdate) {
type joinResult struct {
*DataRoom
error
}
func (r *roomWorker) Join(credentials *DataRoomCredentials, session *Session, sender Sender) (*DataRoom, error) {
results := make(chan joinResult, 1)
worker := func() {
r.mutex.Lock()
defer r.mutex.Unlock()
if rcu.Status {
r.connections[rcu.Sessionid] = rcu.Connection
} else {
if _, ok := r.connections[rcu.Sessionid]; ok {
delete(r.connections, rcu.Sessionid)
if r.credentials == nil && credentials != nil {
results <- joinResult{nil, &DataError{"Error", "authorization_not_required", "No credentials may be provided for this room"}}
r.mutex.Unlock()
return
} else if r.credentials != nil {
if credentials == nil {
results <- joinResult{nil, &DataError{"Error", "authorization_required", "Valid credentials are required to join this room"}}
r.mutex.Unlock()
return
}
if len(r.credentials.PIN) != len(credentials.PIN) || subtle.ConstantTimeCompare([]byte(r.credentials.PIN), []byte(credentials.PIN)) != 1 {
results <- joinResult{nil, &DataError{"Error", "invalid_credentials", "The provided credentials are incorrect"}}
r.mutex.Unlock()
return
}
}
}
r.users[session.Id] = &roomUser{session, sender}
// NOTE(lcooper): Needs to be a copy, else we risk races with
// a subsequent modification of room properties.
result := joinResult{&DataRoom{Name: r.Id}, nil}
r.mutex.Unlock()
results <- result
}
r.Run(worker)
result := <-results
return result.DataRoom, result.error
}
func (r *roomWorker) Leave(session *Session) {
worker := func() {
r.mutex.Lock()
defer r.mutex.Unlock()
if _, ok := r.users[session.Id]; ok {
delete(r.users, session.Id)
}
}
r.Run(worker)
}

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

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
/*
* Spreed WebRTC.
* Copyright (C) 2013-2014 struktur AG
*
* This file is part of Spreed WebRTC.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"testing"
)
const (
testRoomName string = "a-room-name"
)
func NewTestRoomWorker() RoomWorker {
worker := NewRoomWorker(&roomManager{}, testRoomName, nil)
go worker.Start()
return worker
}
func NewTestRoomWorkerWithPIN(t *testing.T) (RoomWorker, string) {
pin := "asdf"
worker := NewRoomWorker(&roomManager{}, testRoomName, &DataRoomCredentials{PIN: pin})
go worker.Start()
return worker, pin
}
func Test_RoomWorker_Join_SucceedsWhenNoCredentialsAreRequired(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(nil, &Session{}, nil)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if userCount := len(worker.GetUsers()); userCount != 1 {
t.Errorf("Expected join to have been accepted but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfCredentialsAreGivenWhenUnneeded(t *testing.T) {
worker := NewTestRoomWorker()
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfNoCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(nil, &Session{}, nil)
assertDataError(t, err, "authorization_required")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_FailsIfIncorrectCredentialsAreGiven(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
_, err := worker.Join(&DataRoomCredentials{PIN: "adfs"}, &Session{}, nil)
assertDataError(t, err, "invalid_credentials")
if userCount := len(worker.GetUsers()); userCount != 0 {
t.Errorf("Expected join to have been rejected but room contains %d users", userCount)
}
}
func Test_RoomWorker_Join_SucceedsWhenTheCorrectPINIsGiven(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error %v", err)
}
if len(worker.GetUsers()) < 1 {
t.Error("Expected join to have been accepted but room contains no users")
}
}
func Test_RoomWorker_Update_AllowsClearingCredentials(t *testing.T) {
worker, _ := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{Credentials: &DataRoomCredentials{PIN: ""}}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
_, err := worker.Join(&DataRoomCredentials{}, &Session{}, nil)
assertDataError(t, err, "authorization_not_required")
}
func Test_RoomWorker_Update_RetainsCredentialsWhenOtherPropertiesAreUpdated(t *testing.T) {
worker, pin := NewTestRoomWorkerWithPIN(t)
if err := worker.Update(&DataRoom{}); err != nil {
t.Fatalf("Failed to update room: %v", err)
}
if _, err := worker.Join(&DataRoomCredentials{PIN: pin}, &Session{}, nil); err != nil {
t.Fatalf("Unexpected error joining room %v", err)
}
}

280
src/app/spreed-webrtc-server/server.go

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

28
src/app/spreed-webrtc-server/session.go

@ -39,26 +39,28 @@ type Session struct { @@ -39,26 +39,28 @@ type Session struct {
Status interface{}
Nonce string
Prio int
Hello bool
Roomid string
mutex sync.RWMutex
userid string
fake bool
stamp int64
attestation *SessionAttestation
attestations *securecookie.SecureCookie
subscriptions map[string]*Session
subscribers map[string]*Session
h *Hub
}
func NewSession(h *Hub, id, sid string) *Session {
func NewSession(attestations *securecookie.SecureCookie, id, sid string) *Session {
session := &Session{
Id: id,
Sid: sid,
Prio: 100,
stamp: time.Now().Unix(),
attestations: attestations,
subscriptions: make(map[string]*Session),
subscribers: make(map[string]*Session),
h: h,
}
session.NewAttestation()
return session
@ -288,35 +290,27 @@ func (s *Session) DataSessionStatus() *DataSession { @@ -288,35 +290,27 @@ func (s *Session) DataSessionStatus() *DataSession {
}
func (s *Session) NewAttestation() {
s.attestation = &SessionAttestation{
s: s,
}
s.attestation.Update()
}
func (s *Session) Attestation() (attestation string) {
s.mutex.RLock()
attestation = s.attestation.Token()
s.mutex.RUnlock()
return
}
func (s *Session) UpdateAttestation() {
s.mutex.Lock()
s.attestation.Update()
s.mutex.Unlock()
}
type SessionUpdate struct {
Id string
Types []string
Roomid string
Ua string
Prio int
Status interface{}
@ -336,39 +330,31 @@ type SessionAttestation struct { @@ -336,39 +330,31 @@ type SessionAttestation struct {
}
func (sa *SessionAttestation) Update() (string, error) {
token, err := sa.Encode()
if err == nil {
sa.token = token
sa.refresh = time.Now().Unix() + 180 // expires after 3 minutes
}
return token, err
}
func (sa *SessionAttestation) Token() (token string) {
if sa.refresh < time.Now().Unix() {
token, _ = sa.Update()
} else {
token = sa.token
}
return
}
func (sa *SessionAttestation) Encode() (string, error) {
return sa.s.h.attestations.Encode("attestation", sa.s.Id)
return sa.s.attestations.Encode("attestation", sa.s.Id)
}
func (sa *SessionAttestation) Decode(token string) (string, error) {
var id string
err := sa.s.h.attestations.Decode("attestation", token, &id)
err := sa.s.attestations.Decode("attestation", token, &id)
return id, err
}
func init() {

173
src/app/spreed-webrtc-server/session_manager.go

@ -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
}

13
src/app/spreed-webrtc-server/sessions.go

@ -23,6 +23,7 @@ package main @@ -23,6 +23,7 @@ package main
import (
"encoding/json"
"errors"
"github.com/gorilla/mux"
"log"
"net/http"
@ -42,7 +43,8 @@ type SessionNonceRequest struct { @@ -42,7 +43,8 @@ type SessionNonceRequest struct {
}
type Sessions struct {
hub *Hub
SessionValidator
SessionStore
users *Users
}
@ -78,7 +80,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H @@ -78,7 +80,7 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
}
// Make sure Sid matches session and is valid.
if !sessions.hub.ValidateSession(snr.Id, snr.Sid) {
if !sessions.ValidateSession(snr.Id, snr.Sid) {
log.Println("Session patch failed - validation failed.")
error = true
}
@ -104,7 +106,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H @@ -104,7 +106,12 @@ func (sessions *Sessions) Patch(request *http.Request) (int, interface{}, http.H
var nonce string
if !error {
// FIXME(longsleep): Not running this might reveal error state with a timing attack.
nonce, err = sessions.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
if session, ok := sessions.GetSession(snr.Id); ok {
nonce, err = session.Authorize(sessions.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil {
log.Println("Session patch failed - handle failed.", err)
error = true

8
src/app/spreed-webrtc-server/stats.go

@ -33,11 +33,11 @@ type Stat struct { @@ -33,11 +33,11 @@ type Stat struct {
Hub *HubStat `json:"hub"`
}
func NewStat(details bool, h *Hub) *Stat {
func NewStat(details bool, statsGenerator StatsGenerator) *Stat {
stat := &Stat{
details: details,
Runtime: &RuntimeStat{},
Hub: h.Stat(details),
Hub: statsGenerator.Stat(details),
}
stat.Runtime.Read()
return stat
@ -69,12 +69,12 @@ func (stat *RuntimeStat) Read() { @@ -69,12 +69,12 @@ func (stat *RuntimeStat) Read() {
}
type Stats struct {
hub *Hub
StatsGenerator
}
func (stats *Stats) Get(request *http.Request) (int, interface{}, http.Header) {
details := request.Form.Get("details") == "1"
return 200, NewStat(details, stats.hub), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}}
return 200, NewStat(details, stats), http.Header{"Content-Type": {"application/json; charset=utf-8"}, "Access-Control-Allow-Origin": {"*"}}
}

104
src/app/spreed-webrtc-server/stats_manager.go

@ -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,
}
}

130
src/app/spreed-webrtc-server/tickets.go

@ -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
}

32
src/app/spreed-webrtc-server/users.go

@ -291,16 +291,21 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) { @@ -291,16 +291,21 @@ func (un *UserNonce) Response() (int, interface{}, http.Header) {
}
type Users struct {
hub *Hub
SessionValidator
SessionManager
SessionStore
realm string
handler UsersHandler
}
func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
func NewUsers(sessionStore SessionStore, sessionValidator SessionValidator, sessionManager SessionManager, mode, realm string, runtime phoenix.Runtime) *Users {
var users = &Users{
hub: hub,
realm: realm,
sessionValidator,
sessionManager,
sessionStore,
realm,
nil,
}
var handler UsersHandler
@ -309,8 +314,8 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { @@ -309,8 +314,8 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
// Create handler based on mode.
if handler, err = users.createHandler(mode, runtime); handler != nil && err == nil {
users.handler = handler
// Register handler Get at the hub.
users.hub.useridRetriever = func(request *http.Request) (userid string, err error) {
// Register handler Get.
sessionManager.RetrieveUsersWith(func(request *http.Request) (userid string, err error) {
userid, err = handler.Get(request)
if err != nil {
log.Printf("Failed to get userid from handler: %s", err)
@ -320,7 +325,7 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users { @@ -320,7 +325,7 @@ func NewUsers(hub *Hub, mode, realm string, runtime phoenix.Runtime) *Users {
}
}
return
}
})
log.Printf("Enabled users handler '%s'\n", mode)
} else if err != nil {
log.Printf("Failed to enable handler '%s': %s\n", mode, err)
@ -450,11 +455,20 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header) @@ -450,11 +455,20 @@ func (users *Users) Post(request *http.Request) (int, interface{}, http.Header)
userid := fmt.Sprintf("%s@%s", uuid.NewV4().String(), users.realm)
// Make sure Sid matches session and is valid.
if !users.hub.ValidateSession(snr.Id, snr.Sid) {
if !users.ValidateSession(snr.Id, snr.Sid) {
return 403, NewApiError("users_invalid_session", "Invalid session"), http.Header{"Content-Type": {"application/json"}}
}
nonce, err := users.hub.sessiontokenHandler(&SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
var (
nonce string
err error
)
if session, ok := users.GetSession(snr.Id); ok {
nonce, err = session.Authorize(users.Realm(), &SessionToken{Id: snr.Id, Sid: snr.Sid, Userid: userid})
} else {
err = errors.New("no such session")
}
if err != nil {
return 400, NewApiError("users_request_failed", fmt.Sprintf("Error: %q", err)), http.Header{"Content-Type": {"application/json"}}
}

35
src/app/spreed-webrtc-server/ws.go

@ -22,9 +22,10 @@ @@ -22,9 +22,10 @@
package main
import (
"github.com/gorilla/websocket"
"log"
"net/http"
"github.com/gorilla/websocket"
)
const (
@ -49,10 +50,8 @@ var ( @@ -49,10 +50,8 @@ var (
}
)
func makeWsHubHandler(h *Hub) http.HandlerFunc {
func makeWSHandler(connectionCounter ConnectionCounter, sessionManager SessionManager, codec Codec, channellingAPI ChannellingAPI) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Validate incoming request.
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
@ -68,30 +67,14 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc { @@ -68,30 +67,14 @@ func makeWsHubHandler(h *Hub) http.HandlerFunc {
return
}
// Read request details.
r.ParseForm()
token := r.FormValue("t")
// Create a new connection instance.
c := NewConnection(h, ws, r)
if token != "" {
if err := c.reregister(token); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
} else {
if err := c.register(); err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
}
session := sessionManager.CreateSession(r)
defer sessionManager.DestroySession(session)
client := NewClient(codec, channellingAPI, session)
conn := NewConnection(connectionCounter.CountConnection(), ws, client)
// Start pumps (readPump blocks).
go c.writePump()
c.readPump()
go conn.writePump()
conn.readPump()
}
}

3
static/js/app.js

@ -184,6 +184,9 @@ define([ @@ -184,6 +184,9 @@ define([
var deferred = $.Deferred();
var globalContext = JSON.parse($("#globalcontext").text());
if (!globalContext.Cfg.Version) {
globalContext.Cfg.Version = "unknown";
}
app.constant("globalContext", globalContext);
// Configure language.

62
static/js/controllers/mediastreamcontroller.js

@ -20,7 +20,7 @@ @@ -20,7 +20,7 @@
*/
define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapter'], function(_, BigScreen, moment, sjcl, Modernizr) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs) {
return ["$scope", "$rootScope", "$element", "$window", "$timeout", "safeDisplayName", "safeApply", "mediaStream", "appData", "playSound", "desktopNotify", "alertify", "toastr", "translation", "fileDownload", "localStorage", "screensharing", "userSettingsData", "localStatus", "dialogs", "rooms", function($scope, $rootScope, $element, $window, $timeout, safeDisplayName, safeApply, mediaStream, appData, playSound, desktopNotify, alertify, toastr, translation, fileDownload, localStorage, screensharing, userSettingsData, localStatus, dialogs, rooms) {
/*console.log("route", $route, $routeParams, $location);*/
@ -544,20 +544,17 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -544,20 +544,17 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
// Unmark authorization process.
if (data.Userid) {
mediaStream.users.authorizing(false);
} else if (!mediaStream.users.authorizing()) {
$rootScope.authorizing(false);
$rootScope.$broadcast("authorization.succeeded");
} else if (!$rootScope.authorizing()) {
// Trigger user data load when not in authorizing phase.
$scope.loadUserSettings();
}
if (!$rootScope.roomid && $scope.master.settings.defaultRoom) {
if (rooms.inDefaultRoom() && $scope.master.settings.defaultRoom) {
console.log("Selecting default room from settings:", [$scope.master.settings.defaultRoom]);
mediaStream.changeRoom($scope.master.settings.defaultRoom, true);
rooms.joinByName($scope.master.settings.defaultRoom, true);
}
// Always apply room after self received to avoid double stuff.
mediaStream.applyRoom();
});
mediaStream.webrtc.e.on("peercall", function(event, peercall) {
@ -660,36 +657,36 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -660,36 +657,36 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
}
};
mediaStream.connector.e.on("open error close", function(event, options) {
var t = event.type;
var opts = $.extend({}, options);
$scope.$on("room.joined", function(ev) {
// TODO(lcooper): Is it really needful to do this stuff?
$timeout.cancel(ttlTimeout);
if (!opts.soft) {
// Reset login information for anything not soft.
$scope.userid = $scope.suserid = null;
}
switch (t) {
connected = true;
reconnecting = false;
$scope.updateStatus(true);
});
mediaStream.connector.e.on("open error close", function(event) {
$timeout.cancel(ttlTimeout);
$scope.userid = $scope.suserid = null;
switch (event.type) {
case "open":
t = "waiting";
connected = true;
reconnecting = false;
$scope.updateStatus(true);
if (opts.soft) {
return;
}
$scope.setStatus("waiting");
break;
case "error":
if (reconnecting || connected) {
reconnecting = false;
reconnect();
return;
} else {
$scope.setStatus(event.type);
}
break;
case "close":
reconnect();
return;
break;
}
$scope.setStatus(t);
});
mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) {
@ -797,23 +794,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte @@ -797,23 +794,6 @@ define(['underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'webrtc.adapte
if (mediaStream.connector.connected) {
$scope.setStatus("waiting");
}
if ($scope.roomstatus) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
$scope.$watch("roomstatus", function(roomstatus) {
if (roomstatus && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
});
mediaStream.webrtc.e.on("busy", function(event, from) {

69
static/js/controllers/roomchangecontroller.js

@ -19,72 +19,7 @@ @@ -19,72 +19,7 @@
*
*/
define([], function() {
// RoomchangeController
return ["$scope", "$element", "$window", "mediaStream", "$http", "$timeout", function($scope, $element, $window, mediaStream, $http, $timeout) {
//console.log("Room change controller", $element, $scope.roomdata);
var url = mediaStream.url.api("rooms");
var ctrl = this;
ctrl.enabled = true;
ctrl.getRoom = function(cb) {
$http({
method: "POST",
url: url,
data: $.param({}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).
success(function(data, status) {
cb(data);
}).
error(function() {
console.error("Failed to retrieve room link.");
cb({});
});
};
$scope.changeRoomToId = function(id) {
return mediaStream.changeRoom(id);
};
$scope.refreshRoom = function() {
if (ctrl.enabled) {
ctrl.getRoom(function(roomdata) {
console.info("Retrieved room data", roomdata);
$scope.roomdata = roomdata;
$element.find(".btn-roomcreate").get(0).focus();
});
}
};
$scope.$on("$destroy", function() {
//console.log("Room change controller destroyed");
ctrl.enabled = false;
});
$scope.roomdata = {};
$scope.$watch("roomdata.name", function(n) {
//console.log("roomdata.name changed", n);
if (!n) {
n = "";
}
var u = encodeURIComponent(n);
$scope.roomdata.url = "/" + u;
$scope.roomdata.link = mediaStream.url.room(n);
});
var roomDataLinkInput = $element.find(".roomdata-link-input");
if (roomDataLinkInput.length) {
$timeout(function() {
$scope.refreshRoom();
}, 100);
}
return ["$scope", "rooms", function($scope, rooms) {
$scope.joinRoomByName = rooms.joinByName;
}];
});

63
static/js/directives/buddylist.js

@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
// buddyList
return ["$compile", "buddyList", "mediaStream", "contacts", function($compile, buddyList, mediaStream, contacts) {
return ["buddyList", "api", "webrtc", "contacts", function(buddyList, api, webrtc, contacts) {
//console.log("buddyList directive");
@ -30,10 +30,34 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -30,10 +30,34 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
$scope.doCall = function(id) {
var inRoom = false;
var updateBuddyListVisibility = function() {
if (inRoom && !$scope.peer) {
$scope.layout.buddylist = true;
$scope.layout.buddylistAutoHide = false;
} else if (!$scope.layout.buddylistAutoHide) {
$scope.layout.buddylist = false;
$scope.layout.buddylistAutoHide = true;
}
};
webrtc.e.on("done", function() {
updateBuddyListVisibility();
});
mediaStream.webrtc.doCall(id);
$scope.$on("room.joined", function(ev) {
inRoom = true;
updateBuddyListVisibility();
});
$scope.$on("room.left", function(ev) {
inRoom = false;
buddylist.onClosed();
updateBuddyListVisibility();
});
$scope.doCall = function(id) {
webrtc.doCall(id);
};
$scope.doChat = function(id) {
@ -61,23 +85,6 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -61,23 +85,6 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
};
/*
$scope.doAudioConference = function(id) {
$scope.updateAutoAccept(id);
mediaStream.api.sendChat(id, null, {
AutoCall: {
Type: "conference",
Id: mediaStream.connector.roomid
}
})
};*/
$scope.setRoomStatus = function(status) {
$scope.$emit("roomStatus", status);
};
var buddylist = $scope.buddylist = buddyList.buddylist($element, $scope, {});
var onJoined = _.bind(buddylist.onJoined, buddylist);
var onLeft = _.bind(buddylist.onLeft, buddylist);
@ -85,15 +92,14 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -85,15 +92,14 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
var onContactAdded = _.bind(buddylist.onContactAdded, buddylist);
var onContactRemoved = _.bind(buddylist.onContactRemoved, buddylist);
var onContactUpdated = _.bind(buddylist.onContactUpdated, buddylist);
mediaStream.api.e.on("received.userleftorjoined", function(event, dataType, data) {
api.e.on("received.userleftorjoined", function(event, dataType, data) {
if (dataType === "Left") {
onLeft(data);
} else {
onJoined(data);
}
});
mediaStream.api.e.on("received.users", function(event, data) {
$scope.setRoomStatus(true);
api.e.on("received.users", function(event, data) {
var selfId = $scope.id;
_.each(data, function(p) {
if (p.Id !== selfId) {
@ -102,17 +108,10 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -102,17 +108,10 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) {
});
$scope.$apply();
});
mediaStream.api.e.on("received.status", function(event, data) {
api.e.on("received.status", function(event, data) {
onStatus(data);
});
mediaStream.connector.e.on("closed error", function() {
$scope.setRoomStatus(false);
buddylist.onClosed();
});
// Request user list whenever the connection comes ready.
mediaStream.connector.ready(function() {
mediaStream.api.requestUsers();
});
// Contacts.
contacts.e.on("contactadded", function(event, data) {
onContactAdded(data);

36
static/js/directives/chat.js

@ -44,16 +44,13 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -44,16 +44,13 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
var res = [];
for (var i = 0; i < ctrl.visibleRooms.length; i++) {
var r = rooms[ctrl.visibleRooms[i]];
if (!r || r.id === ctrl.group) {
if (!r) {
continue;
}
res.push(r);
}
return res;
};
$scope.getGroupRoom = function() {
return rooms[ctrl.group];
};
mediaStream.api.e.on("received.chat", function(event, id, from, data, p2p) {
@ -182,22 +179,31 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -182,22 +179,31 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.showGroupRoom = function(settings, options) {
var stngs = $.extend({
title: translation._("Room chat")
title: translation._("Room chat"),
group: true
}, settings);
return scope.showRoom(controller.group, stngs, options);
};
scope.hideGroupRoom = function(settings, options) {
return scope.hideRoom(controller.group);
};
scope.showRoom = function(id, settings, opts) {
var options = $.extend({}, opts);
var subscope = controller.rooms[id];
var index = controller.visibleRooms.length;
if (!subscope) {
console.log("Create new chatroom", [id]);
controller.visibleRooms.push(id);
if (settings.group) {
controller.visibleRooms.unshift(id);
} else {
controller.visibleRooms.push(id);
}
subscope = controller.rooms[id] = scope.$new();
translation.inject(subscope);
subscope.id = id;
subscope.isgroupchat = id === controller.group ? true : false;
subscope.isgroupchat = !!settings.group;
subscope.index = index;
subscope.settings = settings;
subscope.visible = false;
@ -474,11 +480,6 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -474,11 +480,6 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.currentRoomActive = false;
}
if (!controller.visibleRooms.length) {
scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
// If last visible room was removed, hide chat.
scope.layout.chat = false;
}
@ -544,18 +545,23 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'], @@ -544,18 +545,23 @@ define(['underscore', 'text!partials/chat.html', 'text!partials/chatroom.html'],
scope.layout.chatMaximized = false;
});
scope.$on("room", function(event, room) {
scope.$on("room.updated", function(event, room) {
var subscope = scope.showGroupRoom(null, {
restore: true,
noenable: true,
noactivate: true
});
if (room) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room));
if (scope.currentRoomName != room.Name) {
var msg = $("<span>").text(translation._("You are now in room %s ...", room.Name));
subscope.$broadcast("display", null, $("<i>").append(msg));
scope.currentRoomName = room.Name;
}
});
scope.$on("room.left", function(event) {
scope.hideGroupRoom();
scope.currentRoomName = null;
});
};
};

8
static/js/directives/directives.js

@ -43,7 +43,8 @@ define([ @@ -43,7 +43,8 @@ define([
'directives/odfcanvas',
'directives/presentation',
'directives/youtubevideo',
'directives/bfi'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi) {
'directives/bfi',
'directives/title'], function(_, onEnter, onEscape, statusMessage, buddyList, buddyPictureCapture, buddyPictureUpload, settings, chat, audioVideo, usability, audioLevel, fileInfo, screenshare, roomBar, socialShare, page, contactRequest, defaultDialog, pdfcanvas, odfcanvas, presentation, youtubevideo, bfi, title) {
var directives = {
onEnter: onEnter,
@ -68,13 +69,14 @@ define([ @@ -68,13 +69,14 @@ define([
odfcanvas: odfcanvas,
presentation: presentation,
youtubevideo: youtubevideo,
bfi: bfi
bfi: bfi,
title: title
};
var initialize = function(angModule) {
_.each(directives, function(directive, name) {
angModule.directive(name, directive);
})
});
};
return {

54
static/js/directives/page.js

@ -20,55 +20,37 @@ @@ -20,55 +20,37 @@
*/
define(['text!partials/page.html', 'text!partials/page/welcome.html'], function(template, welcome) {
return ["$templateCache", "mediaStream", function($templateCache, mediaStream) {
return ["$templateCache", "$timeout", "rooms", function($templateCache, $timeout, rooms) {
$templateCache.put('page/welcome.html', welcome);
var link = function(scope, element, attrs) {
scope.room = false;
scope.page = null;
if (mediaStream.config.DefaultRoomEnabled !== true) {
scope.$on("welcome", function() {
if (!scope.initialized) {
scope.initialized = true;
scope.refresh();
}
});
var link = function($scope, $element, attrs) {
$scope.randomRoom = rooms.randomRoom;
scope.$on("room", function(event, room) {
scope.initialized = true;
scope.room = room !== null ? true : false;
scope.refresh();
});
$scope.$on("room.joined", function(event) {
$scope.page = null;
});
scope.$watch("status", function(event) {
if (scope.initialized) {
scope.refresh();
}
$scope.$on("room.random", function(ev, roomdata) {
$scope.page = "page/welcome.html";
$scope.roomdata = roomdata;
$timeout(function() {
$element.find(".btn-roomcreate:visible:enabled:first").focus();
});
});
scope.refresh = function() {
if (scope.roomid || scope.room || scope.status !== "waiting") {
scope.page = null;
} else {
scope.page = "page/welcome.html";
}
};
}
$scope.roomdata = {};
$scope.$watch("roomdata.name", function(name) {
$scope.roomdata.link = rooms.link($scope.roomdata);
});
};
return {
restrict: 'E',
replace: true,
template: template,
controller: "RoomchangeController",
link: link
}
};
}];
});

26
static/js/directives/roombar.js

@ -21,17 +21,19 @@ @@ -21,17 +21,19 @@
define(['underscore', 'text!partials/roombar.html'], function(_, template) {
// roomBar
return ["$window", "$rootScope", "mediaStream", function($window, $rootScope, mediaStream) {
return ["$window", "rooms", function($window, rooms) {
var link = function($scope) {
var clearRoomName = function(ev) {
$scope.currentRoomName = $scope.newRoomName = "";
};
//console.log("roomBar directive link", arguments);
$scope.newroomid = $rootScope.roomid;
$scope.layout.roombar = false;
$scope.save = function() {
var roomid = mediaStream.changeRoom($scope.newroomid);
if (roomid !== $rootScope.roomid) {
var roomName = rooms.joinByName($scope.newRoomName);
if (roomName !== $scope.currentRoomName) {
$scope.roombarform.$setPristine();
}
$scope.layout.roombar = false;
@ -44,23 +46,23 @@ define(['underscore', 'text!partials/roombar.html'], function(_, template) { @@ -44,23 +46,23 @@ define(['underscore', 'text!partials/roombar.html'], function(_, template) {
};
$scope.exit = function() {
$scope.newroomid = "";
$scope.newRoomName = "";
$scope.save();
};
$rootScope.$watch("roomid", function(newroomid, roomid) {
if (!newroomid) {
newroomid = "";
}
$scope.newroomid = newroomid;
$scope.$on("room.updated", function(ev, room) {
$scope.currentRoomName = $scope.newRoomName = room.Name;
});
$scope.$watch("newroomid", function(newroomid) {
if (newroomid === $rootScope.roomid) {
$scope.$on("room.left", clearRoomName);
$scope.$watch("newRoomName", function(name) {
if (name === $scope.currentRoomName) {
$scope.roombarform.$setPristine();
}
});
clearRoomName();
};
return {

10
static/js/directives/socialshare.js

@ -29,7 +29,7 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -29,7 +29,7 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
};
// socialShare
return ["$window", "translation", function($window, translation) {
return ["$window", "translation", "rooms", function($window, translation, rooms) {
var title = $window.encodeURIComponent($window.document.title);
var makeUrl = function(nw, target) {
@ -46,6 +46,14 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) { @@ -46,6 +46,14 @@ define(['jquery', 'text!partials/socialshare.html'], function($, template) {
template: template,
replace: true,
link: function($scope, $element, $attr) {
$scope.$on("room.updated", function(ev, room) {
$scope.roomlink = rooms.link(room);
});
$scope.$on("room.left", function(ev) {
$scope.roomlink = null;
});
$element.on("click", "a", function(event) {
var nw = $(event.currentTarget).data("nw");
var url = makeUrl(nw, $scope.roomlink);

48
static/js/directives/title.js

@ -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
};
}];
});

40
static/js/directives/usability.js

@ -22,15 +22,13 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -22,15 +22,13 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
var MEDIA_CHECK = "1" // First version of media check flag.
return ["mediaStream", function(mediaStream) {
return [function() {
var controller = ['$scope', "mediaStream", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, mediaStream, safeApply, $timeout, localStorage, continueConnector) {
var controller = ['$scope', "webrtc", "safeApply", "$timeout", "localStorage", "continueConnector", function($scope, webrtc, safeApply, $timeout, localStorage, continueConnector) {
var pending = true;
var complete = false;
var initializer = null;
var ctrl = this;
ctrl.setInfo = function(info) {
$scope.usabilityInfo = info;
@ -46,16 +44,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -46,16 +44,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
localStorage.setItem("mediastream-mediacheck", MEDIA_CHECK)
console.log("Continue with connect after media check ...");
continueDeferred.resolve();
if (mediaStream.config.DefaultRoomEnabled !== true) {
ctrl.setInfo("initializing");
initializer = $timeout(function() {
ctrl.setInfo("ok");
$scope.layout.settings = false;
$scope.$emit("welcome");
}, 1000);
} else {
ctrl.setInfo("ok");
}
ctrl.setInfo("ok");
complete = true;
} else {
ctrl.setInfo("denied");
@ -70,7 +59,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -70,7 +59,7 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
// NOTE(longsleep): Checkin for media access makes only sense on
// Chrome for now, as its the only one which remembers this
// decision permanently for https.
mediaStream.webrtc.testMediaAccess($scope.continueConnect);
webrtc.testMediaAccess($scope.continueConnect);
} else {
$scope.continueConnect(true);
}
@ -97,19 +86,16 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _, @@ -97,19 +86,16 @@ define(['jquery', 'underscore', 'text!partials/usability.html'], function($, _,
}
});
$scope.$on("room", function(event, room) {
//console.log("roomStatus", room !== null ? true : false);
$scope.$on("room.joined", function(event) {
if (complete) {
if (initializer !== null) {
$timeout.cancel(initializer);
initializer = null;
}
// Check if we should show settings per default when in a room.
if(room && !$scope.loadedUser) {
$scope.layout.settings = true;
} else {
$scope.layout.settings = false;
}
$scope.layout.settings = !$scope.loadedUser;
ctrl.setInfo("ok");
}
});
$scope.$on("room.left", function(event) {
if (complete) {
$scope.layout.settings = false;
ctrl.setInfo("ok");
}
});

68
static/js/mediastream/api.js

@ -18,13 +18,13 @@ @@ -18,13 +18,13 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore'], function($, _) {
define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
var alive_check_timeout = 5000;
var alive_check_timeout_2 = 10000;
var Api = function(connector) {
var Api = function(version, connector) {
this.version = version;
this.id = null;
this.sid = null;
this.session = {};
@ -33,6 +33,15 @@ define(['jquery', 'underscore'], function($, _) { @@ -33,6 +33,15 @@ define(['jquery', 'underscore'], function($, _) {
this.e = $({});
var ua = uaparser();
if (ua.os.name && /Spreed Desktop Caller/i.test(ua.ua)) {
this.userAgent = ua.ua.match(/Spreed Desktop Caller\/([\d.]+)/i)[1] + " (" + ua.os.name + ")";
} else if (ua.browser.name) {
this.userAgent = ua.browser.name + " " + ua.browser.major;
} else {
this.userAgent = ua.ua;
}
connector.e.on("received", _.bind(function(event, data) {
this.received(data);
}, this));
@ -92,7 +101,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -92,7 +101,7 @@ define(['jquery', 'underscore'], function($, _) {
return this.apply(name, obj);
};
Api.prototype.request = function(type, data, cb) {
Api.prototype.request = function(type, data, cb, noqueue) {
var payload = {
Type: type
@ -103,7 +112,7 @@ define(['jquery', 'underscore'], function($, _) { @@ -103,7 +112,7 @@ define(['jquery', 'underscore'], function($, _) {
payload.Iid = iid;
this.e.one(iid+".request", cb);
}
this.connector.send(payload);
this.connector.send(payload, noqueue);
}
@ -199,6 +208,9 @@ define(['jquery', 'underscore'], function($, _) { @@ -199,6 +208,9 @@ define(['jquery', 'underscore'], function($, _) {
// Do nothing.
//console.log("Alive response received.");
break;
case "Room":
this.e.triggerHandler("received.room", [data]);
break;
default:
console.log("Unhandled type received:", dataType, data);
break;
@ -217,6 +229,37 @@ define(['jquery', 'underscore'], function($, _) { @@ -217,6 +229,37 @@ define(['jquery', 'underscore'], function($, _) {
};
Api.prototype.sendHello = function(name, pin, success, fault) {
var data = {
Version: this.version,
Ua: this.userAgent,
Id: name
};
if (pin) {
data.Credentials = {
PIN: pin
};
}
var that = this;
var onResponse = function(event, type, data) {
if (type === "Welcome") {
if (success) {
success(data.Room);
}
that.e.triggerHandler("received.room", [data.Room]);
that.e.triggerHandler("received.users", [data.Users]);
} else {
if (fault) {
fault(data);
}
}
};
this.request("Hello", data, onResponse, true);
};
Api.prototype.sendOffer = function(to, payload) {
var data = {
@ -253,6 +296,21 @@ define(['jquery', 'underscore'], function($, _) { @@ -253,6 +296,21 @@ define(['jquery', 'underscore'], function($, _) {
}
Api.prototype.requestRoomUpdate = function(room, success, fault) {
var onResponse = function(event, type, data) {
if (type === "Room") {
if (success) {
success(data);
}
} else {
if (fault) {
fault(data);
}
}
};
this.request("Room", room, onResponse, true);
};
Api.prototype.requestUsers = function() {
var data = {

59
static/js/mediastream/connector.js

@ -18,14 +18,12 @@ @@ -18,14 +18,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
define(['jquery', 'underscore'], function($, _, uaparser) {
var timeout = 5000;
var timeout_max = 20000;
var Connector = function(version) {
this.version = version;
var Connector = function() {
this.e = $({});
this.error = false;
this.connected = false;
@ -35,18 +33,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -35,18 +33,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
this.token = null;
this.queue = [];
this.roomid = null;
var ua = uaparser();
if (ua.os.name && /Spreed Desktop Caller/i.test(ua.ua)) {
this.userAgent = ua.ua.match(/Spreed Desktop Caller\/([\d.]+)/i)[1] + " (" + ua.os.name + ")";
} else if (ua.browser.name) {
this.userAgent = ua.browser.name + " " + ua.browser.major;
} else {
this.userAgent = ua.ua;
}
};
Connector.prototype.connect = function(url) {
@ -110,7 +96,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -110,7 +96,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
Connector.prototype.close = function() {
this.connected = false;
this.roomid = null;
if (this.conn) {
var conn = this.conn;
this.conn = null;
@ -131,42 +116,7 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -131,42 +116,7 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
};
Connector.prototype.room = function(roomid) {
var was_connected = this.connected;
if (was_connected) {
if (this.roomid === roomid) {
return;
}
this.e.triggerHandler("closed", [{
soft: true
}]);
}
this.roomid = roomid;
roomid = this.roomid ? this.roomid : "";
this.send({
Type: "Hello",
Hello: {
Version: this.version,
Ua: this.userAgent,
Id: roomid
}
}, true);
this.e.triggerHandler("helloed", [roomid]);
if (was_connected) {
this.e.triggerHandler("open", [{
soft: true
}]);
}
};
Connector.prototype.onopen = function(event) {
window.clearTimeout(this.connecting);
this.connecting_timeout = timeout;
@ -181,9 +131,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -181,9 +131,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
data = this.queue.shift();
this.send(data);
}
this.e.triggerHandler("opened");
};
Connector.prototype.onerror = function(event) {
@ -210,8 +157,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) { @@ -210,8 +157,6 @@ define(['jquery', 'underscore', 'ua-parser'], function($, _, uaparser) {
if (!this.error) {
this.e.triggerHandler("close", [null, event]);
}
this.e.triggerHandler("closed", [null, event]);
};
Connector.prototype.onmessage = function(event) {

27
static/js/services/api.js

@ -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);
}];
});

6
static/js/services/buddypicture.js

@ -22,7 +22,7 @@ @@ -22,7 +22,7 @@
define(['underscore'], function(underscore) {
// buddyPicture
return ["mediaStream", "$window", function(mediaStream, $window) {
return ["$window", "restURL", function($window, restURL) {
var buddyPicture = {
@ -38,7 +38,7 @@ @@ -38,7 +38,7 @@
}
if (url.indexOf("img:") === 0) {
data.buddyPicture = data.buddyPictureLocalUrl = mediaStream.url.buddy(url.substr(4));
data.buddyPicture = data.buddyPictureLocalUrl = restURL.buddy(url.substr(4));
}
},
@ -83,4 +83,4 @@ @@ -83,4 +83,4 @@
}];
});
});

27
static/js/services/connector.js

@ -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();
}];
});

143
static/js/services/mediastream.js

@ -23,29 +23,31 @@ define([ @@ -23,29 +23,31 @@ define([
'underscore',
'ua-parser',
'modernizr',
'mediastream/connector',
'mediastream/api',
'mediastream/webrtc',
'mediastream/tokens'
], function($, _, uaparser, Modernizr, Connector, Api, WebRTC, tokens) {
], function($, _, uaparser, Modernizr, tokens) {
return ["globalContext", "$rootScope", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", "localStorage", "continueConnector", function(context, $rootScope, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce, localStorage, continueConnector) {
return ["globalContext", "connector", "api", "webrtc", "$rootScope", "$route", "$location", "$window", "visibility", "alertify", "$http", "safeApply", "$timeout", "$sce", "localStorage", "continueConnector", "restURL", function(context, connector, api, webrtc, $rootScope, $route, $location, $window, visibility, alertify, $http, safeApply, $timeout, $sce, localStorage, continueConnector, restURL) {
var url = (context.Ssl ? "wss" : "ws") + "://" + context.Host + (context.Cfg.B || "/") + "ws";
var version = context.Cfg.Version || "unknown";
var version = context.Cfg.Version;
console.log("Service version: " + version);
console.log("Ws URL: " + url);
console.log("Secure Contextual Escaping: " + $sce.isEnabled());
var connector = new Connector(version);
var api = new Api(connector);
var webrtc = new WebRTC(api);
var connectMarker = null;
// Create encryption key from server token and browser name.
var secureKey = sjcl.codec.base64.fromBits(sjcl.hash.sha256.hash(context.Cfg.Token + uaparser().browser.name));
var authorizing = false;
var authorizing = context.Cfg.UsersEnabled;
$rootScope.authorizing = function(value) {
// Boolean flag to indicate that an authentication is currently in progress.
if (typeof(value) !== "undefined") {
authorizing = !!value;
}
return authorizing;
};
var mediaStream = {
version: version,
@ -55,21 +57,9 @@ define([ @@ -55,21 +57,9 @@ define([
connector: connector,
api: api,
tokens: tokens,
url: {
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;
}
},
users: {
register: function(form, success_cb, error_cb) {
var url = mediaStream.url.api("users");
var url = restURL.api("users");
if (form) {
// Form submit mode.
$(form).attr("action", url).attr("method", "POST");
@ -135,16 +125,9 @@ define([ @@ -135,16 +125,9 @@ define([
});
}
},
authorizing: function(value) {
// Boolean flag to indicate that an authentication is currently in progress.
if (typeof(value) !== "undefined") {
authorizing = !!value;
}
return authorizing;
},
authorize: function(data, success_cb, error_cb) {
mediaStream.users.authorizing(true);
var url = mediaStream.url.api("sessions") + "/" + mediaStream.api.id + "/";
$rootScope.authorizing(true);
var url = restURL.api("sessions") + "/" + mediaStream.api.id + "/";
var login = _.clone(data);
login.id = mediaStream.api.id;
login.sid = mediaStream.api.sid;
@ -160,14 +143,14 @@ define([ @@ -160,14 +143,14 @@ define([
if (data.nonce !== "" && data.success) {
success_cb(data, status);
} else {
mediaStream.users.authorizing(false);
$rootScope.authorizing(false);
if (error_cb) {
error_cb(data, status);
}
}
}).
error(function(data, status) {
mediaStream.users.authorizing(false);
$rootScope.authorizing(false);
if (error_cb) {
error_cb(data, status)
}
@ -226,40 +209,12 @@ define([ @@ -226,40 +209,12 @@ define([
}
});
},
changeRoom: function(id, replace) {
id = $window.encodeURIComponent(id);
// Allow room ids to start with @,$ and + without quoting.
id = id.replace(/^%40/, "@");
id = id.replace(/^%24/, "$");
id = id.replace(/^%2B/, "+");
safeApply($rootScope, function(scope) {
$location.path("/" + id);
if (replace) {
$location.replace();
}
});
return id;
},
applyRoom: function() {
if (authorizing) {
// Do nothing while authorizing.
return;
}
var roomid = $rootScope.roomid;
if (roomid !== connector.roomid) {
console.log("Apply room", roomid);
connector.room(roomid);
}
},
initialize: function($rootScope, translation) {
var cont = false;
var ready = false;
$rootScope.version = version;
$rootScope.roomid = null;
$rootScope.roomlink = null;
$rootScope.roomstatus = false;
$rootScope.connect = false;
var connect = function() {
@ -278,61 +233,10 @@ define([ @@ -278,61 +233,10 @@ define([
}
};
var title = (function(e) {
return {
element: e,
text: e.text()
}
}($("title")));
// Room selector.
$rootScope.$on("$locationChangeSuccess", function(event) {
var room;
if ($route.current) {
room = $route.current.params.room;
room = $window.decodeURIComponent(room);
} else {
room = "";
}
console.info("Selected room is:", [room], ready, cont);
$rootScope.roomid = room;
if (!ready || !cont) {
ready = true;
connect();
} else {
// Auto apply room when already connected.
mediaStream.applyRoom();
}
$rootScope.roomlink = room ? mediaStream.url.room(room) : null;
if ($rootScope.roomlink) {
title.element.text(room + " - " + title.text);
} else {
title.element.text(title.text);
}
});
// Cache events, to avoid ui flicker during quick room changes.
var roomStatusCache = $rootScope.roomstatus;
var roomCache = null;
var roomCache2 = null;
$rootScope.$on("roomStatus", function(event, status) {
// roomStatus is triggered by the buddylist when received.users.
roomStatusCache = status ? true : false;
roomCache = status ? $rootScope.roomid : null;
$timeout(function() {
if ($rootScope.roomstatus !== roomStatusCache) {
$rootScope.roomstatus = roomStatusCache;
}
if (roomCache !== roomCache2) {
// Let every one know about the new room.
$rootScope.$broadcast("room", roomCache);
roomCache2 = roomCache;
}
}, 100);
$rootScope.$on("rooms.ready", function(event) {
console.info("Initial room path set, continuing to connect ...");
ready = true;
connect();
});
visibility.afterPrerendering(function() {
@ -356,7 +260,7 @@ define([ @@ -356,7 +260,7 @@ define([
}
}, prompt);
};
var url = mediaStream.url.api("tokens");
var url = restURL.api("tokens");
var check = function(code) {
$http({
method: "POST",
@ -406,9 +310,6 @@ define([ @@ -406,9 +310,6 @@ define([
}
};
// For debugging.
$window.changeRoom = mediaStream.changeRoom;
return mediaStream;
}];

38
static/js/services/resturl.js

@ -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;
}
};
}];
});

58
static/js/services/roompin.js

@ -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;
}];
});

204
static/js/services/rooms.js

@ -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;
}];
});

26
static/js/services/services.js

@ -24,6 +24,9 @@ define([ @@ -24,6 +24,9 @@ define([
'services/desktopnotify',
'services/playsound',
'services/safeapply',
'services/connector',
'services/api',
'services/webrtc',
'services/mediastream',
'services/appdata',
'services/buddydata',
@ -56,10 +59,16 @@ define([ @@ -56,10 +59,16 @@ define([
'services/continueconnector',
'services/chromeextension',
'services/usersettingsdata',
'services/localstatus'], function(_,
'services/localstatus',
'services/rooms',
'services/resturl',
'services/roompin'], function(_,
desktopNotify,
playSound,
safeApply,
connector,
api,
webrtc,
mediaStream,
appData,
buddyData,
@ -92,12 +101,18 @@ screensharing, @@ -92,12 +101,18 @@ screensharing,
continueConnector,
chromeExtension,
userSettingsData,
localStatus) {
localStatus,
rooms,
restURL,
roompin) {
var services = {
desktopNotify: desktopNotify,
playSound: playSound,
safeApply: safeApply,
connector: connector,
api: api,
webrtc: webrtc,
mediaStream: mediaStream,
appData: appData,
buddyData: buddyData,
@ -130,13 +145,16 @@ localStatus) { @@ -130,13 +145,16 @@ localStatus) {
continueConnector: continueConnector,
chromeExtension: chromeExtension,
userSettingsData: userSettingsData,
localStatus: localStatus
localStatus: localStatus,
rooms: rooms,
restURL: restURL,
roompin: roompin
};
var initialize = function(angModule) {
_.each(services, function(service, name) {
angModule.factory(name, service);
})
});
};
return {

27
static/js/services/webrtc.js

@ -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);
}];
});

12
static/partials/chat.html

@ -5,12 +5,14 @@ @@ -5,12 +5,14 @@
<div class="chatheader"><div class="chatstatusicon" ng-click="activateRoom(currentRoom.id, true)"><i class="fa fa-angle-right"></i> <i class="fa fa fa-comments-o"></i></div><div class="chatheadertitle"><span>{{_("Chat sessions")}}</span></div> <div class="ctrl"><i ng-show="layout.chatMaximized" ng-click="toggleMax()" class="fa fa-compress"></i></div></div>
<div class="chatbody">
<div class="list-group nicescroll">
<a ng-if="roomstatus" ng-click="activateRoom('', true)" class="list-group-item" ng-class="{newmessage: getGroupRoom().pending}">
<span class="badge" ng-show="getGroupRoom().pending">{{getGroupRoom().pending}}</span>
<i class="fa fa-users fa-lg"></i> {{_("Room chat")}} {{roomid}}
</a>
<a ng-repeat="room in getVisibleRooms()" ng-click="activateRoom(room.id, true)" class="list-group-item" ng-class="{newmessage: room.pending, disabled: !room.enabled}">
<span class="badge" ng-show="room.pending">{{room.pending}}</span><i class="fa fa-user fa-lg"></i> {{room.id|displayName}} <button class="btn btn-sm btn-default" ng-click="hideRoom(room.id)"><i class="fa fa-trash-o"></i></button>
<span class="badge" ng-show="room.pending">{{room.pending}}</span>
<i class="fa fa-lg" ng-class="{'fa-user': room.id !== '', 'fa-users': room.id === ''}"></i>
<span ng-if="room.id !== ''">{{room.id|displayName}}</span>
<span ng-if="room.id === ''">{{_("Room chat")}} {{currentRoomName}}</span>
<button ng-if="room.id !== ''" class="btn btn-sm btn-default" ng-click="hideRoom(room.id)">
<i class="fa fa-trash-o"></i>
</button>
</a>
</div>
</div>

8
static/partials/page/welcome.html

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<div class="welcome container-fluid" ng-controller="RoomchangeController">
<div class="welcome container-fluid">
<div class="welcome-logo"></div>
<h1>{{_("Create a room and talk together")}}</h1>
<div class="welcome-container">
@ -6,13 +6,13 @@ @@ -6,13 +6,13 @@
<div class="form-group welcome-input">
<input type="text" class="form-control roomdata-link-input input-lg" ng-model="roomdata.name" placeholder="{{_('Creating room ...')}}">
<div class="welcome-input-buttons">
<a class="fa fa-refresh" ng-click="refreshRoom()"></a>
<button class="btn btn-primary btn-roomcreate" type="button" ng-disabled="!roomdata.link" ng-click="changeRoomToId(roomdata.name)">{{_("Create")}}</button>
<a class="fa fa-refresh" ng-click="randomRoom()"></a>
<button class="btn btn-primary btn-roomcreate" type="button" ng-disabled="!roomdata.link" ng-click="joinRoomByName(roomdata.name)">{{_("Create")}}</button>
</div>
</div>
</p>
<center>
<p ng-show="roomdata.name"><i class="fa fa-external-link"></i> <a href="{{roomdata.link}}" ng-click="changeRoomToId(roomdata.name);$event.preventDefault()">{{roomdata.link}}</a></p>
<p ng-show="roomdata.name"><i class="fa fa-external-link"></i> <a href="{{roomdata.link}}" ng-click="joinRoomByName(roomdata.name);$event.preventDefault()">{{roomdata.link}}</a></p>
</center>
</div>
</div>

4
static/partials/roombar.html

@ -4,12 +4,12 @@ @@ -4,12 +4,12 @@
<label class="pull-left control-label hidden-xs">{{_('Room')}}</label>
<div class="pull-left">
<div class="input-group">
<input class="form-control input-sm" ng-model="newroomid" ng-keyup="hitEnter($event)" type="text" placeholder="{{_('Main')}}"></input><span ng-if="roomid && roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Leave room')}}" ng-click="exit()"><i class="fa fa-eraser"></i></a></span><span ng-if="!roomid || !roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Change room')}}" ng-click="save()"><i class="fa fa-arrow-right"></i></a></span>
<input class="form-control input-sm" ng-model="newRoomName" ng-keyup="hitEnter($event)" type="text" placeholder="{{_('Main')}}"></input><span ng-if="currentRoomName && roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Leave room')}}" ng-click="exit()"><i class="fa fa-eraser"></i></a></span><span ng-if="!currentRoomName || !roombarform.$pristine" class="input-group-btn"><a class="btn btn-default btn-sm" title="{{_('Change room')}}" ng-click="save()"><i class="fa fa-arrow-right"></i></a></span>
</div>
</div>
<div class="pull-left">
<social-share/>
</div>
</form>
<label class="control-label overlaybar-overlay" title="{{_('Current room')}}">{{roomid}}</label>
<label class="control-label overlaybar-overlay" title="{{_('Current room')}}">{{currentRoomName}}</label>
</div>

1
static/partials/usability.html

@ -1,7 +1,6 @@ @@ -1,7 +1,6 @@
<div class="fadetogglecontainer" ng-hide="peer">
<div class="animate-show" ng-hide="layout.settings">
<div class="fadetogglecontainer" ng-switch="usabilityInfo">
<div ng-switch-when="initializing"></div>
<div ng-switch-when="checking"><i class="fa fa-refresh fa-spin fa-4x pull-right"></i>{{_("Checking camera and microphone access.")}}</div>
<div ng-switch-when="usermedia"><i><i class="fa fa-hand-o-up fa-4x pull-right"></i>{{_("Please allow access to your camera and microphone.")}}</i></div>
<div ng-switch-when="denied">{{_("Camera / microphone access required.")}}</i></div>

Loading…
Cancel
Save