diff options
author | Valery Piashchynski <[email protected]> | 2021-05-14 09:19:00 +0300 |
---|---|---|
committer | Valery Piashchynski <[email protected]> | 2021-05-14 09:19:00 +0300 |
commit | f76b2392cd5b0c4e9f38736323a29b46f9451f0e (patch) | |
tree | b463fd9267323bec1ed8a63f871686ed99843611 /plugins | |
parent | fef96198ee6cc1f23bc869050944aa3071667ae7 (diff) | |
parent | e1ff9daead5033b537296ffb071e551b95af91ab (diff) |
Merge remote-tracking branch 'origin/master' into feature/websockets-plugin
# Conflicts:
# plugins/http/plugin.go
# plugins/static/etag.go
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/http/config/http.go | 21 | ||||
-rw-r--r-- | plugins/http/config/static.go | 58 | ||||
-rw-r--r-- | plugins/http/plugin.go | 59 | ||||
-rw-r--r-- | plugins/http/serve.go | 12 | ||||
-rw-r--r-- | plugins/http/static/static.go | 88 | ||||
-rw-r--r-- | plugins/server/config.go | 2 | ||||
-rw-r--r-- | plugins/server/plugin.go | 3 | ||||
-rw-r--r-- | plugins/static/config.go | 55 | ||||
-rw-r--r-- | plugins/static/etag.go (renamed from plugins/http/static/etag.go) | 27 | ||||
-rw-r--r-- | plugins/static/plugin.go | 188 |
10 files changed, 278 insertions, 235 deletions
diff --git a/plugins/http/config/http.go b/plugins/http/config/http.go index 59735e2e..8b63395f 100644 --- a/plugins/http/config/http.go +++ b/plugins/http/config/http.go @@ -33,9 +33,6 @@ type HTTP struct { // Uploads configures uploads configuration. Uploads *Uploads `mapstructure:"uploads"` - // static configuration - Static *Static `mapstructure:"static"` - // Pool configures worker pool. Pool *poolImpl.Config `mapstructure:"pool"` @@ -103,16 +100,6 @@ func (c *HTTP) InitDefaults() error { c.SSLConfig.Address = "127.0.0.1:443" } - // static files - if c.Static != nil { - if c.Static.Pattern == "" { - c.Static.Pattern = "/static/" - } - if c.Static.Dir == "" { - c.Static.Dir = "." - } - } - err := c.HTTP2Config.InitDefaults() if err != nil { return err @@ -189,13 +176,5 @@ func (c *HTTP) Valid() error { } } - // validate static - if c.Static != nil { - err := c.Static.Valid() - if err != nil { - return errors.E(op, err) - } - } - return nil } diff --git a/plugins/http/config/static.go b/plugins/http/config/static.go deleted file mode 100644 index 4b7b3a9b..00000000 --- a/plugins/http/config/static.go +++ /dev/null @@ -1,58 +0,0 @@ -package config - -import ( - "os" - - "github.com/spiral/errors" -) - -// Static describes file location and controls access to them. -type Static struct { - // Dir contains name of directory to control access to. - // Default - "." - Dir string - - // HTTP pattern, where to serve static files - // for example - `/static/`, `/my-files/static/`, etc - // Default - /static/ - Pattern string - - // CalculateEtag can be true/false and used to calculate etag for the static - CalculateEtag bool `mapstructure:"calculate_etag"` - - // Weak etag `W/` - Weak bool - - // forbid specifies list of file extensions which are forbidden for access. - // example: .php, .exe, .bat, .htaccess and etc. - Forbid []string - - // Allow specifies list of file extensions which are allowed for access. - // example: .php, .exe, .bat, .htaccess and etc. - Allow []string - - // Request headers to add to every static. - Request map[string]string - - // Response headers to add to every static. - Response map[string]string -} - -// Valid returns nil if config is valid. -func (c *Static) Valid() error { - const op = errors.Op("static_plugin_valid") - st, err := os.Stat(c.Dir) - if err != nil { - if os.IsNotExist(err) { - return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Dir)) - } - - return err - } - - if !st.IsDir() { - return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Dir)) - } - - return nil -} diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go index 3b9c12ea..2b68bbe5 100644 --- a/plugins/http/plugin.go +++ b/plugins/http/plugin.go @@ -5,9 +5,6 @@ import ( "fmt" "log" "net/http" - "os" - "path/filepath" - "strings" "sync" "github.com/hashicorp/go-multierror" @@ -16,11 +13,10 @@ import ( "github.com/spiral/roadrunner/v2/pkg/pool" "github.com/spiral/roadrunner/v2/pkg/process" "github.com/spiral/roadrunner/v2/pkg/worker" - handler "github.com/spiral/roadrunner/v2/pkg/worker_handler" "github.com/spiral/roadrunner/v2/plugins/config" "github.com/spiral/roadrunner/v2/plugins/http/attributes" httpConfig "github.com/spiral/roadrunner/v2/plugins/http/config" - "github.com/spiral/roadrunner/v2/plugins/http/static" + handler "github.com/spiral/roadrunner/v2/plugins/http/worker_handler" "github.com/spiral/roadrunner/v2/plugins/logger" "github.com/spiral/roadrunner/v2/plugins/server" "github.com/spiral/roadrunner/v2/plugins/status" @@ -136,7 +132,7 @@ func (s *Plugin) Serve() chan error { return errCh } -func (s *Plugin) serve(errCh chan error) { //nolint:gocognit +func (s *Plugin) serve(errCh chan error) { var err error const op = errors.Op("http_plugin_serve") s.pool, err = s.server.NewWorkerPool(context.Background(), pool.Config{ @@ -165,56 +161,11 @@ func (s *Plugin) serve(errCh chan error) { //nolint:gocognit s.handler.AddListener(s.logCallback) - // Create new HTTP Multiplexer - mux := http.NewServeMux() - - // if we have static, handler here, create a fileserver - if s.cfg.Static != nil { - h := http.FileServer(static.FS(s.cfg.Static)) - // Static files handler - mux.HandleFunc(s.cfg.Static.Pattern, func(w http.ResponseWriter, r *http.Request) { - if s.cfg.Static.Request != nil { - for k, v := range s.cfg.Static.Request { - r.Header.Add(k, v) - } - } - - if s.cfg.Static.Response != nil { - for k, v := range s.cfg.Static.Response { - w.Header().Set(k, v) - } - } - - // calculate etag for the resource - if s.cfg.Static.CalculateEtag { - // do not allow paths like ../../resource - // only specified folder and resources in it - // https://lgtm.com/rules/1510366186013/ - if strings.Contains(r.URL.Path, "..") { - w.WriteHeader(http.StatusForbidden) - return - } - f, errS := os.Open(filepath.Join(s.cfg.Static.Dir, r.URL.Path)) - if errS != nil { - s.log.Warn("error opening file to calculate the Etag", "provided path", r.URL.Path) - } - - // Set etag value to the ResponseWriter - static.SetEtag(s.cfg.Static, f, w) - } - - h.ServeHTTP(w, r) - }) - } - - // handle main route - mux.HandleFunc("/", s.ServeHTTP) - if s.cfg.EnableHTTP() { if s.cfg.EnableH2C() { - s.http = &http.Server{Handler: h2c.NewHandler(mux, &http2.Server{}), ErrorLog: s.stdLog} + s.http = &http.Server{Handler: h2c.NewHandler(s, &http2.Server{}), ErrorLog: s.stdLog} } else { - s.http = &http.Server{Handler: mux, ErrorLog: s.stdLog} + s.http = &http.Server{Handler: s, ErrorLog: s.stdLog} } } @@ -238,7 +189,7 @@ func (s *Plugin) serve(errCh chan error) { //nolint:gocognit } if s.cfg.EnableFCGI() { - s.fcgi = &http.Server{Handler: mux, ErrorLog: s.stdLog} + s.fcgi = &http.Server{Handler: s, ErrorLog: s.stdLog} } // start http, https and fcgi servers if requested in the config diff --git a/plugins/http/serve.go b/plugins/http/serve.go index 26fccf79..734860f5 100644 --- a/plugins/http/serve.go +++ b/plugins/http/serve.go @@ -231,12 +231,24 @@ func (s *Plugin) tlsAddr(host string, forcePort bool) string { return host } +// static plugin name +const static string = "static" + func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log logger.Logger) { for i := len(order) - 1; i >= 0; i-- { + // set static last in the row + if order[i] == static { + continue + } if mdwr, ok := middlewares[order[i]]; ok { server.Handler = mdwr.Middleware(server.Handler) } else { log.Warn("requested middleware does not exist", "requested", order[i]) } } + + // set static if exists + if mdwr, ok := middlewares[static]; ok { + server.Handler = mdwr.Middleware(server.Handler) + } } diff --git a/plugins/http/static/static.go b/plugins/http/static/static.go deleted file mode 100644 index d0278466..00000000 --- a/plugins/http/static/static.go +++ /dev/null @@ -1,88 +0,0 @@ -package static - -import ( - "io/fs" - "net/http" - "path/filepath" - "strings" - - httpConfig "github.com/spiral/roadrunner/v2/plugins/http/config" -) - -type ExtensionFilter struct { - allowed map[string]struct{} - forbidden map[string]struct{} -} - -func NewExtensionFilter(allow, forbid []string) *ExtensionFilter { - ef := &ExtensionFilter{ - allowed: make(map[string]struct{}, len(allow)), - forbidden: make(map[string]struct{}, len(forbid)), - } - - for i := 0; i < len(forbid); i++ { - // skip empty lines - if forbid[i] == "" { - continue - } - ef.forbidden[forbid[i]] = struct{}{} - } - - for i := 0; i < len(allow); i++ { - // skip empty lines - if allow[i] == "" { - continue - } - ef.allowed[allow[i]] = struct{}{} - } - - // check if any forbidden items presented in the allowed - // if presented, delete such items from allowed - for k := range ef.allowed { - if _, ok := ef.forbidden[k]; ok { - delete(ef.allowed, k) - } - } - - return ef -} - -type FileSystem struct { - ef *ExtensionFilter - // embedded - http.FileSystem -} - -// Open wrapper around http.FileSystem Open method, name here is the name of the -func (f FileSystem) Open(name string) (http.File, error) { - file, err := f.FileSystem.Open(name) - if err != nil { - return nil, err - } - - fstat, err := file.Stat() - if err != nil { - return nil, fs.ErrNotExist - } - - if fstat.IsDir() { - return nil, fs.ErrPermission - } - - ext := strings.ToLower(filepath.Ext(fstat.Name())) - if _, ok := f.ef.forbidden[ext]; ok { - return nil, fs.ErrPermission - } - - // if file extension is allowed, append it to the FileInfo slice - if _, ok := f.ef.allowed[ext]; ok { - return file, nil - } - - return nil, fs.ErrNotExist -} - -// FS is a constructor for the http.FileSystem -func FS(config *httpConfig.Static) http.FileSystem { - return FileSystem{NewExtensionFilter(config.Allow, config.Forbid), http.Dir(config.Dir)} -} diff --git a/plugins/server/config.go b/plugins/server/config.go index a4b0d91c..00ce4140 100644 --- a/plugins/server/config.go +++ b/plugins/server/config.go @@ -4,7 +4,7 @@ import ( "time" ) -// All config (.rr.yaml) +// Config All config (.rr.yaml) // For other section use pointer to distinguish between `empty` and `not present` type Config struct { // Server config section diff --git a/plugins/server/plugin.go b/plugins/server/plugin.go index 320da372..ef77f7ab 100644 --- a/plugins/server/plugin.go +++ b/plugins/server/plugin.go @@ -58,8 +58,7 @@ func (server *Plugin) Name() string { } // Available interface implementation -func (server *Plugin) Available() { -} +func (server *Plugin) Available() {} // Serve (Start) server plugin (just a mock here to satisfy interface) func (server *Plugin) Serve() chan error { diff --git a/plugins/static/config.go b/plugins/static/config.go new file mode 100644 index 00000000..c3f9c17d --- /dev/null +++ b/plugins/static/config.go @@ -0,0 +1,55 @@ +package static + +import ( + "os" + + "github.com/spiral/errors" +) + +// Config describes file location and controls access to them. +type Config struct { + Static *struct { + // Dir contains name of directory to control access to. + // Default - "." + Dir string + + // CalculateEtag can be true/false and used to calculate etag for the static + CalculateEtag bool `mapstructure:"calculate_etag"` + + // Weak etag `W/` + Weak bool + + // forbid specifies list of file extensions which are forbidden for access. + // example: .php, .exe, .bat, .htaccess and etc. + Forbid []string + + // Allow specifies list of file extensions which are allowed for access. + // example: .php, .exe, .bat, .htaccess and etc. + Allow []string + + // Request headers to add to every static. + Request map[string]string + + // Response headers to add to every static. + Response map[string]string + } +} + +// Valid returns nil if config is valid. +func (c *Config) Valid() error { + const op = errors.Op("static_plugin_valid") + st, err := os.Stat(c.Static.Dir) + if err != nil { + if os.IsNotExist(err) { + return errors.E(op, errors.Errorf("root directory '%s' does not exists", c.Static.Dir)) + } + + return err + } + + if !st.IsDir() { + return errors.E(op, errors.Errorf("invalid root directory '%s'", c.Static.Dir)) + } + + return nil +} diff --git a/plugins/http/static/etag.go b/plugins/static/etag.go index c457b95e..5ee0d2f3 100644 --- a/plugins/http/static/etag.go +++ b/plugins/static/etag.go @@ -4,9 +4,7 @@ import ( "hash/crc32" "io" "net/http" - "os" - httpConfig "github.com/spiral/roadrunner/v2/plugins/http/config" "github.com/spiral/roadrunner/v2/utils" ) @@ -18,7 +16,22 @@ var weakPrefix = []byte(`W/`) // CRC32 table var crc32q = crc32.MakeTable(0x48D90782) -func SetEtag(cfg *httpConfig.Static, f *os.File, w http.ResponseWriter) { +// SetEtag sets etag for the file +func SetEtag(weak bool, f http.File, name string, w http.ResponseWriter) { + // preallocate + calculatedEtag := make([]byte, 0, 64) + + // write weak + if weak { + calculatedEtag = append(calculatedEtag, weakPrefix...) + calculatedEtag = append(calculatedEtag, '"') + calculatedEtag = appendUint(calculatedEtag, crc32.Checksum(utils.AsBytes(name), crc32q)) + calculatedEtag = append(calculatedEtag, '"') + + w.Header().Set(etag, utils.AsString(calculatedEtag)) + return + } + // read the file content body, err := io.ReadAll(f) if err != nil { @@ -30,14 +43,6 @@ func SetEtag(cfg *httpConfig.Static, f *os.File, w http.ResponseWriter) { return } - // preallocate - calculatedEtag := make([]byte, 0, 64) - - // write weak - if cfg.Weak { - calculatedEtag = append(calculatedEtag, weakPrefix...) - } - calculatedEtag = append(calculatedEtag, '"') calculatedEtag = appendUint(calculatedEtag, uint32(len(body))) calculatedEtag = append(calculatedEtag, '-') diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go new file mode 100644 index 00000000..f6d9a0f2 --- /dev/null +++ b/plugins/static/plugin.go @@ -0,0 +1,188 @@ +package static + +import ( + "net/http" + "path" + "strings" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/logger" +) + +// PluginName contains default service name. +const PluginName = "static" + +const RootPluginName = "http" + +// Plugin serves static files. Potentially convert into middleware? +type Plugin struct { + // server configuration (location, forbidden files and etc) + cfg *Config + + log logger.Logger + + // root is initiated http directory + root http.Dir + + // file extensions which are allowed to be served + allowedExtensions map[string]struct{} + + // file extensions which are forbidden to be served + forbiddenExtensions map[string]struct{} +} + +// 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 *Plugin) Init(cfg config.Configurer, log logger.Logger) error { + const op = errors.Op("static_plugin_init") + if !cfg.Has(RootPluginName) { + return errors.E(op, errors.Disabled) + } + + err := cfg.UnmarshalKey(RootPluginName, &s.cfg) + if err != nil { + return errors.E(op, errors.Disabled, err) + } + + if s.cfg.Static == nil { + return errors.E(op, errors.Disabled) + } + + s.log = log + s.root = http.Dir(s.cfg.Static.Dir) + + err = s.cfg.Valid() + if err != nil { + return errors.E(op, err) + } + + // create 2 hashmaps with the allowed and forbidden file extensions + s.allowedExtensions = make(map[string]struct{}, len(s.cfg.Static.Allow)) + s.forbiddenExtensions = make(map[string]struct{}, len(s.cfg.Static.Forbid)) + + // init forbidden + for i := 0; i < len(s.cfg.Static.Forbid); i++ { + // skip empty lines + if s.cfg.Static.Forbid[i] == "" { + continue + } + s.forbiddenExtensions[s.cfg.Static.Forbid[i]] = struct{}{} + } + + // init allowed + for i := 0; i < len(s.cfg.Static.Allow); i++ { + // skip empty lines + if s.cfg.Static.Allow[i] == "" { + continue + } + s.allowedExtensions[s.cfg.Static.Allow[i]] = struct{}{} + } + + // check if any forbidden items presented in the allowed + // if presented, delete such items from allowed + for k := range s.forbiddenExtensions { + delete(s.allowedExtensions, k) + } + + // at this point we have distinct allowed and forbidden hashmaps, also with alwaysServed + return nil +} + +func (s *Plugin) Name() string { + return PluginName +} + +// Middleware must return true if request/response pair is handled within the middleware. +func (s *Plugin) Middleware(next http.Handler) http.Handler { + // Define the http.HandlerFunc + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // do not allow paths like ../../resource + // only specified folder and resources in it + // https://lgtm.com/rules/1510366186013/ + if strings.Contains(r.URL.Path, "..") { + w.WriteHeader(http.StatusForbidden) + return + } + + if s.cfg.Static.Request != nil { + for k, v := range s.cfg.Static.Request { + r.Header.Add(k, v) + } + } + + if s.cfg.Static.Response != nil { + for k, v := range s.cfg.Static.Response { + w.Header().Set(k, v) + } + } + + // first - create a proper file path + fPath := path.Clean(r.URL.Path) + ext := strings.ToLower(path.Ext(fPath)) + + // check that file extension in the forbidden list + if _, ok := s.forbiddenExtensions[ext]; ok { + s.log.Debug("file extension is forbidden", "ext", ext) + next.ServeHTTP(w, r) + return + } + + // if we have some allowed extensions, we should check them + // if not - all extensions allowed except forbidden + if len(s.allowedExtensions) > 0 { + // not found in allowed + if _, ok := s.allowedExtensions[ext]; !ok { + next.ServeHTTP(w, r) + return + } + + // file extension allowed + } + + // ok, file is not in the forbidden list + // Stat it and get file info + f, err := s.root.Open(fPath) + if err != nil { + // else no such file, show error in logs only in debug mode + s.log.Debug("no such file or directory", "error", err) + // pass request to the worker + next.ServeHTTP(w, r) + return + } + + // at high confidence there is should not be an error + // because we stat-ed the path previously and know, that that is file (not a dir), and it exists + finfo, err := f.Stat() + if err != nil { + // else no such file, show error in logs only in debug mode + s.log.Debug("no such file or directory", "error", err) + // pass request to the worker + next.ServeHTTP(w, r) + return + } + + defer func() { + err = f.Close() + if err != nil { + s.log.Error("file close error", "error", err) + } + }() + + // if provided path to the dir, do not serve the dir, but pass the request to the worker + if finfo.IsDir() { + s.log.Debug("possible path to dir provided") + // pass request to the worker + next.ServeHTTP(w, r) + return + } + + // set etag + if s.cfg.Static.CalculateEtag { + SetEtag(s.cfg.Static.Weak, f, finfo.Name(), w) + } + + // we passed all checks - serve the file + http.ServeContent(w, r, finfo.Name(), finfo.ModTime(), f) + }) +} |