mirror of
https://github.com/strukturag/nextcloud-spreed-signaling.git
synced 2025-03-14 19:42:48 +00:00
466 lines
13 KiB
Go
466 lines
13 KiB
Go
/**
|
|
* Standalone signaling server for the Nextcloud Spreed app.
|
|
* Copyright (C) 2017 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 (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
BackendVersion = "1.0"
|
|
|
|
HeaderBackendSignalingRandom = "Spreed-Signaling-Random"
|
|
HeaderBackendSignalingChecksum = "Spreed-Signaling-Checksum"
|
|
HeaderBackendServer = "Spreed-Signaling-Backend"
|
|
|
|
ConfigGroupSignaling = "signaling"
|
|
|
|
ConfigKeyHelloV2TokenKey = "hello-v2-token-key"
|
|
ConfigKeySessionPingLimit = "session-ping-limit"
|
|
)
|
|
|
|
func newRandomString(length int) string {
|
|
b := make([]byte, length/2)
|
|
if _, err := rand.Read(b); err != nil {
|
|
panic(err)
|
|
}
|
|
return hex.EncodeToString(b)
|
|
}
|
|
|
|
func CalculateBackendChecksum(random string, body []byte, secret []byte) string {
|
|
mac := hmac.New(sha256.New, secret)
|
|
mac.Write([]byte(random)) // nolint
|
|
mac.Write(body) // nolint
|
|
return hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func AddBackendChecksum(r *http.Request, body []byte, secret []byte) {
|
|
// Add checksum so the backend can validate the request.
|
|
rnd := newRandomString(64)
|
|
checksum := CalculateBackendChecksum(rnd, body, secret)
|
|
r.Header.Set(HeaderBackendSignalingRandom, rnd)
|
|
r.Header.Set(HeaderBackendSignalingChecksum, checksum)
|
|
}
|
|
|
|
func ValidateBackendChecksum(r *http.Request, body []byte, secret []byte) bool {
|
|
rnd := r.Header.Get(HeaderBackendSignalingRandom)
|
|
checksum := r.Header.Get(HeaderBackendSignalingChecksum)
|
|
return ValidateBackendChecksumValue(checksum, rnd, body, secret)
|
|
}
|
|
|
|
func ValidateBackendChecksumValue(checksum string, random string, body []byte, secret []byte) bool {
|
|
verify := CalculateBackendChecksum(random, body, secret)
|
|
return subtle.ConstantTimeCompare([]byte(verify), []byte(checksum)) == 1
|
|
}
|
|
|
|
// Requests from Nextcloud to the signaling server.
|
|
|
|
type BackendServerRoomRequest struct {
|
|
room *Room
|
|
|
|
Type string `json:"type"`
|
|
|
|
Invite *BackendRoomInviteRequest `json:"invite,omitempty"`
|
|
|
|
Disinvite *BackendRoomDisinviteRequest `json:"disinvite,omitempty"`
|
|
|
|
Update *BackendRoomUpdateRequest `json:"update,omitempty"`
|
|
|
|
Delete *BackendRoomDeleteRequest `json:"delete,omitempty"`
|
|
|
|
InCall *BackendRoomInCallRequest `json:"incall,omitempty"`
|
|
|
|
Participants *BackendRoomParticipantsRequest `json:"participants,omitempty"`
|
|
|
|
Message *BackendRoomMessageRequest `json:"message,omitempty"`
|
|
|
|
SwitchTo *BackendRoomSwitchToMessageRequest `json:"switchto,omitempty"`
|
|
|
|
Dialout *BackendRoomDialoutRequest `json:"dialout,omitempty"`
|
|
|
|
Transient *BackendRoomTransientRequest `json:"transient,omitempty"`
|
|
|
|
// Internal properties
|
|
ReceivedTime int64 `json:"received,omitempty"`
|
|
}
|
|
|
|
type BackendRoomInviteRequest struct {
|
|
UserIds []string `json:"userids,omitempty"`
|
|
// TODO(jojo): We should get rid of "AllUserIds" and find a better way to
|
|
// notify existing users the room has changed and they need to update it.
|
|
AllUserIds []string `json:"alluserids,omitempty"`
|
|
Properties json.RawMessage `json:"properties,omitempty"`
|
|
}
|
|
|
|
type BackendRoomDisinviteRequest struct {
|
|
UserIds []string `json:"userids,omitempty"`
|
|
SessionIds []string `json:"sessionids,omitempty"`
|
|
// TODO(jojo): We should get rid of "AllUserIds" and find a better way to
|
|
// notify existing users the room has changed and they need to update it.
|
|
AllUserIds []string `json:"alluserids,omitempty"`
|
|
Properties json.RawMessage `json:"properties,omitempty"`
|
|
}
|
|
|
|
type BackendRoomUpdateRequest struct {
|
|
UserIds []string `json:"userids,omitempty"`
|
|
Properties json.RawMessage `json:"properties,omitempty"`
|
|
}
|
|
|
|
type BackendRoomDeleteRequest struct {
|
|
UserIds []string `json:"userids,omitempty"`
|
|
}
|
|
|
|
type BackendRoomInCallRequest struct {
|
|
// TODO(jojo): Change "InCall" to "int" when #914 has landed in NC Talk.
|
|
InCall json.RawMessage `json:"incall,omitempty"`
|
|
All bool `json:"all,omitempty"`
|
|
Changed []map[string]interface{} `json:"changed,omitempty"`
|
|
Users []map[string]interface{} `json:"users,omitempty"`
|
|
}
|
|
|
|
type BackendRoomParticipantsRequest struct {
|
|
Changed []map[string]interface{} `json:"changed,omitempty"`
|
|
Users []map[string]interface{} `json:"users,omitempty"`
|
|
}
|
|
|
|
type BackendRoomMessageRequest struct {
|
|
Data json.RawMessage `json:"data,omitempty"`
|
|
}
|
|
|
|
type BackendRoomSwitchToSessionsList []string
|
|
type BackendRoomSwitchToSessionsMap map[string]json.RawMessage
|
|
|
|
type BackendRoomSwitchToMessageRequest struct {
|
|
// Target room id
|
|
RoomId string `json:"roomid"`
|
|
|
|
// Sessions is either a BackendRoomSwitchToSessionsList or a
|
|
// BackendRoomSwitchToSessionsMap.
|
|
// In the map, the key is the session id, the value additional details
|
|
// (or null) for the session. The details will be included in the request
|
|
// to the connected client.
|
|
Sessions json.RawMessage `json:"sessions,omitempty"`
|
|
|
|
// Internal properties
|
|
SessionsList BackendRoomSwitchToSessionsList `json:"sessionslist,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.
|
|
|
|
type BackendClientAuthRequest struct {
|
|
Version string `json:"version"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
|
|
type BackendClientRequest struct {
|
|
json.Marshaler
|
|
json.Unmarshaler
|
|
|
|
Type string `json:"type"`
|
|
|
|
Auth *BackendClientAuthRequest `json:"auth,omitempty"`
|
|
|
|
Room *BackendClientRoomRequest `json:"room,omitempty"`
|
|
|
|
Ping *BackendClientPingRequest `json:"ping,omitempty"`
|
|
|
|
Session *BackendClientSessionRequest `json:"session,omitempty"`
|
|
}
|
|
|
|
func NewBackendClientAuthRequest(params json.RawMessage) *BackendClientRequest {
|
|
return &BackendClientRequest{
|
|
Type: "auth",
|
|
Auth: &BackendClientAuthRequest{
|
|
Version: BackendVersion,
|
|
Params: params,
|
|
},
|
|
}
|
|
}
|
|
|
|
type BackendClientResponse struct {
|
|
json.Marshaler
|
|
json.Unmarshaler
|
|
|
|
Type string `json:"type"`
|
|
|
|
Error *Error `json:"error,omitempty"`
|
|
|
|
Auth *BackendClientAuthResponse `json:"auth,omitempty"`
|
|
|
|
Room *BackendClientRoomResponse `json:"room,omitempty"`
|
|
|
|
Ping *BackendClientRingResponse `json:"ping,omitempty"`
|
|
|
|
Session *BackendClientSessionResponse `json:"session,omitempty"`
|
|
}
|
|
|
|
type BackendClientAuthResponse struct {
|
|
Version string `json:"version"`
|
|
UserId string `json:"userid"`
|
|
User json.RawMessage `json:"user"`
|
|
}
|
|
|
|
type BackendClientRoomRequest struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
Action string `json:"action,omitempty"`
|
|
UserId string `json:"userid"`
|
|
SessionId string `json:"sessionid"`
|
|
|
|
// For Nextcloud Talk with SIP support and for federated sessions.
|
|
ActorId string `json:"actorid,omitempty"`
|
|
ActorType string `json:"actortype,omitempty"`
|
|
InCall int `json:"incall,omitempty"`
|
|
}
|
|
|
|
func (r *BackendClientRoomRequest) UpdateFromSession(s Session) {
|
|
if s.ClientType() == HelloClientTypeFederation {
|
|
// Need to send additional data for requests of federated users.
|
|
if u, err := s.ParsedUserData(); err == nil && len(u) > 0 {
|
|
if actorType, found := getStringMapEntry[string](u, "actorType"); found {
|
|
if actorId, found := getStringMapEntry[string](u, "actorId"); found {
|
|
r.ActorId = actorId
|
|
r.ActorType = actorType
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func NewBackendClientRoomRequest(roomid string, userid string, sessionid string) *BackendClientRequest {
|
|
return &BackendClientRequest{
|
|
Type: "room",
|
|
Room: &BackendClientRoomRequest{
|
|
Version: BackendVersion,
|
|
RoomId: roomid,
|
|
UserId: userid,
|
|
SessionId: sessionid,
|
|
},
|
|
}
|
|
}
|
|
|
|
type BackendClientRoomResponse struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
Properties json.RawMessage `json:"properties"`
|
|
|
|
// Optional information about the Nextcloud Talk session. Can be used for
|
|
// example to define a "userid" for otherwise anonymous users.
|
|
// See "RoomSessionData" for a possible content.
|
|
Session json.RawMessage `json:"session,omitempty"`
|
|
|
|
Permissions *[]Permission `json:"permissions,omitempty"`
|
|
}
|
|
|
|
type RoomSessionData struct {
|
|
UserId string `json:"userid,omitempty"`
|
|
}
|
|
|
|
type BackendPingEntry struct {
|
|
UserId string `json:"userid,omitempty"`
|
|
SessionId string `json:"sessionid"`
|
|
}
|
|
|
|
type BackendClientPingRequest struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
Entries []BackendPingEntry `json:"entries"`
|
|
}
|
|
|
|
func NewBackendClientPingRequest(roomid string, entries []BackendPingEntry) *BackendClientRequest {
|
|
return &BackendClientRequest{
|
|
Type: "ping",
|
|
Ping: &BackendClientPingRequest{
|
|
Version: BackendVersion,
|
|
RoomId: roomid,
|
|
Entries: entries,
|
|
},
|
|
}
|
|
}
|
|
|
|
type BackendClientRingResponse struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
}
|
|
|
|
type BackendClientSessionRequest struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
Action string `json:"action"`
|
|
SessionId string `json:"sessionid"`
|
|
UserId string `json:"userid,omitempty"`
|
|
User json.RawMessage `json:"user,omitempty"`
|
|
}
|
|
|
|
type BackendClientSessionResponse struct {
|
|
Version string `json:"version"`
|
|
RoomId string `json:"roomid"`
|
|
}
|
|
|
|
func NewBackendClientSessionRequest(roomid string, action string, sessionid string, msg *AddSessionInternalClientMessage) *BackendClientRequest {
|
|
request := &BackendClientRequest{
|
|
Type: "session",
|
|
Session: &BackendClientSessionRequest{
|
|
Version: BackendVersion,
|
|
RoomId: roomid,
|
|
Action: action,
|
|
SessionId: sessionid,
|
|
},
|
|
}
|
|
if msg != nil {
|
|
request.Session.UserId = msg.UserId
|
|
request.Session.User = msg.User
|
|
}
|
|
return request
|
|
}
|
|
|
|
type OcsMeta struct {
|
|
Status string `json:"status"`
|
|
StatusCode int `json:"statuscode"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type OcsBody struct {
|
|
Meta OcsMeta `json:"meta"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
type OcsResponse struct {
|
|
json.Marshaler
|
|
json.Unmarshaler
|
|
|
|
Ocs *OcsBody `json:"ocs"`
|
|
}
|
|
|
|
// See https://tools.ietf.org/html/draft-uberti-behave-turn-rest-00
|
|
type TurnCredentials struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
TTL int64 `json:"ttl"`
|
|
URIs []string `json:"uris"`
|
|
}
|
|
|
|
// Information on a backend in the etcd cluster.
|
|
|
|
type BackendInformationEtcd struct {
|
|
parsedUrl *url.URL
|
|
|
|
Url string `json:"url"`
|
|
Secret string `json:"secret"`
|
|
|
|
MaxStreamBitrate int `json:"maxstreambitrate,omitempty"`
|
|
MaxScreenBitrate int `json:"maxscreenbitrate,omitempty"`
|
|
|
|
SessionLimit uint64 `json:"sessionlimit,omitempty"`
|
|
}
|
|
|
|
func (p *BackendInformationEtcd) CheckValid() error {
|
|
if p.Url == "" {
|
|
return fmt.Errorf("url missing")
|
|
}
|
|
if p.Secret == "" {
|
|
return fmt.Errorf("secret missing")
|
|
}
|
|
|
|
parsedUrl, err := url.Parse(p.Url)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid url: %w", err)
|
|
}
|
|
|
|
if strings.Contains(parsedUrl.Host, ":") && hasStandardPort(parsedUrl) {
|
|
parsedUrl.Host = parsedUrl.Hostname()
|
|
p.Url = parsedUrl.String()
|
|
}
|
|
|
|
p.parsedUrl = parsedUrl
|
|
return nil
|
|
}
|