Macaron: remove custom Request type (#37874)

* remove macaron.Request, use http.Request instead

* remove com dependency from bindings module

* fix another c.Req.Request
This commit is contained in:
Serge Zaitsev 2021-09-01 11:18:30 +02:00 committed by GitHub
parent 56f61001a8
commit c3ab2fdeb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1205 additions and 111 deletions

2
go.mod
View File

@ -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

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)

View File

@ -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/"

View File

@ -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")
}

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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.

View File

@ -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)
}

View File

@ -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
}

View File

@ -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,
}

191
pkg/macaron/binding/LICENSE Normal file
View File

@ -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.

View File

@ -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
}
)

View File

@ -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
}

View File

@ -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 => ../

View File

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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))
})

View File

@ -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

View File

@ -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"),

View File

@ -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{

View File

@ -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)
}

View File

@ -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}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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 {

View File

@ -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()