mirror of
https://github.com/strukturag/nextcloud-spreed-signaling
synced 2024-05-21 15:06:38 +02:00
Merge pull request #563 from strukturag/dialout-support
Implement message handler for dialout support.
This commit is contained in:
commit
0936d40f8b
14
api_async.go
14
api_async.go
|
@ -21,7 +21,11 @@
|
||||||
*/
|
*/
|
||||||
package signaling
|
package signaling
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type AsyncMessage struct {
|
type AsyncMessage struct {
|
||||||
SendTime time.Time `json:"sendtime"`
|
SendTime time.Time `json:"sendtime"`
|
||||||
|
@ -41,6 +45,14 @@ type AsyncMessage struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *AsyncMessage) String() string {
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("Could not serialize %#v: %s", m, err)
|
||||||
|
}
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
|
||||||
type AsyncRoomMessage struct {
|
type AsyncRoomMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -104,6 +106,10 @@ type BackendServerRoomRequest struct {
|
||||||
|
|
||||||
SwitchTo *BackendRoomSwitchToMessageRequest `json:"switchto,omitempty"`
|
SwitchTo *BackendRoomSwitchToMessageRequest `json:"switchto,omitempty"`
|
||||||
|
|
||||||
|
Dialout *BackendRoomDialoutRequest `json:"dialout,omitempty"`
|
||||||
|
|
||||||
|
Transient *BackendRoomTransientRequest `json:"transient,omitempty"`
|
||||||
|
|
||||||
// Internal properties
|
// Internal properties
|
||||||
ReceivedTime int64 `json:"received,omitempty"`
|
ReceivedTime int64 `json:"received,omitempty"`
|
||||||
}
|
}
|
||||||
|
@ -170,6 +176,64 @@ type BackendRoomSwitchToMessageRequest struct {
|
||||||
SessionsMap BackendRoomSwitchToSessionsMap `json:"sessionsmap,omitempty"`
|
SessionsMap BackendRoomSwitchToSessionsMap `json:"sessionsmap,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BackendRoomDialoutRequest struct {
|
||||||
|
// E.164 number to dial (e.g. "+1234567890")
|
||||||
|
Number string `json:"number"`
|
||||||
|
|
||||||
|
Options json.RawMessage `json:"options,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
checkE164Number = regexp.MustCompile(`^\+\d{2,}$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func isValidNumber(s string) bool {
|
||||||
|
return checkE164Number.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BackendRoomDialoutRequest) ValidateNumber() *Error {
|
||||||
|
if r.Number == "" {
|
||||||
|
return NewError("number_missing", "No number provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValidNumber(r.Number) {
|
||||||
|
return NewError("invalid_number", "Expected E.164 number.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TransientAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransientActionSet TransientAction = "set"
|
||||||
|
TransientActionDelete TransientAction = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BackendRoomTransientRequest struct {
|
||||||
|
Action TransientAction `json:"action"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value interface{} `json:"value,omitempty"`
|
||||||
|
TTL time.Duration `json:"ttl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendServerRoomResponse struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
Dialout *BackendRoomDialoutResponse `json:"dialout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendRoomDialoutError struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type BackendRoomDialoutResponse struct {
|
||||||
|
CallId string `json:"callid,omitempty"`
|
||||||
|
|
||||||
|
Error *Error `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Requests from the signaling server to the Nextcloud backend.
|
// Requests from the signaling server to the Nextcloud backend.
|
||||||
|
|
||||||
type BackendClientAuthRequest struct {
|
type BackendClientAuthRequest struct {
|
||||||
|
|
|
@ -56,3 +56,27 @@ func TestBackendChecksum(t *testing.T) {
|
||||||
t.Errorf("Checksum %s could not be validated from request", check1)
|
t.Errorf("Checksum %s could not be validated from request", check1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidNumbers(t *testing.T) {
|
||||||
|
valid := []string{
|
||||||
|
"+12",
|
||||||
|
"+12345",
|
||||||
|
}
|
||||||
|
invalid := []string{
|
||||||
|
"+1",
|
||||||
|
"12345",
|
||||||
|
" +12345",
|
||||||
|
" +12345 ",
|
||||||
|
"+123-45",
|
||||||
|
}
|
||||||
|
for _, number := range valid {
|
||||||
|
if !isValidNumber(number) {
|
||||||
|
t.Errorf("number %s should be valid", number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, number := range invalid {
|
||||||
|
if isValidNumber(number) {
|
||||||
|
t.Errorf("number %s should not be valid", number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ package signaling
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -164,6 +165,10 @@ type ServerMessage struct {
|
||||||
Event *EventServerMessage `json:"event,omitempty"`
|
Event *EventServerMessage `json:"event,omitempty"`
|
||||||
|
|
||||||
TransientData *TransientDataServerMessage `json:"transient,omitempty"`
|
TransientData *TransientDataServerMessage `json:"transient,omitempty"`
|
||||||
|
|
||||||
|
Internal *InternalServerMessage `json:"internal,omitempty"`
|
||||||
|
|
||||||
|
Dialout *DialoutInternalClientMessage `json:"dialout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
func (r *ServerMessage) CloseAfterSend(session Session) bool {
|
||||||
|
@ -444,12 +449,14 @@ const (
|
||||||
ServerFeatureWelcome = "welcome"
|
ServerFeatureWelcome = "welcome"
|
||||||
ServerFeatureHelloV2 = "hello-v2"
|
ServerFeatureHelloV2 = "hello-v2"
|
||||||
ServerFeatureSwitchTo = "switchto"
|
ServerFeatureSwitchTo = "switchto"
|
||||||
|
ServerFeatureDialout = "dialout"
|
||||||
|
|
||||||
// Features to send to internal clients only.
|
// Features to send to internal clients only.
|
||||||
ServerFeatureInternalVirtualSessions = "virtual-sessions"
|
ServerFeatureInternalVirtualSessions = "virtual-sessions"
|
||||||
|
|
||||||
// Possible client features from the "hello" request.
|
// Possible client features from the "hello" request.
|
||||||
ClientFeatureInternalInCall = "internal-incall"
|
ClientFeatureInternalInCall = "internal-incall"
|
||||||
|
ClientFeatureStartDialout = "start-dialout"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -460,6 +467,7 @@ var (
|
||||||
ServerFeatureWelcome,
|
ServerFeatureWelcome,
|
||||||
ServerFeatureHelloV2,
|
ServerFeatureHelloV2,
|
||||||
ServerFeatureSwitchTo,
|
ServerFeatureSwitchTo,
|
||||||
|
ServerFeatureDialout,
|
||||||
}
|
}
|
||||||
DefaultFeaturesInternal = []string{
|
DefaultFeaturesInternal = []string{
|
||||||
ServerFeatureInternalVirtualSessions,
|
ServerFeatureInternalVirtualSessions,
|
||||||
|
@ -468,6 +476,7 @@ var (
|
||||||
ServerFeatureWelcome,
|
ServerFeatureWelcome,
|
||||||
ServerFeatureHelloV2,
|
ServerFeatureHelloV2,
|
||||||
ServerFeatureSwitchTo,
|
ServerFeatureSwitchTo,
|
||||||
|
ServerFeatureDialout,
|
||||||
}
|
}
|
||||||
DefaultWelcomeFeatures = []string{
|
DefaultWelcomeFeatures = []string{
|
||||||
ServerFeatureAudioVideoPermissions,
|
ServerFeatureAudioVideoPermissions,
|
||||||
|
@ -477,6 +486,7 @@ var (
|
||||||
ServerFeatureWelcome,
|
ServerFeatureWelcome,
|
||||||
ServerFeatureHelloV2,
|
ServerFeatureHelloV2,
|
||||||
ServerFeatureSwitchTo,
|
ServerFeatureSwitchTo,
|
||||||
|
ServerFeatureDialout,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -684,6 +694,52 @@ func (m *InCallInternalClientMessage) CheckValid() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DialoutStatus string
|
||||||
|
|
||||||
|
var (
|
||||||
|
DialoutStatusAccepted DialoutStatus = "accepted"
|
||||||
|
DialoutStatusRinging DialoutStatus = "ringing"
|
||||||
|
DialoutStatusConnected DialoutStatus = "connected"
|
||||||
|
DialoutStatusRejected DialoutStatus = "rejected"
|
||||||
|
DialoutStatusCleared DialoutStatus = "cleared"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DialoutStatusInternalClientMessage struct {
|
||||||
|
CallId string `json:"callid"`
|
||||||
|
Status DialoutStatus `json:"status"`
|
||||||
|
|
||||||
|
// Cause is set if Status is "cleared" or "rejected".
|
||||||
|
Cause string `json:"cause,omitempty"`
|
||||||
|
Code int `json:"code,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialoutInternalClientMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
RoomId string `json:"roomid,omitempty"`
|
||||||
|
|
||||||
|
Error *Error `json:"error,omitempty"`
|
||||||
|
|
||||||
|
Status *DialoutStatusInternalClientMessage `json:"status,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DialoutInternalClientMessage) CheckValid() error {
|
||||||
|
switch m.Type {
|
||||||
|
case "":
|
||||||
|
return errors.New("type missing")
|
||||||
|
case "error":
|
||||||
|
if m.Error == nil {
|
||||||
|
return errors.New("error missing")
|
||||||
|
}
|
||||||
|
case "status":
|
||||||
|
if m.Status == nil {
|
||||||
|
return errors.New("status missing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type InternalClientMessage struct {
|
type InternalClientMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
@ -694,10 +750,14 @@ type InternalClientMessage struct {
|
||||||
RemoveSession *RemoveSessionInternalClientMessage `json:"removesession,omitempty"`
|
RemoveSession *RemoveSessionInternalClientMessage `json:"removesession,omitempty"`
|
||||||
|
|
||||||
InCall *InCallInternalClientMessage `json:"incall,omitempty"`
|
InCall *InCallInternalClientMessage `json:"incall,omitempty"`
|
||||||
|
|
||||||
|
Dialout *DialoutInternalClientMessage `json:"dialout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *InternalClientMessage) CheckValid() error {
|
func (m *InternalClientMessage) CheckValid() error {
|
||||||
switch m.Type {
|
switch m.Type {
|
||||||
|
case "":
|
||||||
|
return errors.New("type missing")
|
||||||
case "addsession":
|
case "addsession":
|
||||||
if m.AddSession == nil {
|
if m.AddSession == nil {
|
||||||
return fmt.Errorf("addsession missing")
|
return fmt.Errorf("addsession missing")
|
||||||
|
@ -722,10 +782,28 @@ func (m *InternalClientMessage) CheckValid() error {
|
||||||
} else if err := m.InCall.CheckValid(); err != nil {
|
} else if err := m.InCall.CheckValid(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case "dialout":
|
||||||
|
if m.Dialout == nil {
|
||||||
|
return fmt.Errorf("dialout missing")
|
||||||
|
} else if err := m.Dialout.CheckValid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InternalServerDialoutRequest struct {
|
||||||
|
RoomId string `json:"roomid"`
|
||||||
|
|
||||||
|
Request *BackendRoomDialoutRequest `json:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InternalServerMessage struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
|
||||||
|
Dialout *InternalServerDialoutRequest `json:"dialout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// Type "event"
|
// Type "event"
|
||||||
|
|
||||||
type RoomEventServerMessage struct {
|
type RoomEventServerMessage struct {
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
@ -35,8 +36,10 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dlintw/goconf"
|
"github.com/dlintw/goconf"
|
||||||
|
@ -639,6 +642,121 @@ func (b *BackendServer) sendRoomSwitchTo(roomid string, backend *Backend, reques
|
||||||
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
return b.events.PublishBackendRoomMessage(roomid, backend, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type BackendResponseWithStatus interface {
|
||||||
|
Status() int
|
||||||
|
}
|
||||||
|
|
||||||
|
type DialoutErrorResponse struct {
|
||||||
|
BackendServerRoomResponse
|
||||||
|
|
||||||
|
status int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DialoutErrorResponse) Status() int {
|
||||||
|
return r.status
|
||||||
|
}
|
||||||
|
|
||||||
|
func returnDialoutError(status int, err *Error) (any, error) {
|
||||||
|
response := &DialoutErrorResponse{
|
||||||
|
BackendServerRoomResponse: BackendServerRoomResponse{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutResponse{
|
||||||
|
Error: err,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
status: status,
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var checkNumeric = regexp.MustCompile(`^[0-9]+$`)
|
||||||
|
|
||||||
|
func isNumeric(s string) bool {
|
||||||
|
return checkNumeric.MatchString(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BackendServer) startDialout(roomid string, backend *Backend, request *BackendServerRoomRequest) (any, error) {
|
||||||
|
if err := request.Dialout.ValidateNumber(); err != nil {
|
||||||
|
return returnDialoutError(http.StatusBadRequest, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isNumeric(roomid) {
|
||||||
|
return returnDialoutError(http.StatusBadRequest, NewError("invalid_roomid", "The room id must be numeric."))
|
||||||
|
}
|
||||||
|
|
||||||
|
var session *ClientSession
|
||||||
|
for s := range b.hub.dialoutSessions {
|
||||||
|
if s.GetClient() != nil {
|
||||||
|
session = s
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if session == nil {
|
||||||
|
return returnDialoutError(http.StatusNotFound, NewError("no_client_available", "No available client found to trigger dialout."))
|
||||||
|
}
|
||||||
|
|
||||||
|
id := newRandomString(32)
|
||||||
|
msg := &ServerMessage{
|
||||||
|
Id: id,
|
||||||
|
Type: "internal",
|
||||||
|
Internal: &InternalServerMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &InternalServerDialoutRequest{
|
||||||
|
RoomId: roomid,
|
||||||
|
Request: request.Dialout,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var response atomic.Pointer[DialoutInternalClientMessage]
|
||||||
|
|
||||||
|
session.HandleResponse(id, func(message *ClientMessage) bool {
|
||||||
|
response.Store(message.Internal.Dialout)
|
||||||
|
cancel()
|
||||||
|
// Don't send error to other sessions in the room.
|
||||||
|
return message.Internal.Dialout.Error != nil
|
||||||
|
})
|
||||||
|
defer session.ClearResponseHandler(id)
|
||||||
|
|
||||||
|
if !session.SendMessage(msg) {
|
||||||
|
return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "Could not notify about new dialout."))
|
||||||
|
}
|
||||||
|
|
||||||
|
<-ctx.Done()
|
||||||
|
if err := ctx.Err(); err != nil && !errors.Is(err, context.Canceled) {
|
||||||
|
return returnDialoutError(http.StatusGatewayTimeout, NewError("timeout", "Timeout while waiting for dialout to start."))
|
||||||
|
}
|
||||||
|
|
||||||
|
dialout := response.Load()
|
||||||
|
if dialout == nil {
|
||||||
|
return returnDialoutError(http.StatusBadGateway, NewError("error_notify", "No dialout response received."))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch dialout.Type {
|
||||||
|
case "error":
|
||||||
|
return returnDialoutError(http.StatusBadGateway, dialout.Error)
|
||||||
|
case "status":
|
||||||
|
if dialout.Status.Status != DialoutStatusAccepted {
|
||||||
|
log.Printf("Received unsupported dialout status when triggering dialout: %+v", dialout)
|
||||||
|
return returnDialoutError(http.StatusBadGateway, NewError("unsupported_status", "Unsupported dialout status received."))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BackendServerRoomResponse{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutResponse{
|
||||||
|
CallId: dialout.Status.CallId,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Received unsupported dialout type when triggering dialout: %+v", dialout)
|
||||||
|
return returnDialoutError(http.StatusBadGateway, NewError("unsupported_type", "Unsupported dialout type received."))
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body []byte) {
|
func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body []byte) {
|
||||||
v := mux.Vars(r)
|
v := mux.Vars(r)
|
||||||
roomid := v["roomid"]
|
roomid := v["roomid"]
|
||||||
|
@ -692,6 +810,7 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
||||||
|
|
||||||
request.ReceivedTime = time.Now().UnixNano()
|
request.ReceivedTime = time.Now().UnixNano()
|
||||||
|
|
||||||
|
var response any
|
||||||
var err error
|
var err error
|
||||||
switch request.Type {
|
switch request.Type {
|
||||||
case "invite":
|
case "invite":
|
||||||
|
@ -722,6 +841,8 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
||||||
err = b.sendRoomMessage(roomid, backend, &request)
|
err = b.sendRoomMessage(roomid, backend, &request)
|
||||||
case "switchto":
|
case "switchto":
|
||||||
err = b.sendRoomSwitchTo(roomid, backend, &request)
|
err = b.sendRoomSwitchTo(roomid, backend, &request)
|
||||||
|
case "dialout":
|
||||||
|
response, err = b.startDialout(roomid, backend, &request)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Unsupported request type: "+request.Type, http.StatusBadRequest)
|
http.Error(w, "Unsupported request type: "+request.Type, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
|
@ -733,11 +854,27 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var responseData []byte
|
||||||
|
responseStatus := http.StatusOK
|
||||||
|
if response == nil {
|
||||||
|
// TODO(jojo): Return better response struct.
|
||||||
|
responseData = []byte("{}")
|
||||||
|
} else {
|
||||||
|
if s, ok := response.(BackendResponseWithStatus); ok {
|
||||||
|
responseStatus = s.Status()
|
||||||
|
}
|
||||||
|
responseData, err = json.Marshal(response)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not serialize backend response %+v: %s", response, err)
|
||||||
|
responseStatus = http.StatusInternalServerError
|
||||||
|
responseData = []byte("{\"error\":\"could_not_serialize\"}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(responseStatus)
|
||||||
// TODO(jojo): Return better response struct.
|
w.Write(responseData) // nolint
|
||||||
w.Write([]byte("{}")) // nolint
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
|
func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
|
||||||
|
|
|
@ -1760,3 +1760,296 @@ func TestBackendServer_StatsAllowedIps(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_IsNumeric(t *testing.T) {
|
||||||
|
numeric := []string{
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"12345",
|
||||||
|
}
|
||||||
|
nonNumeric := []string{
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
" 0",
|
||||||
|
"0 ",
|
||||||
|
" 0 ",
|
||||||
|
"-1",
|
||||||
|
"1.2",
|
||||||
|
"1a",
|
||||||
|
"a1",
|
||||||
|
}
|
||||||
|
for _, s := range numeric {
|
||||||
|
if !isNumeric(s) {
|
||||||
|
t.Errorf("%s should be numeric", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range nonNumeric {
|
||||||
|
if isNumeric(s) {
|
||||||
|
t.Errorf("%s should not be numeric", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendServer_DialoutNoSipBridge(t *testing.T) {
|
||||||
|
_, _, _, hub, _, server := CreateBackendServerForTest(t)
|
||||||
|
|
||||||
|
client := NewTestClient(t, server, hub)
|
||||||
|
defer client.CloseWithBye()
|
||||||
|
if err := client.SendHelloInternal(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := client.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomId := "12345"
|
||||||
|
msg := &BackendServerRoomRequest{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutRequest{
|
||||||
|
Number: "+1234567890",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusNotFound {
|
||||||
|
t.Fatalf("Expected error %d, got %s: %s", http.StatusNotFound, res.Status, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BackendServerRoomResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Type != "dialout" || response.Dialout == nil {
|
||||||
|
t.Fatalf("expected type dialout, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error == nil {
|
||||||
|
t.Fatalf("expected dialout error, got %s", string(body))
|
||||||
|
}
|
||||||
|
if expected := "no_client_available"; response.Dialout.Error.Code != expected {
|
||||||
|
t.Errorf("expected error code %s, got %s", expected, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendServer_DialoutAccepted(t *testing.T) {
|
||||||
|
_, _, _, hub, _, server := CreateBackendServerForTest(t)
|
||||||
|
|
||||||
|
client := NewTestClient(t, server, hub)
|
||||||
|
defer client.CloseWithBye()
|
||||||
|
if err := client.SendHelloInternalWithFeatures([]string{"start-dialout"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := client.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomId := "12345"
|
||||||
|
callId := "call-123"
|
||||||
|
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(stopped)
|
||||||
|
|
||||||
|
msg, err := client.RunUntilMessage(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type != "internal" || msg.Internal.Type != "dialout" {
|
||||||
|
t.Errorf("expected internal dialout message, got %+v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Internal.Dialout.RoomId != roomId {
|
||||||
|
t.Errorf("expected room id %s, got %+v", roomId, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &ClientMessage{
|
||||||
|
Id: msg.Id,
|
||||||
|
Type: "internal",
|
||||||
|
Internal: &InternalClientMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &DialoutInternalClientMessage{
|
||||||
|
Type: "status",
|
||||||
|
RoomId: msg.Internal.Dialout.RoomId,
|
||||||
|
Status: &DialoutStatusInternalClientMessage{
|
||||||
|
Status: "accepted",
|
||||||
|
CallId: callId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.WriteJSON(response); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
<-stopped
|
||||||
|
}()
|
||||||
|
|
||||||
|
msg := &BackendServerRoomRequest{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutRequest{
|
||||||
|
Number: "+1234567890",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("Expected error %d, got %s: %s", http.StatusOK, res.Status, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BackendServerRoomResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Type != "dialout" || response.Dialout == nil {
|
||||||
|
t.Fatalf("expected type dialout, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error != nil {
|
||||||
|
t.Fatalf("expected dialout success, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.CallId != callId {
|
||||||
|
t.Errorf("expected call id %s, got %s", callId, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackendServer_DialoutRejected(t *testing.T) {
|
||||||
|
_, _, _, hub, _, server := CreateBackendServerForTest(t)
|
||||||
|
|
||||||
|
client := NewTestClient(t, server, hub)
|
||||||
|
defer client.CloseWithBye()
|
||||||
|
if err := client.SendHelloInternalWithFeatures([]string{"start-dialout"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := client.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomId := "12345"
|
||||||
|
errorCode := "error-code"
|
||||||
|
errorMessage := "rejected call"
|
||||||
|
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(stopped)
|
||||||
|
|
||||||
|
msg, err := client.RunUntilMessage(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type != "internal" || msg.Internal.Type != "dialout" {
|
||||||
|
t.Errorf("expected internal dialout message, got %+v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Internal.Dialout.RoomId != roomId {
|
||||||
|
t.Errorf("expected room id %s, got %+v", roomId, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &ClientMessage{
|
||||||
|
Id: msg.Id,
|
||||||
|
Type: "internal",
|
||||||
|
Internal: &InternalClientMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &DialoutInternalClientMessage{
|
||||||
|
Type: "error",
|
||||||
|
Error: NewError(errorCode, errorMessage),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.WriteJSON(response); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
<-stopped
|
||||||
|
}()
|
||||||
|
|
||||||
|
msg := &BackendServerRoomRequest{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutRequest{
|
||||||
|
Number: "+1234567890",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusBadGateway {
|
||||||
|
t.Fatalf("Expected error %d, got %s: %s", http.StatusBadGateway, res.Status, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BackendServerRoomResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Type != "dialout" || response.Dialout == nil {
|
||||||
|
t.Fatalf("expected type dialout, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error == nil {
|
||||||
|
t.Fatalf("expected dialout error, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error.Code != errorCode {
|
||||||
|
t.Errorf("expected error code %s, got %s", errorCode, string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error.Message != errorMessage {
|
||||||
|
t.Errorf("expected error message %s, got %s", errorMessage, string(body))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ var (
|
||||||
PathToOcsSignalingBackend = "ocs/v2.php/apps/spreed/api/v1/signaling/backend"
|
PathToOcsSignalingBackend = "ocs/v2.php/apps/spreed/api/v1/signaling/backend"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ResponseHandlerFunc will return "true" has been fully processed.
|
||||||
|
type ResponseHandlerFunc func(message *ClientMessage) bool
|
||||||
|
|
||||||
type ClientSession struct {
|
type ClientSession struct {
|
||||||
roomJoinTime int64
|
roomJoinTime int64
|
||||||
inCall uint32
|
inCall uint32
|
||||||
|
@ -89,6 +92,9 @@ type ClientSession struct {
|
||||||
|
|
||||||
seenJoinedLock sync.Mutex
|
seenJoinedLock sync.Mutex
|
||||||
seenJoinedEvents map[string]bool
|
seenJoinedEvents map[string]bool
|
||||||
|
|
||||||
|
responseHandlersLock sync.Mutex
|
||||||
|
responseHandlers map[string]ResponseHandlerFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientSession(hub *Hub, privateId string, publicId string, data *SessionIdData, backend *Backend, hello *HelloClientMessage, auth *BackendClientAuthResponse) (*ClientSession, error) {
|
func NewClientSession(hub *Hub, privateId string, publicId string, data *SessionIdData, backend *Backend, hello *HelloClientMessage, auth *BackendClientAuthResponse) (*ClientSession, error) {
|
||||||
|
@ -1420,3 +1426,38 @@ func (s *ClientSession) GetVirtualSessions() []*VirtualSession {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ClientSession) HandleResponse(id string, handler ResponseHandlerFunc) {
|
||||||
|
s.responseHandlersLock.Lock()
|
||||||
|
defer s.responseHandlersLock.Unlock()
|
||||||
|
|
||||||
|
if s.responseHandlers == nil {
|
||||||
|
s.responseHandlers = make(map[string]ResponseHandlerFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.responseHandlers[id] = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientSession) ClearResponseHandler(id string) {
|
||||||
|
s.responseHandlersLock.Lock()
|
||||||
|
defer s.responseHandlersLock.Unlock()
|
||||||
|
|
||||||
|
delete(s.responseHandlers, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientSession) ProcessResponse(message *ClientMessage) bool {
|
||||||
|
id := message.Id
|
||||||
|
if id == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
s.responseHandlersLock.Lock()
|
||||||
|
cb, found := s.responseHandlers[id]
|
||||||
|
defer s.responseHandlersLock.Unlock()
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return cb(message)
|
||||||
|
}
|
||||||
|
|
|
@ -793,6 +793,74 @@ Message format (Server -> Client, receive message)
|
||||||
- The `userid` is omitted if a message was sent by an anonymous user.
|
- The `userid` is omitted if a message was sent by an anonymous user.
|
||||||
|
|
||||||
|
|
||||||
|
## Control messages
|
||||||
|
|
||||||
|
Similar to regular messages between clients which can be sent by any session,
|
||||||
|
messages with type `control` can only be sent if the permission flag `control`
|
||||||
|
is available.
|
||||||
|
|
||||||
|
These messages can be used to perform actions on clients that should only be
|
||||||
|
possible by some users (e.g. moderators).
|
||||||
|
|
||||||
|
Message format (Client -> Server, mute phone):
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"type": "control",
|
||||||
|
"control": {
|
||||||
|
"recipient": {
|
||||||
|
"type": "session",
|
||||||
|
"sessionid": "the-session-id-to-send-to"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "mute",
|
||||||
|
"audio": "audio-flags"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
The bit-field `audio-flags` supports the following bits:
|
||||||
|
- `1`: mute speaking (i.e. phone can no longer talk)
|
||||||
|
- `2`: mute listening (i.e. phone is on hold and can no longer hear)
|
||||||
|
|
||||||
|
To unmute, a value of `0` must be sent.
|
||||||
|
|
||||||
|
Message format (Client -> Server, hangup phone):
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"type": "control",
|
||||||
|
"control": {
|
||||||
|
"recipient": {
|
||||||
|
"type": "session",
|
||||||
|
"sessionid": "the-session-id-to-send-to"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "hangup"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message format (Client -> Server, send DTMF):
|
||||||
|
|
||||||
|
{
|
||||||
|
"id": "unique-request-id",
|
||||||
|
"type": "control",
|
||||||
|
"control": {
|
||||||
|
"recipient": {
|
||||||
|
"type": "session",
|
||||||
|
"sessionid": "the-session-id-to-send-to"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": "dtmf",
|
||||||
|
"digit": "the-digit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Supported digits are `0`-`9`, `*` and `#`.
|
||||||
|
|
||||||
|
|
||||||
## Transient data
|
## Transient data
|
||||||
|
|
||||||
Transient data can be used to share data in a room that is valid while sessions
|
Transient data can be used to share data in a room that is valid while sessions
|
||||||
|
@ -936,6 +1004,13 @@ Message format (Client -> Server):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Phone sessions will have `type` set to `phone` in the additional user data
|
||||||
|
(which will be included in the `joined` [room event](#room-events)),
|
||||||
|
`callid` will be the id of the phone call and `number` the target of the call.
|
||||||
|
The call id will match the one returned for accepted outgoing calls and the
|
||||||
|
associated session id can be used to hangup a call or send DTMF tones to it.
|
||||||
|
|
||||||
|
|
||||||
### Update virtual session
|
### Update virtual session
|
||||||
|
|
||||||
Message format (Client -> Server):
|
Message format (Client -> Server):
|
||||||
|
@ -1207,3 +1282,49 @@ Message format (Server -> Client):
|
||||||
|
|
||||||
Clients are expected to follow the `switchto` message. If clients don't switch
|
Clients are expected to follow the `switchto` message. If clients don't switch
|
||||||
to the target room after some time, they might get disconnected.
|
to the target room after some time, they might get disconnected.
|
||||||
|
|
||||||
|
|
||||||
|
### Start dialout from a room
|
||||||
|
|
||||||
|
Use this to start a phone dialout to a new user in a given room.
|
||||||
|
|
||||||
|
Message format (Backend -> Server)
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "dialout"
|
||||||
|
"dialout" {
|
||||||
|
"number": "e164-target-number",
|
||||||
|
"options": {
|
||||||
|
...arbitrary options that will be sent back to validate...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Please note that this requires a connected internal client that supports
|
||||||
|
dialout (e.g. the SIP bridge).
|
||||||
|
|
||||||
|
Message format (Server -> Backend, request was accepted)
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "dialout"
|
||||||
|
"dialout" {
|
||||||
|
"callid": "the-unique-call-id"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Message format (Server -> Backend, request could not be processed)
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "dialout"
|
||||||
|
"dialout" {
|
||||||
|
"error": {
|
||||||
|
"code": "the-internal-message-id",
|
||||||
|
"message": "human-readable-error-message",
|
||||||
|
"details": {
|
||||||
|
...optional additional details...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
A HTTP error status code will be set in this case.
|
||||||
|
|
53
hub.go
53
hub.go
|
@ -97,6 +97,9 @@ var (
|
||||||
|
|
||||||
// Delay after which a screen publisher should be cleaned up.
|
// Delay after which a screen publisher should be cleaned up.
|
||||||
cleanupScreenPublisherDelay = time.Second
|
cleanupScreenPublisherDelay = time.Second
|
||||||
|
|
||||||
|
// Delay after which a "cleared" / "rejected" dialout status should be removed.
|
||||||
|
removeCallStatusTTL = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -150,6 +153,7 @@ type Hub struct {
|
||||||
expiredSessions map[Session]bool
|
expiredSessions map[Session]bool
|
||||||
anonymousSessions map[*ClientSession]time.Time
|
anonymousSessions map[*ClientSession]time.Time
|
||||||
expectHelloClients map[*Client]time.Time
|
expectHelloClients map[*Client]time.Time
|
||||||
|
dialoutSessions map[*ClientSession]bool
|
||||||
|
|
||||||
backendTimeout time.Duration
|
backendTimeout time.Duration
|
||||||
backend *BackendClient
|
backend *BackendClient
|
||||||
|
@ -338,6 +342,7 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer
|
||||||
expiredSessions: make(map[Session]bool),
|
expiredSessions: make(map[Session]bool),
|
||||||
anonymousSessions: make(map[*ClientSession]time.Time),
|
anonymousSessions: make(map[*ClientSession]time.Time),
|
||||||
expectHelloClients: make(map[*Client]time.Time),
|
expectHelloClients: make(map[*Client]time.Time),
|
||||||
|
dialoutSessions: make(map[*ClientSession]bool),
|
||||||
|
|
||||||
backendTimeout: backendTimeout,
|
backendTimeout: backendTimeout,
|
||||||
backend: backend,
|
backend: backend,
|
||||||
|
@ -641,6 +646,7 @@ func (h *Hub) removeSession(session Session) (removed bool) {
|
||||||
delete(h.expiredSessions, session)
|
delete(h.expiredSessions, session)
|
||||||
if session, ok := session.(*ClientSession); ok {
|
if session, ok := session.(*ClientSession); ok {
|
||||||
delete(h.anonymousSessions, session)
|
delete(h.anonymousSessions, session)
|
||||||
|
delete(h.dialoutSessions, session)
|
||||||
}
|
}
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
return
|
return
|
||||||
|
@ -802,8 +808,12 @@ func (h *Hub) processRegister(client *Client, message *ClientMessage, backend *B
|
||||||
h.sessions[sessionIdData.Sid] = session
|
h.sessions[sessionIdData.Sid] = session
|
||||||
h.clients[sessionIdData.Sid] = client
|
h.clients[sessionIdData.Sid] = client
|
||||||
delete(h.expectHelloClients, client)
|
delete(h.expectHelloClients, client)
|
||||||
if userId == "" && auth.Type != HelloClientTypeInternal {
|
if userId == "" && session.ClientType() != HelloClientTypeInternal {
|
||||||
h.startWaitAnonymousSessionRoomLocked(session)
|
h.startWaitAnonymousSessionRoomLocked(session)
|
||||||
|
} else if session.ClientType() == HelloClientTypeInternal && session.HasFeature(ClientFeatureStartDialout) {
|
||||||
|
// TODO: There is a small race condition for sessions that take some time
|
||||||
|
// between connecting and joining a room.
|
||||||
|
h.dialoutSessions[session] = true
|
||||||
}
|
}
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
@ -1250,6 +1260,7 @@ func (h *Hub) processRoom(client *Client, message *ClientMessage) {
|
||||||
h.startWaitAnonymousSessionRoom(session)
|
h.startWaitAnonymousSessionRoom(session)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1389,6 +1400,10 @@ func (h *Hub) processJoinRoom(session *ClientSession, message *ClientMessage, ro
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
// The session now joined a room, don't expire if it is anonymous.
|
// The session now joined a room, don't expire if it is anonymous.
|
||||||
delete(h.anonymousSessions, session)
|
delete(h.anonymousSessions, session)
|
||||||
|
if session.ClientType() == HelloClientTypeInternal && session.HasFeature(ClientFeatureStartDialout) {
|
||||||
|
// An internal session in a room can not be used for dialout.
|
||||||
|
delete(h.dialoutSessions, session)
|
||||||
|
}
|
||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
session.SetRoom(r)
|
session.SetRoom(r)
|
||||||
if room.Room.Permissions != nil {
|
if room.Room.Permissions != nil {
|
||||||
|
@ -1772,6 +1787,10 @@ func (h *Hub) processInternalMsg(client *Client, message *ClientMessage) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if session.ProcessResponse(message) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch msg.Type {
|
switch msg.Type {
|
||||||
case "addsession":
|
case "addsession":
|
||||||
msg := msg.AddSession
|
msg := msg.AddSession
|
||||||
|
@ -1923,6 +1942,38 @@ func (h *Hub) processInternalMsg(client *Client, message *ClientMessage) {
|
||||||
room.NotifySessionChanged(session, SessionChangeInCall)
|
room.NotifySessionChanged(session, SessionChangeInCall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "dialout":
|
||||||
|
roomId := msg.Dialout.RoomId
|
||||||
|
msg.Dialout.RoomId = "" // Don't send room id to recipients.
|
||||||
|
if msg.Dialout.Type == "status" {
|
||||||
|
asyncMessage := &AsyncMessage{
|
||||||
|
Type: "room",
|
||||||
|
Room: &BackendServerRoomRequest{
|
||||||
|
Type: "transient",
|
||||||
|
Transient: &BackendRoomTransientRequest{
|
||||||
|
Action: TransientActionSet,
|
||||||
|
Key: "callstatus_" + msg.Dialout.Status.CallId,
|
||||||
|
Value: msg.Dialout.Status,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if msg.Dialout.Status.Status == DialoutStatusCleared || msg.Dialout.Status.Status == DialoutStatusRejected {
|
||||||
|
asyncMessage.Room.Transient.TTL = removeCallStatusTTL
|
||||||
|
}
|
||||||
|
if err := h.events.PublishBackendRoomMessage(roomId, session.Backend(), asyncMessage); err != nil {
|
||||||
|
log.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := h.events.PublishRoomMessage(roomId, session.Backend(), &AsyncMessage{
|
||||||
|
Type: "message",
|
||||||
|
Message: &ServerMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: msg.Dialout,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
log.Printf("Error publishing dialout message %+v to room %s", msg.Dialout, roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
log.Printf("Ignore unsupported internal message %+v from %s", msg, session.PublicId())
|
log.Printf("Ignore unsupported internal message %+v from %s", msg, session.PublicId())
|
||||||
return
|
return
|
||||||
|
|
211
hub_test.go
211
hub_test.go
|
@ -5349,3 +5349,214 @@ func TestGeoipOverrides(t *testing.T) {
|
||||||
t.Errorf("expected country %s, got %s", strings.ToUpper(country3), country)
|
t.Errorf("expected country %s, got %s", strings.ToUpper(country3), country)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDialoutStatus(t *testing.T) {
|
||||||
|
_, _, _, hub, _, server := CreateBackendServerForTest(t)
|
||||||
|
|
||||||
|
internalClient := NewTestClient(t, server, hub)
|
||||||
|
defer internalClient.CloseWithBye()
|
||||||
|
if err := internalClient.SendHelloInternalWithFeatures([]string{"start-dialout"}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, err := internalClient.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomId := "12345"
|
||||||
|
client := NewTestClient(t, server, hub)
|
||||||
|
defer client.CloseWithBye()
|
||||||
|
|
||||||
|
if err := client.SendHello(testDefaultUserId); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hello, err := client.RunUntilHello(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := client.JoinRoom(ctx, roomId); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.RunUntilJoined(ctx, hello.Hello); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
callId := "call-123"
|
||||||
|
|
||||||
|
stopped := make(chan struct{})
|
||||||
|
go func(client *TestClient) {
|
||||||
|
defer close(stopped)
|
||||||
|
|
||||||
|
msg, err := client.RunUntilMessage(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type != "internal" || msg.Internal.Type != "dialout" {
|
||||||
|
t.Errorf("expected internal dialout message, got %+v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Internal.Dialout.RoomId != roomId {
|
||||||
|
t.Errorf("expected room id %s, got %+v", roomId, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := &ClientMessage{
|
||||||
|
Id: msg.Id,
|
||||||
|
Type: "internal",
|
||||||
|
Internal: &InternalClientMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &DialoutInternalClientMessage{
|
||||||
|
Type: "status",
|
||||||
|
RoomId: msg.Internal.Dialout.RoomId,
|
||||||
|
Status: &DialoutStatusInternalClientMessage{
|
||||||
|
Status: "accepted",
|
||||||
|
CallId: callId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := client.WriteJSON(response); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}(internalClient)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
<-stopped
|
||||||
|
}()
|
||||||
|
|
||||||
|
msg := &BackendServerRoomRequest{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: &BackendRoomDialoutRequest{
|
||||||
|
Number: "+1234567890",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
res, err := performBackendRequest(server.URL+"/api/v1/room/"+roomId, data)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
if res.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("Expected error %d, got %s: %s", http.StatusOK, res.Status, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var response BackendServerRoomResponse
|
||||||
|
if err := json.Unmarshal(body, &response); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Type != "dialout" || response.Dialout == nil {
|
||||||
|
t.Fatalf("expected type dialout, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.Error != nil {
|
||||||
|
t.Fatalf("expected dialout success, got %s", string(body))
|
||||||
|
}
|
||||||
|
if response.Dialout.CallId != callId {
|
||||||
|
t.Errorf("expected call id %s, got %s", callId, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
key := "callstatus_" + callId
|
||||||
|
if msg, err := client.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, key, map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "accepted",
|
||||||
|
}, nil); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := internalClient.SendInternalDialout(&DialoutInternalClientMessage{
|
||||||
|
RoomId: roomId,
|
||||||
|
Type: "status",
|
||||||
|
Status: &DialoutStatusInternalClientMessage{
|
||||||
|
CallId: callId,
|
||||||
|
Status: "ringing",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := client.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, key, map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "ringing",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "accepted",
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
old := removeCallStatusTTL
|
||||||
|
defer func() {
|
||||||
|
removeCallStatusTTL = old
|
||||||
|
}()
|
||||||
|
removeCallStatusTTL = 500 * time.Millisecond
|
||||||
|
|
||||||
|
clearedCause := "cleared-call"
|
||||||
|
if err := internalClient.SendInternalDialout(&DialoutInternalClientMessage{
|
||||||
|
RoomId: roomId,
|
||||||
|
Type: "status",
|
||||||
|
Status: &DialoutStatusInternalClientMessage{
|
||||||
|
CallId: callId,
|
||||||
|
Status: "cleared",
|
||||||
|
Cause: clearedCause,
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg, err := client.RunUntilMessage(ctx); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientSet(msg, key, map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "cleared",
|
||||||
|
"cause": clearedCause,
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "ringing",
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx2, cancel := context.WithTimeout(ctx, removeCallStatusTTL*2)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if msg, err := client.RunUntilMessage(ctx2); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
} else {
|
||||||
|
if err := checkMessageTransientRemove(msg, key, map[string]interface{}{
|
||||||
|
"callid": callId,
|
||||||
|
"status": "cleared",
|
||||||
|
"cause": clearedCause,
|
||||||
|
}); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
9
room.go
9
room.go
|
@ -244,6 +244,15 @@ func (r *Room) processBackendRoomRequestRoom(message *BackendServerRoomRequest)
|
||||||
r.publishRoomMessage(message.Message)
|
r.publishRoomMessage(message.Message)
|
||||||
case "switchto":
|
case "switchto":
|
||||||
r.publishSwitchTo(message.SwitchTo)
|
r.publishSwitchTo(message.SwitchTo)
|
||||||
|
case "transient":
|
||||||
|
switch message.Transient.Action {
|
||||||
|
case TransientActionSet:
|
||||||
|
r.SetTransientDataTTL(message.Transient.Key, message.Transient.Value, message.Transient.TTL)
|
||||||
|
case TransientActionDelete:
|
||||||
|
r.RemoveTransientData(message.Transient.Key)
|
||||||
|
default:
|
||||||
|
log.Printf("Unsupported transient action in room %s: %+v", r.Id(), message.Transient)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
log.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.Id(), message)
|
log.Printf("Unsupported backend room request with type %s in %s: %+v", message.Type, r.Id(), message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -211,6 +212,7 @@ type TestClient struct {
|
||||||
hub *Hub
|
hub *Hub
|
||||||
server *httptest.Server
|
server *httptest.Server
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
localAddr net.Addr
|
localAddr net.Addr
|
||||||
|
|
||||||
|
@ -280,6 +282,8 @@ func (c *TestClient) CloseWithBye() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TestClient) Close() {
|
func (c *TestClient) Close() {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err == websocket.ErrCloseSent {
|
if err := c.conn.WriteMessage(websocket.CloseMessage, []byte{}); err == websocket.ErrCloseSent {
|
||||||
// Already closed
|
// Already closed
|
||||||
return
|
return
|
||||||
|
@ -368,6 +372,8 @@ func (c *TestClient) WriteJSON(data interface{}) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
return c.conn.WriteJSON(data)
|
return c.conn.WriteJSON(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -578,6 +584,18 @@ func (c *TestClient) SendInternalRemoveSession(msg *RemoveSessionInternalClientM
|
||||||
return c.WriteJSON(message)
|
return c.WriteJSON(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *TestClient) SendInternalDialout(msg *DialoutInternalClientMessage) error {
|
||||||
|
message := &ClientMessage{
|
||||||
|
Id: "abcd",
|
||||||
|
Type: "internal",
|
||||||
|
Internal: &InternalClientMessage{
|
||||||
|
Type: "dialout",
|
||||||
|
Dialout: msg,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c.WriteJSON(message)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TestClient) SetTransientData(key string, value interface{}, ttl time.Duration) error {
|
func (c *TestClient) SetTransientData(key string, value interface{}, ttl time.Duration) error {
|
||||||
payload, err := json.Marshal(value)
|
payload, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue