diff options
40 files changed, 1329 insertions, 988 deletions
diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 8cc61f09..bb7d646b 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,7 +11,7 @@ jobs: fail-fast: true matrix: php: [ "7.4", "8.0" ] - go: [ "1.15", "1.16" ] + go: [ "1.16" ] os: [ ubuntu-20.04 ] steps: - name: Set up Go ${{ matrix.go }} @@ -76,7 +76,6 @@ jobs: go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/redis.txt -covermode=atomic ./tests/plugins/redis go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/resetter.txt -covermode=atomic ./tests/plugins/resetter go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/rpc.txt -covermode=atomic ./tests/plugins/rpc - go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/static.txt -covermode=atomic ./tests/plugins/static go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/kv_plugin.txt -covermode=atomic ./tests/plugins/kv docker-compose -f ./tests/docker-compose.yaml down cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index f6a2a359..f9291f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,24 @@ CHANGELOG ========= +v2.2.0 (11.05.2021) +------------------- + +## 👀 New: + +- ✏️ Reworked `static` plugin. Now, it does not affect the performance of the main route and persist on the separate + file server (within the `http` plugin). Looong awaited feature: `Etag` (+ weak Etags) as well with the `If-Mach` + , `If-None-Match`, `If-Range`, `Last-Modified` + and `If-Modified-Since` tags supported. Static plugin has a bunch of new options such as: `allow`, `calculate_etag` + , `weak` and `pattern`. + ### Option `always` was deleted from the plugin. + +- ✏️ + +## 🩹 Fixes: + +- 🐛 Fix: issue with wrong ordered middlewares (reverse). Now the order is correct. + v2.1.1 (29.04.2021) ------------------- @@ -11,6 +29,7 @@ v2.1.1 (29.04.2021) v2.1.0 (27.04.2021) ------------------- + ## 👀 New: - ✏️ New `service` plugin. Docs: [link](https://roadrunner.dev/docs/beep-beep-service) @@ -47,7 +47,6 @@ test_coverage: go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/redis.out -covermode=atomic ./tests/plugins/redis go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/resetter.out -covermode=atomic ./tests/plugins/resetter go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/rpc.out -covermode=atomic ./tests/plugins/rpc - go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/static.out -covermode=atomic ./tests/plugins/static go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/boltdb.out -covermode=atomic ./tests/plugins/kv/boltdb go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/memory.out -covermode=atomic ./tests/plugins/kv/memory go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage/memcached.out -covermode=atomic ./tests/plugins/kv/memcached @@ -77,6 +76,5 @@ test: ## Run application tests go test -v -race -tags=debug ./tests/plugins/redis go test -v -race -tags=debug ./tests/plugins/resetter go test -v -race -tags=debug ./tests/plugins/rpc - go test -v -race -tags=debug ./tests/plugins/static go test -v -race -tags=debug ./tests/plugins/kv docker-compose -f tests/docker-compose.yaml down @@ -14,7 +14,7 @@ require ( github.com/golang/mock v1.4.4 github.com/google/flatbuffers v1.12.0 // indirect github.com/hashicorp/go-multierror v1.1.1 - github.com/json-iterator/go v1.1.10 + github.com/json-iterator/go v1.1.11 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.10.0 github.com/shirou/gopsutil v3.21.3+incompatible @@ -331,6 +331,8 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= diff --git a/pkg/worker_watcher/worker_watcher.go b/pkg/worker_watcher/worker_watcher.go index a6dfe43e..5aec4ee6 100755 --- a/pkg/worker_watcher/worker_watcher.go +++ b/pkg/worker_watcher/worker_watcher.go @@ -153,7 +153,7 @@ func (ww *workerWatcher) Allocate() error { return nil } -// Remove +// Remove worker func (ww *workerWatcher) Remove(wb worker.BaseProcess) { ww.Lock() defer ww.Unlock() @@ -172,7 +172,7 @@ func (ww *workerWatcher) Remove(wb worker.BaseProcess) { } } -// O(1) operation +// Push O(1) operation func (ww *workerWatcher) Push(w worker.BaseProcess) { if w.State().Value() != worker.StateReady { _ = w.Kill() 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 -} diff --git a/tests/plugins/static/configs/.rr-http-static-disabled.yaml b/tests/plugins/http/configs/.rr-http-static-disabled.yaml index a85bc408..d248ce48 100644 --- a/tests/plugins/static/configs/.rr-http-static-disabled.yaml +++ b/tests/plugins/http/configs/.rr-http-static-disabled.yaml @@ -17,10 +17,6 @@ http: static: dir: "abc" #not exists forbid: [ ".php", ".htaccess" ] - request: - Example-Request-Header: "Value" - response: - X-Powered-By: "RoadRunner" pool: num_workers: 2 max_jobs: 0 @@ -28,4 +24,4 @@ http: destroy_timeout: 60s logs: mode: development - level: error
\ No newline at end of file + level: error diff --git a/tests/plugins/static/configs/.rr-http-static.yaml b/tests/plugins/http/configs/.rr-http-static-etags.yaml index e5af9043..e18c50dd 100644 --- a/tests/plugins/static/configs/.rr-http-static.yaml +++ b/tests/plugins/http/configs/.rr-http-static-etags.yaml @@ -10,13 +10,17 @@ server: http: address: 127.0.0.1:21603 max_request_size: 1024 - middleware: [ "gzip", "static" ] + middleware: [ "gzip" ] trusted_subnets: [ "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" ] uploads: forbid: [ ".php", ".exe", ".bat" ] static: - dir: "../../../tests" + dir: "../../../" + pattern: "/tests/" forbid: [ "" ] + allow: [ ".txt", ".php" ] + calculate_etag: true + weak: true request: "input": "custom-header" response: @@ -28,4 +32,4 @@ http: destroy_timeout: 60s logs: mode: development - level: error
\ No newline at end of file + level: error diff --git a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml b/tests/plugins/http/configs/.rr-http-static-files-disable.yaml index 6ba47c91..9f91d75b 100644 --- a/tests/plugins/static/configs/.rr-http-static-files-disable.yaml +++ b/tests/plugins/http/configs/.rr-http-static-files-disable.yaml @@ -14,14 +14,6 @@ http: trusted_subnets: [ "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" ] uploads: forbid: [ ".php", ".exe", ".bat" ] - static: - dir: "../../../tests" - forbid: [ ".php" ] - request: - Example-Request-Header: "Value" - # Automatically add headers to every response. - response: - X-Powered-By: "RoadRunner" pool: num_workers: 2 max_jobs: 0 @@ -29,4 +21,4 @@ http: destroy_timeout: 60s logs: mode: development - level: error
\ No newline at end of file + level: error diff --git a/tests/plugins/static/configs/.rr-http-static-files.yaml b/tests/plugins/http/configs/.rr-http-static-files.yaml index d6b3032e..5d8b50e8 100644 --- a/tests/plugins/static/configs/.rr-http-static-files.yaml +++ b/tests/plugins/http/configs/.rr-http-static-files.yaml @@ -10,19 +10,16 @@ server: http: address: 127.0.0.1:34653 max_request_size: 1024 - middleware: [ "gzip", "static" ] + middleware: [ "gzip" ] trusted_subnets: [ "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" ] uploads: forbid: [ ".php", ".exe", ".bat" ] static: - dir: "../../../tests" + dir: "../../../" + pattern: "/tests/" + allow: [ ".ico" ] forbid: [ ".php", ".htaccess" ] - always: [ ".ico" ] - request: - "Example-Request-Header": "Value" - # Automatically add headers to every response. - response: - "X-Powered-By": "RoadRunner" + pool: num_workers: 2 max_jobs: 0 @@ -30,4 +27,4 @@ http: destroy_timeout: 60s logs: mode: development - level: error
\ No newline at end of file + level: error diff --git a/tests/plugins/http/configs/.rr-http-static-security.yaml b/tests/plugins/http/configs/.rr-http-static-security.yaml new file mode 100644 index 00000000..bbec13f9 --- /dev/null +++ b/tests/plugins/http/configs/.rr-http-static-security.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relay_timeout: "20s" + +http: + address: 127.0.0.1:21603 + max_request_size: 1024 + middleware: [ "gzip" ] + trusted_subnets: [ "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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../" + pattern: "/tests/" + forbid: [ "" ] + allow: [ ".txt", ".php" ] + calculate_etag: true + weak: false + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + num_workers: 2 + max_jobs: 0 + allocate_timeout: 60s + destroy_timeout: 60s +logs: + mode: development + level: error diff --git a/tests/plugins/http/configs/.rr-http-static.yaml b/tests/plugins/http/configs/.rr-http-static.yaml new file mode 100644 index 00000000..bbec13f9 --- /dev/null +++ b/tests/plugins/http/configs/.rr-http-static.yaml @@ -0,0 +1,35 @@ +server: + command: "php ../../http/client.php pid pipes" + user: "" + group: "" + env: + "RR_HTTP": "true" + relay: "pipes" + relay_timeout: "20s" + +http: + address: 127.0.0.1:21603 + max_request_size: 1024 + middleware: [ "gzip" ] + trusted_subnets: [ "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" ] + uploads: + forbid: [ ".php", ".exe", ".bat" ] + static: + dir: "../../../" + pattern: "/tests/" + forbid: [ "" ] + allow: [ ".txt", ".php" ] + calculate_etag: true + weak: false + request: + "input": "custom-header" + response: + "output": "output-header" + pool: + num_workers: 2 + max_jobs: 0 + allocate_timeout: 60s + destroy_timeout: 60s +logs: + mode: development + level: error diff --git a/tests/plugins/http/handler_test.go b/tests/plugins/http/handler_test.go index cf445aad..575fe656 100644 --- a/tests/plugins/http/handler_test.go +++ b/tests/plugins/http/handler_test.go @@ -12,8 +12,8 @@ import ( "github.com/spiral/roadrunner/v2/pkg/pool" "github.com/spiral/roadrunner/v2/pkg/transport/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/spiral/roadrunner/v2/plugins/http/config" + handler "github.com/spiral/roadrunner/v2/plugins/http/worker_handler" "github.com/stretchr/testify/assert" "net/http" @@ -35,7 +35,7 @@ func TestHandler_Echo(t *testing.T) { t.Fatal(err) } - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -66,7 +66,7 @@ func TestHandler_Echo(t *testing.T) { } func Test_HandlerErrors(t *testing.T) { - _, err := httpPlugin.NewHandler(1024, config.Uploads{ + _, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, nil) @@ -89,7 +89,7 @@ func TestHandler_Headers(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -150,7 +150,7 @@ func TestHandler_Empty_User_Agent(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -210,7 +210,7 @@ func TestHandler_User_Agent(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -270,7 +270,7 @@ func TestHandler_Cookies(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -335,7 +335,7 @@ func TestHandler_JsonPayload_POST(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -399,7 +399,7 @@ func TestHandler_JsonPayload_PUT(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -459,7 +459,7 @@ func TestHandler_JsonPayload_PATCH(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -519,7 +519,7 @@ func TestHandler_FormData_POST(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -592,7 +592,7 @@ func TestHandler_FormData_POST_Overwrite(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -665,7 +665,7 @@ func TestHandler_FormData_POST_Form_UrlEncoded_Charset(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -737,7 +737,7 @@ func TestHandler_FormData_PUT(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -809,7 +809,7 @@ func TestHandler_FormData_PATCH(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -881,7 +881,7 @@ func TestHandler_Multipart_POST(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -995,7 +995,7 @@ func TestHandler_Multipart_PUT(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1109,7 +1109,7 @@ func TestHandler_Multipart_PATCH(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1225,7 +1225,7 @@ func TestHandler_Error(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1271,7 +1271,7 @@ func TestHandler_Error2(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1317,7 +1317,7 @@ func TestHandler_Error3(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1, config.Uploads{ + h, err := handler.NewHandler(1, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1376,7 +1376,7 @@ func TestHandler_ResponseDuration(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1401,7 +1401,7 @@ func TestHandler_ResponseDuration(t *testing.T) { gotresp := make(chan interface{}) h.AddListener(func(event interface{}) { switch t := event.(type) { - case httpPlugin.ResponseEvent: + case handler.ResponseEvent: if t.Elapsed() > 0 { close(gotresp) } @@ -1437,7 +1437,7 @@ func TestHandler_ResponseDurationDelayed(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1462,7 +1462,7 @@ func TestHandler_ResponseDurationDelayed(t *testing.T) { gotresp := make(chan interface{}) h.AddListener(func(event interface{}) { switch tp := event.(type) { - case httpPlugin.ResponseEvent: + case handler.ResponseEvent: if tp.Elapsed() > time.Second { close(gotresp) } @@ -1497,7 +1497,7 @@ func TestHandler_ErrorDuration(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) @@ -1522,7 +1522,7 @@ func TestHandler_ErrorDuration(t *testing.T) { goterr := make(chan interface{}) h.AddListener(func(event interface{}) { switch tp := event.(type) { - case httpPlugin.ErrorEvent: + case handler.ErrorEvent: if tp.Elapsed() > 0 { close(goterr) } @@ -1571,7 +1571,7 @@ func TestHandler_IP(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, cidrs, p) @@ -1632,7 +1632,7 @@ func TestHandler_XRealIP(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, cidrs, p) @@ -1698,7 +1698,7 @@ func TestHandler_XForwardedFor(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, cidrs, p) @@ -1763,7 +1763,7 @@ func TestHandler_XForwardedFor_NotTrustedRemoteIp(t *testing.T) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, cidrs, p) @@ -1811,7 +1811,7 @@ func BenchmarkHandler_Listen_Echo(b *testing.B) { p.Destroy(context.Background()) }() - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, p) diff --git a/tests/plugins/http/http_plugin_test.go b/tests/plugins/http/http_plugin_test.go index 0e43dac4..8f76e3ba 100644 --- a/tests/plugins/http/http_plugin_test.go +++ b/tests/plugins/http/http_plugin_test.go @@ -5,11 +5,13 @@ import ( "crypto/rand" "crypto/tls" "fmt" + "io" "io/ioutil" "net" "net/http" "net/http/httptest" "net/rpc" + "net/url" "os" "os/signal" "sync" @@ -23,6 +25,7 @@ import ( "github.com/spiral/roadrunner/v2/pkg/events" "github.com/spiral/roadrunner/v2/pkg/process" "github.com/spiral/roadrunner/v2/plugins/config" + "github.com/spiral/roadrunner/v2/plugins/gzip" "github.com/spiral/roadrunner/v2/plugins/informer" "github.com/spiral/roadrunner/v2/plugins/logger" "github.com/spiral/roadrunner/v2/plugins/resetter" @@ -1397,21 +1400,6 @@ func informerTestAfter(t *testing.T) { assert.NotEqual(t, workerPid, list.Workers[0].Pid) } -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) //nolint:gosec - if err != nil { - return "", nil, err - } - b, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", nil, err - } - defer func() { - _ = r.Body.Close() - }() - 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)) @@ -1574,3 +1562,605 @@ func bigEchoHTTP(t *testing.T) { err = r.Body.Close() assert.NoError(t, err) } + +func TestStaticEtagPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSampleEtag", serveStaticSampleEtag) + + stopCh <- struct{}{} + wg.Wait() +} + +func serveStaticSampleEtag(t *testing.T) { + // OK 200 response + b, r, err := get("http://localhost:21603/tests/static/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample\n", b) + assert.Equal(t, r.StatusCode, http.StatusOK) + etag := r.Header.Get("Etag") + + _ = r.Body.Close() + + // Should be 304 response with same etag + c := http.Client{ + Timeout: time.Second * 5, + } + + parsedURL, _ := url.Parse("http://localhost:21603/tests/static/sample.txt") + + req := &http.Request{ + Method: http.MethodGet, + URL: parsedURL, + Header: map[string][]string{"If-None-Match": {etag}}, + } + + resp, err := c.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusNotModified, resp.StatusCode) + _ = resp.Body.Close() +} + +func TestStaticPluginSecurity(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-security.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSampleNotAllowedPath", serveStaticSampleNotAllowedPath) + + stopCh <- struct{}{} + wg.Wait() +} + +func serveStaticSampleNotAllowedPath(t *testing.T) { + // Should be 304 response with same etag + c := http.Client{ + Timeout: time.Second * 5, + } + + parsedURL := &url.URL{ + Scheme: "http", + User: nil, + Host: "localhost:21603", + Path: "%2e%2e%/tests/", + } + + req := &http.Request{ + Method: http.MethodGet, + URL: parsedURL, + } + + resp, err := c.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + _ = resp.Body.Close() + + parsedURL = &url.URL{ + Scheme: "http", + User: nil, + Host: "localhost:21603", + Path: "%2e%2e%5ctests/", + } + + req = &http.Request{ + Method: http.MethodGet, + URL: parsedURL, + } + + resp, err = c.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + _ = resp.Body.Close() + + parsedURL = &url.URL{ + Scheme: "http", + User: nil, + Host: "localhost:21603", + Path: "..%2ftests/", + } + + req = &http.Request{ + Method: http.MethodGet, + URL: parsedURL, + } + + resp, err = c.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + _ = resp.Body.Close() + + parsedURL = &url.URL{ + Scheme: "http", + User: nil, + Host: "localhost:21603", + Path: "%2e%2e%2ftests/", + } + + req = &http.Request{ + Method: http.MethodGet, + URL: parsedURL, + } + + resp, err = c.Do(req) + assert.Nil(t, err) + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) + _ = resp.Body.Close() + + _, r, err := get("http://localhost:21603/../../../../tests/../static/sample.txt") + assert.NoError(t, err) + assert.Equal(t, r.StatusCode, 200) + _ = r.Body.Close() +} + +func TestStaticPlugin(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("ServeSample", serveStaticSample) + t.Run("StaticNotForbid", staticNotForbid) + t.Run("StaticHeaders", staticHeaders) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticHeaders(t *testing.T) { + req, err := http.NewRequest("GET", "http://localhost:21603/tests/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) + } + + defer func() { + _ = resp.Body.Close() + }() + + assert.Equal(t, all("../../../tests/client.php"), string(b)) + assert.Equal(t, all("../../../tests/client.php"), string(b)) +} + +func staticNotForbid(t *testing.T) { + b, r, err := get("http://localhost:21603/tests/client.php") + assert.NoError(t, err) + assert.Equal(t, all("../../../tests/client.php"), b) + assert.Equal(t, all("../../../tests/client.php"), b) + _ = r.Body.Close() +} + +func serveStaticSample(t *testing.T) { + b, r, err := get("http://localhost:21603/tests/static/sample.txt") + assert.NoError(t, err) + assert.Equal(t, "sample\n", b) + _ = r.Body.Close() +} + +func TestStaticDisabled_Error(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-disabled.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + assert.Error(t, cont.Init()) +} + +func TestStaticFilesDisabled(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files-disable.yaml", + Prefix: "rr", + } + + err = cont.RegisterAll( + cfg, + &logger.ZapLogger{}, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticFilesDisabled", staticFilesDisabled) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticFilesDisabled(t *testing.T) { + b, r, err := get("http://localhost:45877/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "WORLD", b) + _ = r.Body.Close() +} + +func TestStaticFilesForbid(t *testing.T) { + cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) + assert.NoError(t, err) + + cfg := &config.Viper{ + Path: "configs/.rr-http-static-files.yaml", + Prefix: "rr", + } + + controller := gomock.NewController(t) + mockLogger := mocks.NewMockLogger(controller) + + mockLogger.EXPECT().Debug("worker destructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("worker constructed", "pid", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Debug("201 GET http://localhost:34653/tests/http?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("201 GET http://localhost:34653/tests/client.XXX?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Debug("201 GET http://localhost:34653/tests/client.php?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) + mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() + mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror + + err = cont.RegisterAll( + cfg, + mockLogger, + &server.Plugin{}, + &httpPlugin.Plugin{}, + &gzip.Plugin{}, + ) + assert.NoError(t, err) + + err = cont.Init() + if err != nil { + t.Fatal(err) + } + + ch, err := cont.Serve() + assert.NoError(t, err) + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + wg := &sync.WaitGroup{} + wg.Add(1) + + stopCh := make(chan struct{}, 1) + + go func() { + defer wg.Done() + for { + select { + case e := <-ch: + assert.Fail(t, "error", e.Error.Error()) + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + case <-sig: + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + case <-stopCh: + // timeout + err = cont.Stop() + if err != nil { + assert.FailNow(t, "error", err.Error()) + } + return + } + } + }() + + time.Sleep(time.Second) + t.Run("StaticTestFilesDir", staticTestFilesDir) + t.Run("StaticNotFound", staticNotFound) + t.Run("StaticFilesForbid", staticFilesForbid) + t.Run("StaticFilesAlways", staticFilesAlways) + + stopCh <- struct{}{} + wg.Wait() +} + +func staticTestFilesDir(t *testing.T) { + b, r, err := get("http://localhost:34653/tests/http?hello=world") + assert.NoError(t, err) + assert.Equal(t, "403 Forbidden\n", b) + _ = r.Body.Close() +} + +func staticNotFound(t *testing.T) { + b, _, _ := get("http://localhost:34653/tests/client.XXX?hello=world") //nolint:bodyclose + assert.Equal(t, "404 page not found\n", b) +} + +func staticFilesAlways(t *testing.T) { + _, r, err := get("http://localhost:34653/tests/favicon.ico") + assert.NoError(t, err) + assert.Equal(t, 404, r.StatusCode) + _ = r.Body.Close() +} + +func staticFilesForbid(t *testing.T) { + b, r, err := get("http://localhost:34653/tests/client.php?hello=world") + if err != nil { + t.Fatal(err) + } + assert.Equal(t, "403 Forbidden\n", b) + _ = r.Body.Close() +} + +// HELPERS +func get(url string) (string, *http.Response, error) { + r, err := http.Get(url) //nolint:gosec + 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 all(fn string) string { + f, _ := os.Open(fn) + + b := new(bytes.Buffer) + _, err := io.Copy(b, f) + if err != nil { + return "" + } + + err = f.Close() + if err != nil { + return "" + } + + return b.String() +} diff --git a/tests/plugins/http/parse_test.go b/tests/plugins/http/parse_test.go index 5cc1ce32..15c82839 100644 --- a/tests/plugins/http/parse_test.go +++ b/tests/plugins/http/parse_test.go @@ -3,7 +3,7 @@ package http import ( "testing" - "github.com/spiral/roadrunner/v2/plugins/http" + handler "github.com/spiral/roadrunner/v2/plugins/http/worker_handler" ) var samples = []struct { @@ -21,7 +21,7 @@ var samples = []struct { func Test_FetchIndexes(t *testing.T) { for i := 0; i < len(samples); i++ { - r := http.FetchIndexes(samples[i].in) + r := handler.FetchIndexes(samples[i].in) if !same(r, samples[i].out) { t.Errorf("got %q, want %q", r, samples[i].out) } @@ -31,7 +31,7 @@ func Test_FetchIndexes(t *testing.T) { func BenchmarkConfig_FetchIndexes(b *testing.B) { for _, tt := range samples { for n := 0; n < b.N; n++ { - r := http.FetchIndexes(tt.in) + r := handler.FetchIndexes(tt.in) if !same(r, tt.out) { b.Fail() } diff --git a/tests/plugins/http/plugin_middleware.go b/tests/plugins/http/plugin_middleware.go index 00640b69..9f04d6db 100644 --- a/tests/plugins/http/plugin_middleware.go +++ b/tests/plugins/http/plugin_middleware.go @@ -18,8 +18,8 @@ func (p *PluginMiddleware) Init(cfg config.Configurer) error { } // Middleware test -func (p *PluginMiddleware) Middleware(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (p *PluginMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/halt" { w.WriteHeader(500) _, err := w.Write([]byte("halted")) @@ -29,7 +29,7 @@ func (p *PluginMiddleware) Middleware(next http.Handler) http.HandlerFunc { } else { next.ServeHTTP(w, r) } - } + }) } // Name test @@ -49,8 +49,8 @@ func (p *PluginMiddleware2) Init(cfg config.Configurer) error { } // Middleware test -func (p *PluginMiddleware2) Middleware(next http.Handler) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { +func (p *PluginMiddleware2) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/boom" { w.WriteHeader(555) _, err := w.Write([]byte("boom")) @@ -60,7 +60,7 @@ func (p *PluginMiddleware2) Middleware(next http.Handler) http.HandlerFunc { } else { next.ServeHTTP(w, r) } - } + }) } // Name test diff --git a/tests/plugins/http/response_test.go b/tests/plugins/http/response_test.go index dc9856ac..3564d9cd 100644 --- a/tests/plugins/http/response_test.go +++ b/tests/plugins/http/response_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/spiral/roadrunner/v2/pkg/payload" - httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" + handler "github.com/spiral/roadrunner/v2/plugins/http/worker_handler" "github.com/stretchr/testify/assert" ) @@ -45,13 +45,13 @@ func (tw *testWriter) Push(target string, opts *http.PushOptions) error { } func TestNewResponse_Error(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{Context: []byte(`invalid payload`)}) + r, err := handler.NewResponse(payload.Payload{Context: []byte(`invalid payload`)}) assert.Error(t, err) assert.Nil(t, r) } func TestNewResponse_Write(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), Body: []byte(`sample body`), }) @@ -68,7 +68,7 @@ func TestNewResponse_Write(t *testing.T) { } func TestNewResponse_Stream(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), }) @@ -93,7 +93,7 @@ func TestNewResponse_Stream(t *testing.T) { } func TestNewResponse_StreamError(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte(`{"headers":{"key":["value"]},"status": 301}`), }) @@ -114,7 +114,7 @@ func TestNewResponse_StreamError(t *testing.T) { } func TestWrite_HandlesPush(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte(`{"headers":{"Http2-Push":["/test.js"],"content-type":["text/html"]},"status": 200}`), }) @@ -129,7 +129,7 @@ func TestWrite_HandlesPush(t *testing.T) { } func TestWrite_HandlesTrailers(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte(`{"headers":{"Trailer":["foo, bar", "baz"],"foo":["test"],"bar":["demo"]},"status": 200}`), }) @@ -139,7 +139,7 @@ func TestWrite_HandlesTrailers(t *testing.T) { w := &testWriter{h: http.Header(make(map[string][]string))} assert.NoError(t, r.Write(w)) - assert.Nil(t, w.h[httpPlugin.TrailerHeaderKey]) + assert.Nil(t, w.h[handler.TrailerHeaderKey]) assert.Nil(t, w.h["foo"]) //nolint:staticcheck assert.Nil(t, w.h["baz"]) //nolint:staticcheck @@ -148,7 +148,7 @@ func TestWrite_HandlesTrailers(t *testing.T) { } func TestWrite_HandlesHandlesWhitespacesInTrailer(t *testing.T) { - r, err := httpPlugin.NewResponse(payload.Payload{ + r, err := handler.NewResponse(payload.Payload{ Context: []byte( `{"headers":{"Trailer":["foo\t,bar , baz"],"foo":["a"],"bar":["b"],"baz":["c"]},"status": 200}`), }) diff --git a/tests/plugins/http/uploads_test.go b/tests/plugins/http/uploads_test.go index bc7e17df..5c39589c 100644 --- a/tests/plugins/http/uploads_test.go +++ b/tests/plugins/http/uploads_test.go @@ -18,8 +18,8 @@ import ( j "github.com/json-iterator/go" poolImpl "github.com/spiral/roadrunner/v2/pkg/pool" "github.com/spiral/roadrunner/v2/pkg/transport/pipe" - httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" "github.com/spiral/roadrunner/v2/plugins/http/config" + handler "github.com/spiral/roadrunner/v2/plugins/http/worker_handler" "github.com/stretchr/testify/assert" ) @@ -40,7 +40,7 @@ func TestHandler_Upload_File(t *testing.T) { t.Fatal(err) } - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, pool) @@ -123,7 +123,7 @@ func TestHandler_Upload_NestedFile(t *testing.T) { t.Fatal(err) } - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{}, }, nil, pool) @@ -206,7 +206,7 @@ func TestHandler_Upload_File_NoTmpDir(t *testing.T) { t.Fatal(err) } - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: "-------", Forbid: []string{}, }, nil, pool) @@ -289,7 +289,7 @@ func TestHandler_Upload_File_Forbids(t *testing.T) { t.Fatal(err) } - h, err := httpPlugin.NewHandler(1024, config.Uploads{ + h, err := handler.NewHandler(1024, config.Uploads{ Dir: os.TempDir(), Forbid: []string{".go"}, }, nil, pool) diff --git a/tests/plugins/static/config_test.go b/tests/plugins/static/config_test.go deleted file mode 100644 index d73fd845..00000000 --- a/tests/plugins/static/config_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package static - -import ( - "testing" - - "github.com/spiral/roadrunner/v2/plugins/static" - "github.com/stretchr/testify/assert" -) - -func TestConfig_Forbids(t *testing.T) { - cfg := static.Config{Static: &struct { - Dir string - Forbid []string - Always []string - Request map[string]string - Response map[string]string - }{Dir: "", Forbid: []string{".php"}, Always: nil, Request: nil, Response: nil}} - - 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, (&static.Config{Static: &struct { - Dir string - Forbid []string - Always []string - Request map[string]string - Response map[string]string - }{Dir: "./"}}).Valid()) - - assert.Error(t, (&static.Config{Static: &struct { - Dir string - Forbid []string - Always []string - Request map[string]string - Response map[string]string - }{Dir: "./http.go"}}).Valid()) - - assert.Error(t, (&static.Config{Static: &struct { - Dir string - Forbid []string - Always []string - Request map[string]string - Response map[string]string - }{Dir: "./dir/"}}).Valid()) -} diff --git a/tests/plugins/static/static_plugin_test.go b/tests/plugins/static/static_plugin_test.go deleted file mode 100644 index 38562537..00000000 --- a/tests/plugins/static/static_plugin_test.go +++ /dev/null @@ -1,386 +0,0 @@ -package static - -import ( - "bytes" - "io" - "io/ioutil" - "net/http" - "os" - "os/signal" - "sync" - "syscall" - "testing" - "time" - - "github.com/golang/mock/gomock" - endure "github.com/spiral/endure/pkg/container" - "github.com/spiral/roadrunner/v2/plugins/config" - "github.com/spiral/roadrunner/v2/plugins/gzip" - httpPlugin "github.com/spiral/roadrunner/v2/plugins/http" - "github.com/spiral/roadrunner/v2/plugins/logger" - "github.com/spiral/roadrunner/v2/plugins/server" - "github.com/spiral/roadrunner/v2/plugins/static" - "github.com/spiral/roadrunner/v2/tests/mocks" - "github.com/stretchr/testify/assert" -) - -func TestStaticPlugin(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) - assert.NoError(t, err) - - cfg := &config.Viper{ - Path: "configs/.rr-http-static.yaml", - Prefix: "rr", - } - - err = cont.RegisterAll( - cfg, - &logger.ZapLogger{}, - &server.Plugin{}, - &httpPlugin.Plugin{}, - &gzip.Plugin{}, - &static.Plugin{}, - ) - assert.NoError(t, err) - - err = cont.Init() - if err != nil { - t.Fatal(err) - } - - ch, err := cont.Serve() - assert.NoError(t, err) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - wg := &sync.WaitGroup{} - wg.Add(1) - - stopCh := make(chan struct{}, 1) - - go func() { - defer wg.Done() - for { - select { - case e := <-ch: - assert.Fail(t, "error", e.Error.Error()) - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - case <-sig: - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - case <-stopCh: - // timeout - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - } - } - }() - - time.Sleep(time.Second) - t.Run("ServeSample", serveStaticSample) - t.Run("StaticNotForbid", staticNotForbid) - t.Run("StaticHeaders", staticHeaders) - - stopCh <- struct{}{} - wg.Wait() -} - -func staticHeaders(t *testing.T) { - req, err := http.NewRequest("GET", "http://localhost:21603/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) - } - - defer func() { - _ = resp.Body.Close() - }() - - assert.Equal(t, all("../../../tests/client.php"), string(b)) - assert.Equal(t, all("../../../tests/client.php"), string(b)) -} - -func staticNotForbid(t *testing.T) { - b, r, err := get("http://localhost:21603/client.php") - assert.NoError(t, err) - assert.Equal(t, all("../../../tests/client.php"), b) - assert.Equal(t, all("../../../tests/client.php"), b) - _ = r.Body.Close() -} - -func serveStaticSample(t *testing.T) { - b, r, err := get("http://localhost:21603/sample.txt") - assert.NoError(t, err) - assert.Equal(t, "sample", b) - _ = r.Body.Close() -} - -func TestStaticDisabled_Error(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) - assert.NoError(t, err) - - cfg := &config.Viper{ - Path: "configs/.rr-http-static-disabled.yaml", - Prefix: "rr", - } - - err = cont.RegisterAll( - cfg, - &logger.ZapLogger{}, - &server.Plugin{}, - &httpPlugin.Plugin{}, - &gzip.Plugin{}, - &static.Plugin{}, - ) - assert.NoError(t, err) - assert.Error(t, cont.Init()) -} - -func TestStaticFilesDisabled(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) - assert.NoError(t, err) - - cfg := &config.Viper{ - Path: "configs/.rr-http-static-files-disable.yaml", - Prefix: "rr", - } - - err = cont.RegisterAll( - cfg, - &logger.ZapLogger{}, - &server.Plugin{}, - &httpPlugin.Plugin{}, - &gzip.Plugin{}, - &static.Plugin{}, - ) - assert.NoError(t, err) - - err = cont.Init() - if err != nil { - t.Fatal(err) - } - - ch, err := cont.Serve() - assert.NoError(t, err) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - wg := &sync.WaitGroup{} - wg.Add(1) - - stopCh := make(chan struct{}, 1) - - go func() { - defer wg.Done() - for { - select { - case e := <-ch: - assert.Fail(t, "error", e.Error.Error()) - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - case <-sig: - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - case <-stopCh: - // timeout - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - } - } - }() - - time.Sleep(time.Second) - t.Run("StaticFilesDisabled", staticFilesDisabled) - - stopCh <- struct{}{} - wg.Wait() -} - -func staticFilesDisabled(t *testing.T) { - b, r, err := get("http://localhost:45877/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - _ = r.Body.Close() -} - -func TestStaticFilesForbid(t *testing.T) { - cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel)) - assert.NoError(t, err) - - cfg := &config.Viper{ - Path: "configs/.rr-http-static-files.yaml", - Prefix: "rr", - } - - controller := gomock.NewController(t) - mockLogger := mocks.NewMockLogger(controller) - - mockLogger.EXPECT().Debug("worker destructed", "pid", gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug("worker constructed", "pid", gomock.Any()).AnyTimes() - mockLogger.EXPECT().Debug("201 GET http://localhost:34653/http?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) - mockLogger.EXPECT().Debug("201 GET http://localhost:34653/client.XXX?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) - mockLogger.EXPECT().Debug("201 GET http://localhost:34653/client.php?hello=world", "remote", "127.0.0.1", "elapsed", gomock.Any()).MinTimes(1) - mockLogger.EXPECT().Error("file open error", "error", gomock.Any()).AnyTimes() - mockLogger.EXPECT().Error(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() // placeholder for the workerlogerror - - err = cont.RegisterAll( - cfg, - mockLogger, - &server.Plugin{}, - &httpPlugin.Plugin{}, - &gzip.Plugin{}, - &static.Plugin{}, - ) - assert.NoError(t, err) - - err = cont.Init() - if err != nil { - t.Fatal(err) - } - - ch, err := cont.Serve() - assert.NoError(t, err) - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) - - wg := &sync.WaitGroup{} - wg.Add(1) - - stopCh := make(chan struct{}, 1) - - go func() { - defer wg.Done() - for { - select { - case e := <-ch: - assert.Fail(t, "error", e.Error.Error()) - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - case <-sig: - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - case <-stopCh: - // timeout - err = cont.Stop() - if err != nil { - assert.FailNow(t, "error", err.Error()) - } - return - } - } - }() - - time.Sleep(time.Second) - t.Run("StaticTestFilesDir", staticTestFilesDir) - t.Run("StaticNotFound", staticNotFound) - t.Run("StaticFilesForbid", staticFilesForbid) - t.Run("StaticFilesAlways", staticFilesAlways) - - stopCh <- struct{}{} - wg.Wait() -} - -func staticTestFilesDir(t *testing.T) { - b, r, err := get("http://localhost:34653/http?hello=world") - assert.NoError(t, err) - assert.Equal(t, "WORLD", b) - _ = r.Body.Close() -} - -func staticNotFound(t *testing.T) { - b, _, _ := get("http://localhost:34653/client.XXX?hello=world") //nolint:bodyclose - assert.Equal(t, "WORLD", b) -} - -func staticFilesAlways(t *testing.T) { - _, r, err := get("http://localhost:34653/favicon.ico") - assert.NoError(t, err) - assert.Equal(t, 404, r.StatusCode) - _ = r.Body.Close() -} - -func staticFilesForbid(t *testing.T) { - b, r, err := get("http://localhost:34653/client.php?hello=world") - if err != nil { - t.Fatal(err) - } - assert.Equal(t, "WORLD", b) - _ = r.Body.Close() -} - -// HELPERS -func get(url string) (string, *http.Response, error) { - r, err := http.Get(url) //nolint:gosec - 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 all(fn string) string { - f, _ := os.Open(fn) - - b := new(bytes.Buffer) - _, err := io.Copy(b, f) - if err != nil { - return "" - } - - err = f.Close() - if err != nil { - return "" - } - - return b.String() -} diff --git a/tests/sample.txt b/tests/sample.txt deleted file mode 100644 index eed7e79a..00000000 --- a/tests/sample.txt +++ /dev/null @@ -1 +0,0 @@ -sample
\ No newline at end of file diff --git a/tests/static/sample.txt b/tests/static/sample.txt new file mode 100644 index 00000000..d64a3d96 --- /dev/null +++ b/tests/static/sample.txt @@ -0,0 +1 @@ +sample |