summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorValery Piashchynski <[email protected]>2021-04-28 16:43:59 +0300
committerValery Piashchynski <[email protected]>2021-04-28 16:43:59 +0300
commitb789df7dcc9268f98f2bacfb40f753b10d521e4f (patch)
tree126b0fd8271f27e77f9107906bb0e6713a49d6ab
parent30c25f17fa7d6386e33a4894c812f7ca5db990ad (diff)
- Update CHANGELOG
- Add tests for the etags functionality Signed-off-by: Valery Piashchynski <[email protected]>
-rw-r--r--CHANGELOG.md18
-rwxr-xr-xpkg/worker_watcher/worker_watcher.go4
-rw-r--r--plugins/http/config/static.go6
-rw-r--r--plugins/http/plugin.go246
-rw-r--r--plugins/http/serve.go242
-rw-r--r--plugins/http/static/etag.go71
-rw-r--r--tests/plugins/http/configs/.rr-http-static-etags.yaml35
-rw-r--r--tests/plugins/http/configs/.rr-http-static.yaml6
-rw-r--r--tests/plugins/http/http_plugin_test.go98
9 files changed, 490 insertions, 236 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 47e2a940..ca0758b1 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.0 (27.04.2021)
-------------------
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/http/config/static.go b/plugins/http/config/static.go
index e9acc3e4..4b7b3a9b 100644
--- a/plugins/http/config/static.go
+++ b/plugins/http/config/static.go
@@ -17,6 +17,12 @@ type Static struct {
// 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
diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go
index 33efaf37..58336c17 100644
--- a/plugins/http/plugin.go
+++ b/plugins/http/plugin.go
@@ -2,15 +2,11 @@ package http
import (
"context"
- "crypto/tls"
- "crypto/x509"
"fmt"
- "io/ioutil"
"log"
"net/http"
- "net/http/fcgi"
- "net/url"
- "strings"
+ "os"
+ "path/filepath"
"sync"
"github.com/hashicorp/go-multierror"
@@ -27,10 +23,8 @@ import (
"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 (
@@ -190,10 +184,22 @@ func (s *Plugin) serve(errCh chan error) { //nolint:gocognit
}
}
+ // calculate etag for the resource
+ if s.cfg.Static.CalculateEtag {
+ 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() {
@@ -241,78 +247,6 @@ func (s *Plugin) serve(errCh chan error) { //nolint:gocognit
}()
}
-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
- }
-}
-
// Stop stops the http.
func (s *Plugin) Stop() error {
s.Lock()
@@ -498,155 +432,3 @@ func (s *Plugin) Ready() status.Status {
Code: http.StatusServiceUnavailable,
}
}
-
-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 := 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) {
- 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/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/tests/plugins/http/configs/.rr-http-static-etags.yaml b/tests/plugins/http/configs/.rr-http-static-etags.yaml
new file mode 100644
index 00000000..e18c50dd
--- /dev/null
+++ b/tests/plugins/http/configs/.rr-http-static-etags.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: true
+ 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
index 9351f020..bbec13f9 100644
--- a/tests/plugins/http/configs/.rr-http-static.yaml
+++ b/tests/plugins/http/configs/.rr-http-static.yaml
@@ -1,5 +1,5 @@
server:
- command: "php ../../psr-worker-bench.php"
+ command: "php ../../http/client.php pid pipes"
user: ""
group: ""
env:
@@ -19,12 +19,14 @@ http:
pattern: "/tests/"
forbid: [ "" ]
allow: [ ".txt", ".php" ]
+ calculate_etag: true
+ weak: false
request:
"input": "custom-header"
response:
"output": "output-header"
pool:
- num_workers: 12
+ num_workers: 2
max_jobs: 0
allocate_timeout: 60s
destroy_timeout: 60s
diff --git a/tests/plugins/http/http_plugin_test.go b/tests/plugins/http/http_plugin_test.go
index 61486eef..f48194c9 100644
--- a/tests/plugins/http/http_plugin_test.go
+++ b/tests/plugins/http/http_plugin_test.go
@@ -11,6 +11,7 @@ import (
"net/http"
"net/http/httptest"
"net/rpc"
+ "net/url"
"os"
"os/signal"
"sync"
@@ -1562,6 +1563,103 @@ func bigEchoHTTP(t *testing.T) {
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 TestStaticPlugin(t *testing.T) {
cont, err := endure.NewContainer(nil, endure.SetLogLevel(endure.ErrorLevel))
assert.NoError(t, err)