/** * Standalone signaling server for the Nextcloud Spreed app. * Copyright (C) 2017 struktur AG * * @author Joachim Bauch * * @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 . */ 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. ActorId string `json:"actorid,omitempty"` ActorType string `json:"actortype,omitempty"` InCall int `json:"incall,omitempty"` } 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 }