Browse Source
In addition to adding unit tests for the "Hello" message, the following notable improvements are included: * Separate websocket callbacks from the hub via a handler API and adaptor. * Move all application specific state to the session. * Session no longer refers to the hub. * Remove redundant MessageRequest struct. * Hub is no longer responsible for buffer pool or buddy image management. * Consolidated connection table locking in the hub. * Remove redundant session table from the hub. * Split room join and leave into separate handlers. This also removes the RoomConnectionUpdate struct. * Entirely remove room management from the hub. This also provides room operations with a separate mutex. * Split stats into a separate service. * Simplify the session token handler. * Buddy image HTTP handler no longer takes the entire hub. * Centralize JSON encoding and decoding. This removes JSON encoding from the room worker queue. * Improve unicast message statistics. * Numerous other renamings and cleanup items.pull/112/head
19 changed files with 1465 additions and 979 deletions
@ -0,0 +1,268 @@ |
|||||||
|
/* |
||||||
|
* 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")) |
||||||
|
} |
||||||
|
|
||||||
|
if api.CanJoinRoom(msg.Hello.Id) { |
||||||
|
session.Hello = true |
||||||
|
session.Roomid = msg.Hello.Id |
||||||
|
api.JoinRoom(session, c) |
||||||
|
api.Broadcast(session, session.DataSessionJoined()) |
||||||
|
} else { |
||||||
|
session.Hello = false |
||||||
|
} |
||||||
|
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}) |
||||||
|
} |
||||||
|
default: |
||||||
|
log.Println("OnText unhandled message type", msg.Type) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) OnDisconnect(session *Session) { |
||||||
|
dsl := session.DataSessionLeft("hard") |
||||||
|
if session.Hello { |
||||||
|
api.LeaveRoom(session) |
||||||
|
api.Broadcast(session, dsl) |
||||||
|
} |
||||||
|
|
||||||
|
session.RunForAllSubscribers(func(session *Session) { |
||||||
|
log.Println("Notifying subscriber that we are gone", session.Id, session.Id) |
||||||
|
api.Unicast(session, session.Id, dsl) |
||||||
|
}) |
||||||
|
|
||||||
|
api.Unicaster.OnDisconnect(session) |
||||||
|
|
||||||
|
api.buddyImages.Delete(session.Id) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) SendSelf(c Responder, session *Session) { |
||||||
|
token, err := api.EncodeSessionToken(session) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error in OnRegister", err) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
log.Println("Created new session token", len(token), token) |
||||||
|
self := &DataSelf{ |
||||||
|
Type: "Self", |
||||||
|
Id: session.Id, |
||||||
|
Sid: session.Sid, |
||||||
|
Userid: session.Userid(), |
||||||
|
Suserid: api.EncodeSessionUserID(session), |
||||||
|
Token: token, |
||||||
|
Version: api.version, |
||||||
|
Turn: api.CreateTurnData(session), |
||||||
|
Stun: api.StunURIs, |
||||||
|
} |
||||||
|
c.Reply("", self) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) UpdateSession(session *Session, s *SessionUpdate) uint64 { |
||||||
|
if s.Status != nil { |
||||||
|
status, ok := s.Status.(map[string]interface{}) |
||||||
|
if ok && status["buddyPicture"] != nil { |
||||||
|
pic := status["buddyPicture"].(string) |
||||||
|
if strings.HasPrefix(pic, "data:") { |
||||||
|
imageId := api.buddyImages.Update(session.Id, pic[5:]) |
||||||
|
if imageId != "" { |
||||||
|
status["buddyPicture"] = "img:" + imageId |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return session.Update(s) |
||||||
|
} |
||||||
|
|
||||||
|
func (api *channellingAPI) BroadcastSessionStatus(session *Session) { |
||||||
|
if session.Hello { |
||||||
|
api.Broadcast(session, session.DataSessionStatus()) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,134 @@ |
|||||||
|
/* |
||||||
|
* 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 ( |
||||||
|
testAppVersion string = "0.0.0+unittests" |
||||||
|
) |
||||||
|
|
||||||
|
type fakeClient struct { |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeClient) Send(_ Buffer) { |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeClient) Reply(_ string, _ interface{}) { |
||||||
|
} |
||||||
|
|
||||||
|
type fakeRoomManager struct { |
||||||
|
disallowJoin bool |
||||||
|
joinedRoomID string |
||||||
|
leftRoomID string |
||||||
|
roomUsers []*DataSession |
||||||
|
joinedID string |
||||||
|
leftID string |
||||||
|
broadcasts []interface{} |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) CanJoinRoom(roomID string) bool { |
||||||
|
return !fake.disallowJoin |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) RoomUsers(session *Session) []*DataSession { |
||||||
|
return fake.roomUsers |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) JoinRoom(session *Session, _ Sender) { |
||||||
|
fake.joinedID = session.Roomid |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) LeaveRoom(session *Session) { |
||||||
|
fake.leftID = session.Roomid |
||||||
|
} |
||||||
|
|
||||||
|
func (fake *fakeRoomManager) Broadcast(_ *Session, msg interface{}) { |
||||||
|
fake.broadcasts = append(fake.broadcasts, msg) |
||||||
|
} |
||||||
|
|
||||||
|
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.disallowJoin = true |
||||||
|
|
||||||
|
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) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,87 @@ |
|||||||
|
/* |
||||||
|
* Spreed WebRTC. |
||||||
|
* Copyright (C) 2013-2014 struktur AG |
||||||
|
* |
||||||
|
* This file is part of Spreed WebRTC. |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as published by |
||||||
|
* the Free Software Foundation, either version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"log" |
||||||
|
) |
||||||
|
|
||||||
|
type Sender interface { |
||||||
|
Send(Buffer) |
||||||
|
} |
||||||
|
|
||||||
|
type ResponseSender interface { |
||||||
|
Sender |
||||||
|
Responder |
||||||
|
} |
||||||
|
|
||||||
|
type Responder interface { |
||||||
|
Reply(iid string, m interface{}) |
||||||
|
} |
||||||
|
|
||||||
|
type Client interface { |
||||||
|
ResponseSender |
||||||
|
Session() *Session |
||||||
|
Index() uint64 |
||||||
|
Close(bool) |
||||||
|
} |
||||||
|
|
||||||
|
type client struct { |
||||||
|
Codec |
||||||
|
ChannellingAPI |
||||||
|
Connection |
||||||
|
session *Session |
||||||
|
} |
||||||
|
|
||||||
|
func NewClient(codec Codec, api ChannellingAPI, session *Session) *client { |
||||||
|
return &client{codec, api, nil, session} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnConnect(conn Connection) { |
||||||
|
client.Connection = conn |
||||||
|
client.ChannellingAPI.OnConnect(client, client.session) |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnText(b Buffer) { |
||||||
|
if incoming, err := client.DecodeIncoming(b); err == nil { |
||||||
|
client.OnIncoming(client, client.session, incoming) |
||||||
|
} else { |
||||||
|
log.Println("OnText error while decoding JSON", err) |
||||||
|
log.Printf("JSON:\n%s\n", b) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) OnDisconnect() { |
||||||
|
client.ChannellingAPI.OnDisconnect(client.session) |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) Reply(iid string, m interface{}) { |
||||||
|
outgoing := &DataOutgoing{From: client.session.Id, Iid: iid, Data: m} |
||||||
|
if b, err := client.EncodeOutgoing(outgoing); err == nil { |
||||||
|
client.Send(b) |
||||||
|
b.Decref() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (client *client) Session() *Session { |
||||||
|
return client.session |
||||||
|
} |
@ -0,0 +1,69 @@ |
|||||||
|
/* |
||||||
|
* Spreed WebRTC. |
||||||
|
* Copyright (C) 2013-2014 struktur AG |
||||||
|
* |
||||||
|
* This file is part of Spreed WebRTC. |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as published by |
||||||
|
* the Free Software Foundation, either version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"encoding/json" |
||||||
|
"log" |
||||||
|
) |
||||||
|
|
||||||
|
type IncomingDecoder interface { |
||||||
|
DecodeIncoming(Buffer) (*DataIncoming, error) |
||||||
|
} |
||||||
|
|
||||||
|
type OutgoingEncoder interface { |
||||||
|
EncodeOutgoing(*DataOutgoing) (Buffer, error) |
||||||
|
} |
||||||
|
|
||||||
|
type Codec interface { |
||||||
|
NewBuffer() Buffer |
||||||
|
IncomingDecoder |
||||||
|
OutgoingEncoder |
||||||
|
} |
||||||
|
|
||||||
|
type incomingCodec struct { |
||||||
|
buffers BufferCache |
||||||
|
} |
||||||
|
|
||||||
|
func NewCodec() Codec { |
||||||
|
return &incomingCodec{NewBufferCache(1024, bytes.MinRead)} |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) NewBuffer() Buffer { |
||||||
|
return codec.buffers.New() |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) DecodeIncoming(b Buffer) (*DataIncoming, error) { |
||||||
|
incoming := &DataIncoming{} |
||||||
|
return incoming, json.Unmarshal(b.Bytes(), incoming) |
||||||
|
} |
||||||
|
|
||||||
|
func (codec incomingCodec) EncodeOutgoing(outgoing *DataOutgoing) (Buffer, error) { |
||||||
|
b := codec.NewBuffer() |
||||||
|
if err := json.NewEncoder(b).Encode(outgoing); err != nil { |
||||||
|
log.Println("Error while encoding JSON", err) |
||||||
|
b.Decref() |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return b, nil |
||||||
|
} |
@ -0,0 +1,170 @@ |
|||||||
|
/* |
||||||
|
* 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 { |
||||||
|
CanJoinRoom(id string) bool |
||||||
|
RoomUsers(*Session) []*DataSession |
||||||
|
JoinRoom(*Session, Sender) |
||||||
|
LeaveRoom(*Session) |
||||||
|
} |
||||||
|
|
||||||
|
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) CanJoinRoom(id string) bool { |
||||||
|
return id != "" || rooms.defaultRoomEnabled |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) RoomUsers(session *Session) []*DataSession { |
||||||
|
return <-rooms.getRoomWorker(session.Roomid).GetUsers() |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) JoinRoom(session *Session, sender Sender) { |
||||||
|
rooms.getRoomWorker(session.Roomid).Join(session, sender) |
||||||
|
} |
||||||
|
|
||||||
|
func (rooms *roomManager) LeaveRoom(session *Session) { |
||||||
|
rooms.getRoomWorker(session.Roomid).Leave(session) |
||||||
|
} |
||||||
|
|
||||||
|
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 { |
||||||
|
room := rooms.getRoomWorker(id) |
||||||
|
room.Broadcast(session, message) |
||||||
|
} |
||||||
|
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) getRoomWorker(id string) RoomWorker { |
||||||
|
|
||||||
|
rooms.RLock() |
||||||
|
room, ok := rooms.roomTable[id] |
||||||
|
if !ok { |
||||||
|
rooms.RUnlock() |
||||||
|
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) |
||||||
|
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() |
||||||
|
} |
||||||
|
} else { |
||||||
|
rooms.RUnlock() |
||||||
|
} |
||||||
|
|
||||||
|
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) |
||||||
|
} |
@ -1,280 +0,0 @@ |
|||||||
/* |
|
||||||
* Spreed WebRTC. |
|
||||||
* Copyright (C) 2013-2014 struktur AG |
|
||||||
* |
|
||||||
* This file is part of Spreed WebRTC. |
|
||||||
* |
|
||||||
* This program is free software: you can redistribute it and/or modify |
|
||||||
* it under the terms of the GNU Affero General Public License as published by |
|
||||||
* the Free Software Foundation, either version 3 of the License, or |
|
||||||
* (at your option) any later version. |
|
||||||
* |
|
||||||
* This program is distributed in the hope that it will be useful, |
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||||
* GNU Affero General Public License for more details. |
|
||||||
* |
|
||||||
* You should have received a copy of the GNU Affero General Public License |
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
* |
|
||||||
*/ |
|
||||||
|
|
||||||
package main |
|
||||||
|
|
||||||
import ( |
|
||||||
"encoding/json" |
|
||||||
"log" |
|
||||||
"sync/atomic" |
|
||||||
"time" |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
maxConferenceSize = 100 |
|
||||||
) |
|
||||||
|
|
||||||
type Server struct { |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnRegister(c *Connection) { |
|
||||||
//log.Println("OnRegister", c.id)
|
|
||||||
if token, err := c.h.EncodeSessionToken(c.Session.Token()); err == nil { |
|
||||||
log.Println("Created new session token", len(token), token) |
|
||||||
// Send stuff back.
|
|
||||||
s.Unicast(c, c.Id, &DataSelf{ |
|
||||||
Type: "Self", |
|
||||||
Id: c.Id, |
|
||||||
Sid: c.Session.Sid, |
|
||||||
Userid: c.Session.Userid(), |
|
||||||
Suserid: c.h.CreateSuserid(c.Session), |
|
||||||
Token: token, |
|
||||||
Version: c.h.version, |
|
||||||
Turn: c.h.CreateTurnData(c.Id), |
|
||||||
Stun: c.h.config.StunURIs, |
|
||||||
}) |
|
||||||
} else { |
|
||||||
log.Println("Error in OnRegister", c.Idx, err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnUnregister(c *Connection) { |
|
||||||
//log.Println("OnUnregister", c.id)
|
|
||||||
dsl := c.Session.DataSessionLeft("hard") |
|
||||||
if c.Hello { |
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) |
|
||||||
s.Broadcast(c, dsl) |
|
||||||
} |
|
||||||
c.Session.RunForAllSubscribers(func(session *Session) { |
|
||||||
log.Println("Notifying subscriber that we are gone", c.Id, session.Id) |
|
||||||
s.Unicast(c, session.Id, dsl) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) OnText(c *Connection, b Buffer) { |
|
||||||
|
|
||||||
//log.Printf("OnText from %d: %s\n", c.Id, b)
|
|
||||||
var msg DataIncoming |
|
||||||
err := json.Unmarshal(b.Bytes(), &msg) |
|
||||||
if err != nil { |
|
||||||
log.Println("OnText error while decoding JSON", err) |
|
||||||
log.Printf("JSON:\n%s\n", b) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
switch msg.Type { |
|
||||||
case "Self": |
|
||||||
s.OnRegister(c) |
|
||||||
case "Hello": |
|
||||||
//log.Println("Hello", msg.Hello, c.Idx)
|
|
||||||
// TODO(longsleep): Filter room id and user agent.
|
|
||||||
s.UpdateSession(c, &SessionUpdate{Types: []string{"Roomid", "Ua"}, Roomid: msg.Hello.Id, Ua: msg.Hello.Ua}) |
|
||||||
if c.Hello && c.Roomid != msg.Hello.Id { |
|
||||||
// Room changed.
|
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid}) |
|
||||||
s.Broadcast(c, c.Session.DataSessionLeft("soft")) |
|
||||||
} |
|
||||||
c.Roomid = msg.Hello.Id |
|
||||||
if c.h.config.DefaultRoomEnabled || !c.h.isDefaultRoomid(c.Roomid) { |
|
||||||
c.Hello = true |
|
||||||
s.UpdateRoomConnection(c, &RoomConnectionUpdate{Id: c.Roomid, Status: true}) |
|
||||||
s.Broadcast(c, c.Session.DataSessionJoined()) |
|
||||||
} else { |
|
||||||
c.Hello = false |
|
||||||
} |
|
||||||
case "Offer": |
|
||||||
// TODO(longsleep): Validate offer
|
|
||||||
s.Unicast(c, msg.Offer.To, msg.Offer) |
|
||||||
case "Candidate": |
|
||||||
// TODO(longsleep): Validate candidate
|
|
||||||
s.Unicast(c, msg.Candidate.To, msg.Candidate) |
|
||||||
case "Answer": |
|
||||||
// TODO(longsleep): Validate Answer
|
|
||||||
s.Unicast(c, msg.Answer.To, msg.Answer) |
|
||||||
case "Users": |
|
||||||
if c.Hello { |
|
||||||
s.Users(c) |
|
||||||
} |
|
||||||
case "Authentication": |
|
||||||
if msg.Authentication.Authentication != nil && s.Authenticate(c, msg.Authentication.Authentication) { |
|
||||||
s.OnRegister(c) |
|
||||||
if c.Hello { |
|
||||||
s.Broadcast(c, c.Session.DataSessionStatus()) |
|
||||||
} |
|
||||||
} |
|
||||||
case "Bye": |
|
||||||
s.Unicast(c, msg.Bye.To, msg.Bye) |
|
||||||
case "Status": |
|
||||||
//log.Println("Status", msg.Status)
|
|
||||||
s.UpdateSession(c, &SessionUpdate{Types: []string{"Status"}, Status: msg.Status.Status}) |
|
||||||
if c.Hello { |
|
||||||
s.Broadcast(c, c.Session.DataSessionStatus()) |
|
||||||
} |
|
||||||
case "Chat": |
|
||||||
// TODO(longsleep): Limit sent chat messages per incoming connection.
|
|
||||||
if !msg.Chat.Chat.NoEcho { |
|
||||||
s.Unicast(c, c.Id, msg.Chat) |
|
||||||
} |
|
||||||
msg.Chat.Chat.Time = time.Now().Format(time.RFC3339) |
|
||||||
if msg.Chat.To == "" { |
|
||||||
// TODO(longsleep): Check if chat broadcast is allowed.
|
|
||||||
if c.Hello { |
|
||||||
atomic.AddUint64(&c.h.broadcastChatMessages, 1) |
|
||||||
s.Broadcast(c, msg.Chat) |
|
||||||
} |
|
||||||
} else { |
|
||||||
if msg.Chat.Chat.Status != nil && msg.Chat.Chat.Status.ContactRequest != nil { |
|
||||||
err = s.ContactRequest(c, msg.Chat.To, msg.Chat.Chat.Status.ContactRequest) |
|
||||||
if err != nil { |
|
||||||
log.Println("Ignoring invalid contact request.", err) |
|
||||||
return |
|
||||||
} |
|
||||||
msg.Chat.Chat.Status.ContactRequest.Userid = c.Session.Userid() |
|
||||||
} |
|
||||||
atomic.AddUint64(&c.h.unicastChatMessages, 1) |
|
||||||
s.Unicast(c, msg.Chat.To, msg.Chat) |
|
||||||
if msg.Chat.Chat.Mid != "" { |
|
||||||
// Send out delivery confirmation status chat message.
|
|
||||||
s.Unicast(c, c.Id, &DataChat{To: msg.Chat.To, Type: "Chat", Chat: &DataChatMessage{Mid: msg.Chat.Chat.Mid, Status: &DataChatStatus{State: "sent"}}}) |
|
||||||
} |
|
||||||
} |
|
||||||
case "Conference": |
|
||||||
// Check conference maximum size.
|
|
||||||
if len(msg.Conference.Conference) > maxConferenceSize { |
|
||||||
log.Println("Refusing to create conference above limit.", len(msg.Conference.Conference)) |
|
||||||
} else { |
|
||||||
// Send conference update to anyone.
|
|
||||||
for _, id := range msg.Conference.Conference { |
|
||||||
if id != c.Id { |
|
||||||
//log.Println("participant", id)
|
|
||||||
s.Unicast(c, id, msg.Conference) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
case "Alive": |
|
||||||
s.Alive(c, msg.Alive, msg.Iid) |
|
||||||
case "Sessions": |
|
||||||
s.Sessions(c, msg.Sessions.Sessions, msg.Iid) |
|
||||||
default: |
|
||||||
log.Println("OnText unhandled message type", msg.Type) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Unicast(c *Connection, to string, m interface{}) { |
|
||||||
|
|
||||||
outgoing := &DataOutgoing{From: c.Id, To: to, Data: m} |
|
||||||
if !c.isClosing && c.Id != to { |
|
||||||
outgoing.A = c.Session.Attestation() |
|
||||||
} |
|
||||||
b := c.h.buffers.New() |
|
||||||
encoder := json.NewEncoder(b) |
|
||||||
err := encoder.Encode(outgoing) |
|
||||||
if err != nil { |
|
||||||
b.Decref() |
|
||||||
log.Println("Unicast error while encoding JSON", err) |
|
||||||
return |
|
||||||
} |
|
||||||
//log.Println("Unicast", b)
|
|
||||||
|
|
||||||
var msg = &MessageRequest{From: c.Id, To: to, Message: b} |
|
||||||
c.h.unicastHandler(msg) |
|
||||||
b.Decref() |
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Broadcast(c *Connection, m interface{}) { |
|
||||||
|
|
||||||
b := c.h.buffers.New() |
|
||||||
encoder := json.NewEncoder(b) |
|
||||||
err := encoder.Encode(&DataOutgoing{From: c.Id, Data: m, A: c.Session.Attestation()}) |
|
||||||
if err != nil { |
|
||||||
b.Decref() |
|
||||||
log.Println("Broadcast error while encoding JSON", err) |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if c.h.isGlobalRoomid(c.Roomid) { |
|
||||||
c.h.RunForAllRooms(func(room *RoomWorker) { |
|
||||||
var msg = &MessageRequest{From: c.Id, Message: b, Id: room.Id} |
|
||||||
room.broadcastHandler(msg) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
var msg = &MessageRequest{From: c.Id, Message: b, Id: c.Roomid} |
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.broadcastHandler(msg) |
|
||||||
} |
|
||||||
b.Decref() |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Alive(c *Connection, alive *DataAlive, iid string) { |
|
||||||
|
|
||||||
c.h.aliveHandler(c, alive, iid) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Sessions(c *Connection, srq *DataSessionsRequest, iid string) { |
|
||||||
|
|
||||||
c.h.sessionsHandler(c, srq, iid) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) UpdateSession(c *Connection, su *SessionUpdate) uint64 { |
|
||||||
|
|
||||||
su.Id = c.Id |
|
||||||
return c.h.sessionupdateHandler(su) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) ContactRequest(c *Connection, to string, cr *DataContactRequest) (err error) { |
|
||||||
|
|
||||||
return c.h.contactrequestHandler(c, to, cr) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Users(c *Connection) { |
|
||||||
|
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.usersHandler(c) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) Authenticate(c *Connection, st *SessionToken) bool { |
|
||||||
|
|
||||||
err := c.h.authenticateHandler(c.Session, st, "") |
|
||||||
if err == nil { |
|
||||||
log.Println("Authentication success", c.Id, c.Idx, c.Session.Userid) |
|
||||||
return true |
|
||||||
} else { |
|
||||||
log.Println("Authentication failed", err, c.Id, c.Idx, st.Userid, st.Nonce) |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
func (s *Server) UpdateRoomConnection(c *Connection, rcu *RoomConnectionUpdate) { |
|
||||||
|
|
||||||
rcu.Sessionid = c.Id |
|
||||||
rcu.Connection = c |
|
||||||
room := c.h.GetRoom(c.Roomid) |
|
||||||
room.connectionHandler(rcu) |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,173 @@ |
|||||||
|
/* |
||||||
|
* Spreed WebRTC. |
||||||
|
* Copyright (C) 2013-2014 struktur AG |
||||||
|
* |
||||||
|
* This file is part of Spreed WebRTC. |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as published by |
||||||
|
* the Free Software Foundation, either version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/sha256" |
||||||
|
"net/http" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" |
||||||
|
) |
||||||
|
|
||||||
|
type UserStats interface { |
||||||
|
UserInfo(bool) (int, map[string]*DataUser) |
||||||
|
} |
||||||
|
|
||||||
|
type SessionManager interface { |
||||||
|
UserStats |
||||||
|
RetrieveUsersWith(func(*http.Request) (string, error)) |
||||||
|
CreateSession(*http.Request) *Session |
||||||
|
DestroySession(*Session) |
||||||
|
Authenticate(*Session, *SessionToken, string) error |
||||||
|
GetUserSessions(session *Session, id string) []*DataSession |
||||||
|
} |
||||||
|
|
||||||
|
type sessionManager struct { |
||||||
|
Tickets |
||||||
|
sync.RWMutex |
||||||
|
config *Config |
||||||
|
userTable map[string]*User |
||||||
|
fakesessionTable map[string]*Session |
||||||
|
useridRetriever func(*http.Request) (string, error) |
||||||
|
attestations *securecookie.SecureCookie |
||||||
|
} |
||||||
|
|
||||||
|
func NewSessionManager(config *Config, tickets Tickets, sessionSecret []byte) SessionManager { |
||||||
|
sessionManager := &sessionManager{ |
||||||
|
tickets, |
||||||
|
sync.RWMutex{}, |
||||||
|
config, |
||||||
|
make(map[string]*User), |
||||||
|
make(map[string]*Session), |
||||||
|
nil, |
||||||
|
nil, |
||||||
|
} |
||||||
|
|
||||||
|
sessionManager.attestations = securecookie.New(sessionSecret, nil) |
||||||
|
sessionManager.attestations.MaxAge(300) // 5 minutes
|
||||||
|
sessionManager.attestations.HashFunc(sha256.New) |
||||||
|
|
||||||
|
return sessionManager |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) UserInfo(details bool) (userCount int, users map[string]*DataUser) { |
||||||
|
sessionManager.RLock() |
||||||
|
defer sessionManager.RUnlock() |
||||||
|
|
||||||
|
userCount = len(sessionManager.userTable) |
||||||
|
if details { |
||||||
|
users := make(map[string]*DataUser) |
||||||
|
for userid, user := range sessionManager.userTable { |
||||||
|
users[userid] = user.Data() |
||||||
|
} |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) RetrieveUsersWith(retriever func(*http.Request) (string, error)) { |
||||||
|
sessionManager.useridRetriever = retriever |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) CreateSession(request *http.Request) *Session { |
||||||
|
request.ParseForm() |
||||||
|
token := request.FormValue("t") |
||||||
|
st := sessionManager.DecodeSessionToken(token) |
||||||
|
|
||||||
|
var userid string |
||||||
|
if sessionManager.config.UsersEnabled { |
||||||
|
if sessionManager.useridRetriever != nil { |
||||||
|
userid, _ = sessionManager.useridRetriever(request) |
||||||
|
if userid == "" { |
||||||
|
userid = st.Userid |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
session := NewSession(sessionManager.attestations, st.Id, st.Sid) |
||||||
|
|
||||||
|
if userid != "" { |
||||||
|
// XXX(lcooper): Should errors be handled here?
|
||||||
|
sessionManager.Authenticate(session, st, userid) |
||||||
|
} |
||||||
|
|
||||||
|
return session |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) DestroySession(session *Session) { |
||||||
|
session.Close() |
||||||
|
|
||||||
|
sessionManager.Lock() |
||||||
|
if suserid := session.Userid(); suserid != "" { |
||||||
|
user, ok := sessionManager.userTable[suserid] |
||||||
|
if ok && user.RemoveSession(session) { |
||||||
|
delete(sessionManager.userTable, suserid) |
||||||
|
} |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) Authenticate(session *Session, st *SessionToken, userid string) error { |
||||||
|
if err := session.Authenticate(sessionManager.Realm(), st, userid); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
// Authentication success.
|
||||||
|
suserid := session.Userid() |
||||||
|
sessionManager.Lock() |
||||||
|
user, ok := sessionManager.userTable[suserid] |
||||||
|
if !ok { |
||||||
|
user = NewUser(suserid) |
||||||
|
sessionManager.userTable[suserid] = user |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
user.AddSession(session) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (sessionManager *sessionManager) GetUserSessions(session *Session, userid string) (users []*DataSession) { |
||||||
|
var ( |
||||||
|
user *User |
||||||
|
ok bool |
||||||
|
) |
||||||
|
sessionManager.RLock() |
||||||
|
user, ok = sessionManager.userTable[userid] |
||||||
|
sessionManager.RUnlock() |
||||||
|
if !ok { |
||||||
|
// No user. Create fake session.
|
||||||
|
sessionManager.Lock() |
||||||
|
session, ok := sessionManager.fakesessionTable[userid] |
||||||
|
if !ok { |
||||||
|
st := sessionManager.FakeSessionToken(userid) |
||||||
|
session = NewSession(sessionManager.attestations, st.Id, st.Sid) |
||||||
|
session.SetUseridFake(st.Userid) |
||||||
|
sessionManager.fakesessionTable[userid] = session |
||||||
|
} |
||||||
|
sessionManager.Unlock() |
||||||
|
users = make([]*DataSession, 1, 1) |
||||||
|
users[0] = session.Data() |
||||||
|
} else { |
||||||
|
// Add sessions for foreign user.
|
||||||
|
users = user.SubscribeSessions(session) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
@ -0,0 +1,104 @@ |
|||||||
|
/* |
||||||
|
* Spreed WebRTC. |
||||||
|
* Copyright (C) 2013-2014 struktur AG |
||||||
|
* |
||||||
|
* This file is part of Spreed WebRTC. |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as published by |
||||||
|
* the Free Software Foundation, either version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync/atomic" |
||||||
|
) |
||||||
|
|
||||||
|
type HubStat struct { |
||||||
|
Rooms int `json:"rooms"` |
||||||
|
Connections int `json:"connections"` |
||||||
|
Sessions int `json:"sessions"` |
||||||
|
Users int `json:"users"` |
||||||
|
Count uint64 `json:"count"` |
||||||
|
BroadcastChatMessages uint64 `json:"broadcastchatmessages"` |
||||||
|
UnicastChatMessages uint64 `json:"unicastchatmessages"` |
||||||
|
IdsInRoom map[string][]string `json:"idsinroom,omitempty"` |
||||||
|
SessionsById map[string]*DataSession `json:"sessionsbyid,omitempty"` |
||||||
|
UsersById map[string]*DataUser `json:"usersbyid,omitempty"` |
||||||
|
ConnectionsByIdx map[string]string `json:"connectionsbyidx,omitempty"` |
||||||
|
} |
||||||
|
|
||||||
|
type ConnectionCounter interface { |
||||||
|
CountConnection() uint64 |
||||||
|
} |
||||||
|
|
||||||
|
type StatsCounter interface { |
||||||
|
CountBroadcastChat() |
||||||
|
CountUnicastChat() |
||||||
|
} |
||||||
|
|
||||||
|
type StatsGenerator interface { |
||||||
|
Stat(details bool) *HubStat |
||||||
|
} |
||||||
|
|
||||||
|
type StatsManager interface { |
||||||
|
ConnectionCounter |
||||||
|
StatsCounter |
||||||
|
StatsGenerator |
||||||
|
} |
||||||
|
|
||||||
|
type statsManager struct { |
||||||
|
ClientStats |
||||||
|
RoomStats |
||||||
|
UserStats |
||||||
|
connectionCount uint64 |
||||||
|
broadcastChatMessages uint64 |
||||||
|
unicastChatMessages uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func NewStatsManager(clientStats ClientStats, roomStats RoomStats, userStats UserStats) StatsManager { |
||||||
|
return &statsManager{clientStats, roomStats, userStats, 0, 0, 0} |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountConnection() uint64 { |
||||||
|
return atomic.AddUint64(&stats.connectionCount, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountBroadcastChat() { |
||||||
|
atomic.AddUint64(&stats.broadcastChatMessages, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) CountUnicastChat() { |
||||||
|
atomic.AddUint64(&stats.unicastChatMessages, 1) |
||||||
|
} |
||||||
|
|
||||||
|
func (stats *statsManager) Stat(details bool) *HubStat { |
||||||
|
roomCount, roomSessionInfo := stats.RoomInfo(details) |
||||||
|
clientCount, sessions, connections := stats.ClientInfo(details) |
||||||
|
userCount, users := stats.UserInfo(details) |
||||||
|
|
||||||
|
return &HubStat{ |
||||||
|
Rooms: roomCount, |
||||||
|
Connections: clientCount, |
||||||
|
Sessions: clientCount, |
||||||
|
Users: userCount, |
||||||
|
Count: atomic.LoadUint64(&stats.connectionCount), |
||||||
|
BroadcastChatMessages: atomic.LoadUint64(&stats.broadcastChatMessages), |
||||||
|
UnicastChatMessages: atomic.LoadUint64(&stats.unicastChatMessages), |
||||||
|
IdsInRoom: roomSessionInfo, |
||||||
|
SessionsById: sessions, |
||||||
|
UsersById: users, |
||||||
|
ConnectionsByIdx: connections, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,130 @@ |
|||||||
|
/* |
||||||
|
* Spreed WebRTC. |
||||||
|
* Copyright (C) 2013-2014 struktur AG |
||||||
|
* |
||||||
|
* This file is part of Spreed WebRTC. |
||||||
|
* |
||||||
|
* This program is free software: you can redistribute it and/or modify |
||||||
|
* it under the terms of the GNU Affero General Public License as published by |
||||||
|
* the Free Software Foundation, either version 3 of the License, or |
||||||
|
* (at your option) any later version. |
||||||
|
* |
||||||
|
* This program is distributed in the hope that it will be useful, |
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||||
|
* GNU Affero General Public License for more details. |
||||||
|
* |
||||||
|
* You should have received a copy of the GNU Affero General Public License |
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
* |
||||||
|
*/ |
||||||
|
|
||||||
|
package main |
||||||
|
|
||||||
|
import ( |
||||||
|
"crypto/aes" |
||||||
|
"crypto/hmac" |
||||||
|
"crypto/sha256" |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"log" |
||||||
|
|
||||||
|
"github.com/gorilla/securecookie" |
||||||
|
) |
||||||
|
|
||||||
|
type SessionValidator interface { |
||||||
|
Realm() string |
||||||
|
ValidateSession(string, string) bool |
||||||
|
} |
||||||
|
|
||||||
|
type SessionEncoder interface { |
||||||
|
EncodeSessionToken(*Session) (string, error) |
||||||
|
EncodeSessionUserID(*Session) string |
||||||
|
} |
||||||
|
|
||||||
|
type Tickets interface { |
||||||
|
SessionValidator |
||||||
|
SessionEncoder |
||||||
|
DecodeSessionToken(token string) (st *SessionToken) |
||||||
|
FakeSessionToken(userid string) *SessionToken |
||||||
|
} |
||||||
|
|
||||||
|
type tickets struct { |
||||||
|
*securecookie.SecureCookie |
||||||
|
realm string |
||||||
|
tokenName string |
||||||
|
encryptionSecret []byte |
||||||
|
} |
||||||
|
|
||||||
|
func NewTickets(sessionSecret, encryptionSecret []byte, realm string) Tickets { |
||||||
|
tickets := &tickets{ |
||||||
|
nil, |
||||||
|
realm, |
||||||
|
fmt.Sprintf("token@%s", realm), |
||||||
|
encryptionSecret, |
||||||
|
} |
||||||
|
tickets.SecureCookie = securecookie.New(sessionSecret, encryptionSecret) |
||||||
|
tickets.MaxAge(86400 * 30) // 30 days
|
||||||
|
tickets.HashFunc(sha256.New) |
||||||
|
tickets.BlockFunc(aes.NewCipher) |
||||||
|
|
||||||
|
return tickets |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) Realm() string { |
||||||
|
return tickets.realm |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) DecodeSessionToken(token string) (st *SessionToken) { |
||||||
|
var err error |
||||||
|
if token != "" { |
||||||
|
st = &SessionToken{} |
||||||
|
err = tickets.Decode(tickets.tokenName, token, st) |
||||||
|
if err != nil { |
||||||
|
log.Println("Error while decoding session token", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if st == nil || err != nil { |
||||||
|
sid := NewRandomString(32) |
||||||
|
id, _ := tickets.Encode("id", sid) |
||||||
|
st = &SessionToken{Id: id, Sid: sid} |
||||||
|
log.Println("Created new session id", id) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) FakeSessionToken(userid string) *SessionToken { |
||||||
|
st := &SessionToken{} |
||||||
|
st.Sid = fmt.Sprintf("fake-%s", NewRandomString(27)) |
||||||
|
st.Id, _ = tickets.Encode("id", st.Sid) |
||||||
|
st.Userid = userid |
||||||
|
log.Println("Created new fake session id", st.Id) |
||||||
|
return st |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) ValidateSession(id, sid string) bool { |
||||||
|
var decoded string |
||||||
|
if err := tickets.Decode("id", id, &decoded); err != nil { |
||||||
|
log.Println("Session validation error", err, id, sid) |
||||||
|
return false |
||||||
|
} |
||||||
|
if decoded != sid { |
||||||
|
log.Println("Session validation failed", id, sid) |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) EncodeSessionToken(session *Session) (string, error) { |
||||||
|
return tickets.Encode(tickets.tokenName, session.Token()) |
||||||
|
} |
||||||
|
|
||||||
|
func (tickets *tickets) EncodeSessionUserID(session *Session) (suserid string) { |
||||||
|
if userid := session.Userid(); userid != "" { |
||||||
|
m := hmac.New(sha256.New, tickets.encryptionSecret) |
||||||
|
m.Write([]byte(userid)) |
||||||
|
suserid = base64.StdEncoding.EncodeToString(m.Sum(nil)) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
Loading…
Reference in new issue