nextcloud-spreed-signaling/backend_configuration.go

386 lines
9.8 KiB
Go

/**
* 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 signaling
import (
"log"
"net/url"
"reflect"
"strings"
"sync"
"github.com/dlintw/goconf"
)
var (
SessionLimitExceeded = NewError("session_limit_exceeded", "Too many sessions connected for this backend.")
)
type Backend struct {
id string
url string
secret []byte
compat bool
maxStreamBitrate int
maxScreenBitrate int
sessionLimit uint64
sessionsLock sync.Mutex
sessions map[string]bool
}
func (b *Backend) Id() string {
return b.id
}
func (b *Backend) Secret() []byte {
return b.secret
}
func (b *Backend) IsCompat() bool {
return b.compat
}
func (b *Backend) AddSession(session Session) error {
if session.ClientType() == HelloClientTypeInternal || session.ClientType() == HelloClientTypeVirtual {
// Internal and virtual sessions are not counting to the limit.
return nil
}
if b.sessionLimit == 0 {
// Not limited
return nil
}
b.sessionsLock.Lock()
defer b.sessionsLock.Unlock()
if b.sessions == nil {
b.sessions = make(map[string]bool)
} else if uint64(len(b.sessions)) >= b.sessionLimit {
return SessionLimitExceeded
}
b.sessions[session.PublicId()] = true
return nil
}
func (b *Backend) RemoveSession(session Session) {
b.sessionsLock.Lock()
defer b.sessionsLock.Unlock()
delete(b.sessions, session.PublicId())
}
type BackendConfiguration struct {
backends map[string][]*Backend
// Deprecated
allowAll bool
commonSecret []byte
compatBackend *Backend
}
func NewBackendConfiguration(config *goconf.ConfigFile) (*BackendConfiguration, error) {
allowAll, _ := config.GetBool("backend", "allowall")
commonSecret, _ := config.GetString("backend", "secret")
sessionLimit, err := config.GetInt("backend", "sessionlimit")
if err != nil || sessionLimit < 0 {
sessionLimit = 0
}
backends := make(map[string][]*Backend)
var compatBackend *Backend
if allowAll {
log.Println("WARNING: All backend hostnames are allowed, only use for development!")
compatBackend = &Backend{
id: "compat",
secret: []byte(commonSecret),
compat: true,
sessionLimit: uint64(sessionLimit),
}
if sessionLimit > 0 {
log.Printf("Allow a maximum of %d sessions", sessionLimit)
}
} else if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
for host, configuredBackends := range getConfiguredHosts(backendIds, config) {
backends[host] = append(backends[host], configuredBackends...)
for _, be := range configuredBackends {
log.Printf("Backend %s added for %s", be.id, be.url)
}
}
} else if allowedUrls, _ := config.GetString("backend", "allowed"); allowedUrls != "" {
// Old-style configuration, only hosts are configured and are using a common secret.
allowMap := make(map[string]bool)
for _, u := range strings.Split(allowedUrls, ",") {
u = strings.TrimSpace(u)
if idx := strings.IndexByte(u, '/'); idx != -1 {
log.Printf("WARNING: Removing path from allowed hostname \"%s\", check your configuration!", u)
u = u[:idx]
}
if u != "" {
allowMap[strings.ToLower(u)] = true
}
}
if len(allowMap) == 0 {
log.Println("WARNING: No backend hostnames are allowed, check your configuration!")
} else {
compatBackend = &Backend{
id: "compat",
secret: []byte(commonSecret),
compat: true,
sessionLimit: uint64(sessionLimit),
}
hosts := make([]string, 0, len(allowMap))
for host := range allowMap {
hosts = append(hosts, host)
backends[host] = []*Backend{compatBackend}
}
if len(hosts) > 1 {
log.Println("WARNING: Using deprecated backend configuration. Please migrate the \"allowed\" setting to the new \"backends\" configuration.")
}
log.Printf("Allowed backend hostnames: %s\n", hosts)
if sessionLimit > 0 {
log.Printf("Allow a maximum of %d sessions", sessionLimit)
}
}
}
return &BackendConfiguration{
backends: backends,
allowAll: allowAll,
commonSecret: []byte(commonSecret),
compatBackend: compatBackend,
}, nil
}
func (b *BackendConfiguration) RemoveBackendsForHost(host string) {
if oldBackends := b.backends[host]; len(oldBackends) > 0 {
for _, backend := range oldBackends {
log.Printf("Backend %s removed for %s", backend.id, backend.url)
}
}
delete(b.backends, host)
}
func (b *BackendConfiguration) UpsertHost(host string, backends []*Backend) {
for existingIndex, existingBackend := range b.backends[host] {
found := false
index := 0
for _, newBackend := range backends {
if reflect.DeepEqual(existingBackend, newBackend) { // otherwise we could manually compare the struct members here
found = true
backends = append(backends[:index], backends[index+1:]...)
break
} else if newBackend.id == existingBackend.id {
found = true
b.backends[host][existingIndex] = newBackend
backends = append(backends[:index], backends[index+1:]...)
log.Printf("Backend %s updated for %s", newBackend.id, newBackend.url)
break
}
index++
}
if !found {
removed := b.backends[host][existingIndex]
log.Printf("Backend %s removed for %s", removed.id, removed.url)
b.backends[host] = append(b.backends[host][:existingIndex], b.backends[host][existingIndex+1:]...)
}
}
b.backends[host] = append(b.backends[host], backends...)
for _, added := range backends {
log.Printf("Backend %s added for %s", added.id, added.url)
}
}
func getConfiguredBackendIDs(backendIds string) (ids []string) {
seen := make(map[string]bool)
for _, id := range strings.Split(backendIds, ",") {
id = strings.TrimSpace(id)
if id == "" {
continue
}
if seen[id] {
continue
}
ids = append(ids, id)
seen[id] = true
}
return ids
}
func getConfiguredHosts(backendIds string, config *goconf.ConfigFile) (hosts map[string][]*Backend) {
hosts = make(map[string][]*Backend)
for _, id := range getConfiguredBackendIDs(backendIds) {
u, _ := config.GetString(id, "url")
if u == "" {
log.Printf("Backend %s is missing or incomplete, skipping", id)
continue
}
if u[len(u)-1] != '/' {
u += "/"
}
parsed, err := url.Parse(u)
if err != nil {
log.Printf("Backend %s has an invalid url %s configured (%s), skipping", id, u, err)
continue
}
if strings.Contains(parsed.Host, ":") && hasStandardPort(parsed) {
parsed.Host = parsed.Hostname()
u = parsed.String()
}
secret, _ := config.GetString(id, "secret")
if u == "" || secret == "" {
log.Printf("Backend %s is missing or incomplete, skipping", id)
continue
}
sessionLimit, err := config.GetInt(id, "sessionlimit")
if err != nil || sessionLimit < 0 {
sessionLimit = 0
}
if sessionLimit > 0 {
log.Printf("Backend %s allows a maximum of %d sessions", id, sessionLimit)
}
maxStreamBitrate, err := config.GetInt(id, "maxstreambitrate")
if err != nil || maxStreamBitrate < 0 {
maxStreamBitrate = 0
}
maxScreenBitrate, err := config.GetInt(id, "maxscreenbitrate")
if err != nil || maxScreenBitrate < 0 {
maxScreenBitrate = 0
}
hosts[parsed.Host] = append(hosts[parsed.Host], &Backend{
id: id,
url: u,
secret: []byte(secret),
maxStreamBitrate: maxStreamBitrate,
maxScreenBitrate: maxScreenBitrate,
sessionLimit: uint64(sessionLimit),
})
}
return hosts
}
func (b *BackendConfiguration) Reload(config *goconf.ConfigFile) {
if b.compatBackend != nil {
log.Println("Old-style configuration active, reload is not supported")
return
}
if backendIds, _ := config.GetString("backend", "backends"); backendIds != "" {
configuredHosts := getConfiguredHosts(backendIds, config)
// remove backends that are no longer configured
for hostname := range b.backends {
if _, ok := configuredHosts[hostname]; !ok {
b.RemoveBackendsForHost(hostname)
}
}
// rewrite backends adding newly configured ones and rewriting existing ones
for hostname, configuredBackends := range configuredHosts {
b.UpsertHost(hostname, configuredBackends)
}
}
}
func (b *BackendConfiguration) GetCompatBackend() *Backend {
return b.compatBackend
}
func (b *BackendConfiguration) GetBackend(u *url.URL) *Backend {
if strings.Contains(u.Host, ":") && hasStandardPort(u) {
u.Host = u.Hostname()
}
entries, found := b.backends[u.Host]
if !found {
if b.allowAll {
return b.compatBackend
}
return nil
}
s := u.String()
if s[len(s)-1] != '/' {
s += "/"
}
for _, entry := range entries {
if entry.url == "" {
// Old-style configuration, only hosts are configured.
return entry
} else if strings.HasPrefix(s, entry.url) {
return entry
}
}
return nil
}
func (b *BackendConfiguration) GetBackends() []*Backend {
var result []*Backend
for _, entries := range b.backends {
result = append(result, entries...)
}
return result
}
func (b *BackendConfiguration) IsUrlAllowed(u *url.URL) bool {
if u == nil {
// Reject all invalid URLs.
return false
}
backend := b.GetBackend(u)
return backend != nil
}
func (b *BackendConfiguration) GetSecret(u *url.URL) []byte {
if u == nil {
// Reject all invalid URLs.
return nil
}
entry := b.GetBackend(u)
if entry == nil {
return nil
}
return entry.secret
}