diff options
Diffstat (limited to 'service')
73 files changed, 0 insertions, 12868 deletions
diff --git a/service/env/config.go b/service/env/config.go deleted file mode 100644 index a7da695e..00000000 --- a/service/env/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package env - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config defines set of env values for RR workers. -type Config struct { - // values to set as worker _ENV. - Values map[string]string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(&c.Values) -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *Config) InitDefaults() error { - c.Values = make(map[string]string) - return nil -} diff --git a/service/env/config_test.go b/service/env/config_test.go deleted file mode 100644 index a526990d..00000000 --- a/service/env/config_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package env - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"key":"value"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 1) -} - -func Test_Config_Hydrate_Empty(t *testing.T) { - cfg := &mockCfg{`{}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Len(t, c.Values, 0) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("Test_Config_Defaults failed: error %v", err) - } - assert.Len(t, c.Values, 0) -} diff --git a/service/env/environment.go b/service/env/environment.go deleted file mode 100644 index ab8febf7..00000000 --- a/service/env/environment.go +++ /dev/null @@ -1,23 +0,0 @@ -package env - -// Environment aggregates list of environment variables. This interface can be used in custom implementation to drive -// values from external sources. -type Environment interface { - Setter - Getter - - // Copy all environment values. - Copy(setter Setter) error -} - -// Setter provides ability to set environment value. -type Setter interface { - // SetEnv sets or creates environment value. - SetEnv(key, value string) -} - -// Getter provides ability to set environment value. -type Getter interface { - // GetEnv must return list of env variables. - GetEnv() (map[string]string, error) -} diff --git a/service/env/service.go b/service/env/service.go deleted file mode 100644 index 83175b36..00000000 --- a/service/env/service.go +++ /dev/null @@ -1,55 +0,0 @@ -package env - -// ID contains default service name. -const ID = "env" - -// Service provides ability to map _ENV values from config file. -type Service struct { - // values is default set of values. - values map[string]string -} - -// NewService creates new env service instance for given rr version. -func NewService(defaults map[string]string) *Service { - s := &Service{values: defaults} - return s -} - -// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config) (bool, error) { - if s.values == nil { - s.values = make(map[string]string) - s.values["RR"] = "true" - } - - for k, v := range cfg.Values { - s.values[k] = v - } - - return true, nil -} - -// GetEnv must return list of env variables. -func (s *Service) GetEnv() (map[string]string, error) { - return s.values, nil -} - -// SetEnv sets or creates environment value. -func (s *Service) SetEnv(key, value string) { - s.values[key] = value -} - -// Copy all environment values. -func (s *Service) Copy(setter Setter) error { - values, err := s.GetEnv() - if err != nil { - return err - } - - for k, v := range values { - setter.SetEnv(k, v) - } - - return nil -} diff --git a/service/env/service_test.go b/service/env/service_test.go deleted file mode 100644 index 19cc03c7..00000000 --- a/service/env/service_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package env - -import ( - "github.com/stretchr/testify/assert" - "testing" -) - -func Test_NewService(t *testing.T) { - s := NewService(map[string]string{"version": "test"}) - assert.Len(t, s.values, 1) -} - -func Test_Init(t *testing.T) { - var err error - s := &Service{} - _, err = s.Init(&Config{}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 1) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Equal(t, "true", values["RR"]) -} - -func Test_Extend(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 2) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value", values["key"]) -} - -func Test_Set(t *testing.T) { - var err error - s := NewService(map[string]string{"RR": "version"}) - - _, err = s.Init(&Config{Values: map[string]string{"key": "value"}}) - if err != nil { - t.Errorf("error during the s.Init: error %v", err) - } - assert.Len(t, s.values, 2) - - s.SetEnv("key", "value-new") - s.SetEnv("other", "new") - - values, err := s.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} - -func Test_Copy(t *testing.T) { - s1 := NewService(map[string]string{"RR": "version"}) - s2 := NewService(map[string]string{}) - - s1.SetEnv("key", "value-new") - s1.SetEnv("other", "new") - - assert.NoError(t, s1.Copy(s2)) - - values, err := s2.GetEnv() - assert.NoError(t, err) - assert.Len(t, values, 3) - assert.Equal(t, "version", values["RR"]) - assert.Equal(t, "value-new", values["key"]) - assert.Equal(t, "new", values["other"]) -} diff --git a/service/gzip/config.go b/service/gzip/config.go deleted file mode 100644 index 00ac559d..00000000 --- a/service/gzip/config.go +++ /dev/null @@ -1,22 +0,0 @@ -package gzip - -import ( - "github.com/spiral/roadrunner/service" -) - -// Config describes file location and controls access to them. -type Config struct { - Enable bool -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Enable = true - - return nil -} diff --git a/service/gzip/config_test.go b/service/gzip/config_test.go deleted file mode 100644 index c2168166..00000000 --- a/service/gzip/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"enable": true}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &mockCfg{`{"enable": "invalid"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"enable": 1}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - assert.Equal(t, true, c.Enable) -} diff --git a/service/gzip/service.go b/service/gzip/service.go deleted file mode 100644 index 231ba4d9..00000000 --- a/service/gzip/service.go +++ /dev/null @@ -1,36 +0,0 @@ -package gzip - -import ( - "errors" - "github.com/NYTimes/gziphandler" - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" -) - -// ID contains default service name. -const ID = "gzip" -var httpNotInitialized = errors.New("http service should be defined properly in config to use gzip") - -type Service struct { - cfg *Config -} - -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - s.cfg = cfg - if !s.cfg.Enable { - return false, nil - } - if r == nil { - return false, httpNotInitialized - } - - r.AddMiddleware(s.middleware) - - return true, nil -} - -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - gziphandler.GzipHandler(f).ServeHTTP(w, r) - } -} diff --git a/service/gzip/service_test.go b/service/gzip/service_test.go deleted file mode 100644 index 778bdacd..00000000 --- a/service/gzip/service_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package gzip - -import ( - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "testing" -) - -type testCfg struct { - gzip string - httpCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.gzip} - } - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Disabled(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{cfg: &Config{Enable: true}}) - - assert.NoError(t, c.Init(&testCfg{ - httpCfg: `{ - "address": ":6029", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - } - }`, - gzip: `{"enable":false}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -// TEST bug #275 -func Test_Bug275(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - httpCfg: "", - gzip: `{"enable":true}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} diff --git a/service/headers/config.go b/service/headers/config.go deleted file mode 100644 index f9af1df2..00000000 --- a/service/headers/config.go +++ /dev/null @@ -1,41 +0,0 @@ -package headers - -import "github.com/spiral/roadrunner/service" - -// Config declares headers service configuration. -type Config struct { - // CORS settings. - CORS *CORSConfig - - // Request headers to add to every payload send to PHP. - Request map[string]string - - // Response headers to add to every payload generated by PHP. - Response map[string]string -} - -// CORSConfig headers configuration. -type CORSConfig struct { - // AllowedOrigin: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin - AllowedOrigin string - - // AllowedHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers - AllowedHeaders string - - // AllowedMethods: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods - AllowedMethods string - - // AllowCredentials https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials - AllowCredentials *bool - - // ExposeHeaders: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers - ExposedHeaders string - - // MaxAge of CORS headers in seconds/ - MaxAge int -} - -// Hydrate service config. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} diff --git a/service/headers/config_test.go b/service/headers/config_test.go deleted file mode 100644 index 6ea02f67..00000000 --- a/service/headers/config_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package headers - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"request": {"From": "Something"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} diff --git a/service/headers/service.go b/service/headers/service.go deleted file mode 100644 index 429219d7..00000000 --- a/service/headers/service.go +++ /dev/null @@ -1,113 +0,0 @@ -package headers - -import ( - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" - "strconv" -) - -// ID contains default service name. -const ID = "headers" - -// Service serves headers files. Potentially convert into middleware? -type Service struct { - // server configuration (location, forbidden files and etc) - cfg *Config -} - -// Init must return configure service and return true if service hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - if r == nil { - return false, nil - } - - s.cfg = cfg - r.AddMiddleware(s.middleware) - - return true, nil -} - -// middleware must return true if request/response pair is handled within the middleware. -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - // Define the http.HandlerFunc - return func(w http.ResponseWriter, r *http.Request) { - - if s.cfg.Request != nil { - for k, v := range s.cfg.Request { - r.Header.Add(k, v) - } - } - - if s.cfg.Response != nil { - for k, v := range s.cfg.Response { - w.Header().Set(k, v) - } - } - - if s.cfg.CORS != nil { - if r.Method == http.MethodOptions { - s.preflightRequest(w, r) - return - } - - s.corsHeaders(w, r) - } - - f(w, r) - } -} - -// configure OPTIONS response -func (s *Service) preflightRequest(w http.ResponseWriter, r *http.Request) { - headers := w.Header() - - headers.Add("Vary", "Origin") - headers.Add("Vary", "Access-Control-Request-Method") - headers.Add("Vary", "Access-Control-Request-Headers") - - if s.cfg.CORS.AllowedOrigin != "" { - headers.Set("Access-Control-Allow-Origin", s.cfg.CORS.AllowedOrigin) - } - - if s.cfg.CORS.AllowedHeaders != "" { - headers.Set("Access-Control-Allow-Headers", s.cfg.CORS.AllowedHeaders) - } - - if s.cfg.CORS.AllowedMethods != "" { - headers.Set("Access-Control-Allow-Methods", s.cfg.CORS.AllowedMethods) - } - - if s.cfg.CORS.AllowCredentials != nil { - headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.CORS.AllowCredentials)) - } - - if s.cfg.CORS.MaxAge > 0 { - headers.Set("Access-Control-Max-Age", strconv.Itoa(s.cfg.CORS.MaxAge)) - } - - w.WriteHeader(http.StatusOK) -} - -// configure CORS headers -func (s *Service) corsHeaders(w http.ResponseWriter, r *http.Request) { - headers := w.Header() - - headers.Add("Vary", "Origin") - - if s.cfg.CORS.AllowedOrigin != "" { - headers.Set("Access-Control-Allow-Origin", s.cfg.CORS.AllowedOrigin) - } - - if s.cfg.CORS.AllowedHeaders != "" { - headers.Set("Access-Control-Allow-Headers", s.cfg.CORS.AllowedHeaders) - } - - if s.cfg.CORS.ExposedHeaders != "" { - headers.Set("Access-Control-Expose-Headers", s.cfg.CORS.ExposedHeaders) - } - - if s.cfg.CORS.AllowCredentials != nil { - headers.Set("Access-Control-Allow-Credentials", strconv.FormatBool(*s.cfg.CORS.AllowCredentials)) - } -} diff --git a/service/headers/service_test.go b/service/headers/service_test.go deleted file mode 100644 index a67def02..00000000 --- a/service/headers/service_test.go +++ /dev/null @@ -1,340 +0,0 @@ -package headers - -import ( - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - headers string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.headers} - } - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - return json.Unmarshal([]byte(cfg.target), out) -} - -func Test_RequestHeaders(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{"request":{"input": "custom-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":6078", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php header pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6078?hello=value", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "CUSTOM-HEADER", string(b)) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_ResponseHeaders(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{"response":{"output": "output-header"},"request":{"input": "custom-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":6079", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php header pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6079?hello=value", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "output-header", r.Header.Get("output")) - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "CUSTOM-HEADER", string(b)) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func TestCORS_OPTIONS(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{ -"cors":{ - "allowedOrigin": "*", - "allowedHeaders": "*", - "allowedMethods": "GET,POST,PUT,DELETE", - "allowCredentials": true, - "exposedHeaders": "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", - "maxAge": 600 -} -}`, - httpCfg: `{ - "enable": true, - "address": ":16379", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php headers pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("OPTIONS", "http://localhost:16379", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) - assert.Equal(t, "GET,POST,PUT,DELETE", r.Header.Get("Access-Control-Allow-Methods")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) - assert.Equal(t, "600", r.Header.Get("Access-Control-Max-Age")) - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - - _, err = ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func TestCORS_Pass(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - headers: `{ -"cors":{ - "allowedOrigin": "*", - "allowedHeaders": "*", - "allowedMethods": "GET,POST,PUT,DELETE", - "allowCredentials": true, - "exposedHeaders": "Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma", - "maxAge": 600 -} -}`, - httpCfg: `{ - "enable": true, - "address": ":6672", - "maxRequestSize": 1024, - "workers":{ - "command": "php ../../tests/http/client.php headers pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("GET", "http://localhost:6672", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Headers")) - assert.Equal(t, "*", r.Header.Get("Access-Control-Allow-Origin")) - assert.Equal(t, "true", r.Header.Get("Access-Control-Allow-Credentials")) - - _, err = ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, 200, r.StatusCode) - - err = r.Body.Close() - if err != nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} diff --git a/service/health/config.go b/service/health/config.go deleted file mode 100644 index 60a52d6e..00000000 --- a/service/health/config.go +++ /dev/null @@ -1,32 +0,0 @@ -package health - -import ( - "errors" - "strings" - - "github.com/spiral/roadrunner/service" -) - -// Config configures the health service -type Config struct { - // Address to listen on - Address string -} - -// Hydrate the config -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - return c.Valid() -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - // Validate the address - if c.Address != "" && !strings.Contains(c.Address, ":") { - return errors.New("malformed http server address") - } - - return nil -} diff --git a/service/health/config_test.go b/service/health/config_test.go deleted file mode 100644 index ba7d7c12..00000000 --- a/service/health/config_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package health - -import ( - json "github.com/json-iterator/go" - "testing" - - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost:8080"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - assert.Equal(t, "localhost:8080", c.Address) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Valid1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Valid2(t *testing.T) { - cfg := &mockCfg{`{"address": ":1111"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} diff --git a/service/health/service.go b/service/health/service.go deleted file mode 100644 index ce127340..00000000 --- a/service/health/service.go +++ /dev/null @@ -1,116 +0,0 @@ -package health - -import ( - "context" - "fmt" - "github.com/sirupsen/logrus" - "net/http" - "sync" - "time" - - rrhttp "github.com/spiral/roadrunner/service/http" -) - -const ( - // ID declares public service name. - ID = "health" - // maxHeaderSize declares max header size for prometheus server - maxHeaderSize = 1024 * 1024 * 100 // 104MB -) - -// Service to serve an endpoint for checking the health of the worker pool -type Service struct { - cfg *Config - log *logrus.Logger - mu sync.Mutex - http *http.Server - httpService *rrhttp.Service -} - -// Init health service -func (s *Service) Init(cfg *Config, r *rrhttp.Service, log *logrus.Logger) (bool, error) { - // Ensure the httpService is set - if r == nil { - return false, nil - } - - s.cfg = cfg - s.log = log - s.httpService = r - return true, nil -} - -// Serve the health endpoint -func (s *Service) Serve() error { - // Configure and start the http server - s.mu.Lock() - s.http = &http.Server{ - Addr: s.cfg.Address, - Handler: s, - IdleTimeout: time.Hour * 24, - ReadTimeout: time.Minute * 60, - MaxHeaderBytes: maxHeaderSize, - ReadHeaderTimeout: time.Minute * 60, - WriteTimeout: time.Minute * 60, - } - s.mu.Unlock() - - err := s.http.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - return err - } - - return nil -} - -// Stop the health endpoint -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.http != nil { - // gracefully stop the server - go func() { - err := s.http.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the metrics server: error %v", err)) - } - }() - } -} - -// ServeHTTP returns the health of the pool of workers -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - status := http.StatusOK - if !s.isHealthy() { - status = http.StatusInternalServerError - } - w.WriteHeader(status) -} - -// isHealthy checks the server, pool and ensures at least one worker is active -func (s *Service) isHealthy() bool { - httpService := s.httpService - if httpService == nil { - return false - } - - server := httpService.Server() - if server == nil { - return false - } - - pool := server.Pool() - if pool == nil { - return false - } - - // Ensure at least one worker is active - for _, w := range pool.Workers() { - if w.State().IsActive() { - return true - } - } - - return false -} diff --git a/service/health/service_test.go b/service/health/service_test.go deleted file mode 100644 index fc743a62..00000000 --- a/service/health/service_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package health - -import ( - json "github.com/json-iterator/go" - "io/ioutil" - "net/http" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" -) - -type testCfg struct { - healthCfg string - httpCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - return &testCfg{target: cfg.healthCfg} - } - - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - return err -} - -func TestService_Serve(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2116" - }`, - httpCfg: `{ - "address": "localhost:2115", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - _, res, err := get("http://localhost:2116/") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) -} - -func TestService_Serve_DeadWorker(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2117" - }`, - httpCfg: `{ - "address": "localhost:2118", - "workers":{ - "command": "php ../../tests/http/slow-client.php echo pipes 1000", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("server error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Kill the worker - httpSvc := hS.(*rrhttp.Service) - err := httpSvc.Server().Workers()[0].Kill() - if err != nil { - t.Errorf("error killing the worker: error %v", err) - } - - // Check health check - _, res, err := get("http://localhost:2117/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -func TestService_Serve_DeadWorkerStillHealthy(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2119" - }`, - httpCfg: `{ - "address": "localhost:2120", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 2} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Second * 1) - defer c.Stop() - - // Kill one of the workers - httpSvc := hS.(*rrhttp.Service) - err := httpSvc.Server().Workers()[0].Kill() - if err != nil { - t.Errorf("error killing the worker: error %v", err) - } - - // Check health check - _, res, err := get("http://localhost:2119/") - assert.NoError(t, err) - assert.Equal(t, http.StatusOK, res.StatusCode) -} - -func TestService_Serve_NoHTTPService(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2121" - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, status) -} - -func TestService_Serve_NoServer(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - healthSvc := &Service{} - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, healthSvc) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2122" - }`, - httpCfg: `{ - "address": "localhost:2123", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Set the httpService to nil - healthSvc.httpService = nil - - _, res, err := get("http://localhost:2122/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -func TestService_Serve_NoPool(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - httpSvc := &rrhttp.Service{} - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, httpSvc) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - healthCfg: `{ - "address": "localhost:2124" - }`, - httpCfg: `{ - "address": "localhost:2125", - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`, - })) - - s, status := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, status) - - hS, httpStatus := c.Get(rrhttp.ID) - assert.NotNil(t, hS) - assert.Equal(t, service.StatusOK, httpStatus) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - defer c.Stop() - - // Stop the pool - httpSvc.Server().Stop() - - _, res, err := get("http://localhost:2124/") - assert.NoError(t, err) - assert.Equal(t, http.StatusInternalServerError, res.StatusCode) -} - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} diff --git a/service/http/attributes/attributes.go b/service/http/attributes/attributes.go deleted file mode 100644 index 77d6ea69..00000000 --- a/service/http/attributes/attributes.go +++ /dev/null @@ -1,76 +0,0 @@ -package attributes - -import ( - "context" - "errors" - "net/http" -) - -type attrKey int - -const contextKey attrKey = iota - -type attrs map[string]interface{} - -func (v attrs) get(key string) interface{} { - if v == nil { - return "" - } - - return v[key] -} - -func (v attrs) set(key string, value interface{}) { - v[key] = value -} - -func (v attrs) del(key string) { - delete(v, key) -} - -// Init returns request with new context and attribute bag. -func Init(r *http.Request) *http.Request { - return r.WithContext(context.WithValue(r.Context(), contextKey, attrs{})) -} - -// All returns all context attributes. -func All(r *http.Request) map[string]interface{} { - v := r.Context().Value(contextKey) - if v == nil { - return attrs{} - } - - return v.(attrs) -} - -// Get gets the value from request context. It replaces any existing -// values. -func Get(r *http.Request, key string) interface{} { - v := r.Context().Value(contextKey) - if v == nil { - return nil - } - - return v.(attrs).get(key) -} - -// Set sets the key to value. It replaces any existing -// values. Context specific. -func Set(r *http.Request, key string, value interface{}) error { - v := r.Context().Value(contextKey) - if v == nil { - return errors.New("unable to find `psr:attributes` context key") - } - - v.(attrs).set(key, value) - return nil -} - -// Delete deletes values associated with attribute key. -func (v attrs) Delete(key string) { - if v == nil { - return - } - - v.del(key) -} diff --git a/service/http/attributes/attributes_test.go b/service/http/attributes/attributes_test.go deleted file mode 100644 index 2360fd12..00000000 --- a/service/http/attributes/attributes_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package attributes - -import ( - "github.com/stretchr/testify/assert" - "net/http" - "testing" -) - -func TestAllAttributes(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - - assert.Equal(t, All(r), map[string]interface{}{ - "key": "value", - }) -} - -func TestAllAttributesNone(t *testing.T) { - r := &http.Request{} - r = Init(r) - - assert.Equal(t, All(r), map[string]interface{}{}) -} - -func TestAllAttributesNone2(t *testing.T) { - r := &http.Request{} - - assert.Equal(t, All(r), map[string]interface{}{}) -} - -func TestGetAttribute(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), "value") -} - -func TestGetAttributeNone(t *testing.T) { - r := &http.Request{} - r = Init(r) - - assert.Equal(t, Get(r, "key"), nil) -} - -func TestGetAttributeNone2(t *testing.T) { - r := &http.Request{} - - assert.Equal(t, Get(r, "key"), nil) -} - -func TestSetAttribute(t *testing.T) { - r := &http.Request{} - r = Init(r) - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), "value") -} - -func TestSetAttributeNone(t *testing.T) { - r := &http.Request{} - - err := Set(r, "key", "value") - if err != nil { - t.Errorf("error during the Set: error %v", err) - } - assert.Equal(t, Get(r, "key"), nil) -} diff --git a/service/http/config.go b/service/http/config.go deleted file mode 100644 index 00f61652..00000000 --- a/service/http/config.go +++ /dev/null @@ -1,263 +0,0 @@ -package http - -import ( - "errors" - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "net" - "os" - "strings" -) - -// Config configures RoadRunner HTTP server. -type Config struct { - // Port and port to handle as http server. - Address string - - // SSL defines https server options. - SSL SSLConfig - - // FCGI configuration. You can use FastCGI without HTTP server. - FCGI *FCGIConfig - - // HTTP2 configuration - HTTP2 *HTTP2Config - - // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. - MaxRequestSize int64 - - // TrustedSubnets declare IP subnets which are allowed to set ip using X-Real-Ip and X-Forwarded-For - TrustedSubnets []string - cidrs []*net.IPNet - - // Uploads configures uploads configuration. - Uploads *UploadsConfig - - // Workers configures rr server and worker pool. - Workers *roadrunner.ServerConfig -} - -// FCGIConfig for FastCGI server. -type FCGIConfig struct { - // Address and port to handle as http server. - Address string -} - -// HTTP2Config HTTP/2 server customizations. -type HTTP2Config struct { - // Enable or disable HTTP/2 extension, default enable. - Enabled bool - - // H2C enables HTTP/2 over TCP - H2C bool - - // MaxConcurrentStreams defaults to 128. - MaxConcurrentStreams uint32 -} - -// InitDefaults sets default values for HTTP/2 configuration. -func (cfg *HTTP2Config) InitDefaults() error { - cfg.Enabled = true - cfg.MaxConcurrentStreams = 128 - - return nil -} - -// SSLConfig defines https server configuration. -type SSLConfig struct { - // Port to listen as HTTPS server, defaults to 443. - Port int - - // Redirect when enabled forces all http connections to switch to https. - Redirect bool - - // Key defined private server key. - Key string - - // Cert is https certificate. - Cert string - - // Root CA file - RootCA string -} - -// EnableHTTP is true when http server must run. -func (c *Config) EnableHTTP() bool { - return c.Address != "" -} - -// EnableTLS returns true if rr must listen TLS connections. -func (c *Config) EnableTLS() bool { - return c.SSL.Key != "" || c.SSL.Cert != "" || c.SSL.RootCA != "" -} - -// EnableHTTP2 when HTTP/2 extension must be enabled (only with TSL). -func (c *Config) EnableHTTP2() bool { - return c.HTTP2.Enabled -} - -// EnableH2C when HTTP/2 extension must be enabled on TCP. -func (c *Config) EnableH2C() bool { - return c.HTTP2.H2C -} - -// EnableFCGI is true when FastCGI server must be enabled. -func (c *Config) EnableFCGI() bool { - return c.FCGI.Address != "" -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if c.Workers == nil { - c.Workers = &roadrunner.ServerConfig{} - } - - if c.HTTP2 == nil { - c.HTTP2 = &HTTP2Config{} - } - - if c.FCGI == nil { - c.FCGI = &FCGIConfig{} - } - - if c.Uploads == nil { - c.Uploads = &UploadsConfig{} - } - - if c.SSL.Port == 0 { - c.SSL.Port = 443 - } - - err := c.HTTP2.InitDefaults() - if err != nil { - return err - } - err = c.Uploads.InitDefaults() - if err != nil { - return err - } - err = c.Workers.InitDefaults() - if err != nil { - return err - } - - if err := cfg.Unmarshal(c); err != nil { - return err - } - - c.Workers.UpscaleDurations() - - if c.TrustedSubnets == nil { - // @see https://en.wikipedia.org/wiki/Reserved_IP_addresses - c.TrustedSubnets = []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - } - } - - if err := c.parseCIDRs(); err != nil { - return err - } - - return c.Valid() -} - -func (c *Config) parseCIDRs() error { - for _, cidr := range c.TrustedSubnets { - _, cr, err := net.ParseCIDR(cidr) - if err != nil { - return err - } - - c.cidrs = append(c.cidrs, cr) - } - - return nil -} - -// IsTrusted if api can be trusted to use X-Real-Ip, X-Forwarded-For -func (c *Config) IsTrusted(ip string) bool { - if c.cidrs == nil { - return false - } - - i := net.ParseIP(ip) - if i == nil { - return false - } - - for _, cird := range c.cidrs { - if cird.Contains(i) { - return true - } - } - - return false -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - if c.Uploads == nil { - return errors.New("malformed uploads config") - } - - if c.HTTP2 == nil { - return errors.New("malformed http2 config") - } - - if c.Workers == nil { - return errors.New("malformed workers config") - } - - if c.Workers.Pool == nil { - return errors.New("malformed workers config (pool config is missing)") - } - - if err := c.Workers.Pool.Valid(); err != nil { - return err - } - - if !c.EnableHTTP() && !c.EnableTLS() && !c.EnableFCGI() { - return errors.New("unable to run http service, no method has been specified (http, https, http/2 or FastCGI)") - } - - if c.Address != "" && !strings.Contains(c.Address, ":") { - return errors.New("malformed http server address") - } - - if c.EnableTLS() { - if _, err := os.Stat(c.SSL.Key); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("key file '%s' does not exists", c.SSL.Key) - } - - return err - } - - if _, err := os.Stat(c.SSL.Cert); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("cert file '%s' does not exists", c.SSL.Cert) - } - - return err - } - - // RootCA is optional, but if provided - check it - if c.SSL.RootCA != "" { - if _, err := os.Stat(c.SSL.RootCA); err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("root ca path provided, but path '%s' does not exists", c.SSL.RootCA) - } - return err - } - } - } - - return nil -} diff --git a/service/http/config_test.go b/service/http/config_test.go deleted file mode 100644 index d95e0995..00000000 --- a/service/http/config_test.go +++ /dev/null @@ -1,331 +0,0 @@ -package http - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "os" - "testing" - "time" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"address": "localhost:8080"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Valid(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.NoError(t, cfg.Valid()) -} - -func Test_Trusted_Subnets(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - TrustedSubnets: []string{"200.1.0.0/16"}, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.NoError(t, cfg.parseCIDRs()) - - assert.True(t, cfg.IsTrusted("200.1.0.10")) - assert.False(t, cfg.IsTrusted("127.0.0.0.1")) -} - -func Test_Trusted_Subnets_Err(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - TrustedSubnets: []string{"200.1.0.0"}, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.parseCIDRs()) -} - -func Test_Config_Valid_SSL(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Cert: "fixtures/server.crt", - Key: "fixtures/server.key", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Hydrate(&testCfg{httpCfg: "{}"})) - - assert.NoError(t, cfg.Valid()) - assert.True(t, cfg.EnableTLS()) - assert.Equal(t, 443, cfg.SSL.Port) -} - -func Test_Config_SSL_No_key(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Cert: "fixtures/server.crt", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_SSL_No_Cert(t *testing.T) { - cfg := &Config{ - Address: ":8080", - SSL: SSLConfig{ - Key: "fixtures/server.key", - }, - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoUploads(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoHTTP2(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 0, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoWorkers(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_NoPool(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 0, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_DeadPool(t *testing.T) { - cfg := &Config{ - Address: ":8080", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - }, - } - - assert.Error(t, cfg.Valid()) -} - -func Test_Config_InvalidAddress(t *testing.T) { - cfg := &Config{ - Address: "unexpected_address", - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - HTTP2: &HTTP2Config{ - Enabled: true, - }, - Workers: &roadrunner.ServerConfig{ - Command: "php tests/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: time.Second, - DestroyTimeout: time.Second, - }, - }, - } - - assert.Error(t, cfg.Valid()) -} diff --git a/service/http/constants.go b/service/http/constants.go deleted file mode 100644 index a25f52a4..00000000 --- a/service/http/constants.go +++ /dev/null @@ -1,6 +0,0 @@ -package http - -import "net/http" - -var http2pushHeaderKey = http.CanonicalHeaderKey("http2-push") -var trailerHeaderKey = http.CanonicalHeaderKey("trailer") diff --git a/service/http/errors.go b/service/http/errors.go deleted file mode 100644 index fb8762ef..00000000 --- a/service/http/errors.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build !windows - -package http - -import ( - "errors" - "net" - "os" - "syscall" -) - -// Broken pipe -var errEPIPE = errors.New("EPIPE(32) -> connection reset by peer") - -// handleWriteError just check if error was caused by aborted connection on linux -func handleWriteError(err error) error { - if netErr, ok2 := err.(*net.OpError); ok2 { - if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { - if syscallErr.Err == syscall.EPIPE { - return errEPIPE - } - } - } - return err -} diff --git a/service/http/errors_windows.go b/service/http/errors_windows.go deleted file mode 100644 index 3d0ba04c..00000000 --- a/service/http/errors_windows.go +++ /dev/null @@ -1,27 +0,0 @@ -// +build windows - -package http - -import ( - "errors" - "net" - "os" - "syscall" -) - -//Software caused connection abort. -//An established connection was aborted by the software in your host computer, -//possibly due to a data transmission time-out or protocol error. -var errEPIPE = errors.New("WSAECONNABORTED (10053) -> an established connection was aborted by peer") - -// handleWriteError just check if error was caused by aborted connection on windows -func handleWriteError(err error) error { - if netErr, ok2 := err.(*net.OpError); ok2 { - if syscallErr, ok3 := netErr.Err.(*os.SyscallError); ok3 { - if syscallErr.Err == syscall.WSAECONNABORTED { - return errEPIPE - } - } - } - return err -} diff --git a/service/http/fcgi_test.go b/service/http/fcgi_test.go deleted file mode 100644 index e68b2e7f..00000000 --- a/service/http/fcgi_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package http - -import ( - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "github.com/yookoala/gofast" - "io/ioutil" - "net/http/httptest" - "testing" - "time" -) - -func Test_FCGI_Service_Echo(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "fcgi": { - "address": "tcp://0.0.0.0:6082" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { assert.NoError(t, c.Serve()) }() - time.Sleep(time.Second * 1) - - fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6082") - - fcgiHandler := gofast.NewHandler( - gofast.BasicParamsMap(gofast.BasicSession), - gofast.SimpleClientFactory(fcgiConnFactory, 0), - ) - - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://site.local/?hello=world", nil) - fcgiHandler.ServeHTTP(w, req) - - body, err := ioutil.ReadAll(w.Result().Body) - - assert.NoError(t, err) - assert.Equal(t, 201, w.Result().StatusCode) - assert.Equal(t, "WORLD", string(body)) - c.Stop() -} - -func Test_FCGI_Service_Request_Uri(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "fcgi": { - "address": "tcp://0.0.0.0:6083" - }, - "workers":{ - "command": "php ../../tests/http/client.php request-uri pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { assert.NoError(t, c.Serve()) }() - time.Sleep(time.Second * 1) - - fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6083") - - fcgiHandler := gofast.NewHandler( - gofast.BasicParamsMap(gofast.BasicSession), - gofast.SimpleClientFactory(fcgiConnFactory, 0), - ) - - w := httptest.NewRecorder() - req := httptest.NewRequest("GET", "http://site.local/hello-world", nil) - fcgiHandler.ServeHTTP(w, req) - - body, err := ioutil.ReadAll(w.Result().Body) - - assert.NoError(t, err) - assert.Equal(t, 200, w.Result().StatusCode) - assert.Equal(t, "http://site.local/hello-world", string(body)) - c.Stop() -} diff --git a/service/http/fixtures/server.crt b/service/http/fixtures/server.crt deleted file mode 100644 index 24d67fd7..00000000 --- a/service/http/fixtures/server.crt +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICTTCCAdOgAwIBAgIJAOKyUd+llTRKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYT -AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv -MRMwEQYDVQQKDApSb2FkUnVubmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgw -OTMwMTMzNDUzWhcNMjgwOTI3MTMzNDUzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE -CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwK -Um9hZFJ1bm5lcjESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EE -ACIDYgAEVnbShsM+l5RR3wfWWmGhzuFGwNzKCk7i9xyobDIyBUxG/UUSfj7KKlUX -puDnDEtF5xXcepl744CyIAYFLOXHb5WqI4jCOzG0o9f/00QQ4bQudJOdbqV910QF -C2vb7Fxro1MwUTAdBgNVHQ4EFgQU9xUexnbB6ORKayA7Pfjzs33otsAwHwYDVR0j -BBgwFoAU9xUexnbB6ORKayA7Pfjzs33otsAwDwYDVR0TAQH/BAUwAwEB/zAKBggq -hkjOPQQDAgNoADBlAjEAue3HhR/MUhxoa9tSDBtOJT3FYbDQswrsdqBTz97CGKst -e7XeZ3HMEvEXy0hGGEMhAjAqcD/4k9vViVppgWFtkk6+NFbm+Kw/QeeAiH5FgFSj -8xQcb+b7nPwNLp3JOkXkVd4= ------END CERTIFICATE----- diff --git a/service/http/fixtures/server.key b/service/http/fixtures/server.key deleted file mode 100644 index 7501dd46..00000000 --- a/service/http/fixtures/server.key +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN EC PARAMETERS----- -BgUrgQQAIg== ------END EC PARAMETERS----- ------BEGIN EC PRIVATE KEY----- -MIGkAgEBBDCQP8utxNbHR6xZOLAJgUhn88r6IrPqmN0MsgGJM/jePB+T9UhkmIU8 -PMm2HeScbcugBwYFK4EEACKhZANiAARWdtKGwz6XlFHfB9ZaYaHO4UbA3MoKTuL3 -HKhsMjIFTEb9RRJ+PsoqVRem4OcMS0XnFdx6mXvjgLIgBgUs5cdvlaojiMI7MbSj -1//TRBDhtC50k51upX3XRAULa9vsXGs= ------END EC PRIVATE KEY----- diff --git a/service/http/h2c_test.go b/service/http/h2c_test.go deleted file mode 100644 index f17538bc..00000000 --- a/service/http/h2c_test.go +++ /dev/null @@ -1,83 +0,0 @@ -package http - -import ( - "net/http" - "testing" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" -) - -func Test_Service_H2C(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "address": ":6029", - "http2": {"h2c":true}, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error serving: %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - req, err := http.NewRequest("PRI", "http://localhost:6029?hello=world", nil) - if err != nil { - return err - } - - req.Header.Add("Upgrade", "h2c") - req.Header.Add("Connection", "HTTP2-Settings") - req.Header.Add("HTTP2-Settings", "") - - r, err2 := http.DefaultClient.Do(req) - if err2 != nil { - return err2 - } - - assert.Equal(t, "101 Switching Protocols", r.Status) - - err3 := r.Body.Close() - if err3 != nil { - return err3 - } - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} diff --git a/service/http/handler.go b/service/http/handler.go deleted file mode 100644 index eca05483..00000000 --- a/service/http/handler.go +++ /dev/null @@ -1,208 +0,0 @@ -package http - -import ( - "fmt" - "net" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" -) - -const ( - // EventResponse thrown after the request been processed. See ErrorEvent as payload. - EventResponse = iota + 500 - - // EventError thrown on any non job error provided by road runner server. - EventError -) - -// ErrorEvent represents singular http error event. -type ErrorEvent struct { - // Request contains client request, must not be stored. - Request *http.Request - - // Error - associated error, if any. - Error error - - // event timings - start time.Time - elapsed time.Duration -} - -// Elapsed returns duration of the invocation. -func (e *ErrorEvent) Elapsed() time.Duration { - return e.elapsed -} - -// ResponseEvent represents singular http response event. -type ResponseEvent struct { - // Request contains client request, must not be stored. - Request *Request - - // Response contains service response. - Response *Response - - // event timings - start time.Time - elapsed time.Duration -} - -// Elapsed returns duration of the invocation. -func (e *ResponseEvent) Elapsed() time.Duration { - return e.elapsed -} - -// Handler serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, -// parsed files and query, payload will include parsed form dataTree (if any). -type Handler struct { - cfg *Config - log *logrus.Logger - rr *roadrunner.Server - mul sync.Mutex - lsn func(event int, ctx interface{}) -} - -// Listen attaches handler event controller. -func (h *Handler) Listen(l func(event int, ctx interface{})) { - h.mul.Lock() - defer h.mul.Unlock() - - h.lsn = l -} - -// mdwr serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled. -func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - // validating request size - if h.cfg.MaxRequestSize != 0 { - if length := r.Header.Get("content-length"); length != "" { - if size, err := strconv.ParseInt(length, 10, 64); err != nil { - h.handleError(w, r, err, start) - return - } else if size > h.cfg.MaxRequestSize*1024*1024 { - h.handleError(w, r, errors.New("request body max size is exceeded"), start) - return - } - } - } - - req, err := NewRequest(r, h.cfg.Uploads) - if err != nil { - h.handleError(w, r, err, start) - return - } - - // proxy IP resolution - h.resolveIP(req) - - req.Open(h.log) - defer req.Close(h.log) - - p, err := req.Payload() - if err != nil { - h.handleError(w, r, err, start) - return - } - - rsp, err := h.rr.Exec(p) - if err != nil { - h.handleError(w, r, err, start) - return - } - - resp, err := NewResponse(rsp) - if err != nil { - h.handleError(w, r, err, start) - return - } - - h.handleResponse(req, resp, start) - err = resp.Write(w) - if err != nil { - h.handleError(w, r, err, start) - } -} - -// handleError sends error. -func (h *Handler) handleError(w http.ResponseWriter, r *http.Request, err error, start time.Time) { - // if pipe is broken, there is no sense to write the header - // in this case we just report about error - if err == errEPIPE { - h.throw(EventError, &ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) - return - } - // ResponseWriter is ok, write the error code - w.WriteHeader(500) - _, err2 := w.Write([]byte(err.Error())) - // error during the writing to the ResponseWriter - if err2 != nil { - // concat original error with ResponseWriter error - h.throw(EventError, &ErrorEvent{Request: r, Error: errors.New(fmt.Sprintf("error: %v, during handle this error, ResponseWriter error occurred: %v", err, err2)), start: start, elapsed: time.Since(start)}) - return - } - h.throw(EventError, &ErrorEvent{Request: r, Error: err, start: start, elapsed: time.Since(start)}) -} - -// handleResponse triggers response event. -func (h *Handler) handleResponse(req *Request, resp *Response, start time.Time) { - h.throw(EventResponse, &ResponseEvent{Request: req, Response: resp, start: start, elapsed: time.Since(start)}) -} - -// throw invokes event handler if any. -func (h *Handler) throw(event int, ctx interface{}) { - h.mul.Lock() - defer h.mul.Unlock() - - if h.lsn != nil { - h.lsn(event, ctx) - } -} - -// get real ip passing multiple proxy -func (h *Handler) resolveIP(r *Request) { - if !h.cfg.IsTrusted(r.RemoteAddr) { - return - } - - if r.Header.Get("X-Forwarded-For") != "" { - ips := strings.Split(r.Header.Get("X-Forwarded-For"), ",") - ipCount := len(ips) - - for i := ipCount - 1; i >= 0; i-- { - addr := strings.TrimSpace(ips[i]) - if net.ParseIP(addr) != nil { - r.RemoteAddr = addr - return - } - } - - return - } - - // The logic here is the following: - // In general case, we only expect X-Real-Ip header. If it exist, we get the IP addres from header and set request Remote address - // But, if there is no X-Real-Ip header, we also trying to check CloudFlare headers - // True-Client-IP is a general CF header in which copied information from X-Real-Ip in CF. - // CF-Connecting-IP is an Enterprise feature and we check it last in order. - // This operations are near O(1) because Headers struct are the map type -> type MIMEHeader map[string][]string - if r.Header.Get("X-Real-Ip") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("X-Real-Ip")) - return - } - - if r.Header.Get("True-Client-IP") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("True-Client-IP")) - return - } - - if r.Header.Get("CF-Connecting-IP") != "" { - r.RemoteAddr = fetchIP(r.Header.Get("CF-Connecting-IP")) - } -} diff --git a/service/http/handler_test.go b/service/http/handler_test.go deleted file mode 100644 index cb1cd728..00000000 --- a/service/http/handler_test.go +++ /dev/null @@ -1,1961 +0,0 @@ -package http - -import ( - "bytes" - "context" - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" - "io/ioutil" - "mime/multipart" - "net/http" - "net/http/httptest" - "net/url" - "os" - "runtime" - "strings" - "testing" - "time" -) - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -// get request and return body -func getHeader(url string, h map[string]string) (string, *http.Response, error) { - req, err := http.NewRequest("GET", url, bytes.NewBuffer(nil)) - if err != nil { - return "", nil, err - } - - for k, v := range h { - req.Header.Set(k, v) - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -func TestHandler_Echo(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func Test_HandlerErrors(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - wr := httptest.NewRecorder() - rq := httptest.NewRequest("POST", "/", bytes.NewBuffer([]byte("data"))) - - h.ServeHTTP(wr, rq) - assert.Equal(t, 500, wr.Code) -} - -func Test_Handler_JSON_error(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - wr := httptest.NewRecorder() - rq := httptest.NewRequest("POST", "/", bytes.NewBuffer([]byte("{sd"))) - rq.Header.Add("Content-Type", "application/json") - rq.Header.Add("Content-Size", "3") - - h.ServeHTTP(wr, rq) - assert.Equal(t, 500, wr.Code) -} - -func TestHandler_Headers(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php header pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8078", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:8078?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("input", "sample") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "world", r.Header.Get("Header")) - assert.Equal(t, "SAMPLE", string(b)) -} - -func TestHandler_Empty_User_Agent(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php user-agent pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8088", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("user-agent", "") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "", string(b)) -} - -func TestHandler_User_Agent(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php user-agent pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8088", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8088?hello=world", nil) - assert.NoError(t, err) - - req.Header.Add("User-Agent", "go-agent") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "go-agent", string(b)) -} - -func TestHandler_Cookies(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php cookie pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8079", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("GET", "http://localhost:8079", nil) - assert.NoError(t, err) - - req.AddCookie(&http.Cookie{Name: "input", Value: "input-value"}) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "INPUT-VALUE", string(b)) - - for _, c := range r.Cookies() { - assert.Equal(t, "output", c.Name) - assert.Equal(t, "cookie-output", c.Value) - } -} - -func TestHandler_JsonPayload_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8090", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest( - "POST", - "http://localhost"+hs.Addr, - bytes.NewBufferString(`{"key":"value"}`), - ) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_JsonPayload_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8081", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_JsonPayload_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php payload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8082", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, bytes.NewBufferString(`{"key":"value"}`)) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/json") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, `{"value":"key"}`, string(b)) -} - -func TestHandler_FormData_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_POST_Overwrite(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value1") - form.Add("key", "value2") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"key":"value2","arr":{"x":{"y":null}}}`, string(b)) -} - -func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8083", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8084", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_FormData_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8085", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - form := url.Values{} - - form.Add("key", "value") - form.Add("name[]", "name1") - form.Add("name[]", "name2") - form.Add("name[]", "name3") - form.Add("arr[x][y][z]", "y") - form.Add("arr[x][y][e]", "f") - form.Add("arr[c]p", "l") - form.Add("arr[c]z", "") - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, strings.NewReader(form.Encode())) - assert.NoError(t, err) - - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_POST(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8019", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_PUT(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8020", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("PUT", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Multipart_PATCH(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php data pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - err := w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("key", "value") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name1") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name2") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("name[]", "name3") - - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][z]", "y") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[x][y][e]", "f") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]p", "l") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.WriteField("arr[c]z", "") - if err != nil { - t.Errorf("error writing the field: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the writer: error %v", err) - } - - req, err := http.NewRequest("PATCH", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - assert.Equal(t, `{"arr":{"c":{"p":"l","z":""},"x":{"y":{"e":"f","z":"y"}}},"key":"value","name":["name1","name2","name3"]}`, string(b)) -} - -func TestHandler_Error(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_Error2(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error2 pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_Error3(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php pid pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - b2 := &bytes.Buffer{} - for i := 0; i < 1024*1024; i++ { - b2.Write([]byte(" ")) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, b2) - assert.NoError(t, err) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error during the closing Body: error %v", err) - - } - }() - - assert.NoError(t, err) - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_ResponseDuration(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - gotresp := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventResponse { - c := ctx.(*ResponseEvent) - - if c.Elapsed() > 0 { - close(gotresp) - } - } - }) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-gotresp - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func TestHandler_ResponseDurationDelayed(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echoDelay pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - gotresp := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventResponse { - c := ctx.(*ResponseEvent) - - if c.Elapsed() > time.Second { - close(gotresp) - } - } - }) - - body, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-gotresp - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", body) -} - -func TestHandler_ErrorDuration(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php error pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - goterr := make(chan interface{}) - h.Listen(func(event int, ctx interface{}) { - if event == EventError { - c := ctx.(*ErrorEvent) - - if c.Elapsed() > 0 { - close(goterr) - } - } - }) - - _, r, err := get("http://localhost:8177/?hello=world") - assert.NoError(t, err) - - <-goterr - - assert.Equal(t, 500, r.StatusCode) -} - -func TestHandler_IP(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := get("http://127.0.0.1:8177/") - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "127.0.0.1", body) -} - -func TestHandler_XRealIP(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Real-Ip": "200.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "200.0.0.1", body) -} - -func TestHandler_XForwardedFor(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "100.0.0.0/16", - "200.0.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "101.0.0.1", body) - - body, r, err = getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, 101.0.0.1, invalid", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "101.0.0.1", body) -} - -func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - TrustedSubnets: []string{ - "10.0.0.0/8", - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php ip pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.cfg.parseCIDRs() - if err != nil { - t.Errorf("error parsing CIDRs: error %v", err) - } - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: "127.0.0.1:8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - body, r, err := getHeader("http://127.0.0.1:8177/", map[string]string{ - "X-Forwarded-For": "100.0.0.1, 200.0.0.1, invalid, 101.0.0.1", - }) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "127.0.0.1", body) -} - -func BenchmarkHandler_Listen_Echo(b *testing.B) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php echo pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: int64(runtime.NumCPU()), - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - err := h.rr.Start() - if err != nil { - b.Errorf("error starting the worker pool: error %v", err) - } - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8177", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - b.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - b.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - bb := "WORLD" - for n := 0; n < b.N; n++ { - r, err := http.Get("http://localhost:8177/?hello=world") - if err != nil { - b.Fail() - } - // Response might be nil here - if r != nil { - br, err := ioutil.ReadAll(r.Body) - if err != nil { - b.Errorf("error reading Body: error %v", err) - } - if string(br) != bb { - b.Fail() - } - err = r.Body.Close() - if err != nil { - b.Errorf("error closing the Body: error %v", err) - } - } else { - b.Errorf("got nil response") - } - } -} diff --git a/service/http/parse.go b/service/http/parse.go deleted file mode 100644 index 9b58d328..00000000 --- a/service/http/parse.go +++ /dev/null @@ -1,147 +0,0 @@ -package http - -import ( - "net/http" -) - -// MaxLevel defines maximum tree depth for incoming request data and files. -const MaxLevel = 127 - -type dataTree map[string]interface{} -type fileTree map[string]interface{} - -// parseData parses incoming request body into data tree. -func parseData(r *http.Request) dataTree { - data := make(dataTree) - if r.PostForm != nil { - for k, v := range r.PostForm { - data.push(k, v) - } - } - - if r.MultipartForm != nil { - for k, v := range r.MultipartForm.Value { - data.push(k, v) - } - } - - return data -} - -// pushes value into data tree. -func (d dataTree) push(k string, v []string) { - keys := fetchIndexes(k) - if len(keys) <= MaxLevel { - d.mount(keys, v) - } -} - -// mount mounts data tree recursively. -func (d dataTree) mount(i []string, v []string) { - if len(i) == 1 { - // single value context (last element) - d[i[0]] = v[len(v)-1] - return - } - - if len(i) == 2 && i[1] == "" { - // non associated array of elements - d[i[0]] = v - return - } - - if p, ok := d[i[0]]; ok { - p.(dataTree).mount(i[1:], v) - return - } - - d[i[0]] = make(dataTree) - d[i[0]].(dataTree).mount(i[1:], v) -} - -// parse incoming dataTree request into JSON (including contentMultipart form dataTree) -func parseUploads(r *http.Request, cfg *UploadsConfig) *Uploads { - u := &Uploads{ - cfg: cfg, - tree: make(fileTree), - list: make([]*FileUpload, 0), - } - - for k, v := range r.MultipartForm.File { - files := make([]*FileUpload, 0, len(v)) - for _, f := range v { - files = append(files, NewUpload(f)) - } - - u.list = append(u.list, files...) - u.tree.push(k, files) - } - - return u -} - -// pushes new file upload into it's proper place. -func (d fileTree) push(k string, v []*FileUpload) { - keys := fetchIndexes(k) - if len(keys) <= MaxLevel { - d.mount(keys, v) - } -} - -// mount mounts data tree recursively. -func (d fileTree) mount(i []string, v []*FileUpload) { - if len(i) == 1 { - // single value context - d[i[0]] = v[0] - return - } - - if len(i) == 2 && i[1] == "" { - // non associated array of elements - d[i[0]] = v - return - } - - if p, ok := d[i[0]]; ok { - p.(fileTree).mount(i[1:], v) - return - } - - d[i[0]] = make(fileTree) - d[i[0]].(fileTree).mount(i[1:], v) -} - -// fetchIndexes parses input name and splits it into separate indexes list. -func fetchIndexes(s string) []string { - var ( - pos int - ch string - keys = make([]string, 1) - ) - - for _, c := range s { - ch = string(c) - switch ch { - case " ": - // ignore all spaces - continue - case "[": - pos = 1 - continue - case "]": - if pos == 1 { - keys = append(keys, "") - } - pos = 2 - default: - if pos == 1 || pos == 2 { - keys = append(keys, "") - } - - keys[len(keys)-1] += ch - pos = 0 - } - } - - return keys -} diff --git a/service/http/parse_test.go b/service/http/parse_test.go deleted file mode 100644 index f95a3f9d..00000000 --- a/service/http/parse_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package http - -import "testing" - -var samples = []struct { - in string - out []string -}{ - {"key", []string{"key"}}, - {"key[subkey]", []string{"key", "subkey"}}, - {"key[subkey]value", []string{"key", "subkey", "value"}}, - {"key[subkey][value]", []string{"key", "subkey", "value"}}, - {"key[subkey][value][]", []string{"key", "subkey", "value", ""}}, - {"key[subkey] [value][]", []string{"key", "subkey", "value", ""}}, - {"key [ subkey ] [ value ] [ ]", []string{"key", "subkey", "value", ""}}, -} - -func Test_FetchIndexes(t *testing.T) { - for _, tt := range samples { - t.Run(tt.in, func(t *testing.T) { - r := fetchIndexes(tt.in) - if !same(r, tt.out) { - t.Errorf("got %q, want %q", r, tt.out) - } - }) - } -} - -func BenchmarkConfig_FetchIndexes(b *testing.B) { - for _, tt := range samples { - for n := 0; n < b.N; n++ { - r := fetchIndexes(tt.in) - if !same(r, tt.out) { - b.Fail() - } - } - } -} - -func same(in, out []string) bool { - if len(in) != len(out) { - return false - } - - for i, v := range in { - if v != out[i] { - return false - } - } - - return true -} diff --git a/service/http/request.go b/service/http/request.go deleted file mode 100644 index 8da5440f..00000000 --- a/service/http/request.go +++ /dev/null @@ -1,183 +0,0 @@ -package http - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "strings" - - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service/http/attributes" -) - -const ( - defaultMaxMemory = 32 << 20 // 32 MB - contentNone = iota + 900 - contentStream - contentMultipart - contentFormData -) - -// Request maps net/http requests to PSR7 compatible structure and managed state of temporary uploaded files. -type Request struct { - // RemoteAddr contains ip address of client, make sure to check X-Real-Ip and X-Forwarded-For for real client address. - RemoteAddr string `json:"remoteAddr"` - - // Protocol includes HTTP protocol version. - Protocol string `json:"protocol"` - - // Method contains name of HTTP method used for the request. - Method string `json:"method"` - - // URI contains full request URI with scheme and query. - URI string `json:"uri"` - - // Header contains list of request headers. - Header http.Header `json:"headers"` - - // Cookies contains list of request cookies. - Cookies map[string]string `json:"cookies"` - - // RawQuery contains non parsed query string (to be parsed on php end). - RawQuery string `json:"rawQuery"` - - // Parsed indicates that request body has been parsed on RR end. - Parsed bool `json:"parsed"` - - // Uploads contains list of uploaded files, their names, sized and associations with temporary files. - Uploads *Uploads `json:"uploads"` - - // Attributes can be set by chained mdwr to safely pass value from Golang to PHP. See: GetAttribute, SetAttribute functions. - Attributes map[string]interface{} `json:"attributes"` - - // request body can be parsedData or []byte - body interface{} -} - -func fetchIP(pair string) string { - if !strings.ContainsRune(pair, ':') { - return pair - } - - addr, _, _ := net.SplitHostPort(pair) - return addr -} - -// NewRequest creates new PSR7 compatible request using net/http request. -func NewRequest(r *http.Request, cfg *UploadsConfig) (req *Request, err error) { - req = &Request{ - RemoteAddr: fetchIP(r.RemoteAddr), - Protocol: r.Proto, - Method: r.Method, - URI: uri(r), - Header: r.Header, - Cookies: make(map[string]string), - RawQuery: r.URL.RawQuery, - Attributes: attributes.All(r), - } - - for _, c := range r.Cookies() { - if v, err := url.QueryUnescape(c.Value); err == nil { - req.Cookies[c.Name] = v - } - } - - switch req.contentType() { - case contentNone: - return req, nil - - case contentStream: - req.body, err = ioutil.ReadAll(r.Body) - return req, err - - case contentMultipart: - if err = r.ParseMultipartForm(defaultMaxMemory); err != nil { - return nil, err - } - - req.Uploads = parseUploads(r, cfg) - fallthrough - case contentFormData: - if err = r.ParseForm(); err != nil { - return nil, err - } - - req.body = parseData(r) - } - - req.Parsed = true - return req, nil -} - -// Open moves all uploaded files to temporary directory so it can be given to php later. -func (r *Request) Open(log *logrus.Logger) { - if r.Uploads == nil { - return - } - - r.Uploads.Open(log) -} - -// Close clears all temp file uploads -func (r *Request) Close(log *logrus.Logger) { - if r.Uploads == nil { - return - } - - r.Uploads.Clear(log) -} - -// Payload request marshaled RoadRunner payload based on PSR7 data. values encode method is JSON. Make sure to open -// files prior to calling this method. -func (r *Request) Payload() (p *roadrunner.Payload, err error) { - p = &roadrunner.Payload{} - - j := json.ConfigCompatibleWithStandardLibrary - if p.Context, err = j.Marshal(r); err != nil { - return nil, err - } - - if r.Parsed { - if p.Body, err = j.Marshal(r.body); err != nil { - return nil, err - } - } else if r.body != nil { - p.Body = r.body.([]byte) - } - - return p, nil -} - -// contentType returns the payload content type. -func (r *Request) contentType() int { - if r.Method == "HEAD" || r.Method == "OPTIONS" { - return contentNone - } - - ct := r.Header.Get("content-type") - if strings.Contains(ct, "application/x-www-form-urlencoded") { - return contentFormData - } - - if strings.Contains(ct, "multipart/form-data") { - return contentMultipart - } - - return contentStream -} - -// uri fetches full uri from request in a form of string (including https scheme if TLS connection is enabled). -func uri(r *http.Request) string { - if r.URL.Host != "" { - return r.URL.String() - } - if r.TLS != nil { - return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) - } - - return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) -} diff --git a/service/http/response.go b/service/http/response.go deleted file mode 100644 index f34754be..00000000 --- a/service/http/response.go +++ /dev/null @@ -1,107 +0,0 @@ -package http - -import ( - "io" - "net/http" - "strings" - - json "github.com/json-iterator/go" - - "github.com/spiral/roadrunner" -) - - -// Response handles PSR7 response logic. -type Response struct { - // Status contains response status. - Status int `json:"status"` - - // Header contains list of response headers. - Headers map[string][]string `json:"headers"` - - // associated body payload. - body interface{} -} - -// NewResponse creates new response based on given rr payload. -func NewResponse(p *roadrunner.Payload) (*Response, error) { - r := &Response{body: p.Body} - j := json.ConfigCompatibleWithStandardLibrary - if err := j.Unmarshal(p.Context, r); err != nil { - return nil, err - } - - return r, nil -} - -// Write writes response headers, status and body into ResponseWriter. -func (r *Response) Write(w http.ResponseWriter) error { - // INFO map is the reference type in golang - p := handlePushHeaders(r.Headers) - if pusher, ok := w.(http.Pusher); ok { - for _, v := range p { - err := pusher.Push(v, nil) - if err != nil { - return err - } - } - } - - handleTrailers(r.Headers) - for n, h := range r.Headers { - for _, v := range h { - w.Header().Add(n, v) - } - } - - w.WriteHeader(r.Status) - - if data, ok := r.body.([]byte); ok { - _, err := w.Write(data) - if err != nil { - return handleWriteError(err) - } - } - - if rc, ok := r.body.(io.Reader); ok { - if _, err := io.Copy(w, rc); err != nil { - return err - } - } - - return nil -} - -func handlePushHeaders(h map[string][]string) []string { - var p []string - pushHeader, ok := h[http2pushHeaderKey] - if !ok { - return p - } - - p = append(p, pushHeader...) - - delete(h, http2pushHeaderKey) - - return p -} - -func handleTrailers(h map[string][]string) { - trailers, ok := h[trailerHeaderKey] - if !ok { - return - } - - for _, tr := range trailers { - for _, n := range strings.Split(tr, ",") { - n = strings.Trim(n, "\t ") - if v, ok := h[n]; ok { - h["Trailer:"+n] = v - - delete(h, n) - } - } - } - - delete(h, trailerHeaderKey) -} diff --git a/service/http/response_test.go b/service/http/response_test.go deleted file mode 100644 index 1f394276..00000000 --- a/service/http/response_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package http - -import ( - "bytes" - "errors" - "net/http" - "testing" - - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" -) - -type testWriter struct { - h http.Header - buf bytes.Buffer - wroteHeader bool - code int - err error - pushErr error - pushes []string -} - -func (tw *testWriter) Header() http.Header { return tw.h } - -func (tw *testWriter) Write(p []byte) (int, error) { - if !tw.wroteHeader { - tw.WriteHeader(http.StatusOK) - } - - n, e := tw.buf.Write(p) - if e == nil { - e = tw.err - } - - return n, e -} - -func (tw *testWriter) WriteHeader(code int) { tw.wroteHeader = true; tw.code = code } - -func (tw *testWriter) Push(target string, opts *http.PushOptions) error { - tw.pushes = append(tw.pushes, target) - - return tw.pushErr -} - -func TestNewResponse_Error(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{Context: []byte(`invalid payload`)}) - assert.Error(t, err) - assert.Nil(t, r) -} - -func TestNewResponse_Write(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - Body: []byte(`sample body`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, 301, w.code) - assert.Equal(t, "value", w.h.Get("key")) - assert.Equal(t, "sample body", w.buf.String()) -} - -func TestNewResponse_Stream(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - }) - - // r is pointer, so, it might be nil - if r == nil { - t.Fatal("response is nil") - } - - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, 301, w.code) - assert.Equal(t, "value", w.h.Get("key")) - assert.Equal(t, "hello world", w.buf.String()) -} - -func TestNewResponse_StreamError(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), - }) - - // r is pointer, so, it might be nil - if r == nil { - t.Fatal("response is nil") - } - - r.body = &bytes.Buffer{} - r.body.(*bytes.Buffer).WriteString("hello world") - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string)), err: errors.New("error")} - assert.Error(t, r.Write(w)) -} - -func TestWrite_HandlesPush(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Nil(t, w.h["Http2-Push"]) - assert.Equal(t, []string{"/test.js"}, w.pushes) -} - -func TestWrite_HandlesTrailers(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Nil(t, w.h[trailerHeaderKey]) - assert.Nil(t, w.h["foo"]) //nolint:golint,staticcheck - assert.Nil(t, w.h["baz"]) //nolint:golint,staticcheck - - assert.Equal(t, "test", w.h.Get("Trailer:foo")) - assert.Equal(t, "demo", w.h.Get("Trailer:bar")) -} - -func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { - r, err := NewResponse(&roadrunner.Payload{ - Context: []byte( - `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), - }) - - assert.NoError(t, err) - assert.NotNil(t, r) - - w := &testWriter{h: http.Header(make(map[string][]string))} - assert.NoError(t, r.Write(w)) - - assert.Equal(t, "a", w.h.Get("Trailer:foo")) - assert.Equal(t, "b", w.h.Get("Trailer:bar")) - assert.Equal(t, "c", w.h.Get("Trailer:baz")) -} diff --git a/service/http/rpc.go b/service/http/rpc.go deleted file mode 100644 index 7b38dece..00000000 --- a/service/http/rpc.go +++ /dev/null @@ -1,34 +0,0 @@ -package http - -import ( - "github.com/pkg/errors" - "github.com/spiral/roadrunner/util" -) - -type rpcServer struct{ svc *Service } - -// WorkerList contains list of workers. -type WorkerList struct { - // Workers is list of workers. - Workers []*util.State `json:"workers"` -} - -// Reset resets underlying RR worker pool and restarts all of it's workers. -func (rpc *rpcServer) Reset(reset bool, r *string) error { - if rpc.svc == nil || rpc.svc.handler == nil { - return errors.New("http server is not running") - } - - *r = "OK" - return rpc.svc.Server().Reset() -} - -// Workers returns list of active workers and their stats. -func (rpc *rpcServer) Workers(list bool, r *WorkerList) (err error) { - if rpc.svc == nil || rpc.svc.handler == nil { - return errors.New("http server is not running") - } - - r.Workers, err = util.ServerState(rpc.svc.Server()) - return err -} diff --git a/service/http/rpc_test.go b/service/http/rpc_test.go deleted file mode 100644 index e57a8699..00000000 --- a/service/http/rpc_test.go +++ /dev/null @@ -1,221 +0,0 @@ -package http - -import ( - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "os" - "strconv" - "testing" - "time" -) - -func Test_RPC(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:5004"}`, - httpCfg: `{ - "enable": true, - "address": ":16031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Second) - - res, _, err := get("http://localhost:16031") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res) - - cl, err := rs.Client() - assert.NoError(t, err) - - r := "" - assert.NoError(t, cl.Call("http.Reset", true, &r)) - assert.Equal(t, "OK", r) - - res2, _, err := get("http://localhost:16031") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res2) - assert.NotEqual(t, res, res2) - c.Stop() -} - -func Test_RPC_Unix(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - sock := `unix://` + os.TempDir() + `/rpc.unix` - j := json.ConfigCompatibleWithStandardLibrary - data, _ := j.Marshal(sock) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":` + string(data) + `}`, - httpCfg: `{ - "enable": true, - "address": ":6032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - res, _, err := get("http://localhost:6032") - if err != nil { - c.Stop() - t.Fatal(err) - } - if ss.rr.Workers() != nil && len(ss.rr.Workers()) > 0 { - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res) - } else { - c.Stop() - t.Fatal("no workers initialized") - } - - cl, err := rs.Client() - if err != nil { - c.Stop() - t.Fatal(err) - } - - r := "" - assert.NoError(t, cl.Call("http.Reset", true, &r)) - assert.Equal(t, "OK", r) - - res2, _, err := get("http://localhost:6032") - if err != nil { - c.Stop() - t.Fatal(err) - } - assert.Equal(t, strconv.Itoa(*ss.rr.Workers()[0].Pid), res2) - assert.NotEqual(t, res, res2) - c.Stop() -} - -func Test_Workers(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:5005"}`, - httpCfg: `{ - "enable": true, - "address": ":6033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - s, _ := c.Get(ID) - ss := s.(*Service) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - cl, err := rs.Client() - assert.NoError(t, err) - - r := &WorkerList{} - assert.NoError(t, cl.Call("http.Workers", true, &r)) - assert.Len(t, r.Workers, 1) - - assert.Equal(t, *ss.rr.Workers()[0].Pid, r.Workers[0].Pid) - c.Stop() -} - -func Test_Errors(t *testing.T) { - r := &rpcServer{nil} - - assert.Error(t, r.Reset(true, nil)) - assert.Error(t, r.Workers(true, nil)) -} diff --git a/service/http/service.go b/service/http/service.go deleted file mode 100644 index 25a10064..00000000 --- a/service/http/service.go +++ /dev/null @@ -1,427 +0,0 @@ -package http - -import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/http/fcgi" - "net/url" - "strings" - "sync" - - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service/env" - "github.com/spiral/roadrunner/service/http/attributes" - "github.com/spiral/roadrunner/service/rpc" - "github.com/spiral/roadrunner/util" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" - "golang.org/x/sys/cpu" -) - -const ( - // ID contains default service name. - ID = "http" - - // EventInitSSL thrown at moment of https initialization. SSL server passed as context. - EventInitSSL = 750 -) - -var couldNotAppendPemError = errors.New("could not append Certs from PEM") - -// http middleware type. -type middleware func(f http.HandlerFunc) http.HandlerFunc - -// Service manages rr, http servers. -type Service struct { - sync.Mutex - sync.WaitGroup - - cfg *Config - log *logrus.Logger - cprod roadrunner.CommandProducer - env env.Environment - lsns []func(event int, ctx interface{}) - mdwr []middleware - - rr *roadrunner.Server - controller roadrunner.Controller - handler *Handler - - http *http.Server - https *http.Server - fcgi *http.Server -} - -// Attach attaches controller. Currently only one controller is supported. -func (s *Service) Attach(w roadrunner.Controller) { - s.controller = w -} - -// ProduceCommands changes the default command generator method -func (s *Service) ProduceCommands(producer roadrunner.CommandProducer) { - s.cprod = producer -} - -// AddMiddleware adds new net/http mdwr. -func (s *Service) AddMiddleware(m middleware) { - s.mdwr = append(s.mdwr, m) -} - -// AddListener attaches server event controller. -func (s *Service) AddListener(l func(event int, ctx interface{})) { - s.lsns = append(s.lsns, l) -} - -// Init must return configure svc and return true if svc hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rpc.Service, e env.Environment, log *logrus.Logger) (bool, error) { - s.cfg = cfg - s.log = log - s.env = e - - if r != nil { - if err := r.Register(ID, &rpcServer{s}); err != nil { - return false, err - } - } - - if !cfg.EnableHTTP() && !cfg.EnableTLS() && !cfg.EnableFCGI() { - return false, nil - } - - return true, nil -} - -// Serve serves the svc. -func (s *Service) Serve() error { - s.Lock() - - if s.env != nil { - if err := s.env.Copy(s.cfg.Workers); err != nil { - return nil - } - } - - s.cfg.Workers.CommandProducer = s.cprod - s.cfg.Workers.SetEnv("RR_HTTP", "true") - - s.rr = roadrunner.NewServer(s.cfg.Workers) - s.rr.Listen(s.throw) - - if s.controller != nil { - s.rr.Attach(s.controller) - } - - s.handler = &Handler{cfg: s.cfg, rr: s.rr} - s.handler.Listen(s.throw) - - if s.cfg.EnableHTTP() { - if s.cfg.EnableH2C() { - s.http = &http.Server{Addr: s.cfg.Address, Handler: h2c.NewHandler(s, &http2.Server{})} - } else { - s.http = &http.Server{Addr: s.cfg.Address, Handler: s} - } - } - - if s.cfg.EnableTLS() { - s.https = s.initSSL() - if s.cfg.SSL.RootCA != "" { - err := s.appendRootCa() - if err != nil { - return err - } - } - - if s.cfg.EnableHTTP2() { - if err := s.initHTTP2(); err != nil { - return err - } - } - } - - if s.cfg.EnableFCGI() { - s.fcgi = &http.Server{Handler: s} - } - - s.Unlock() - - if err := s.rr.Start(); err != nil { - return err - } - defer s.rr.Stop() - - err := make(chan error, 3) - - if s.http != nil { - go func() { - httpErr := s.http.ListenAndServe() - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - } else { - err <- nil - } - }() - } - - if s.https != nil { - go func() { - httpErr := s.https.ListenAndServeTLS( - s.cfg.SSL.Cert, - s.cfg.SSL.Key, - ) - - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - return - } - err <- nil - }() - } - - if s.fcgi != nil { - go func() { - httpErr := s.serveFCGI() - if httpErr != nil && httpErr != http.ErrServerClosed { - err <- httpErr - return - } - err <- nil - }() - } - return <-err -} - -// Stop stops the http. -func (s *Service) Stop() { - s.Lock() - defer s.Unlock() - - if s.fcgi != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.fcgi.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - // Stop() error - // push error from goroutines to the channel and block unil error or success shutdown or timeout - s.log.Error(fmt.Errorf("error shutting down the fcgi server, error: %v", err)) - return - } - }() - } - - if s.https != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.https.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the https server, error: %v", err)) - return - } - }() - } - - if s.http != nil { - s.Add(1) - go func() { - defer s.Done() - err := s.http.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - s.log.Error(fmt.Errorf("error shutting down the http server, error: %v", err)) - return - } - }() - } - - s.Wait() -} - -// Server returns associated rr server (if any). -func (s *Service) Server() *roadrunner.Server { - s.Lock() - defer s.Unlock() - - return s.rr -} - -// ServeHTTP handles connection using set of middleware and rr PSR-7 server. -func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if s.https != nil && r.TLS == nil && s.cfg.SSL.Redirect { - target := &url.URL{ - Scheme: "https", - Host: s.tlsAddr(r.Host, false), - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - - http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) - return - } - - if s.https != nil && r.TLS != nil { - w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") - } - - r = attributes.Init(r) - - // chaining middleware - f := s.handler.ServeHTTP - for _, m := range s.mdwr { - f = m(f) - } - f(w, r) -} - -// append RootCA to the https server TLS config -func (s *Service) appendRootCa() error { - rootCAs, err := x509.SystemCertPool() - if err != nil { - s.throw(EventInitSSL, nil) - return nil - } - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } - - CA, err := ioutil.ReadFile(s.cfg.SSL.RootCA) - if err != nil { - s.throw(EventInitSSL, nil) - return err - } - - // should append our CA cert - ok := rootCAs.AppendCertsFromPEM(CA) - if !ok { - return couldNotAppendPemError - } - config := &tls.Config{ - InsecureSkipVerify: false, - RootCAs: rootCAs, - } - s.http.TLSConfig = config - - return nil -} - -// Init https server -func (s *Service) initSSL() *http.Server { - var topCipherSuites []uint16 - var defaultCipherSuitesTLS13 []uint16 - - hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ - hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL - // Keep in sync with crypto/aes/cipher_s390x.go. - hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) - - hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X - - if hasGCMAsm { - // If AES-GCM hardware is provided then prioritise AES-GCM - // cipher suites. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } else { - // Without AES-GCM hardware, we put the ChaCha20-Poly1305 - // cipher suites first. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } - - DefaultCipherSuites := make([]uint16, 0, 22) - DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) - DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) - - server := &http.Server{ - Addr: s.tlsAddr(s.cfg.Address, true), - Handler: s, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - s.throw(EventInitSSL, server) - - return server -} - -// init http/2 server -func (s *Service) initHTTP2() error { - return http2.ConfigureServer(s.https, &http2.Server{ - MaxConcurrentStreams: s.cfg.HTTP2.MaxConcurrentStreams, - }) -} - -// serveFCGI starts FastCGI server. -func (s *Service) serveFCGI() error { - l, err := util.CreateListener(s.cfg.FCGI.Address) - if err != nil { - return err - } - - err = fcgi.Serve(l, s.fcgi.Handler) - if err != nil { - return err - } - - return nil -} - -// throw handles service, server and pool events. -func (s *Service) throw(event int, ctx interface{}) { - for _, l := range s.lsns { - l(event, ctx) - } - - if event == roadrunner.EventServerFailure { - // underlying rr server is dead - s.Stop() - } -} - -// tlsAddr replaces listen or host port with port configured by SSL config. -func (s *Service) tlsAddr(host string, forcePort bool) string { - // remove current forcePort first - host = strings.Split(host, ":")[0] - - if forcePort || s.cfg.SSL.Port != 443 { - host = fmt.Sprintf("%s:%v", host, s.cfg.SSL.Port) - } - - return host -} diff --git a/service/http/service_test.go b/service/http/service_test.go deleted file mode 100644 index f7ee33cc..00000000 --- a/service/http/service_test.go +++ /dev/null @@ -1,759 +0,0 @@ -package http - -import ( - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "os" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - rpcCfg string - envCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - if cfg.httpCfg == "" { - return nil - } - - return &testCfg{target: cfg.httpCfg} - } - - if name == rpc.ID { - return &testCfg{target: cfg.rpcCfg} - } - - if name == env.ID { - return &testCfg{target: cfg.envCfg} - } - - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Service_NoConfig(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{"Enable":true}`}) - assert.Error(t, err) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Service_Configure_Disable(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Service_Configure_Enable(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":8070", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Echo(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6536", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:6536?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Env(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(env.ID, env.NewService(map[string]string{"rr": "test"})) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":10031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php env pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`, envCfg: `{"env_key":"ENV_VALUE"}`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:10031", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 200, r.StatusCode) - assert.Equal(t, "ENV_VALUE", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ErrorEcho(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6030", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echoerr pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - goterr := make(chan interface{}) - s.(*Service).AddListener(func(event int, ctx interface{}) { - if event == roadrunner.EventStderrOutput { - if string(ctx.([]byte)) == "WORLD\n" { - goterr <- nil - } - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6030?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - <-goterr - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - c.Stop() - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Middleware(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - s.(*Service).AddMiddleware(func(f http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/halt" { - w.WriteHeader(500) - _, err := w.Write([]byte("halted")) - if err != nil { - t.Errorf("error writing the data to the http reply: error %v", err) - } - } else { - f(w, r) - } - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6032?hello=world", nil) - if err != nil { - c.Stop() - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - - req, err = http.NewRequest("GET", "http://localhost:6032/halt", nil) - if err != nil { - c.Stop() - return err - } - - r, err = http.DefaultClient.Do(req) - if err != nil { - c.Stop() - return err - } - b, err = ioutil.ReadAll(r.Body) - if err != nil { - c.Stop() - return err - } - - assert.Equal(t, 500, r.StatusCode) - assert.Equal(t, "halted", string(b)) - - err = r.Body.Close() - if err != nil { - c.Stop() - return err - } - c.Stop() - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Listener(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - stop := make(chan interface{}) - s.(*Service).AddListener(func(event int, ctx interface{}) { - if event == roadrunner.EventServerStart { - stop <- nil - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - c.Stop() - assert.True(t, true) - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6034", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "---", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - // assert error - err = c.Serve() - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error2(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6035", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - if err != nil { - return err - } - - // assert error - err = c.Serve() - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Error3(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": ":6036", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers" - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - // assert error - if err == nil { - return err - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_Error4(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{httpCfg: `{ - "enable": true, - "address": "----", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php broken pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`}) - // assert error - if err != nil { - return nil - } - - return err - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func tmpDir() string { - p := os.TempDir() - j := json.ConfigCompatibleWithStandardLibrary - r, _ := j.Marshal(p) - - return string(r) -} diff --git a/service/http/ssl_test.go b/service/http/ssl_test.go deleted file mode 100644 index cf147be9..00000000 --- a/service/http/ssl_test.go +++ /dev/null @@ -1,254 +0,0 @@ -package http - -import ( - "crypto/tls" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -var sslClient = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, -} - -func Test_SSL_Service_Echo(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6029", - "ssl": { - "port": 6900, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "https://localhost:6900?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - - c.Stop() -} - -func Test_SSL_Service_NoRedirect(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6030", - "ssl": { - "port": 6901, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6030?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - assert.Nil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} - -func Test_SSL_Service_Redirect(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6831", - "ssl": { - "port": 6902, - "redirect": true, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:6831?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - assert.NotNil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} - -func Test_SSL_Service_Push(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{httpCfg: `{ - "address": ":6032", - "ssl": { - "port": 6903, - "redirect": true, - "key": "fixtures/server.key", - "cert": "fixtures/server.crt" - }, - "workers":{ - "command": "php ../../tests/http/client.php push pipes", - "pool": {"numWorkers": 1} - } - }`})) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusOK, st) - - // should do nothing - s.(*Service).Stop() - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "https://localhost:6903?hello=world", nil) - assert.NoError(t, err) - - r, err := sslClient.Do(req) - assert.NoError(t, err) - - assert.NotNil(t, r.TLS) - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.Equal(t, "", r.Header.Get("Http2-Push")) - - assert.NoError(t, err) - assert.Equal(t, 201, r.StatusCode) - assert.Equal(t, "WORLD", string(b)) - - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("fail to close the Body: error %v", err2) - } - c.Stop() -} diff --git a/service/http/uploads.go b/service/http/uploads.go deleted file mode 100644 index 39a9eaf2..00000000 --- a/service/http/uploads.go +++ /dev/null @@ -1,160 +0,0 @@ -package http - -import ( - "fmt" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "io" - "io/ioutil" - "mime/multipart" - "os" - "sync" -) - -const ( - // UploadErrorOK - no error, the file uploaded with success. - UploadErrorOK = 0 - - // UploadErrorNoFile - no file was uploaded. - UploadErrorNoFile = 4 - - // UploadErrorNoTmpDir - missing a temporary folder. - UploadErrorNoTmpDir = 5 - - // UploadErrorCantWrite - failed to write file to disk. - UploadErrorCantWrite = 6 - - // UploadErrorExtension - forbidden file extension. - UploadErrorExtension = 7 -) - -// Uploads tree manages uploaded files tree and temporary files. -type Uploads struct { - // associated temp directory and forbidden extensions. - cfg *UploadsConfig - - // pre processed data tree for Uploads. - tree fileTree - - // flat list of all file Uploads. - list []*FileUpload -} - -// MarshalJSON marshal tree tree into JSON. -func (u *Uploads) MarshalJSON() ([]byte, error) { - j := json.ConfigCompatibleWithStandardLibrary - return j.Marshal(u.tree) -} - -// Open moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors -// will be handled individually. -func (u *Uploads) Open(log *logrus.Logger) { - var wg sync.WaitGroup - for _, f := range u.list { - wg.Add(1) - go func(f *FileUpload) { - defer wg.Done() - err := f.Open(u.cfg) - if err != nil && log != nil { - log.Error(fmt.Errorf("error opening the file: error %v", err)) - } - }(f) - } - - wg.Wait() -} - -// Clear deletes all temporary files. -func (u *Uploads) Clear(log *logrus.Logger) { - for _, f := range u.list { - if f.TempFilename != "" && exists(f.TempFilename) { - err := os.Remove(f.TempFilename) - if err != nil && log != nil { - log.Error(fmt.Errorf("error removing the file: error %v", err)) - } - } - } -} - -// FileUpload represents singular file NewUpload. -type FileUpload struct { - // ID contains filename specified by the client. - Name string `json:"name"` - - // Mime contains mime-type provided by the client. - Mime string `json:"mime"` - - // Size of the uploaded file. - Size int64 `json:"size"` - - // Error indicates file upload error (if any). See http://php.net/manual/en/features.file-upload.errors.php - Error int `json:"error"` - - // TempFilename points to temporary file location. - TempFilename string `json:"tmpName"` - - // associated file header - header *multipart.FileHeader -} - -// NewUpload wraps net/http upload into PRS-7 compatible structure. -func NewUpload(f *multipart.FileHeader) *FileUpload { - return &FileUpload{ - Name: f.Filename, - Mime: f.Header.Get("Content-Type"), - Error: UploadErrorOK, - header: f, - } -} - -// Open moves file content into temporary file available for PHP. -// NOTE: -// There is 2 deferred functions, and in case of getting 2 errors from both functions -// error from close of temp file would be overwritten by error from the main file -// STACK -// DEFER FILE CLOSE (2) -// DEFER TMP CLOSE (1) -func (f *FileUpload) Open(cfg *UploadsConfig) (err error) { - if cfg.Forbids(f.Name) { - f.Error = UploadErrorExtension - return nil - } - - file, err := f.header.Open() - if err != nil { - f.Error = UploadErrorNoFile - return err - } - - defer func() { - // close the main file - err = file.Close() - }() - - tmp, err := ioutil.TempFile(cfg.TmpDir(), "upload") - if err != nil { - // most likely cause of this issue is missing tmp dir - f.Error = UploadErrorNoTmpDir - return err - } - - f.TempFilename = tmp.Name() - defer func() { - // close the temp file - err = tmp.Close() - }() - - if f.Size, err = io.Copy(tmp, file); err != nil { - f.Error = UploadErrorCantWrite - } - - return err -} - -// exists if file exists. -func exists(path string) bool { - if _, err := os.Stat(path); os.IsNotExist(err) { - return false - } - return true -} diff --git a/service/http/uploads_config.go b/service/http/uploads_config.go deleted file mode 100644 index 3f655064..00000000 --- a/service/http/uploads_config.go +++ /dev/null @@ -1,45 +0,0 @@ -package http - -import ( - "os" - "path" - "strings" -) - -// UploadsConfig describes file location and controls access to them. -type UploadsConfig struct { - // Dir contains name of directory to control access to. - Dir string - - // Forbid specifies list of file extensions which are forbidden for access. - // Example: .php, .exe, .bat, .htaccess and etc. - Forbid []string -} - -// InitDefaults sets missing values to their default values. -func (cfg *UploadsConfig) InitDefaults() error { - cfg.Forbid = []string{".php", ".exe", ".bat"} - return nil -} - -// TmpDir returns temporary directory. -func (cfg *UploadsConfig) TmpDir() string { - if cfg.Dir != "" { - return cfg.Dir - } - - return os.TempDir() -} - -// Forbids must return true if file extension is not allowed for the upload. -func (cfg *UploadsConfig) Forbids(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range cfg.Forbid { - if ext == v { - return true - } - } - - return false -} diff --git a/service/http/uploads_config_test.go b/service/http/uploads_config_test.go deleted file mode 100644 index 2b6ceebc..00000000 --- a/service/http/uploads_config_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package http - -import ( - "github.com/stretchr/testify/assert" - "os" - "testing" -) - -func TestFsConfig_Forbids(t *testing.T) { - cfg := UploadsConfig{Forbid: []string{".php"}} - - assert.True(t, cfg.Forbids("index.php")) - assert.True(t, cfg.Forbids("index.PHP")) - assert.True(t, cfg.Forbids("phpadmin/index.bak.php")) - assert.False(t, cfg.Forbids("index.html")) -} - -func TestFsConfig_TmpFallback(t *testing.T) { - cfg := UploadsConfig{Dir: "test"} - assert.Equal(t, "test", cfg.TmpDir()) - - cfg = UploadsConfig{Dir: ""} - assert.Equal(t, os.TempDir(), cfg.TmpDir()) -} diff --git a/service/http/uploads_test.go b/service/http/uploads_test.go deleted file mode 100644 index 08177c72..00000000 --- a/service/http/uploads_test.go +++ /dev/null @@ -1,434 +0,0 @@ -package http - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/hex" - "fmt" - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner" - "github.com/stretchr/testify/assert" - "io" - "io/ioutil" - "mime/multipart" - "net/http" - "os" - "testing" - "time" -) - -func TestHandler_Upload_File(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 0, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func TestHandler_Upload_NestedFile(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload[x][y][z][]", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 0, "application/octet-stream") - - assert.Equal(t, `{"upload":{"x":{"y":{"z":[`+fs+`]}}}}`, string(b)) -} - -func TestHandler_Upload_File_NoTmpDir(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: "-----", - Forbid: []string{}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 5, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func TestHandler_Upload_File_Forbids(t *testing.T) { - h := &Handler{ - cfg: &Config{ - MaxRequestSize: 1024, - Uploads: &UploadsConfig{ - Dir: os.TempDir(), - Forbid: []string{".go"}, - }, - }, - rr: roadrunner.NewServer(&roadrunner.ServerConfig{ - Command: "php ../../tests/http/client.php upload pipes", - Relay: "pipes", - Pool: &roadrunner.Config{ - NumWorkers: 1, - AllocateTimeout: 10000000, - DestroyTimeout: 10000000, - }, - }), - } - - assert.NoError(t, h.rr.Start()) - defer h.rr.Stop() - - hs := &http.Server{Addr: ":8021", Handler: h} - defer func() { - err := hs.Shutdown(context.Background()) - if err != nil { - t.Errorf("error during the shutdown: error %v", err) - } - }() - - go func() { - err := hs.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - t.Errorf("error listening the interface: error %v", err) - } - }() - time.Sleep(time.Millisecond * 10) - - var mb bytes.Buffer - w := multipart.NewWriter(&mb) - - f := mustOpen("uploads_test.go") - defer func() { - err := f.Close() - if err != nil { - t.Errorf("failed to close a file: error %v", err) - } - }() - fw, err := w.CreateFormFile("upload", f.Name()) - assert.NotNil(t, fw) - assert.NoError(t, err) - _, err = io.Copy(fw, f) - if err != nil { - t.Errorf("error copying the file: error %v", err) - } - - err = w.Close() - if err != nil { - t.Errorf("error closing the file: error %v", err) - } - - req, err := http.NewRequest("POST", "http://localhost"+hs.Addr, &mb) - assert.NoError(t, err) - - req.Header.Set("Content-Type", w.FormDataContentType()) - - r, err := http.DefaultClient.Do(req) - assert.NoError(t, err) - defer func() { - err := r.Body.Close() - if err != nil { - t.Errorf("error closing the Body: error %v", err) - } - }() - - b, err := ioutil.ReadAll(r.Body) - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, 200, r.StatusCode) - - fs := fileString("uploads_test.go", 7, "application/octet-stream") - - assert.Equal(t, `{"upload":`+fs+`}`, string(b)) -} - -func Test_FileExists(t *testing.T) { - assert.True(t, exists("uploads_test.go")) - assert.False(t, exists("uploads_test.")) -} - -func mustOpen(f string) *os.File { - r, err := os.Open(f) - if err != nil { - panic(err) - } - return r -} - -type fInfo struct { - Name string `json:"name"` - Size int64 `json:"size"` - Mime string `json:"mime"` - Error int `json:"error"` - MD5 string `json:"md5,omitempty"` -} - -func fileString(f string, errNo int, mime string) string { - s, err := os.Stat(f) - if err != nil { - fmt.Println(fmt.Errorf("error stat the file, error: %v", err)) - } - - ff, err := os.Open(f) - if err != nil { - fmt.Println(fmt.Errorf("error opening the file, error: %v", err)) - } - - defer func() { - er := ff.Close() - if er != nil { - fmt.Println(fmt.Errorf("error closing the file, error: %v", er)) - } - }() - - h := md5.New() - _, err = io.Copy(h, ff) - if err != nil { - fmt.Println(fmt.Errorf("error copying the file, error: %v", err)) - } - - v := &fInfo{ - Name: s.Name(), - Size: s.Size(), - Error: errNo, - Mime: mime, - MD5: hex.EncodeToString(h.Sum(nil)), - } - - if errNo != 0 { - v.MD5 = "" - v.Size = 0 - } - - j := json.ConfigCompatibleWithStandardLibrary - r, err := j.Marshal(v) - if err != nil { - fmt.Println(fmt.Errorf("error marshalling fInfo, error: %v", err)) - } - return string(r) - -} diff --git a/service/limit/config.go b/service/limit/config.go deleted file mode 100644 index 203db11b..00000000 --- a/service/limit/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "time" -) - -// Config of Limit service. -type Config struct { - // Interval defines the update duration for underlying controllers, default 1s. - Interval time.Duration - - // Services declares list of services to be watched. - Services map[string]*controllerConfig -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - // Always use second based definition for time durations - if c.Interval < time.Microsecond { - c.Interval = time.Second * time.Duration(c.Interval.Nanoseconds()) - } - - return nil -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Interval = time.Second - - return nil -} - -// Controllers returns list of defined Services -func (c *Config) Controllers(l listener) (controllers map[string]roadrunner.Controller) { - controllers = make(map[string]roadrunner.Controller) - - for name, cfg := range c.Services { - controllers[name] = &controller{lsn: l, tick: c.Interval, cfg: cfg} - } - - return controllers -} diff --git a/service/limit/config_test.go b/service/limit/config_test.go deleted file mode 100644 index c79836b8..00000000 --- a/service/limit/config_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package limit - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"enable: true}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Controller_Default(t *testing.T) { - cfg := &mockCfg{` -{ - "services":{ - "http": { - "TTL": 1 - } - } -} -`} - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("failed to InitDefaults: error %v", err) - } - - assert.NoError(t, c.Hydrate(cfg)) - assert.Equal(t, time.Second, c.Interval) - - list := c.Controllers(func(event int, ctx interface{}) { - }) - - sc := list["http"] - - assert.Equal(t, time.Second, sc.(*controller).tick) -} diff --git a/service/limit/controller.go b/service/limit/controller.go deleted file mode 100644 index 24a158f7..00000000 --- a/service/limit/controller.go +++ /dev/null @@ -1,166 +0,0 @@ -package limit - -import ( - "fmt" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/util" - "time" -) - -const ( - // EventMaxMemory caused when worker consumes more memory than allowed. - EventMaxMemory = iota + 8000 - - // EventTTL thrown when worker is removed due TTL being reached. Context is rr.WorkerError - EventTTL - - // EventIdleTTL triggered when worker spends too much time at rest. - EventIdleTTL - - // EventExecTTL triggered when worker spends too much time doing the task (max_execution_time). - EventExecTTL -) - -// handles controller events -type listener func(event int, ctx interface{}) - -// defines the controller behaviour -type controllerConfig struct { - // MaxMemory defines maximum amount of memory allowed for worker. In megabytes. - MaxMemory uint64 - - // TTL defines maximum time worker is allowed to live. - TTL int64 - - // IdleTTL defines maximum duration worker can spend in idle mode. - IdleTTL int64 - - // ExecTTL defines maximum lifetime per job. - ExecTTL int64 -} - -type controller struct { - lsn listener - tick time.Duration - cfg *controllerConfig - - // list of workers which are currently working - sw *stateFilter - - stop chan interface{} -} - -// control the pool state -func (c *controller) control(p roadrunner.Pool) { - c.loadWorkers(p) - - now := time.Now() - - if c.cfg.ExecTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateWorking, - now.Add(-time.Second*time.Duration(c.cfg.ExecTTL)), - ) { - eID := w.State().NumExecs() - err := fmt.Errorf("max exec time reached (%vs)", c.cfg.ExecTTL) - - // make sure worker still on initial request - if p.Remove(w, err) && w.State().NumExecs() == eID { - go func() { - err := w.Kill() - if err != nil { - fmt.Printf("error killing worker with PID number: %d, created: %s", w.Pid, w.Created) - } - }() - c.report(EventExecTTL, w, err) - } - } - } - - // locale workers which are in idle mode for too long - if c.cfg.IdleTTL != 0 { - for _, w := range c.sw.find( - roadrunner.StateReady, - now.Add(-time.Second*time.Duration(c.cfg.IdleTTL)), - ) { - err := fmt.Errorf("max idle time reached (%vs)", c.cfg.IdleTTL) - if p.Remove(w, err) { - c.report(EventIdleTTL, w, err) - } - } - } -} - -func (c *controller) loadWorkers(p roadrunner.Pool) { - now := time.Now() - - for _, w := range p.Workers() { - if w.State().Value() == roadrunner.StateInvalid { - // skip duplicate assessment - continue - } - - s, err := util.WorkerState(w) - if err != nil { - continue - } - - if c.cfg.TTL != 0 && now.Sub(w.Created).Seconds() >= float64(c.cfg.TTL) { - err := fmt.Errorf("max TTL reached (%vs)", c.cfg.TTL) - if p.Remove(w, err) { - c.report(EventTTL, w, err) - } - continue - } - - if c.cfg.MaxMemory != 0 && s.MemoryUsage >= c.cfg.MaxMemory*1024*1024 { - err := fmt.Errorf("max allowed memory reached (%vMB)", c.cfg.MaxMemory) - if p.Remove(w, err) { - c.report(EventMaxMemory, w, err) - } - continue - } - - // control the worker state changes - c.sw.push(w) - } - - c.sw.sync(now) -} - -// throw controller event -func (c *controller) report(event int, worker *roadrunner.Worker, caused error) { - if c.lsn != nil { - c.lsn(event, roadrunner.WorkerError{Worker: worker, Caused: caused}) - } -} - -// Attach controller to the pool -func (c *controller) Attach(pool roadrunner.Pool) roadrunner.Controller { - wp := &controller{ - tick: c.tick, - lsn: c.lsn, - cfg: c.cfg, - sw: newStateFilter(), - stop: make(chan interface{}), - } - - go func(wp *controller, pool roadrunner.Pool) { - ticker := time.NewTicker(wp.tick) - for { - select { - case <-ticker.C: - wp.control(pool) - case <-wp.stop: - return - } - } - }(wp, pool) - - return wp -} - -// Detach controller from the pool. -func (c *controller) Detach() { - close(c.stop) -} diff --git a/service/limit/service.go b/service/limit/service.go deleted file mode 100644 index c0b4139c..00000000 --- a/service/limit/service.go +++ /dev/null @@ -1,39 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" -) - -// ID defines controller service name. -const ID = "limit" - -// Service to control the state of rr service inside other services. -type Service struct { - lsns []func(event int, ctx interface{}) -} - -// Init controller service -func (s *Service) Init(cfg *Config, c service.Container) (bool, error) { - // mount Services to designated services - for id, watcher := range cfg.Controllers(s.throw) { - svc, _ := c.Get(id) - if ctrl, ok := svc.(roadrunner.Attacher); ok { - ctrl.Attach(watcher) - } - } - - return true, nil -} - -// AddListener attaches server event controller. -func (s *Service) AddListener(l func(event int, ctx interface{})) { - s.lsns = append(s.lsns, l) -} - -// throw handles service, server and pool events. -func (s *Service) throw(event int, ctx interface{}) { - for _, l := range s.lsns { - l(event, ctx) - } -} diff --git a/service/limit/service_test.go b/service/limit/service_test.go deleted file mode 100644 index b358c1c1..00000000 --- a/service/limit/service_test.go +++ /dev/null @@ -1,500 +0,0 @@ -package limit - -import ( - "fmt" - "github.com/cenkalti/backoff/v4" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - limitCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - if cfg.httpCfg == "" { - return nil - } - - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.limitCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - - if cl, ok := out.(*Config); ok { - // to speed up tests - cl.Interval = time.Millisecond - } - - return err -} - -func Test_Service_PidEcho(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":17029", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - req, err := http.NewRequest("GET", "http://localhost:17029", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - - assert.Equal(t, getPID(s), string(b)) - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - c.Stop() - return nil - - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7030", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "ttl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7030", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7030?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} - -func Test_Service_ListenerPlusIdleTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7031", - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "idleTtl": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventIdleTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7031", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return err - } - assert.Equal(t, lastPID, string(b)) - - <-captured - - // clean state - req, err = http.NewRequest("GET", "http://localhost:7031?new", nil) - if err != nil { - return err - } - - _, err = http.DefaultClient.Do(req) - if err != nil { - return err - } - - assert.NotEqual(t, lastPID, getPID(s)) - - c.Stop() - err2 := r.Body.Close() - if err2 != nil { - t.Errorf("error during the body closing: error %v", err2) - } - return nil - }, bkoff) - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxExecTTL(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7032", - "workers":{ - "command": "php ../../tests/http/client.php stuck pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "execTTL": 1 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventExecTTL { - close(captured) - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - req, err := http.NewRequest("GET", "http://localhost:7032", nil) - if err != nil { - return err - } - - r, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - assert.Equal(t, 500, r.StatusCode) - - <-captured - - c.Stop() - return nil - }, bkoff) - - if err != nil { - t.Fatal(err) - } -} - -func Test_Service_Listener_MaxMemoryUsage(t *testing.T) { - bkoff := backoff.NewExponentialBackOff() - bkoff.MaxElapsedTime = time.Second * 15 - - err := backoff.Retry(func() error { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - err := c.Init(&testCfg{ - httpCfg: `{ - "address": ":7033", - "workers":{ - "command": "php ../../tests/http/client.php memleak pipes", - "pool": {"numWorkers": 1} - } - }`, - limitCfg: `{ - "services": { - "http": { - "maxMemory": 10 - } - } - }`, - }) - if err != nil { - return err - } - - s, _ := c.Get(rrhttp.ID) - assert.NotNil(t, s) - - l, _ := c.Get(ID) - captured := make(chan interface{}) - once := false - l.(*Service).AddListener(func(event int, ctx interface{}) { - if event == EventMaxMemory && !once { - close(captured) - once = true - } - }) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - - time.Sleep(time.Millisecond * 100) - - lastPID := getPID(s) - - req, err := http.NewRequest("GET", "http://localhost:7033", nil) - if err != nil { - return err - } - - for { - select { - case <-captured: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - assert.NotEqual(t, lastPID, getPID(s)) - c.Stop() - return nil - default: - _, err := http.DefaultClient.Do(req) - if err != nil { - c.Stop() - t.Errorf("error during sending the http request: error %v", err) - } - c.Stop() - return nil - } - } - }, bkoff) - - if err != nil { - t.Fatal(err) - } - -} -func getPID(s interface{}) string { - if len(s.(*rrhttp.Service).Server().Workers()) > 0 { - w := s.(*rrhttp.Service).Server().Workers()[0] - return fmt.Sprintf("%v", *w.Pid) - } else { - panic("no workers") - } -} diff --git a/service/limit/state_filter.go b/service/limit/state_filter.go deleted file mode 100644 index cd2eca94..00000000 --- a/service/limit/state_filter.go +++ /dev/null @@ -1,58 +0,0 @@ -package limit - -import ( - "github.com/spiral/roadrunner" - "time" -) - -type stateFilter struct { - prev map[*roadrunner.Worker]state - next map[*roadrunner.Worker]state -} - -type state struct { - state int64 - numExecs int64 - since time.Time -} - -func newStateFilter() *stateFilter { - return &stateFilter{ - prev: make(map[*roadrunner.Worker]state), - next: make(map[*roadrunner.Worker]state), - } -} - -// add new worker to be watched -func (sw *stateFilter) push(w *roadrunner.Worker) { - sw.next[w] = state{state: w.State().Value(), numExecs: w.State().NumExecs()} -} - -// update worker states. -func (sw *stateFilter) sync(t time.Time) { - for w := range sw.prev { - if _, ok := sw.next[w]; !ok { - delete(sw.prev, w) - } - } - - for w, s := range sw.next { - ps, ok := sw.prev[w] - if !ok || ps.state != s.state || ps.numExecs != s.numExecs { - sw.prev[w] = state{state: s.state, numExecs: s.numExecs, since: t} - } - - delete(sw.next, w) - } -} - -// find all workers which spend given amount of time in a specific state. -func (sw *stateFilter) find(state int64, since time.Time) (workers []*roadrunner.Worker) { - for w, s := range sw.prev { - if s.state == state && s.since.Before(since) { - workers = append(workers, w) - } - } - - return -} diff --git a/service/metrics/config.go b/service/metrics/config.go deleted file mode 100644 index c95fd940..00000000 --- a/service/metrics/config.go +++ /dev/null @@ -1,136 +0,0 @@ -package metrics - -import ( - "fmt" - "github.com/prometheus/client_golang/prometheus" - "github.com/spiral/roadrunner/service" -) - -// Config configures metrics service. -type Config struct { - // Address to listen - Address string - - // Collect define application specific metrics. - Collect map[string]Collector -} - -type NamedCollector struct { - // Name of the collector - Name string `json:"name"` - - // Collector structure - Collector `json:"collector"` -} - -// CollectorType represents prometheus collector types -type CollectorType string - -const ( - // Histogram type - Histogram CollectorType = "histogram" - - // Gauge type - Gauge CollectorType = "gauge" - - // Counter type - Counter CollectorType = "counter" - - // Summary type - Summary CollectorType = "summary" -) - -// Collector describes single application specific metric. -type Collector struct { - // Namespace of the metric. - Namespace string `json:"namespace"` - // Subsystem of the metric. - Subsystem string `json:"subsystem"` - // Collector type (histogram, gauge, counter, summary). - Type CollectorType `json:"type"` - // Help of collector. - Help string `json:"help"` - // Labels for vectorized metrics. - Labels []string `json:"labels"` - // Buckets for histogram metric. - Buckets []float64 `json:"buckets"` -} - -// Hydrate configuration. -func (c *Config) Hydrate(cfg service.Config) error { - return cfg.Unmarshal(c) -} - -// register application specific metrics. -func (c *Config) getCollectors() (map[string]prometheus.Collector, error) { - if c.Collect == nil { - return nil, nil - } - - collectors := make(map[string]prometheus.Collector) - - for name, m := range c.Collect { - var collector prometheus.Collector - switch m.Type { - case Histogram: - opts := prometheus.HistogramOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - Buckets: m.Buckets, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewHistogramVec(opts, m.Labels) - } else { - collector = prometheus.NewHistogram(opts) - } - case Gauge: - opts := prometheus.GaugeOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewGaugeVec(opts, m.Labels) - } else { - collector = prometheus.NewGauge(opts) - } - case Counter: - opts := prometheus.CounterOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewCounterVec(opts, m.Labels) - } else { - collector = prometheus.NewCounter(opts) - } - case Summary: - opts := prometheus.SummaryOpts{ - Name: name, - Namespace: m.Namespace, - Subsystem: m.Subsystem, - Help: m.Help, - } - - if len(m.Labels) != 0 { - collector = prometheus.NewSummaryVec(opts, m.Labels) - } else { - collector = prometheus.NewSummary(opts) - } - default: - return nil, fmt.Errorf("invalid metric type `%s` for `%s`", m.Type, name) - } - - collectors[name] = collector - } - - return collectors, nil -} diff --git a/service/metrics/config_test.go b/service/metrics/config_test.go deleted file mode 100644 index a64e9047..00000000 --- a/service/metrics/config_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package metrics - -import ( - json "github.com/json-iterator/go" - "github.com/prometheus/client_golang/prometheus" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate_Error1(t *testing.T) { - cfg := &mockCfg{`{"request": {"From": "Something"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &mockCfg{`{"dir": "/dir/"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Metrics(t *testing.T) { - cfg := &mockCfg{`{ -"collect":{ - "metric1":{"type": "gauge"}, - "metric2":{ "type": "counter"}, - "metric3":{"type": "summary"}, - "metric4":{"type": "histogram"} -} -}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - - m, err := c.getCollectors() - assert.NoError(t, err) - - assert.IsType(t, prometheus.NewGauge(prometheus.GaugeOpts{}), m["metric1"]) - assert.IsType(t, prometheus.NewCounter(prometheus.CounterOpts{}), m["metric2"]) - assert.IsType(t, prometheus.NewSummary(prometheus.SummaryOpts{}), m["metric3"]) - assert.IsType(t, prometheus.NewHistogram(prometheus.HistogramOpts{}), m["metric4"]) -} - -func Test_Config_MetricsVector(t *testing.T) { - cfg := &mockCfg{`{ -"collect":{ - "metric1":{"type": "gauge","labels":["label"]}, - "metric2":{ "type": "counter","labels":["label"]}, - "metric3":{"type": "summary","labels":["label"]}, - "metric4":{"type": "histogram","labels":["label"]} -} -}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) - - m, err := c.getCollectors() - assert.NoError(t, err) - - assert.IsType(t, prometheus.NewGaugeVec(prometheus.GaugeOpts{}, []string{}), m["metric1"]) - assert.IsType(t, prometheus.NewCounterVec(prometheus.CounterOpts{}, []string{}), m["metric2"]) - assert.IsType(t, prometheus.NewSummaryVec(prometheus.SummaryOpts{}, []string{}), m["metric3"]) - assert.IsType(t, prometheus.NewHistogramVec(prometheus.HistogramOpts{}, []string{}), m["metric4"]) -} diff --git a/service/metrics/rpc.go b/service/metrics/rpc.go deleted file mode 100644 index 377d6173..00000000 --- a/service/metrics/rpc.go +++ /dev/null @@ -1,260 +0,0 @@ -package metrics - -import ( - "fmt" - "github.com/prometheus/client_golang/prometheus" -) - -type rpcServer struct { - svc *Service -} - -// Metric represent single metric produced by the application. -type Metric struct { - // Collector name. - Name string - - // Collector value. - Value float64 - - // Labels associated with metric. Only for vector metrics. Must be provided in a form of label values. - Labels []string -} - -// Add new metric to the designated collector. -func (rpc *rpcServer) Add(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Add(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Add(m.Value) - - case prometheus.Counter: - c.Add(m.Value) - - case *prometheus.CounterVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Add(m.Value) - - default: - return fmt.Errorf("collector `%s` does not support method `Add`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} - -// Sub subtract the value from the specific metric (gauge only). -func (rpc *rpcServer) Sub(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Sub(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Sub(m.Value) - default: - return fmt.Errorf("collector `%s` does not support method `Sub`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} - -// Observe the value (histogram and summary only). -func (rpc *rpcServer) Observe(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case *prometheus.SummaryVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Observe(m.Value) - - case prometheus.Histogram: - c.Observe(m.Value) - - case *prometheus.HistogramVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Observe(m.Value) - default: - return fmt.Errorf("collector `%s` does not support method `Observe`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} -// Declare is used to register new collector in prometheus -// THE TYPES ARE: -// NamedCollector -> Collector with the name -// bool -> RPC reply value -// RETURNS: -// error -func (rpc *rpcServer) Declare(c *NamedCollector, ok *bool) (err error) { - // MustRegister could panic, so, to return error and not shutdown whole app - // we recover and return error - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - if rpc.svc.Collector(c.Name) != nil { - *ok = false - // alternative is to return error - // fmt.Errorf("tried to register existing collector with the name `%s`", c.Name) - return nil - } - - var collector prometheus.Collector - switch c.Type { - case Histogram: - opts := prometheus.HistogramOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - Buckets: c.Buckets, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewHistogramVec(opts, c.Labels) - } else { - collector = prometheus.NewHistogram(opts) - } - case Gauge: - opts := prometheus.GaugeOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewGaugeVec(opts, c.Labels) - } else { - collector = prometheus.NewGauge(opts) - } - case Counter: - opts := prometheus.CounterOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewCounterVec(opts, c.Labels) - } else { - collector = prometheus.NewCounter(opts) - } - case Summary: - opts := prometheus.SummaryOpts{ - Name: c.Name, - Namespace: c.Namespace, - Subsystem: c.Subsystem, - Help: c.Help, - } - - if len(c.Labels) != 0 { - collector = prometheus.NewSummaryVec(opts, c.Labels) - } else { - collector = prometheus.NewSummary(opts) - } - - default: - return fmt.Errorf("unknown collector type `%s`", c.Type) - - } - - // add collector to sync.Map - rpc.svc.collectors.Store(c.Name, collector) - // that method might panic, we handle it by recover - rpc.svc.MustRegister(collector) - - *ok = true - return nil -} - -// Set the metric value (only for gaude). -func (rpc *rpcServer) Set(m *Metric, ok *bool) (err error) { - defer func() { - if r, fail := recover().(error); fail { - err = r - } - }() - - c := rpc.svc.Collector(m.Name) - if c == nil { - return fmt.Errorf("undefined collector `%s`", m.Name) - } - - switch c := c.(type) { - case prometheus.Gauge: - c.Set(m.Value) - - case *prometheus.GaugeVec: - if len(m.Labels) == 0 { - return fmt.Errorf("required labels for collector `%s`", m.Name) - } - - c.WithLabelValues(m.Labels...).Set(m.Value) - - default: - return fmt.Errorf("collector `%s` does not support method `Set`", m.Name) - } - - // RPC, set ok to true as return value. Need by rpc.Call reply argument - *ok = true - return nil -} diff --git a/service/metrics/rpc_test.go b/service/metrics/rpc_test.go deleted file mode 100644 index 2fc4bc32..00000000 --- a/service/metrics/rpc_test.go +++ /dev/null @@ -1,861 +0,0 @@ -package metrics - -import ( - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - rpc2 "net/rpc" - "strconv" - "testing" - "time" -) - -var port = 5004 - -func setup(t *testing.T, metric string, portNum string) (*rpc2.Client, service.Container) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rpc.ID, &rpc.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - rpcCfg: `{"enable":true, "listen":"tcp://:` + strconv.Itoa(port) + `"}`, - metricsCfg: `{ - "address": "localhost:` + portNum + `", - "collect":{ - ` + metric + ` - } - }`})) - - // rotate ports for travis - port++ - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - s2, _ := c.Get(rpc.ID) - rs := s2.(*rpc.Service) - - assert.True(t, s.(*Service).Enabled()) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 200) - - client, err := rs.Client() - assert.NoError(t, err) - if err != nil { - panic(err) - } - - return client, c -} - -func Test_Set_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge" - }`, - "2112", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2112/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 100`) -} - -func Test_Set_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2113", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2113/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 100`) -} - -func Test_Set_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2114", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Set_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2115", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Set_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2116", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -func Test_Set_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2117", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Set", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -// sub - -func Test_Sub_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge" - }`, - "2118", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 10.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2118/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 90`) -} - -func Test_Sub_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2119", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 10.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2119/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 90`) -} - -func Test_Register_RPC_Histogram(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2319", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_histogram", - Collector: Collector{ - Namespace: "test_histogram", - Subsystem: "test_histogram", - Type: Histogram, - Help: "test_histogram", - Labels: nil, - Buckets: []float64{0.1, 0.2, 0.5}, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // histogram does not support Add, should be an error - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "custom_histogram", - }, &ok2)) - // ok should became false - assert.False(t, ok2) - - out, _, err := get("http://localhost:2319/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `TYPE test_histogram_test_histogram_custom_histogram histogram`) - - // check buckets - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.1"} 0`) - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.2"} 0`) - assert.Contains(t, out, `test_histogram_test_histogram_custom_histogram_bucket{le="0.5"} 0`) -} - -func Test_Register_RPC_Gauge(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2324", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_gauge", - Collector: Collector{ - Namespace: "test_gauge", - Subsystem: "test_gauge", - Type: Gauge, - Help: "test_gauge", - Labels: []string{"type", "section"}, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_gauge - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "custom_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok2)) - // ok should became true - assert.True(t, ok2) - - // Subtract from custom runtime metric - var ok3 bool - assert.NoError(t, client.Call("metrics.Sub", Metric{ - Name: "custom_gauge", - Value: 10.0, - Labels: []string{"core", "first"}, - }, &ok3)) - assert.True(t, ok3) - - out, _, err := get("http://localhost:2324/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_gauge_test_gauge_custom_gauge{section="first",type="core"} 90`) -} - -func Test_Register_RPC_Counter(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2328", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_counter", - Collector: Collector{ - Namespace: "test_counter", - Subsystem: "test_counter", - Type: Counter, - Help: "test_counter", - Labels: []string{"type", "section"}, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_counter - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "custom_counter", - Value: 100.0, - Labels: []string{"type2", "section2"}, - }, &ok2)) - // ok should became true - assert.True(t, ok2) - - out, _, err := get("http://localhost:2328/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_counter_test_counter_custom_counter{section="section2",type="type2"} 100`) -} - -func Test_Register_RPC_Summary(t *testing.T) { - // FOR register method, setup used just to init the rpc - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "6666", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Declare", &NamedCollector{ - Name: "custom_summary", - Collector: Collector{ - Namespace: "test_summary", - Subsystem: "test_summary", - Type: Summary, - Help: "test_summary", - Labels: nil, - Buckets: nil, - }, - }, &ok)) - assert.True(t, ok) - - var ok2 bool - // Add to custom_summary is not supported - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "custom_summary", - Value: 100.0, - Labels: []string{"type22", "section22"}, - }, &ok2)) - // ok should became false - assert.False(t, ok2) - - out, _, err := get("http://localhost:6666/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `test_summary_test_summary_custom_summary_sum 0`) - assert.Contains(t, out, `test_summary_test_summary_custom_summary_count 0`) -} - -func Test_Sub_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2120", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Sub_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2121", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Sub_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "gauge", - "labels": ["type", "section"] - }`, - "2122", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -func Test_Sub_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2123", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Sub", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} - -// -- observe - -func Test_Observe_RPC(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram" - }`, - "2124", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2124/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2125", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2125/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2126", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2127", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2128", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -// -- observe summary - -func Test_Observe2_RPC(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary" - }`, - "2129", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2129/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe2_RPC_Invalid(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary" - }`, - "2130", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_Invalid_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "gauge" - }`, - "2131", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -func Test_Observe2_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2132", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2132/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_histogram`) -} - -func Test_Observe2_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2133", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2134", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) -} - -func Test_Observe2_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_histogram":{ - "type": "summary", - "labels": ["type", "section"] - }`, - "2135", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Observe", Metric{ - Name: "user_histogram", - Value: 100.0, - }, &ok)) -} - -// add -func Test_Add_RPC(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter" - }`, - "2136", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2136/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge 100`) -} - -func Test_Add_RPC_Vector(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2137", - ) - defer c.Stop() - - var ok bool - assert.NoError(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"core", "first"}, - }, &ok)) - assert.True(t, ok) - - out, _, err := get("http://localhost:2137/metrics") - assert.NoError(t, err) - assert.Contains(t, out, `user_gauge{section="first",type="core"} 100`) -} - -func Test_Add_RPC_CollectorError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2138", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge_2", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2139", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - Labels: []string{"missing"}, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError_2(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "counter", - "labels": ["type", "section"] - }`, - "2140", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) - - assert.False(t, ok) -} - -func Test_Add_RPC_MetricError_3(t *testing.T) { - client, c := setup( - t, - `"user_gauge":{ - "type": "histogram", - "labels": ["type", "section"] - }`, - "2141", - ) - defer c.Stop() - - var ok bool - assert.Error(t, client.Call("metrics.Add", Metric{ - Name: "user_gauge", - Value: 100.0, - }, &ok)) -} diff --git a/service/metrics/service.go b/service/metrics/service.go deleted file mode 100644 index 4656ae04..00000000 --- a/service/metrics/service.go +++ /dev/null @@ -1,191 +0,0 @@ -package metrics - -// todo: declare metric at runtime - -import ( - "context" - "crypto/tls" - "fmt" - "net/http" - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner/service/rpc" - "golang.org/x/sys/cpu" -) - -const ( - // ID declares public service name. - ID = "metrics" - // maxHeaderSize declares max header size for prometheus server - maxHeaderSize = 1024 * 1024 * 100 // 104MB -) - -// Service to manage application metrics using Prometheus. -type Service struct { - cfg *Config - log *logrus.Logger - mu sync.Mutex - http *http.Server - collectors sync.Map - registry *prometheus.Registry -} - -// Init service. -func (s *Service) Init(cfg *Config, r *rpc.Service, log *logrus.Logger) (bool, error) { - s.cfg = cfg - s.log = log - s.registry = prometheus.NewRegistry() - - s.registry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) - s.registry.MustRegister(prometheus.NewGoCollector()) - - if r != nil { - if err := r.Register(ID, &rpcServer{s}); err != nil { - return false, err - } - } - - return true, nil -} - -// Enabled indicates that server is able to collect metrics. -func (s *Service) Enabled() bool { - return s.cfg != nil -} - -// Register new prometheus collector. -func (s *Service) Register(c prometheus.Collector) error { - return s.registry.Register(c) -} - -// MustRegister registers new collector or fails with panic. -func (s *Service) MustRegister(c prometheus.Collector) { - s.registry.MustRegister(c) -} - -// Serve prometheus metrics service. -func (s *Service) Serve() error { - // register application specific metrics - collectors, err := s.cfg.getCollectors() - if err != nil { - return err - } - - for name, collector := range collectors { - if err := s.registry.Register(collector); err != nil { - return err - } - - s.collectors.Store(name, collector) - } - - s.mu.Lock() - - var topCipherSuites []uint16 - var defaultCipherSuitesTLS13 []uint16 - - hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ - hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL - // Keep in sync with crypto/aes/cipher_s390x.go. - hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM) - - hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X - - if hasGCMAsm { - // If AES-GCM hardware is provided then prioritise AES-GCM - // cipher suites. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } else { - // Without AES-GCM hardware, we put the ChaCha20-Poly1305 - // cipher suites first. - topCipherSuites = []uint16{ - tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, - } - defaultCipherSuitesTLS13 = []uint16{ - tls.TLS_CHACHA20_POLY1305_SHA256, - tls.TLS_AES_128_GCM_SHA256, - tls.TLS_AES_256_GCM_SHA384, - } - } - - DefaultCipherSuites := make([]uint16, 0, 22) - DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...) - DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...) - - s.http = &http.Server{ - Addr: s.cfg.Address, - Handler: promhttp.HandlerFor(s.registry, promhttp.HandlerOpts{}), - IdleTimeout: time.Hour * 24, - ReadTimeout: time.Minute * 60, - MaxHeaderBytes: maxHeaderSize, - ReadHeaderTimeout: time.Minute * 60, - WriteTimeout: time.Minute * 60, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - s.mu.Unlock() - - err = s.http.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - return err - } - - return nil -} - -// Stop prometheus metrics service. -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.http != nil { - // gracefully stop server - go func() { - err := s.http.Shutdown(context.Background()) - if err != nil { - // Function should be Stop() error - s.log.Error(fmt.Errorf("error shutting down the metrics server: error %v", err)) - } - }() - } -} - -// Collector returns application specific collector by name or nil if collector not found. -func (s *Service) Collector(name string) prometheus.Collector { - collector, ok := s.collectors.Load(name) - if !ok { - return nil - } - - return collector.(prometheus.Collector) -} diff --git a/service/metrics/service_test.go b/service/metrics/service_test.go deleted file mode 100644 index cdb81147..00000000 --- a/service/metrics/service_test.go +++ /dev/null @@ -1,247 +0,0 @@ -package metrics - -import ( - json "github.com/json-iterator/go" - "github.com/prometheus/client_golang/prometheus" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/rpc" - "github.com/stretchr/testify/assert" - "io/ioutil" - "net/http" - "testing" - "time" -) - -type testCfg struct { - rpcCfg string - metricsCfg string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == ID { - return &testCfg{target: cfg.metricsCfg} - } - - if name == rpc.ID { - return &testCfg{target: cfg.rpcCfg} - } - - return nil -} - -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - err := j.Unmarshal([]byte(cfg.target), out) - return err -} - -// get request and return body -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - return string(b), r, err -} - -func TestService_Serve(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2116" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - out, _, err := get("http://localhost:2116/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "go_gc_duration_seconds") -} - -func Test_ServiceCustomMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2115" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - collector := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "my_gauge", - Help: "My gauge value", - }) - - assert.NoError(t, s.(*Service).Register(collector)) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - collector.Set(100) - - out, _, err := get("http://localhost:2115/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "my_gauge 100") -} - -func Test_ServiceCustomMetricMust(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2114" - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - collector := prometheus.NewGauge(prometheus.GaugeOpts{ - Name: "my_gauge_2", - Help: "My gauge value", - }) - - s.(*Service).MustRegister(collector) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - collector.Set(100) - - out, _, err := get("http://localhost:2114/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "my_gauge_2 100") -} - -func Test_ConfiguredMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2113", - "collect":{ - "user_gauge":{ - "type": "gauge" - } - } - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - assert.True(t, s.(*Service).Enabled()) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("error during the Serve: error %v", err) - } - }() - time.Sleep(time.Millisecond * 100) - defer c.Stop() - - s.(*Service).Collector("user_gauge").(prometheus.Gauge).Set(100) - - assert.Nil(t, s.(*Service).Collector("invalid")) - - out, _, err := get("http://localhost:2113/metrics") - assert.NoError(t, err) - - assert.Contains(t, out, "user_gauge 100") -} - -func Test_ConfiguredDuplicateMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2112", - "collect":{ - "go_gc_duration_seconds":{ - "type": "gauge" - } - } - }`})) - - s, _ := c.Get(ID) - assert.NotNil(t, s) - - assert.True(t, s.(*Service).Enabled()) - - assert.Error(t, c.Serve()) -} - -func Test_ConfiguredInvalidMetric(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{metricsCfg: `{ - "address": "localhost:2112", - "collect":{ - "user_gauge":{ - "type": "invalid" - } - } - - }`})) - - assert.Error(t, c.Serve()) -} diff --git a/service/reload/config.go b/service/reload/config.go deleted file mode 100644 index efc71972..00000000 --- a/service/reload/config.go +++ /dev/null @@ -1,72 +0,0 @@ -package reload - -import ( - "errors" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "time" -) - -// Config is a Reload configuration point. -type Config struct { - // Interval is a global refresh interval - Interval time.Duration - - // Patterns is a global file patterns to watch. It will be applied to every directory in project - Patterns []string - - // Services is set of services which would be reloaded in case of FS changes - Services map[string]ServiceConfig -} - -type ServiceConfig struct { - // Enabled indicates that service must be watched, doest not required when any other option specified - Enabled bool - - // Recursive is options to use nested files from root folder - Recursive bool - - // Patterns is per-service specific files to watch - Patterns []string - - // Dirs is per-service specific dirs which will be combined with Patterns - Dirs []string - - // Ignore is set of files which would not be watched - Ignore []string - - // service is a link to service to restart - service *roadrunner.Controllable -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return nil -} - -// InitDefaults sets missing values to their default values. -func (c *Config) InitDefaults() error { - c.Interval = time.Second - c.Patterns = []string{".php"} - - return nil -} - -// Valid validates the configuration. -func (c *Config) Valid() error { - if c.Interval < time.Second { - return errors.New("too short interval") - } - - if c.Services == nil { - return errors.New("should add at least 1 service") - } else if len(c.Services) == 0 { - return errors.New("service initialized, however, no config added") - } - - return nil -} diff --git a/service/reload/config_test.go b/service/reload/config_test.go deleted file mode 100644 index 600975d3..00000000 --- a/service/reload/config_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package reload - -import ( - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -func Test_Config_Valid(t *testing.T) { - services := make(map[string]ServiceConfig) - services["test"] = ServiceConfig{ - Recursive: false, - Patterns: nil, - Dirs: nil, - Ignore: nil, - service: nil, - } - - cfg := &Config{ - Interval: time.Second, - Patterns: nil, - Services: services, - } - assert.NoError(t, cfg.Valid()) -} - -func Test_Fake_ServiceConfig(t *testing.T) { - services := make(map[string]ServiceConfig) - cfg := &Config{ - Interval: time.Microsecond, - Patterns: nil, - Services: services, - } - assert.Error(t, cfg.Valid()) -} - -func Test_Interval(t *testing.T) { - services := make(map[string]ServiceConfig) - services["test"] = ServiceConfig{ - Enabled: false, - Recursive: false, - Patterns: nil, - Dirs: nil, - Ignore: nil, - service: nil, - } - - cfg := &Config{ - Interval: time.Millisecond, // should crash here - Patterns: nil, - Services: services, - } - assert.Error(t, cfg.Valid()) -} - -func Test_NoServiceConfig(t *testing.T) { - cfg := &Config{ - Interval: time.Second, - Patterns: nil, - Services: nil, - } - assert.Error(t, cfg.Valid()) -} diff --git a/service/reload/samefile.go b/service/reload/samefile.go deleted file mode 100644 index 80df0431..00000000 --- a/service/reload/samefile.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !windows - -package reload - -import "os" - -func sameFile(fi1, fi2 os.FileInfo) bool { - return os.SameFile(fi1, fi2) -} diff --git a/service/reload/samefile_windows.go b/service/reload/samefile_windows.go deleted file mode 100644 index 5f70d327..00000000 --- a/service/reload/samefile_windows.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build windows - -package reload - -import "os" - -func sameFile(fi1, fi2 os.FileInfo) bool { - return fi1.ModTime() == fi2.ModTime() && - fi1.Size() == fi2.Size() && - fi1.Mode() == fi2.Mode() && - fi1.IsDir() == fi2.IsDir() -} diff --git a/service/reload/service.go b/service/reload/service.go deleted file mode 100644 index 9c615e0b..00000000 --- a/service/reload/service.go +++ /dev/null @@ -1,162 +0,0 @@ -package reload - -import ( - "errors" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner" - "github.com/spiral/roadrunner/service" - "os" - "strings" - "time" -) - -// ID contains default service name. -const ID = "reload" - -type Service struct { - cfg *Config - log *logrus.Logger - watcher *Watcher - stopc chan struct{} -} - -// Init controller service -func (s *Service) Init(cfg *Config, log *logrus.Logger, c service.Container) (bool, error) { - if cfg == nil || len(cfg.Services) == 0 { - return false, nil - } - - s.cfg = cfg - s.log = log - s.stopc = make(chan struct{}) - - var configs []WatcherConfig - - // mount Services to designated services - for serviceName := range cfg.Services { - svc, _ := c.Get(serviceName) - if ctrl, ok := svc.(roadrunner.Controllable); ok { - tmp := cfg.Services[serviceName] - tmp.service = &ctrl - cfg.Services[serviceName] = tmp - } - } - - for serviceName, config := range s.cfg.Services { - if cfg.Services[serviceName].service == nil { - continue - } - ignored, err := ConvertIgnored(config.Ignore) - if err != nil { - return false, err - } - configs = append(configs, WatcherConfig{ - serviceName: serviceName, - recursive: config.Recursive, - directories: config.Dirs, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: ignored, - filePatterns: append(config.Patterns, cfg.Patterns...), - }) - } - - var err error - s.watcher, err = NewWatcher(configs) - if err != nil { - return false, err - } - - return true, nil -} - -func (s *Service) Serve() error { - if s.cfg.Interval < time.Second { - return errors.New("reload interval is too fast") - } - - // make a map with unique services - // so, if we would have a 100 events from http service - // in map we would see only 1 key and it's config - treshholdc := make(chan struct { - serviceConfig ServiceConfig - service string - }, 100) - - // use the same interval - ticker := time.NewTicker(s.cfg.Interval) - - // drain channel in case of leaved messages - defer func() { - go func() { - for range treshholdc { - - } - }() - }() - - go func() { - for e := range s.watcher.Event { - treshholdc <- struct { - serviceConfig ServiceConfig - service string - }{serviceConfig: s.cfg.Services[e.service], service: e.service} - } - }() - - // map with configs by services - updated := make(map[string]ServiceConfig, 100) - - go func() { - for { - select { - case config := <-treshholdc: - // replace previous value in map by more recent without adding new one - updated[config.service] = config.serviceConfig - // stop ticker - ticker.Stop() - // restart - // logic is following: - // if we getting a lot of events, we should't restart particular service on each of it (user doing bug move or very fast typing) - // instead, we are resetting the ticker and wait for Interval time - // If there is no more events, we restart service only once - ticker = time.NewTicker(s.cfg.Interval) - case <-ticker.C: - if len(updated) > 0 { - for k, v := range updated { - sv := *v.service - err := sv.Server().Reset() - if err != nil { - s.log.Error(err) - } - s.log.Debugf("[%s] found %v file(s) changes, reloading", k, len(updated)) - } - // zero map - updated = make(map[string]ServiceConfig, 100) - } - case <-s.stopc: - ticker.Stop() - return - } - } - }() - - err := s.watcher.StartPolling(s.cfg.Interval) - if err != nil { - return err - } - - return nil -} - -func (s *Service) Stop() { - s.watcher.Stop() - s.stopc <- struct{}{} -} diff --git a/service/reload/service_test.go b/service/reload/service_test.go deleted file mode 100644 index 7cad4a5d..00000000 --- a/service/reload/service_test.go +++ /dev/null @@ -1 +0,0 @@ -package reload diff --git a/service/reload/watcher.go b/service/reload/watcher.go deleted file mode 100644 index 027d2d0d..00000000 --- a/service/reload/watcher.go +++ /dev/null @@ -1,409 +0,0 @@ -package reload - -import ( - "errors" - "io/ioutil" - "os" - "path/filepath" - "sync" - "time" -) - -var ErrorSkip = errors.New("file is skipped") -var NoWalkerConfig = errors.New("should add at least one walker config, when reload is set to true") - -// SimpleHook is used to filter by simple criteria, CONTAINS -type SimpleHook func(filename string, pattern []string) error - -// An Event describes an event that is received when files or directory -// changes occur. It includes the os.FileInfo of the changed file or -// directory and the type of event that's occurred and the full path of the file. -type Event struct { - path string - info os.FileInfo - - service string // type of service, http, grpc, etc... -} - -type WatcherConfig struct { - // service name - serviceName string - - // recursive or just add by singe directory - recursive bool - - // directories used per-service - directories []string - - // simple hook, just CONTAINS - filterHooks func(filename string, pattern []string) error - - // path to file with files - files map[string]os.FileInfo - - // ignored directories, used map for O(1) amortized get - ignored map[string]struct{} - - // filePatterns to ignore - filePatterns []string -} - -type Watcher struct { - // main event channel - Event chan Event - close chan struct{} - - //============================= - mu *sync.Mutex - - // indicates is walker started or not - started bool - - // config for each service - // need pointer here to assign files - watcherConfigs map[string]WatcherConfig -} - -// Options is used to set Watcher Options -type Options func(*Watcher) - -// NewWatcher returns new instance of File Watcher -func NewWatcher(configs []WatcherConfig, options ...Options) (*Watcher, error) { - w := &Watcher{ - Event: make(chan Event), - mu: &sync.Mutex{}, - - close: make(chan struct{}), - - //workingDir: workDir, - watcherConfigs: make(map[string]WatcherConfig), - } - - // add watcherConfigs by service names - for _, v := range configs { - w.watcherConfigs[v.serviceName] = v - } - - // apply options - for _, option := range options { - option(w) - } - err := w.initFs() - if err != nil { - return nil, err - } - - return w, nil -} - -// initFs makes initial map with files -func (w *Watcher) initFs() error { - for srvName, config := range w.watcherConfigs { - fileList, err := w.retrieveFileList(srvName, config) - if err != nil { - return err - } - // workaround. in golang you can't assign to map in struct field - tmp := w.watcherConfigs[srvName] - tmp.files = fileList - w.watcherConfigs[srvName] = tmp - } - return nil -} - -// ConvertIgnored is used to convert slice to map with ignored files -func ConvertIgnored(ignored []string) (map[string]struct{}, error) { - if len(ignored) == 0 { - return nil, nil - } - - ign := make(map[string]struct{}, len(ignored)) - for i := 0; i < len(ignored); i++ { - abs, err := filepath.Abs(ignored[i]) - if err != nil { - return nil, err - } - ign[abs] = struct{}{} - } - - return ign, nil - -} - -// GetAllFiles returns all files initialized for particular company -func (w *Watcher) GetAllFiles(serviceName string) []os.FileInfo { - var ret []os.FileInfo - - for _, v := range w.watcherConfigs[serviceName].files { - ret = append(ret, v) - } - - return ret -} - -// https://en.wikipedia.org/wiki/Inotify -// SetMaxFileEvents sets max file notify events for Watcher -// In case of file watch errors, this value can be increased system-wide -// For linux: set --> fs.inotify.max_user_watches = 600000 (under /etc/<choose_name_here>.conf) -// Add apply: sudo sysctl -p --system -//func SetMaxFileEvents(events int) Options { -// return func(watcher *Watcher) { -// watcher.maxFileWatchEvents = events -// } -// -//} - -// pass map from outside -func (w *Watcher) retrieveFilesSingle(serviceName, path string) (map[string]os.FileInfo, error) { - stat, err := os.Stat(path) - if err != nil { - return nil, err - } - - filesList := make(map[string]os.FileInfo, 10) - filesList[path] = stat - - // if it's not a dir, return - if !stat.IsDir() { - return filesList, nil - } - - fileInfoList, err := ioutil.ReadDir(path) - if err != nil { - return nil, err - } - - // recursive calls are slow in compare to goto - // so, we will add files with goto pattern -outer: - for i := 0; i < len(fileInfoList); i++ { - var pathToFile string - // BCE check elimination - // https://go101.org/article/bounds-check-elimination.html - if len(fileInfoList) != 0 && len(fileInfoList) >= i { - pathToFile = filepath.Join(pathToFile, fileInfoList[i].Name()) - } else { - return nil, errors.New("file info list len") - } - - // if file in ignored --> continue - if _, ignored := w.watcherConfigs[serviceName].ignored[path]; ignored { - continue - } - - // if filename does not contain pattern --> ignore that file - if w.watcherConfigs[serviceName].filePatterns != nil && w.watcherConfigs[serviceName].filterHooks != nil { - err = w.watcherConfigs[serviceName].filterHooks(fileInfoList[i].Name(), w.watcherConfigs[serviceName].filePatterns) - if err == ErrorSkip { - continue outer - } - } - - filesList[pathToFile] = fileInfoList[i] - } - - return filesList, nil -} - -func (w *Watcher) StartPolling(duration time.Duration) error { - w.mu.Lock() - if w.started { - w.mu.Unlock() - return errors.New("already started") - } - - w.started = true - w.mu.Unlock() - - return w.waitEvent(duration) -} - -// this is blocking operation -func (w *Watcher) waitEvent(d time.Duration) error { - ticker := time.NewTicker(d) - for { - select { - case <-w.close: - ticker.Stop() - // just exit - // no matter for the pollEvents - return nil - case <-ticker.C: - // this is not very effective way - // because we have to wait on Lock - // better is to listen files in parallel, but, since that would be used in debug... TODO - for serviceName, config := range w.watcherConfigs { - go func(sn string, c WatcherConfig) { - fileList, _ := w.retrieveFileList(sn, c) - w.pollEvents(c.serviceName, fileList) - }(serviceName, config) - } - } - } - -} - -// retrieveFileList get file list for service -func (w *Watcher) retrieveFileList(serviceName string, config WatcherConfig) (map[string]os.FileInfo, error) { - w.mu.Lock() - defer w.mu.Unlock() - fileList := make(map[string]os.FileInfo) - if config.recursive { - // walk through directories recursively - for _, dir := range config.directories { - // full path is workdir/relative_path - fullPath, err := filepath.Abs(dir) - if err != nil { - return nil, err - } - list, err := w.retrieveFilesRecursive(serviceName, fullPath) - if err != nil { - return nil, err - } - - for k, v := range list { - fileList[k] = v - } - } - return fileList, nil - } - - for _, dir := range config.directories { - // full path is workdir/relative_path - fullPath, err := filepath.Abs(dir) - if err != nil { - return nil, err - } - - // list is pathToFiles with files - list, err := w.retrieveFilesSingle(serviceName, fullPath) - if err != nil { - return nil, err - } - - for pathToFile, file := range list { - fileList[pathToFile] = file - } - } - - return fileList, nil -} - -func (w *Watcher) retrieveFilesRecursive(serviceName, root string) (map[string]os.FileInfo, error) { - fileList := make(map[string]os.FileInfo) - - return fileList, filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - - // If path is ignored and it's a directory, skip the directory. If it's - // ignored and it's a single file, skip the file. - _, ignored := w.watcherConfigs[serviceName].ignored[path] - if ignored { - if info.IsDir() { - // if it's dir, ignore whole - return filepath.SkipDir - } - return nil - } - - // if filename does not contain pattern --> ignore that file - err = w.watcherConfigs[serviceName].filterHooks(info.Name(), w.watcherConfigs[serviceName].filePatterns) - if err == ErrorSkip { - return nil - } - - // Add the path and it's info to the file list. - fileList[path] = info - return nil - }) -} - -func (w *Watcher) pollEvents(serviceName string, files map[string]os.FileInfo) { - w.mu.Lock() - defer w.mu.Unlock() - - // Store create and remove events for use to check for rename events. - creates := make(map[string]os.FileInfo) - removes := make(map[string]os.FileInfo) - - // Check for removed files. - for pth, info := range w.watcherConfigs[serviceName].files { - if _, found := files[pth]; !found { - removes[pth] = info - } - } - - // Check for created files, writes and chmods. - for pth, info := range files { - if info.IsDir() { - continue - } - oldInfo, found := w.watcherConfigs[serviceName].files[pth] - if !found { - // A file was created. - creates[pth] = info - continue - } - if oldInfo.ModTime() != info.ModTime() { - w.watcherConfigs[serviceName].files[pth] = info - w.Event <- Event{ - path: pth, - info: info, - service: serviceName, - } - } - if oldInfo.Mode() != info.Mode() { - w.watcherConfigs[serviceName].files[pth] = info - w.Event <- Event{ - path: pth, - info: info, - service: serviceName, - } - } - } - - //Check for renames and moves. - for path1, info1 := range removes { - for path2, info2 := range creates { - if sameFile(info1, info2) { - e := Event{ - path: path2, - info: info2, - service: serviceName, - } - - // remove initial path - delete(w.watcherConfigs[serviceName].files, path1) - // update with new - w.watcherConfigs[serviceName].files[path2] = info2 - - - w.Event <- e - } - } - } - - //Send all the remaining create and remove events. - for pth, info := range creates { - w.watcherConfigs[serviceName].files[pth] = info - w.Event <- Event{ - path: pth, - info: info, - service: serviceName, - } - } - for pth, info := range removes { - delete(w.watcherConfigs[serviceName].files, pth) - w.Event <- Event{ - path: pth, - info: info, - service: serviceName, - } - } -} - -func (w *Watcher) Stop() { - w.close <- struct{}{} -} diff --git a/service/reload/watcher_test.go b/service/reload/watcher_test.go deleted file mode 100644 index 9683d2de..00000000 --- a/service/reload/watcher_test.go +++ /dev/null @@ -1,673 +0,0 @@ -package reload - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "strings" - "testing" - "time" -) - -var testServiceName = "test" - -// scenario -// Create walker instance, init with default config, check that Watcher found all files from config -func Test_Correct_Watcher_Init(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - if err != nil { - t.Fatal(err) - } - }() - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: false, - directories: []string{tempDir}, - filterHooks: nil, - files: make(map[string]os.FileInfo), - ignored: nil, - filePatterns: nil, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - if len(w.GetAllFiles(testServiceName)) != 2 { - t.Fatal("incorrect directories len") - } -} - -// scenario -// create 3 files, create walker instance -// Start poll events -// change file and see, if event had come to handler -func Test_Get_FileEvent(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: false, - directories: []string{tempDir}, - filterHooks: nil, - files: make(map[string]os.FileInfo), - ignored: nil, - filePatterns: []string{"aaa", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - // should be 3 files and directory - if len(w.GetAllFiles(testServiceName)) != 4 { - t.Fatal("incorrect directories len") - } - - go limitTime(time.Second * 10, t.Name(), c) - - go func() { - go func() { - time.Sleep(time.Second) - err2 := ioutil.WriteFile(filepath.Join(tempDir, "file2.txt"), - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - runtime.Goexit() - }() - - go func() { - for e := range w.Event { - if e.path != "file2.txt" { - panic("didn't handle event when write file2") - } - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// scenario -// create 3 files with different extensions, create walker instance -// Start poll events -// change file with txt extension, and see, if event had not come to handler because it was filtered -func Test_FileExtensionFilter(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: false, - directories: []string{tempDir}, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: nil, - filePatterns: []string{"aaa", "bbb"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (one filtered) and directory - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go limitTime(time.Second * 5, t.Name(), c) - - go func() { - go func() { - err2 := ioutil.WriteFile(filepath.Join(tempDir, "file3.txt"), - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - - runtime.Goexit() - }() - - go func() { - for e := range w.Event { - fmt.Println(e.info.Name()) - panic("handled event from filtered file") - } - }() - w.Stop() - runtime.Goexit() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// nested -// scenario -// create dir and nested dir -// make files with aaa, bbb and txt extensions, filter txt -// change not filtered file, handle event -func Test_Recursive_Support(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - defer func() { - err = freeResources(tempDir) - if err != nil { - t.Fatal(err) - } - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: true, - directories: []string{tempDir}, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: nil, - filePatterns: []string{"aaa", "bbb"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 3 files (2 from root dir, and 1 from nested), filtered txt - if dirLen != 3 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - // change file in nested directory - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{1, 1, 1}, 0755) - if err != nil { - panic(err) - } - go func() { - for e := range w.Event { - if e.info.Name() != "file4.aaa" { - panic("wrong handled event from watcher in nested dir") - } - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -func Test_Wrong_Dir(t *testing.T) { - // no such file or directory - wrongDir := "askdjfhaksdlfksdf" - - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: true, - directories: []string{wrongDir}, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: nil, - filePatterns: []string{"aaa", "bbb"}, - } - - _, err := NewWatcher([]WatcherConfig{wc}) - if err == nil { - t.Fatal(err) - } -} - -func Test_Filter_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func(name string) { - err = freeResources(name) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }(tempDir) - - go limitTime(time.Second*10, t.Name(), c) - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - ignored, err := ConvertIgnored([]string{nestedDir}) - if err != nil { - t.Fatal(err) - } - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: true, - directories: []string{tempDir}, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: ignored, - filePatterns: []string{"aaa", "bbb", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go func() { - go func() { - err2 := ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{1, 1, 1}, 0755) - if err2 != nil { - panic(err2) - } - }() - - go func() { - for e := range w.Event { - fmt.Println("file: " + e.info.Name()) - panic("handled event from watcher in nested dir") - } - }() - - // time sleep is used here because StartPolling is blocking operation - time.Sleep(time.Second * 5) - w.Stop() - - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -// copy files from nested dir to not ignored -// should fire an event -func Test_Copy_Directory(t *testing.T) { - tempDir, err := ioutil.TempDir(".", "") - c := make(chan struct{}) - defer func() { - err = freeResources(tempDir) - if err != nil { - c <- struct{}{} - t.Fatal(err) - } - c <- struct{}{} - }() - - nestedDir, err := ioutil.TempDir(tempDir, "nested") - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file1.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(tempDir, "file2.bbb"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - err = ioutil.WriteFile(filepath.Join(nestedDir, "file3.txt"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - err = ioutil.WriteFile(filepath.Join(nestedDir, "file4.aaa"), - []byte{}, 0755) - if err != nil { - t.Fatal(err) - } - - ignored, err := ConvertIgnored([]string{nestedDir}) - if err != nil { - t.Fatal(err) - } - - wc := WatcherConfig{ - serviceName: testServiceName, - recursive: true, - directories: []string{tempDir}, - filterHooks: func(filename string, patterns []string) error { - for i := 0; i < len(patterns); i++ { - if strings.Contains(filename, patterns[i]) { - return nil - } - } - return ErrorSkip - }, - files: make(map[string]os.FileInfo), - ignored: ignored, - filePatterns: []string{"aaa", "bbb", "txt"}, - } - - w, err := NewWatcher([]WatcherConfig{wc}) - if err != nil { - t.Fatal(err) - } - - dirLen := len(w.GetAllFiles(testServiceName)) - // should be 2 files (2 from root dir), filtered other - if dirLen != 2 { - t.Fatalf("incorrect directories len, len is: %d", dirLen) - } - - go limitTime(time.Second*10, t.Name(), c) - - go func() { - go func() { - err2 := copyDir(nestedDir, filepath.Join(tempDir, "copyTo")) - if err2 != nil { - panic(err2) - } - - // exit from current goroutine - runtime.Goexit() - }() - - go func() { - for range w.Event { - // here should be event, otherwise we won't stop - w.Stop() - } - }() - }() - - err = w.StartPolling(time.Second) - if err != nil { - t.Fatal(err) - } -} - -func limitTime(d time.Duration, name string, free chan struct{}) { - go func() { - ticket := time.NewTicker(d) - for { - select { - case <-ticket.C: - ticket.Stop() - panic("timeout exceed, test: " + name) - case <-free: - ticket.Stop() - return - } - } - }() -} - -func copyFile(src, dst string) (err error) { - in, err := os.Open(src) - if err != nil { - return - } - defer in.Close() - - out, err := os.Create(dst) - if err != nil { - return - } - defer func() { - if e := out.Close(); e != nil { - err = e - } - }() - - _, err = io.Copy(out, in) - if err != nil { - return - } - - err = out.Sync() - if err != nil { - return - } - - si, err := os.Stat(src) - if err != nil { - return - } - err = os.Chmod(dst, si.Mode()) - if err != nil { - return - } - - return -} - -func copyDir(src string, dst string) (err error) { - src = filepath.Clean(src) - dst = filepath.Clean(dst) - - si, err := os.Stat(src) - if err != nil { - return err - } - if !si.IsDir() { - return fmt.Errorf("source is not a directory") - } - - _, err = os.Stat(dst) - if err != nil && !os.IsNotExist(err) { - return - } - if err == nil { - return fmt.Errorf("destination already exists") - } - - err = os.MkdirAll(dst, si.Mode()) - if err != nil { - return - } - - entries, err := ioutil.ReadDir(src) - if err != nil { - return - } - - for _, entry := range entries { - srcPath := filepath.Join(src, entry.Name()) - dstPath := filepath.Join(dst, entry.Name()) - - if entry.IsDir() { - err = copyDir(srcPath, dstPath) - if err != nil { - return - } - } else { - // Skip symlinks. - if entry.Mode()&os.ModeSymlink != 0 { - continue - } - - err = copyFile(srcPath, dstPath) - if err != nil { - return - } - } - } - - return -} - -func freeResources(path string) error { - return os.RemoveAll(path) -} diff --git a/service/rpc/config.go b/service/rpc/config.go deleted file mode 100644 index cc492622..00000000 --- a/service/rpc/config.go +++ /dev/null @@ -1,60 +0,0 @@ -package rpc - -import ( - "errors" - "net" - "strings" - - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/util" -) - -// Config defines RPC service config. -type Config struct { - // Indicates if RPC connection is enabled. - Enable bool - - // Listen string - Listen string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return c.Valid() -} - -// InitDefaults allows to init blank config with pre-defined set of default values. -func (c *Config) InitDefaults() error { - c.Enable = true - c.Listen = "tcp://127.0.0.1:6001" - - return nil -} - -// Valid returns nil if config is valid. -func (c *Config) Valid() error { - if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 { - return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") - } - - return nil -} - -// Listener creates new rpc socket Listener. -func (c *Config) Listener() (net.Listener, error) { - return util.CreateListener(c.Listen) -} - -// Dialer creates rpc socket Dialer. -func (c *Config) Dialer() (net.Conn, error) { - dsn := strings.Split(c.Listen, "://") - if len(dsn) != 2 { - return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") - } - - return net.Dial(dsn[0], dsn[1]) -} diff --git a/service/rpc/config_test.go b/service/rpc/config_test.go deleted file mode 100644 index 1ecd71b3..00000000 --- a/service/rpc/config_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package rpc - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type testCfg struct{ cfg string } - -func (cfg *testCfg) Get(name string) service.Config { return nil } -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "tcp://:18001"}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "invalid"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error2(t *testing.T) { - cfg := &testCfg{`{"enable": true, "listen": "invalid"`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func TestConfig_Listener(t *testing.T) { - cfg := &Config{Listen: "tcp://:18001"} - - ln, err := cfg.Listener() - assert.NoError(t, err) - assert.NotNil(t, ln) - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - assert.Equal(t, "tcp", ln.Addr().Network()) - assert.Equal(t, "0.0.0.0:18001", ln.Addr().String()) -} - -func TestConfig_ListenerUnix(t *testing.T) { - cfg := &Config{Listen: "unix://file.sock"} - - ln, err := cfg.Listener() - assert.NoError(t, err) - assert.NotNil(t, ln) - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - assert.Equal(t, "unix", ln.Addr().Network()) - assert.Equal(t, "file.sock", ln.Addr().String()) -} - -func Test_Config_Error(t *testing.T) { - cfg := &Config{Listen: "uni:unix.sock"} - ln, err := cfg.Listener() - assert.Nil(t, ln) - assert.Error(t, err) - assert.Equal(t, "invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) -} - -func Test_Config_ErrorMethod(t *testing.T) { - cfg := &Config{Listen: "xinu://unix.sock"} - - ln, err := cfg.Listener() - assert.Nil(t, ln) - assert.Error(t, err) -} - -func TestConfig_Dialer(t *testing.T) { - cfg := &Config{Listen: "tcp://:18001"} - - ln, _ := cfg.Listener() - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - conn, err := cfg.Dialer() - assert.NoError(t, err) - assert.NotNil(t, conn) - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("error closing the connection: error %v", err) - } - }() - - assert.Equal(t, "tcp", conn.RemoteAddr().Network()) - assert.Equal(t, "127.0.0.1:18001", conn.RemoteAddr().String()) -} - -func TestConfig_DialerUnix(t *testing.T) { - cfg := &Config{Listen: "unix://file.sock"} - - ln, _ := cfg.Listener() - defer func() { - err := ln.Close() - if err != nil { - t.Errorf("error closing the listener: error %v", err) - } - }() - - conn, err := cfg.Dialer() - assert.NoError(t, err) - assert.NotNil(t, conn) - defer func() { - err := conn.Close() - if err != nil { - t.Errorf("error closing the connection: error %v", err) - } - }() - - assert.Equal(t, "unix", conn.RemoteAddr().Network()) - assert.Equal(t, "file.sock", conn.RemoteAddr().String()) -} - -func Test_Config_DialerError(t *testing.T) { - cfg := &Config{Listen: "uni:unix.sock"} - ln, err := cfg.Dialer() - assert.Nil(t, ln) - assert.Error(t, err) - assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) -} - -func Test_Config_DialerErrorMethod(t *testing.T) { - cfg := &Config{Listen: "xinu://unix.sock"} - - ln, err := cfg.Dialer() - assert.Nil(t, ln) - assert.Error(t, err) -} - -func Test_Config_Defaults(t *testing.T) { - c := &Config{} - err := c.InitDefaults() - if err != nil { - t.Errorf("error during the InitDefaults: error %v", err) - } - assert.Equal(t, true, c.Enable) - assert.Equal(t, "tcp://127.0.0.1:6001", c.Listen) -} diff --git a/service/rpc/service.go b/service/rpc/service.go deleted file mode 100644 index 7a649f1b..00000000 --- a/service/rpc/service.go +++ /dev/null @@ -1,124 +0,0 @@ -package rpc - -import ( - "errors" - "github.com/spiral/goridge/v2" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "net/rpc" - "sync" -) - -// ID contains default service name. -const ID = "rpc" - -// Service is RPC service. -type Service struct { - cfg *Config - stop chan interface{} - rpc *rpc.Server - mu sync.Mutex - serving bool -} - -// Init rpc service. Must return true if service is enabled. -func (s *Service) Init(cfg *Config, c service.Container, env env.Environment) (bool, error) { - if !cfg.Enable { - return false, nil - } - - s.cfg = cfg - s.rpc = rpc.NewServer() - - if env != nil { - env.SetEnv("RR_RPC", cfg.Listen) - } - - if err := s.Register("system", &systemService{c}); err != nil { - return false, err - } - - return true, nil -} - -// Serve serves the service. -func (s *Service) Serve() error { - if s.rpc == nil { - return errors.New("RPC service is not configured") - } - - s.mu.Lock() - s.serving = true - s.stop = make(chan interface{}) - s.mu.Unlock() - - ln, err := s.cfg.Listener() - if err != nil { - return err - } - defer ln.Close() - - go func() { - for { - select { - case <-s.stop: - return - default: - conn, err := ln.Accept() - if err != nil { - continue - } - - go s.rpc.ServeCodec(goridge.NewCodec(conn)) - } - } - }() - - <-s.stop - - s.mu.Lock() - s.serving = false - s.mu.Unlock() - - return nil -} - -// Stop stops the service. -func (s *Service) Stop() { - s.mu.Lock() - defer s.mu.Unlock() - - if s.serving { - close(s.stop) - } -} - -// Register publishes in the server the set of methods of the -// receiver value that satisfy the following conditions: -// - exported method of exported type -// - two arguments, both of exported type -// - the second argument is a pointer -// - one return value, of type error -// It returns an error if the receiver is not an exported type or has -// no suitable methods. It also logs the error using package log. -func (s *Service) Register(name string, svc interface{}) error { - if s.rpc == nil { - return errors.New("RPC service is not configured") - } - - return s.rpc.RegisterName(name, svc) -} - -// Client creates new RPC client. -func (s *Service) Client() (*rpc.Client, error) { - if s.cfg == nil { - return nil, errors.New("RPC service is not configured") - } - - conn, err := s.cfg.Dialer() - if err != nil { - return nil, err - } - - return rpc.NewClientWithCodec(goridge.NewClientCodec(conn)), nil -} diff --git a/service/rpc/service_test.go b/service/rpc/service_test.go deleted file mode 100644 index 51c1b337..00000000 --- a/service/rpc/service_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package rpc - -import ( - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/service/env" - "github.com/stretchr/testify/assert" - "testing" - "time" -) - -type testService struct{} - -func (ts *testService) Echo(msg string, r *string) error { *r = msg; return nil } - -func Test_Disabled(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: false}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.False(t, ok) -} - -func Test_RegisterNotConfigured(t *testing.T) { - s := &Service{} - assert.Error(t, s.Register("test", &testService{})) - - client, err := s.Client() - assert.Nil(t, client) - assert.Error(t, err) - assert.Error(t, s.Serve()) -} - -func Test_Enabled(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9008"}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.True(t, ok) -} - -func Test_StopNonServing(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9008"}, service.NewContainer(nil), nil) - - assert.NoError(t, err) - assert.True(t, ok) - s.Stop() -} - -func Test_Serve_Errors(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "malformed"}, service.NewContainer(nil), nil) - assert.NoError(t, err) - assert.True(t, ok) - - assert.Error(t, s.Serve()) - - client, err := s.Client() - assert.Nil(t, client) - assert.Error(t, err) -} - -func Test_Serve_Client(t *testing.T) { - s := &Service{} - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9018"}, service.NewContainer(nil), nil) - assert.NoError(t, err) - assert.True(t, ok) - - defer s.Stop() - - assert.NoError(t, s.Register("test", &testService{})) - - go func() { assert.NoError(t, s.Serve()) }() - time.Sleep(time.Second) - - client, err := s.Client() - assert.NotNil(t, client) - assert.NoError(t, err) - - var resp string - assert.NoError(t, client.Call("test.Echo", "hello world", &resp)) - assert.Equal(t, "hello world", resp) - assert.NoError(t, client.Close()) -} - -func TestSetEnv(t *testing.T) { - s := &Service{} - e := env.NewService(map[string]string{}) - ok, err := s.Init(&Config{Enable: true, Listen: "tcp://localhost:9018"}, service.NewContainer(nil), e) - - assert.NoError(t, err) - assert.True(t, ok) - - v, _ := e.GetEnv() - assert.Equal(t, "tcp://localhost:9018", v["RR_RPC"]) -} diff --git a/service/rpc/system.go b/service/rpc/system.go deleted file mode 100644 index ffba3782..00000000 --- a/service/rpc/system.go +++ /dev/null @@ -1,18 +0,0 @@ -package rpc - -import "github.com/spiral/roadrunner/service" - -// systemService service controls rr server. -type systemService struct { - c service.Container -} - -// Detach the underlying c. -func (s *systemService) Stop(stop bool, r *string) error { - if stop { - s.c.Stop() - } - *r = "OK" - - return nil -} diff --git a/service/static/config.go b/service/static/config.go deleted file mode 100644 index 3ca20a83..00000000 --- a/service/static/config.go +++ /dev/null @@ -1,82 +0,0 @@ -package static - -import ( - "fmt" - "github.com/spiral/roadrunner/service" - "os" - "path" - "strings" -) - -// Config describes file location and controls access to them. -type Config struct { - // Dir contains name of directory to control access to. - Dir string - - // Forbid specifies list of file extensions which are forbidden for access. - // Example: .php, .exe, .bat, .htaccess and etc. - Forbid []string - - // Always specifies list of extensions which must always be served by static - // service, even if file not found. - Always []string - - // Request headers to add to every static. - Request map[string]string - - // Response headers to add to every static. - Response map[string]string -} - -// Hydrate must populate Config values using given Config source. Must return error if Config is not valid. -func (c *Config) Hydrate(cfg service.Config) error { - if err := cfg.Unmarshal(c); err != nil { - return err - } - - return c.Valid() -} - -// Valid returns nil if config is valid. -func (c *Config) Valid() error { - st, err := os.Stat(c.Dir) - if err != nil { - if os.IsNotExist(err) { - return fmt.Errorf("root directory '%s' does not exists", c.Dir) - } - - return err - } - - if !st.IsDir() { - return fmt.Errorf("invalid root directory '%s'", c.Dir) - } - - return nil -} - -// AlwaysForbid must return true if file extension is not allowed for the upload. -func (c *Config) AlwaysForbid(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range c.Forbid { - if ext == v { - return true - } - } - - return false -} - -// AlwaysServe must indicate that file is expected to be served by static service. -func (c *Config) AlwaysServe(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range c.Always { - if ext == v { - return true - } - } - - return false -} diff --git a/service/static/config_test.go b/service/static/config_test.go deleted file mode 100644 index 8bf0d372..00000000 --- a/service/static/config_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package static - -import ( - json "github.com/json-iterator/go" - "github.com/spiral/roadrunner/service" - "github.com/stretchr/testify/assert" - "testing" -) - -type mockCfg struct{ cfg string } - -func (cfg *mockCfg) Get(name string) service.Config { return nil } -func (cfg *mockCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.cfg), out) -} - -func Test_Config_Hydrate(t *testing.T) { - cfg := &mockCfg{`{"dir": "./", "request":{"foo": "bar"}, "response":{"xxx": "yyy"}}`} - c := &Config{} - - assert.NoError(t, c.Hydrate(cfg)) -} - -func Test_Config_Hydrate_Error(t *testing.T) { - cfg := &mockCfg{`{"enable": true,"dir": "/dir/"}`} - c := &Config{} - - assert.Error(t, c.Hydrate(cfg)) -} - -func TestConfig_Forbids(t *testing.T) { - cfg := Config{Forbid: []string{".php"}} - - assert.True(t, cfg.AlwaysForbid("index.php")) - assert.True(t, cfg.AlwaysForbid("index.PHP")) - assert.True(t, cfg.AlwaysForbid("phpadmin/index.bak.php")) - assert.False(t, cfg.AlwaysForbid("index.html")) -} - -func TestConfig_Valid(t *testing.T) { - assert.NoError(t, (&Config{Dir: "./"}).Valid()) - assert.Error(t, (&Config{Dir: "./config.go"}).Valid()) - assert.Error(t, (&Config{Dir: "./dir/"}).Valid()) -} diff --git a/service/static/service.go b/service/static/service.go deleted file mode 100644 index 95b99860..00000000 --- a/service/static/service.go +++ /dev/null @@ -1,87 +0,0 @@ -package static - -import ( - rrhttp "github.com/spiral/roadrunner/service/http" - "net/http" - "path" -) - -// ID contains default service name. -const ID = "static" - -// Service serves static files. Potentially convert into middleware? -type Service struct { - // server configuration (location, forbidden files and etc) - cfg *Config - - // root is initiated http directory - root http.Dir -} - -// Init must return configure service and return true if service hasStatus enabled. Must return error in case of -// misconfiguration. Services must not be used without proper configuration pushed first. -func (s *Service) Init(cfg *Config, r *rrhttp.Service) (bool, error) { - if r == nil { - return false, nil - } - - s.cfg = cfg - s.root = http.Dir(s.cfg.Dir) - r.AddMiddleware(s.middleware) - - return true, nil -} - -// middleware must return true if request/response pair is handled within the middleware. -func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { - // Define the http.HandlerFunc - return func(w http.ResponseWriter, r *http.Request) { - if s.cfg.Request != nil { - for k, v := range s.cfg.Request { - r.Header.Add(k, v) - } - } - - if s.cfg.Response != nil { - for k, v := range s.cfg.Response { - w.Header().Set(k, v) - } - } - - if !s.handleStatic(w, r) { - f(w, r) - } - } -} - -func (s *Service) handleStatic(w http.ResponseWriter, r *http.Request) bool { - fPath := path.Clean(r.URL.Path) - - if s.cfg.AlwaysForbid(fPath) { - return false - } - - f, err := s.root.Open(fPath) - if err != nil { - if s.cfg.AlwaysServe(fPath) { - w.WriteHeader(404) - return true - } - - return false - } - defer f.Close() - - d, err := f.Stat() - if err != nil { - return false - } - - // do not serve directories - if d.IsDir() { - return false - } - - http.ServeContent(w, r, d.Name(), d.ModTime(), f) - return true -} diff --git a/service/static/service_test.go b/service/static/service_test.go deleted file mode 100644 index 842662c9..00000000 --- a/service/static/service_test.go +++ /dev/null @@ -1,531 +0,0 @@ -package static - -import ( - "bytes" - json "github.com/json-iterator/go" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" - "github.com/spiral/roadrunner/service" - rrhttp "github.com/spiral/roadrunner/service/http" - "github.com/stretchr/testify/assert" - "io" - "io/ioutil" - "net/http" - "os" - "testing" - "time" -) - -type testCfg struct { - httpCfg string - static string - target string -} - -func (cfg *testCfg) Get(name string) service.Config { - if name == rrhttp.ID { - return &testCfg{target: cfg.httpCfg} - } - - if name == ID { - return &testCfg{target: cfg.static} - } - return nil -} -func (cfg *testCfg) Unmarshal(out interface{}) error { - j := json.ConfigCompatibleWithStandardLibrary - return j.Unmarshal([]byte(cfg.target), out) -} - -func Test_Files(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - httpCfg: `{ - "enable": true, - "address": ":8029", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Second) - - - b, _, _ := get("http://localhost:8029/sample.txt") - assert.Equal(t, "sample", b) - c.Stop() -} - -func Test_Disabled(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - })) - - s, st := c.Get(ID) - assert.NotNil(t, s) - assert.Equal(t, service.StatusInactive, st) -} - -func Test_Files_Disable(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":false, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8030", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Second) - - b, _, err := get("http://localhost:8030/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Error(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"dir/invalid", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8031", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) -} - -func Test_Files_Error2(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.Error(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"dir/invalid", "forbid":[".php"]`, - httpCfg: `{ - "enable": true, - "address": ":8032", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) -} - -func Test_Files_Forbid(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8033", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - b, _, err := get("http://localhost:8033/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Always(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"], "always":[".ico"]}`, - httpCfg: `{ - "enable": true, - "address": ":8034", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - _, r, err := get("http://localhost:8034/favicon.ico") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, 404, r.StatusCode) - c.Stop() -} - -func Test_Files_NotFound(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8035", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8035/client.XXX?hello=world") - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_Dir(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[".php"]}`, - httpCfg: `{ - "enable": true, - "address": ":8036", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php echo pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8036/http?hello=world") - assert.Equal(t, "WORLD", b) - c.Stop() -} - -func Test_Files_NotForbid(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, - httpCfg: `{ - "enable": true, - "address": ":8037", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - b, _, _ := get("http://localhost:8037/client.php") - assert.Equal(t, all("../../tests/client.php"), b) - assert.Equal(t, all("../../tests/client.php"), b) - c.Stop() -} - -func TestStatic_Headers(t *testing.T) { - logger, _ := test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - - c := service.NewContainer(logger) - c.Register(rrhttp.ID, &rrhttp.Service{}) - c.Register(ID, &Service{}) - - assert.NoError(t, c.Init(&testCfg{ - static: `{"enable":true, "dir":"../../tests", "forbid":[], "request":{"input": "custom-header"}, "response":{"output": "output-header"}}`, - httpCfg: `{ - "enable": true, - "address": ":8037", - "maxRequestSize": 1024, - "uploads": { - "dir": ` + tmpDir() + `, - "forbid": [] - }, - "workers":{ - "command": "php ../../tests/http/client.php pid pipes", - "relay": "pipes", - "pool": { - "numWorkers": 1, - "allocateTimeout": 10000000, - "destroyTimeout": 10000000 - } - } - }`})) - - go func() { - err := c.Serve() - if err != nil { - t.Errorf("serve error: %v", err) - } - }() - - time.Sleep(time.Millisecond * 500) - - req, err := http.NewRequest("GET", "http://localhost:8037/client.php", nil) - if err != nil { - t.Fatal(err) - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - - if resp.Header.Get("Output") != "output-header" { - t.Fatal("can't find output header in response") - } - - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, all("../../tests/client.php"), string(b)) - assert.Equal(t, all("../../tests/client.php"), string(b)) - c.Stop() -} - -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) - if err != nil { - return "", nil, err - } - - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - - err = r.Body.Close() - if err != nil { - return "", nil, err - } - - return string(b), r, err -} - -func tmpDir() string { - p := os.TempDir() - j := json.ConfigCompatibleWithStandardLibrary - r, _ := j.Marshal(p) - - return string(r) -} - -func all(fn string) string { - f, _ := os.Open(fn) - - b := &bytes.Buffer{} - _, err := io.Copy(b, f) - if err != nil { - return "" - } - - err = f.Close() - if err != nil { - return "" - } - - return b.String() -} |