diff options
-rw-r--r-- | .github/workflows/linux.yml | 1 | ||||
-rw-r--r-- | .github/workflows/windows.yml | 3 | ||||
-rwxr-xr-x | Makefile | 3 | ||||
-rw-r--r-- | plugins/websockets/config.go | 35 | ||||
-rw-r--r-- | plugins/websockets/origin.go | 28 | ||||
-rw-r--r-- | plugins/websockets/origin_test.go | 69 | ||||
-rw-r--r-- | plugins/websockets/plugin.go | 3 | ||||
-rw-r--r-- | plugins/websockets/wildcard.go | 12 | ||||
-rw-r--r-- | tests/worker-origin.php | 14 |
9 files changed, 161 insertions, 7 deletions
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 3b3f7e9f..acde0901 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -92,6 +92,7 @@ jobs: go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/rpc.txt -covermode=atomic ./tests/plugins/rpc go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/kv_plugin.txt -covermode=atomic ./tests/plugins/kv go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/websockets.txt -covermode=atomic ./tests/plugins/websockets + go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/ws_origin.txt -covermode=atomic ./plugins/websockets docker-compose -f ./tests/docker-compose.yaml down cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 92d76d2c..227c725b 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -67,7 +67,6 @@ jobs: - name: Run golang tests on Windows run: | docker-compose -f ./tests/docker-compose.yaml up -d - mkdir ./coverage-ci go test -v -race ./pkg/transport/pipe go test -v -race ./pkg/transport/socket go test -v -race ./pkg/pool @@ -92,5 +91,5 @@ jobs: go test -v -race ./tests/plugins/rpc go test -v -race ./tests/plugins/kv go test -v -race ./tests/plugins/websockets + go test -v -race ./plugins/websockets docker-compose -f ./tests/docker-compose.yaml down - cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt @@ -32,6 +32,7 @@ test_coverage: go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/rpc.out -covermode=atomic ./tests/plugins/rpc go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/kv_plugin.out -covermode=atomic ./tests/plugins/kv go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/ws_plugin.out -covermode=atomic ./tests/plugins/websockets + go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/ws_origin.out -covermode=atomic ./plugins/websockets cat ./coverage/*.out > ./coverage/summary.out docker-compose -f tests/docker-compose.yaml down @@ -61,6 +62,7 @@ test: ## Run application tests go test -v -race -tags=debug ./tests/plugins/rpc go test -v -race -tags=debug ./tests/plugins/kv go test -v -race -tags=debug ./tests/plugins/websockets + go test -v -race -tags=debug ./plugins/websockets docker-compose -f tests/docker-compose.yaml down testGo1.17beta1: ## Run application tests @@ -89,4 +91,5 @@ testGo1.17beta1: ## Run application tests go1.17beta1 test -v -race -tags=debug ./tests/plugins/rpc go1.17beta1 test -v -race -tags=debug ./tests/plugins/kv go1.17beta1 test -v -race -tags=debug ./tests/plugins/websockets + go1.17beta1 test -v -race -tags=debug ./plugins/websockets docker-compose -f tests/docker-compose.yaml down diff --git a/plugins/websockets/config.go b/plugins/websockets/config.go index 93d9ac3b..deb4406c 100644 --- a/plugins/websockets/config.go +++ b/plugins/websockets/config.go @@ -1,6 +1,7 @@ package websockets import ( + "strings" "time" "github.com/spiral/roadrunner/v2/pkg/pool" @@ -57,9 +58,15 @@ type Config struct { PubSubs []string `mapstructure:"pubsubs"` Middleware []string `mapstructure:"middleware"` - Redis *RedisConfig `mapstructure:"redis"` + AllowedOrigin string `mapstructure:"allowed_origin"` + + // wildcard origin + allowedWOrigins []wildcard + allowedOrigins []string + allowedAll bool - Pool *pool.Config `mapstructure:"pool"` + Redis *RedisConfig `mapstructure:"redis"` + Pool *pool.Config `mapstructure:"pool"` } // InitDefault initialize default values for the ws config @@ -67,6 +74,7 @@ func (c *Config) InitDefault() { if c.Path == "" { c.Path = "/ws" } + if len(c.PubSubs) == 0 { // memory used by default c.PubSubs = append(c.PubSubs, "memory") @@ -86,10 +94,9 @@ func (c *Config) InitDefault() { if c.Pool.DestroyTimeout == 0 { c.Pool.DestroyTimeout = time.Minute } - if c.Pool.Supervisor == nil { - return + if c.Pool.Supervisor != nil { + c.Pool.Supervisor.InitDefaults() } - c.Pool.Supervisor.InitDefaults() } if c.Redis != nil { @@ -98,4 +105,22 @@ func (c *Config) InitDefault() { c.Redis.Addrs = append(c.Redis.Addrs, "localhost:6379") } } + + if c.AllowedOrigin == "" { + c.AllowedOrigin = "*" + } + + // Normalize + origin := strings.ToLower(c.AllowedOrigin) + if origin == "*" { + // If "*" is present in the list, turn the whole list into a match all + c.allowedAll = true + return + } else if i := strings.IndexByte(origin, '*'); i >= 0 { + // Split the origin in two: start and end string without the * + w := wildcard{origin[0:i], origin[i+1:]} + c.allowedWOrigins = append(c.allowedWOrigins, w) + } else { + c.allowedOrigins = append(c.allowedOrigins, origin) + } } diff --git a/plugins/websockets/origin.go b/plugins/websockets/origin.go new file mode 100644 index 00000000..c6d9c9b8 --- /dev/null +++ b/plugins/websockets/origin.go @@ -0,0 +1,28 @@ +package websockets + +import ( + "strings" +) + +func isOriginAllowed(origin string, cfg *Config) bool { + if cfg.allowedAll { + return true + } + + origin = strings.ToLower(origin) + // simple case + origin = strings.ToLower(origin) + for _, o := range cfg.allowedOrigins { + if o == origin { + return true + } + } + // check wildcards + for _, w := range cfg.allowedWOrigins { + if w.match(origin) { + return true + } + } + + return false +} diff --git a/plugins/websockets/origin_test.go b/plugins/websockets/origin_test.go new file mode 100644 index 00000000..ccd94d21 --- /dev/null +++ b/plugins/websockets/origin_test.go @@ -0,0 +1,69 @@ +package websockets + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_Origin(t *testing.T) { + cfg := &Config{ + AllowedOrigin: "*", + } + + cfg.InitDefault() + + assert.True(t, isOriginAllowed("http://some.some.some.sssome", cfg)) + assert.True(t, isOriginAllowed("http://", cfg)) + assert.True(t, isOriginAllowed("http://google.com", cfg)) + assert.True(t, isOriginAllowed("ws://*", cfg)) + assert.True(t, isOriginAllowed("*", cfg)) + assert.True(t, isOriginAllowed("you are bad programmer", cfg)) // True :( + assert.True(t, isOriginAllowed("****", cfg)) + assert.True(t, isOriginAllowed("asde!@#!!@#!%", cfg)) + assert.True(t, isOriginAllowed("http://*.domain.com", cfg)) +} + +func TestConfig_OriginWildCard(t *testing.T) { + cfg := &Config{ + AllowedOrigin: "https://*my.site.com", + } + + cfg.InitDefault() + + assert.True(t, isOriginAllowed("https://my.site.com", cfg)) + assert.False(t, isOriginAllowed("http://", cfg)) + assert.False(t, isOriginAllowed("http://google.com", cfg)) + assert.False(t, isOriginAllowed("ws://*", cfg)) + assert.False(t, isOriginAllowed("*", cfg)) + assert.False(t, isOriginAllowed("you are bad programmer", cfg)) // True :( + assert.False(t, isOriginAllowed("****", cfg)) + assert.False(t, isOriginAllowed("asde!@#!!@#!%", cfg)) + assert.False(t, isOriginAllowed("http://*.domain.com", cfg)) + + + assert.False(t, isOriginAllowed("https://*site.com", cfg)) + assert.True(t, isOriginAllowed("https://some.my.site.com", cfg)) +} + +func TestConfig_OriginWildCard2(t *testing.T) { + cfg := &Config{ + AllowedOrigin: "https://my.*.com", + } + + cfg.InitDefault() + + assert.True(t, isOriginAllowed("https://my.site.com", cfg)) + assert.False(t, isOriginAllowed("http://", cfg)) + assert.False(t, isOriginAllowed("http://google.com", cfg)) + assert.False(t, isOriginAllowed("ws://*", cfg)) + assert.False(t, isOriginAllowed("*", cfg)) + assert.False(t, isOriginAllowed("you are bad programmer", cfg)) // True :( + assert.False(t, isOriginAllowed("****", cfg)) + assert.False(t, isOriginAllowed("asde!@#!!@#!%", cfg)) + assert.False(t, isOriginAllowed("http://*.domain.com", cfg)) + + + assert.False(t, isOriginAllowed("https://*site.com", cfg)) + assert.True(t, isOriginAllowed("https://my.bad.com", cfg)) +} diff --git a/plugins/websockets/plugin.go b/plugins/websockets/plugin.go index 6dfe6ca3..8b708187 100644 --- a/plugins/websockets/plugin.go +++ b/plugins/websockets/plugin.go @@ -82,6 +82,9 @@ func (p *Plugin) Init(cfg config.Configurer, log logger.Logger, server server.Se HandshakeTimeout: time.Second * 60, ReadBufferSize: 1024, WriteBufferSize: 1024, + CheckOrigin: func(r *http.Request) bool { + return isOriginAllowed(r.Header.Get("Origin"), p.cfg) + }, } p.serveExit = make(chan struct{}) p.server = server diff --git a/plugins/websockets/wildcard.go b/plugins/websockets/wildcard.go new file mode 100644 index 00000000..2f1c6601 --- /dev/null +++ b/plugins/websockets/wildcard.go @@ -0,0 +1,12 @@ +package websockets + +import "strings" + +type wildcard struct { + prefix string + suffix string +} + +func (w wildcard) match(s string) bool { + return len(s) >= len(w.prefix)+len(w.suffix) && strings.HasPrefix(s, w.prefix) && strings.HasSuffix(s, w.suffix) +} diff --git a/tests/worker-origin.php b/tests/worker-origin.php new file mode 100644 index 00000000..6ce4de59 --- /dev/null +++ b/tests/worker-origin.php @@ -0,0 +1,14 @@ +<?php + +use Spiral\RoadRunner\Worker; +use Spiral\RoadRunner\Http\HttpWorker; + +require __DIR__ . '/vendor/autoload.php'; + +$http = new HttpWorker(Worker::create()); + +while ($req = $http->waitRequest()) { + $http->respond(200, 'Response', [ + 'Access-Control-Allow-Origin' => ['*'] + ]); +} |