diff --git a/go.mod b/go.mod index 4394a2d..aa25733 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23 require ( github.com/fatih/color v1.18.0 github.com/fsouza/go-dockerclient v1.12.0 - github.com/go-chi/chi v4.0.2+incompatible + github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/render v1.0.3 github.com/go-pkgz/lgr v0.11.1 github.com/go-pkgz/mongo/v2 v2.2.1 diff --git a/go.sum b/go.sum index 0f542d2..73362d9 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsouza/go-dockerclient v1.12.0 h1:S2f2crEUbBNCFiF06kR/GvioEB8EMsb3Td/bpawD+aU= github.com/fsouza/go-dockerclient v1.12.0/go.mod h1:YWUtjg8japrqD/80L98nTtCoxQFp5B5wrSsnyeB5lFo= -github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= -github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= +github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM= diff --git a/vendor/github.com/go-chi/chi/.travis.yml b/vendor/github.com/go-chi/chi/.travis.yml index de3287e..7b8e26b 100644 --- a/vendor/github.com/go-chi/chi/.travis.yml +++ b/vendor/github.com/go-chi/chi/.travis.yml @@ -4,6 +4,8 @@ go: - 1.10.x - 1.11.x - 1.12.x + - 1.13.x + - 1.14.x script: - go get -d -t ./... diff --git a/vendor/github.com/go-chi/chi/CHANGELOG.md b/vendor/github.com/go-chi/chi/CHANGELOG.md index d03e40c..9a64a72 100644 --- a/vendor/github.com/go-chi/chi/CHANGELOG.md +++ b/vendor/github.com/go-chi/chi/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## v4.1.2 (2020-06-02) + +- fix that handles MethodNotAllowed with path variables, thank you @caseyhadden for your contribution +- fix to replace nested wildcards correctly in RoutePattern, thank you @@unmultimedio for your contribution +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.1...v4.1.2 + + +## v4.1.1 (2020-04-16) + +- fix for issue https://github.com/go-chi/chi/issues/411 which allows for overlapping regexp + route to the correct handler through a recursive tree search, thanks to @Jahaja for the PR/fix! +- new middleware.RouteHeaders as a simple router for request headers with wildcard support +- History of changes: see https://github.com/go-chi/chi/compare/v4.1.0...v4.1.1 + + +## v4.1.0 (2020-04-1) + +- middleware.LogEntry: Write method on interface now passes the response header + and an extra interface type useful for custom logger implementations. +- middleware.WrapResponseWriter: minor fix +- middleware.Recoverer: a bit prettier +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.4...v4.1.0 + + +## v4.0.4 (2020-03-24) + +- middleware.Recoverer: new pretty stack trace printing (https://github.com/go-chi/chi/pull/496) +- a few minor improvements and fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.3...v4.0.4 + + +## v4.0.3 (2020-01-09) + +- core: fix regexp routing to include default value when param is not matched +- middleware: rewrite of middleware.Compress +- middleware: suppress http.ErrAbortHandler in middleware.Recoverer +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.2...v4.0.3 + + +## v4.0.2 (2019-02-26) + +- Minor fixes +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.1...v4.0.2 + + +## v4.0.1 (2019-01-21) + +- Fixes issue with compress middleware: #382 #385 +- History of changes: see https://github.com/go-chi/chi/compare/v4.0.0...v4.0.1 + + ## v4.0.0 (2019-01-10) - chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 diff --git a/vendor/github.com/go-chi/chi/README.md b/vendor/github.com/go-chi/chi/README.md index d36d4db..5a8fc9d 100644 --- a/vendor/github.com/go-chi/chi/README.md +++ b/vendor/github.com/go-chi/chi/README.md @@ -28,7 +28,7 @@ included some useful/optional subpackages: [middleware](/middleware), [render](h * **Fast** - yes, see [benchmarks](#benchmarks) * **100% compatible with net/http** - use any http or middleware pkg in the ecosystem that is also compatible with `net/http` * **Designed for modular/composable APIs** - middlewares, inline middlewares, route groups and subrouter mounting -* **Context control** - built on new `context` package, providing value chaining, cancelations and timeouts +* **Context control** - built on new `context` package, providing value chaining, cancellations and timeouts * **Robust** - in production at Pressly, CloudFlare, Heroku, 99Designs, and many others (see [discussion](https://github.com/go-chi/chi/issues/91)) * **Doc generation** - `docgen` auto-generates routing documentation from your source to JSON or Markdown * **No external dependencies** - plain ol' Go stdlib + net/http @@ -46,11 +46,14 @@ package main import ( "net/http" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" ) func main() { r := chi.NewRouter() + r.Use(middleware.Logger) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("welcome")) }) @@ -179,7 +182,7 @@ type Router interface { http.Handler Routes - // Use appends one of more middlewares onto the Router stack. + // Use appends one or more middlewares onto the Router stack. Use(middlewares ...func(http.Handler) http.Handler) // With adds inline middlewares for an endpoint handler. @@ -312,9 +315,10 @@ with `net/http` can be used with chi's mux. ### Core middlewares ----------------------------------------------------------------------------------------------------------- -| chi/middleware Handler | description | +| chi/middleware Handler | description | |:----------------------|:--------------------------------------------------------------------------------- | AllowContentType | Explicit whitelist of accepted request Content-Types | +| BasicAuth | Basic HTTP authentication | | Compress | Gzip compression for clients that accept compressed responses | | GetHead | Automatically route undefined HEAD requests to GET handlers | | Heartbeat | Monitoring endpoint to check the servers pulse | @@ -333,7 +337,7 @@ with `net/http` can be used with chi's mux. | WithValue | Short-hand middleware to set a key/value on the request context | ----------------------------------------------------------------------------------------------------------- -### Auxiliary middlewares & packages +### Extra middlewares & packages Please see https://github.com/go-chi for additional packages. @@ -344,9 +348,11 @@ Please see https://github.com/go-chi for additional packages. | [docgen](https://github.com/go-chi/docgen) | Print chi.Router routes at runtime | | [jwtauth](https://github.com/go-chi/jwtauth) | JWT authentication | | [hostrouter](https://github.com/go-chi/hostrouter) | Domain/host based request routing | -| [httpcoala](https://github.com/go-chi/httpcoala) | HTTP request coalescer | -| [chi-authz](https://github.com/casbin/chi-authz) | Request ACL via https://github.com/hsluoyz/casbin | -| [phi](https://github.com/fate-lovely/phi) | Port chi to [fasthttp](https://github.com/valyala/fasthttp) | +| [httplog](https://github.com/go-chi/httplog) | Small but powerful structured HTTP request logging | +| [httprate](https://github.com/go-chi/httprate) | HTTP request rate limiter | +| [httptracer](https://github.com/go-chi/httptracer) | HTTP request performance tracing library | +| [httpvcr](https://github.com/go-chi/httpvcr) | Write deterministic tests for external sources | +| [stampede](https://github.com/go-chi/stampede) | HTTP request coalescer | -------------------------------------------------------------------------------------------------------------------- please [submit a PR](./CONTRIBUTING.md) if you'd like to include a link to a chi-compatible middleware @@ -412,18 +418,15 @@ We'll be more than happy to see [your contributions](./CONTRIBUTING.md)! ## Beyond REST chi is just a http router that lets you decompose request handling into many smaller layers. -Many companies including Pressly.com (of course) use chi to write REST services for their public -APIs. But, REST is just a convention for managing state via HTTP, and there's a lot of other pieces -required to write a complete client-server system or network of microservices. - -Looking ahead beyond REST, I also recommend some newer works in the field coming from -[gRPC](https://github.com/grpc/grpc-go), [NATS](https://nats.io), [go-kit](https://github.com/go-kit/kit) -and even [graphql](https://github.com/graphql-go/graphql). They're all pretty cool with their -own unique approaches and benefits. Specifically, I'd look at gRPC since it makes client-server -communication feel like a single program on a single computer, no need to hand-write a client library -and the request/response payloads are typed contracts. NATS is pretty amazing too as a super -fast and lightweight pub-sub transport that can speak protobufs, with nice service discovery - -an excellent combination with gRPC. +Many companies use chi to write REST services for their public APIs. But, REST is just a convention +for managing state via HTTP, and there's a lot of other pieces required to write a complete client-server +system or network of microservices. + +Looking beyond REST, I also recommend some newer works in the field: +* [webrpc](https://github.com/webrpc/webrpc) - Web-focused RPC client+server framework with code-gen +* [gRPC](https://github.com/grpc/grpc-go) - Google's RPC framework via protobufs +* [graphql](https://github.com/99designs/gqlgen) - Declarative query language +* [NATS](https://nats.io) - lightweight pub-sub ## License diff --git a/vendor/github.com/go-chi/chi/chi.go b/vendor/github.com/go-chi/chi/chi.go index 9962229..b7063dc 100644 --- a/vendor/github.com/go-chi/chi/chi.go +++ b/vendor/github.com/go-chi/chi/chi.go @@ -1,7 +1,7 @@ // // Package chi is a small, idiomatic and composable router for building HTTP services. // -// chi requires Go 1.7 or newer. +// chi requires Go 1.10 or newer. // // Example: // package main @@ -68,7 +68,7 @@ type Router interface { http.Handler Routes - // Use appends one of more middlewares onto the Router stack. + // Use appends one or more middlewares onto the Router stack. Use(middlewares ...func(http.Handler) http.Handler) // With adds inline middlewares for an endpoint handler. diff --git a/vendor/github.com/go-chi/chi/context.go b/vendor/github.com/go-chi/chi/context.go index 229c9cb..26c609e 100644 --- a/vendor/github.com/go-chi/chi/context.go +++ b/vendor/github.com/go-chi/chi/context.go @@ -7,6 +7,54 @@ import ( "strings" ) +// URLParam returns the url parameter from a http.Request object. +func URLParam(r *http.Request, key string) string { + if rctx := RouteContext(r.Context()); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// URLParamFromCtx returns the url parameter from a http.Request Context. +func URLParamFromCtx(ctx context.Context, key string) string { + if rctx := RouteContext(ctx); rctx != nil { + return rctx.URLParam(key) + } + return "" +} + +// RouteContext returns chi's routing Context object from a +// http.Request Context. +func RouteContext(ctx context.Context) *Context { + val, _ := ctx.Value(RouteCtxKey).(*Context) + return val +} + +// ServerBaseContext wraps an http.Handler to set the request context to the +// `baseCtx`. +func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + baseCtx := baseCtx + + // Copy over default net/http server context keys + if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok { + baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v) + } + if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok { + baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v) + } + + h.ServeHTTP(w, r.WithContext(baseCtx)) + }) + return fn +} + +// NewRouteContext returns a new routing Context object. +func NewRouteContext() *Context { + return &Context{} +} + var ( // RouteCtxKey is the context.Context key to store the request context. RouteCtxKey = &contextKey{"RouteContext"} @@ -46,11 +94,6 @@ type Context struct { methodNotAllowed bool } -// NewRouteContext returns a new routing Context object. -func NewRouteContext() *Context { - return &Context{} -} - // Reset a routing context to its initial state. func (x *Context) Reset() { x.Routes = nil @@ -93,29 +136,17 @@ func (x *Context) URLParam(key string) string { // } func (x *Context) RoutePattern() string { routePattern := strings.Join(x.RoutePatterns, "") - return strings.Replace(routePattern, "/*/", "/", -1) -} - -// RouteContext returns chi's routing Context object from a -// http.Request Context. -func RouteContext(ctx context.Context) *Context { - return ctx.Value(RouteCtxKey).(*Context) + return replaceWildcards(routePattern) } -// URLParam returns the url parameter from a http.Request object. -func URLParam(r *http.Request, key string) string { - if rctx := RouteContext(r.Context()); rctx != nil { - return rctx.URLParam(key) +// replaceWildcards takes a route pattern and recursively replaces all +// occurrences of "/*/" to "/". +func replaceWildcards(p string) string { + if strings.Contains(p, "/*/") { + return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) } - return "" -} -// URLParamFromCtx returns the url parameter from a http.Request Context. -func URLParamFromCtx(ctx context.Context, key string) string { - if rctx := RouteContext(ctx); rctx != nil { - return rctx.URLParam(key) - } - return "" + return p } // RouteParams is a structure to track URL routing parameters efficiently. @@ -125,28 +156,8 @@ type RouteParams struct { // Add will append a URL parameter to the end of the route param func (s *RouteParams) Add(key, value string) { - (*s).Keys = append((*s).Keys, key) - (*s).Values = append((*s).Values, value) -} - -// ServerBaseContext wraps an http.Handler to set the request context to the -// `baseCtx`. -func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler { - fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - baseCtx := baseCtx - - // Copy over default net/http server context keys - if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok { - baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v) - } - if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok { - baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v) - } - - h.ServeHTTP(w, r.WithContext(baseCtx)) - }) - return fn + s.Keys = append(s.Keys, key) + s.Values = append(s.Values, value) } // contextKey is a value for use with context.WithValue. It's used as diff --git a/vendor/github.com/go-chi/chi/middleware/basic_auth.go b/vendor/github.com/go-chi/chi/middleware/basic_auth.go new file mode 100644 index 0000000..87b2641 --- /dev/null +++ b/vendor/github.com/go-chi/chi/middleware/basic_auth.go @@ -0,0 +1,32 @@ +package middleware + +import ( + "fmt" + "net/http" +) + +// BasicAuth implements a simple middleware handler for adding basic http auth to a route. +func BasicAuth(realm string, creds map[string]string) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok { + basicAuthFailed(w, realm) + return + } + + credPass, credUserOk := creds[user] + if !credUserOk || pass != credPass { + basicAuthFailed(w, realm) + return + } + + next.ServeHTTP(w, r) + }) + } +} + +func basicAuthFailed(w http.ResponseWriter, realm string) { + w.Header().Add("WWW-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm)) + w.WriteHeader(http.StatusUnauthorized) +} diff --git a/vendor/github.com/go-chi/chi/middleware/compress.go b/vendor/github.com/go-chi/chi/middleware/compress.go index d2876d4..2f40cc1 100644 --- a/vendor/github.com/go-chi/chi/middleware/compress.go +++ b/vendor/github.com/go-chi/chi/middleware/compress.go @@ -5,26 +5,101 @@ import ( "compress/flate" "compress/gzip" "errors" + "fmt" "io" + "io/ioutil" "net" "net/http" "strings" + "sync" ) -var encoders = map[string]EncoderFunc{} +var defaultCompressibleContentTypes = []string{ + "text/html", + "text/css", + "text/plain", + "text/javascript", + "application/javascript", + "application/x-javascript", + "application/json", + "application/atom+xml", + "application/rss+xml", + "image/svg+xml", +} + +// Compress is a middleware that compresses response +// body of a given content types to a data format based +// on Accept-Encoding request header. It uses a given +// compression level. +// +// NOTE: make sure to set the Content-Type header on your response +// otherwise this middleware will not compress the response body. For ex, in +// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody)) +// or set it manually. +// +// Passing a compression level of 5 is sensible value +func Compress(level int, types ...string) func(next http.Handler) http.Handler { + compressor := NewCompressor(level, types...) + return compressor.Handler +} + +// Compressor represents a set of encoding configurations. +type Compressor struct { + level int // The compression level. + // The mapping of encoder names to encoder functions. + encoders map[string]EncoderFunc + // The mapping of pooled encoders to pools. + pooledEncoders map[string]*sync.Pool + // The set of content types allowed to be compressed. + allowedTypes map[string]struct{} + allowedWildcards map[string]struct{} + // The list of encoders in order of decreasing precedence. + encodingPrecedence []string +} + +// NewCompressor creates a new Compressor that will handle encoding responses. +// +// The level should be one of the ones defined in the flate package. +// The types are the content types that are allowed to be compressed. +func NewCompressor(level int, types ...string) *Compressor { + // If types are provided, set those as the allowed types. If none are + // provided, use the default list. + allowedTypes := make(map[string]struct{}) + allowedWildcards := make(map[string]struct{}) + if len(types) > 0 { + for _, t := range types { + if strings.Contains(strings.TrimSuffix(t, "/*"), "*") { + panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t)) + } + if strings.HasSuffix(t, "/*") { + allowedWildcards[strings.TrimSuffix(t, "/*")] = struct{}{} + } else { + allowedTypes[t] = struct{}{} + } + } + } else { + for _, t := range defaultCompressibleContentTypes { + allowedTypes[t] = struct{}{} + } + } -var encodingPrecedence = []string{"br", "gzip", "deflate"} + c := &Compressor{ + level: level, + encoders: make(map[string]EncoderFunc), + pooledEncoders: make(map[string]*sync.Pool), + allowedTypes: allowedTypes, + allowedWildcards: allowedWildcards, + } -func init() { + // Set the default encoders. The precedence order uses the reverse + // ordering that the encoders were added. This means adding new encoders + // will move them to the front of the order. + // // TODO: // lzma: Opera. // sdch: Chrome, Android. Gzip output + dictionary header. // br: Brotli, see https://github.com/go-chi/chi/pull/326 - // TODO: Exception for old MSIE browsers that can't handle non-HTML? - // https://zoompf.com/blog/2012/02/lose-the-wait-http-compression - SetEncoder("gzip", encoderGzip) - // HTTP 1.1 "deflate" (RFC 2616) stands for DEFLATE data (RFC 1951) // wrapped with zlib (RFC 1950). The zlib wrapper uses Adler-32 // checksum compared to CRC-32 used in "gzip" and thus is faster. @@ -41,21 +116,20 @@ func init() { // // That's why we prefer gzip over deflate. It's just more reliable // and not significantly slower than gzip. - SetEncoder("deflate", encoderDeflate) + c.SetEncoder("deflate", encoderDeflate) + + // TODO: Exception for old MSIE browsers that can't handle non-HTML? + // https://zoompf.com/blog/2012/02/lose-the-wait-http-compression + c.SetEncoder("gzip", encoderGzip) // NOTE: Not implemented, intentionally: // case "compress": // LZW. Deprecated. // case "bzip2": // Too slow on-the-fly. // case "zopfli": // Too slow on-the-fly. // case "xz": // Too slow on-the-fly. + return c } -// An EncoderFunc is a function that wraps the provided ResponseWriter with a -// streaming compression algorithm and returns it. -// -// In case of failure, the function should return nil. -type EncoderFunc func(w http.ResponseWriter, level int) io.Writer - // SetEncoder can be used to set the implementation of a compression algorithm. // // The encoding should be a standardised identifier. See: @@ -65,12 +139,13 @@ type EncoderFunc func(w http.ResponseWriter, level int) io.Writer // // import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc" // -// middleware.SetEncoder("br", func(w http.ResponseWriter, level int) io.Writer { +// compressor := middleware.NewCompressor(5, "text/html") +// compressor.SetEncoder("br", func(w http.ResponseWriter, level int) io.Writer { // params := brotli_enc.NewBrotliParams() // params.SetQuality(level) // return brotli_enc.NewBrotliWriter(params, w) // }) -func SetEncoder(encoding string, fn EncoderFunc) { +func (c *Compressor) SetEncoder(encoding string, fn EncoderFunc) { encoding = strings.ToLower(encoding) if encoding == "" { panic("the encoding can not be empty") @@ -78,118 +153,153 @@ func SetEncoder(encoding string, fn EncoderFunc) { if fn == nil { panic("attempted to set a nil encoder function") } - encoders[encoding] = fn - var e string - for _, v := range encodingPrecedence { - if v == encoding { - e = v - } + // If we are adding a new encoder that is already registered, we have to + // clear that one out first. + if _, ok := c.pooledEncoders[encoding]; ok { + delete(c.pooledEncoders, encoding) } - - if e == "" { - encodingPrecedence = append([]string{e}, encodingPrecedence...) + if _, ok := c.encoders[encoding]; ok { + delete(c.encoders, encoding) } -} -var defaultContentTypes = map[string]struct{}{ - "text/html": {}, - "text/css": {}, - "text/plain": {}, - "text/javascript": {}, - "application/javascript": {}, - "application/x-javascript": {}, - "application/json": {}, - "application/atom+xml": {}, - "application/rss+xml": {}, - "image/svg+xml": {}, -} - -// DefaultCompress is a middleware that compresses response -// body of predefined content types to a data format based -// on Accept-Encoding request header. It uses a default -// compression level. -func DefaultCompress(next http.Handler) http.Handler { - return Compress(flate.DefaultCompression)(next) -} + // If the encoder supports Resetting (IoReseterWriter), then it can be pooled. + encoder := fn(ioutil.Discard, c.level) + if encoder != nil { + if _, ok := encoder.(ioResetterWriter); ok { + pool := &sync.Pool{ + New: func() interface{} { + return fn(ioutil.Discard, c.level) + }, + } + c.pooledEncoders[encoding] = pool + } + } + // If the encoder is not in the pooledEncoders, add it to the normal encoders. + if _, ok := c.pooledEncoders[encoding]; !ok { + c.encoders[encoding] = fn + } -// Compress is a middleware that compresses response -// body of a given content types to a data format based -// on Accept-Encoding request header. It uses a given -// compression level. -// -// NOTE: make sure to set the Content-Type header on your response -// otherwise this middleware will not compress the response body. For ex, in -// your handler you should set w.Header().Set("Content-Type", http.DetectContentType(yourBody)) -// or set it manually. -func Compress(level int, types ...string) func(next http.Handler) http.Handler { - contentTypes := defaultContentTypes - if len(types) > 0 { - contentTypes = make(map[string]struct{}, len(types)) - for _, t := range types { - contentTypes[t] = struct{}{} + for i, v := range c.encodingPrecedence { + if v == encoding { + c.encodingPrecedence = append(c.encodingPrecedence[:i], c.encodingPrecedence[i+1:]...) } } - return func(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - encoder, encoding := selectEncoder(r.Header) - - cw := &compressResponseWriter{ - ResponseWriter: w, - w: w, - contentTypes: contentTypes, - encoder: encoder, - encoding: encoding, - level: level, - } - defer cw.Close() + c.encodingPrecedence = append([]string{encoding}, c.encodingPrecedence...) +} - next.ServeHTTP(cw, r) +// Handler returns a new middleware that will compress the response based on the +// current Compressor. +func (c *Compressor) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + encoder, encoding, cleanup := c.selectEncoder(r.Header, w) + + cw := &compressResponseWriter{ + ResponseWriter: w, + w: w, + contentTypes: c.allowedTypes, + contentWildcards: c.allowedWildcards, + encoding: encoding, + compressable: false, // determined in post-handler + } + if encoder != nil { + cw.w = encoder } + // Re-add the encoder to the pool if applicable. + defer cleanup() + defer cw.Close() - return http.HandlerFunc(fn) - } + next.ServeHTTP(cw, r) + }) } -func selectEncoder(h http.Header) (EncoderFunc, string) { +// selectEncoder returns the encoder, the name of the encoder, and a closer function. +func (c *Compressor) selectEncoder(h http.Header, w io.Writer) (io.Writer, string, func()) { header := h.Get("Accept-Encoding") // Parse the names of all accepted algorithms from the header. accepted := strings.Split(strings.ToLower(header), ",") // Find supported encoder by accepted list by precedence - for _, name := range encodingPrecedence { - if fn, ok := encoders[name]; ok && matchAcceptEncoding(accepted, name) { - return fn, name + for _, name := range c.encodingPrecedence { + if matchAcceptEncoding(accepted, name) { + if pool, ok := c.pooledEncoders[name]; ok { + encoder := pool.Get().(ioResetterWriter) + cleanup := func() { + pool.Put(encoder) + } + encoder.Reset(w) + return encoder, name, cleanup + + } + if fn, ok := c.encoders[name]; ok { + return fn(w, c.level), name, func() {} + } } + } // No encoder found to match the accepted encoding - return nil, "" + return nil, "", func() {} } func matchAcceptEncoding(accepted []string, encoding string) bool { for _, v := range accepted { - if strings.Index(v, encoding) >= 0 { + if strings.Contains(v, encoding) { return true } } return false } +// An EncoderFunc is a function that wraps the provided io.Writer with a +// streaming compression algorithm and returns it. +// +// In case of failure, the function should return nil. +type EncoderFunc func(w io.Writer, level int) io.Writer + +// Interface for types that allow resetting io.Writers. +type ioResetterWriter interface { + io.Writer + Reset(w io.Writer) +} + type compressResponseWriter struct { http.ResponseWriter - w io.Writer - encoder EncoderFunc - encoding string - contentTypes map[string]struct{} - level int - wroteHeader bool + + // The streaming encoder writer to be used if there is one. Otherwise, + // this is just the normal writer. + w io.Writer + encoding string + contentTypes map[string]struct{} + contentWildcards map[string]struct{} + wroteHeader bool + compressable bool +} + +func (cw *compressResponseWriter) isCompressable() bool { + // Parse the first part of the Content-Type response header. + contentType := cw.Header().Get("Content-Type") + if idx := strings.Index(contentType, ";"); idx >= 0 { + contentType = contentType[0:idx] + } + + // Is the content type compressable? + if _, ok := cw.contentTypes[contentType]; ok { + return true + } + if idx := strings.Index(contentType, "/"); idx > 0 { + contentType = contentType[0:idx] + _, ok := cw.contentWildcards[contentType] + return ok + } + return false } func (cw *compressResponseWriter) WriteHeader(code int) { if cw.wroteHeader { + cw.ResponseWriter.WriteHeader(code) // Allow multiple calls to propagate. return } cw.wroteHeader = true @@ -200,26 +310,18 @@ func (cw *compressResponseWriter) WriteHeader(code int) { return } - // Parse the first part of the Content-Type response header. - contentType := "" - parts := strings.Split(cw.Header().Get("Content-Type"), ";") - if len(parts) > 0 { - contentType = parts[0] - } - - // Is the content type compressable? - if _, ok := cw.contentTypes[contentType]; !ok { + if !cw.isCompressable() { + cw.compressable = false return } - if cw.encoder != nil && cw.encoding != "" { - if wr := cw.encoder(cw.ResponseWriter, cw.level); wr != nil { - cw.w = wr - cw.Header().Set("Content-Encoding", cw.encoding) + if cw.encoding != "" { + cw.compressable = true + cw.Header().Set("Content-Encoding", cw.encoding) + cw.Header().Set("Vary", "Accept-Encoding") - // The content-length after compression is unknown - cw.Header().Del("Content-Length") - } + // The content-length after compression is unknown + cw.Header().Del("Content-Length") } } @@ -228,37 +330,59 @@ func (cw *compressResponseWriter) Write(p []byte) (int, error) { cw.WriteHeader(http.StatusOK) } - return cw.w.Write(p) + return cw.writer().Write(p) +} + +func (cw *compressResponseWriter) writer() io.Writer { + if cw.compressable { + return cw.w + } else { + return cw.ResponseWriter + } +} + +type compressFlusher interface { + Flush() error } func (cw *compressResponseWriter) Flush() { - if f, ok := cw.w.(http.Flusher); ok { + if f, ok := cw.writer().(http.Flusher); ok { + f.Flush() + } + // If the underlying writer has a compression flush signature, + // call this Flush() method instead + if f, ok := cw.writer().(compressFlusher); ok { f.Flush() + + // Also flush the underlying response writer + if f, ok := cw.ResponseWriter.(http.Flusher); ok { + f.Flush() + } } } func (cw *compressResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - if hj, ok := cw.w.(http.Hijacker); ok { + if hj, ok := cw.writer().(http.Hijacker); ok { return hj.Hijack() } return nil, nil, errors.New("chi/middleware: http.Hijacker is unavailable on the writer") } func (cw *compressResponseWriter) Push(target string, opts *http.PushOptions) error { - if ps, ok := cw.w.(http.Pusher); ok { + if ps, ok := cw.writer().(http.Pusher); ok { return ps.Push(target, opts) } return errors.New("chi/middleware: http.Pusher is unavailable on the writer") } func (cw *compressResponseWriter) Close() error { - if c, ok := cw.w.(io.WriteCloser); ok { + if c, ok := cw.writer().(io.WriteCloser); ok { return c.Close() } return errors.New("chi/middleware: io.WriteCloser is unavailable on the writer") } -func encoderGzip(w http.ResponseWriter, level int) io.Writer { +func encoderGzip(w io.Writer, level int) io.Writer { gw, err := gzip.NewWriterLevel(w, level) if err != nil { return nil @@ -266,7 +390,7 @@ func encoderGzip(w http.ResponseWriter, level int) io.Writer { return gw } -func encoderDeflate(w http.ResponseWriter, level int) io.Writer { +func encoderDeflate(w io.Writer, level int) io.Writer { dw, err := flate.NewWriter(w, level) if err != nil { return nil diff --git a/vendor/github.com/go-chi/chi/middleware/content_encoding.go b/vendor/github.com/go-chi/chi/middleware/content_encoding.go new file mode 100644 index 0000000..e0b9ccc --- /dev/null +++ b/vendor/github.com/go-chi/chi/middleware/content_encoding.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "net/http" + "strings" +) + +// AllowContentEncoding enforces a whitelist of request Content-Encoding otherwise responds +// with a 415 Unsupported Media Type status. +func AllowContentEncoding(contentEncoding ...string) func(next http.Handler) http.Handler { + allowedEncodings := make(map[string]struct{}, len(contentEncoding)) + for _, encoding := range contentEncoding { + allowedEncodings[strings.TrimSpace(strings.ToLower(encoding))] = struct{}{} + } + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + requestEncodings := r.Header["Content-Encoding"] + // skip check for empty content body or no Content-Encoding + if r.ContentLength == 0 { + next.ServeHTTP(w, r) + return + } + // All encodings in the request must be allowed + for _, encoding := range requestEncodings { + if _, ok := allowedEncodings[strings.TrimSpace(strings.ToLower(encoding))]; !ok { + w.WriteHeader(http.StatusUnsupportedMediaType) + return + } + } + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } +} diff --git a/vendor/github.com/go-chi/chi/middleware/logger.go b/vendor/github.com/go-chi/chi/middleware/logger.go index 9f119d5..158a6a3 100644 --- a/vendor/github.com/go-chi/chi/middleware/logger.go +++ b/vendor/github.com/go-chi/chi/middleware/logger.go @@ -25,8 +25,8 @@ var ( // print in color, otherwise it will print in black and white. Logger prints a // request ID if one is provided. // -// Alternatively, look at https://github.com/pressly/lg and the `lg.RequestLogger` -// middleware pkg. +// Alternatively, look at https://github.com/goware/httplog for a more in-depth +// http logger with structured logging support. func Logger(next http.Handler) http.Handler { return DefaultLogger(next) } @@ -40,7 +40,7 @@ func RequestLogger(f LogFormatter) func(next http.Handler) http.Handler { t1 := time.Now() defer func() { - entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1)) + entry.Write(ww.Status(), ww.BytesWritten(), ww.Header(), time.Since(t1), nil) }() next.ServeHTTP(ww, WithLogEntry(r, entry)) @@ -58,7 +58,7 @@ type LogFormatter interface { // LogEntry records the final log when a request completes. // See defaultLogEntry for an example implementation. type LogEntry interface { - Write(status, bytes int, elapsed time.Duration) + Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) Panic(v interface{}, stack []byte) } @@ -122,7 +122,7 @@ type defaultLogEntry struct { useColor bool } -func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) { +func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { switch { case status < 200: cW(l.buf, l.useColor, bBlue, "%03d", status) @@ -151,8 +151,5 @@ func (l *defaultLogEntry) Write(status, bytes int, elapsed time.Duration) { } func (l *defaultLogEntry) Panic(v interface{}, stack []byte) { - panicEntry := l.NewLogEntry(l.request).(*defaultLogEntry) - cW(panicEntry.buf, l.useColor, bRed, "panic: %+v", v) - l.Logger.Print(panicEntry.buf.String()) - l.Logger.Print(string(stack)) + PrintPrettyStack(v) } diff --git a/vendor/github.com/go-chi/chi/middleware/middleware.go b/vendor/github.com/go-chi/chi/middleware/middleware.go index be6a44f..cc371e0 100644 --- a/vendor/github.com/go-chi/chi/middleware/middleware.go +++ b/vendor/github.com/go-chi/chi/middleware/middleware.go @@ -1,5 +1,16 @@ package middleware +import "net/http" + +// New will create a new middleware handler from a http.Handler. +func New(h http.Handler) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + }) + } +} + // contextKey is a value for use with context.WithValue. It's used as // a pointer so it fits in an interface{} without allocation. This technique // for defining context keys was copied from Go 1.7's new use of context in net/http. diff --git a/vendor/github.com/go-chi/chi/middleware/realip.go b/vendor/github.com/go-chi/chi/middleware/realip.go index 146c2b0..72db6ca 100644 --- a/vendor/github.com/go-chi/chi/middleware/realip.go +++ b/vendor/github.com/go-chi/chi/middleware/realip.go @@ -40,14 +40,14 @@ func RealIP(h http.Handler) http.Handler { func realIP(r *http.Request) string { var ip string - if xff := r.Header.Get(xForwardedFor); xff != "" { + if xrip := r.Header.Get(xRealIP); xrip != "" { + ip = xrip + } else if xff := r.Header.Get(xForwardedFor); xff != "" { i := strings.Index(xff, ", ") if i == -1 { i = len(xff) } ip = xff[:i] - } else if xrip := r.Header.Get(xRealIP); xrip != "" { - ip = xrip } return ip diff --git a/vendor/github.com/go-chi/chi/middleware/recoverer.go b/vendor/github.com/go-chi/chi/middleware/recoverer.go index 57fc3eb..785b18c 100644 --- a/vendor/github.com/go-chi/chi/middleware/recoverer.go +++ b/vendor/github.com/go-chi/chi/middleware/recoverer.go @@ -4,10 +4,13 @@ package middleware // https://github.com/zenazn/goji/tree/master/web/middleware import ( + "bytes" + "errors" "fmt" "net/http" "os" "runtime/debug" + "strings" ) // Recoverer is a middleware that recovers from panics, logs the panic (and a @@ -18,17 +21,16 @@ import ( func Recoverer(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { - if rvr := recover(); rvr != nil { + if rvr := recover(); rvr != nil && rvr != http.ErrAbortHandler { logEntry := GetLogEntry(r) if logEntry != nil { logEntry.Panic(rvr, debug.Stack()) } else { - fmt.Fprintf(os.Stderr, "Panic: %+v\n", rvr) - debug.PrintStack() + PrintPrettyStack(rvr) } - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + w.WriteHeader(http.StatusInternalServerError) } }() @@ -37,3 +39,154 @@ func Recoverer(next http.Handler) http.Handler { return http.HandlerFunc(fn) } + +func PrintPrettyStack(rvr interface{}) { + debugStack := debug.Stack() + s := prettyStack{} + out, err := s.parse(debugStack, rvr) + if err == nil { + os.Stderr.Write(out) + } else { + // print stdlib output as a fallback + os.Stderr.Write(debugStack) + } +} + +type prettyStack struct { +} + +func (s prettyStack) parse(debugStack []byte, rvr interface{}) ([]byte, error) { + var err error + useColor := true + buf := &bytes.Buffer{} + + cW(buf, false, bRed, "\n") + cW(buf, useColor, bCyan, " panic: ") + cW(buf, useColor, bBlue, "%v", rvr) + cW(buf, false, bWhite, "\n \n") + + // process debug stack info + stack := strings.Split(string(debugStack), "\n") + lines := []string{} + + // locate panic line, as we may have nested panics + for i := len(stack) - 1; i > 0; i-- { + lines = append(lines, stack[i]) + if strings.HasPrefix(stack[i], "panic(0x") { + lines = lines[0 : len(lines)-2] // remove boilerplate + break + } + } + + // reverse + for i := len(lines)/2 - 1; i >= 0; i-- { + opp := len(lines) - 1 - i + lines[i], lines[opp] = lines[opp], lines[i] + } + + // decorate + for i, line := range lines { + lines[i], err = s.decorateLine(line, useColor, i) + if err != nil { + return nil, err + } + } + + for _, l := range lines { + fmt.Fprintf(buf, "%s", l) + } + return buf.Bytes(), nil +} + +func (s prettyStack) decorateLine(line string, useColor bool, num int) (string, error) { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "\t") || strings.Contains(line, ".go:") { + return s.decorateSourceLine(line, useColor, num) + } else if strings.HasSuffix(line, ")") { + return s.decorateFuncCallLine(line, useColor, num) + } else { + if strings.HasPrefix(line, "\t") { + return strings.Replace(line, "\t", " ", 1), nil + } else { + return fmt.Sprintf(" %s\n", line), nil + } + } +} + +func (s prettyStack) decorateFuncCallLine(line string, useColor bool, num int) (string, error) { + idx := strings.LastIndex(line, "(") + if idx < 0 { + return "", errors.New("not a func call line") + } + + buf := &bytes.Buffer{} + pkg := line[0:idx] + // addr := line[idx:] + method := "" + + idx = strings.LastIndex(pkg, string(os.PathSeparator)) + if idx < 0 { + idx = strings.Index(pkg, ".") + method = pkg[idx:] + pkg = pkg[0:idx] + } else { + method = pkg[idx+1:] + pkg = pkg[0 : idx+1] + idx = strings.Index(method, ".") + pkg += method[0:idx] + method = method[idx:] + } + pkgColor := nYellow + methodColor := bGreen + + if num == 0 { + cW(buf, useColor, bRed, " -> ") + pkgColor = bMagenta + methodColor = bRed + } else { + cW(buf, useColor, bWhite, " ") + } + cW(buf, useColor, pkgColor, "%s", pkg) + cW(buf, useColor, methodColor, "%s\n", method) + // cW(buf, useColor, nBlack, "%s", addr) + return buf.String(), nil +} + +func (s prettyStack) decorateSourceLine(line string, useColor bool, num int) (string, error) { + idx := strings.LastIndex(line, ".go:") + if idx < 0 { + return "", errors.New("not a source line") + } + + buf := &bytes.Buffer{} + path := line[0 : idx+3] + lineno := line[idx+3:] + + idx = strings.LastIndex(path, string(os.PathSeparator)) + dir := path[0 : idx+1] + file := path[idx+1:] + + idx = strings.Index(lineno, " ") + if idx > 0 { + lineno = lineno[0:idx] + } + fileColor := bCyan + lineColor := bGreen + + if num == 1 { + cW(buf, useColor, bRed, " -> ") + fileColor = bRed + lineColor = bMagenta + } else { + cW(buf, false, bWhite, " ") + } + cW(buf, useColor, bWhite, "%s", dir) + cW(buf, useColor, fileColor, "%s", file) + cW(buf, useColor, lineColor, "%s", lineno) + if num == 1 { + cW(buf, false, bWhite, "\n") + } + cW(buf, false, bWhite, "\n") + + return buf.String(), nil +} diff --git a/vendor/github.com/go-chi/chi/middleware/request_id.go b/vendor/github.com/go-chi/chi/middleware/request_id.go index 65b58f6..4903ecc 100644 --- a/vendor/github.com/go-chi/chi/middleware/request_id.go +++ b/vendor/github.com/go-chi/chi/middleware/request_id.go @@ -20,6 +20,10 @@ type ctxKeyRequestID int // RequestIDKey is the key that holds the unique request ID in a request context. const RequestIDKey ctxKeyRequestID = 0 +// RequestIDHeader is the name of the HTTP Header which contains the request id. +// Exported so that it can be changed by developers +var RequestIDHeader = "X-Request-Id" + var prefix string var reqid uint64 @@ -63,7 +67,7 @@ func init() { func RequestID(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - requestID := r.Header.Get("X-Request-Id") + requestID := r.Header.Get(RequestIDHeader) if requestID == "" { myid := atomic.AddUint64(&reqid, 1) requestID = fmt.Sprintf("%s-%06d", prefix, myid) diff --git a/vendor/github.com/go-chi/chi/middleware/route_headers.go b/vendor/github.com/go-chi/chi/middleware/route_headers.go new file mode 100644 index 0000000..7ee30c8 --- /dev/null +++ b/vendor/github.com/go-chi/chi/middleware/route_headers.go @@ -0,0 +1,160 @@ +package middleware + +import ( + "net/http" + "strings" +) + +// RouteHeaders is a neat little header-based router that allows you to direct +// the flow of a request through a middleware stack based on a request header. +// +// For example, lets say you'd like to setup multiple routers depending on the +// request Host header, you could then do something as so: +// +// r := chi.NewRouter() +// rSubdomain := chi.NewRouter() +// +// r.Use(middleware.RouteHeaders(). +// Route("Host", "example.com", middleware.New(r)). +// Route("Host", "*.example.com", middleware.New(rSubdomain)). +// Handler) +// +// r.Get("/", h) +// rSubdomain.Get("/", h2) +// +// +// Another example, imagine you want to setup multiple CORS handlers, where for +// your origin servers you allow authorized requests, but for third-party public +// requests, authorization is disabled. +// +// r := chi.NewRouter() +// +// r.Use(middleware.RouteHeaders(). +// Route("Origin", "https://app.skyweaver.net", cors.Handler(cors.Options{ +// AllowedOrigins: []string{"https://api.skyweaver.net"}, +// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, +// AllowedHeaders: []string{"Accept", "Authorization", "Content-Type"}, +// AllowCredentials: true, // <----------<<< allow credentials +// })). +// Route("Origin", "*", cors.Handler(cors.Options{ +// AllowedOrigins: []string{"*"}, +// AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, +// AllowedHeaders: []string{"Accept", "Content-Type"}, +// AllowCredentials: false, // <----------<<< do not allow credentials +// })). +// Handler) +// +func RouteHeaders() HeaderRouter { + return HeaderRouter{} +} + +type HeaderRouter map[string][]HeaderRoute + +func (hr HeaderRouter) Route(header string, match string, middlewareHandler func(next http.Handler) http.Handler) HeaderRouter { + header = strings.ToLower(header) + k := hr[header] + if k == nil { + hr[header] = []HeaderRoute{} + } + hr[header] = append(hr[header], HeaderRoute{MatchOne: NewPattern(match), Middleware: middlewareHandler}) + return hr +} + +func (hr HeaderRouter) RouteAny(header string, match []string, middlewareHandler func(next http.Handler) http.Handler) HeaderRouter { + header = strings.ToLower(header) + k := hr[header] + if k == nil { + hr[header] = []HeaderRoute{} + } + patterns := []Pattern{} + for _, m := range match { + patterns = append(patterns, NewPattern(m)) + } + hr[header] = append(hr[header], HeaderRoute{MatchAny: patterns, Middleware: middlewareHandler}) + return hr +} + +func (hr HeaderRouter) RouteDefault(handler func(next http.Handler) http.Handler) HeaderRouter { + hr["*"] = []HeaderRoute{{Middleware: handler}} + return hr +} + +func (hr HeaderRouter) Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if len(hr) == 0 { + // skip if no routes set + next.ServeHTTP(w, r) + } + + // find first matching header route, and continue + for header, matchers := range hr { + headerValue := r.Header.Get(header) + if headerValue == "" { + continue + } + headerValue = strings.ToLower(headerValue) + for _, matcher := range matchers { + if matcher.IsMatch(headerValue) { + matcher.Middleware(next).ServeHTTP(w, r) + return + } + } + } + + // if no match, check for "*" default route + matcher, ok := hr["*"] + if !ok || matcher[0].Middleware == nil { + next.ServeHTTP(w, r) + return + } + matcher[0].Middleware(next).ServeHTTP(w, r) + }) +} + +type HeaderRoute struct { + MatchAny []Pattern + MatchOne Pattern + Middleware func(next http.Handler) http.Handler +} + +func (r HeaderRoute) IsMatch(value string) bool { + if len(r.MatchAny) > 0 { + for _, m := range r.MatchAny { + if m.Match(value) { + return true + } + } + } else if r.MatchOne.Match(value) { + return true + } + return false +} + +type Pattern struct { + prefix string + suffix string + wildcard bool +} + +func NewPattern(value string) Pattern { + p := Pattern{} + if i := strings.IndexByte(value, '*'); i >= 0 { + p.wildcard = true + p.prefix = value[0:i] + p.suffix = value[i+1:] + } else { + p.prefix = value + } + return p +} + +func (p Pattern) Match(v string) bool { + if !p.wildcard { + if p.prefix == v { + return true + } else { + return false + } + } + return len(v) >= len(p.prefix+p.suffix) && strings.HasPrefix(v, p.prefix) && strings.HasSuffix(v, p.suffix) +} diff --git a/vendor/github.com/go-chi/chi/middleware/terminal.go b/vendor/github.com/go-chi/chi/middleware/terminal.go index a5d4241..5ead7b9 100644 --- a/vendor/github.com/go-chi/chi/middleware/terminal.go +++ b/vendor/github.com/go-chi/chi/middleware/terminal.go @@ -32,7 +32,7 @@ var ( reset = []byte{'\033', '[', '0', 'm'} ) -var isTTY bool +var IsTTY bool func init() { // This is sort of cheating: if stdout is a character device, we assume @@ -47,17 +47,17 @@ func init() { fi, err := os.Stdout.Stat() if err == nil { m := os.ModeDevice | os.ModeCharDevice - isTTY = fi.Mode()&m == m + IsTTY = fi.Mode()&m == m } } // colorWrite func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) { - if isTTY && useColor { + if IsTTY && useColor { w.Write(color) } fmt.Fprintf(w, s, args...) - if isTTY && useColor { + if IsTTY && useColor { w.Write(reset) } } diff --git a/vendor/github.com/go-chi/chi/middleware/throttle.go b/vendor/github.com/go-chi/chi/middleware/throttle.go index d935e2c..fdedd3c 100644 --- a/vendor/github.com/go-chi/chi/middleware/throttle.go +++ b/vendor/github.com/go-chi/chi/middleware/throttle.go @@ -2,6 +2,7 @@ package middleware import ( "net/http" + "strconv" "time" ) @@ -15,44 +16,100 @@ var ( defaultBacklogTimeout = time.Second * 60 ) +// ThrottleOpts represents a set of throttling options. +type ThrottleOpts struct { + Limit int + BacklogLimit int + BacklogTimeout time.Duration + RetryAfterFn func(ctxDone bool) time.Duration +} + // Throttle is a middleware that limits number of currently processed requests -// at a time. +// at a time across all users. Note: Throttle is not a rate-limiter per user, +// instead it just puts a ceiling on the number of currentl in-flight requests +// being processed from the point from where the Throttle middleware is mounted. func Throttle(limit int) func(http.Handler) http.Handler { - return ThrottleBacklog(limit, 0, defaultBacklogTimeout) + return ThrottleWithOpts(ThrottleOpts{Limit: limit, BacklogTimeout: defaultBacklogTimeout}) } // ThrottleBacklog is a middleware that limits number of currently processed // requests at a time and provides a backlog for holding a finite number of // pending requests. func ThrottleBacklog(limit int, backlogLimit int, backlogTimeout time.Duration) func(http.Handler) http.Handler { - if limit < 1 { + return ThrottleWithOpts(ThrottleOpts{Limit: limit, BacklogLimit: backlogLimit, BacklogTimeout: backlogTimeout}) +} + +// ThrottleWithOpts is a middleware that limits number of currently processed requests using passed ThrottleOpts. +func ThrottleWithOpts(opts ThrottleOpts) func(http.Handler) http.Handler { + if opts.Limit < 1 { panic("chi/middleware: Throttle expects limit > 0") } - if backlogLimit < 0 { + if opts.BacklogLimit < 0 { panic("chi/middleware: Throttle expects backlogLimit to be positive") } t := throttler{ - tokens: make(chan token, limit), - backlogTokens: make(chan token, limit+backlogLimit), - backlogTimeout: backlogTimeout, + tokens: make(chan token, opts.Limit), + backlogTokens: make(chan token, opts.Limit+opts.BacklogLimit), + backlogTimeout: opts.BacklogTimeout, + retryAfterFn: opts.RetryAfterFn, } // Filling tokens. - for i := 0; i < limit+backlogLimit; i++ { - if i < limit { + for i := 0; i < opts.Limit+opts.BacklogLimit; i++ { + if i < opts.Limit { t.tokens <- token{} } t.backlogTokens <- token{} } - fn := func(h http.Handler) http.Handler { - t.h = h - return &t - } + return func(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + select { + + case <-ctx.Done(): + t.setRetryAfterHeaderIfNeeded(w, true) + http.Error(w, errContextCanceled, http.StatusServiceUnavailable) + return + + case btok := <-t.backlogTokens: + timer := time.NewTimer(t.backlogTimeout) - return fn + defer func() { + t.backlogTokens <- btok + }() + + select { + case <-timer.C: + t.setRetryAfterHeaderIfNeeded(w, false) + http.Error(w, errTimedOut, http.StatusServiceUnavailable) + return + case <-ctx.Done(): + timer.Stop() + t.setRetryAfterHeaderIfNeeded(w, true) + http.Error(w, errContextCanceled, http.StatusServiceUnavailable) + return + case tok := <-t.tokens: + defer func() { + timer.Stop() + t.tokens <- tok + }() + next.ServeHTTP(w, r) + } + return + + default: + t.setRetryAfterHeaderIfNeeded(w, false) + http.Error(w, errCapacityExceeded, http.StatusServiceUnavailable) + return + } + } + + return http.HandlerFunc(fn) + } } // token represents a request that is being processed. @@ -60,42 +117,16 @@ type token struct{} // throttler limits number of currently processed requests at a time. type throttler struct { - h http.Handler tokens chan token backlogTokens chan token backlogTimeout time.Duration + retryAfterFn func(ctxDone bool) time.Duration } -// ServeHTTP is the primary throttler request handler -func (t *throttler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - select { - case <-ctx.Done(): - http.Error(w, errContextCanceled, http.StatusServiceUnavailable) - return - case btok := <-t.backlogTokens: - timer := time.NewTimer(t.backlogTimeout) - - defer func() { - t.backlogTokens <- btok - }() - - select { - case <-timer.C: - http.Error(w, errTimedOut, http.StatusServiceUnavailable) - return - case <-ctx.Done(): - http.Error(w, errContextCanceled, http.StatusServiceUnavailable) - return - case tok := <-t.tokens: - defer func() { - t.tokens <- tok - }() - t.h.ServeHTTP(w, r) - } - return - default: - http.Error(w, errCapacityExceeded, http.StatusServiceUnavailable) +// setRetryAfterHeaderIfNeeded sets Retry-After HTTP header if corresponding retryAfterFn option of throttler is initialized. +func (t throttler) setRetryAfterHeaderIfNeeded(w http.ResponseWriter, ctxDone bool) { + if t.retryAfterFn == nil { return } + w.Header().Set("Retry-After", strconv.Itoa(int(t.retryAfterFn(ctxDone).Seconds()))) } diff --git a/vendor/github.com/go-chi/chi/middleware/wrap_writer.go b/vendor/github.com/go-chi/chi/middleware/wrap_writer.go index 5e5594f..382a523 100644 --- a/vendor/github.com/go-chi/chi/middleware/wrap_writer.go +++ b/vendor/github.com/go-chi/chi/middleware/wrap_writer.go @@ -75,7 +75,7 @@ func (b *basicWriter) WriteHeader(code int) { } func (b *basicWriter) Write(buf []byte) (int, error) { - b.WriteHeader(http.StatusOK) + b.maybeWriteHeader() n, err := b.ResponseWriter.Write(buf) if b.tee != nil { _, err2 := b.tee.Write(buf[:n]) @@ -116,7 +116,6 @@ type flushWriter struct { func (f *flushWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) fl.Flush() } @@ -133,7 +132,6 @@ type httpFancyWriter struct { func (f *httpFancyWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) fl.Flush() } @@ -175,7 +173,6 @@ type http2FancyWriter struct { func (f *http2FancyWriter) Flush() { f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) fl.Flush() } diff --git a/vendor/github.com/go-chi/chi/mux.go b/vendor/github.com/go-chi/chi/mux.go index e553287..52950e9 100644 --- a/vendor/github.com/go-chi/chi/mux.go +++ b/vendor/github.com/go-chi/chi/mux.go @@ -78,7 +78,11 @@ func (mx *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { rctx = mx.pool.Get().(*Context) rctx.Reset() rctx.Routes = mx + + // NOTE: r.WithContext() causes 2 allocations and context.WithValue() causes 1 allocation r = r.WithContext(context.WithValue(r.Context(), RouteCtxKey, rctx)) + + // Serve the request and once its done, put the request context back in the sync pool mx.handler.ServeHTTP(w, r) mx.pool.Put(rctx) } @@ -220,7 +224,7 @@ func (mx *Mux) MethodNotAllowed(handlerFn http.HandlerFunc) { // With adds inline middlewares for an endpoint handler. func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { - // Similarly as in handle(), we must build the mux handler once further + // Similarly as in handle(), we must build the mux handler once additional // middleware registration isn't allowed for this stack, like now. if !mx.inline && mx.handler == nil { mx.buildRouteHandler() @@ -234,7 +238,10 @@ func (mx *Mux) With(middlewares ...func(http.Handler) http.Handler) Router { } mws = append(mws, middlewares...) - im := &Mux{pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws} + im := &Mux{ + pool: mx.pool, inline: true, parent: mx, tree: mx.tree, middlewares: mws, + notFoundHandler: mx.notFoundHandler, methodNotAllowedHandler: mx.methodNotAllowedHandler, + } return im } @@ -285,7 +292,6 @@ func (mx *Mux) Mount(pattern string, handler http.Handler) { subr.MethodNotAllowed(mx.methodNotAllowedHandler) } - // Wrap the sub-router in a handlerFunc to scope the request path for routing. mountHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { rctx := RouteContext(r.Context()) rctx.RoutePath = mx.nextRoutePath(rctx) @@ -376,7 +382,7 @@ func (mx *Mux) handle(method methodTyp, pattern string, handler http.Handler) *n panic(fmt.Sprintf("chi: routing pattern must begin with '/' in '%s'", pattern)) } - // Build the final routing handler for this Mux. + // Build the computed routing handler for this routing pattern. if !mx.inline && mx.handler == nil { mx.buildRouteHandler() } @@ -436,7 +442,7 @@ func (mx *Mux) nextRoutePath(rctx *Context) string { routePath := "/" nx := len(rctx.routeParams.Keys) - 1 // index of last param in list if nx >= 0 && rctx.routeParams.Keys[nx] == "*" && len(rctx.routeParams.Values) > nx { - routePath += rctx.routeParams.Values[nx] + routePath = "/" + rctx.routeParams.Values[nx] } return routePath } diff --git a/vendor/github.com/go-chi/chi/tree.go b/vendor/github.com/go-chi/chi/tree.go index 8a044f3..59b5b5f 100644 --- a/vendor/github.com/go-chi/chi/tree.go +++ b/vendor/github.com/go-chi/chi/tree.go @@ -331,7 +331,7 @@ func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { func (n *node) setEndpoint(method methodTyp, handler http.Handler, pattern string) { // Set the handler for the method type on the node if n.endpoints == nil { - n.endpoints = make(endpoints, 0) + n.endpoints = make(endpoints) } paramKeys := patParamKeys(pattern) @@ -433,7 +433,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { } if ntyp == ntRegexp && xn.rex != nil { - if xn.rex.Match([]byte(xsearch[:p])) == false { + if !xn.rex.Match([]byte(xsearch[:p])) { continue } } else if strings.IndexByte(xsearch[:p], '/') != -1 { @@ -441,11 +441,37 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { continue } + prevlen := len(rctx.routeParams.Values) rctx.routeParams.Values = append(rctx.routeParams.Values, xsearch[:p]) xsearch = xsearch[p:] - break + + if len(xsearch) == 0 { + if xn.isLeaf() { + h := xn.endpoints[method] + if h != nil && h.handler != nil { + rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) + return xn + } + + // flag that the routing context found a route, but not a corresponding + // supported method + rctx.methodNotAllowed = true + } + } + + // recursively find the next node on this branch + fin := xn.findRoute(rctx, method, xsearch) + if fin != nil { + return fin + } + + // not found on this branch, reset vars + rctx.routeParams.Values = rctx.routeParams.Values[:prevlen] + xsearch = search } + rctx.routeParams.Values = append(rctx.routeParams.Values, "") + default: // catch-all nodes rctx.routeParams.Values = append(rctx.routeParams.Values, search) @@ -460,7 +486,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { // did we find it yet? if len(xsearch) == 0 { if xn.isLeaf() { - h, _ := xn.endpoints[method] + h := xn.endpoints[method] if h != nil && h.handler != nil { rctx.routeParams.Keys = append(rctx.routeParams.Keys, h.paramKeys...) return xn @@ -518,15 +544,6 @@ func (n *node) findEdge(ntyp nodeTyp, label byte) *node { } } -func (n *node) isEmpty() bool { - for _, nds := range n.children { - if len(nds) > 0 { - return false - } - } - return true -} - func (n *node) isLeaf() bool { return n.endpoints != nil } @@ -582,7 +599,7 @@ func (n *node) routes() []Route { } // Group methodHandlers by unique patterns - pats := make(map[string]endpoints, 0) + pats := make(map[string]endpoints) for mt, h := range eps { if h.pattern == "" { @@ -597,7 +614,7 @@ func (n *node) routes() []Route { } for p, mh := range pats { - hs := make(map[string]http.Handler, 0) + hs := make(map[string]http.Handler) if mh[mALL] != nil && mh[mALL].handler != nil { hs["*"] = mh[mALL].handler } @@ -698,7 +715,7 @@ func patNextSegment(pattern string) (nodeTyp, string, string, byte, int, int) { rexpat = "^" + rexpat } if rexpat[len(rexpat)-1] != '$' { - rexpat = rexpat + "$" + rexpat += "$" } } @@ -795,6 +812,7 @@ func (ns nodes) findEdge(label byte) *node { } // Route describes the details of a routing handler. +// Handlers map key is an HTTP method type Route struct { Pattern string Handlers map[string]http.Handler @@ -829,6 +847,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H } fullRoute := parentRoute + route.Pattern + fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) if chain, ok := handler.(*ChainHandler); ok { if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { diff --git a/vendor/modules.txt b/vendor/modules.txt index a2a979b..f35d142 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -53,7 +53,7 @@ github.com/fatih/color # github.com/fsouza/go-dockerclient v1.12.0 ## explicit; go 1.22 github.com/fsouza/go-dockerclient -# github.com/go-chi/chi v4.0.2+incompatible +# github.com/go-chi/chi v4.1.2+incompatible ## explicit github.com/go-chi/chi github.com/go-chi/chi/middleware