|
|
@ -0,0 +1,962 @@ |
|
|
|
/** |
|
|
|
* Standalone signaling server for the Nextcloud Spreed app. |
|
|
|
* Copyright (C) 2020 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 main |
|
|
|
|
|
|
|
import ( |
|
|
|
"crypto/rsa" |
|
|
|
"encoding/json" |
|
|
|
"fmt" |
|
|
|
"io/ioutil" |
|
|
|
"log" |
|
|
|
"net" |
|
|
|
"net/http" |
|
|
|
"net/http/pprof" |
|
|
|
"os" |
|
|
|
"os/signal" |
|
|
|
runtimepprof "runtime/pprof" |
|
|
|
"sort" |
|
|
|
"strings" |
|
|
|
"sync" |
|
|
|
"sync/atomic" |
|
|
|
"time" |
|
|
|
|
|
|
|
"github.com/dlintw/goconf" |
|
|
|
"github.com/google/uuid" |
|
|
|
"github.com/gorilla/mux" |
|
|
|
"github.com/gorilla/securecookie" |
|
|
|
"github.com/gorilla/websocket" |
|
|
|
|
|
|
|
"golang.org/x/net/context" |
|
|
|
|
|
|
|
"gopkg.in/dgrijalva/jwt-go.v3" |
|
|
|
|
|
|
|
"signaling" |
|
|
|
) |
|
|
|
|
|
|
|
const ( |
|
|
|
// Buffer sizes when reading/writing websocket connections.
|
|
|
|
websocketReadBufferSize = 4096 |
|
|
|
websocketWriteBufferSize = 4096 |
|
|
|
|
|
|
|
initialMcuRetry = time.Second |
|
|
|
maxMcuRetry = time.Second * 16 |
|
|
|
|
|
|
|
updateLoadInterval = time.Second |
|
|
|
expireSessionsInterval = 10 * time.Second |
|
|
|
|
|
|
|
// Maximum age a token may have to prevent reuse of old tokens.
|
|
|
|
maxTokenAge = 5 * time.Minute |
|
|
|
) |
|
|
|
|
|
|
|
type ContextKey string |
|
|
|
|
|
|
|
var ( |
|
|
|
ContextKeySession = ContextKey("session") |
|
|
|
|
|
|
|
TimeoutCreatingPublisher = signaling.NewError("timeout", "Timeout creating publisher.") |
|
|
|
TimeoutCreatingSubscriber = signaling.NewError("timeout", "Timeout creating subscriber.") |
|
|
|
TokenAuthFailed = signaling.NewError("auth_failed", "The token could not be authenticated.") |
|
|
|
TokenExpired = signaling.NewError("token_expired", "The token is expired.") |
|
|
|
UnknownClient = signaling.NewError("unknown_client", "Unknown client id given.") |
|
|
|
UnsupportedCommand = signaling.NewError("bad_request", "Unsupported command received.") |
|
|
|
UnsupportedMessage = signaling.NewError("bad_request", "Unsupported message received.") |
|
|
|
UnsupportedPayload = signaling.NewError("unsupported_payload", "Unsupported payload type.") |
|
|
|
) |
|
|
|
|
|
|
|
type ProxyServer struct { |
|
|
|
version string |
|
|
|
country string |
|
|
|
|
|
|
|
url string |
|
|
|
nats signaling.NatsClient |
|
|
|
mcu signaling.Mcu |
|
|
|
stopped uint32 |
|
|
|
load int64 |
|
|
|
|
|
|
|
upgrader websocket.Upgrader |
|
|
|
|
|
|
|
tokenKeys atomic.Value |
|
|
|
statsAllowedIps map[string]bool |
|
|
|
|
|
|
|
sid uint64 |
|
|
|
cookie *securecookie.SecureCookie |
|
|
|
sessions map[uint64]*ProxySession |
|
|
|
sessionsLock sync.RWMutex |
|
|
|
|
|
|
|
clients map[string]signaling.McuClient |
|
|
|
clientIds map[string]string |
|
|
|
clientsLock sync.RWMutex |
|
|
|
} |
|
|
|
|
|
|
|
func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile, nats signaling.NatsClient) (*ProxyServer, error) { |
|
|
|
hashKey, _ := config.GetString("sessions", "hashkey") |
|
|
|
switch len(hashKey) { |
|
|
|
case 32: |
|
|
|
case 64: |
|
|
|
default: |
|
|
|
log.Printf("WARNING: The sessions hash key should be 32 or 64 bytes but is %d bytes", len(hashKey)) |
|
|
|
} |
|
|
|
|
|
|
|
blockKey, _ := config.GetString("sessions", "blockkey") |
|
|
|
blockBytes := []byte(blockKey) |
|
|
|
switch len(blockKey) { |
|
|
|
case 0: |
|
|
|
blockBytes = nil |
|
|
|
case 16: |
|
|
|
case 24: |
|
|
|
case 32: |
|
|
|
default: |
|
|
|
return nil, fmt.Errorf("The sessions block key must be 16, 24 or 32 bytes but is %d bytes", len(blockKey)) |
|
|
|
} |
|
|
|
|
|
|
|
tokenKeys := make(map[string]*rsa.PublicKey) |
|
|
|
options, _ := config.GetOptions("tokens") |
|
|
|
for _, id := range options { |
|
|
|
filename, _ := config.GetString("tokens", id) |
|
|
|
if filename == "" { |
|
|
|
return nil, fmt.Errorf("No filename given for token %s", id) |
|
|
|
} |
|
|
|
|
|
|
|
keyData, err := ioutil.ReadFile(filename) |
|
|
|
if err != nil { |
|
|
|
return nil, fmt.Errorf("Could not read public key from %s: %s", filename, err) |
|
|
|
} |
|
|
|
key, err := jwt.ParseRSAPublicKeyFromPEM(keyData) |
|
|
|
if err != nil { |
|
|
|
return nil, fmt.Errorf("Could not parse public key from %s: %s", filename, err) |
|
|
|
} |
|
|
|
|
|
|
|
tokenKeys[id] = key |
|
|
|
} |
|
|
|
|
|
|
|
var keyIds []string |
|
|
|
for k, _ := range tokenKeys { |
|
|
|
keyIds = append(keyIds, k) |
|
|
|
} |
|
|
|
sort.Strings(keyIds) |
|
|
|
log.Printf("Enabled token keys: %v", keyIds) |
|
|
|
|
|
|
|
statsAllowed, _ := config.GetString("stats", "allowed_ips") |
|
|
|
var statsAllowedIps map[string]bool |
|
|
|
if statsAllowed == "" { |
|
|
|
log.Printf("No IPs configured for the stats endpoint, only allowing access from 127.0.0.1") |
|
|
|
statsAllowedIps = map[string]bool{ |
|
|
|
"127.0.0.1": true, |
|
|
|
} |
|
|
|
} else { |
|
|
|
log.Printf("Only allowing access to the stats endpoing from %s", statsAllowed) |
|
|
|
statsAllowedIps = make(map[string]bool) |
|
|
|
for _, ip := range strings.Split(statsAllowed, ",") { |
|
|
|
ip = strings.TrimSpace(ip) |
|
|
|
if ip != "" { |
|
|
|
statsAllowedIps[ip] = true |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
country, _ := config.GetString("app", "country") |
|
|
|
if country != "" { |
|
|
|
log.Printf("Sending %s as country information", country) |
|
|
|
} else { |
|
|
|
log.Printf("Not sending country information") |
|
|
|
} |
|
|
|
|
|
|
|
result := &ProxyServer{ |
|
|
|
version: version, |
|
|
|
country: country, |
|
|
|
|
|
|
|
nats: nats, |
|
|
|
|
|
|
|
upgrader: websocket.Upgrader{ |
|
|
|
ReadBufferSize: websocketReadBufferSize, |
|
|
|
WriteBufferSize: websocketWriteBufferSize, |
|
|
|
}, |
|
|
|
|
|
|
|
statsAllowedIps: statsAllowedIps, |
|
|
|
|
|
|
|
cookie: securecookie.New([]byte(hashKey), blockBytes).MaxAge(0), |
|
|
|
sessions: make(map[uint64]*ProxySession), |
|
|
|
|
|
|
|
clients: make(map[string]signaling.McuClient), |
|
|
|
clientIds: make(map[string]string), |
|
|
|
} |
|
|
|
|
|
|
|
result.setTokenKeys(tokenKeys) |
|
|
|
result.upgrader.CheckOrigin = result.checkOrigin |
|
|
|
|
|
|
|
if debug, _ := config.GetBool("app", "debug"); debug { |
|
|
|
log.Println("Installing debug handlers in \"/debug/pprof\"") |
|
|
|
r.Handle("/debug/pprof/", http.HandlerFunc(pprof.Index)) |
|
|
|
r.Handle("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline)) |
|
|
|
r.Handle("/debug/pprof/profile", http.HandlerFunc(pprof.Profile)) |
|
|
|
r.Handle("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol)) |
|
|
|
r.Handle("/debug/pprof/trace", http.HandlerFunc(pprof.Trace)) |
|
|
|
for _, profile := range runtimepprof.Profiles() { |
|
|
|
name := profile.Name() |
|
|
|
r.Handle("/debug/pprof/"+name, pprof.Handler(name)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
r.HandleFunc("/proxy", result.setCommonHeaders(result.proxyHandler)).Methods("GET") |
|
|
|
r.HandleFunc("/stats", result.setCommonHeaders(result.validateStatsRequest(result.statsHandler))).Methods("GET") |
|
|
|
return result, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) checkOrigin(r *http.Request) bool { |
|
|
|
// We allow any Origin to connect to the service.
|
|
|
|
return true |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) setTokenKeys(keys map[string]*rsa.PublicKey) { |
|
|
|
s.tokenKeys.Store(keys) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) getTokenKeys() map[string]*rsa.PublicKey { |
|
|
|
return s.tokenKeys.Load().(map[string]*rsa.PublicKey) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) Start(config *goconf.ConfigFile) error { |
|
|
|
s.url, _ = config.GetString("mcu", "url") |
|
|
|
if s.url == "" { |
|
|
|
return fmt.Errorf("No MCU server url configured") |
|
|
|
} |
|
|
|
|
|
|
|
mcuType, _ := config.GetString("mcu", "type") |
|
|
|
if mcuType == "" { |
|
|
|
mcuType = signaling.McuTypeDefault |
|
|
|
} |
|
|
|
|
|
|
|
interrupt := make(chan os.Signal, 1) |
|
|
|
signal.Notify(interrupt, os.Interrupt) |
|
|
|
defer signal.Stop(interrupt) |
|
|
|
|
|
|
|
var err error |
|
|
|
var mcu signaling.Mcu |
|
|
|
mcuRetry := initialMcuRetry |
|
|
|
mcuRetryTimer := time.NewTimer(mcuRetry) |
|
|
|
for { |
|
|
|
switch mcuType { |
|
|
|
case signaling.McuTypeJanus: |
|
|
|
mcu, err = signaling.NewMcuJanus(s.url, config, s.nats) |
|
|
|
default: |
|
|
|
return fmt.Errorf("Unsupported MCU type: %s", mcuType) |
|
|
|
} |
|
|
|
if err == nil { |
|
|
|
mcu.SetOnConnected(s.onMcuConnected) |
|
|
|
mcu.SetOnDisconnected(s.onMcuDisconnected) |
|
|
|
err = mcu.Start() |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not create %s MCU at %s: %s", mcuType, s.url, err) |
|
|
|
} |
|
|
|
} |
|
|
|
if err == nil { |
|
|
|
break |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Could not initialize %s MCU at %s (%s) will retry in %s", mcuType, s.url, err, mcuRetry) |
|
|
|
mcuRetryTimer.Reset(mcuRetry) |
|
|
|
select { |
|
|
|
case <-interrupt: |
|
|
|
return fmt.Errorf("Cancelled") |
|
|
|
case <-mcuRetryTimer.C: |
|
|
|
// Retry connection
|
|
|
|
mcuRetry = mcuRetry * 2 |
|
|
|
if mcuRetry > maxMcuRetry { |
|
|
|
mcuRetry = maxMcuRetry |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
s.mcu = mcu |
|
|
|
|
|
|
|
go s.run() |
|
|
|
|
|
|
|
return nil |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) run() { |
|
|
|
updateLoadTicker := time.NewTicker(updateLoadInterval) |
|
|
|
expireSessionsTicker := time.NewTicker(expireSessionsInterval) |
|
|
|
loop: |
|
|
|
for { |
|
|
|
select { |
|
|
|
case <-updateLoadTicker.C: |
|
|
|
if atomic.LoadUint32(&s.stopped) != 0 { |
|
|
|
break loop |
|
|
|
} |
|
|
|
s.updateLoad() |
|
|
|
case <-expireSessionsTicker.C: |
|
|
|
if atomic.LoadUint32(&s.stopped) != 0 { |
|
|
|
break loop |
|
|
|
} |
|
|
|
s.expireSessions() |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) updateLoad() { |
|
|
|
// TODO: Take maximum bandwidth of clients into account when calculating
|
|
|
|
// load (screensharing requires more than regular audio/video).
|
|
|
|
load := s.GetClientCount() |
|
|
|
if load == atomic.LoadInt64(&s.load) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
atomic.StoreInt64(&s.load, load) |
|
|
|
msg := &signaling.ProxyServerMessage{ |
|
|
|
Type: "event", |
|
|
|
Event: &signaling.EventProxyServerMessage{ |
|
|
|
Type: "update-load", |
|
|
|
Load: load, |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
s.IterateSessions(func(session *ProxySession) { |
|
|
|
session.sendMessage(msg) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) getExpiredSessions() []*ProxySession { |
|
|
|
var expired []*ProxySession |
|
|
|
s.IterateSessions(func(session *ProxySession) { |
|
|
|
if session.IsExpired() { |
|
|
|
expired = append(expired, session) |
|
|
|
} |
|
|
|
}) |
|
|
|
return expired |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) expireSessions() { |
|
|
|
expired := s.getExpiredSessions() |
|
|
|
if len(expired) == 0 { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
s.sessionsLock.Lock() |
|
|
|
defer s.sessionsLock.Unlock() |
|
|
|
for _, session := range expired { |
|
|
|
if !session.IsExpired() { |
|
|
|
// Session was used while waiting for the lock.
|
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Delete expired session %s", session.PublicId()) |
|
|
|
s.deleteSessionLocked(session.Sid()) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) Stop() { |
|
|
|
if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) { |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
s.mcu.Stop() |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) Reload(config *goconf.ConfigFile) { |
|
|
|
tokenKeys := make(map[string]*rsa.PublicKey) |
|
|
|
options, _ := config.GetOptions("tokens") |
|
|
|
for _, id := range options { |
|
|
|
filename, _ := config.GetString("tokens", id) |
|
|
|
if filename == "" { |
|
|
|
log.Printf("No filename given for token %s, ignoring", id) |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
keyData, err := ioutil.ReadFile(filename) |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not read public key from %s, ignoring: %s", filename, err) |
|
|
|
continue |
|
|
|
} |
|
|
|
key, err := jwt.ParseRSAPublicKeyFromPEM(keyData) |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not parse public key from %s, ignoring: %s", filename, err) |
|
|
|
continue |
|
|
|
} |
|
|
|
|
|
|
|
tokenKeys[id] = key |
|
|
|
} |
|
|
|
|
|
|
|
if len(tokenKeys) == 0 { |
|
|
|
log.Printf("No token keys loaded") |
|
|
|
} else { |
|
|
|
var keyIds []string |
|
|
|
for k, _ := range tokenKeys { |
|
|
|
keyIds = append(keyIds, k) |
|
|
|
} |
|
|
|
sort.Strings(keyIds) |
|
|
|
log.Printf("Enabled token keys: %v", keyIds) |
|
|
|
} |
|
|
|
s.setTokenKeys(tokenKeys) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) setCommonHeaders(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { |
|
|
|
return func(w http.ResponseWriter, r *http.Request) { |
|
|
|
w.Header().Set("Server", "nextcloud-spreed-signaling-proxy/"+s.version) |
|
|
|
f(w, r) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func getRealUserIP(r *http.Request) string { |
|
|
|
// Note this function assumes it is running behind a trusted proxy, so
|
|
|
|
// the headers can be trusted.
|
|
|
|
if ip := r.Header.Get("X-Real-IP"); ip != "" { |
|
|
|
return ip |
|
|
|
} |
|
|
|
|
|
|
|
if ip := r.Header.Get("X-Forwarded-For"); ip != "" { |
|
|
|
// Result could be a list "clientip, proxy1, proxy2", so only use first element.
|
|
|
|
if pos := strings.Index(ip, ","); pos >= 0 { |
|
|
|
ip = strings.TrimSpace(ip[:pos]) |
|
|
|
} |
|
|
|
return ip |
|
|
|
} |
|
|
|
|
|
|
|
return r.RemoteAddr |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) proxyHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
addr := getRealUserIP(r) |
|
|
|
conn, err := s.upgrader.Upgrade(w, r, nil) |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not upgrade request from %s: %s", addr, err) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
client, err := NewProxyClient(s, conn, addr) |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not create client for %s: %s", addr, err) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
client.OnClosed = s.clientClosed |
|
|
|
client.OnMessageReceived = func(c *signaling.Client, data []byte) { |
|
|
|
s.processMessage(client, data) |
|
|
|
} |
|
|
|
client.OnRTTReceived = func(c *signaling.Client, rtt time.Duration) { |
|
|
|
if session := client.GetSession(); session != nil { |
|
|
|
session.MarkUsed() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
go client.WritePump() |
|
|
|
go client.ReadPump() |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) clientClosed(client *signaling.Client) { |
|
|
|
log.Printf("Connection from %s closed", client.RemoteAddr()) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) onMcuConnected() { |
|
|
|
log.Printf("Connection to %s established", s.url) |
|
|
|
msg := &signaling.ProxyServerMessage{ |
|
|
|
Type: "event", |
|
|
|
Event: &signaling.EventProxyServerMessage{ |
|
|
|
Type: "backend-connected", |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
s.IterateSessions(func(session *ProxySession) { |
|
|
|
session.sendMessage(msg) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) onMcuDisconnected() { |
|
|
|
if atomic.LoadUint32(&s.stopped) != 0 { |
|
|
|
// Shutting down, no need to notify.
|
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Connection to %s lost", s.url) |
|
|
|
msg := &signaling.ProxyServerMessage{ |
|
|
|
Type: "event", |
|
|
|
Event: &signaling.EventProxyServerMessage{ |
|
|
|
Type: "backend-disconnected", |
|
|
|
}, |
|
|
|
} |
|
|
|
|
|
|
|
s.IterateSessions(func(session *ProxySession) { |
|
|
|
session.sendMessage(msg) |
|
|
|
session.NotifyDisconnected() |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) sendCurrentLoad(session *ProxySession) { |
|
|
|
msg := &signaling.ProxyServerMessage{ |
|
|
|
Type: "event", |
|
|
|
Event: &signaling.EventProxyServerMessage{ |
|
|
|
Type: "update-load", |
|
|
|
Load: atomic.LoadInt64(&s.load), |
|
|
|
}, |
|
|
|
} |
|
|
|
session.sendMessage(msg) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) processMessage(client *ProxyClient, data []byte) { |
|
|
|
if proxyDebugMessages { |
|
|
|
log.Printf("Message: %s", string(data)) |
|
|
|
} |
|
|
|
var message signaling.ProxyClientMessage |
|
|
|
if err := message.UnmarshalJSON(data); err != nil { |
|
|
|
if session := client.GetSession(); session != nil { |
|
|
|
log.Printf("Error decoding message from client %s: %v", session.PublicId(), err) |
|
|
|
} else { |
|
|
|
log.Printf("Error decoding message from %s: %v", client.RemoteAddr(), err) |
|
|
|
} |
|
|
|
client.SendError(signaling.InvalidFormat) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if err := message.CheckValid(); err != nil { |
|
|
|
if session := client.GetSession(); session != nil { |
|
|
|
log.Printf("Invalid message %+v from client %s: %v", message, session.PublicId(), err) |
|
|
|
} else { |
|
|
|
log.Printf("Invalid message %+v from %s: %v", message, client.RemoteAddr(), err) |
|
|
|
} |
|
|
|
client.SendMessage(message.NewErrorServerMessage(signaling.InvalidFormat)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
session := client.GetSession() |
|
|
|
if session == nil { |
|
|
|
if message.Type != "hello" { |
|
|
|
client.SendMessage(message.NewErrorServerMessage(signaling.HelloExpected)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
var session *ProxySession |
|
|
|
if resumeId := message.Hello.ResumeId; resumeId != "" { |
|
|
|
var data signaling.SessionIdData |
|
|
|
if s.cookie.Decode("session", resumeId, &data) == nil { |
|
|
|
session = s.GetSession(data.Sid) |
|
|
|
} |
|
|
|
|
|
|
|
if session == nil { |
|
|
|
client.SendMessage(message.NewErrorServerMessage(signaling.NoSuchSession)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Resumed session %s", session.PublicId()) |
|
|
|
s.sendCurrentLoad(session) |
|
|
|
} else { |
|
|
|
var err error |
|
|
|
if session, err = s.NewSession(message.Hello); err != nil { |
|
|
|
if e, ok := err.(*signaling.Error); ok { |
|
|
|
client.SendMessage(message.NewErrorServerMessage(e)) |
|
|
|
} else { |
|
|
|
client.SendMessage(message.NewWrappedErrorServerMessage(err)) |
|
|
|
} |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
prev := session.SetClient(client) |
|
|
|
if prev != nil { |
|
|
|
msg := &signaling.ProxyServerMessage{ |
|
|
|
Type: "bye", |
|
|
|
Bye: &signaling.ByeProxyServerMessage{ |
|
|
|
Reason: "session_resumed", |
|
|
|
}, |
|
|
|
} |
|
|
|
prev.SendMessage(msg) |
|
|
|
} |
|
|
|
response := &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "hello", |
|
|
|
Hello: &signaling.HelloProxyServerMessage{ |
|
|
|
Version: signaling.HelloVersion, |
|
|
|
SessionId: session.PublicId(), |
|
|
|
Server: &signaling.HelloServerMessageServer{ |
|
|
|
Version: s.version, |
|
|
|
Country: s.country, |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
client.SendMessage(response) |
|
|
|
s.sendCurrentLoad(session) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
ctx := context.WithValue(context.Background(), ContextKeySession, session) |
|
|
|
session.MarkUsed() |
|
|
|
|
|
|
|
switch message.Type { |
|
|
|
case "command": |
|
|
|
s.processCommand(ctx, client, session, &message) |
|
|
|
case "payload": |
|
|
|
s.processPayload(ctx, client, session, &message) |
|
|
|
default: |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnsupportedMessage)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) processCommand(ctx context.Context, client *ProxyClient, session *ProxySession, message *signaling.ProxyClientMessage) { |
|
|
|
cmd := message.Command |
|
|
|
switch cmd.Type { |
|
|
|
case "create-publisher": |
|
|
|
id := uuid.New().String() |
|
|
|
publisher, err := s.mcu.NewPublisher(ctx, session, id, cmd.StreamType) |
|
|
|
if err == context.DeadlineExceeded { |
|
|
|
log.Printf("Timeout while creating %s publisher %s for %s", cmd.StreamType, id, session.PublicId()) |
|
|
|
session.sendMessage(message.NewErrorServerMessage(TimeoutCreatingPublisher)) |
|
|
|
return |
|
|
|
} else if err != nil { |
|
|
|
log.Printf("Error while creating %s publisher %s for %s: %s", cmd.StreamType, id, session.PublicId(), err) |
|
|
|
session.sendMessage(message.NewWrappedErrorServerMessage(err)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Created %s publisher %s as %s", cmd.StreamType, publisher.Id(), id) |
|
|
|
session.StorePublisher(ctx, id, publisher) |
|
|
|
s.StoreClient(id, publisher) |
|
|
|
|
|
|
|
response := &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "command", |
|
|
|
Command: &signaling.CommandProxyServerMessage{ |
|
|
|
Id: id, |
|
|
|
}, |
|
|
|
} |
|
|
|
session.sendMessage(response) |
|
|
|
case "create-subscriber": |
|
|
|
id := uuid.New().String() |
|
|
|
publisherId := cmd.PublisherId |
|
|
|
subscriber, err := s.mcu.NewSubscriber(ctx, session, publisherId, cmd.StreamType) |
|
|
|
if err == context.DeadlineExceeded { |
|
|
|
log.Printf("Timeout while creating %s subscriber on %s for %s", cmd.StreamType, publisherId, session.PublicId()) |
|
|
|
session.sendMessage(message.NewErrorServerMessage(TimeoutCreatingSubscriber)) |
|
|
|
return |
|
|
|
} else if err != nil { |
|
|
|
log.Printf("Error while creating %s subscriber on %s for %s: %s", cmd.StreamType, publisherId, session.PublicId(), err) |
|
|
|
session.sendMessage(message.NewWrappedErrorServerMessage(err)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Created %s subscriber %s as %s", cmd.StreamType, subscriber.Id(), id) |
|
|
|
session.StoreSubscriber(ctx, id, subscriber) |
|
|
|
s.StoreClient(id, subscriber) |
|
|
|
|
|
|
|
response := &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "command", |
|
|
|
Command: &signaling.CommandProxyServerMessage{ |
|
|
|
Id: id, |
|
|
|
}, |
|
|
|
} |
|
|
|
session.sendMessage(response) |
|
|
|
case "delete-publisher": |
|
|
|
client := s.GetClient(cmd.ClientId) |
|
|
|
if client == nil { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if session.DeletePublisher(client) == "" { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
s.DeleteClient(cmd.ClientId, client) |
|
|
|
|
|
|
|
go func() { |
|
|
|
log.Printf("Closing %s publisher %s as %s", client.StreamType(), client.Id(), cmd.ClientId) |
|
|
|
client.Close(context.Background()) |
|
|
|
}() |
|
|
|
|
|
|
|
response := &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "command", |
|
|
|
Command: &signaling.CommandProxyServerMessage{ |
|
|
|
Id: cmd.ClientId, |
|
|
|
}, |
|
|
|
} |
|
|
|
session.sendMessage(response) |
|
|
|
case "delete-subscriber": |
|
|
|
client := s.GetClient(cmd.ClientId) |
|
|
|
if client == nil { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
subscriber, ok := client.(signaling.McuSubscriber) |
|
|
|
if !ok { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
if session.DeleteSubscriber(subscriber) == "" { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
s.DeleteClient(cmd.ClientId, client) |
|
|
|
|
|
|
|
go func() { |
|
|
|
log.Printf("Closing %s subscriber %s as %s", client.StreamType(), client.Id(), cmd.ClientId) |
|
|
|
client.Close(context.Background()) |
|
|
|
}() |
|
|
|
|
|
|
|
response := &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "command", |
|
|
|
Command: &signaling.CommandProxyServerMessage{ |
|
|
|
Id: cmd.ClientId, |
|
|
|
}, |
|
|
|
} |
|
|
|
session.sendMessage(response) |
|
|
|
default: |
|
|
|
log.Printf("Unsupported command %+v", message.Command) |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnsupportedCommand)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) processPayload(ctx context.Context, client *ProxyClient, session *ProxySession, message *signaling.ProxyClientMessage) { |
|
|
|
payload := message.Payload |
|
|
|
mcuClient := s.GetClient(payload.ClientId) |
|
|
|
if mcuClient == nil { |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnknownClient)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
var mcuData *signaling.MessageClientMessageData |
|
|
|
switch payload.Type { |
|
|
|
case "offer": |
|
|
|
fallthrough |
|
|
|
case "answer": |
|
|
|
fallthrough |
|
|
|
case "candidate": |
|
|
|
mcuData = &signaling.MessageClientMessageData{ |
|
|
|
Type: payload.Type, |
|
|
|
Payload: payload.Payload, |
|
|
|
} |
|
|
|
case "endOfCandidates": |
|
|
|
// Ignore but confirm, not passed along to Janus anyway.
|
|
|
|
session.sendMessage(&signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "payload", |
|
|
|
Payload: &signaling.PayloadProxyServerMessage{ |
|
|
|
Type: payload.Type, |
|
|
|
ClientId: payload.ClientId, |
|
|
|
}, |
|
|
|
}) |
|
|
|
return |
|
|
|
case "requestoffer": |
|
|
|
fallthrough |
|
|
|
case "sendoffer": |
|
|
|
mcuData = &signaling.MessageClientMessageData{ |
|
|
|
Type: payload.Type, |
|
|
|
} |
|
|
|
default: |
|
|
|
session.sendMessage(message.NewErrorServerMessage(UnsupportedPayload)) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
mcuClient.SendMessage(ctx, nil, mcuData, func(err error, response map[string]interface{}) { |
|
|
|
var responseMsg *signaling.ProxyServerMessage |
|
|
|
if err != nil { |
|
|
|
log.Printf("Error sending %s to %s client %s: %s", mcuData, mcuClient.StreamType(), payload.ClientId, err) |
|
|
|
responseMsg = message.NewWrappedErrorServerMessage(err) |
|
|
|
} else { |
|
|
|
responseMsg = &signaling.ProxyServerMessage{ |
|
|
|
Id: message.Id, |
|
|
|
Type: "payload", |
|
|
|
Payload: &signaling.PayloadProxyServerMessage{ |
|
|
|
Type: payload.Type, |
|
|
|
ClientId: payload.ClientId, |
|
|
|
Payload: response, |
|
|
|
}, |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
session.sendMessage(responseMsg) |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) NewSession(hello *signaling.HelloProxyClientMessage) (*ProxySession, error) { |
|
|
|
if proxyDebugMessages { |
|
|
|
log.Printf("Hello: %+v", hello) |
|
|
|
} |
|
|
|
|
|
|
|
token, err := jwt.ParseWithClaims(hello.Token, &signaling.TokenClaims{}, func(token *jwt.Token) (interface{}, error) { |
|
|
|
// Don't forget to validate the alg is what you expect:
|
|
|
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok { |
|
|
|
log.Printf("Unexpected signing method: %v", token.Header["alg"]) |
|
|
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) |
|
|
|
} |
|
|
|
|
|
|
|
claims, ok := token.Claims.(*signaling.TokenClaims) |
|
|
|
if !ok { |
|
|
|
log.Printf("Unsupported claims type: %+v", token.Claims) |
|
|
|
return nil, fmt.Errorf("Unsupported claims type") |
|
|
|
} |
|
|
|
|
|
|
|
tokenKeys := s.getTokenKeys() |
|
|
|
publicKey := tokenKeys[claims.Issuer] |
|
|
|
if publicKey == nil { |
|
|
|
log.Printf("Issuer %s is not supported", claims.Issuer) |
|
|
|
return nil, fmt.Errorf("No key found for issuer") |
|
|
|
} |
|
|
|
return publicKey, nil |
|
|
|
}) |
|
|
|
if err != nil { |
|
|
|
return nil, TokenAuthFailed |
|
|
|
} |
|
|
|
|
|
|
|
claims, ok := token.Claims.(*signaling.TokenClaims) |
|
|
|
if !ok || !token.Valid { |
|
|
|
return nil, TokenAuthFailed |
|
|
|
} |
|
|
|
|
|
|
|
minIssuedAt := time.Now().Add(-maxTokenAge) |
|
|
|
if issuedAt := time.Unix(claims.IssuedAt, 0); issuedAt.Before(minIssuedAt) { |
|
|
|
return nil, TokenExpired |
|
|
|
} |
|
|
|
|
|
|
|
sid := atomic.AddUint64(&s.sid, 1) |
|
|
|
for sid == 0 { |
|
|
|
sid = atomic.AddUint64(&s.sid, 1) |
|
|
|
} |
|
|
|
|
|
|
|
sessionIdData := &signaling.SessionIdData{ |
|
|
|
Sid: sid, |
|
|
|
Created: time.Now(), |
|
|
|
} |
|
|
|
|
|
|
|
encoded, err := s.cookie.Encode("session", sessionIdData) |
|
|
|
if err != nil { |
|
|
|
return nil, err |
|
|
|
} |
|
|
|
|
|
|
|
log.Printf("Created session %s for %+v", encoded, claims) |
|
|
|
session := NewProxySession(s, sid, encoded) |
|
|
|
s.StoreSession(sid, session) |
|
|
|
return session, nil |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) StoreSession(id uint64, session *ProxySession) { |
|
|
|
s.sessionsLock.Lock() |
|
|
|
defer s.sessionsLock.Unlock() |
|
|
|
s.sessions[id] = session |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) GetSession(id uint64) *ProxySession { |
|
|
|
s.sessionsLock.RLock() |
|
|
|
defer s.sessionsLock.RUnlock() |
|
|
|
return s.sessions[id] |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) GetSessionsCount() int64 { |
|
|
|
s.sessionsLock.RLock() |
|
|
|
defer s.sessionsLock.RUnlock() |
|
|
|
return int64(len(s.sessions)) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) IterateSessions(f func(*ProxySession)) { |
|
|
|
s.sessionsLock.RLock() |
|
|
|
defer s.sessionsLock.RUnlock() |
|
|
|
|
|
|
|
for _, session := range s.sessions { |
|
|
|
f(session) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) DeleteSession(id uint64) { |
|
|
|
s.sessionsLock.Lock() |
|
|
|
defer s.sessionsLock.Unlock() |
|
|
|
s.deleteSessionLocked(id) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) deleteSessionLocked(id uint64) { |
|
|
|
delete(s.sessions, id) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) StoreClient(id string, client signaling.McuClient) { |
|
|
|
s.clientsLock.Lock() |
|
|
|
defer s.clientsLock.Unlock() |
|
|
|
s.clients[id] = client |
|
|
|
s.clientIds[client.Id()] = id |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) DeleteClient(id string, client signaling.McuClient) { |
|
|
|
s.clientsLock.Lock() |
|
|
|
defer s.clientsLock.Unlock() |
|
|
|
delete(s.clients, id) |
|
|
|
delete(s.clientIds, client.Id()) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) GetClientCount() int64 { |
|
|
|
s.clientsLock.RLock() |
|
|
|
defer s.clientsLock.RUnlock() |
|
|
|
return int64(len(s.clients)) |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) GetClient(id string) signaling.McuClient { |
|
|
|
s.clientsLock.RLock() |
|
|
|
defer s.clientsLock.RUnlock() |
|
|
|
return s.clients[id] |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) GetClientId(client signaling.McuClient) string { |
|
|
|
s.clientsLock.RLock() |
|
|
|
defer s.clientsLock.RUnlock() |
|
|
|
return s.clientIds[client.Id()] |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) getStats() map[string]interface{} { |
|
|
|
result := map[string]interface{}{ |
|
|
|
"sessions": s.GetSessionsCount(), |
|
|
|
"mcu": s.mcu.GetStats(), |
|
|
|
} |
|
|
|
return result |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { |
|
|
|
return func(w http.ResponseWriter, r *http.Request) { |
|
|
|
addr := getRealUserIP(r) |
|
|
|
if strings.Contains(addr, ":") { |
|
|
|
if host, _, err := net.SplitHostPort(addr); err == nil { |
|
|
|
addr = host |
|
|
|
} |
|
|
|
} |
|
|
|
if !s.statsAllowedIps[addr] { |
|
|
|
http.Error(w, "Authentication check failed", http.StatusForbidden) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
f(w, r) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
func (s *ProxyServer) statsHandler(w http.ResponseWriter, r *http.Request) { |
|
|
|
stats := s.getStats() |
|
|
|
statsData, err := json.MarshalIndent(stats, "", " ") |
|
|
|
if err != nil { |
|
|
|
log.Printf("Could not serialize stats %+v: %s", stats, err) |
|
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError) |
|
|
|
return |
|
|
|
} |
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8") |
|
|
|
w.Header().Set("X-Content-Type-Options", "nosniff") |
|
|
|
w.WriteHeader(http.StatusOK) |
|
|
|
w.Write(statsData) |
|
|
|
} |