From 1079bb8f8bbf5fe636c966172b10f99e956f2692 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Thu, 28 May 2020 19:29:14 +0300 Subject: [PATCH] add a new 'Context.GzipReader(bool) method and 'iris.GzipReader' middleware as requested at #1528 Former-commit-id: 7665545069bf1784d17a9db1e5f9f5f8df4b0c43 --- HISTORY.md | 5 +- _examples/README.md | 1 + _examples/http_request/read-body/main.go | 3 + _examples/http_request/read-gzip/main.go | 44 +++++++++ _examples/http_request/read-gzip/main_test.go | 38 ++++++++ context/context.go | 89 ++++++++++++++++++- go.mod | 6 +- iris.go | 10 +++ 8 files changed, 191 insertions(+), 5 deletions(-) create mode 100644 _examples/http_request/read-gzip/main.go create mode 100644 _examples/http_request/read-gzip/main_test.go diff --git a/HISTORY.md b/HISTORY.md index dd3d2023..86a57125 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -371,7 +371,9 @@ Other Improvements: ![DBUG routes](https://iris-go.com/images/v12.2.0-dbug2.png?v=0) -- New builtin [JWT](https://github.com/kataras/iris/tree/master/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary. +- New builtin [requestid](https://github.com/kataras/iris/tree/master/middleware/requestid) middleware. + +- New builtin [JWT](https://github.com/kataras/iris/tree/master/middleware/jwt) middleware based on [square/go-jose](https://github.com/square/go-jose) featured with optional encryption to set claims with sensitive data when necessary. - `Context.ReadForm` now can return an `iris.ErrEmptyForm` instead of `nil` when the new `Configuration.FireEmptyFormError` is true (or `iris.WithEmptyFormError`) on missing form body to read from. @@ -413,6 +415,7 @@ New Package-level Variables: New Context Methods: +- `Context.GzipReader(enable bool)` method and `iris.GzipReader` middleware to enable future request read body calls to decompress data using gzip, [example](_examples/http_request/read-gzip). - `Context.RegisterDependency(v interface{})` and `Context.RemoveDependency(typ reflect.Type)` to register/remove struct dependencies on serve-time through a middleware. - `Context.SetID(id interface{})` and `Context.GetID() interface{}` added to register a custom unique indetifier to the Context, if necessary. - `Context.GetDomain() string` returns the domain. diff --git a/_examples/README.md b/_examples/README.md index c12ec938..3f6c3386 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -112,6 +112,7 @@ * [Bind Custom per type](http_request/read-custom-per-type/main.go) * [Bind Custom via Unmarshaler](http_request/read-custom-via-unmarshaler/main.go) * [Bind Many times](http_request/read-many/main.go) + * [Read/Bind Gzip compressed data](http_request/read-gzip/main.go) * [Upload/Read File](http_request/upload-file/main.go) * [Upload multiple Files](http_request/upload-files/main.go) * [Extract Referrer](http_request/extract-referer/main.go) diff --git a/_examples/http_request/read-body/main.go b/_examples/http_request/read-body/main.go index 1fdcd446..38b8aac4 100644 --- a/_examples/http_request/read-body/main.go +++ b/_examples/http_request/read-body/main.go @@ -12,6 +12,9 @@ func main() { func newApp() *iris.Application { app := iris.New() + // To automatically decompress using gzip: + // app.Use(iris.GzipReader) + app.Use(setAllowedResponses) app.Post("/", readBody) diff --git a/_examples/http_request/read-gzip/main.go b/_examples/http_request/read-gzip/main.go new file mode 100644 index 00000000..def777b8 --- /dev/null +++ b/_examples/http_request/read-gzip/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "github.com/kataras/iris/v12" +) + +func main() { + app := newApp() + app.Logger().SetLevel("debug") + + app.Listen(":8080") +} + +type payload struct { + Message string `json:"message"` +} + +func newApp() *iris.Application { + app := iris.New() + + // GzipReader is a middleware which enables gzip decompression, + // when client sends gzip compressed data. + // + // A shortcut of: + // func(ctx iris.Context) { + // ctx.GzipReader(true) + // ctx.Next() + // } + app.Use(iris.GzipReader) + + app.Post("/", func(ctx iris.Context) { + // Bind incoming gzip compressed JSON to "p". + var p payload + if err := ctx.ReadJSON(&p); err != nil { + ctx.StopWithError(iris.StatusBadRequest, err) + return + } + + // Send back the message as plain text. + ctx.WriteString(p.Message) + }) + + return app +} diff --git a/_examples/http_request/read-gzip/main_test.go b/_examples/http_request/read-gzip/main_test.go new file mode 100644 index 00000000..f75206ea --- /dev/null +++ b/_examples/http_request/read-gzip/main_test.go @@ -0,0 +1,38 @@ +package main + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "testing" + + "github.com/kataras/iris/v12/httptest" +) + +func TestGzipReader(t *testing.T) { + app := newApp() + + expected := payload{Message: "test"} + b, err := json.Marshal(expected) + if err != nil { + t.Fatal(err) + } + + buf := new(bytes.Buffer) + w := gzip.NewWriter(buf) + _, err = w.Write(b) + if err != nil { + t.Fatal(err) + } + err = w.Close() + if err != nil { + t.Fatal(err) + } + + e := httptest.New(t, app) + // send gzip compressed. + e.POST("/").WithHeader("Content-Encoding", "gzip").WithHeader("Content-Type", "application/json"). + WithBytes(buf.Bytes()).Expect().Status(httptest.StatusOK).Body().Equal(expected.Message) + // raw. + e.POST("/").WithJSON(expected).Expect().Status(httptest.StatusOK).Body().Equal(expected.Message) +} diff --git a/context/context.go b/context/context.go index 122be22e..ddb650a4 100644 --- a/context/context.go +++ b/context/context.go @@ -34,6 +34,7 @@ import ( "github.com/iris-contrib/blackfriday" "github.com/iris-contrib/schema" jsoniter "github.com/json-iterator/go" + "github.com/klauspost/compress/gzip" "github.com/microcosm-cc/bluemonday" "github.com/vmihailenco/msgpack/v5" "golang.org/x/net/publicsuffix" @@ -789,6 +790,24 @@ type Context interface { // supports gzip compression, so the following response data will // be sent as compressed gzip data to the client. Gzip(enable bool) + // GzipReader accepts a boolean, which, if set to true + // it wraps the request body reader with a gzip reader one (decompress request data on read). + // If the "enable" input argument is false then the request body will reset to the default one. + // + // Useful when incoming request data are gzip compressed. + // All future calls of `ctx.GetBody/ReadXXX/UnmarshalBody` methods will respect this option. + // + // Usage: + // app.Use(func(ctx iris.Context){ + // ctx.GzipReader(true) + // ctx.Next() + // }) + // + // If a client request's body is not gzip compressed then + // it returns with a `ErrGzipNotSupported` error, which can be safety ignored. + // + // See `GzipReader` package-level middleware too. + GzipReader(enable bool) error // +------------------------------------------------------------+ // | Rich Body Content Writers/Renderers | @@ -1197,6 +1216,18 @@ var Gzip = func(ctx Context) { ctx.Next() } +// GzipReader is a middleware which enables gzip decompression, +// when client sends gzip compressed data. +// +// Similar to: func(ctx iris.Context) { +// ctx.GzipReader(true) +// ctx.Next() +// } +var GzipReader = func(ctx Context) { + ctx.GzipReader(true) + ctx.Next() +} + // Map is just a type alias of the map[string]interface{} type. type Map = map[string]interface{} @@ -3256,7 +3287,7 @@ func (ctx *context) ClientSupportsGzip() bool { return false } -// ErrGzipNotSupported may be returned from `WriteGzip` methods if +// ErrGzipNotSupported may be returned from `WriteGzip` and `GzipReader` methods if // the client does not support the "gzip" compression. var ErrGzipNotSupported = errors.New("client does not support gzip compression") @@ -3319,6 +3350,62 @@ func (ctx *context) Gzip(enable bool) { } } +type gzipReadCloser struct { + requestReader io.ReadCloser + gzipReader io.ReadCloser +} + +func (rc *gzipReadCloser) Close() error { + rc.gzipReader.Close() + return rc.requestReader.Close() +} + +func (rc *gzipReadCloser) Read(p []byte) (n int, err error) { + return rc.gzipReader.Read(p) +} + +const gzipEncodingHeaderValue = "gzip" + +// GzipReader accepts a boolean, which, if set to true +// it wraps the request body reader with a gzip reader one (decompress request data on read).. +// If the "enable" input argument is false then the request body will reset to the default one. +// +// Useful when incoming request data are gzip compressed. +// All future calls of `ctx.GetBody/ReadXXX/UnmarshalBody` methods will respect this option. +// +// Usage: +// app.Use(func(ctx iris.Context){ +// ctx.GzipReader(true) +// ctx.Next() +// }) +// +// If a client request's body is not gzip compressed then +// it returns with a `ErrGzipNotSupported` error, which can be safety ignored. +// +// See `GzipReader` package-level middleware too. +func (ctx *context) GzipReader(enable bool) error { + if enable { + if ctx.GetHeader(ContentEncodingHeaderKey) == gzipEncodingHeaderValue { + reader, err := gzip.NewReader(ctx.request.Body) + if err != nil { + return err + } + + // Wrap the reader so on Close it will close both request body and gzip reader. + ctx.request.Body = &gzipReadCloser{requestReader: ctx.request.Body, gzipReader: reader} + return nil + } + + return ErrGzipNotSupported + } + + if gzipReader, ok := ctx.request.Body.(*gzipReadCloser); ok { + ctx.request.Body = gzipReader.requestReader + } + + return nil +} + // +------------------------------------------------------------+ // | Rich Body Content Writers/Renderers | // +------------------------------------------------------------+ diff --git a/go.mod b/go.mod index 1d80c7f5..d17c918d 100644 --- a/go.mod +++ b/go.mod @@ -24,8 +24,8 @@ require ( github.com/kataras/neffos v0.0.16 github.com/kataras/pio v0.0.6 github.com/kataras/sitemap v0.0.5 - github.com/klauspost/compress v1.10.5 - github.com/mediocregopher/radix/v3 v3.5.0 + github.com/klauspost/compress v1.10.6 + github.com/mediocregopher/radix/v3 v3.5.1 github.com/microcosm-cc/bluemonday v1.0.2 github.com/ryanuber/columnize v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible @@ -35,6 +35,6 @@ require ( golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 golang.org/x/text v0.3.2 golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 - gopkg.in/ini.v1 v1.56.0 + gopkg.in/ini.v1 v1.57.0 gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 ) diff --git a/iris.go b/iris.go index 619bde6e..78a32722 100644 --- a/iris.go +++ b/iris.go @@ -453,6 +453,16 @@ var ( // // A shortcut for the `context#Gzip`. Gzip = context.Gzip + // GzipReader is a middleware which enables gzip decompression, + // when client sends gzip compressed data. + // + // Similar to: func(ctx iris.Context) { + // ctx.GzipReader(true) + // ctx.Next() + // } + // + // A shortcut for the `context#GzipReader`. + GzipReader = context.GzipReader // FromStd converts native http.Handler, http.HandlerFunc & func(w, r, next) to context.Handler. // // Supported form types: