Add API for transient room data.
This commit is contained in:
parent
99f5fb0be1
commit
fca30af201
|
@ -53,6 +53,8 @@ type ClientMessage struct {
|
||||||
Control *ControlClientMessage `json:"control,omitempty"`
|
Control *ControlClientMessage `json:"control,omitempty"`
|
||||||
|
|
||||||
Internal *InternalClientMessage `json:"internal,omitempty"`
|
Internal *InternalClientMessage `json:"internal,omitempty"`
|
||||||
|
|
||||||
|
TransientData *TransientDataClientMessage `json:"transient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ClientMessage) CheckValid() error {
|
func (m *ClientMessage) CheckValid() error {
|
||||||
|
@ -91,6 +93,12 @@ func (m *ClientMessage) CheckValid() error {
|
||||||
} else if err := m.Internal.CheckValid(); err != nil {
|
} else if err := m.Internal.CheckValid(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "transient":
|
||||||
|
if m.TransientData == nil {
|
||||||
|
return fmt.Errorf("transient missing")
|
||||||
|
} else if err := m.TransientData.CheckValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -138,6 +146,8 @@ type ServerMessage struct {
|
||||||
Control *ControlServerMessage `json:"control,omitempty"`
|
Control *ControlServerMessage `json:"control,omitempty"`
|
||||||
|
|
||||||
Event *EventServerMessage `json:"event,omitempty"`
|
Event *EventServerMessage `json:"event,omitempty"`
|
||||||
|
|
||||||
|
TransientData *TransientDataServerMessage `json:"transient,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
||||||
|
@ -326,6 +336,7 @@ const (
|
||||||
ServerFeatureMcu = "mcu"
|
ServerFeatureMcu = "mcu"
|
||||||
ServerFeatureSimulcast = "simulcast"
|
ServerFeatureSimulcast = "simulcast"
|
||||||
ServerFeatureAudioVideoPermissions = "audio-video-permissions"
|
ServerFeatureAudioVideoPermissions = "audio-video-permissions"
|
||||||
|
ServerFeatureTransientData = "transient-data"
|
||||||
|
|
||||||
// Features for internal clients only.
|
// Features for internal clients only.
|
||||||
ServerFeatureInternalVirtualSessions = "virtual-sessions"
|
ServerFeatureInternalVirtualSessions = "virtual-sessions"
|
||||||
|
@ -334,9 +345,11 @@ const (
|
||||||
var (
|
var (
|
||||||
DefaultFeatures = []string{
|
DefaultFeatures = []string{
|
||||||
ServerFeatureAudioVideoPermissions,
|
ServerFeatureAudioVideoPermissions,
|
||||||
|
ServerFeatureTransientData,
|
||||||
}
|
}
|
||||||
DefaultFeaturesInternal = []string{
|
DefaultFeaturesInternal = []string{
|
||||||
ServerFeatureInternalVirtualSessions,
|
ServerFeatureInternalVirtualSessions,
|
||||||
|
ServerFeatureTransientData,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -636,3 +649,36 @@ type AnswerOfferMessage struct {
|
||||||
RoomType string `json:"roomType"`
|
RoomType string `json:"roomType"`
|
||||||
Payload map[string]interface{} `json:"payload"`
|
Payload map[string]interface{} `json:"payload"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type "transient"
|
||||||
|
|
||||||
|
type TransientDataClientMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
Value *json.RawMessage `json:"value,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *TransientDataClientMessage) CheckValid() error {
|
||||||
|
switch m.Type {
|
||||||
|
case "set":
|
||||||
|
if m.Key == "" {
|
||||||
|
return fmt.Errorf("key missing")
|
||||||
|
}
|
||||||
|
// A "nil" value is allowed and will remove the key.
|
||||||
|
case "remove":
|
||||||
|
if m.Key == "" {
|
||||||
|
return fmt.Errorf("key missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransientDataServerMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
|
OldValue interface{} `json:"oldvalue,omitempty"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
55
hub.go
55
hub.go
|
@ -832,6 +832,8 @@ func (h *Hub) processMessage(client *Client, data []byte) {
|
||||||
h.processControlMsg(client, &message)
|
h.processControlMsg(client, &message)
|
||||||
case "internal":
|
case "internal":
|
||||||
h.processInternalMsg(client, &message)
|
h.processInternalMsg(client, &message)
|
||||||
|
case "transient":
|
||||||
|
h.processTransientMsg(client, &message)
|
||||||
case "bye":
|
case "bye":
|
||||||
h.processByeMsg(client, &message)
|
h.processByeMsg(client, &message)
|
||||||
case "hello":
|
case "hello":
|
||||||
|
@ -1656,6 +1658,59 @@ func (h *Hub) processInternalMsg(client *Client, message *ClientMessage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isAllowedToUpdateTransientData(session Session) bool {
|
||||||
|
if session.ClientType() == HelloClientTypeInternal {
|
||||||
|
// Internal clients are always allowed.
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.HasPermission(PERMISSION_TRANSIENT_DATA) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Hub) processTransientMsg(client *Client, message *ClientMessage) {
|
||||||
|
msg := message.TransientData
|
||||||
|
session := client.GetSession()
|
||||||
|
if session == nil {
|
||||||
|
// Client is not connected yet.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room := session.GetRoom()
|
||||||
|
if room == nil {
|
||||||
|
response := message.NewErrorServerMessage(NewError("not_in_room", "No room joined yet."))
|
||||||
|
session.SendMessage(response)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case "set":
|
||||||
|
if !isAllowedToUpdateTransientData(session) {
|
||||||
|
sendNotAllowed(session, message, "Not allowed to update transient data.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Value == nil {
|
||||||
|
room.SetTransientData(msg.Key, nil)
|
||||||
|
} else {
|
||||||
|
room.SetTransientData(msg.Key, *msg.Value)
|
||||||
|
}
|
||||||
|
case "remove":
|
||||||
|
if !isAllowedToUpdateTransientData(session) {
|
||||||
|
sendNotAllowed(session, message, "Not allowed to update transient data.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
room.RemoveTransientData(msg.Key)
|
||||||
|
default:
|
||||||
|
response := message.NewErrorServerMessage(NewError("ignored", "Unsupported message type."))
|
||||||
|
session.SendMessage(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func sendNotAllowed(session *ClientSession, message *ClientMessage, reason string) {
|
func sendNotAllowed(session *ClientSession, message *ClientMessage, reason string) {
|
||||||
response := message.NewErrorServerMessage(NewError("not_allowed", reason))
|
response := message.NewErrorServerMessage(NewError("not_allowed", reason))
|
||||||
session.SendMessage(response)
|
session.SendMessage(response)
|
||||||
|
|
18
room.go
18
room.go
|
@ -79,6 +79,8 @@ type Room struct {
|
||||||
|
|
||||||
// Timestamps of last NATS backend requests for the different types.
|
// Timestamps of last NATS backend requests for the different types.
|
||||||
lastNatsRoomRequests map[string]int64
|
lastNatsRoomRequests map[string]int64
|
||||||
|
|
||||||
|
transientData *TransientData
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSubjectForRoomId(roomId string, backend *Backend) string {
|
func GetSubjectForRoomId(roomId string, backend *Backend) string {
|
||||||
|
@ -139,6 +141,8 @@ func NewRoom(roomId string, properties *json.RawMessage, hub *Hub, n NatsClient,
|
||||||
backendSubscription: backendSubscription,
|
backendSubscription: backendSubscription,
|
||||||
|
|
||||||
lastNatsRoomRequests: make(map[string]int64),
|
lastNatsRoomRequests: make(map[string]int64),
|
||||||
|
|
||||||
|
transientData: NewTransientData(),
|
||||||
}
|
}
|
||||||
go room.run()
|
go room.run()
|
||||||
|
|
||||||
|
@ -331,6 +335,9 @@ func (r *Room) AddSession(session Session, sessionData *json.RawMessage) []Sessi
|
||||||
r.publishSessionFlagsChanged(session)
|
r.publishSessionFlagsChanged(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if clientSession, ok := session.(*ClientSession); ok {
|
||||||
|
r.transientData.AddListener(clientSession)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -364,6 +371,9 @@ func (r *Room) RemoveSession(session Session) bool {
|
||||||
if virtualSession, ok := session.(*VirtualSession); ok {
|
if virtualSession, ok := session.(*VirtualSession); ok {
|
||||||
delete(r.virtualSessions, virtualSession)
|
delete(r.virtualSessions, virtualSession)
|
||||||
}
|
}
|
||||||
|
if clientSession, ok := session.(*ClientSession); ok {
|
||||||
|
r.transientData.RemoveListener(clientSession)
|
||||||
|
}
|
||||||
delete(r.inCallSessions, session)
|
delete(r.inCallSessions, session)
|
||||||
delete(r.roomSessionData, sid)
|
delete(r.roomSessionData, sid)
|
||||||
if len(r.sessions) > 0 {
|
if len(r.sessions) > 0 {
|
||||||
|
@ -792,3 +802,11 @@ func (r *Room) notifyInternalRoomDeleted() {
|
||||||
s.(*ClientSession).SendMessage(msg)
|
s.(*ClientSession).SendMessage(msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Room) SetTransientData(key string, value interface{}) {
|
||||||
|
r.transientData.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Room) RemoveTransientData(key string) {
|
||||||
|
r.transientData.Remove(key)
|
||||||
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ var (
|
||||||
PERMISSION_MAY_PUBLISH_VIDEO Permission = "publish-video"
|
PERMISSION_MAY_PUBLISH_VIDEO Permission = "publish-video"
|
||||||
PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen"
|
PERMISSION_MAY_PUBLISH_SCREEN Permission = "publish-screen"
|
||||||
PERMISSION_MAY_CONTROL Permission = "control"
|
PERMISSION_MAY_CONTROL Permission = "control"
|
||||||
|
PERMISSION_TRANSIENT_DATA Permission = "transient-data"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionIdData struct {
|
type SessionIdData struct {
|
||||||
|
|
|
@ -30,6 +30,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -112,6 +113,10 @@ func checkMessageType(message *ServerMessage, expectedType string) error {
|
||||||
if message.Event == nil {
|
if message.Event == nil {
|
||||||
return fmt.Errorf("Expected \"%s\" message, got %+v (%s)", expectedType, message, toJsonString(message))
|
return fmt.Errorf("Expected \"%s\" message, got %+v (%s)", expectedType, message, toJsonString(message))
|
||||||
}
|
}
|
||||||
|
case "transient":
|
||||||
|
if message.TransientData == nil {
|
||||||
|
return fmt.Errorf("Expected \"%s\" message, got %+v (%s)", expectedType, message, toJsonString(message))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -407,6 +412,36 @@ func (c *TestClient) SendMessage(recipient MessageClientMessageRecipient, data i
|
||||||
return c.WriteJSON(message)
|
return c.WriteJSON(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TestClient) SetTransientData(key string, value interface{}) error {
|
||||||
|
payload, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
c.t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := &ClientMessage{
|
||||||
|
Id: "efgh",
|
||||||
|
Type: "transient",
|
||||||
|
TransientData: &TransientDataClientMessage{
|
||||||
|
Type: "set",
|
||||||
|
Key: key,
|
||||||
|
Value: (*json.RawMessage)(&payload),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c.WriteJSON(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *TestClient) RemoveTransientData(key string) error {
|
||||||
|
message := &ClientMessage{
|
||||||
|
Id: "ijkl",
|
||||||
|
Type: "transient",
|
||||||
|
TransientData: &TransientDataClientMessage{
|
||||||
|
Type: "remove",
|
||||||
|
Key: key,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c.WriteJSON(message)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TestClient) DrainMessages(ctx context.Context) error {
|
func (c *TestClient) DrainMessages(ctx context.Context) error {
|
||||||
select {
|
select {
|
||||||
case err := <-c.readErrorChan:
|
case err := <-c.readErrorChan:
|
||||||
|
@ -764,3 +799,45 @@ func (c *TestClient) RunUntilAnswer(ctx context.Context, answer string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkMessageTransientSet(message *ServerMessage, key string, value interface{}, oldValue interface{}) error {
|
||||||
|
if err := checkMessageType(message, "transient"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if message.TransientData.Type != "set" {
|
||||||
|
return fmt.Errorf("Expected transient set, got %+v", message.TransientData)
|
||||||
|
} else if message.TransientData.Key != key {
|
||||||
|
return fmt.Errorf("Expected transient set key %s, got %+v", key, message.TransientData)
|
||||||
|
} else if !reflect.DeepEqual(message.TransientData.Value, value) {
|
||||||
|
return fmt.Errorf("Expected transient set value %+v, got %+v", value, message.TransientData.Value)
|
||||||
|
} else if !reflect.DeepEqual(message.TransientData.OldValue, oldValue) {
|
||||||
|
return fmt.Errorf("Expected transient set old value %+v, got %+v", oldValue, message.TransientData.OldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMessageTransientRemove(message *ServerMessage, key string, oldValue interface{}) error {
|
||||||
|
if err := checkMessageType(message, "transient"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if message.TransientData.Type != "remove" {
|
||||||
|
return fmt.Errorf("Expected transient remove, got %+v", message.TransientData)
|
||||||
|
} else if message.TransientData.Key != key {
|
||||||
|
return fmt.Errorf("Expected transient remove key %s, got %+v", key, message.TransientData)
|
||||||
|
} else if !reflect.DeepEqual(message.TransientData.OldValue, oldValue) {
|
||||||
|
return fmt.Errorf("Expected transient remove old value %+v, got %+v", oldValue, message.TransientData.OldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMessageTransientInitial(message *ServerMessage, data map[string]interface{}) error {
|
||||||
|
if err := checkMessageType(message, "transient"); err != nil {
|
||||||
|
return err
|
||||||
|
} else if message.TransientData.Type != "initial" {
|
||||||
|
return fmt.Errorf("Expected transient initial, got %+v", message.TransientData)
|
||||||
|
} else if !reflect.DeepEqual(message.TransientData.Data, data) {
|
||||||
|
return fmt.Errorf("Expected transient initial data %+v, got %+v", data, message.TransientData.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
/**
|
||||||
|
* Standalone signaling server for the Nextcloud Spreed app.
|
||||||
|
* Copyright (C) 2021 struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <bauch@struktur.de>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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 signaling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TransientListener interface {
|
||||||
|
SendMessage(message *ServerMessage) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransientData struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
data map[string]interface{}
|
||||||
|
listeners map[TransientListener]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransientData creates a new transient data container.
|
||||||
|
func NewTransientData() *TransientData {
|
||||||
|
return &TransientData{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TransientData) notifySet(key string, prev, value interface{}) {
|
||||||
|
msg := &ServerMessage{
|
||||||
|
Type: "transient",
|
||||||
|
TransientData: &TransientDataServerMessage{
|
||||||
|
Type: "set",
|
||||||
|
Key: key,
|
||||||
|
OldValue: prev,
|
||||||
|
Value: value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for listener := range t.listeners {
|
||||||
|
listener.SendMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TransientData) notifyDeleted(key string, prev interface{}) {
|
||||||
|
msg := &ServerMessage{
|
||||||
|
Type: "transient",
|
||||||
|
TransientData: &TransientDataServerMessage{
|
||||||
|
Type: "remove",
|
||||||
|
Key: key,
|
||||||
|
OldValue: prev,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for listener := range t.listeners {
|
||||||
|
listener.SendMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddListener adds a new listener to be notified about changes.
|
||||||
|
func (t *TransientData) AddListener(listener TransientListener) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if t.listeners == nil {
|
||||||
|
t.listeners = make(map[TransientListener]bool)
|
||||||
|
}
|
||||||
|
t.listeners[listener] = true
|
||||||
|
if len(t.data) > 0 {
|
||||||
|
msg := &ServerMessage{
|
||||||
|
Type: "transient",
|
||||||
|
TransientData: &TransientDataServerMessage{
|
||||||
|
Type: "initial",
|
||||||
|
Data: t.data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
listener.SendMessage(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveListener removes a previously registered listener.
|
||||||
|
func (t *TransientData) RemoveListener(listener TransientListener) {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
delete(t.listeners, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets a new value for the given key and notifies listeners
|
||||||
|
// if the value has been changed.
|
||||||
|
func (t *TransientData) Set(key string, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
return t.Remove(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
prev, found := t.data[key]
|
||||||
|
if found && reflect.DeepEqual(prev, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.data == nil {
|
||||||
|
t.data = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
t.data[key] = value
|
||||||
|
t.notifySet(key, prev, value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareAndSet sets a new value for the given key only for a given old value
|
||||||
|
// and notifies listeners if the value has been changed.
|
||||||
|
func (t *TransientData) CompareAndSet(key string, old, value interface{}) bool {
|
||||||
|
if value == nil {
|
||||||
|
return t.CompareAndRemove(key, old)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
prev, found := t.data[key]
|
||||||
|
if old != nil && (!found || !reflect.DeepEqual(prev, old)) {
|
||||||
|
return false
|
||||||
|
} else if old == nil && found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t.data[key] = value
|
||||||
|
t.notifySet(key, prev, value)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove deletes the value with the given key and notifies listeners
|
||||||
|
// if the key was removed.
|
||||||
|
func (t *TransientData) Remove(key string) bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
prev, found := t.data[key]
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(t.data, key)
|
||||||
|
t.notifyDeleted(key, prev)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompareAndRemove deletes the value with the given key if it has a given value
|
||||||
|
// and notifies listeners if the key was removed.
|
||||||
|
func (t *TransientData) CompareAndRemove(key string, old interface{}) bool {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
prev, found := t.data[key]
|
||||||
|
if !found || !reflect.DeepEqual(prev, old) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(t.data, key)
|
||||||
|
t.notifyDeleted(key, prev)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetData returns a copy of the internal data.
|
||||||
|
func (t *TransientData) GetData() map[string]interface{} {
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
for k, v := range t.data {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,294 @@
|
||||||
|
/**
|
||||||
|
* Standalone signaling server for the Nextcloud Spreed app.
|
||||||
|
* Copyright (C) 2021 struktur AG
|
||||||
|
*
|
||||||
|
* @author Joachim Bauch <bauch@struktur.de>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* 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 signaling
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_TransientData(t *testing.T) {
|
||||||
|
data := NewTransientData()
|
||||||
|
if data.Set("foo", nil) {
|
||||||
|
t.Errorf("should not have set value")
|
||||||
|
}
|
||||||
|
if !data.Set("foo", "bar") {
|
||||||
|
t.Errorf("should have set value")
|
||||||
|
}
|
||||||
|
if data.Set("foo", "bar") {
|
||||||
|
t.Errorf("should not have set value")
|
||||||
|
}
|
||||||
|
if !data.Set("foo", "baz") {
|
||||||
|
t.Errorf("should have set value")
|
||||||
|
}
|
||||||
|
if data.CompareAndSet("foo", "bar", "lala") {
|
||||||
|
t.Errorf("should not have set value")
|
||||||
|
}
|
||||||
|
if !data.CompareAndSet("foo", "baz", "lala") {
|
||||||
|
t.Errorf("should have set value")
|
||||||
|
}
|
||||||
|
if data.CompareAndSet("test", nil, nil) {
|
||||||
|
t.Errorf("should not have set value")
|
||||||
|
}
|
||||||
|
if !data.CompareAndSet("test", nil, "123") {
|
||||||
|
t.Errorf("should have set value")
|
||||||
|
}
|
||||||
|
if data.CompareAndRemove("test", "1234") {
|
||||||
|
t.Errorf("should not have removed value")
|
||||||
|
}
|
||||||
|
if !data.CompareAndRemove("test", "123") {
|
||||||
|
t.Errorf("should have removed value")
|
||||||
|
}
|
||||||
|
if data.Remove("lala") {
|
||||||
|
t.Errorf("should not have removed value")
|
||||||
|
}
|
||||||
|
if !data.Remove("foo") {
|
||||||
|
t.Errorf("should have removed value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_TransientMessages(t *testing.T) {
|
||||||
|
hub, _, _, server, shutdown := CreateHubForTest(t)
|
||||||
|
defer shutdown()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client1 := NewTestClient(t, server, hub)
|
||||||
|
defer client1.CloseWithBye()
|
||||||
|
if err := client1.SendHello(testDefaultUserId + "1"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hello1, err := client1.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client1.SetTransientData("foo", "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageError(msg, "not_in_room"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client2 := NewTestClient(t, server, hub)
|
||||||
|
defer client2.CloseWithBye()
|
||||||
|
if err := client2.SendHello(testDefaultUserId + "2"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hello2, err := client2.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join room by id.
|
||||||
|
roomId := "test-room"
|
||||||
|
if room, err := client1.JoinRoom(ctx, roomId); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if room.Room.RoomId != roomId {
|
||||||
|
t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give message processing some time.
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
|
||||||
|
if room, err := client2.JoinRoom(ctx, roomId); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if room.Room.RoomId != roomId {
|
||||||
|
t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForUsersJoined(ctx, t, client1, hello1, client2, hello2)
|
||||||
|
|
||||||
|
session1 := hub.GetSessionByPublicId(hello1.Hello.SessionId).(*ClientSession)
|
||||||
|
if session1 == nil {
|
||||||
|
t.Fatalf("Session %s does not exist", hello1.Hello.SessionId)
|
||||||
|
}
|
||||||
|
session2 := hub.GetSessionByPublicId(hello2.Hello.SessionId).(*ClientSession)
|
||||||
|
if session2 == nil {
|
||||||
|
t.Fatalf("Session %s does not exist", hello2.Hello.SessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client 1 may modify transient data.
|
||||||
|
session1.SetPermissions([]Permission{PERMISSION_TRANSIENT_DATA})
|
||||||
|
// Client 2 may not modify transient data.
|
||||||
|
session2.SetPermissions([]Permission{})
|
||||||
|
|
||||||
|
if err := client2.SetTransientData("foo", "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg, err := client2.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageError(msg, "not_allowed"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client1.SetTransientData("foo", "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, "foo", "bar", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg, err := client2.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, "foo", "bar", nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client2.RemoveTransientData("foo"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg, err := client2.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageError(msg, "not_allowed"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting the same value is ignored by the server.
|
||||||
|
if err := client1.SetTransientData("foo", "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx2, cancel2 := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
defer cancel2()
|
||||||
|
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx2); err != nil {
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected no payload, got %+v", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"hello": "world",
|
||||||
|
}
|
||||||
|
if err := client1.SetTransientData("foo", data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, "foo", data, "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg, err := client2.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, "foo", data, "bar"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client1.RemoveTransientData("foo"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientRemove(msg, "foo", data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if msg, err := client2.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientRemove(msg, "foo", data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing a non-existing key is ignored by the server.
|
||||||
|
if err := client1.RemoveTransientData("foo"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
ctx3, cancel3 := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
defer cancel3()
|
||||||
|
|
||||||
|
if msg, err := client1.RunUntilMessage(ctx3); err != nil {
|
||||||
|
if err != context.DeadlineExceeded {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Errorf("Expected no payload, got %+v", msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client1.SetTransientData("abc", data); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client3 := NewTestClient(t, server, hub)
|
||||||
|
defer client3.CloseWithBye()
|
||||||
|
if err := client3.SendHello(testDefaultUserId + "3"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
hello3, err := client3.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if room, err := client3.JoinRoom(ctx, roomId); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else if room.Room.RoomId != roomId {
|
||||||
|
t.Fatalf("Expected room %s, got %s", roomId, room.Room.RoomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
ignored, err := client3.RunUntilJoinedAndReturnIgnored(ctx, hello1.Hello, hello2.Hello, hello3.Hello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg *ServerMessage
|
||||||
|
if len(ignored) == 0 {
|
||||||
|
if msg, err = client3.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
} else if len(ignored) == 1 {
|
||||||
|
msg = ignored[0]
|
||||||
|
} else {
|
||||||
|
t.Fatalf("Received too many messages: %+v", ignored)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := checkMessageTransientInitial(msg, map[string]interface{}{
|
||||||
|
"abc": data,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue