diff options
author | Valery Piashchynski <[email protected]> | 2021-04-29 10:25:45 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2021-04-29 10:25:45 +0300 |
commit | 7297e5f2fad841466024f8622da3e14b7874f989 (patch) | |
tree | 6c982f5ace059292ec7f748bd32fa6d1ca7719f0 /plugins | |
parent | a6b755e344324505ea0d327ff12fb9eeae7d6dab (diff) | |
parent | 2812157be7a9c1411d02872f0b9fa567bcf7a9b7 (diff) |
#646 feat(static): completely rework `static` plugin
#646 feat(static): completely rework `static` plugin
Diffstat (limited to 'plugins')
18 files changed, 558 insertions, 449 deletions
diff --git a/plugins/gzip/plugin.go b/plugins/gzip/plugin.go index 949c6888..18ee7b88 100644 --- a/plugins/gzip/plugin.go +++ b/plugins/gzip/plugin.go @@ -15,10 +15,10 @@ func (g *Plugin) Init() error { return nil } -func (g *Plugin) Middleware(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (g *Plugin) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gziphandler.GzipHandler(next).ServeHTTP(w, r) - } + }) } func (g *Plugin) Name() string { diff --git a/plugins/headers/plugin.go b/plugins/headers/plugin.go index a5ee702f..dea0d127 100644 --- a/plugins/headers/plugin.go +++ b/plugins/headers/plugin.go @@ -38,9 +38,9 @@ func (s *Plugin) Init(cfg config.Configurer) error { } // middleware must return true if request/response pair is handled within the middleware. -func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { +func (s *Plugin) Middleware(next http.Handler) http.Handler { // Define the http.HandlerFunc - return func(w http.ResponseWriter, r *http.Request) { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if s.cfg.Headers.Request != nil { for k, v := range s.cfg.Headers.Request { r.Header.Add(k, v) @@ -62,7 +62,7 @@ func (s *Plugin) Middleware(next http.Handler) http.HandlerFunc { } next.ServeHTTP(w, r) - } + }) } func (s *Plugin) Name() string { diff --git a/plugins/http/config/http.go b/plugins/http/config/http.go index 8b63395f..59735e2e 100644 --- a/plugins/http/config/http.go +++ b/plugins/http/config/http.go @@ -33,6 +33,9 @@ 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"` @@ -100,6 +103,16 @@ 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 @@ -176,5 +189,13 @@ 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 new file mode 100644 index 00000000..4b7b3a9b --- /dev/null +++ b/plugins/http/config/static.go @@ -0,0 +1,58 @@ +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 01bd243f..2b1dec89 100644 --- a/plugins/http/plugin.go +++ b/plugins/http/plugin.go @@ -2,14 +2,11 @@ package http import ( "context" - "crypto/tls" - "crypto/x509" "fmt" - "io/ioutil" "log" "net/http" - "net/http/fcgi" - "net/url" + "os" + "path/filepath" "strings" "sync" @@ -22,29 +19,28 @@ import ( "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" - "github.com/spiral/roadrunner/v2/utils" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" - "golang.org/x/sys/cpu" ) const ( // PluginName declares plugin name. PluginName = "http" - // RR_HTTP env variable key (internal) if the HTTP presents - RR_MODE = "RR_MODE" //nolint:golint,stylecheck + // RrMode RR_HTTP env variable key (internal) if the HTTP presents + RrMode = "RR_MODE" - // HTTPS_SCHEME - HTTPS_SCHEME = "https" //nolint:golint,stylecheck + HTTPSScheme = "https" ) // Middleware interface type Middleware interface { - Middleware(f http.Handler) http.HandlerFunc + Middleware(f http.Handler) http.Handler } type middleware map[string]Middleware @@ -59,7 +55,9 @@ type Plugin struct { // stdlog passed to the http/https/fcgi servers to log their internal messages stdLog *log.Logger + // http configuration cfg *httpConfig.HTTP `mapstructure:"http"` + // middlewares to chain mdwr middleware @@ -67,7 +65,7 @@ type Plugin struct { pool pool.Pool // servers RR handler - handler *Handler + handler *handler.Handler // servers http *http.Server @@ -109,14 +107,14 @@ func (s *Plugin) Init(cfg config.Configurer, rrLogger logger.Logger, server serv s.cfg.Env = make(map[string]string) } - s.cfg.Env[RR_MODE] = "http" + s.cfg.Env[RrMode] = "http" s.server = server return nil } func (s *Plugin) logCallback(event interface{}) { - if ev, ok := event.(ResponseEvent); ok { + if ev, ok := event.(handler.ResponseEvent); ok { s.log.Debug(fmt.Sprintf("%d %s %s", ev.Response.Status, ev.Request.Method, ev.Request.URI), "remote", ev.Request.RemoteAddr, "elapsed", ev.Elapsed().String(), @@ -138,7 +136,7 @@ func (s *Plugin) Serve() chan error { return errCh } -func (s *Plugin) serve(errCh chan error) { +func (s *Plugin) serve(errCh chan error) { //nolint:gocognit var err error const op = errors.Op("http_plugin_serve") s.pool, err = s.server.NewWorkerPool(context.Background(), pool.Config{ @@ -154,7 +152,7 @@ func (s *Plugin) serve(errCh chan error) { return } - s.handler, err = NewHandler( + s.handler, err = handler.NewHandler( s.cfg.MaxRequestSize, *s.cfg.Uploads, s.cfg.Cidrs, @@ -167,11 +165,56 @@ func (s *Plugin) serve(errCh chan error) { 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(s, &http2.Server{}), ErrorLog: s.stdLog} + s.http = &http.Server{Handler: h2c.NewHandler(mux, &http2.Server{}), ErrorLog: s.stdLog} } else { - s.http = &http.Server{Handler: s, ErrorLog: s.stdLog} + s.http = &http.Server{Handler: mux, ErrorLog: s.stdLog} } } @@ -195,7 +238,7 @@ func (s *Plugin) serve(errCh chan error) { } if s.cfg.EnableFCGI() { - s.fcgi = &http.Server{Handler: s, ErrorLog: s.stdLog} + s.fcgi = &http.Server{Handler: mux, ErrorLog: s.stdLog} } // start http, https and fcgi servers if requested in the config @@ -212,72 +255,6 @@ func (s *Plugin) serve(errCh chan error) { }() } -func (s *Plugin) serveHTTP(errCh chan error) { - if s.http == nil { - return - } - - const op = errors.Op("http_plugin_serve_http") - applyMiddlewares(s.http, s.mdwr, s.cfg.Middleware, s.log) - l, err := utils.CreateListener(s.cfg.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = s.http.Serve(l) - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - -func (s *Plugin) serveHTTPS(errCh chan error) { - if s.https == nil { - return - } - - const op = errors.Op("http_plugin_serve_https") - applyMiddlewares(s.https, s.mdwr, s.cfg.Middleware, s.log) - l, err := utils.CreateListener(s.cfg.SSLConfig.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = s.https.ServeTLS( - l, - s.cfg.SSLConfig.Cert, - s.cfg.SSLConfig.Key, - ) - - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - -// serveFCGI starts FastCGI server. -func (s *Plugin) serveFCGI(errCh chan error) { - if s.fcgi == nil { - return - } - - const op = errors.Op("http_plugin_serve_fcgi") - applyMiddlewares(s.fcgi, s.mdwr, s.cfg.Middleware, s.log) - l, err := utils.CreateListener(s.cfg.FCGIConfig.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = fcgi.Serve(l, s.fcgi.Handler) - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - // Stop stops the http. func (s *Plugin) Stop() error { s.Lock() @@ -395,7 +372,7 @@ func (s *Plugin) Reset() error { s.log.Info("HTTP workers Pool successfully restarted") - s.handler, err = NewHandler( + s.handler, err = handler.NewHandler( s.cfg.MaxRequestSize, *s.cfg.Uploads, s.cfg.Cidrs, @@ -463,158 +440,3 @@ func (s *Plugin) Ready() status.Status { Code: http.StatusServiceUnavailable, } } - -func (s *Plugin) redirect(w http.ResponseWriter, r *http.Request) { - target := &url.URL{ - Scheme: HTTPS_SCHEME, - // host or host:port - Host: s.tlsAddr(r.Host, false), - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - - http.Redirect(w, r, target.String(), http.StatusPermanentRedirect) -} - -// https://golang.org/pkg/net/http/#Hijacker -//go:inline -func headerContainsUpgrade(r *http.Request) bool { - if _, ok := r.Header["Upgrade"]; ok { - return true - } - return false -} - -// append RootCA to the https server TLS config -func (s *Plugin) appendRootCa() error { - const op = errors.Op("http_plugin_append_root_ca") - rootCAs, err := x509.SystemCertPool() - if err != nil { - return nil - } - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } - - CA, err := ioutil.ReadFile(s.cfg.SSLConfig.RootCA) - if err != nil { - return err - } - - // should append our CA cert - ok := rootCAs.AppendCertsFromPEM(CA) - if !ok { - return errors.E(op, errors.Str("could not append Certs from PEM")) - } - // disable "G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)" - // #nosec G402 - cfg := &tls.Config{ - InsecureSkipVerify: false, - RootCAs: rootCAs, - } - s.http.TLSConfig = cfg - - return nil -} - -// Init https server -func (s *Plugin) 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 priorities 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...) - - sslServer := &http.Server{ - Addr: s.tlsAddr(s.cfg.Address, true), - Handler: s, - ErrorLog: s.stdLog, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - - return sslServer -} - -// init http/2 server -func (s *Plugin) initHTTP2() error { - return http2.ConfigureServer(s.https, &http2.Server{ - MaxConcurrentStreams: s.cfg.HTTP2Config.MaxConcurrentStreams, - }) -} - -// tlsAddr replaces listen or host port with port configured by SSLConfig config. -func (s *Plugin) tlsAddr(host string, forcePort bool) string { - // remove current forcePort first - host = strings.Split(host, ":")[0] - - if forcePort || s.cfg.SSLConfig.Port != 443 { - host = fmt.Sprintf("%s:%v", host, s.cfg.SSLConfig.Port) - } - - return host -} - -func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log logger.Logger) { - if len(middlewares) == 0 { - return - } - for i := 0; i < len(order); i++ { - if mdwr, ok := middlewares[order[i]]; ok { - server.Handler = mdwr.Middleware(server.Handler) - } else { - log.Warn("requested middleware does not exist", "requested", order[i]) - } - } -} diff --git a/plugins/http/serve.go b/plugins/http/serve.go new file mode 100644 index 00000000..338d4339 --- /dev/null +++ b/plugins/http/serve.go @@ -0,0 +1,242 @@ +package http + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "net/http/fcgi" + "net/url" + "os" + "strings" + + "github.com/spiral/errors" + "github.com/spiral/roadrunner/v2/plugins/logger" + "github.com/spiral/roadrunner/v2/utils" + "golang.org/x/net/http2" + "golang.org/x/sys/cpu" +) + +func (s *Plugin) serveHTTP(errCh chan error) { + if s.http == nil { + return + } + const op = errors.Op("http_plugin_serve_http") + + if len(s.mdwr) > 0 { + applyMiddlewares(s.http, s.mdwr, s.cfg.Middleware, s.log) + } + l, err := utils.CreateListener(s.cfg.Address) + if err != nil { + errCh <- errors.E(op, err) + return + } + + err = s.http.Serve(l) + if err != nil && err != http.ErrServerClosed { + errCh <- errors.E(op, err) + return + } +} + +func (s *Plugin) serveHTTPS(errCh chan error) { + if s.https == nil { + return + } + const op = errors.Op("http_plugin_serve_https") + if len(s.mdwr) > 0 { + applyMiddlewares(s.https, s.mdwr, s.cfg.Middleware, s.log) + } + l, err := utils.CreateListener(s.cfg.SSLConfig.Address) + if err != nil { + errCh <- errors.E(op, err) + return + } + + err = s.https.ServeTLS( + l, + s.cfg.SSLConfig.Cert, + s.cfg.SSLConfig.Key, + ) + + if err != nil && err != http.ErrServerClosed { + errCh <- errors.E(op, err) + return + } +} + +// serveFCGI starts FastCGI server. +func (s *Plugin) serveFCGI(errCh chan error) { + if s.fcgi == nil { + return + } + const op = errors.Op("http_plugin_serve_fcgi") + + if len(s.mdwr) > 0 { + applyMiddlewares(s.https, s.mdwr, s.cfg.Middleware, s.log) + } + + l, err := utils.CreateListener(s.cfg.FCGIConfig.Address) + if err != nil { + errCh <- errors.E(op, err) + return + } + + err = fcgi.Serve(l, s.fcgi.Handler) + if err != nil && err != http.ErrServerClosed { + errCh <- errors.E(op, err) + return + } +} + +func (s *Plugin) redirect(w http.ResponseWriter, r *http.Request) { + target := &url.URL{ + Scheme: HTTPSScheme, + // host or host:port + Host: s.tlsAddr(r.Host, false), + Path: r.URL.Path, + RawQuery: r.URL.RawQuery, + } + + http.Redirect(w, r, target.String(), http.StatusPermanentRedirect) +} + +// https://golang.org/pkg/net/http/#Hijacker +//go:inline +func headerContainsUpgrade(r *http.Request) bool { + if _, ok := r.Header["Upgrade"]; ok { + return true + } + return false +} + +// append RootCA to the https server TLS config +func (s *Plugin) appendRootCa() error { + const op = errors.Op("http_plugin_append_root_ca") + rootCAs, err := x509.SystemCertPool() + if err != nil { + return nil + } + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + CA, err := os.ReadFile(s.cfg.SSLConfig.RootCA) + if err != nil { + return err + } + + // should append our CA cert + ok := rootCAs.AppendCertsFromPEM(CA) + if !ok { + return errors.E(op, errors.Str("could not append Certs from PEM")) + } + // disable "G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)" + // #nosec G402 + cfg := &tls.Config{ + InsecureSkipVerify: false, + RootCAs: rootCAs, + } + s.http.TLSConfig = cfg + + return nil +} + +// Init https server +func (s *Plugin) 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 priorities 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...) + + sslServer := &http.Server{ + Addr: s.tlsAddr(s.cfg.Address, true), + Handler: s, + ErrorLog: s.stdLog, + TLSConfig: &tls.Config{ + CurvePreferences: []tls.CurveID{ + tls.CurveP256, + tls.CurveP384, + tls.CurveP521, + tls.X25519, + }, + CipherSuites: DefaultCipherSuites, + MinVersion: tls.VersionTLS12, + PreferServerCipherSuites: true, + }, + } + + return sslServer +} + +// init http/2 server +func (s *Plugin) initHTTP2() error { + return http2.ConfigureServer(s.https, &http2.Server{ + MaxConcurrentStreams: s.cfg.HTTP2Config.MaxConcurrentStreams, + }) +} + +// tlsAddr replaces listen or host port with port configured by SSLConfig config. +func (s *Plugin) tlsAddr(host string, forcePort bool) string { + // remove current forcePort first + host = strings.Split(host, ":")[0] + + if forcePort || s.cfg.SSLConfig.Port != 443 { + host = fmt.Sprintf("%s:%v", host, s.cfg.SSLConfig.Port) + } + + return host +} + +func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log logger.Logger) { + for i := len(order) - 1; i >= 0; i-- { + if mdwr, ok := middlewares[order[i]]; ok { + server.Handler = mdwr.Middleware(server.Handler) + } else { + log.Warn("requested middleware does not exist", "requested", order[i]) + } + } +} diff --git a/plugins/http/static/etag.go b/plugins/http/static/etag.go new file mode 100644 index 00000000..5d41cc53 --- /dev/null +++ b/plugins/http/static/etag.go @@ -0,0 +1,71 @@ +package static + +import ( + "hash/crc32" + "io" + "net/http" + "os" + "unsafe" + + httpConfig "github.com/spiral/roadrunner/v2/plugins/http/config" +) + +const etag string = "Etag" + +// weak Etag prefix +var weakPrefix = []byte(`W/`) + +// CRC32 table +var crc32q = crc32.MakeTable(0x48D90782) + +func SetEtag(cfg *httpConfig.Static, f *os.File, w http.ResponseWriter) { + // read the file content + body, err := io.ReadAll(f) + if err != nil { + return + } + + // skip for 0 body + if len(body) == 0 { + 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, '-') + calculatedEtag = appendUint(calculatedEtag, crc32.Checksum(body, crc32q)) + calculatedEtag = append(calculatedEtag, '"') + + w.Header().Set(etag, byteToSrt(calculatedEtag)) +} + +// appendUint appends n to dst and returns the extended dst. +func appendUint(dst []byte, n uint32) []byte { + var b [20]byte + buf := b[:] + i := len(buf) + var q uint32 + for n >= 10 { + i-- + q = n / 10 + buf[i] = '0' + byte(n-q*10) + n = q + } + i-- + buf[i] = '0' + byte(n) + + dst = append(dst, buf[i:]...) + return dst +} + +func byteToSrt(b []byte) string { + return *(*string)(unsafe.Pointer(&b)) +} diff --git a/plugins/http/static/static.go b/plugins/http/static/static.go new file mode 100644 index 00000000..d0278466 --- /dev/null +++ b/plugins/http/static/static.go @@ -0,0 +1,88 @@ +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/http/constants.go b/plugins/http/worker_handler/constants.go index c3d5c589..3355d9c2 100644 --- a/plugins/http/constants.go +++ b/plugins/http/worker_handler/constants.go @@ -1,4 +1,4 @@ -package http +package handler import "net/http" diff --git a/plugins/http/errors.go b/plugins/http/worker_handler/errors.go index 5889aa76..5fa8e64e 100644 --- a/plugins/http/errors.go +++ b/plugins/http/worker_handler/errors.go @@ -1,6 +1,6 @@ // +build !windows -package http +package handler import ( "errors" diff --git a/plugins/http/errors_windows.go b/plugins/http/worker_handler/errors_windows.go index 3d0ba04c..390cc7d1 100644 --- a/plugins/http/errors_windows.go +++ b/plugins/http/worker_handler/errors_windows.go @@ -1,6 +1,6 @@ // +build windows -package http +package handler import ( "errors" diff --git a/plugins/http/handler.go b/plugins/http/worker_handler/handler.go index d3c928aa..be53fc12 100644 --- a/plugins/http/handler.go +++ b/plugins/http/worker_handler/handler.go @@ -1,4 +1,4 @@ -package http +package handler import ( "net" diff --git a/plugins/http/parse.go b/plugins/http/worker_handler/parse.go index 780e1279..2790da2a 100644 --- a/plugins/http/parse.go +++ b/plugins/http/worker_handler/parse.go @@ -1,4 +1,4 @@ -package http +package handler import ( "net/http" diff --git a/plugins/http/request.go b/plugins/http/worker_handler/request.go index a1398819..178bc827 100644 --- a/plugins/http/request.go +++ b/plugins/http/worker_handler/request.go @@ -1,4 +1,4 @@ -package http +package handler import ( "fmt" diff --git a/plugins/http/response.go b/plugins/http/worker_handler/response.go index 17049ce1..1763d304 100644 --- a/plugins/http/response.go +++ b/plugins/http/worker_handler/response.go @@ -1,4 +1,4 @@ -package http +package handler import ( "io" diff --git a/plugins/http/uploads.go b/plugins/http/worker_handler/uploads.go index f9f8e1c8..e695000e 100644 --- a/plugins/http/uploads.go +++ b/plugins/http/worker_handler/uploads.go @@ -1,4 +1,4 @@ -package http +package handler import ( "github.com/spiral/roadrunner/v2/plugins/http/config" diff --git a/plugins/static/config.go b/plugins/static/config.go deleted file mode 100644 index 90efea76..00000000 --- a/plugins/static/config.go +++ /dev/null @@ -1,76 +0,0 @@ -package static - -import ( - "os" - "path" - "strings" - - "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. - 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 - } -} - -// 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 -} - -// 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.Static.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.Static.Always { - if ext == v { - return true - } - } - - return false -} diff --git a/plugins/static/plugin.go b/plugins/static/plugin.go deleted file mode 100644 index 76cb9e68..00000000 --- a/plugins/static/plugin.go +++ /dev/null @@ -1,117 +0,0 @@ -package static - -import ( - "net/http" - "path" - - "github.com/spiral/errors" - "github.com/spiral/roadrunner/v2/plugins/config" - "github.com/spiral/roadrunner/v2/plugins/logger" -) - -// ID 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 -} - -// 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) - } - - 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.HandlerFunc { - // Define the http.HandlerFunc - return 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) - } - } - - if !s.handleStatic(w, r) { - next.ServeHTTP(w, r) - } - } -} - -func (s *Plugin) 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 func() { - err = f.Close() - if err != nil { - s.log.Error("file closing error", "error", err) - } - }() - - 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 -} |