mirror of https://github.com/grafana/grafana
Datasources: Introduce `response_limit` for datasource responses (#38962)
* Introduce response_limit for datasource responses * Fix lint * Fix tests * Add case where limit <= 0 - added parametrized tests * Add max_bytes_reader.go * Use new httpclient.MaxBytesReader instead of net/http one * Fixes according to reviewer's comments * Add tests for max_bytes_reader * Add small piece in configuration.md * Further fixes according to reviewer's comments * Fix linting - fix testpull/39061/head^2
parent
5fcc9fe193
commit
ba9d5540b8
@ -0,0 +1,28 @@ |
||||
package httpclientprovider |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
"github.com/grafana/grafana/pkg/infra/httpclient" |
||||
) |
||||
|
||||
// ResponseLimitMiddlewareName is the middleware name used by ResponseLimitMiddleware.
|
||||
const ResponseLimitMiddlewareName = "response-limit" |
||||
|
||||
func ResponseLimitMiddleware(limit int64) sdkhttpclient.Middleware { |
||||
return sdkhttpclient.NamedMiddlewareFunc(ResponseLimitMiddlewareName, func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { |
||||
if limit <= 0 { |
||||
return next |
||||
} |
||||
return sdkhttpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { |
||||
res, err := next.RoundTrip(req) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
res.Body = httpclient.MaxBytesReader(res.Body, limit) |
||||
return res, nil |
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,60 @@ |
||||
package httpclientprovider |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestResponseLimitMiddleware(t *testing.T) { |
||||
tcs := []struct { |
||||
limit int64 |
||||
bodyLength int |
||||
body string |
||||
err error |
||||
}{ |
||||
{limit: 1, bodyLength: 1, body: "d", err: errors.New("error: http: response body too large, response limit is set to: 1")}, |
||||
{limit: 1000000, bodyLength: 5, body: "dummy", err: nil}, |
||||
{limit: 0, bodyLength: 5, body: "dummy", err: nil}, |
||||
} |
||||
for _, tc := range tcs { |
||||
t.Run(fmt.Sprintf("Test ResponseLimitMiddleware with limit: %d", tc.limit), func(t *testing.T) { |
||||
finalRoundTripper := httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { |
||||
return &http.Response{StatusCode: http.StatusOK, Request: req, Body: ioutil.NopCloser(strings.NewReader("dummy"))}, nil |
||||
}) |
||||
|
||||
mw := ResponseLimitMiddleware(tc.limit) |
||||
rt := mw.CreateMiddleware(httpclient.Options{}, finalRoundTripper) |
||||
require.NotNil(t, rt) |
||||
middlewareName, ok := mw.(httpclient.MiddlewareName) |
||||
require.True(t, ok) |
||||
require.Equal(t, ResponseLimitMiddlewareName, middlewareName.MiddlewareName()) |
||||
|
||||
ctx := context.Background() |
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://test.com/query", nil) |
||||
require.NoError(t, err) |
||||
res, err := rt.RoundTrip(req) |
||||
require.NoError(t, err) |
||||
require.NotNil(t, res) |
||||
require.NotNil(t, res.Body) |
||||
require.NoError(t, res.Body.Close()) |
||||
|
||||
bodyBytes, err := ioutil.ReadAll(res.Body) |
||||
if err != nil { |
||||
require.EqualError(t, tc.err, err.Error()) |
||||
} else { |
||||
require.NoError(t, tc.err) |
||||
} |
||||
|
||||
require.Len(t, bodyBytes, tc.bodyLength) |
||||
require.Equal(t, string(bodyBytes), tc.body) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,66 @@ |
||||
package httpclient |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
) |
||||
|
||||
// Similar implementation to http/net MaxBytesReader
|
||||
// https://pkg.go.dev/net/http#MaxBytesReader
|
||||
// What's happening differently here, is that the field that
|
||||
// is limited is the response and not the request, thus
|
||||
// the error handling/message needed to be accurate.
|
||||
|
||||
// ErrResponseBodyTooLarge indicates response body is too large
|
||||
var ErrResponseBodyTooLarge = errors.New("http: response body too large") |
||||
|
||||
// MaxBytesReader is similar to io.LimitReader but is intended for
|
||||
// limiting the size of incoming request bodies. In contrast to
|
||||
// io.LimitReader, MaxBytesReader's result is a ReadCloser, returns a
|
||||
// non-EOF error for a Read beyond the limit, and closes the
|
||||
// underlying reader when its Close method is called.
|
||||
//
|
||||
// MaxBytesReader prevents clients from accidentally or maliciously
|
||||
// sending a large request and wasting server resources.
|
||||
func MaxBytesReader(r io.ReadCloser, n int64) io.ReadCloser { |
||||
return &maxBytesReader{r: r, n: n} |
||||
} |
||||
|
||||
type maxBytesReader struct { |
||||
r io.ReadCloser // underlying reader
|
||||
n int64 // max bytes remaining
|
||||
err error // sticky error
|
||||
} |
||||
|
||||
func (l *maxBytesReader) Read(p []byte) (n int, err error) { |
||||
if l.err != nil { |
||||
return 0, l.err |
||||
} |
||||
if len(p) == 0 { |
||||
return 0, nil |
||||
} |
||||
// If they asked for a 32KB byte read but only 5 bytes are
|
||||
// remaining, no need to read 32KB. 6 bytes will answer the
|
||||
// question of the whether we hit the limit or go past it.
|
||||
if int64(len(p)) > l.n+1 { |
||||
p = p[:l.n+1] |
||||
} |
||||
n, err = l.r.Read(p) |
||||
|
||||
if int64(n) <= l.n { |
||||
l.n -= int64(n) |
||||
l.err = err |
||||
return n, err |
||||
} |
||||
|
||||
n = int(l.n) |
||||
l.n = 0 |
||||
|
||||
l.err = fmt.Errorf("error: %w, response limit is set to: %d", ErrResponseBodyTooLarge, n) |
||||
return n, l.err |
||||
} |
||||
|
||||
func (l *maxBytesReader) Close() error { |
||||
return l.r.Close() |
||||
} |
@ -0,0 +1,40 @@ |
||||
package httpclient |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestMaxBytesReader(t *testing.T) { |
||||
tcs := []struct { |
||||
limit int64 |
||||
bodyLength int |
||||
body string |
||||
err error |
||||
}{ |
||||
{limit: 1, bodyLength: 1, body: "d", err: errors.New("error: http: response body too large, response limit is set to: 1")}, |
||||
{limit: 1000000, bodyLength: 5, body: "dummy", err: nil}, |
||||
{limit: 0, bodyLength: 0, body: "", err: errors.New("error: http: response body too large, response limit is set to: 0")}, |
||||
} |
||||
for _, tc := range tcs { |
||||
t.Run(fmt.Sprintf("Test MaxBytesReader with limit: %d", tc.limit), func(t *testing.T) { |
||||
body := ioutil.NopCloser(strings.NewReader("dummy")) |
||||
readCloser := MaxBytesReader(body, tc.limit) |
||||
|
||||
bodyBytes, err := ioutil.ReadAll(readCloser) |
||||
if err != nil { |
||||
require.EqualError(t, tc.err, err.Error()) |
||||
} else { |
||||
require.NoError(t, tc.err) |
||||
} |
||||
|
||||
require.Len(t, bodyBytes, tc.bodyLength) |
||||
require.Equal(t, string(bodyBytes), tc.body) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue