normalized builtin middlewares to return hook.Handler

This commit is contained in:
Gani Georgiev 2024-10-14 18:17:31 +03:00
parent 47d5ea3ce2
commit f9ee710cdd
3 changed files with 153 additions and 144 deletions

View File

@ -19,6 +19,7 @@ import (
"strings" "strings"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/hook"
) )
const ( const (
@ -124,7 +125,7 @@ var DefaultCORSConfig = CORSConfig{
} }
// CORSWithConfig returns a CORS middleware with config. // CORSWithConfig returns a CORS middleware with config.
func CORSWithConfig(config CORSConfig) func(e *core.RequestEvent) error { func CORSWithConfig(config CORSConfig) *hook.Handler[*core.RequestEvent] {
// Defaults // Defaults
if len(config.AllowOrigins) == 0 { if len(config.AllowOrigins) == 0 {
config.AllowOrigins = DefaultCORSConfig.AllowOrigins config.AllowOrigins = DefaultCORSConfig.AllowOrigins
@ -151,108 +152,112 @@ func CORSWithConfig(config CORSConfig) func(e *core.RequestEvent) error {
maxAge = strconv.Itoa(config.MaxAge) maxAge = strconv.Itoa(config.MaxAge)
} }
return func(e *core.RequestEvent) error { return &hook.Handler[*core.RequestEvent]{
req := e.Request Id: DefaultCorsMiddlewareId,
res := e.Response Priority: DefaultCorsMiddlewarePriority,
origin := req.Header.Get("Origin") Func: func(e *core.RequestEvent) error {
allowOrigin := "" req := e.Request
res := e.Response
origin := req.Header.Get("Origin")
allowOrigin := ""
res.Header().Add("Vary", "Origin") res.Header().Add("Vary", "Origin")
// Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method, // Preflight request is an OPTIONS request, using three HTTP request headers: Access-Control-Request-Method,
// Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request // Access-Control-Request-Headers, and the Origin header. See: https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
// For simplicity we just consider method type and later `Origin` header. // For simplicity we just consider method type and later `Origin` header.
preflight := req.Method == http.MethodOptions preflight := req.Method == http.MethodOptions
// No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain // No Origin provided. This is (probably) not request from actual browser - proceed executing middleware chain
if origin == "" { if origin == "" {
if !preflight { if !preflight {
return e.Next() return e.Next()
}
return e.NoContent(http.StatusNoContent)
} }
return e.NoContent(http.StatusNoContent)
}
if config.AllowOriginFunc != nil { if config.AllowOriginFunc != nil {
allowed, err := config.AllowOriginFunc(origin) allowed, err := config.AllowOriginFunc(origin)
if err != nil { if err != nil {
return err return err
} }
if allowed { if allowed {
allowOrigin = origin
}
} else {
// Check allowed origins
for _, o := range config.AllowOrigins {
if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials {
allowOrigin = origin allowOrigin = origin
break
} }
if o == "*" || o == origin { } else {
allowOrigin = o // Check allowed origins
break for _, o := range config.AllowOrigins {
} if o == "*" && config.AllowCredentials && config.UnsafeWildcardOriginWithAllowCredentials {
if matchSubdomain(origin, o) { allowOrigin = origin
allowOrigin = origin break
break }
} if o == "*" || o == origin {
} allowOrigin = o
break
checkPatterns := false }
if allowOrigin == "" { if matchSubdomain(origin, o) {
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
if len(origin) <= (253+3+5) && strings.Contains(origin, "://") {
checkPatterns = true
}
}
if checkPatterns {
for _, re := range allowOriginPatterns {
if match, _ := regexp.MatchString(re, origin); match {
allowOrigin = origin allowOrigin = origin
break break
} }
} }
}
}
// Origin not allowed checkPatterns := false
if allowOrigin == "" { if allowOrigin == "" {
// to avoid regex cost by invalid (long) domains (253 is domain name max limit)
if len(origin) <= (253+3+5) && strings.Contains(origin, "://") {
checkPatterns = true
}
}
if checkPatterns {
for _, re := range allowOriginPatterns {
if match, _ := regexp.MatchString(re, origin); match {
allowOrigin = origin
break
}
}
}
}
// Origin not allowed
if allowOrigin == "" {
if !preflight {
return e.Next()
}
return e.NoContent(http.StatusNoContent)
}
res.Header().Set("Access-Control-Allow-Origin", allowOrigin)
if config.AllowCredentials {
res.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Simple request
if !preflight { if !preflight {
if exposeHeaders != "" {
res.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
}
return e.Next() return e.Next()
} }
// Preflight request
res.Header().Add("Vary", "Access-Control-Request-Method")
res.Header().Add("Vary", "Access-Control-Request-Headers")
res.Header().Set("Access-Control-Allow-Methods", allowMethods)
if allowHeaders != "" {
res.Header().Set("Access-Control-Allow-Headers", allowHeaders)
} else {
h := req.Header.Get("Access-Control-Request-Headers")
if h != "" {
res.Header().Set("Access-Control-Allow-Headers", h)
}
}
if config.MaxAge != 0 {
res.Header().Set("Access-Control-Max-Age", maxAge)
}
return e.NoContent(http.StatusNoContent) return e.NoContent(http.StatusNoContent)
} },
res.Header().Set("Access-Control-Allow-Origin", allowOrigin)
if config.AllowCredentials {
res.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Simple request
if !preflight {
if exposeHeaders != "" {
res.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
}
return e.Next()
}
// Preflight request
res.Header().Add("Vary", "Access-Control-Request-Method")
res.Header().Add("Vary", "Access-Control-Request-Headers")
res.Header().Set("Access-Control-Allow-Methods", allowMethods)
if allowHeaders != "" {
res.Header().Set("Access-Control-Allow-Headers", allowHeaders)
} else {
h := req.Header.Get("Access-Control-Request-Headers")
if h != "" {
res.Header().Set("Access-Control-Allow-Headers", h)
}
}
if config.MaxAge != 0 {
res.Header().Set("Access-Control-Max-Age", maxAge)
}
return e.NoContent(http.StatusNoContent)
} }
} }

View File

@ -18,6 +18,7 @@ import (
"sync" "sync"
"github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/hook"
"github.com/pocketbase/pocketbase/tools/router" "github.com/pocketbase/pocketbase/tools/router"
) )
@ -25,6 +26,10 @@ const (
gzipScheme = "gzip" gzipScheme = "gzip"
) )
const (
DefaultGzipMiddlewareId = "pbGzip"
)
// GzipConfig defines the config for Gzip middleware. // GzipConfig defines the config for Gzip middleware.
type GzipConfig struct { type GzipConfig struct {
// Gzip compression level. // Gzip compression level.
@ -46,12 +51,12 @@ type GzipConfig struct {
} }
// Gzip returns a middleware which compresses HTTP response using Gzip compression scheme. // Gzip returns a middleware which compresses HTTP response using Gzip compression scheme.
func Gzip() func(*core.RequestEvent) error { func Gzip() *hook.Handler[*core.RequestEvent] {
return GzipWithConfig(GzipConfig{}) return GzipWithConfig(GzipConfig{})
} }
// GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme. // GzipWithConfig returns a middleware which compresses HTTP response using gzip compression scheme.
func GzipWithConfig(config GzipConfig) func(*core.RequestEvent) error { func GzipWithConfig(config GzipConfig) *hook.Handler[*core.RequestEvent] {
if config.Level < -2 || config.Level > 9 { // these are consts: gzip.HuffmanOnly and gzip.BestCompression if config.Level < -2 || config.Level > 9 { // these are consts: gzip.HuffmanOnly and gzip.BestCompression
panic(errors.New("invalid gzip level")) panic(errors.New("invalid gzip level"))
} }
@ -79,54 +84,57 @@ func GzipWithConfig(config GzipConfig) func(*core.RequestEvent) error {
}, },
} }
return func(e *core.RequestEvent) error { return &hook.Handler[*core.RequestEvent]{
e.Response.Header().Add("Vary", "Accept-Encoding") Id: DefaultGzipMiddlewareId,
if strings.Contains(e.Request.Header.Get("Accept-Encoding"), gzipScheme) { Func: func(e *core.RequestEvent) error {
w, ok := pool.Get().(*gzip.Writer) e.Response.Header().Add("Vary", "Accept-Encoding")
if !ok { if strings.Contains(e.Request.Header.Get("Accept-Encoding"), gzipScheme) {
return e.InternalServerError("", errors.New("failed to get gzip.Writer")) w, ok := pool.Get().(*gzip.Writer)
if !ok {
return e.InternalServerError("", errors.New("failed to get gzip.Writer"))
}
rw := e.Response
w.Reset(rw)
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() {
// There are different reasons for cases when we have not yet written response to the client and now need to do so.
// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
if !grw.wroteBody {
if rw.Header().Get("Content-Encoding") == gzipScheme {
rw.Header().Del("Content-Encoding")
}
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
// We have to reset response to it's pristine state when
// nothing is written to body or error is returned.
// See issue echo#424, echo#407.
e.Response = rw
w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// Write uncompressed response
e.Response = rw
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
}
w.Close()
bpool.Put(buf)
pool.Put(w)
}()
e.Response = grw
} }
rw := e.Response return e.Next()
w.Reset(rw) },
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() {
// There are different reasons for cases when we have not yet written response to the client and now need to do so.
// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
if !grw.wroteBody {
if rw.Header().Get("Content-Encoding") == gzipScheme {
rw.Header().Del("Content-Encoding")
}
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
// We have to reset response to it's pristine state when
// nothing is written to body or error is returned.
// See issue echo#424, echo#407.
e.Response = rw
w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// Write uncompressed response
e.Response = rw
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
}
w.Close()
bpool.Put(buf)
pool.Put(w)
}()
e.Response = grw
}
return e.Next()
} }
} }

View File

@ -83,21 +83,17 @@ func Serve(app core.App, config ServeConfig) error {
return err return err
} }
pbRouter.Bind(&hook.Handler[*core.RequestEvent]{ pbRouter.Bind(CORSWithConfig(CORSConfig{
Id: DefaultCorsMiddlewareId, AllowOrigins: config.AllowedOrigins,
Func: CORSWithConfig(CORSConfig{ AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
AllowOrigins: config.AllowedOrigins, }))
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
}),
Priority: DefaultCorsMiddlewarePriority,
})
pbRouter.BindFunc(installerRedirect(app, config.DashboardPath)) pbRouter.BindFunc(installerRedirect(app, config.DashboardPath))
pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)). pbRouter.GET(config.DashboardPath, Static(ui.DistDirFS, false)).
BindFunc(dashboardRemoveInstallerParam()). BindFunc(dashboardRemoveInstallerParam()).
BindFunc(dashboardCacheControl()). BindFunc(dashboardCacheControl()).
BindFunc(Gzip()) Bind(Gzip())
// start http server // start http server
// --- // ---