├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── csrf.go ├── csrf_test.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.10" 5 | - tip 6 | 7 | script: 8 | - go test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nikita Koptelov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin-csrf [![Build Status](https://travis-ci.org/utrack/gin-csrf.svg?branch=master)](https://travis-ci.org/utrack/gin-csrf) 2 | 3 | CSRF protection middleware for [Gin]. This middleware has to be used with [gin-contrib/sessions](https://github.com/gin-contrib/sessions). 4 | 5 | Original credit to [tommy351](https://github.com/tommy351/gin-csrf), this fork makes it work with gin-gonic contrib sessions. 6 | 7 | ## Installation 8 | 9 | ``` bash 10 | $ go get github.com/utrack/gin-csrf 11 | ``` 12 | 13 | ## Usage 14 | 15 | ``` go 16 | package main 17 | 18 | import ( 19 | "github.com/gin-contrib/sessions" 20 | "github.com/gin-contrib/sessions/cookie" 21 | "github.com/gin-gonic/gin" 22 | "github.com/utrack/gin-csrf" 23 | ) 24 | 25 | func main() { 26 | r := gin.Default() 27 | store := cookie.NewStore([]byte("secret")) 28 | r.Use(sessions.Sessions("mysession", store)) 29 | r.Use(csrf.Middleware(csrf.Options{ 30 | Secret: "secret123", 31 | ErrorFunc: func(c *gin.Context) { 32 | c.String(400, "CSRF token mismatch") 33 | c.Abort() 34 | }, 35 | })) 36 | 37 | r.GET("/protected", func(c *gin.Context) { 38 | c.String(200, csrf.GetToken(c)) 39 | }) 40 | 41 | r.POST("/protected", func(c *gin.Context) { 42 | c.String(200, "CSRF token is valid") 43 | }) 44 | 45 | r.Run(":8080") 46 | } 47 | 48 | ``` 49 | 50 | [Gin]: http://gin-gonic.github.io/gin/ 51 | [gin-sessions]: https://github.com/utrack/gin-sessions 52 | -------------------------------------------------------------------------------- /csrf.go: -------------------------------------------------------------------------------- 1 | package csrf 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "errors" 7 | "io" 8 | 9 | "github.com/dchest/uniuri" 10 | "github.com/gin-contrib/sessions" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | const ( 15 | csrfSecret = "csrfSecret" 16 | csrfSalt = "csrfSalt" 17 | csrfToken = "csrfToken" 18 | ) 19 | 20 | var defaultIgnoreMethods = []string{"GET", "HEAD", "OPTIONS"} 21 | 22 | var defaultErrorFunc = func(c *gin.Context) { 23 | panic(errors.New("CSRF token mismatch")) 24 | } 25 | 26 | var defaultTokenGetter = func(c *gin.Context) string { 27 | r := c.Request 28 | 29 | if t := r.FormValue("_csrf"); len(t) > 0 { 30 | return t 31 | } else if t := r.URL.Query().Get("_csrf"); len(t) > 0 { 32 | return t 33 | } else if t := r.Header.Get("X-CSRF-TOKEN"); len(t) > 0 { 34 | return t 35 | } else if t := r.Header.Get("X-XSRF-TOKEN"); len(t) > 0 { 36 | return t 37 | } 38 | 39 | return "" 40 | } 41 | 42 | // Options stores configurations for a CSRF middleware. 43 | type Options struct { 44 | Secret string 45 | IgnoreMethods []string 46 | ErrorFunc gin.HandlerFunc 47 | TokenGetter func(c *gin.Context) string 48 | } 49 | 50 | func tokenize(secret, salt string) string { 51 | h := sha1.New() 52 | io.WriteString(h, salt+"-"+secret) 53 | hash := base64.URLEncoding.EncodeToString(h.Sum(nil)) 54 | 55 | return hash 56 | } 57 | 58 | func inArray(arr []string, value string) bool { 59 | inarr := false 60 | 61 | for _, v := range arr { 62 | if v == value { 63 | inarr = true 64 | break 65 | } 66 | } 67 | 68 | return inarr 69 | } 70 | 71 | // Middleware validates CSRF token. 72 | func Middleware(options Options) gin.HandlerFunc { 73 | ignoreMethods := options.IgnoreMethods 74 | errorFunc := options.ErrorFunc 75 | tokenGetter := options.TokenGetter 76 | 77 | if ignoreMethods == nil { 78 | ignoreMethods = defaultIgnoreMethods 79 | } 80 | 81 | if errorFunc == nil { 82 | errorFunc = defaultErrorFunc 83 | } 84 | 85 | if tokenGetter == nil { 86 | tokenGetter = defaultTokenGetter 87 | } 88 | 89 | return func(c *gin.Context) { 90 | session := sessions.Default(c) 91 | c.Set(csrfSecret, options.Secret) 92 | 93 | if inArray(ignoreMethods, c.Request.Method) { 94 | c.Next() 95 | return 96 | } 97 | 98 | salt, ok := session.Get(csrfSalt).(string) 99 | 100 | if !ok || len(salt) == 0 { 101 | errorFunc(c) 102 | return 103 | } 104 | 105 | token := tokenGetter(c) 106 | 107 | if tokenize(options.Secret, salt) != token { 108 | errorFunc(c) 109 | return 110 | } 111 | 112 | c.Next() 113 | } 114 | } 115 | 116 | // GetToken returns a CSRF token. 117 | func GetToken(c *gin.Context) string { 118 | session := sessions.Default(c) 119 | secret := c.MustGet(csrfSecret).(string) 120 | 121 | if t, ok := c.Get(csrfToken); ok { 122 | return t.(string) 123 | } 124 | 125 | salt, ok := session.Get(csrfSalt).(string) 126 | if !ok { 127 | salt = uniuri.New() 128 | session.Set(csrfSalt, salt) 129 | session.Save() 130 | } 131 | token := tokenize(secret, salt) 132 | c.Set(csrfToken, token) 133 | 134 | return token 135 | } 136 | -------------------------------------------------------------------------------- /csrf_test.go: -------------------------------------------------------------------------------- 1 | package csrf 2 | 3 | import ( 4 | "github.com/gin-contrib/sessions/cookie" 5 | "io" 6 | "net/http" 7 | "net/http/httptest" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/gin-contrib/sessions" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func init() { 16 | gin.SetMode(gin.TestMode) 17 | } 18 | 19 | func newServer(options Options) *gin.Engine { 20 | g := gin.New() 21 | 22 | store := cookie.NewStore([]byte("secret123")) 23 | 24 | g.Use(sessions.Sessions("my_session", store)) 25 | g.Use(Middleware(options)) 26 | 27 | return g 28 | } 29 | 30 | type requestOptions struct { 31 | Method string 32 | URL string 33 | Headers map[string]string 34 | Body io.Reader 35 | } 36 | 37 | func request(server *gin.Engine, options requestOptions) *httptest.ResponseRecorder { 38 | if options.Method == "" { 39 | options.Method = "GET" 40 | } 41 | 42 | w := httptest.NewRecorder() 43 | req, err := http.NewRequest(options.Method, options.URL, options.Body) 44 | 45 | if options.Headers != nil { 46 | for key, value := range options.Headers { 47 | req.Header.Set(key, value) 48 | } 49 | } 50 | 51 | server.ServeHTTP(w, req) 52 | 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | return w 58 | } 59 | 60 | func TestForm(t *testing.T) { 61 | var token string 62 | g := newServer(Options{ 63 | Secret: "secret123", 64 | }) 65 | 66 | g.GET("/login", func(c *gin.Context) { 67 | token = GetToken(c) 68 | }) 69 | 70 | g.POST("/login", func(c *gin.Context) { 71 | c.String(http.StatusOK, "OK") 72 | }) 73 | 74 | r1 := request(g, requestOptions{URL: "/login"}) 75 | r2 := request(g, requestOptions{ 76 | Method: "POST", 77 | URL: "/login", 78 | Headers: map[string]string{ 79 | "Cookie": r1.Header().Get("Set-Cookie"), 80 | "Content-Type": "application/x-www-form-urlencoded", 81 | }, 82 | Body: strings.NewReader("_csrf=" + token), 83 | }) 84 | 85 | if body := r2.Body.String(); body != "OK" { 86 | t.Error("Response is not OK: ", body) 87 | } 88 | } 89 | 90 | func TestQueryString(t *testing.T) { 91 | var token string 92 | g := newServer(Options{ 93 | Secret: "secret123", 94 | }) 95 | 96 | g.GET("/login", func(c *gin.Context) { 97 | token = GetToken(c) 98 | }) 99 | 100 | g.POST("/login", func(c *gin.Context) { 101 | c.String(http.StatusOK, "OK") 102 | }) 103 | 104 | r1 := request(g, requestOptions{URL: "/login"}) 105 | r2 := request(g, requestOptions{ 106 | Method: "POST", 107 | URL: "/login?_csrf=" + token, 108 | Headers: map[string]string{ 109 | "Cookie": r1.Header().Get("Set-Cookie"), 110 | }, 111 | }) 112 | 113 | if body := r2.Body.String(); body != "OK" { 114 | t.Error("Response is not OK: ", body) 115 | } 116 | } 117 | 118 | func TestQueryHeader1(t *testing.T) { 119 | var token string 120 | g := newServer(Options{ 121 | Secret: "secret123", 122 | }) 123 | 124 | g.GET("/login", func(c *gin.Context) { 125 | token = GetToken(c) 126 | }) 127 | 128 | g.POST("/login", func(c *gin.Context) { 129 | c.String(http.StatusOK, "OK") 130 | }) 131 | 132 | r1 := request(g, requestOptions{URL: "/login"}) 133 | r2 := request(g, requestOptions{ 134 | Method: "POST", 135 | URL: "/login", 136 | Headers: map[string]string{ 137 | "Cookie": r1.Header().Get("Set-Cookie"), 138 | "X-CSRF-Token": token, 139 | }, 140 | }) 141 | 142 | if body := r2.Body.String(); body != "OK" { 143 | t.Error("Response is not OK: ", body) 144 | } 145 | } 146 | 147 | func TestQueryHeader2(t *testing.T) { 148 | var token string 149 | g := newServer(Options{ 150 | Secret: "secret123", 151 | }) 152 | 153 | g.GET("/login", func(c *gin.Context) { 154 | token = GetToken(c) 155 | }) 156 | 157 | g.POST("/login", func(c *gin.Context) { 158 | c.String(http.StatusOK, "OK") 159 | }) 160 | 161 | r1 := request(g, requestOptions{URL: "/login"}) 162 | r2 := request(g, requestOptions{ 163 | Method: "POST", 164 | URL: "/login", 165 | Headers: map[string]string{ 166 | "Cookie": r1.Header().Get("Set-Cookie"), 167 | "X-XSRF-Token": token, 168 | }, 169 | }) 170 | 171 | if body := r2.Body.String(); body != "OK" { 172 | t.Error("Response is not OK: ", body) 173 | } 174 | } 175 | 176 | func TestErrorFunc(t *testing.T) { 177 | result := "" 178 | g := newServer(Options{ 179 | Secret: "secret123", 180 | ErrorFunc: func(c *gin.Context) { 181 | result = "something wrong" 182 | }, 183 | }) 184 | 185 | g.GET("/login", func(c *gin.Context) { 186 | GetToken(c) 187 | }) 188 | 189 | g.POST("/login", func(c *gin.Context) { 190 | c.String(http.StatusOK, "OK") 191 | }) 192 | 193 | r1 := request(g, requestOptions{URL: "/login"}) 194 | request(g, requestOptions{ 195 | Method: "POST", 196 | URL: "/login", 197 | Headers: map[string]string{ 198 | "Cookie": r1.Header().Get("Set-Cookie"), 199 | }, 200 | }) 201 | 202 | if result != "something wrong" { 203 | t.Error("Error function was not called") 204 | } 205 | } 206 | 207 | func TestIgnoreMethods(t *testing.T) { 208 | g := newServer(Options{ 209 | Secret: "secret123", 210 | IgnoreMethods: []string{"GET", "POST"}, 211 | }) 212 | 213 | g.GET("/login", func(c *gin.Context) { 214 | GetToken(c) 215 | }) 216 | 217 | g.POST("/login", func(c *gin.Context) { 218 | c.String(http.StatusOK, "OK") 219 | }) 220 | 221 | r1 := request(g, requestOptions{URL: "/login"}) 222 | r2 := request(g, requestOptions{ 223 | Method: "POST", 224 | URL: "/login", 225 | Headers: map[string]string{ 226 | "Cookie": r1.Header().Get("Set-Cookie"), 227 | }, 228 | }) 229 | 230 | if body := r2.Body.String(); body != "OK" { 231 | t.Error("Response is not OK: ", body) 232 | } 233 | } 234 | 235 | func TestTokenGetter(t *testing.T) { 236 | var token string 237 | g := newServer(Options{ 238 | Secret: "secret123", 239 | TokenGetter: func(c *gin.Context) string { 240 | return c.Request.FormValue("wtf") 241 | }, 242 | }) 243 | 244 | g.GET("/login", func(c *gin.Context) { 245 | token = GetToken(c) 246 | }) 247 | 248 | g.POST("/login", func(c *gin.Context) { 249 | c.String(http.StatusOK, "OK") 250 | }) 251 | 252 | r1 := request(g, requestOptions{URL: "/login"}) 253 | r2 := request(g, requestOptions{ 254 | Method: "POST", 255 | URL: "/login", 256 | Headers: map[string]string{ 257 | "Cookie": r1.Header().Get("Set-Cookie"), 258 | "Content-Type": "application/x-www-form-urlencoded", 259 | }, 260 | Body: strings.NewReader("wtf=" + token), 261 | }) 262 | 263 | if body := r2.Body.String(); body != "OK" { 264 | t.Error("Response is not OK: ", body) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/utrack/gin-csrf 2 | 3 | require ( 4 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 5 | github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963 6 | github.com/gin-gonic/gin v1.3.0 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= 2 | github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= 3 | github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9 h1:74lLNRzvsdIlkTgfDSMuaPjBr4cf6k7pwQQANm/yLKU= 6 | github.com/dchest/uniuri v0.0.0-20160212164326-8902c56451e9/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= 7 | github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= 8 | github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963 h1:ldKXSIxdVtXVCP4JW0p4ErvnPhISjbb5QSIqMnBa3ak= 9 | github.com/gin-contrib/sessions v0.0.0-20190101140330-dc5246754963/go.mod h1:4lkInX8nHSR62NSmhXM3xtPeMSyfiR58NaEz+om1lHM= 10 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY= 11 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 12 | github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= 13 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 14 | github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= 15 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 16 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 17 | github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= 18 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 19 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 20 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 21 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 22 | github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 23 | github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= 24 | github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 25 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 26 | github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= 27 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 28 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 30 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 31 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 32 | github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= 33 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 34 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= 37 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 38 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4= 39 | github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 40 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 41 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 42 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 43 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 44 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 45 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 46 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 47 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 48 | gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 49 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 50 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 51 | --------------------------------------------------------------------------------