Skip to content

Commit

Permalink
Merge pull request #28 from strukturag/multiple-backends
Browse files Browse the repository at this point in the history
Add support for multiple Nextcloud backends
  • Loading branch information
fancycode authored Jul 31, 2020
2 parents 2d21c98 + 1ef50ea commit 023f271
Show file tree
Hide file tree
Showing 19 changed files with 1,012 additions and 220 deletions.
34 changes: 28 additions & 6 deletions server.conf.in
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,21 @@ blockkey = -encryption-key-
internalsecret = the-shared-secret-for-internal-clients

[backend]
# Comma-separated list of hostnames that are allowed to be used as backend
# endpoints.
allowed = nextcloud.domain.invalid
# Comma-separated list of backend ids from which clients are allowed to connect
# from. Each backend will have isolated rooms, i.e. clients connecting to room
# "abc12345" on backend 1 will be in a different room than clients connected to
# a room with the same name on backend 2. Also sessions connected from different
# backends will not be able to communicate with each other.
#backends = backend-id, another-backend

# Allow any hostname as backend endpoint. This is extremely insecure and should
# only be used while running the benchmark client against the server.
allowall = false

# Shared secret for requests from and to the backend servers. This must be the
# same value as configured in the Nextcloud admin ui.
secret = the-shared-secret
# Common shared secret for requests from and to the backend servers if
# "allowall" is enabled. This must be the same value as configured in the
# Nextcloud admin ui.
# secret = the-shared-secret

# Timeout in seconds for requests to the backend.
timeout = 10
Expand All @@ -68,6 +72,24 @@ connectionsperhost = 8
# certificates.
#skipverify = false

# Backend configurations as defined in the "[backend]" section above. The
# section names must match the ids used in "backends" above.
#[backend-id]
# URL of the Nextcloud instance
#url = https://cloud.domain.invalid

# Shared secret for requests from and to the backend servers. This must be the
# same value as configured in the Nextcloud admin ui.
#secret = the-shared-secret

#[another-backend]
# URL of the Nextcloud instance
#url = https://cloud.otherdomain.invalid

# Shared secret for requests from and to the backend servers. This must be the
# same value as configured in the Nextcloud admin ui.
#secret = the-shared-secret

[nats]
# Url of NATS backend to use. This can also be a list of URLs to connect to
# multiple backends. For local development, this can be set to ":loopback:"
Expand Down
1 change: 1 addition & 0 deletions src/signaling/api_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const (

HeaderBackendSignalingRandom = "Spreed-Signaling-Random"
HeaderBackendSignalingChecksum = "Spreed-Signaling-Checksum"
HeaderBackendServer = "Spreed-Signaling-Backend"
)

func newRandomString(length int) string {
Expand Down
16 changes: 16 additions & 0 deletions src/signaling/api_signaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,20 @@ const (
type ClientTypeInternalAuthParams struct {
Random string `json:"random"`
Token string `json:"token"`

Backend string `json:"backend"`
parsedBackend *url.URL
}

func (p *ClientTypeInternalAuthParams) CheckValid() error {
if p.Backend == "" {
return fmt.Errorf("backend missing")
} else if u, err := url.Parse(p.Backend); err != nil {
return err
} else {
p.parsedBackend = u
}
return nil
}

type HelloClientMessageAuth struct {
Expand Down Expand Up @@ -247,6 +261,8 @@ func (m *HelloClientMessage) CheckValid() error {
case HelloClientTypeInternal:
if err := json.Unmarshal(*m.Auth.Params, &m.Auth.internalParams); err != nil {
return err
} else if err := m.Auth.internalParams.CheckValid(); err != nil {
return err
}
default:
return fmt.Errorf("unsupported auth type")
Expand Down
10 changes: 9 additions & 1 deletion src/signaling/api_signaling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func TestClientMessage(t *testing.T) {
}

func TestHelloClientMessage(t *testing.T) {
internalAuthParams := []byte("{\"backend\":\"https://domain.invalid\"}")
valid_messages := []testCheckValid{
&HelloClientMessage{
Version: HelloVersion,
Expand All @@ -107,7 +108,7 @@ func TestHelloClientMessage(t *testing.T) {
Version: HelloVersion,
Auth: HelloClientMessageAuth{
Type: "internal",
Params: &json.RawMessage{'{', '}'},
Params: (*json.RawMessage)(&internalAuthParams),
},
},
&HelloClientMessage{
Expand Down Expand Up @@ -145,6 +146,13 @@ func TestHelloClientMessage(t *testing.T) {
Url: "invalid-url",
},
},
&HelloClientMessage{
Version: HelloVersion,
Auth: HelloClientMessageAuth{
Type: "internal",
Params: &json.RawMessage{'{', '}'},
},
},
&HelloClientMessage{
Version: HelloVersion,
Auth: HelloClientMessageAuth{
Expand Down
72 changes: 28 additions & 44 deletions src/signaling/backend_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,9 @@ var (
)

type BackendClient struct {
transport *http.Transport
whitelist map[string]bool
whitelistAll bool
secret []byte
version string
transport *http.Transport
version string
backends *BackendConfiguration

mu sync.Mutex

Expand All @@ -57,40 +55,16 @@ type BackendClient struct {
}

func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost int, version string) (*BackendClient, error) {
whitelist := make(map[string]bool)
whitelistAll, _ := config.GetBool("backend", "allowall")
if whitelistAll {
log.Println("WARNING: All backend hostnames are allowed, only use for development!")
} else {
urls, _ := config.GetString("backend", "allowed")
for _, u := range strings.Split(urls, ",") {
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 != "" {
whitelist[strings.ToLower(u)] = true
}
}
if len(whitelist) == 0 {
log.Println("WARNING: No backend hostnames are allowed, check your configuration!")
} else {
hosts := make([]string, 0, len(whitelist))
for u := range whitelist {
hosts = append(hosts, u)
}
log.Printf("Allowed backend hostnames: %s\n", hosts)
}
backends, err := NewBackendConfiguration(config)
if err != nil {
return nil, err
}

skipverify, _ := config.GetBool("backend", "skipverify")
if skipverify {
log.Println("WARNING: Backend verification is disabled!")
}

secret, _ := config.GetString("backend", "secret")

tlsconfig := &tls.Config{
InsecureSkipVerify: skipverify,
}
Expand All @@ -100,11 +74,9 @@ func NewBackendClient(config *goconf.ConfigFile, maxConcurrentRequestsPerHost in
}

return &BackendClient{
transport: transport,
whitelist: whitelist,
whitelistAll: whitelistAll,
secret: []byte(secret),
version: version,
transport: transport,
version: version,
backends: backends,

maxConcurrentRequestsPerHost: maxConcurrentRequestsPerHost,
clients: make(map[string]*HttpClientPool),
Expand Down Expand Up @@ -135,13 +107,20 @@ func (b *BackendClient) getPool(url *url.URL) (*HttpClientPool, error) {
return pool, nil
}

func (b *BackendClient) IsUrlAllowed(u *url.URL) bool {
if u == nil {
// Reject all invalid URLs.
return false
}
func (b *BackendClient) GetCompatBackend() *Backend {
return b.backends.GetCompatBackend()
}

func (b *BackendClient) GetBackend(u *url.URL) *Backend {
return b.backends.GetBackend(u)
}

return b.whitelistAll || b.whitelist[u.Host]
func (b *BackendClient) GetBackends() []*Backend {
return b.backends.GetBackends()
}

func (b *BackendClient) IsUrlAllowed(u *url.URL) bool {
return b.backends.IsUrlAllowed(u)
}

func isOcsRequest(u *url.URL) bool {
Expand Down Expand Up @@ -304,6 +283,11 @@ func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, requ
return fmt.Errorf("No url passed to perform JSON request %+v", request)
}

secret := b.backends.GetSecret(u)
if secret == nil {
return fmt.Errorf("No backend secret configured for for %s", u)
}

pool, err := b.getPool(u)
if err != nil {
log.Printf("Could not get client pool for host %s: %s\n", u.Host, err)
Expand Down Expand Up @@ -338,7 +322,7 @@ func (b *BackendClient) PerformJSONRequest(ctx context.Context, u *url.URL, requ
req.Header.Set("User-Agent", "nextcloud-spreed-signaling/"+b.version)

// Add checksum so the backend can validate the request.
AddBackendChecksum(req, data, b.secret)
AddBackendChecksum(req, data, secret)

resp, err := performRequestWithRedirects(ctx, c, req, data)
if err != nil {
Expand Down
76 changes: 7 additions & 69 deletions src/signaling/backend_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,71 +35,6 @@ import (
"golang.org/x/net/context"
)

func testUrls(t *testing.T, client *BackendClient, valid_urls []string, invalid_urls []string) {
for _, u := range valid_urls {
parsed, err := url.ParseRequestURI(u)
if err != nil {
t.Errorf("The url %s should be valid, got %s", u, err)
continue
}
if !client.IsUrlAllowed(parsed) {
t.Errorf("The url %s should be allowed", u)
}
}
for _, u := range invalid_urls {
parsed, _ := url.ParseRequestURI(u)
if client.IsUrlAllowed(parsed) {
t.Errorf("The url %s should not be allowed", u)
}
}
}

func TestIsUrlAllowed(t *testing.T) {
valid_urls := []string{
"http://domain.invalid",
"https://domain.invalid",
}
invalid_urls := []string{
"http://otherdomain.invalid",
"https://otherdomain.invalid",
"domain.invalid",
}
client := &BackendClient{
whitelistAll: false,
whitelist: map[string]bool{
"domain.invalid": true,
},
}
testUrls(t, client, valid_urls, invalid_urls)
}

func TestIsUrlAllowed_EmptyWhitelist(t *testing.T) {
valid_urls := []string{}
invalid_urls := []string{
"http://domain.invalid",
"https://domain.invalid",
"domain.invalid",
}
client := &BackendClient{
whitelistAll: false,
}
testUrls(t, client, valid_urls, invalid_urls)
}

func TestIsUrlAllowed_WhitelistAll(t *testing.T) {
valid_urls := []string{
"http://domain.invalid",
"https://domain.invalid",
}
invalid_urls := []string{
"domain.invalid",
}
client := &BackendClient{
whitelistAll: true,
}
testUrls(t, client, valid_urls, invalid_urls)
}

func TestPostOnRedirect(t *testing.T) {
r := mux.NewRouter()
r.HandleFunc("/ocs/v2.php/one", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -142,17 +77,20 @@ func TestPostOnRedirect(t *testing.T) {
server := httptest.NewServer(r)
defer server.Close()

config := &goconf.ConfigFile{}
client, err := NewBackendClient(config, 1, "0.0")
u, err := url.Parse(server.URL + "/ocs/v2.php/one")
if err != nil {
t.Fatal(err)
}

ctx := context.Background()
u, err := url.Parse(server.URL + "/ocs/v2.php/one")
config := goconf.NewConfigFile()
config.AddOption("backend", "allowed", u.Host)
config.AddOption("backend", "secret", string(testBackendSecret))
client, err := NewBackendClient(config, 1, "0.0")
if err != nil {
t.Fatal(err)
}

ctx := context.Background()
request := map[string]string{
"foo": "bar",
}
Expand Down
Loading

0 comments on commit 023f271

Please sign in to comment.