diff --git a/proxy.conf.in b/proxy.conf.in index 4770eda4..9aab79be 100644 --- a/proxy.conf.in +++ b/proxy.conf.in @@ -12,6 +12,13 @@ # servers to determine the closest proxy for publishers. #country = DE +# Type of token configuration for signaling servers allowed to connect, see +# below for details. Defaults to "static". +# +# Possible values: +# - static: A mapping of token id -> public key is configured below. +token_type = static + [sessions] # Secret value used to generate checksums of sessions. This should be a random # string of 32 or 64 bytes. @@ -30,7 +37,8 @@ blockkey = -encryption-key- #url = nats://localhost:4222 [tokens] -# Mapping of = of signaling servers allowed to connect. +# For token_type "static": Mapping of = of signaling +# servers allowed to connect. #server1 = pubkey1.pem #server2 = pubkey2.pem diff --git a/src/proxy/proxy_server.go b/src/proxy/proxy_server.go index 05c4fa0d..b4e80afc 100644 --- a/src/proxy/proxy_server.go +++ b/src/proxy/proxy_server.go @@ -22,10 +22,8 @@ package main import ( - "crypto/rsa" "encoding/json" "fmt" - "io/ioutil" "log" "net" "net/http" @@ -33,7 +31,6 @@ import ( "os" "os/signal" runtimepprof "runtime/pprof" - "sort" "strings" "sync" "sync/atomic" @@ -98,7 +95,7 @@ type ProxyServer struct { upgrader websocket.Upgrader - tokenKeys atomic.Value + tokens ProxyTokens statsAllowedIps map[string]bool sid uint64 @@ -132,32 +129,22 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile, na 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 tokens ProxyTokens + var err error + tokenType, _ := config.GetString("app", "token_type") + if tokenType == "" { + tokenType = TokenTypeDefault } - var keyIds []string - for k, _ := range tokenKeys { - keyIds = append(keyIds, k) + switch tokenType { + case TokenTypeStatic: + tokens, err = NewProxyTokensStatic(config) + default: + return nil, fmt.Errorf("Unsupported token type configured: %s", tokenType) + } + if err != nil { + return nil, err } - sort.Strings(keyIds) - log.Printf("Enabled token keys: %v", keyIds) statsAllowed, _ := config.GetString("stats", "allowed_ips") var statsAllowedIps map[string]bool @@ -200,6 +187,7 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile, na WriteBufferSize: websocketWriteBufferSize, }, + tokens: tokens, statsAllowedIps: statsAllowedIps, cookie: securecookie.New([]byte(hashKey), blockBytes).MaxAge(0), @@ -209,7 +197,6 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile, na clientIds: make(map[string]string), } - result.setTokenKeys(tokenKeys) result.upgrader.CheckOrigin = result.checkOrigin if debug, _ := config.GetBool("app", "debug"); debug { @@ -235,14 +222,6 @@ func (s *ProxyServer) checkOrigin(r *http.Request) bool { 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 == "" { @@ -413,40 +392,7 @@ func (s *ProxyServer) ScheduleShutdown() { } 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) + s.tokens.Reload(config) } func (s *ProxyServer) setCommonHeaders(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { @@ -878,13 +824,17 @@ func (s *ProxyServer) NewSession(hello *signaling.HelloProxyClientMessage) (*Pro return nil, fmt.Errorf("Unsupported claims type") } - tokenKeys := s.getTokenKeys() - publicKey := tokenKeys[claims.Issuer] - if publicKey == nil { + tokenKey, err := s.tokens.Get(claims.Issuer) + if err != nil { + log.Printf("Could not get token for %s: %s", claims.Issuer, err) + return nil, err + } + + if tokenKey == nil || tokenKey.key == nil { log.Printf("Issuer %s is not supported", claims.Issuer) return nil, fmt.Errorf("No key found for issuer") } - return publicKey, nil + return tokenKey.key, nil }) if err != nil { return nil, TokenAuthFailed diff --git a/src/proxy/proxy_tokens.go b/src/proxy/proxy_tokens.go new file mode 100644 index 00000000..b0502d65 --- /dev/null +++ b/src/proxy/proxy_tokens.go @@ -0,0 +1,45 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2020 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 main + +import ( + "crypto/rsa" + + "github.com/dlintw/goconf" +) + +const ( + TokenTypeStatic = "static" + + TokenTypeDefault = TokenTypeStatic +) + +type ProxyToken struct { + id string + key *rsa.PublicKey +} + +type ProxyTokens interface { + Get(id string) (*ProxyToken, error) + + Reload(config *goconf.ConfigFile) +} diff --git a/src/proxy/proxy_tokens_static.go b/src/proxy/proxy_tokens_static.go new file mode 100644 index 00000000..3b8a9842 --- /dev/null +++ b/src/proxy/proxy_tokens_static.go @@ -0,0 +1,118 @@ +/** + * Standalone signaling server for the Nextcloud Spreed app. + * Copyright (C) 2020 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 main + +import ( + "fmt" + "io/ioutil" + "log" + "sort" + "sync/atomic" + + "github.com/dlintw/goconf" + + "gopkg.in/dgrijalva/jwt-go.v3" +) + +type tokensStatic struct { + tokenKeys atomic.Value +} + +func NewProxyTokensStatic(config *goconf.ConfigFile) (ProxyTokens, error) { + result := &tokensStatic{} + if err := result.load(config, false); err != nil { + return nil, err + } + + return result, nil +} + +func (t *tokensStatic) setTokenKeys(keys map[string]*ProxyToken) { + t.tokenKeys.Store(keys) +} + +func (t *tokensStatic) getTokenKeys() map[string]*ProxyToken { + return t.tokenKeys.Load().(map[string]*ProxyToken) +} + +func (t *tokensStatic) Get(id string) (*ProxyToken, error) { + tokenKeys := t.getTokenKeys() + token := tokenKeys[id] + return token, nil +} + +func (t *tokensStatic) load(config *goconf.ConfigFile, ignoreErrors bool) error { + tokenKeys := make(map[string]*ProxyToken) + options, _ := config.GetOptions("tokens") + for _, id := range options { + filename, _ := config.GetString("tokens", id) + if filename == "" { + if !ignoreErrors { + return fmt.Errorf("No filename given for token %s", id) + } + + log.Printf("No filename given for token %s, ignoring", id) + continue + } + + keyData, err := ioutil.ReadFile(filename) + if err != nil { + if !ignoreErrors { + return fmt.Errorf("Could not read public key from %s: %s", filename, err) + } + + log.Printf("Could not read public key from %s, ignoring: %s", filename, err) + continue + } + key, err := jwt.ParseRSAPublicKeyFromPEM(keyData) + if err != nil { + if !ignoreErrors { + return fmt.Errorf("Could not parse public key from %s: %s", filename, err) + } + + log.Printf("Could not parse public key from %s, ignoring: %s", filename, err) + continue + } + + tokenKeys[id] = &ProxyToken{ + id: id, + key: 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) + } + t.setTokenKeys(tokenKeys) + return nil +} + +func (t *tokensStatic) Reload(config *goconf.ConfigFile) { + t.load(config, true) +}