From dbd6fcd2d7a096c8f540db250557baf5fad96ac2 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sat, 2 May 2020 17:46:17 +0300 Subject: [PATCH] add Context.SendFileWithRate, ServeFileWithRate and ServeContentWithRate as requested at: https://github.com/kataras/iris/issues/1493 Former-commit-id: 7783fde04b4247056e6309e7ec1df27f027dc655 --- HISTORY.md | 44 ++-- _examples/README.md | 4 +- .../file-server/send-files/files/first.zip | Bin 35369 -> 2901 bytes _examples/file-server/send-files/main.go | 23 +- .../http_responsewriter/write-gzip/main.go | 3 + context/context.go | 212 +++++++++++++----- iris.go | 11 + middleware/rate/rate.go | 31 +-- 8 files changed, 223 insertions(+), 105 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 0d78cb1a..6b81232e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -394,31 +394,33 @@ Other Improvements: New Context Methods: -- `context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2 -- `context.IsGRPC() bool` reports whether the request came from a gRPC client -- `context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too -- `context.StopWithStatus(int)` stops the handlers chain and writes the status code -- `context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message -- `context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message -- `context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response -- `context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response -- `context.Protobuf(proto.Message)` sends protobuf to the client -- `context.MsgPack(interface{})` sends msgpack format data to the client -- `context.ReadProtobuf(ptr)` binds request body to a proto message -- `context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct -- `context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type -- `context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too) -- `context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead -- `context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(context)` -- `context.Controller() reflect.Value` returns the current MVC Controller value. +- `Context.ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` methods to throttle the "download" speed of the client. +- `Context.IsHTTP2() bool` reports whether the protocol version for incoming request was HTTP/2 +- `Context.IsGRPC() bool` reports whether the request came from a gRPC client +- `Context.UpsertCookie(*http.Cookie, cookieOptions ...context.CookieOption)` upserts a cookie, fixes [#1485](https://github.com/kataras/iris/issues/1485) too +- `Context.StopWithStatus(int)` stops the handlers chain and writes the status code +- `Context.StopWithText(int, string)` stops the handlers chain, writes thre status code and a plain text message +- `Context.StopWithError(int, error)` stops the handlers chain, writes thre status code and the error's message +- `Context.StopWithJSON(int, interface{})` stops the handlers chain, writes the status code and sends a JSON response +- `Context.StopWithProblem(int, iris.Problem)` stops the handlers, writes the status code and sends an `application/problem+json` response +- `Context.Protobuf(proto.Message)` sends protobuf to the client +- `Context.MsgPack(interface{})` sends msgpack format data to the client +- `Context.ReadProtobuf(ptr)` binds request body to a proto message +- `Context.ReadMsgPack(ptr)` binds request body of a msgpack format to a struct +- `Context.ReadBody(ptr)` binds the request body to the "ptr" depending on the request's Method and Content-Type +- `Context.SetSameSite(http.SameSite)` to set cookie "SameSite" option (respectful by sessions too) +- `Context.Defer(Handler)` works like `Party.Done` but for the request life-cycle instead +- `Context.ReflectValue() []reflect.Value` stores and returns the `[]reflect.ValueOf(ctx)` +- `Context.Controller() reflect.Value` returns the current MVC Controller value. Breaking Changes: -Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See . - +- Change the MIME type of `Javascript .js` and `JSONP` as the HTML specification now recommends to `"text/javascript"` instead of the obselete `"application/javascript"`. This change was pushed to the `Go` language itself as well. See . +- Remove the last input argument of `enableGzipCompression` in `Context.ServeContent`, `ServeFile` methods. This was deprecated a few versions ago. A middleware (`app.Use(iris.Gzip)`) or a prior call to `Context.Gzip(true)` will enable gzip compression. Also these two methods and `Context.SendFile` one now support `Content-Range` and `Accept-Ranges` correctly out of the box (`net/http` had a bug, which is now fixed). +- `Context.ServeContent` no longer returns an error, see `ServeContentWithRate`, `ServeFileWithRate` and `SendFileWithRate` new methods too. - `route.Trace() string` changed to `route.Trace(w io.Writer)`, to achieve the same result just pass a `bytes.Buffer` -- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)) -- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)) +- `var mvc.AutoBinding` removed as the default behavior now resolves such dependencies automatically (see [[FEATURE REQUEST] MVC serving gRPC-compatible controller](https://github.com/kataras/iris/issues/1449)). +- `mvc#Application.SortByNumMethods()` removed as the default behavior now binds the "thinnest" empty `interface{}` automatically (see [MVC: service injecting fails](https://github.com/kataras/iris/issues/1343)). - `mvc#BeforeActivation.Dependencies().Add` should be replaced with `mvc#BeforeActivation.Dependencies().Register` instead - **REMOVE** the `kataras/iris/v12/typescript` package in favor of the new [iris-cli](https://github.com/kataras/iris-cli). Also, the alm typescript online editor was removed as it is deprecated by its author, please consider using the [designtsx](https://designtsx.com/) instead. diff --git a/_examples/README.md b/_examples/README.md index 723f27dd..cae94200 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -237,7 +237,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [Basic](file-server/basic/main.go) - [Embedding Files Into App Executable File](file-server/embedding-files-into-app/main.go) - [Embedding Gziped Files Into App Executable File](file-server/embedding-gziped-files-into-app/main.go) -- [Send/Force-Download Files](file-server/send-files/main.go) +- [Send/Force-Download Files](file-server/send-files/main.go) **UPDATED** - Single Page Applications * [single Page Application](file-server/single-page-application/basic/main.go) * [embedded Single Page Application](file-server/single-page-application/embedded-single-page-application/main.go) @@ -246,7 +246,7 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her ### How to Read from `context.Request() *http.Request` - [Read JSON](http_request/read-json/main.go) - * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDaTE** + * [Struct Validation](http_request/read-json-struct-validation/main.go) **UPDATED** - [Read XML](http_request/read-xml/main.go) - [Read MsgPack](http_request/read-msgpack/main.go) **NEW** - [Read YAML](http_request/read-yaml/main.go) diff --git a/_examples/file-server/send-files/files/first.zip b/_examples/file-server/send-files/files/first.zip index 431fa5fdf52a1b3d2b2918e34f8ec7b232533d1b..c98f5ac8f564fa8b28df5f3efa10e5cb7e121ecb 100644 GIT binary patch literal 2901 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%0DO?_N7LMj%b7Ur^=W^ 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 d0001oi2wi*0C{Of diff --git a/_examples/file-server/send-files/main.go b/_examples/file-server/send-files/main.go index bc03b0a1..3e5c46a1 100644 --- a/_examples/file-server/send-files/main.go +++ b/_examples/file-server/send-files/main.go @@ -6,11 +6,26 @@ import ( func main() { app := iris.New() + app.Logger().SetLevel("debug") - app.Get("/", func(ctx iris.Context) { - file := "./files/first.zip" - ctx.SendFile(file, "c.zip") - }) + app.Get("/", download) + app.Get("/download", downloadWithRateLimit) app.Listen(":8080") } + +func download(ctx iris.Context) { + src := "./files/first.zip" + ctx.SendFile(src, "client.zip") +} + +func downloadWithRateLimit(ctx iris.Context) { + // REPLACE THAT WITH A BIG LOCAL FILE OF YOUR OWN. + src := "./files/first.zip" + dest := "" /* optionally, keep it empty to resolve the filename based on the "src" */ + + // Limit download speed to ~50Kb/s with a burst of 100KB. + limit := 50.0 * iris.KB + burst := 100 * iris.KB + ctx.SendFileWithRate(src, dest, limit, burst) +} diff --git a/_examples/http_responsewriter/write-gzip/main.go b/_examples/http_responsewriter/write-gzip/main.go index f1d6369d..080e4164 100644 --- a/_examples/http_responsewriter/write-gzip/main.go +++ b/_examples/http_responsewriter/write-gzip/main.go @@ -4,6 +4,9 @@ import "github.com/kataras/iris/v12" func main() { app := iris.New() + // app.Use(iris.Gzip) + // func(ctx iris.Context) { ctx.Gzip(true/false)} + // OR: app.Get("/", func(ctx iris.Context) { ctx.WriteGzip([]byte("Hello World!")) ctx.Header("X-Custom", diff --git a/context/context.go b/context/context.go index 18e57cf5..2eff50dc 100644 --- a/context/context.go +++ b/context/context.go @@ -2,6 +2,7 @@ package context import ( "bytes" + stdContext "context" "encoding/json" "encoding/xml" "errors" @@ -35,6 +36,7 @@ import ( jsoniter "github.com/json-iterator/go" "github.com/microcosm-cc/bluemonday" "github.com/vmihailenco/msgpack/v5" + "golang.org/x/time/rate" "gopkg.in/yaml.v3" ) @@ -916,31 +918,57 @@ type Context interface { // | Serve files | // +------------------------------------------------------------+ - // ServeContent serves content, headers are autoset - // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) + // ServeContent replies to the request using the content in the + // provided ReadSeeker. The main benefit of ServeContent over io.Copy + // is that it handles Range requests properly, sets the MIME type, and + // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, + // and If-Range requests. // + // If the response's Content-Type header is not set, ServeContent + // first tries to deduce the type from name's file extension. // - // You can define your own "Content-Type" with `context#ContentType`, before this function call. + // The name is otherwise unused; in particular it can be empty and is + // never sent in the response. // - // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `HandleDir` instead. - ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error - // ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead) - // receives two parameters - // filename/path (string) - // gzipCompression (bool) + // If modtime is not the zero time or Unix epoch, ServeContent + // includes it in a Last-Modified header in the response. If the + // request includes an If-Modified-Since header, ServeContent uses + // modtime to decide whether the content needs to be sent at all. // - // You can define your own "Content-Type" with `context#ContentType`, before this function call. + // The content's Seek method must work: ServeContent uses + // a seek to the end of the content to determine its size. // - // This function doesn't support resuming (by range), - // use ctx.SendFile or router's `HandleDir` instead. + // If the caller has set w's ETag header formatted per RFC 7232, section 2.3, + // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. // - // Use it when you want to serve dynamic files to the client. - ServeFile(filename string, gzipCompression bool) error - // SendFile sends file for force-download to the client + // Note that *os.File implements the io.ReadSeeker interface. + // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. + ServeContent(content io.ReadSeeker, filename string, modtime time.Time) + // ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading + // and though writing the "content" to the client. + ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) + // ServeFile replies to the request with the contents of the named + // file or directory. // - // Use this instead of ServeFile to 'force-download' bigger files to the client. + // If the provided file or directory name is a relative path, it is + // interpreted relative to the current directory and may ascend to + // parent directories. If the provided name is constructed from user + // input, it should be sanitized before calling `ServeFile`. + // + // Use it when you want to serve assets like css and javascript files. + // If client should confirm and save the file use the `SendFile` instead. + // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. + ServeFile(filename string) error + // ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading + // and though writing the file to the client. + ServeFileWithRate(filename string, limit float64, burst int) error + // SendFile sends a file as an attachment, that is downloaded and saved locally from client. + // Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. + // Use `ServeFile` if a file should be served as a page asset instead. SendFile(filename string, destinationName string) error + // SendFileWithRate same as `SendFile` but it can throttle the speed of reading + // and though writing the file to the client. + SendFileWithRate(src, destName string, limit float64, burst int) error // +------------------------------------------------------------+ // | Cookies | @@ -4544,65 +4572,135 @@ func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder { // | Serve files | // +------------------------------------------------------------+ -// ServeContent serves content, headers are autoset -// receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) +// ServeContent replies to the request using the content in the +// provided ReadSeeker. The main benefit of ServeContent over io.Copy +// is that it handles Range requests properly, sets the MIME type, and +// handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since, +// and If-Range requests. // -// You can define your own "Content-Type" header also, after this function call -// Doesn't implements resuming (by range), use ctx.SendFile instead -func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error { - if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil { - ctx.WriteNotModified() - return nil +// If the response's Content-Type header is not set, ServeContent +// first tries to deduce the type from name's file extension. +// +// The name is otherwise unused; in particular it can be empty and is +// never sent in the response. +// +// If modtime is not the zero time or Unix epoch, ServeContent +// includes it in a Last-Modified header in the response. If the +// request includes an If-Modified-Since header, ServeContent uses +// modtime to decide whether the content needs to be sent at all. +// +// The content's Seek method must work: ServeContent uses +// a seek to the end of the content to determine its size. +// +// If the caller has set w's ETag header formatted per RFC 7232, section 2.3, +// ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range. +// +// Note that *os.File implements the io.ReadSeeker interface. +// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. +func (ctx *context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) { + ctx.ServeContentWithRate(content, filename, modtime, 0, 0) +} + +// rateReadSeeker is a io.ReadSeeker that is rate limited by +// the given token bucket. Each token in the bucket +// represents one byte. See "golang.org/x/time/rate" package. +type rateReadSeeker struct { + io.ReadSeeker + ctx stdContext.Context + limiter *rate.Limiter +} + +func (rs *rateReadSeeker) Read(buf []byte) (int, error) { + n, err := rs.ReadSeeker.Read(buf) + if n <= 0 { + return n, err + } + rs.limiter.WaitN(rs.ctx, n) + return n, err +} + +// ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading +// and though writing the "content" to the client. +func (ctx *context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) { + if limit > 0 { + content = &rateReadSeeker{ + ReadSeeker: content, + ctx: ctx.request.Context(), + limiter: rate.NewLimiter(rate.Limit(limit), burst), + } } if ctx.GetContentType() == "" { ctx.ContentType(filename) } - ctx.SetLastModified(modtime) - var out io.Writer - if gzipCompression && ctx.ClientSupportsGzip() { - AddGzipHeaders(ctx.writer) - - gzipWriter := acquireGzipWriter(ctx.writer) - defer releaseGzipWriter(gzipWriter) - out = gzipWriter - } else { - out = ctx.writer - } - _, err := io.Copy(out, content) - return err ///TODO: add an int64 as return value for the content length written like other writers or let it as it's in order to keep the stable api? + http.ServeContent(ctx.writer, ctx.request, filename, modtime, content) } -// ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) -// receives two parameters -// filename/path (string) -// gzipCompression (bool) +// ServeFile replies to the request with the contents of the named +// file or directory. // -// You can define your own "Content-Type" header also, after this function call -// This function doesn't implement resuming (by range), use ctx.SendFile instead +// If the provided file or directory name is a relative path, it is +// interpreted relative to the current directory and may ascend to +// parent directories. If the provided name is constructed from user +// input, it should be sanitized before calling `ServeFile`. // -// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile. -func (ctx *context) ServeFile(filename string, gzipCompression bool) error { +// Use it when you want to serve assets like css and javascript files. +// If client should confirm and save the file use the `SendFile` instead. +// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. +func (ctx *context) ServeFile(filename string) error { + return ctx.ServeFileWithRate(filename, 0, 0) +} + +// ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading +// and though writing the file to the client. +func (ctx *context) ServeFileWithRate(filename string, limit float64, burst int) error { f, err := os.Open(filename) if err != nil { - return fmt.Errorf("%d", http.StatusNotFound) + ctx.StatusCode(http.StatusNotFound) + return err } defer f.Close() - fi, _ := f.Stat() - if fi.IsDir() { - return ctx.ServeFile(path.Join(filename, "index.html"), gzipCompression) + + st, err := f.Stat() + if err != nil { + code := http.StatusInternalServerError + if os.IsNotExist(err) { + code = http.StatusNotFound + } + + if os.IsPermission(err) { + code = http.StatusForbidden + } + + ctx.StatusCode(code) + return err } - return ctx.ServeContent(f, fi.Name(), fi.ModTime(), gzipCompression) + if st.IsDir() { + return ctx.ServeFile(path.Join(filename, "index.html")) + } + + ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst) + return nil } -// SendFile sends file for force-download to the client -// -// Use this instead of ServeFile to 'force-download' bigger files to the client. -func (ctx *context) SendFile(filename string, destinationName string) error { - ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destinationName) - return ctx.ServeFile(filename, false) +// SendFile sends a file as an attachment, that is downloaded and saved locally from client. +// Note that gzip compression can be registered through `ctx.Gzip(true)` or `app.Use(iris.Gzip)`. +// Use `ServeFile` if a file should be served as a page asset instead. +func (ctx *context) SendFile(src string, destName string) error { + return ctx.SendFileWithRate(src, destName, 0, 0) +} + +// SendFileWithRate same as `SendFile` but it can throttle the speed of reading +// and though writing the file to the client. +func (ctx *context) SendFileWithRate(src, destName string, limit float64, burst int) error { + if destName == "" { + destName = filepath.Base(src) + } + + ctx.writer.Header().Set(ContentDispositionHeaderKey, "attachment;filename="+destName) + return ctx.ServeFileWithRate(src, limit, burst) } // +------------------------------------------------------------+ diff --git a/iris.go b/iris.go index a4fa5fda..3c1823f6 100644 --- a/iris.go +++ b/iris.go @@ -591,6 +591,17 @@ const ( ReferrerGoogleAdwords = context.ReferrerGoogleAdwords ) +// Byte unit helpers. +const ( + B = 1 << (10 * iota) + KB + MB + GB + TB + PB + EB +) + // ConfigureHost accepts one or more `host#Configuration`, these configurators functions // can access the host created by `app.Run`, // they're being executed when application is ready to being served to the public. diff --git a/middleware/rate/rate.go b/middleware/rate/rate.go index 40fffb86..ce29ba9f 100644 --- a/middleware/rate/rate.go +++ b/middleware/rate/rate.go @@ -50,10 +50,11 @@ type ( Limiter struct { clientDataFunc func(ctx context.Context) interface{} // fill the Client's Data field. exceedHandler context.Handler // when too many requests. + limit rate.Limit + burstSize int clients map[string]*Client mu sync.RWMutex // mutex for clients. - pool *sync.Pool // object pool for clients. } Client struct { @@ -68,14 +69,10 @@ type ( const Inf = math.MaxFloat64 func Limit(limit float64, burst int, options ...Option) context.Handler { - rateLimit := rate.Limit(limit) - l := &Limiter{ - clients: make(map[string]*Client), - pool: &sync.Pool{New: func() interface{} { - return &Client{limiter: rate.NewLimiter(rateLimit, burst)} - }}, - + clients: make(map[string]*Client), + limit: rate.Limit(limit), + burstSize: burst, exceedHandler: func(ctx context.Context) { ctx.StopWithStatus(429) // Too Many Requests. }, @@ -88,21 +85,10 @@ func Limit(limit float64, burst int, options ...Option) context.Handler { return l.serveHTTP } -func (l *Limiter) acquire() *Client { - return l.pool.Get().(*Client) -} - -func (l *Limiter) release(client *Client) { - client.IP = "" - client.Data = nil - l.pool.Put(client) -} - func (l *Limiter) Purge(condition func(*Client) bool) { l.mu.Lock() for ip, client := range l.clients { if condition(client) { - l.release(client) delete(l.clients, ip) } } @@ -116,12 +102,15 @@ func (l *Limiter) serveHTTP(ctx context.Context) { l.mu.RUnlock() if !ok { - client = l.acquire() - client.IP = ip + client = &Client{ + limiter: rate.NewLimiter(l.limit, l.burstSize), + IP: ip, + } if l.clientDataFunc != nil { client.Data = l.clientDataFunc(ctx) } + // if l.store(ctx, client) { // ^ no, let's keep it simple. l.mu.Lock()