diff options
-rw-r--r-- | _____/http/config.go | 88 | ||||
-rw-r--r-- | _____/http/service.go | 80 | ||||
-rw-r--r-- | cmd/rr/main.go | 14 | ||||
-rw-r--r-- | cmd/rr/utils/size.go | 28 | ||||
-rw-r--r-- | cmd/rr/utils/utils_test.go | 12 | ||||
-rw-r--r-- | service/container.go | 13 | ||||
-rw-r--r-- | service/http/config.go | 34 | ||||
-rw-r--r-- | service/http/fs_config.go (renamed from http/fs_config.go) | 0 | ||||
-rw-r--r-- | service/http/fs_config_test.go (renamed from http/fs_config_test.go) | 0 | ||||
-rw-r--r-- | service/http/handler.go | 71 | ||||
-rw-r--r-- | service/http/parse.go (renamed from http/parse.go) | 0 | ||||
-rw-r--r-- | service/http/request.go (renamed from _____/http/request.go) | 0 | ||||
-rw-r--r-- | service/http/response.go (renamed from http/response.go) | 0 | ||||
-rw-r--r-- | service/http/rpc.go (renamed from _____/http/rpc.go) | 0 | ||||
-rw-r--r-- | service/http/service.go | 86 | ||||
-rw-r--r-- | service/http/uploads.go (renamed from http/uploads.go) | 0 | ||||
-rw-r--r-- | service/rpc/config.go (renamed from rpc/config.go) | 0 | ||||
-rw-r--r-- | service/rpc/config_test.go (renamed from rpc/config_test.go) | 0 | ||||
-rw-r--r-- | service/rpc/service.go (renamed from rpc/service.go) | 11 | ||||
-rw-r--r-- | service/rpc/service_test.go (renamed from rpc/service_test.go) | 0 | ||||
-rw-r--r-- | service/service.go (renamed from service/entry.go) | 13 | ||||
-rw-r--r-- | service/static/config.go | 52 | ||||
-rw-r--r-- | service/static/config_test.go | 21 | ||||
-rw-r--r-- | service/static/service.go | 120 |
24 files changed, 415 insertions, 228 deletions
diff --git a/_____/http/config.go b/_____/http/config.go deleted file mode 100644 index bd8cec5e..00000000 --- a/_____/http/config.go +++ /dev/null @@ -1,88 +0,0 @@ -package http - -import ( - "fmt" - "github.com/spiral/roadrunner/service" - "github.com/spiral/roadrunner/_____/utils" - "os" - "path" - "strings" - "github.com/spiral/roadrunner/http" -) - -// Configures RoadRunner HTTP server. -type Config struct { - // serve enables static file serving from desired root directory. - ServeStatic bool - - Static *http.FsConfig - - // Root directory, required when serve set to true. - Root string - - // Dir contains name of temporary directory to store uploaded files passed to underlying PHP process. - TmpDir string - - // MaxRequest specified max size for payload body in bytes, set 0 to unlimited. - MaxRequest int64 - - // Forbid specifies list of file extensions which are forbidden for uploads. - // Example: .php, .exe, .bat, .htaccess and etc. - ForbidUploads []string -} - -// Forbid must return true if file extension is not allowed for the upload. -func (cfg Config) Forbidden(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range cfg.ForbidUploads { - if ext == v { - return true - } - } - - return false -} - -type serviceConfig struct { - Enabled bool - Host string - Port string - MaxRequest string - Static struct { - Serve bool - Root string - } - - Uploads struct { - TmpDir string - Forbid []string - } - - Pool service.PoolConfig - - //todo: verbose ? -} - -func (cfg *serviceConfig) httpAddr() string { - return fmt.Sprintf("%s:%v", cfg.Host, cfg.Port) -} - -func (cfg *serviceConfig) httpConfig() *Config { - tmpDir := cfg.Uploads.TmpDir - if tmpDir == "" { - tmpDir = os.TempDir() - } - - return &Config{ - ServeStatic: cfg.Static.Serve, - Root: cfg.Static.Root, - TmpDir: tmpDir, - MaxRequest: utils.ParseSize(cfg.MaxRequest), - ForbidUploads: cfg.Uploads.Forbid, - } -} - -func (cfg *serviceConfig) Valid() error { - return nil -} diff --git a/_____/http/service.go b/_____/http/service.go deleted file mode 100644 index 008aeab8..00000000 --- a/_____/http/service.go +++ /dev/null @@ -1,80 +0,0 @@ -package http - -import ( - "context" - "github.com/sirupsen/logrus" - "github.com/spiral/roadrunner/service" - "net/http" - "github.com/spiral/roadrunner" -) - -const ServiceName = "http" - -type Service struct { - cfg *serviceConfig - http *http.Server - srv *Server -} - -func (s *Service) Name() string { - return ServiceName -} - -func (s *Service) Configure(cfg service.Config) (bool, error) { - config := &serviceConfig{} - if err := cfg.Unmarshal(config); err != nil { - return false, err - } - - if !config.Enabled { - return false, nil - } - - if err := config.Valid(); err != nil { - return false, err - } - - s.cfg = config - return true, nil -} - -func (s *Service) RPC() interface{} { - return &rpcServer{s} -} - -func (s *Service) Serve() error { - logrus.Debugf("http: started") - defer logrus.Debugf("http: stopped") - - rr, term, err := s.cfg.Pool.NewServer() - if err != nil { - return err - } - defer term() - - //todo: remove - rr.Observe(func(event int, ctx interface{}) { - switch event { - case roadrunner.EventPoolError: - logrus.Error(ctx) - case roadrunner.EventWorkerError: - logrus.Errorf("%s: %s", ctx.(roadrunner.WorkerError).Worker, ctx.(roadrunner.WorkerError).Error()) - } - }) - - s.srv = NewServer(s.cfg.httpConfig(), rr) - s.http = &http.Server{ - Addr: s.cfg.httpAddr(), - Handler: s.srv, - } - - if err := s.http.ListenAndServe(); err != nil { - return err - } - - return nil -} - -func (s *Service) Stop() error { - return s.http.Shutdown(context.Background()) -} diff --git a/cmd/rr/main.go b/cmd/rr/main.go index 625e879c..ea16a99a 100644 --- a/cmd/rr/main.go +++ b/cmd/rr/main.go @@ -24,7 +24,11 @@ package main import ( rr "github.com/spiral/roadrunner/cmd/rr/cmd" - "github.com/spiral/roadrunner/rpc" + + // services (plugins) + "github.com/spiral/roadrunner/service/http" + "github.com/spiral/roadrunner/service/rpc" + "github.com/spiral/roadrunner/service/static" // cli plugins _ "github.com/spiral/roadrunner/cmd/rr/http" @@ -32,7 +36,13 @@ import ( func main() { // provides ability to make local connection to services - rr.Container.Register("rpc", new(rpc.Service)) + rr.Container.Register(rpc.Name, new(rpc.Service)) + + // http serving + rr.Container.Register(http.Name, new(http.Service)) + + // serving static files + rr.Container.Register(static.Name, new(static.Service)) // you can register additional commands using cmd.CLI rr.Execute() diff --git a/cmd/rr/utils/size.go b/cmd/rr/utils/size.go deleted file mode 100644 index 176cc9e1..00000000 --- a/cmd/rr/utils/size.go +++ /dev/null @@ -1,28 +0,0 @@ -package utils - -import ( - "strconv" - "strings" -) - -func ParseSize(size string) int64 { - if len(size) == 0 { - return 0 - } - - s, err := strconv.Atoi(size[:len(size)-1]) - if err != nil { - return 0 - } - - switch strings.ToLower(size[len(size)-1:]) { - case "k", "kb": - return int64(s * 1024) - case "m", "mb": - return int64(s * 1024 * 1024) - case "g", "gb": - return int64(s * 1024 * 1024 * 1024) - } - - return 0 -} diff --git a/cmd/rr/utils/utils_test.go b/cmd/rr/utils/utils_test.go deleted file mode 100644 index f67b2a10..00000000 --- a/cmd/rr/utils/utils_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package utils - -import ( - "testing" - "github.com/magiconair/properties/assert" -) - -func TestUtils(t *testing.T) { - assert.Equal(t, int64(1024), ParseSize("1K")) - assert.Equal(t, int64(1024*1024), ParseSize("1M")) - assert.Equal(t, int64(2*1024*1024*1024), ParseSize("2G")) -} diff --git a/service/container.go b/service/container.go index bf712092..3395cd86 100644 --- a/service/container.go +++ b/service/container.go @@ -39,19 +39,6 @@ type Container interface { Stop() } -// svc provides high level functionality for road runner svc. -type Service interface { - // Configure 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. - Configure(cfg Config, c Container) (enabled bool, err error) - - // Serve serves svc. - Serve() error - - // Close setStopped svc svc. - Stop() -} - type container struct { log logrus.FieldLogger mu sync.Mutex diff --git a/service/http/config.go b/service/http/config.go new file mode 100644 index 00000000..b828eb08 --- /dev/null +++ b/service/http/config.go @@ -0,0 +1,34 @@ +package http + +import ( + "github.com/spiral/roadrunner" + "fmt" +) + +// Configures RoadRunner HTTP server. +type Config struct { + // Enable enables http service. + Enable bool + + // Host and port to handle as http server. + Host, Port string + + // MaxRequest specified max size for payload body in bytes, set 0 to unlimited. + MaxRequest int64 + + // Uploads configures uploads configuration. + Uploads *FsConfig + + // Workers configures roadrunner server and worker pool. + Workers *roadrunner.ServerConfig +} + +// Valid validates the configuration. +func (cfg *Config) Valid() error { + return nil +} + +// httpAddr returns prepared http listen address. +func (cfg *Config) httpAddr() string { + return fmt.Sprintf("%s:%v", cfg.Host, cfg.Port) +} diff --git a/http/fs_config.go b/service/http/fs_config.go index de5b1389..de5b1389 100644 --- a/http/fs_config.go +++ b/service/http/fs_config.go diff --git a/http/fs_config_test.go b/service/http/fs_config_test.go index 05f568e5..05f568e5 100644 --- a/http/fs_config_test.go +++ b/service/http/fs_config_test.go diff --git a/service/http/handler.go b/service/http/handler.go new file mode 100644 index 00000000..1319200c --- /dev/null +++ b/service/http/handler.go @@ -0,0 +1,71 @@ +package http + +import ( + "net/http" + "strconv" + "github.com/sirupsen/logrus" + "github.com/spiral/roadrunner" + "github.com/pkg/errors" +) + +// 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 + rr *roadrunner.Server +} + +// Handle serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled. +func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) { + // validating request size + if h.cfg.MaxRequest != 0 { + if length := r.Header.Get("content-length"); length != "" { + if size, err := strconv.ParseInt(length, 10, 64); err != nil { + h.sendError(w, r, err) + return + } else if size > h.cfg.MaxRequest { + h.sendError(w, r, errors.New("request body max size is exceeded")) + return + } + } + } + + req, err := NewRequest(r) + if err != nil { + h.sendError(w, r, err) + return + } + + if err = req.Open(h.cfg); err != nil { + h.sendError(w, r, err) + return + } + defer req.Close() + + p, err := req.Payload() + if err != nil { + h.sendError(w, r, err) + return + } + + rsp, err := h.rr.Exec(p) + if err != nil { + h.sendError(w, r, err) + return + } + + resp, err := NewResponse(rsp) + if err != nil { + h.sendError(w, r, err) + return + } + + resp.Write(w) +} + +// sendError sends error +func (h *Handler) sendError(w http.ResponseWriter, r *http.Request, err error) { + logrus.Errorf("http: %s", err) + w.WriteHeader(500) + w.Write([]byte(err.Error())) +} diff --git a/http/parse.go b/service/http/parse.go index 898f39a1..898f39a1 100644 --- a/http/parse.go +++ b/service/http/parse.go diff --git a/_____/http/request.go b/service/http/request.go index fd483744..fd483744 100644 --- a/_____/http/request.go +++ b/service/http/request.go diff --git a/http/response.go b/service/http/response.go index dd092353..dd092353 100644 --- a/http/response.go +++ b/service/http/response.go diff --git a/_____/http/rpc.go b/service/http/rpc.go index 673ff2bb..673ff2bb 100644 --- a/_____/http/rpc.go +++ b/service/http/rpc.go diff --git a/service/http/service.go b/service/http/service.go new file mode 100644 index 00000000..5a0d4c16 --- /dev/null +++ b/service/http/service.go @@ -0,0 +1,86 @@ +package http + +import ( + "net/http" + "github.com/spiral/roadrunner/service" + "context" + "github.com/spiral/roadrunner" +) + +// Name contains default service name. +const Name = "http" + +type Middleware interface { + // Handle must return true if request/response pair is handled withing the middleware. + Handle(w http.ResponseWriter, r *http.Request) bool +} + +// Service manages rr, http servers. +type Service struct { + middleware []Middleware + cfg *Config + rr *roadrunner.Server + handler *Handler + http *http.Server +} + +func (s *Service) Add(m Middleware) { + s.middleware = append(s.middleware, m) +} + +// Configure 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) Configure(cfg service.Config, c service.Container) (bool, error) { + config := &Config{} + if err := cfg.Unmarshal(config); err != nil { + return false, err + } + + if !config.Enable { + return false, nil + } + + if err := config.Valid(); err != nil { + return false, err + } + + s.cfg = config + return true, nil +} + +// Serve serves the service. +func (s *Service) Serve() error { + rr := roadrunner.NewServer(s.cfg.Workers) + if err := rr.Start(); err != nil { + return err + } + defer s.rr.Stop() + + // todo: observer + + s.rr = rr + s.handler = &Handler{cfg: s.cfg, rr: s.rr} + s.http = &http.Server{Addr: s.cfg.httpAddr(), Handler: s} + + if err := s.http.ListenAndServe(); err != nil { + return err + } + + return nil +} + +// Stop stops the service. +func (s *Service) Stop() { + s.http.Shutdown(context.Background()) +} + +// Handle handles connection using set of middleware and rr PSR-7 server. +func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { + for _, m := range s.middleware { + if m.Handle(w, r) { + return + } + } + + s.handler.Handle(w, r) +} diff --git a/http/uploads.go b/service/http/uploads.go index cdd3e52c..cdd3e52c 100644 --- a/http/uploads.go +++ b/service/http/uploads.go diff --git a/rpc/config.go b/service/rpc/config.go index 8a34752a..8a34752a 100644 --- a/rpc/config.go +++ b/service/rpc/config.go diff --git a/rpc/config_test.go b/service/rpc/config_test.go index a953e30e..a953e30e 100644 --- a/rpc/config_test.go +++ b/service/rpc/config_test.go diff --git a/rpc/service.go b/service/rpc/service.go index 516f341a..ce1e3351 100644 --- a/rpc/service.go +++ b/service/rpc/service.go @@ -8,7 +8,8 @@ import ( "sync" ) -// todo: improved logging +// Name contains default service name. +const Name = "rpc" // Service is RPC service. type Service struct { @@ -20,8 +21,8 @@ type Service struct { serving bool } -// Configure must return configure service and return true if service is enabled. Must return error in case of -// misconfiguration. +// Configure 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) Configure(cfg service.Config, reg service.Container) (enabled bool, err error) { config := &config{} if err := cfg.Unmarshal(config); err != nil { @@ -38,7 +39,7 @@ func (s *Service) Configure(cfg service.Config, reg service.Container) (enabled return true, nil } -// Serve serves Service. +// Serve serves the service. func (s *Service) Serve() error { if s.rpc == nil { return errors.New("RPC service is not configured") @@ -80,7 +81,7 @@ func (s *Service) Serve() error { return nil } -// Close stop Service Service. +// Stop stops the service. func (s *Service) Stop() { s.mu.Lock() defer s.mu.Unlock() diff --git a/rpc/service_test.go b/service/rpc/service_test.go index a57ce1bd..a57ce1bd 100644 --- a/rpc/service_test.go +++ b/service/rpc/service_test.go diff --git a/service/entry.go b/service/service.go index cf44349c..4c159fe1 100644 --- a/service/entry.go +++ b/service/service.go @@ -2,6 +2,19 @@ package service import "sync" +// svc provides high level functionality for road runner svc. +type Service interface { + // Configure 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. + Configure(cfg Config, c Container) (enabled bool, err error) + + // Serve serves. + Serve() error + + // Stop stops the service. + Stop() +} + const ( // StatusUndefined when service bus can not find the service. StatusUndefined = iota diff --git a/service/static/config.go b/service/static/config.go new file mode 100644 index 00000000..2a1f6c13 --- /dev/null +++ b/service/static/config.go @@ -0,0 +1,52 @@ +package static + +import ( + "strings" + "path" + "os" + "github.com/pkg/errors" +) + +// Config describes file location and controls access to them. +type Config struct { + // Enables StaticFile service. + Enable bool + + // 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 +} + +// Forbid must return true if file extension is not allowed for the upload. +func (cfg *Config) Forbids(filename string) bool { + ext := strings.ToLower(path.Ext(filename)) + + for _, v := range cfg.Forbid { + if ext == v { + return true + } + } + + return false +} + +// Valid validates existence of directory. +func (cfg *Config) Valid() error { + st, err := os.Stat(cfg.Dir) + if err != nil { + if os.IsNotExist(err) { + return errors.New("root directory does not exists") + } + + return err + } + + if !st.IsDir() { + return errors.New("invalid root directory") + } + + return nil +} diff --git a/service/static/config_test.go b/service/static/config_test.go new file mode 100644 index 00000000..ce31348a --- /dev/null +++ b/service/static/config_test.go @@ -0,0 +1,21 @@ +package static + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestConfig_Forbids(t *testing.T) { + cfg := Config{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 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 new file mode 100644 index 00000000..916c18a2 --- /dev/null +++ b/service/static/service.go @@ -0,0 +1,120 @@ +package static + +import ( + "github.com/sirupsen/logrus" + "net/http" + "os" + "path" + "strings" + rrttp "github.com/spiral/roadrunner/service/http" + "github.com/spiral/roadrunner/service" +) + +// Name contains default service name. +const Name = "static-server" + +// Service serves static files. Potentially convert into middleware? +type Service struct { + // Logger is associated debug and error logger. Can be empty. + Logger *logrus.Logger + + // server configuration (location, forbidden files and etc) + cfg *Config + + // root is initiated http directory + root http.Dir + + // let's service stay running + done chan interface{} +} + +// Configure 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) Configure(cfg service.Config, c service.Container) (enabled bool, err error) { + config := &Config{} + if err := cfg.Unmarshal(config); err != nil { + return false, err + } + + if !config.Enable { + return false, nil + } + + if err := config.Valid(); err != nil { + return false, err + } + + s.cfg = config + s.root = http.Dir(s.cfg.Dir) + + // registering as middleware + if h, ok := c.Get(rrttp.Name); ok >= service.StatusConfigured { + if h, ok := h.(*rrttp.Service); ok { + h.Add(s) + } + } else { + if s.Logger != nil { + s.Logger.Warningf("no http service found") + } + } + + return true, nil +} + +// Serve serves the service. +func (s *Service) Serve() error { + s.done = make(chan interface{}) + <-s.done + + return nil +} + +// Stop stops the service. +func (s *Service) Stop() { + //todo: this is not safe (TODO CHECK IT?) + close(s.done) +} + +// Handle must return true if request/response pair is handled withing the middleware. +func (s *Service) Handle(w http.ResponseWriter, r *http.Request) bool { + fPath := r.URL.Path + if !strings.HasPrefix(fPath, "/") { + fPath = "/" + fPath + } + fPath = path.Clean(fPath) + + if s.cfg.Forbids(fPath) { + if s.Logger != nil { + s.Logger.Warningf("attempt to access forbidden file %s", fPath) + } + return false + } + + f, err := s.root.Open(fPath) + if err != nil { + if !os.IsNotExist(err) { + if s.Logger != nil { + s.Logger.Error(err) + } + } + + return false + } + defer f.Close() + + d, err := f.Stat() + if err != nil { + if s.Logger != nil { + s.Logger.Error(err) + } + return false + } + + // do not Handle directories + if d.IsDir() { + return false + } + + http.ServeContent(w, r, d.Name(), d.ModTime(), f) + return true +} |