From c3ab2fdeb721094181eb0aa47863003f76597302 Mon Sep 17 00:00:00 2001 From: Serge Zaitsev Date: Wed, 1 Sep 2021 11:18:30 +0200 Subject: [PATCH] Macaron: remove custom Request type (#37874) * remove macaron.Request, use http.Request instead * remove com dependency from bindings module * fix another c.Req.Request --- go.mod | 2 + pkg/api/app_routes.go | 2 +- pkg/api/basic_auth.go | 5 +- pkg/api/basic_auth_test.go | 11 +- pkg/api/dashboard_test.go | 2 +- pkg/api/grafana_com_proxy.go | 2 +- pkg/api/http_server.go | 2 +- pkg/api/pluginproxy/ds_proxy.go | 26 +- pkg/api/pluginproxy/ds_proxy_test.go | 34 +- pkg/api/plugins.go | 2 +- pkg/api/render.go | 2 +- pkg/api/static/static.go | 4 +- pkg/api/team_test.go | 8 +- pkg/macaron/binding/LICENSE | 191 +++++ pkg/macaron/binding/binding.go | 786 ++++++++++++++++++ pkg/macaron/binding/errors.go | 159 ++++ pkg/macaron/binding/go.mod | 7 + pkg/macaron/binding/go.sum | 0 pkg/macaron/context.go | 9 +- pkg/macaron/macaron.go | 4 +- pkg/middleware/logger.go | 2 +- pkg/middleware/request_tracing.go | 4 +- pkg/models/context_test.go | 4 +- pkg/plugins/backendplugin/manager/manager.go | 2 +- .../contexthandler/auth_proxy_test.go | 4 +- .../authproxy/authproxy_test.go | 6 +- pkg/services/contexthandler/contexthandler.go | 6 +- .../contexthandler/contexthandler_test.go | 8 +- .../datasourceproxy/datasourceproxy.go | 2 +- .../libraryelements/libraryelements_test.go | 4 +- .../librarypanels/librarypanels_test.go | 4 +- pkg/services/live/live.go | 6 +- pkg/services/live/pushhttp/push.go | 2 +- pkg/services/ngalert/api/api_alertmanager.go | 2 +- pkg/services/ngalert/api/util.go | 2 +- 35 files changed, 1205 insertions(+), 111 deletions(-) create mode 100644 pkg/macaron/binding/LICENSE create mode 100644 pkg/macaron/binding/binding.go create mode 100644 pkg/macaron/binding/errors.go create mode 100644 pkg/macaron/binding/go.mod create mode 100644 pkg/macaron/binding/go.sum diff --git a/go.mod b/go.mod index ffce8a031bc..80ef6f8e085 100644 --- a/go.mod +++ b/go.mod @@ -236,4 +236,6 @@ replace github.com/apache/thrift => github.com/apache/thrift v0.14.1 replace gopkg.in/macaron.v1 => ./pkg/macaron +replace github.com/go-macaron/binding => ./pkg/macaron/binding + replace github.com/hashicorp/consul => github.com/hashicorp/consul v1.9.8 diff --git a/pkg/api/app_routes.go b/pkg/api/app_routes.go index 875c17d61f3..b30a757cd76 100644 --- a/pkg/api/app_routes.go +++ b/pkg/api/app_routes.go @@ -59,6 +59,6 @@ func AppPluginRoute(route *plugins.AppPluginRoute, appID string, hs *HTTPServer) proxy := pluginproxy.NewApiPluginProxy(c, path, route, appID, hs.Cfg) proxy.Transport = pluginProxyTransport - proxy.ServeHTTP(c.Resp, c.Req.Request) + proxy.ServeHTTP(c.Resp, c.Req) } } diff --git a/pkg/api/basic_auth.go b/pkg/api/basic_auth.go index 0c4890525f0..ab28273d86c 100644 --- a/pkg/api/basic_auth.go +++ b/pkg/api/basic_auth.go @@ -2,15 +2,14 @@ package api import ( "crypto/subtle" - - macaron "gopkg.in/macaron.v1" + "net/http" ) // BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials // and returns true if the provided credentials match the expected username and password. // Returns false if the request is unauthenticated. // Uses constant-time comparison in order to mitigate timing attacks. -func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool { +func BasicAuthenticatedRequest(req *http.Request, expectedUser, expectedPass string) bool { user, pass, ok := req.BasicAuth() if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 { return false diff --git a/pkg/api/basic_auth_test.go b/pkg/api/basic_auth_test.go index 72e9383ec1f..978e6222817 100644 --- a/pkg/api/basic_auth_test.go +++ b/pkg/api/basic_auth_test.go @@ -8,7 +8,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "gopkg.in/macaron.v1" ) func TestBasicAuthenticatedRequest(t *testing.T) { @@ -16,11 +15,8 @@ func TestBasicAuthenticatedRequest(t *testing.T) { const expectedPass = "password" t.Run("Given a valid set of basic auth credentials", func(t *testing.T) { - httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + req, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) require.NoError(t, err) - req := macaron.Request{ - Request: httpReq, - } encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass) req.Header.Set("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) @@ -29,11 +25,8 @@ func TestBasicAuthenticatedRequest(t *testing.T) { }) t.Run("Given an invalid set of basic auth credentials", func(t *testing.T) { - httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + req, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) require.NoError(t, err) - req := macaron.Request{ - Request: httpReq, - } encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass") req.Header.Set("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 53f9b680b52..63fcaabc55c 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -33,7 +33,7 @@ import ( func TestGetHomeDashboard(t *testing.T) { httpReq, err := http.NewRequest(http.MethodGet, "", nil) require.NoError(t, err) - req := &models.ReqContext{SignedInUser: &models.SignedInUser{}, Context: &macaron.Context{Req: macaron.Request{Request: httpReq}}} + req := &models.ReqContext{SignedInUser: &models.SignedInUser{}, Context: &macaron.Context{Req: httpReq}} cfg := setting.NewCfg() cfg.StaticRootPath = "../../public/" diff --git a/pkg/api/grafana_com_proxy.go b/pkg/api/grafana_com_proxy.go index 2c0e258df63..0c7c5213bdf 100644 --- a/pkg/api/grafana_com_proxy.go +++ b/pkg/api/grafana_com_proxy.go @@ -44,6 +44,6 @@ func ProxyGnetRequest(c *models.ReqContext) { proxyPath := c.Params("*") proxy := ReverseProxyGnetReq(proxyPath) proxy.Transport = grafanaComProxyTransport - proxy.ServeHTTP(c.Resp, c.Req.Request) + proxy.ServeHTTP(c.Resp, c.Req) c.Resp.Header().Del("Set-Cookie") } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 57826632acc..1c924157425 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -457,7 +457,7 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { promhttp. HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}). - ServeHTTP(ctx.Resp, ctx.Req.Request) + ServeHTTP(ctx.Resp, ctx.Req) } // healthzHandler always return 200 - Ok if Grafana's web server is running diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index fb08d33c395..fde878d6479 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -147,7 +147,7 @@ func (proxy *DataSourceProxy) HandleRequest() { span, ctx := opentracing.StartSpanFromContext(proxy.ctx.Req.Context(), "datasource reverse proxy") defer span.Finish() - proxy.ctx.Req.Request = proxy.ctx.Req.WithContext(ctx) + proxy.ctx.Req = proxy.ctx.Req.WithContext(ctx) span.SetTag("datasource_name", proxy.ds.Name) span.SetTag("datasource_type", proxy.ds.Type) @@ -160,11 +160,11 @@ func (proxy *DataSourceProxy) HandleRequest() { if err := opentracing.GlobalTracer().Inject( span.Context(), opentracing.HTTPHeaders, - opentracing.HTTPHeadersCarrier(proxy.ctx.Req.Request.Header)); err != nil { + opentracing.HTTPHeadersCarrier(proxy.ctx.Req.Header)); err != nil { logger.Error("Failed to inject span context instance", "err", err) } - reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request) + reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req) } func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, headerName string, tagName string) { @@ -252,13 +252,13 @@ func (proxy *DataSourceProxy) validateRequest() error { } if proxy.ds.Type == models.DS_ES { - if proxy.ctx.Req.Request.Method == "DELETE" { + if proxy.ctx.Req.Method == "DELETE" { return errors.New("deletes not allowed on proxied Elasticsearch datasource") } - if proxy.ctx.Req.Request.Method == "PUT" { + if proxy.ctx.Req.Method == "PUT" { return errors.New("puts not allowed on proxied Elasticsearch datasource") } - if proxy.ctx.Req.Request.Method == "POST" && proxy.proxyPath != "_msearch" { + if proxy.ctx.Req.Method == "POST" && proxy.proxyPath != "_msearch" { return errors.New("posts not allowed on proxied Elasticsearch datasource except on /_msearch") } } @@ -289,13 +289,13 @@ func (proxy *DataSourceProxy) validateRequest() error { // Trailing validation below this point for routes that were not matched if proxy.ds.Type == models.DS_PROMETHEUS { - if proxy.ctx.Req.Request.Method == "DELETE" { + if proxy.ctx.Req.Method == "DELETE" { return errors.New("non allow-listed DELETEs not allowed on proxied Prometheus datasource") } - if proxy.ctx.Req.Request.Method == "PUT" { + if proxy.ctx.Req.Method == "PUT" { return errors.New("non allow-listed PUTs not allowed on proxied Prometheus datasource") } - if proxy.ctx.Req.Request.Method == "POST" { + if proxy.ctx.Req.Method == "POST" { return errors.New("non allow-listed POSTs not allowed on proxied Prometheus datasource") } } @@ -309,10 +309,10 @@ func (proxy *DataSourceProxy) logRequest() { } var body string - if proxy.ctx.Req.Request.Body != nil { - buffer, err := ioutil.ReadAll(proxy.ctx.Req.Request.Body) + if proxy.ctx.Req.Body != nil { + buffer, err := ioutil.ReadAll(proxy.ctx.Req.Body) if err == nil { - proxy.ctx.Req.Request.Body = ioutil.NopCloser(bytes.NewBuffer(buffer)) + proxy.ctx.Req.Body = ioutil.NopCloser(bytes.NewBuffer(buffer)) body = string(buffer) } } @@ -323,7 +323,7 @@ func (proxy *DataSourceProxy) logRequest() { "username", proxy.ctx.Login, "datasource", proxy.ds.Type, "uri", proxy.ctx.Req.RequestURI, - "method", proxy.ctx.Req.Request.Method, + "method", proxy.ctx.Req.Method, "body", body) } diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index f241d232b87..ddf2a417651 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -103,9 +103,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) ctx := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } return ctx, req @@ -239,9 +237,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) ctx := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } @@ -450,9 +446,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { require.NoError(t, err) ctx := &models.ReqContext{ SignedInUser: &models.SignedInUser{UserId: 1}, - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, } mockAuthToken := mockOAuthTokenService{ token: &oauth2.Token{ @@ -582,9 +576,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { return &models.ReqContext{ SignedInUser: &models.SignedInUser{}, Context: &macaron.Context{ - Req: macaron.Request{ - Request: httptest.NewRequest("GET", "/render", nil), - }, + Req: httptest.NewRequest("GET", "/render", nil), Resp: responseWriter, }, }, ds @@ -647,7 +639,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { }, }) - ctx.Req.Request = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil) + ctx.Req = httptest.NewRequest("GET", "/api/datasources/proxy/1/path/%2Ftest%2Ftest%2F?query=%2Ftest%2Ftest%2F", nil) proxy, err := NewDataSourceProxy(ds, plugin, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}) require.NoError(t, err) @@ -661,9 +653,7 @@ func TestDataSourceProxy_requestHandling(t *testing.T) { func TestNewDataSourceProxy_InvalidURL(t *testing.T) { ctx := models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{}, - }, + Context: &macaron.Context{}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } ds := models.DataSource{ @@ -679,9 +669,7 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) { func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { ctx := models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{}, - }, + Context: &macaron.Context{}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } ds := models.DataSource{ @@ -699,9 +687,7 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) { // Test wth MSSQL type data sources. func TestNewDataSourceProxy_MSSQL(t *testing.T) { ctx := models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{}, - }, + Context: &macaron.Context{}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR}, } tcs := []struct { @@ -899,9 +885,7 @@ func Test_PathCheck(t *testing.T) { req, err := http.NewRequest("GET", "http://localhost/asd", nil) require.NoError(t, err) ctx := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_VIEWER}, } return ctx, req diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index b7bc2e2b47f..7ece17c06cf 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -312,7 +312,7 @@ func (hs *HTTPServer) GetPluginAssets(c *models.ReqContext) { c.Resp.Header().Set("Cache-Control", "public, max-age=3600") } - http.ServeContent(c.Resp, c.Req.Request, pluginFilePath, fi.ModTime(), f) + http.ServeContent(c.Resp, c.Req, pluginFilePath, fi.ModTime(), f) } // CheckHealth returns the health of a plugin. diff --git a/pkg/api/render.go b/pkg/api/render.go index a52b9b7f603..6b53d73e161 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -76,5 +76,5 @@ func (hs *HTTPServer) RenderToPng(c *models.ReqContext) { } c.Resp.Header().Set("Content-Type", "image/png") - http.ServeFile(c.Resp, c.Req.Request, result.FilePath) + http.ServeFile(c.Resp, c.Req, result.FilePath) } diff --git a/pkg/api/static/static.go b/pkg/api/static/static.go index 98b2c5d7222..f44d494ca15 100644 --- a/pkg/api/static/static.go +++ b/pkg/api/static/static.go @@ -160,7 +160,7 @@ func staticHandler(ctx *macaron.Context, log log.Logger, opt StaticOptions) bool rePrefix := regexp.MustCompile(`^(?:/\\|/+)`) path = rePrefix.ReplaceAllString(path, "/") } - http.Redirect(ctx.Resp, ctx.Req.Request, path, http.StatusFound) + http.Redirect(ctx.Resp, ctx.Req, path, http.StatusFound) return true } @@ -190,7 +190,7 @@ func staticHandler(ctx *macaron.Context, log log.Logger, opt StaticOptions) bool opt.AddHeaders(ctx) } - http.ServeContent(ctx.Resp, ctx.Req.Request, file, fi.ModTime(), f) + http.ServeContent(ctx.Resp, ctx.Req, file, fi.ModTime(), f) return true } diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 3bf1473dc4d..1ac6c593ee8 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -126,9 +126,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with no real signed in user", func(t *testing.T) { stub := &testLogger{} c := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, SignedInUser: &models.SignedInUser{}, Logger: stub, } @@ -144,9 +142,7 @@ func TestTeamAPIEndpoint(t *testing.T) { t.Run("with real signed in user", func(t *testing.T) { stub := &testLogger{} c := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, SignedInUser: &models.SignedInUser{UserId: 42}, Logger: stub, } diff --git a/pkg/macaron/binding/LICENSE b/pkg/macaron/binding/LICENSE new file mode 100644 index 00000000000..8405e89a0b1 --- /dev/null +++ b/pkg/macaron/binding/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/pkg/macaron/binding/binding.go b/pkg/macaron/binding/binding.go new file mode 100644 index 00000000000..9ba5496d9e0 --- /dev/null +++ b/pkg/macaron/binding/binding.go @@ -0,0 +1,786 @@ +// Copyright 2014 Martini Authors +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package binding is a middleware that provides request data binding and validation for Macaron. +package binding + +import ( + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/url" + "reflect" + "regexp" + "strconv" + "strings" + "unicode/utf8" + + "gopkg.in/macaron.v1" +) + +func bind(ctx *macaron.Context, obj interface{}, ifacePtr ...interface{}) { + contentType := ctx.Req.Header.Get("Content-Type") + if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { + switch { + case strings.Contains(contentType, "form-urlencoded"): + _, _ = ctx.Invoke(Form(obj, ifacePtr...)) + case strings.Contains(contentType, "multipart/form-data"): + _, _ = ctx.Invoke(MultipartForm(obj, ifacePtr...)) + case strings.Contains(contentType, "json"): + _, _ = ctx.Invoke(Json(obj, ifacePtr...)) + default: + var errors Errors + if contentType == "" { + errors.Add([]string{}, ERR_CONTENT_TYPE, "Empty Content-Type") + } else { + errors.Add([]string{}, ERR_CONTENT_TYPE, "Unsupported Content-Type") + } + ctx.Map(errors) + ctx.Map(obj) // Map a fake struct so handler won't panic. + } + } else { + _, _ = ctx.Invoke(Form(obj, ifacePtr...)) + } +} + +const ( + _JSON_CONTENT_TYPE = "application/json; charset=utf-8" + STATUS_UNPROCESSABLE_ENTITY = 422 +) + +// errorHandler simply counts the number of errors in the +// context and, if more than 0, writes a response with an +// error code and a JSON payload describing the errors. +// The response will have a JSON content-type. +// Middleware remaining on the stack will not even see the request +// if, by this point, there are any errors. +// This is a "default" handler, of sorts, and you are +// welcome to use your own instead. The Bind middleware +// invokes this automatically for convenience. +func errorHandler(errs Errors, rw http.ResponseWriter) { + if len(errs) > 0 { + rw.Header().Set("Content-Type", _JSON_CONTENT_TYPE) + if errs.Has(ERR_DESERIALIZATION) { + rw.WriteHeader(http.StatusBadRequest) + } else if errs.Has(ERR_CONTENT_TYPE) { + rw.WriteHeader(http.StatusUnsupportedMediaType) + } else { + rw.WriteHeader(STATUS_UNPROCESSABLE_ENTITY) + } + errOutput, _ := json.Marshal(errs) + _, _ = rw.Write(errOutput) + return + } +} + +// CustomErrorHandler will be invoked if errors occured. +var CustomErrorHandler func(*macaron.Context, Errors) + +// Bind wraps up the functionality of the Form and Json middleware +// according to the Content-Type and verb of the request. +// A Content-Type is required for POST and PUT requests. +// Bind invokes the ErrorHandler middleware to bail out if errors +// occurred. If you want to perform your own error handling, use +// Form or Json middleware directly. An interface pointer can +// be added as a second argument in order to map the struct to +// a specific interface. +func Bind(obj interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + bind(ctx, obj, ifacePtr...) + if handler, ok := obj.(ErrorHandler); ok { + _, _ = ctx.Invoke(handler.Error) + } else if CustomErrorHandler != nil { + _, _ = ctx.Invoke(CustomErrorHandler) + } else { + _, _ = ctx.Invoke(errorHandler) + } + } +} + +// BindIgnErr will do the exactly same thing as Bind but without any +// error handling, which user has freedom to deal with them. +// This allows user take advantages of validation. +func BindIgnErr(obj interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + bind(ctx, obj, ifacePtr...) + } +} + +// Form is middleware to deserialize form-urlencoded data from the request. +// It gets data from the form-urlencoded body, if present, or from the +// query string. It uses the http.Request.ParseForm() method +// to perform deserialization, then reflection is used to map each field +// into the struct with the proper type. Structs with primitive slice types +// (bool, float, int, string) can support deserialization of repeated form +// keys, for example: key=val1&key=val2&key=val3 +// An interface pointer can be added as a second argument in order +// to map the struct to a specific interface. +func Form(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + var errors Errors + + ensureNotPointer(formStruct) + formStruct := reflect.New(reflect.TypeOf(formStruct)) + parseErr := ctx.Req.ParseForm() + + // Format validation of the request body or the URL would add considerable overhead, + // and ParseForm does not complain when URL encoding is off. + // Because an empty request body or url can also mean absence of all needed values, + // it is not in all cases a bad request, so let's return 422. + if parseErr != nil { + errors.Add([]string{}, ERR_DESERIALIZATION, parseErr.Error()) + } + errors = mapForm(formStruct, ctx.Req.Form, nil, errors) + validateAndMap(formStruct, ctx, errors, ifacePtr...) + } +} + +// Maximum amount of memory to use when parsing a multipart form. +// Set this to whatever value you prefer; default is 10 MB. +var MaxMemory = int64(1024 * 1024 * 10) + +// MultipartForm works much like Form, except it can parse multipart forms +// and handle file uploads. Like the other deserialization middleware handlers, +// you can pass in an interface to make the interface available for injection +// into other handlers later. +func MultipartForm(formStruct interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + var errors Errors + ensureNotPointer(formStruct) + formStruct := reflect.New(reflect.TypeOf(formStruct)) + // This if check is necessary due to https://github.com/martini-contrib/csrf/issues/6 + if ctx.Req.MultipartForm == nil { + // Workaround for multipart forms returning nil instead of an error + // when content is not multipart; see https://code.google.com/p/go/issues/detail?id=6334 + if multipartReader, err := ctx.Req.MultipartReader(); err != nil { + errors.Add([]string{}, ERR_DESERIALIZATION, err.Error()) + } else { + form, parseErr := multipartReader.ReadForm(MaxMemory) + if parseErr != nil { + errors.Add([]string{}, ERR_DESERIALIZATION, parseErr.Error()) + } + + if ctx.Req.Form == nil { + _ = ctx.Req.ParseForm() + } + for k, v := range form.Value { + ctx.Req.Form[k] = append(ctx.Req.Form[k], v...) + } + + ctx.Req.MultipartForm = form + } + } + errors = mapForm(formStruct, ctx.Req.MultipartForm.Value, ctx.Req.MultipartForm.File, errors) + validateAndMap(formStruct, ctx, errors, ifacePtr...) + } +} + +// Json is middleware to deserialize a JSON payload from the request +// into the struct that is passed in. The resulting struct is then +// validated, but no error handling is actually performed here. +// An interface pointer can be added as a second argument in order +// to map the struct to a specific interface. +func Json(jsonStruct interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + var errors Errors + ensureNotPointer(jsonStruct) + jsonStruct := reflect.New(reflect.TypeOf(jsonStruct)) + if ctx.Req.Body != nil { + defer ctx.Req.Body.Close() + err := json.NewDecoder(ctx.Req.Body).Decode(jsonStruct.Interface()) + if err != nil && err != io.EOF { + errors.Add([]string{}, ERR_DESERIALIZATION, err.Error()) + } + } + validateAndMap(jsonStruct, ctx, errors, ifacePtr...) + } +} + +// URL is the middleware to parse URL parameters into struct fields. +func URL(obj interface{}, ifacePtr ...interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + var errors Errors + + ensureNotPointer(obj) + obj := reflect.New(reflect.TypeOf(obj)) + + val := obj.Elem() + for k, v := range ctx.AllParams() { + field := val.FieldByName(k[1:]) + if field.IsValid() { + errors = setWithProperType(field.Kind(), v, field, k, errors) + } + } + validateAndMap(obj, ctx, errors, ifacePtr...) + } +} + +// RawValidate is same as Validate but does not require a HTTP context, +// and can be used independently just for validation. +// This function does not support Validator interface. +func RawValidate(obj interface{}) Errors { + var errs Errors + v := reflect.ValueOf(obj) + k := v.Kind() + if k == reflect.Interface || k == reflect.Ptr { + v = v.Elem() + k = v.Kind() + } + if k == reflect.Slice || k == reflect.Array { + for i := 0; i < v.Len(); i++ { + e := v.Index(i).Interface() + errs = validateStruct(errs, e) + } + } else { + errs = validateStruct(errs, obj) + } + return errs +} + +// Validate is middleware to enforce required fields. If the struct +// passed in implements Validator, then the user-defined Validate method +// is executed, and its errors are mapped to the context. This middleware +// performs no error handling: it merely detects errors and maps them. +func Validate(obj interface{}) macaron.Handler { + return func(ctx *macaron.Context) { + var errs Errors + v := reflect.ValueOf(obj) + k := v.Kind() + if k == reflect.Interface || k == reflect.Ptr { + v = v.Elem() + k = v.Kind() + } + if k == reflect.Slice || k == reflect.Array { + for i := 0; i < v.Len(); i++ { + e := v.Index(i).Interface() + errs = validateStruct(errs, e) + if validator, ok := e.(Validator); ok { + errs = validator.Validate(ctx, errs) + } + } + } else { + errs = validateStruct(errs, obj) + if validator, ok := obj.(Validator); ok { + errs = validator.Validate(ctx, errs) + } + } + ctx.Map(errs) + } +} + +var ( + AlphaDashPattern = regexp.MustCompile(`[^\d\w-_]`) + AlphaDashDotPattern = regexp.MustCompile(`[^\d\w-_\.]`) + EmailPattern = regexp.MustCompile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[a-zA-Z0-9](?:[\\w-]*[\\w])?") +) + +// Copied from github.com/asaskevich/govalidator. +const _MAX_URL_RUNE_COUNT = 2083 +const _MIN_URL_RUNE_COUNT = 3 + +var ( + urlSchemaRx = `((ftp|tcp|udp|wss?|https?):\/\/)` + urlUsernameRx = `(\S+(:\S*)?@)` + urlIPRx = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))` + ipRx = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))` + urlSubdomainRx = `((www\.)|([a-zA-Z0-9]([-\.][-\._a-zA-Z0-9]+)*))` + urlPortRx = `(:(\d{1,5}))` + urlPathRx = `((\/|\?|#)[^\s]*)` + URLPattern = regexp.MustCompile(`^` + urlSchemaRx + urlUsernameRx + `?` + `((` + urlIPRx + `|(\[` + ipRx + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + urlSubdomainRx + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + urlPortRx + `?` + urlPathRx + `?$`) +) + +// IsURL check if the string is an URL. +func isURL(str string) bool { + if str == "" || utf8.RuneCountInString(str) >= _MAX_URL_RUNE_COUNT || len(str) <= _MIN_URL_RUNE_COUNT || strings.HasPrefix(str, ".") { + return false + } + u, err := url.Parse(str) + if err != nil { + return false + } + if strings.HasPrefix(u.Host, ".") { + return false + } + if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) { + return false + } + return URLPattern.MatchString(str) + +} + +type ( + // Rule represents a validation rule. + Rule struct { + // IsMatch checks if rule matches. + IsMatch func(string) bool + // IsValid applies validation rule to condition. + IsValid func(Errors, string, interface{}) (bool, Errors) + } + + // ParamRule does same thing as Rule but passes rule itself to IsValid method. + ParamRule struct { + // IsMatch checks if rule matches. + IsMatch func(string) bool + // IsValid applies validation rule to condition. + IsValid func(Errors, string, string, interface{}) (bool, Errors) + } + + // RuleMapper and ParamRuleMapper represent validation rule mappers, + // it allwos users to add custom validation rules. + RuleMapper []*Rule + ParamRuleMapper []*ParamRule +) + +var ruleMapper RuleMapper +var paramRuleMapper ParamRuleMapper + +// AddRule adds new validation rule. +func AddRule(r *Rule) { + ruleMapper = append(ruleMapper, r) +} + +// AddParamRule adds new validation rule. +func AddParamRule(r *ParamRule) { + paramRuleMapper = append(paramRuleMapper, r) +} + +func in(fieldValue interface{}, arr string) bool { + val := fmt.Sprintf("%v", fieldValue) + vals := strings.Split(arr, ",") + isIn := false + for _, v := range vals { + if v == val { + isIn = true + break + } + } + return isIn +} + +func parseFormName(raw, actual string) string { + if len(actual) > 0 { + return actual + } + return nameMapper(raw) +} + +// Performs required field checking on a struct +func validateStruct(errors Errors, obj interface{}) Errors { + typ := reflect.TypeOf(obj) + val := reflect.ValueOf(obj) + + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + val = val.Elem() + } + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Allow ignored fields in the struct + if field.Tag.Get("form") == "-" || !val.Field(i).CanInterface() { + continue + } + + fieldVal := val.Field(i) + fieldValue := fieldVal.Interface() + zero := reflect.Zero(field.Type).Interface() + + // Validate nested and embedded structs (if pointer, only do so if not nil) + if field.Type.Kind() == reflect.Struct || + (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue) && + field.Type.Elem().Kind() == reflect.Struct) { + errors = validateStruct(errors, fieldValue) + } + errors = validateField(errors, zero, field, fieldVal, fieldValue) + } + return errors +} + +func validateField(errors Errors, zero interface{}, field reflect.StructField, fieldVal reflect.Value, fieldValue interface{}) Errors { + if fieldVal.Kind() == reflect.Slice { + for i := 0; i < fieldVal.Len(); i++ { + sliceVal := fieldVal.Index(i) + if sliceVal.Kind() == reflect.Ptr { + sliceVal = sliceVal.Elem() + } + + sliceValue := sliceVal.Interface() + zero := reflect.Zero(sliceVal.Type()).Interface() + if sliceVal.Kind() == reflect.Struct || + (sliceVal.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, sliceValue) && + sliceVal.Elem().Kind() == reflect.Struct) { + errors = validateStruct(errors, sliceValue) + } + /* Apply validation rules to each item in a slice. ISSUE #3 + else { + errors = validateField(errors, zero, field, sliceVal, sliceValue) + }*/ + } + } + +VALIDATE_RULES: + for _, rule := range strings.Split(field.Tag.Get("binding"), ";") { + if len(rule) == 0 { + continue + } + + switch { + case rule == "OmitEmpty": + if reflect.DeepEqual(zero, fieldValue) { + break VALIDATE_RULES + } + case rule == "Required": + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice { + if v.Len() == 0 { + errors.Add([]string{field.Name}, ERR_REQUIRED, "Required") + break VALIDATE_RULES + } + + continue + } + + if reflect.DeepEqual(zero, fieldValue) { + errors.Add([]string{field.Name}, ERR_REQUIRED, "Required") + break VALIDATE_RULES + } + case rule == "AlphaDash": + if AlphaDashPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Add([]string{field.Name}, ERR_ALPHA_DASH, "AlphaDash") + break VALIDATE_RULES + } + case rule == "AlphaDashDot": + if AlphaDashDotPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Add([]string{field.Name}, ERR_ALPHA_DASH_DOT, "AlphaDashDot") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "Size("): + size, _ := strconv.Atoi(rule[5 : len(rule)-1]) + if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) != size { + errors.Add([]string{field.Name}, ERR_SIZE, "Size") + break VALIDATE_RULES + } + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice && v.Len() != size { + errors.Add([]string{field.Name}, ERR_SIZE, "Size") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "MinSize("): + min, _ := strconv.Atoi(rule[8 : len(rule)-1]) + if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) < min { + errors.Add([]string{field.Name}, ERR_MIN_SIZE, "MinSize") + break VALIDATE_RULES + } + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice && v.Len() < min { + errors.Add([]string{field.Name}, ERR_MIN_SIZE, "MinSize") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "MaxSize("): + max, _ := strconv.Atoi(rule[8 : len(rule)-1]) + if str, ok := fieldValue.(string); ok && utf8.RuneCountInString(str) > max { + errors.Add([]string{field.Name}, ERR_MAX_SIZE, "MaxSize") + break VALIDATE_RULES + } + v := reflect.ValueOf(fieldValue) + if v.Kind() == reflect.Slice && v.Len() > max { + errors.Add([]string{field.Name}, ERR_MAX_SIZE, "MaxSize") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "Range("): + nums := strings.Split(rule[6:len(rule)-1], ",") + if len(nums) != 2 { + break VALIDATE_RULES + } + if min, err := strconv.Atoi(nums[0]); err != nil { + errors.Add([]string{field.Name}, ERR_RANGE, "Range") + break VALIDATE_RULES + } else if max, err := strconv.Atoi(nums[1]); err != nil { + errors.Add([]string{field.Name}, ERR_RANGE, "Range") + break VALIDATE_RULES + } else if val, err := strconv.Atoi(fmt.Sprintf("%v", fieldValue)); err != nil { + errors.Add([]string{field.Name}, ERR_RANGE, "Range") + break VALIDATE_RULES + } else if val < min || val > max { + errors.Add([]string{field.Name}, ERR_RANGE, "Range") + break VALIDATE_RULES + } + case rule == "Email": + if !EmailPattern.MatchString(fmt.Sprintf("%v", fieldValue)) { + errors.Add([]string{field.Name}, ERR_EMAIL, "Email") + break VALIDATE_RULES + } + case rule == "Url": + str := fmt.Sprintf("%v", fieldValue) + if len(str) == 0 { + continue + } else if !isURL(str) { + errors.Add([]string{field.Name}, ERR_URL, "Url") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "In("): + if !in(fieldValue, rule[3:len(rule)-1]) { + errors.Add([]string{field.Name}, ERR_IN, "In") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "NotIn("): + if in(fieldValue, rule[6:len(rule)-1]) { + errors.Add([]string{field.Name}, ERR_NOT_INT, "NotIn") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "Include("): + if !strings.Contains(fmt.Sprintf("%v", fieldValue), rule[8:len(rule)-1]) { + errors.Add([]string{field.Name}, ERR_INCLUDE, "Include") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "Exclude("): + if strings.Contains(fmt.Sprintf("%v", fieldValue), rule[8:len(rule)-1]) { + errors.Add([]string{field.Name}, ERR_EXCLUDE, "Exclude") + break VALIDATE_RULES + } + case strings.HasPrefix(rule, "Default("): + if reflect.DeepEqual(zero, fieldValue) { + if fieldVal.CanAddr() { + errors = setWithProperType(field.Type.Kind(), rule[8:len(rule)-1], fieldVal, field.Tag.Get("form"), errors) + } else { + errors.Add([]string{field.Name}, ERR_EXCLUDE, "Default") + break VALIDATE_RULES + } + } + default: + // Apply custom validation rules + var isValid bool + for i := range ruleMapper { + if ruleMapper[i].IsMatch(rule) { + isValid, errors = ruleMapper[i].IsValid(errors, field.Name, fieldValue) + if !isValid { + break VALIDATE_RULES + } + } + } + for i := range paramRuleMapper { + if paramRuleMapper[i].IsMatch(rule) { + isValid, errors = paramRuleMapper[i].IsValid(errors, rule, field.Name, fieldValue) + if !isValid { + break VALIDATE_RULES + } + } + } + } + } + return errors +} + +// NameMapper represents a form tag name mapper. +type NameMapper func(string) string + +var ( + nameMapper = func(field string) string { + newstr := make([]rune, 0, len(field)) + for i, chr := range field { + if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { + if i > 0 { + newstr = append(newstr, '_') + } + chr -= ('A' - 'a') + } + newstr = append(newstr, chr) + } + return string(newstr) + } +) + +// SetNameMapper sets name mapper. +func SetNameMapper(nm NameMapper) { + nameMapper = nm +} + +// Takes values from the form data and puts them into a struct +func mapForm(formStruct reflect.Value, form map[string][]string, + formfile map[string][]*multipart.FileHeader, errors Errors) Errors { + + if formStruct.Kind() == reflect.Ptr { + formStruct = formStruct.Elem() + } + typ := formStruct.Type() + + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := formStruct.Field(i) + + if typeField.Type.Kind() == reflect.Ptr && typeField.Anonymous { + structField.Set(reflect.New(typeField.Type.Elem())) + errors = mapForm(structField.Elem(), form, formfile, errors) + if reflect.DeepEqual(structField.Elem().Interface(), reflect.Zero(structField.Elem().Type()).Interface()) { + structField.Set(reflect.Zero(structField.Type())) + } + } else if typeField.Type.Kind() == reflect.Struct { + errors = mapForm(structField, form, formfile, errors) + } + + inputFieldName := parseFormName(typeField.Name, typeField.Tag.Get("form")) + if len(inputFieldName) == 0 || !structField.CanSet() { + continue + } + + inputValue, exists := form[inputFieldName] + if exists { + numElems := len(inputValue) + if structField.Kind() == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + errors = setWithProperType(sliceOf, inputValue[i], slice.Index(i), inputFieldName, errors) + } + formStruct.Field(i).Set(slice) + } else { + errors = setWithProperType(typeField.Type.Kind(), inputValue[0], structField, inputFieldName, errors) + } + continue + } + + inputFile, exists := formfile[inputFieldName] + if !exists { + continue + } + fhType := reflect.TypeOf((*multipart.FileHeader)(nil)) + numElems := len(inputFile) + if structField.Kind() == reflect.Slice && numElems > 0 && structField.Type().Elem() == fhType { + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + slice.Index(i).Set(reflect.ValueOf(inputFile[i])) + } + structField.Set(slice) + } else if structField.Type() == fhType { + structField.Set(reflect.ValueOf(inputFile[0])) + } + } + return errors +} + +// This sets the value in a struct of an indeterminate type to the +// matching value from the request (via Form middleware) in the +// same type, so that not all deserialized values have to be strings. +// Supported types are string, int, float, and bool. +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value, nameInTag string, errors Errors) Errors { + switch valueKind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, 64) + if err != nil { + errors.Add([]string{nameInTag}, ERR_INTERGER_TYPE, "Value could not be parsed as integer") + } else { + structField.SetInt(intVal) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, 64) + if err != nil { + errors.Add([]string{nameInTag}, ERR_INTERGER_TYPE, "Value could not be parsed as unsigned integer") + } else { + structField.SetUint(uintVal) + } + case reflect.Bool: + if val == "on" { + structField.SetBool(true) + break + } + + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err != nil { + errors.Add([]string{nameInTag}, ERR_BOOLEAN_TYPE, "Value could not be parsed as boolean") + } else if boolVal { + structField.SetBool(true) + } + case reflect.Float32: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 32) + if err != nil { + errors.Add([]string{nameInTag}, ERR_FLOAT_TYPE, "Value could not be parsed as 32-bit float") + } else { + structField.SetFloat(floatVal) + } + case reflect.Float64: + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, 64) + if err != nil { + errors.Add([]string{nameInTag}, ERR_FLOAT_TYPE, "Value could not be parsed as 64-bit float") + } else { + structField.SetFloat(floatVal) + } + case reflect.String: + structField.SetString(val) + } + return errors +} + +// Don't pass in pointers to bind to. Can lead to bugs. +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + panic("Pointers are not accepted as binding models") + } +} + +// Performs validation and combines errors from validation +// with errors from deserialization, then maps both the +// resulting struct and the errors to the context. +func validateAndMap(obj reflect.Value, ctx *macaron.Context, errors Errors, ifacePtr ...interface{}) { + _, _ = ctx.Invoke(Validate(obj.Interface())) + errors = append(errors, getErrors(ctx)...) + ctx.Map(errors) + ctx.Map(obj.Elem().Interface()) + if len(ifacePtr) > 0 { + ctx.MapTo(obj.Elem().Interface(), ifacePtr[0]) + } +} + +// getErrors simply gets the errors from the context (it's kind of a chore) +func getErrors(ctx *macaron.Context) Errors { + return ctx.GetVal(reflect.TypeOf(Errors{})).Interface().(Errors) +} + +type ( + // ErrorHandler is the interface that has custom error handling process. + ErrorHandler interface { + // Error handles validation errors with custom process. + Error(*macaron.Context, Errors) + } + + // Validator is the interface that handles some rudimentary + // request validation logic so your application doesn't have to. + Validator interface { + // Validate validates that the request is OK. It is recommended + // that validation be limited to checking values for syntax and + // semantics, enough to know that you can make sense of the request + // in your application. For example, you might verify that a credit + // card number matches a valid pattern, but you probably wouldn't + // perform an actual credit card authorization here. + Validate(*macaron.Context, Errors) Errors + } +) diff --git a/pkg/macaron/binding/errors.go b/pkg/macaron/binding/errors.go new file mode 100644 index 00000000000..8cbe44a9d17 --- /dev/null +++ b/pkg/macaron/binding/errors.go @@ -0,0 +1,159 @@ +// Copyright 2014 Martini Authors +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package binding + +const ( + // Type mismatch errors. + ERR_CONTENT_TYPE = "ContentTypeError" + ERR_DESERIALIZATION = "DeserializationError" + ERR_INTERGER_TYPE = "IntegerTypeError" + ERR_BOOLEAN_TYPE = "BooleanTypeError" + ERR_FLOAT_TYPE = "FloatTypeError" + + // Validation errors. + ERR_REQUIRED = "RequiredError" + ERR_ALPHA_DASH = "AlphaDashError" + ERR_ALPHA_DASH_DOT = "AlphaDashDotError" + ERR_SIZE = "SizeError" + ERR_MIN_SIZE = "MinSizeError" + ERR_MAX_SIZE = "MaxSizeError" + ERR_RANGE = "RangeError" + ERR_EMAIL = "EmailError" + ERR_URL = "UrlError" + ERR_IN = "InError" + ERR_NOT_INT = "NotInError" + ERR_INCLUDE = "IncludeError" + ERR_EXCLUDE = "ExcludeError" + ERR_DEFAULT = "DefaultError" +) + +type ( + // Errors may be generated during deserialization, binding, + // or validation. This type is mapped to the context so you + // can inject it into your own handlers and use it in your + // application if you want all your errors to look the same. + Errors []Error + + Error struct { + // An error supports zero or more field names, because an + // error can morph three ways: (1) it can indicate something + // wrong with the request as a whole, (2) it can point to a + // specific problem with a particular input field, or (3) it + // can span multiple related input fields. + FieldNames []string `json:"fieldNames,omitempty"` + + // The classification is like an error code, convenient to + // use when processing or categorizing an error programmatically. + // It may also be called the "kind" of error. + Classification string `json:"classification,omitempty"` + + // Message should be human-readable and detailed enough to + // pinpoint and resolve the problem, but it should be brief. For + // example, a payload of 100 objects in a JSON array might have + // an error in the 41st object. The message should help the + // end user find and fix the error with their request. + Message string `json:"message,omitempty"` + } +) + +// Add adds an error associated with the fields indicated +// by fieldNames, with the given classification and message. +func (e *Errors) Add(fieldNames []string, classification, message string) { + *e = append(*e, Error{ + FieldNames: fieldNames, + Classification: classification, + Message: message, + }) +} + +// Len returns the number of errors. +func (e *Errors) Len() int { + return len(*e) +} + +// Has determines whether an Errors slice has an Error with +// a given classification in it; it does not search on messages +// or field names. +func (e *Errors) Has(class string) bool { + for _, err := range *e { + if err.Kind() == class { + return true + } + } + return false +} + +/* +// WithClass gets a copy of errors that are classified by the +// the given classification. +func (e *Errors) WithClass(classification string) Errors { + var errs Errors + for _, err := range *e { + if err.Kind() == classification { + errs = append(errs, err) + } + } + return errs +} + +// ForField gets a copy of errors that are associated with the +// field by the given name. +func (e *Errors) ForField(name string) Errors { + var errs Errors + for _, err := range *e { + for _, fieldName := range err.Fields() { + if fieldName == name { + errs = append(errs, err) + break + } + } + } + return errs +} + +// Get gets errors of a particular class for the specified +// field name. +func (e *Errors) Get(class, fieldName string) Errors { + var errs Errors + for _, err := range *e { + if err.Kind() == class { + for _, nameOfField := range err.Fields() { + if nameOfField == fieldName { + errs = append(errs, err) + break + } + } + } + } + return errs +} +*/ + +// Fields returns the list of field names this error is +// associated with. +func (e Error) Fields() []string { + return e.FieldNames +} + +// Kind returns this error's classification. +func (e Error) Kind() string { + return e.Classification +} + +// Error returns this error's message. +func (e Error) Error() string { + return e.Message +} diff --git a/pkg/macaron/binding/go.mod b/pkg/macaron/binding/go.mod new file mode 100644 index 00000000000..4ac21f88d6d --- /dev/null +++ b/pkg/macaron/binding/go.mod @@ -0,0 +1,7 @@ +module github.com/go-macaron/binding + +go 1.16 + +require gopkg.in/macaron.v1 v1.4.0 + +replace gopkg.in/macaron.v1 => ../ diff --git a/pkg/macaron/binding/go.sum b/pkg/macaron/binding/go.sum new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pkg/macaron/context.go b/pkg/macaron/context.go index a26b37aafdb..d3e917bfdab 100644 --- a/pkg/macaron/context.go +++ b/pkg/macaron/context.go @@ -24,11 +24,6 @@ import ( "strings" ) -// Request represents an HTTP request received by a server or to be sent by a client. -type Request struct { - *http.Request -} - // ContextInvoker is an inject.FastInvoker wrapper of func(ctx *Context). type ContextInvoker func(ctx *Context) @@ -46,7 +41,7 @@ type Context struct { index int *Router - Req Request + Req *http.Request Resp ResponseWriter params Params template *template.Template @@ -144,7 +139,7 @@ func (ctx *Context) Redirect(location string, status ...int) { code = status[0] } - http.Redirect(ctx.Resp, ctx.Req.Request, location, code) + http.Redirect(ctx.Resp, ctx.Req, location, code) } // MaxMemory is the maximum amount of memory to use when parsing a multipart form. diff --git a/pkg/macaron/macaron.go b/pkg/macaron/macaron.go index 782d2b403f6..af0625a18a0 100644 --- a/pkg/macaron/macaron.go +++ b/pkg/macaron/macaron.go @@ -128,7 +128,7 @@ func FromContext(c context.Context) *Context { func (m *Macaron) UseMiddleware(middleware func(http.Handler) http.Handler) { next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { c := FromContext(req.Context()) - c.Req.Request = req + c.Req = req if mrw, ok := rw.(*responseWriter); ok { c.Resp = mrw } else { @@ -163,7 +163,7 @@ func (m *Macaron) createContext(rw http.ResponseWriter, req *http.Request) *Cont c.Map(c) c.MapTo(c.Resp, (*http.ResponseWriter)(nil)) c.Map(req) - c.Req = Request{req} + c.Req = req return c } diff --git a/pkg/middleware/logger.go b/pkg/middleware/logger.go index 455231835ca..d9e1daa411a 100644 --- a/pkg/middleware/logger.go +++ b/pkg/middleware/logger.go @@ -61,7 +61,7 @@ func Logger(cfg *setting.Cfg) macaron.Handler { "referer", req.Referer(), } - traceID, exist := cw.ExtractTraceID(ctxTyped.Req.Request.Context()) + traceID, exist := cw.ExtractTraceID(ctxTyped.Req.Context()) if exist { logParams = append(logParams, "traceID", traceID) } diff --git a/pkg/middleware/request_tracing.go b/pkg/middleware/request_tracing.go index 2a531c0533c..32e44097dae 100644 --- a/pkg/middleware/request_tracing.go +++ b/pkg/middleware/request_tracing.go @@ -22,7 +22,7 @@ var routeOperationNameKey = contextKey{} func ProvideRouteOperationName(name string) macaron.Handler { return func(res http.ResponseWriter, req *http.Request, c *macaron.Context) { ctx := context.WithValue(c.Req.Context(), routeOperationNameKey, name) - c.Req.Request = c.Req.WithContext(ctx) + c.Req = c.Req.WithContext(ctx) } } @@ -51,7 +51,7 @@ func RequestTracing() macaron.Handler { span := tracer.StartSpan(fmt.Sprintf("HTTP %s %s", req.Method, req.URL.Path), ext.RPCServerOption(wireContext)) ctx := opentracing.ContextWithSpan(req.Context(), span) - c.Req.Request = req.WithContext(ctx) + c.Req = req.WithContext(ctx) c.Next() diff --git a/pkg/models/context_test.go b/pkg/models/context_test.go index 4e78843a8fd..5994895f332 100644 --- a/pkg/models/context_test.go +++ b/pkg/models/context_test.go @@ -31,9 +31,7 @@ func TestQueryBoolWithDefault(t *testing.T) { req, err := http.NewRequest("GET", tt.url, nil) require.NoError(t, err) r := ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{Request: req}, - }, + Context: &macaron.Context{Req: req}, } require.Equal(t, tt.expected, r.QueryBoolWithDefault("silenced", tt.defaultValue)) }) diff --git a/pkg/plugins/backendplugin/manager/manager.go b/pkg/plugins/backendplugin/manager/manager.go index 9bbcb94ea39..b4976a0d850 100644 --- a/pkg/plugins/backendplugin/manager/manager.go +++ b/pkg/plugins/backendplugin/manager/manager.go @@ -388,7 +388,7 @@ func (m *Manager) CallResource(pCtx backend.PluginContext, reqCtx *models.ReqCon dsURL = pCtx.DataSourceInstanceSettings.URL } - err := m.PluginRequestValidator.Validate(dsURL, reqCtx.Req.Request) + err := m.PluginRequestValidator.Validate(dsURL, reqCtx.Req) if err != nil { reqCtx.JsonApiErr(http.StatusForbidden, "Access denied", err) return diff --git a/pkg/services/contexthandler/auth_proxy_test.go b/pkg/services/contexthandler/auth_proxy_test.go index 719f780dd3c..072a0004283 100644 --- a/pkg/services/contexthandler/auth_proxy_test.go +++ b/pkg/services/contexthandler/auth_proxy_test.go @@ -59,9 +59,7 @@ func TestInitContextWithAuthProxy_CachedInvalidUserID(t *testing.T) { require.NoError(t, err) ctx := &models.ReqContext{ Context: &macaron.Context{ - Req: macaron.Request{ - Request: req, - }, + Req: req, Data: map[string]interface{}{}, }, Logger: log.New("Test"), diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index 24fbdd83752..e5f7a5c7d5f 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -64,11 +64,7 @@ func prepareMiddleware(t *testing.T, remoteCache *remotecache.RemoteCache, cb fu } ctx := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{ - Request: req, - }, - }, + Context: &macaron.Context{Req: req}, } auth := New(cfg, &Options{ diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 21b00cdfc94..a593d673f2f 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -85,10 +85,10 @@ func (h *ContextHandler) Middleware(mContext *macaron.Context) { } // Inject ReqContext into a request context and replace the request instance in the macaron context - mContext.Req.Request = mContext.Req.WithContext(context.WithValue(mContext.Req.Context(), reqContextKey{}, reqContext)) - mContext.Map(mContext.Req.Request) + mContext.Req = mContext.Req.WithContext(context.WithValue(mContext.Req.Context(), reqContextKey{}, reqContext)) + mContext.Map(mContext.Req) - traceID, exists := cw.ExtractTraceID(mContext.Req.Request.Context()) + traceID, exists := cw.ExtractTraceID(mContext.Req.Context()) if exists { reqContext.Logger = reqContext.Logger.New("traceID", traceID) } diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index bfbab42fc12..b9b0ecbf216 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -96,12 +96,8 @@ func initTokenRotationScenario(ctx context.Context, t *testing.T, ctxHdlr *Conte return nil, nil, err } reqContext := &models.ReqContext{ - Context: &macaron.Context{ - Req: macaron.Request{ - Request: req, - }, - }, - Logger: log.New("testlogger"), + Context: &macaron.Context{Req: req}, + Logger: log.New("testlogger"), } mw := mockWriter{rr} diff --git a/pkg/services/datasourceproxy/datasourceproxy.go b/pkg/services/datasourceproxy/datasourceproxy.go index 234cc02321b..be40693c3f1 100644 --- a/pkg/services/datasourceproxy/datasourceproxy.go +++ b/pkg/services/datasourceproxy/datasourceproxy.go @@ -60,7 +60,7 @@ func (p *DataSourceProxyService) ProxyDatasourceRequestWithID(c *models.ReqConte return } - err = p.PluginRequestValidator.Validate(ds.Url, c.Req.Request) + err = p.PluginRequestValidator.Validate(ds.Url, c.Req) if err != nil { c.JsonApiErr(http.StatusForbidden, "Access denied", err) return diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 024bb8af1ce..b795d1c550c 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -280,9 +280,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo t.Helper() t.Run(desc, func(t *testing.T) { - ctx := macaron.Context{ - Req: macaron.Request{Request: &http.Request{}}, - } + ctx := macaron.Context{Req: &http.Request{}} orgID := int64(1) role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 1e81460199d..467d5c15bb4 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -715,9 +715,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo t.Helper() t.Run(desc, func(t *testing.T) { - ctx := macaron.Context{ - Req: macaron.Request{Request: &http.Request{}}, - } + ctx := macaron.Context{Req: &http.Request{}} cfg := setting.NewCfg() orgID := int64(1) role := models.ROLE_ADMIN diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 4294b1c6d28..791e0c91200 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -282,8 +282,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r } newCtx := centrifuge.SetCredentials(ctx.Req.Context(), cred) newCtx = livecontext.SetContextSignedUser(newCtx, user) - r := ctx.Req.Request - r = r.WithContext(newCtx) + r := ctx.Req.WithContext(newCtx) wsHandler.ServeHTTP(ctx.Resp, r) } @@ -291,8 +290,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r user := ctx.SignedInUser newCtx := livecontext.SetContextSignedUser(ctx.Req.Context(), user) newCtx = livecontext.SetContextStreamID(newCtx, ctx.Params(":streamId")) - r := ctx.Req.Request - r = r.WithContext(newCtx) + r := ctx.Req.WithContext(newCtx) pushWSHandler.ServeHTTP(ctx.Resp, r) } diff --git a/pkg/services/live/pushhttp/push.go b/pkg/services/live/pushhttp/push.go index cae30da5297..c8d0594b865 100644 --- a/pkg/services/live/pushhttp/push.go +++ b/pkg/services/live/pushhttp/push.go @@ -56,7 +56,7 @@ func (g *Gateway) Handle(ctx *models.ReqContext) { urlValues := ctx.Req.URL.Query() frameFormat := pushurl.FrameFormatFromValues(urlValues) - body, err := io.ReadAll(ctx.Req.Request.Body) + body, err := io.ReadAll(ctx.Req.Body) if err != nil { logger.Error("Error reading body", "error", err) ctx.Resp.WriteHeader(http.StatusInternalServerError) diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 30d49d2d118..2f647330841 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -372,7 +372,7 @@ func (srv AlertmanagerSrv) RoutePostTestReceivers(c *models.ReqContext, body api ctx, cancelFunc, err := contextWithTimeoutFromRequest( c.Req.Context(), - c.Req.Request, + c.Req, defaultTestReceiversTimeout, maxTestReceiversTimeout) if err != nil { diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index e863c02b977..d428c3abe5b 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -103,7 +103,7 @@ func (p *AlertingProxy) withReq( req.Header.Add(h, v) } newCtx, resp := replacedResponseWriter(ctx) - newCtx.Req.Request = req + newCtx.Req = req p.DataProxy.ProxyDatasourceRequestWithID(newCtx, ctx.ParamsInt64("Recipient")) status := resp.Status()