diff --git a/.travis.yml b/.travis.yml index f6ec8a82..27c80ef8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: go matrix: fast_finish: true include: - - go: 1.8.x - - go: 1.9.x - go: 1.10.x - go: 1.11.x env: GO111MODULE=on diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea2495d..15dfb1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Gin 1.4.0 - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) -- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) +- [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) diff --git a/README.md b/README.md index 092e91ff..529746a1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi - [Only Bind Query String](#only-bind-query-string) - [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Uri](#bind-uri) + - [Bind Header](#bind-header) - [Bind HTML checkboxes](#bind-html-checkboxes) - [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) @@ -69,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi To install Gin package, you need to install Go and set your Go workspace first. -1. The first need [Go](https://golang.org/) installed (**version 1.8+ is required**), then you can use the below Go command to install Gin. +1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin @@ -621,10 +622,10 @@ Note that you need to set the corresponding binding tag on all fields you want t Also, Gin provides two sets of methods for binding: - **Type** - Must bind - - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` + - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML`, `BindHeader` - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. - **Type** - Should bind - - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML` + - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML`, `ShouldBindHeader` - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use `MustBindWith` or `ShouldBindWith`. @@ -755,7 +756,7 @@ func bookableDate( ) bool { if date, ok := field.Interface().(time.Time); ok { today := time.Now() - if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + if today.After(date) { return false } } @@ -845,9 +846,11 @@ import ( ) type Person struct { - Name string `form:"name"` - Address string `form:"address"` - Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` + Name string `form:"name"` + Address string `form:"address"` + Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` } func main() { @@ -861,11 +864,13 @@ func startPage(c *gin.Context) { // If `GET`, only `Form` binding engine (`query`) used. // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 - if c.ShouldBind(&person) == nil { - log.Println(person.Name) - log.Println(person.Address) - log.Println(person.Birthday) - } + if c.ShouldBind(&person) == nil { + log.Println(person.Name) + log.Println(person.Address) + log.Println(person.Birthday) + log.Println(person.CreateTime) + log.Println(person.UnixTime) + } c.String(200, "Success") } @@ -873,7 +878,7 @@ func startPage(c *gin.Context) { Test it with: ```sh -$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033" ``` ### Bind Uri @@ -910,6 +915,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 $ curl -v localhost:8088/thinkerou/not-uuid ``` +### Bind Header + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" +) + +type testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` +} + +func main() { + r := gin.Default() + r.GET("/", func(c *gin.Context) { + h := testHeader{} + + if err := c.ShouldBindHeader(&h); err != nil { + c.JSON(200, err) + } + + fmt.Printf("%#v\n", h) + c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain}) + }) + + r.Run() + +// client +// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/ +// output +// {"Domain":"music","Rate":300} +} +``` + ### Bind HTML checkboxes See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) @@ -959,32 +1001,36 @@ result: ### Multipart/Urlencoded binding ```go -package main +type ProfileForm struct { + Name string `form:"name" binding:"required"` + Avatar *multipart.FileHeader `form:"avatar" binding:"required"` -import ( - "github.com/gin-gonic/gin" -) - -type LoginForm struct { - User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` + // or for multiple files + // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"` } func main() { router := gin.Default() - router.POST("/login", func(c *gin.Context) { + router.POST("/profile", func(c *gin.Context) { // you can bind multipart form with explicit binding declaration: // c.ShouldBindWith(&form, binding.Form) // or you can simply use autobinding with ShouldBind method: - var form LoginForm + var form ProfileForm // in this case proper binding will be automatically selected - if c.ShouldBind(&form) == nil { - if form.User == "user" && form.Password == "password" { - c.JSON(200, gin.H{"status": "you are logged in"}) - } else { - c.JSON(401, gin.H{"status": "unauthorized"}) - } + if err := c.ShouldBind(&form); err != nil { + c.String(http.StatusBadRequest, "bad request") + return } + + err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename) + if err != nil { + c.String(http.StatusInternalServerError, "unknown error") + return + } + + // db.Save(&form) + + c.String(http.StatusOK, "ok") }) router.Run(":8080") } @@ -992,7 +1038,7 @@ func main() { Test it with: ```sh -$ curl -v --form user=user --form password=password http://localhost:8080/login +$ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile ``` ### XML, JSON, YAML and ProtoBuf rendering @@ -1077,8 +1123,8 @@ Using JSONP to request data from a server in a different domain. Add callback t func main() { r := gin.Default() - r.GET("/JSONP?callback=x", func(c *gin.Context) { - data := map[string]interface{}{ + r.GET("/JSONP", func(c *gin.Context) { + data := gin.H{ "foo": "bar", } @@ -1089,6 +1135,9 @@ func main() { // Listen and serve on 0.0.0.0:8080 r.Run(":8080") + + // client + // curl http://127.0.0.1:8080/JSONP?callback=x } ``` @@ -1101,7 +1150,7 @@ func main() { r := gin.Default() r.GET("/someJSON", func(c *gin.Context) { - data := map[string]interface{}{ + data := gin.H{ "lang": "GO语言", "tag": "
", } @@ -1310,7 +1359,7 @@ func main() { router.LoadHTMLFiles("./testdata/template/raw.tmpl") router.GET("/raw", func(c *gin.Context) { - c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + c.HTML(http.StatusOK, "raw.tmpl", gin.H{ "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), }) }) @@ -2069,3 +2118,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor * [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. +* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes. diff --git a/auth.go b/auth.go index 9ed81b5d..c96b1e29 100644 --- a/auth.go +++ b/auth.go @@ -5,7 +5,6 @@ package gin import ( - "crypto/subtle" "encoding/base64" "net/http" "strconv" @@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string { base := user + ":" + password return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) } - -func secureCompare(given, actual string) bool { - if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { - return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 - } - // Securely compare actual to itself to keep constant time, but always return false. - return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false -} diff --git a/auth_test.go b/auth_test.go index 197e9208..e44bd100 100644 --- a/auth_test.go +++ b/auth_test.go @@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) { assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) } -func TestBasicAuthSecureCompare(t *testing.T) { - assert.True(t, secureCompare("1234567890", "1234567890")) - assert.False(t, secureCompare("123456789", "1234567890")) - assert.False(t, secureCompare("12345678900", "1234567890")) - assert.False(t, secureCompare("1234567891", "1234567890")) -} - func TestBasicAuthSucceed(t *testing.T) { accounts := Accounts{"admin": "password"} router := New() diff --git a/binding/binding.go b/binding/binding.go index 520c5109..6d58c3cd 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -78,6 +78,7 @@ var ( MsgPack = msgpackBinding{} YAML = yamlBinding{} Uri = uriBinding{} + Header = headerBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method diff --git a/binding/binding_test.go b/binding/binding_test.go index 73019207..3d08d693 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -65,8 +65,15 @@ type FooStructUseNumber struct { } type FooBarStructForTimeType struct { - TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` - TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` + TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` + TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` +} + +type FooStructForTimeTypeNotUnixFormat struct { + CreateTime time.Time `form:"createTime" time_format:"unixNano"` + UnixTime time.Time `form:"unixTime" time_format:"unix"` } type FooStructForTimeTypeNotFormat struct { @@ -226,7 +233,10 @@ func TestBindingFormDefaultValue2(t *testing.T) { func TestBindingFormForTime(t *testing.T) { testFormBindingForTime(t, "POST", "/", "/", - "time_foo=2017-11-15&time_bar=", "bar2=foo") + "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo") + testFormBindingForTimeNotUnixFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, "POST", "/", "/", "time_foo=2017-11-15", "bar2=foo") @@ -240,8 +250,11 @@ func TestBindingFormForTime(t *testing.T) { func TestBindingFormForTime2(t *testing.T) { testFormBindingForTime(t, "GET", - "/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", + "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo", "", "") + testFormBindingForTimeNotUnixFormat(t, "POST", + "/", "/", + "time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo") testFormBindingForTimeNotFormat(t, "GET", "/?time_foo=2017-11-15", "/?bar2=foo", "", "") @@ -667,6 +680,31 @@ func TestRequiredFails(t *testing.T) { assert.Error(t, err) } +func TestHeaderBinding(t *testing.T) { + h := Header + assert.Equal(t, "header", h.Name()) + + type tHeader struct { + Limit int `header:"limit"` + } + + var theader tHeader + req := requestWithBody("GET", "/", "") + req.Header.Add("limit", "1000") + assert.NoError(t, h.Bind(req, &theader)) + assert.Equal(t, 1000, theader.Limit) + + req = requestWithBody("GET", "/", "") + req.Header.Add("fail", `{fail:fail}`) + + type failStruct struct { + Fail map[string]interface{} `header:"fail"` + } + + err := h.Bind(req, &failStruct{}) + assert.Error(t, err) +} + func TestUriBinding(t *testing.T) { b := Uri assert.Equal(t, "uri", b.Name()) @@ -824,6 +862,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, "UTC", obj.TimeBar.Location().String()) + assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano()) + assert.Equal(t, int64(1562400033), obj.UnixTime.Unix()) obj = FooBarStructForTimeType{} req = requestWithBody(method, badPath, badBody) @@ -831,6 +871,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s assert.Error(t, err) } +func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, "form", b.Name()) + + obj := FooStructForTimeTypeNotUnixFormat{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.Error(t, err) + + obj = FooStructForTimeTypeNotUnixFormat{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { b := Form assert.Equal(t, "form", b.Name()) diff --git a/binding/form.go b/binding/form.go index 0b28aa8a..9e9fc3de 100644 --- a/binding/form.go +++ b/binding/form.go @@ -5,9 +5,7 @@ package binding import ( - "mime/multipart" "net/http" - "reflect" ) const defaultMemory = 32 * 1024 * 1024 @@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { return validate(obj) } - -type multipartRequest http.Request - -var _ setter = (*multipartRequest)(nil) - -var ( - multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{}) -) - -// TrySet tries to set a value by the multipart request with the binding a form file -func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { - if value.Type() == multipartFileHeaderStructType { - _, file, err := (*http.Request)(r).FormFile(key) - if err != nil { - return false, err - } - if file != nil { - value.Set(reflect.ValueOf(*file)) - return true, nil - } - } - - return setByForm(value, field, r.MultipartForm.Value, key, opt) -} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index ebf3b199..80b1d15a 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -266,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val timeFormat = time.RFC3339 } + switch tf := strings.ToLower(timeFormat); tf { + case "unix", "unixnano": + tv, err := strconv.ParseInt(val, 10, 0) + if err != nil { + return err + } + + d := time.Duration(1) + if tf == "unixnano" { + d = time.Second + } + + t := time.Unix(tv/int64(d), tv%int64(d)) + value.Set(reflect.ValueOf(t)) + return nil + + } + if val == "" { value.Set(reflect.ValueOf(time.Time{})) return nil diff --git a/binding/header.go b/binding/header.go new file mode 100644 index 00000000..179ce4ea --- /dev/null +++ b/binding/header.go @@ -0,0 +1,34 @@ +package binding + +import ( + "net/http" + "net/textproto" + "reflect" +) + +type headerBinding struct{} + +func (headerBinding) Name() string { + return "header" +} + +func (headerBinding) Bind(req *http.Request, obj interface{}) error { + + if err := mapHeader(obj, req.Header); err != nil { + return err + } + + return validate(obj) +} + +func mapHeader(ptr interface{}, h map[string][]string) error { + return mappingByPtr(ptr, headerSource(h), "header") +} + +type headerSource map[string][]string + +var _ setter = headerSource(nil) + +func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) { + return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt) +} diff --git a/binding/multipart_form_mapping.go b/binding/multipart_form_mapping.go new file mode 100644 index 00000000..f85a1aa6 --- /dev/null +++ b/binding/multipart_form_mapping.go @@ -0,0 +1,66 @@ +// Copyright 2019 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "mime/multipart" + "net/http" + "reflect" +) + +type multipartRequest http.Request + +var _ setter = (*multipartRequest)(nil) + +// TrySet tries to set a value by the multipart request with the binding a form file +func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { + if files := r.MultipartForm.File[key]; len(files) != 0 { + return setByMultipartFormFile(value, field, files) + } + + return setByForm(value, field, r.MultipartForm.Value, key, opt) +} + +func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + switch value.Kind() { + case reflect.Ptr: + switch value.Interface().(type) { + case *multipart.FileHeader: + value.Set(reflect.ValueOf(files[0])) + return true, nil + } + case reflect.Struct: + switch value.Interface().(type) { + case multipart.FileHeader: + value.Set(reflect.ValueOf(*files[0])) + return true, nil + } + case reflect.Slice: + slice := reflect.MakeSlice(value.Type(), len(files), len(files)) + isSetted, err = setArrayOfMultipartFormFiles(slice, field, files) + if err != nil || !isSetted { + return isSetted, err + } + value.Set(slice) + return true, nil + case reflect.Array: + return setArrayOfMultipartFormFiles(value, field, files) + } + return false, errors.New("unsupported field type for multipart.FileHeader") +} + +func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) { + if value.Len() != len(files) { + return false, errors.New("unsupported len of array for []*multipart.FileHeader") + } + for i := range files { + setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1]) + if err != nil || !setted { + return setted, err + } + } + return true, nil +} diff --git a/binding/multipart_form_mapping_test.go b/binding/multipart_form_mapping_test.go new file mode 100644 index 00000000..4c75d1fe --- /dev/null +++ b/binding/multipart_form_mapping_test.go @@ -0,0 +1,138 @@ +// Copyright 2019 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "bytes" + "io/ioutil" + "mime/multipart" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormMultipartBindingBindOneFile(t *testing.T) { + var s struct { + FileValue multipart.FileHeader `form:"file"` + FilePtr *multipart.FileHeader `form:"file"` + SliceValues []multipart.FileHeader `form:"file"` + SlicePtrs []*multipart.FileHeader `form:"file"` + ArrayValues [1]multipart.FileHeader `form:"file"` + ArrayPtrs [1]*multipart.FileHeader `form:"file"` + } + file := testFile{"file", "file1", []byte("hello")} + + req := createRequestMultipartFiles(t, file) + err := FormMultipart.Bind(req, &s) + assert.NoError(t, err) + + assertMultipartFileHeader(t, &s.FileValue, file) + assertMultipartFileHeader(t, s.FilePtr, file) + assert.Len(t, s.SliceValues, 1) + assertMultipartFileHeader(t, &s.SliceValues[0], file) + assert.Len(t, s.SlicePtrs, 1) + assertMultipartFileHeader(t, s.SlicePtrs[0], file) + assertMultipartFileHeader(t, &s.ArrayValues[0], file) + assertMultipartFileHeader(t, s.ArrayPtrs[0], file) +} + +func TestFormMultipartBindingBindTwoFiles(t *testing.T) { + var s struct { + SliceValues []multipart.FileHeader `form:"file"` + SlicePtrs []*multipart.FileHeader `form:"file"` + ArrayValues [2]multipart.FileHeader `form:"file"` + ArrayPtrs [2]*multipart.FileHeader `form:"file"` + } + files := []testFile{ + {"file", "file1", []byte("hello")}, + {"file", "file2", []byte("world")}, + } + + req := createRequestMultipartFiles(t, files...) + err := FormMultipart.Bind(req, &s) + assert.NoError(t, err) + + assert.Len(t, s.SliceValues, len(files)) + assert.Len(t, s.SlicePtrs, len(files)) + assert.Len(t, s.ArrayValues, len(files)) + assert.Len(t, s.ArrayPtrs, len(files)) + + for i, file := range files { + assertMultipartFileHeader(t, &s.SliceValues[i], file) + assertMultipartFileHeader(t, s.SlicePtrs[i], file) + assertMultipartFileHeader(t, &s.ArrayValues[i], file) + assertMultipartFileHeader(t, s.ArrayPtrs[i], file) + } +} + +func TestFormMultipartBindingBindError(t *testing.T) { + files := []testFile{ + {"file", "file1", []byte("hello")}, + {"file", "file2", []byte("world")}, + } + + for _, tt := range []struct { + name string + s interface{} + }{ + {"wrong type", &struct { + Files int `form:"file"` + }{}}, + {"wrong array size", &struct { + Files [1]*multipart.FileHeader `form:"file"` + }{}}, + {"wrong slice type", &struct { + Files []int `form:"file"` + }{}}, + } { + req := createRequestMultipartFiles(t, files...) + err := FormMultipart.Bind(req, tt.s) + assert.Error(t, err) + } +} + +type testFile struct { + Fieldname string + Filename string + Content []byte +} + +func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request { + var body bytes.Buffer + + mw := multipart.NewWriter(&body) + for _, file := range files { + fw, err := mw.CreateFormFile(file.Fieldname, file.Filename) + assert.NoError(t, err) + + n, err := fw.Write(file.Content) + assert.NoError(t, err) + assert.Equal(t, len(file.Content), n) + } + err := mw.Close() + assert.NoError(t, err) + + req, err := http.NewRequest("POST", "/", &body) + assert.NoError(t, err) + + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary()) + return req +} + +func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) { + assert.Equal(t, file.Filename, fh.Filename) + // assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8 + + fl, err := fh.Open() + assert.NoError(t, err) + + body, err := ioutil.ReadAll(fl) + assert.NoError(t, err) + assert.Equal(t, string(file.Content), string(body)) + + err = fl.Close() + assert.NoError(t, err) +} diff --git a/context.go b/context.go index 425b627f..d9fcc285 100644 --- a/context.go +++ b/context.go @@ -60,6 +60,13 @@ type Context struct { // Accepted defines a list of manually accepted formats for content negotiation. Accepted []string + + // queryCache use url.ParseQuery cached the param query result from c.Request.URL.Query() + queryCache url.Values + + // formCache use url.ParseQuery cached PostForm contains the parsed form data from POST, PATCH, + // or PUT body parameters. + formCache url.Values } /************************************/ @@ -75,6 +82,8 @@ func (c *Context) reset() { c.Keys = nil c.Errors = c.Errors[0:0] c.Accepted = nil + c.queryCache = nil + c.formCache = nil } // Copy returns a copy of the current context that can be safely used outside the request's scope. @@ -89,6 +98,9 @@ func (c *Context) Copy() *Context { for k, v := range c.Keys { cp.Keys[k] = v } + paramCopy := make([]Param, len(cp.Params)) + copy(paramCopy, cp.Params) + cp.Params = paramCopy return &cp } @@ -379,10 +391,18 @@ func (c *Context) QueryArray(key string) []string { return values } +func (c *Context) getQueryCache() { + if c.queryCache == nil { + c.queryCache = make(url.Values) + c.queryCache, _ = url.ParseQuery(c.Request.URL.RawQuery) + } +} + // GetQueryArray returns a slice of strings for a given query key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetQueryArray(key string) ([]string, bool) { - if values, ok := c.Request.URL.Query()[key]; ok && len(values) > 0 { + c.getQueryCache() + if values, ok := c.queryCache[key]; ok && len(values) > 0 { return values, true } return []string{}, false @@ -397,7 +417,8 @@ func (c *Context) QueryMap(key string) map[string]string { // GetQueryMap returns a map for a given query key, plus a boolean value // whether at least one value exists for the given key. func (c *Context) GetQueryMap(key string) (map[string]string, bool) { - return c.get(c.Request.URL.Query(), key) + c.getQueryCache() + return c.get(c.queryCache, key) } // PostForm returns the specified key from a POST urlencoded form or multipart form @@ -438,16 +459,24 @@ func (c *Context) PostFormArray(key string) []string { return values } +func (c *Context) getFormCache() { + if c.formCache == nil { + c.formCache = make(url.Values) + req := c.Request + if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if err != http.ErrNotMultipart { + debugPrint("error on parse multipart form array: %v", err) + } + } + c.formCache = req.PostForm + } +} + // GetPostFormArray returns a slice of strings for a given form key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetPostFormArray(key string) ([]string, bool) { - req := c.Request - if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { - if err != http.ErrNotMultipart { - debugPrint("error on parse multipart form array: %v", err) - } - } - if values := req.PostForm[key]; len(values) > 0 { + c.getFormCache() + if values := c.formCache[key]; len(values) > 0 { return values, true } return []string{}, false @@ -554,6 +583,11 @@ func (c *Context) BindYAML(obj interface{}) error { return c.MustBindWith(obj, binding.YAML) } +// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header). +func (c *Context) BindHeader(obj interface{}) error { + return c.MustBindWith(obj, binding.Header) +} + // BindUri binds the passed struct pointer using binding.Uri. // It will abort the request with HTTP 400 if any error occurs. func (c *Context) BindUri(obj interface{}) error { @@ -608,6 +642,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error { return c.ShouldBindWith(obj, binding.YAML) } +// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header). +func (c *Context) ShouldBindHeader(obj interface{}) error { + return c.ShouldBindWith(obj, binding.Header) +} + // ShouldBindUri binds the passed struct pointer using the specified binding engine. func (c *Context) ShouldBindUri(obj interface{}) error { m := make(map[string][]string) diff --git a/context_test.go b/context_test.go index e8dcd3dc..f7bb0f51 100644 --- a/context_test.go +++ b/context_test.go @@ -6,6 +6,7 @@ package gin import ( "bytes" + "context" "errors" "fmt" "html/template" @@ -13,8 +14,10 @@ import ( "mime/multipart" "net/http" "net/http/httptest" + "os" "reflect" "strings" + "sync" "testing" "time" @@ -22,7 +25,6 @@ import ( "github.com/gin-gonic/gin/binding" "github.com/golang/protobuf/proto" "github.com/stretchr/testify/assert" - "golang.org/x/net/context" testdata "github.com/gin-gonic/gin/testdata/protoexample" ) @@ -674,7 +676,7 @@ func TestContextRenderJSONP(t *testing.T) { c.JSONP(http.StatusCreated, H{"foo": "bar"}) assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "x({\"foo\":\"bar\"})", w.Body.String()) + assert.Equal(t, "x({\"foo\":\"bar\"});", w.Body.String()) assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type")) } @@ -1434,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextBindHeader(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("rate", "8000") + c.Request.Header.Add("domain", "music") + c.Request.Header.Add("limit", "1000") + + var testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` + Limit int `header:"limit"` + } + + assert.NoError(t, c.BindHeader(&testHeader)) + assert.Equal(t, 8000, testHeader.Rate) + assert.Equal(t, "music", testHeader.Domain) + assert.Equal(t, 1000, testHeader.Limit) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1541,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) { assert.Equal(t, 0, w.Body.Len()) } +func TestContextShouldBindHeader(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("rate", "8000") + c.Request.Header.Add("domain", "music") + c.Request.Header.Add("limit", "1000") + + var testHeader struct { + Rate int `header:"Rate"` + Domain string `header:"Domain"` + Limit int `header:"limit"` + } + + assert.NoError(t, c.ShouldBindHeader(&testHeader)) + assert.Equal(t, 8000, testHeader.Rate) + assert.Equal(t, "music", testHeader.Domain) + assert.Equal(t, 1000, testHeader.Limit) + assert.Equal(t, 0, w.Body.Len()) +} + func TestContextShouldBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) @@ -1821,3 +1867,24 @@ func TestContextResetInHandler(t *testing.T) { c.Next() }) } + +func TestRaceParamsContextCopy(t *testing.T) { + DefaultWriter = os.Stdout + router := Default() + nameGroup := router.Group("/:name") + var wg sync.WaitGroup + wg.Add(2) + { + nameGroup.GET("/api", func(c *Context) { + go func(c *Context, param string) { + defer wg.Done() + // First assert must be executed after the second request + time.Sleep(50 * time.Millisecond) + assert.Equal(t, c.Param("name"), param) + }(c.Copy(), c.Param("name")) + }) + } + performRequest(router, "GET", "/name1/api") + performRequest(router, "GET", "/name2/api") + wg.Wait() +} diff --git a/debug.go b/debug.go index 19e380fb..49080dbf 100644 --- a/debug.go +++ b/debug.go @@ -5,7 +5,6 @@ package gin import ( - "bytes" "fmt" "html/template" "runtime" @@ -13,7 +12,7 @@ import ( "strings" ) -const ginSupportMinGoVer = 8 +const ginSupportMinGoVer = 10 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. @@ -38,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { func debugPrintLoadTemplate(tmpl *template.Template) { if IsDebugging() { - var buf bytes.Buffer + var buf strings.Builder for _, tmpl := range tmpl.Templates() { buf.WriteString("\t- ") buf.WriteString(tmpl.Name()) @@ -68,7 +67,7 @@ func getMinVer(v string) (uint64, error) { func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { - debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. + debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon. `) } diff --git a/debug_test.go b/debug_test.go index 9ace2989..d6f320ef 100644 --- a/debug_test.go +++ b/debug_test.go @@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) { }) m, e := getMinVer(runtime.Version()) if e == nil && m <= ginSupportMinGoVer { - assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) + assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } else { assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } diff --git a/errors.go b/errors.go index 6070ff55..25e8ff60 100644 --- a/errors.go +++ b/errors.go @@ -5,9 +5,9 @@ package gin import ( - "bytes" "fmt" "reflect" + "strings" "github.com/gin-gonic/gin/internal/json" ) @@ -158,7 +158,7 @@ func (a errorMsgs) String() string { if len(a) == 0 { return "" } - var buffer bytes.Buffer + var buffer strings.Builder for i, msg := range a { fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) if msg.Meta != nil { diff --git a/gin.go b/gin.go index 220f0401..cbdd080e 100644 --- a/gin.go +++ b/gin.go @@ -437,7 +437,6 @@ func serveError(c *Context, code int, defaultMessage []byte) { return } c.writermem.WriteHeaderNow() - return } func redirectTrailingSlash(c *Context) { diff --git a/logger.go b/logger.go index 5ab4639e..fcf90c25 100644 --- a/logger.go +++ b/logger.go @@ -22,18 +22,19 @@ const ( forceColor ) -var ( - green = string([]byte{27, 91, 57, 55, 59, 52, 50, 109}) - white = string([]byte{27, 91, 57, 48, 59, 52, 55, 109}) - yellow = string([]byte{27, 91, 57, 48, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - blue = string([]byte{27, 91, 57, 55, 59, 52, 52, 109}) - magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109}) - cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) - reset = string([]byte{27, 91, 48, 109}) - consoleColorMode = autoColor +const ( + green = "\033[97;42m" + white = "\033[90;47m" + yellow = "\033[90;43m" + red = "\033[97;41m" + blue = "\033[97;44m" + magenta = "\033[97;45m" + cyan = "\033[97;46m" + reset = "\033[0m" ) +var consoleColorMode = autoColor + // LoggerConfig defines the config for Logger middleware. type LoggerConfig struct { // Optional. Default value is gin.defaultLogFormatter diff --git a/logger_test.go b/logger_test.go index 9177e1d9..fc53f356 100644 --- a/logger_test.go +++ b/logger_test.go @@ -291,14 +291,14 @@ func TestColorForMethod(t *testing.T) { return p.MethodColor() } - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue") - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan") - assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow") - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForMethod("DELETE"), "delete should be red") - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForMethod("PATCH"), "patch should be green") - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), colorForMethod("HEAD"), "head should be magenta") - assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForMethod("OPTIONS"), "options should be white") - assert.Equal(t, string([]byte{27, 91, 48, 109}), colorForMethod("TRACE"), "trace is not defined and should be the reset color") + assert.Equal(t, blue, colorForMethod("GET"), "get should be blue") + assert.Equal(t, cyan, colorForMethod("POST"), "post should be cyan") + assert.Equal(t, yellow, colorForMethod("PUT"), "put should be yellow") + assert.Equal(t, red, colorForMethod("DELETE"), "delete should be red") + assert.Equal(t, green, colorForMethod("PATCH"), "patch should be green") + assert.Equal(t, magenta, colorForMethod("HEAD"), "head should be magenta") + assert.Equal(t, white, colorForMethod("OPTIONS"), "options should be white") + assert.Equal(t, reset, colorForMethod("TRACE"), "trace is not defined and should be the reset color") } func TestColorForStatus(t *testing.T) { @@ -309,10 +309,10 @@ func TestColorForStatus(t *testing.T) { return p.StatusCodeColor() } - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green") - assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white") - assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow") - assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red") + assert.Equal(t, green, colorForStatus(http.StatusOK), "2xx should be green") + assert.Equal(t, white, colorForStatus(http.StatusMovedPermanently), "3xx should be white") + assert.Equal(t, yellow, colorForStatus(http.StatusNotFound), "4xx should be yellow") + assert.Equal(t, red, colorForStatus(2), "other things should be red") } func TestResetColor(t *testing.T) { diff --git a/render/json.go b/render/json.go index 2b07cba0..70506f78 100644 --- a/render/json.go +++ b/render/json.go @@ -138,7 +138,7 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) { if err != nil { return err } - _, err = w.Write([]byte(")")) + _, err = w.Write([]byte(");")) if err != nil { return err } diff --git a/render/reader.go b/render/reader.go index 312af741..502d9398 100644 --- a/render/reader.go +++ b/render/reader.go @@ -21,7 +21,9 @@ type Reader struct { // Render (Reader) writes data with custom ContentType and headers. func (r Reader) Render(w http.ResponseWriter) (err error) { r.WriteContentType(w) - r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) + if r.ContentLength >= 0 { + r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) + } r.writeHeaders(w, r.Headers) _, err = io.Copy(w, r.Reader) return diff --git a/render/render_test.go b/render/render_test.go index 9d7eaeef..b27134ff 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -45,7 +45,7 @@ func TestRenderMsgPack(t *testing.T) { err = codec.NewEncoder(buf, h).Encode(data) assert.NoError(t, err) - assert.Equal(t, w.Body.String(), string(buf.Bytes())) + assert.Equal(t, w.Body.String(), buf.String()) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) } @@ -146,7 +146,7 @@ func TestRenderJsonpJSON(t *testing.T) { err1 := (JsonpJSON{"x", data}).Render(w1) assert.NoError(t, err1) - assert.Equal(t, "x({\"foo\":\"bar\"})", w1.Body.String()) + assert.Equal(t, "x({\"foo\":\"bar\"});", w1.Body.String()) assert.Equal(t, "application/javascript; charset=utf-8", w1.Header().Get("Content-Type")) w2 := httptest.NewRecorder() @@ -158,7 +158,7 @@ func TestRenderJsonpJSON(t *testing.T) { err2 := (JsonpJSON{"x", datas}).Render(w2) assert.NoError(t, err2) - assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}])", w2.Body.String()) + assert.Equal(t, "x([{\"foo\":\"bar\"},{\"bar\":\"foo\"}]);", w2.Body.String()) assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type")) } @@ -498,3 +498,26 @@ func TestRenderReader(t *testing.T) { assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) } + +func TestRenderReaderNoContentLength(t *testing.T) { + w := httptest.NewRecorder() + + body := "#!PNG some raw data" + headers := make(map[string]string) + headers["Content-Disposition"] = `attachment; filename="filename.png"` + headers["x-request-id"] = "requestId" + + err := (Reader{ + ContentLength: -1, + ContentType: "image/png", + Reader: strings.NewReader(body), + Headers: headers, + }).Render(w) + + assert.NoError(t, err) + assert.Equal(t, body, w.Body.String()) + assert.Equal(t, "image/png", w.Header().Get("Content-Type")) + assert.NotContains(t, "Content-Length", w.Header()) + assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) + assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) +} diff --git a/routes_test.go b/routes_test.go index 457c923e..0c2f9a0c 100644 --- a/routes_test.go +++ b/routes_test.go @@ -560,10 +560,15 @@ func TestRouteContextHoldsFullPath(t *testing.T) { // Test routes routes := []string{ - "/", "/simple", "/project/:name", + "/", + "/news/home", + "/news", + "/simple-two/one", + "/simple-two/one-two", "/project/:name/build/*params", + "/project/:name/bui", } for _, route := range routes { diff --git a/tree.go b/tree.go index 9a789f2f..371d5ad1 100644 --- a/tree.go +++ b/tree.go @@ -128,6 +128,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) { n.priority++ numParams := countParams(path) + parentFullPathIndex := 0 + // non-empty tree if len(n.path) > 0 || len(n.children) > 0 { walk: @@ -155,7 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { children: n.children, handlers: n.handlers, priority: n.priority - 1, - fullPath: fullPath, + fullPath: n.fullPath, } // Update maxParams (max of all children) @@ -171,6 +173,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { n.path = path[:i] n.handlers = nil n.wildChild = false + n.fullPath = fullPath[:parentFullPathIndex+i] } // Make new node a child of this node @@ -178,6 +181,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { path = path[i:] if n.wildChild { + parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ @@ -211,6 +215,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { // slash after param if n.nType == param && c == '/' && len(n.children) == 1 { + parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ continue walk @@ -219,6 +224,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) { // Check if a child with the next path byte exists for i := 0; i < len(n.indices); i++ { if c == n.indices[i] { + parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) n = n.children[i] continue walk @@ -369,6 +375,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle // insert remaining path part and handle to the leaf n.path = path[offset:] n.handlers = handlers + n.fullPath = fullPath } // nodeValue holds return values of (*Node).getValue method diff --git a/utils.go b/utils.go index f4532d56..71b80de7 100644 --- a/utils.go +++ b/utils.go @@ -146,6 +146,6 @@ func resolveAddress(addr []string) string { case 1: return addr[0] default: - panic("too much parameters") + panic("too many parameters") } } diff --git a/vendor/vendor.json b/vendor/vendor.json index 349b6f92..d441d4a6 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -11,10 +11,12 @@ "versionExact": "v1.1.1" }, { - "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", + "checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=", "path": "github.com/gin-contrib/sse", - "revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae", - "revisionTime": "2019-03-01T06:25:29Z" + "revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f", + "revisionTime": "2019-06-02T15:02:53Z", + "version": "v0.1", + "versionExact": "v0.1.0" }, { "checksumSHA1": "b4DmyMT9bicTRVJw1hJXHLhIH+0=", @@ -111,18 +113,12 @@ "versionExact": "v1.2.2" }, { - "checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=", + "checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=", "path": "github.com/ugorji/go/codec", - "revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13", - "revisionTime": "2019-04-08T19:08:48Z", + "revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f", + "revisionTime": "2019-07-02T14:15:27Z", "version": "v1.1", - "versionExact": "v1.1.4" - }, - { - "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", - "path": "golang.org/x/net/context", - "revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7", - "revisionTime": "2019-05-02T22:26:14Z" + "versionExact": "v1.1.6" }, { "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",