From 2c3cdbb69f1d6676f688187ab900e38a603272fb Mon Sep 17 00:00:00 2001 From: mopemoepe Date: Thu, 17 Jul 2014 00:37:56 +0900 Subject: [PATCH 01/36] Add Pluggable View Renderer Example Gin meets pongo2! (https://github.com/flosch/pongo2) --- examples/pluggable_renderer/example_pongo2.go | 58 +++++++++++++++++++ examples/pluggable_renderer/index.html | 12 ++++ 2 files changed, 70 insertions(+) create mode 100644 examples/pluggable_renderer/example_pongo2.go create mode 100644 examples/pluggable_renderer/index.html diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go new file mode 100644 index 0000000..8b6fd94 --- /dev/null +++ b/examples/pluggable_renderer/example_pongo2.go @@ -0,0 +1,58 @@ +package main + +import ( + "github.com/flosch/pongo2" + "github.com/gin-gonic/gin" + "net/http" +) + +type pongoRender struct { + cache map[string]*pongo2.Template +} + +func newPongoRender() *pongoRender { + return &pongoRender{map[string]*pongo2.Template{}} +} + +func writeHeader(w http.ResponseWriter, code int, contentType string) { + if code >= 0 { + w.Header().Set("Content-Type", contentType) + w.WriteHeader(code) + } +} + +func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { + file := data[0].(string) + ctx := data[1].(pongo2.Context) + var t *pongo2.Template + + if tmpl, ok := p.cache[file]; ok { + t = tmpl + } else { + tmpl, err := pongo2.FromFile(file) + if err != nil { + return err + } + p.cache[file] = tmpl + t = tmpl + } + writeHeader(w, code, "text/html") + return t.ExecuteRW(w, ctx) +} + +func main() { + r := gin.Default() + r.HTMLRender = newPongoRender() + + r.GET("/index", func(c *gin.Context) { + name := c.Request.FormValue("name") + ctx := pongo2.Context{ + "title": "Gin meets pongo2 !", + "name": name, + } + c.HTML(200, "index.html", ctx) + }) + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} diff --git a/examples/pluggable_renderer/index.html b/examples/pluggable_renderer/index.html new file mode 100644 index 0000000..8b293ed --- /dev/null +++ b/examples/pluggable_renderer/index.html @@ -0,0 +1,12 @@ + + + + + {{ title }} + + + + + Hello {{ name }} ! + + From ffea7e88a28d2809563bd366944ff71a3e217c1e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 30 Aug 2014 22:22:57 +0200 Subject: [PATCH 02/36] Working on content type negotiation API --- CHANGELOG.md | 5 ++++ context.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ utils.go | 26 +++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ec8c5a..c89a3c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ #Changelog +###Gin 0.5 (Aug 21, 2014) + +- [NEW] Content Negotiation + + ###Gin 0.4 (Aug 21, 2014) - [NEW] Development mode diff --git a/context.go b/context.go index 294d1cc..570314b 100644 --- a/context.go +++ b/context.go @@ -67,6 +67,7 @@ type Context struct { Engine *Engine handlers []HandlerFunc index int8 + accepted []string } /************************************/ @@ -275,3 +276,75 @@ func (c *Context) Data(code int, contentType string, data []byte) { func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } + +/************************************/ +/******** CONTENT NEGOTIATION *******/ +/************************************/ +type Negotiate struct { + Offered []string + Data interface{} + JsonData interface{} + XMLData interface{} + HTMLData interface{} + HTMLPath string +} + +func (c *Context) Negotiate2(code int, config Negotiate) { + result := c.NegotiateFormat(config.Offered...) + switch result { + case MIMEJSON: + c.JSON(code, config.Data) + + case MIMEHTML: + name := config.HTMLPath + c.HTML(code, name, config.Data) + + case MIMEXML: + c.XML(code, config.Data) + default: + c.Fail(400, errors.New("m")) + } +} + +func (c *Context) Negotiate(code int, config map[string]interface{}, offerts ...string) { + result := c.NegotiateFormat(offerts...) + switch result { + case MIMEJSON: + data := readData("json.data", config) + c.JSON(code, data) + + case MIMEHTML: + data := readData("html.data", config) + name := config["html.path"].(string) + c.HTML(code, name, data) + + case MIMEXML: + data := readData("xml.data", config) + c.XML(code, data) + default: + c.Fail(400, errors.New("m")) + } +} + +func (c *Context) NegotiateFormat(offered ...string) string { + if c.accepted == nil { + c.accepted = parseAccept(c.Request.Header.Get("Accept")) + } + if len(c.accepted) == 0 { + return offered[0] + + } else { + for _, accepted := range c.accepted { + for _, offert := range offered { + if accepted == offert { + return offert + } + } + } + return "" + } +} + +func (c *Context) SetAccepted(formats ...string) { + c.accepted = formats +} diff --git a/utils.go b/utils.go index f58097a..96d403f 100644 --- a/utils.go +++ b/utils.go @@ -8,6 +8,7 @@ import ( "encoding/xml" "reflect" "runtime" + "strings" ) type H map[string]interface{} @@ -45,6 +46,31 @@ func filterFlags(content string) string { return content } +func readData(key string, config map[string]interface{}) interface{} { + data, ok := config[key] + if ok { + return data + } + data, ok = config["*.data"] + if !ok { + panic("negotiation config is invalid") + } + return data +} + +func parseAccept(accept string) []string { + parts := strings.Split(accept, ",") + for i, part := range parts { + index := strings.IndexByte(part, ';') + if index >= 0 { + part = part[0:index] + } + part = strings.TrimSpace(part) + parts[i] = part + } + return parts +} + func funcName(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } From 275bdc194ed699776c960e33a80959d1c2ea9570 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 31 Aug 2014 18:28:18 +0200 Subject: [PATCH 03/36] Fixes Content.Negotiate API --- context.go | 42 +++++++++++++++--------------------------- utils.go | 16 +++++++--------- 2 files changed, 22 insertions(+), 36 deletions(-) diff --git a/context.go b/context.go index 570314b..123d257 100644 --- a/context.go +++ b/context.go @@ -82,6 +82,7 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa c.handlers = handlers c.Keys = nil c.index = -1 + c.accepted = nil c.Errors = c.Errors[0:0] return c } @@ -280,47 +281,34 @@ func (c *Context) File(filepath string) { /************************************/ /******** CONTENT NEGOTIATION *******/ /************************************/ + type Negotiate struct { Offered []string - Data interface{} - JsonData interface{} - XMLData interface{} - HTMLData interface{} HTMLPath string + HTMLData interface{} + JSONData interface{} + XMLData interface{} + Data interface{} } -func (c *Context) Negotiate2(code int, config Negotiate) { +func (c *Context) Negotiate(code int, config Negotiate) { result := c.NegotiateFormat(config.Offered...) switch result { case MIMEJSON: - c.JSON(code, config.Data) - - case MIMEHTML: - name := config.HTMLPath - c.HTML(code, name, config.Data) - - case MIMEXML: - c.XML(code, config.Data) - default: - c.Fail(400, errors.New("m")) - } -} - -func (c *Context) Negotiate(code int, config map[string]interface{}, offerts ...string) { - result := c.NegotiateFormat(offerts...) - switch result { - case MIMEJSON: - data := readData("json.data", config) + data := chooseData(config.JSONData, config.Data) c.JSON(code, data) case MIMEHTML: - data := readData("html.data", config) - name := config["html.path"].(string) - c.HTML(code, name, data) + data := chooseData(config.HTMLData, config.Data) + if len(config.HTMLPath) == 0 { + panic("negotiate config is wrong. html path is needed") + } + c.HTML(code, config.HTMLPath, data) case MIMEXML: - data := readData("xml.data", config) + data := chooseData(config.XMLData, config.Data) c.XML(code, data) + default: c.Fail(400, errors.New("m")) } diff --git a/utils.go b/utils.go index 96d403f..fee5728 100644 --- a/utils.go +++ b/utils.go @@ -46,16 +46,14 @@ func filterFlags(content string) string { return content } -func readData(key string, config map[string]interface{}) interface{} { - data, ok := config[key] - if ok { - return data +func chooseData(custom, wildcard interface{}) interface{} { + if custom == nil { + if wildcard == nil { + panic("negotiation config is invalid") + } + return wildcard } - data, ok = config["*.data"] - if !ok { - panic("negotiation config is invalid") - } - return data + return custom } func parseAccept(accept string) []string { From 012c935a465079ae7edc75cce9100a13b3d2cd8e Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sun, 31 Aug 2014 18:41:11 +0200 Subject: [PATCH 04/36] Better errors in Context.Negotiation --- context.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 123d257..178379b 100644 --- a/context.go +++ b/context.go @@ -292,8 +292,7 @@ type Negotiate struct { } func (c *Context) Negotiate(code int, config Negotiate) { - result := c.NegotiateFormat(config.Offered...) - switch result { + switch c.NegotiateFormat(config.Offered...) { case MIMEJSON: data := chooseData(config.JSONData, config.Data) c.JSON(code, data) @@ -310,11 +309,14 @@ func (c *Context) Negotiate(code int, config Negotiate) { c.XML(code, data) default: - c.Fail(400, errors.New("m")) + c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) } } func (c *Context) NegotiateFormat(offered ...string) string { + if len(offered) == 0 { + panic("you must provide at least one offer") + } if c.accepted == nil { c.accepted = parseAccept(c.Request.Header.Get("Accept")) } From daedc0bc171c97f834c8d928f7cb49f1e1309a13 Mon Sep 17 00:00:00 2001 From: Matt Newberry Date: Tue, 2 Sep 2014 13:17:37 -0400 Subject: [PATCH 05/36] Add response body size to writer interface --- response_writer.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/response_writer.go b/response_writer.go index 3ce8414..7088fcf 100644 --- a/response_writer.go +++ b/response_writer.go @@ -20,6 +20,7 @@ type ( http.CloseNotifier Status() int + Size() int Written() bool WriteHeaderNow() } @@ -27,6 +28,7 @@ type ( responseWriter struct { http.ResponseWriter status int + size int written bool } ) @@ -55,13 +57,19 @@ func (w *responseWriter) WriteHeaderNow() { func (w *responseWriter) Write(data []byte) (n int, err error) { w.WriteHeaderNow() - return w.ResponseWriter.Write(data) + n, err = w.ResponseWriter.Write(data) + w.size = n + return } func (w *responseWriter) Status() int { return w.status } +func (w *responseWriter) Size() int { + return w.size +} + func (w *responseWriter) Written() bool { return w.written } From 21e53c0db60e4dca72e8573898387336bb8d715a Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 3 Sep 2014 04:25:08 +0200 Subject: [PATCH 06/36] Fixes responseWriter.Size() - "written" boolean is not longer needed - size is reseted - addition instead of assignation --- response_writer.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/response_writer.go b/response_writer.go index 7088fcf..9899395 100644 --- a/response_writer.go +++ b/response_writer.go @@ -12,6 +12,10 @@ import ( "net/http" ) +const ( + NoWritten = -1 +) + type ( ResponseWriter interface { http.ResponseWriter @@ -27,30 +31,29 @@ type ( responseWriter struct { http.ResponseWriter - status int - size int - written bool + status int + size int } ) func (w *responseWriter) reset(writer http.ResponseWriter) { w.ResponseWriter = writer w.status = 200 - w.written = false + w.size = NoWritten } func (w *responseWriter) WriteHeader(code int) { if code > 0 { w.status = code - if w.written { + if w.Written() { log.Println("[GIN] WARNING. Headers were already written!") } } } func (w *responseWriter) WriteHeaderNow() { - if !w.written { - w.written = true + if !w.Written() { + w.size = 0 w.ResponseWriter.WriteHeader(w.status) } } @@ -58,7 +61,7 @@ func (w *responseWriter) WriteHeaderNow() { func (w *responseWriter) Write(data []byte) (n int, err error) { w.WriteHeaderNow() n, err = w.ResponseWriter.Write(data) - w.size = n + w.size += n return } @@ -71,7 +74,7 @@ func (w *responseWriter) Size() int { } func (w *responseWriter) Written() bool { - return w.written + return w.size != NoWritten } // Implements the http.Hijacker interface From d3249800e9e4603ecaa952615ece93e5cd2f7415 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 3 Sep 2014 17:42:49 +0200 Subject: [PATCH 07/36] Removing exclamation marks in Go. --- AUTHORS.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index c09e263..47cb58a 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -1,4 +1,4 @@ -List of all the awesome people working to make Gin the best Web Framework in Go! +List of all the awesome people working to make Gin the best Web Framework in Go. diff --git a/README.md b/README.md index c97164d..5ceab2f 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin ## Start using it -Obviously, you need to have Git and Go! already installed to run Gin. +Obviously, you need to have Git and Go already installed to run Gin. Run this in your terminal ``` go get github.com/gin-gonic/gin ``` -Then import it in your Go! code: +Then import it in your Go code: ``` import "github.com/gin-gonic/gin" From 6634f04d9b4603d96cb665c4772d2cdec7013d9e Mon Sep 17 00:00:00 2001 From: Damon Zhao Date: Fri, 5 Sep 2014 15:53:53 +0800 Subject: [PATCH 08/36] (feature)add http method log-color,like http response status code --- logger.go | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/logger.go b/logger.go index 56602c0..878ce19 100644 --- a/logger.go +++ b/logger.go @@ -27,11 +27,14 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } 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, 55, 59, 52, 51, 109}) - red = string([]byte{27, 91, 57, 55, 59, 52, 49, 109}) - reset = string([]byte{27, 91, 48, 109}) + 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, 55, 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}) ) func Logger() HandlerFunc { @@ -68,14 +71,34 @@ func Logger() HandlerFunc { default: color = red } + + var methodColor string + method := c.Request.Method + switch { + case method == "GET": + methodColor = blue + case method == "POST": + methodColor = cyan + case method == "PUT": + methodColor = yellow + case method == "DELETE": + methodColor = red + case method == "PATCH": + methodColor = green + case method == "HEAD": + methodColor = magenta + case method == "OPTIONS": + methodColor = white + } end := time.Now() latency := end.Sub(start) - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n%s", + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %4s %s| %s\n%s", end.Format("2006/01/02 - 15:04:05"), color, code, reset, latency, requester, - c.Request.Method, c.Request.URL.Path, + methodColor, method, reset, + c.Request.URL.Path, c.Errors.String(), ) } From fd2e342569850bca6285955b88246bc29cbd3b3f Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Mon, 8 Sep 2014 20:54:08 +0200 Subject: [PATCH 09/36] Adds API for interrogating current mode It returns one of the following values: - gin. DebugMode - gin. ReleaseMode - gin. TestMode --- mode.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mode.go b/mode.go index 166c09c..9856789 100644 --- a/mode.go +++ b/mode.go @@ -22,6 +22,7 @@ const ( ) var gin_mode int = debugCode +var mode_name string = DebugMode func SetMode(value string) { switch value { @@ -34,6 +35,11 @@ func SetMode(value string) { default: panic("gin mode unknown, the allowed modes are: " + DebugMode + " and " + ReleaseMode) } + mode_name = value +} + +func Mode() string { + return mode_name } func init() { From 953c589b3236df613a769d52cbf6e0b3e1cc3982 Mon Sep 17 00:00:00 2001 From: Damon Zhao Date: Sat, 13 Sep 2014 12:40:37 +0800 Subject: [PATCH 10/36] change %4s to %-7s for align --- logger.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logger.go b/logger.go index 878ce19..4a95f66 100644 --- a/logger.go +++ b/logger.go @@ -92,7 +92,7 @@ func Logger() HandlerFunc { } end := time.Now() latency := end.Sub(start) - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %4s %s| %s\n%s", + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %-7s %s| %s\n%s", end.Format("2006/01/02 - 15:04:05"), color, code, reset, latency, From b4a6510edff7d580da9e0d1aed85352969d546d5 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 13 Sep 2014 15:51:29 +0200 Subject: [PATCH 11/36] Enables colored tag for http method in gin.Logger() --- logger.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logger.go b/logger.go index 4a95f66..5292ab8 100644 --- a/logger.go +++ b/logger.go @@ -92,12 +92,12 @@ func Logger() HandlerFunc { } end := time.Now() latency := end.Sub(start) - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %-7s %s| %s\n%s", + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), color, code, reset, latency, requester, - methodColor, method, reset, + methodColor, reset, method, c.Request.URL.Path, c.Errors.String(), ) From 6fcc0a5b1dc8ecaea7c28d0321d92bc03a73e964 Mon Sep 17 00:00:00 2001 From: "Manu Mtz.-Almeida" Date: Sat, 13 Sep 2014 20:26:47 +0200 Subject: [PATCH 12/36] Updates logger screenshot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5ceab2f..67d8d96 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. -![Gin console logger](http://gin-gonic.github.io/gin/other/console.png) +![Gin console logger](http://forzefield.com/gin_example.png) ##Gin is new, will it be supported? From 0808f8a824cfb9aef6ea4fd664af238544b66fc1 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Sat, 13 Sep 2014 20:37:27 +0200 Subject: [PATCH 13/36] Adds logger source code --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 67d8d96..43b19cc 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,36 @@ [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) -Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. +Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. + ![Gin console logger](http://forzefield.com/gin_example.png) +``` +$ cat test.go +``` +```go +package main + +import "github.com/gin-gonic/gin" + +func main() { + router := gin.Default() + router.GET("/", func(c *gin.Context) { + c.String(200, "hello world") + }) + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + router.POST("/submit", func(c *gin.Context) { + c.String(401, "not authorized") + }) + router.PUT("/error", func(c *gin.Context) { + c.String(500, "and error hapenned :(") + }) + router.Run(":8080") +} +``` + ##Gin is new, will it be supported? Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcoming startup. We developed it and we are going to continue using and improve it. From 97ae4a6b654f7ce2a2b77b3905ffe16f0b6b2652 Mon Sep 17 00:00:00 2001 From: Ludwig Valda Vasquez Date: Wed, 17 Sep 2014 07:33:13 +0400 Subject: [PATCH 14/36] Fix for #119. gin.LoadHTML* incorrectly works in debug mode. --- gin.go | 2 ++ render/render.go | 34 ++++++++++++++++++++++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/gin.go b/gin.go index 45b3807..1ff2acf 100644 --- a/gin.go +++ b/gin.go @@ -87,6 +87,7 @@ func Default() *Engine { func (engine *Engine) LoadHTMLGlob(pattern string) { if gin_mode == debugCode { + render.HTMLDebug.AddGlob(pattern) engine.HTMLRender = render.HTMLDebug } else { templ := template.Must(template.ParseGlob(pattern)) @@ -96,6 +97,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if gin_mode == debugCode { + render.HTMLDebug.AddFiles(files...) engine.HTMLRender = render.HTMLDebug } else { templ := template.Must(template.ParseFiles(files...)) diff --git a/render/render.go b/render/render.go index 699b4e9..21b6160 100644 --- a/render/render.go +++ b/render/render.go @@ -30,7 +30,10 @@ type ( redirectRender struct{} // Redirects - htmlDebugRender struct{} + htmlDebugRender struct { + files []string + globs []string + } // form binding HTMLRender struct { @@ -43,7 +46,7 @@ var ( XML = xmlRender{} Plain = plainRender{} Redirect = redirectRender{} - HTMLDebug = htmlDebugRender{} + HTMLDebug = &htmlDebugRender{} ) func writeHeader(w http.ResponseWriter, code int, contentType string) { @@ -82,14 +85,33 @@ func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{} return err } -func (_ htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { +func (r *htmlDebugRender) AddGlob(pattern string) { + r.globs = append(r.globs, pattern) +} + +func (r *htmlDebugRender) AddFiles(files ...string) { + r.files = append(r.files, files...) +} + +func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { writeHeader(w, code, "text/html") file := data[0].(string) obj := data[1] - t, err := template.ParseFiles(file) - if err != nil { - return err + + t := template.New("") + + if len(r.files) > 0 { + if _, err := t.ParseFiles(r.files...); err != nil { + return err + } } + + for _, glob := range r.globs { + if _, err = t.ParseGlob(glob); err != nil { + return err + } + } + return t.ExecuteTemplate(w, file, obj) } From e5a7bcd6df90bb290bc5b802ba1e43388db163ac Mon Sep 17 00:00:00 2001 From: Ludwig Valda Vasquez Date: Wed, 17 Sep 2014 07:44:42 +0400 Subject: [PATCH 15/36] fix typo in PR for #119 --- render/render.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render/render.go b/render/render.go index 21b6160..a81fffe 100644 --- a/render/render.go +++ b/render/render.go @@ -107,7 +107,7 @@ func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interf } for _, glob := range r.globs { - if _, err = t.ParseGlob(glob); err != nil { + if _, err := t.ParseGlob(glob); err != nil { return err } } From e36f2945bbc380f7520b44b4db0d0f166b6ae445 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Tue, 7 Oct 2014 01:52:18 +0400 Subject: [PATCH 16/36] Validate should ignore unexported struct fields. --- binding/binding.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/binding/binding.go b/binding/binding.go index 61a57b1..a40aa74 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -169,8 +169,8 @@ func Validate(obj interface{}) error { for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) - // Allow ignored fields in the struct - if field.Tag.Get("form") == "-" { + // Allow ignored and unexported fields in the struct + if field.Tag.Get("form") == "-" || field.PkgPath != "" { continue } From 07a3961941779068491f54fbbab47651d084bbcd Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Oct 2014 21:37:26 +0200 Subject: [PATCH 17/36] General refactoring --- context.go | 62 ++++++++++------- context_test.go | 6 +- gin.go | 175 +++++++---------------------------------------- mode.go | 4 ++ recovery_test.go | 2 +- routergroup.go | 137 +++++++++++++++++++++++++++++++++++++ utils.go | 17 ++++- 7 files changed, 223 insertions(+), 180 deletions(-) create mode 100644 routergroup.go diff --git a/context.go b/context.go index 178379b..7fcdd93 100644 --- a/context.go +++ b/context.go @@ -71,11 +71,11 @@ type Context struct { } /************************************/ -/********** ROUTES GROUPING *********/ +/********** CONTEXT CREATION ********/ /************************************/ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.cache.Get().(*Context) + c := engine.pool.Get().(*Context) c.writermem.reset(w) c.Request = req c.Params = params @@ -87,9 +87,9 @@ func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, pa return c } -/************************************/ -/****** FLOW AND ERROR MANAGEMENT****/ -/************************************/ +func (engine *Engine) reuseContext(c *Context) { + engine.pool.Put(c) +} func (c *Context) Copy() *Context { var cp Context = *c @@ -98,6 +98,10 @@ func (c *Context) Copy() *Context { return &cp } +/************************************/ +/*************** FLOW ***************/ +/************************************/ + // Next should be used only in the middlewares. // It executes the pending handlers in the chain inside the calling handler. // See example in github. @@ -109,25 +113,31 @@ func (c *Context) Next() { } } -// Forces the system to do not continue calling the pending handlers. -// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. -// The rest of pending handlers would never be called for that request. -func (c *Context) Abort(code int) { - if code >= 0 { - c.Writer.WriteHeader(code) - } +// Forces the system to do not continue calling the pending handlers in the chain. +func (c *Context) Abort() { c.index = AbortIndex } +// Same than AbortWithStatus() but also writes the specified response status code. +// For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called. +func (c *Context) AbortWithStatus(code int) { + c.Writer.WriteHeader(code) + c.Abort() +} + +/************************************/ +/********* ERROR MANAGEMENT *********/ +/************************************/ + // Fail is the same as Abort plus an error message. // Calling `context.Fail(500, err)` is equivalent to: // ``` // context.Error("Operation aborted", err) -// context.Abort(500) +// context.AbortWithStatus(500) // ``` func (c *Context) Fail(code int, err error) { c.Error(err, "Operation aborted") - c.Abort(code) + c.AbortWithStatus(code) } func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { @@ -146,9 +156,9 @@ func (c *Context) Error(err error, meta interface{}) { } func (c *Context) LastError() error { - s := len(c.Errors) - if s > 0 { - return errors.New(c.Errors[s-1].Err) + nuErrors := len(c.Errors) + if nuErrors > 0 { + return errors.New(c.Errors[nuErrors-1].Err) } else { return nil } @@ -170,9 +180,9 @@ func (c *Context) Set(key string, item interface{}) { // Get returns the value for the given key or an error if the key does not exist. func (c *Context) Get(key string) (interface{}, error) { if c.Keys != nil { - item, ok := c.Keys[key] + value, ok := c.Keys[key] if ok { - return item, nil + return value, nil } } return nil, errors.New("Key does not exist.") @@ -182,13 +192,13 @@ func (c *Context) Get(key string) (interface{}, error) { func (c *Context) MustGet(key string) interface{} { value, err := c.Get(key) if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", key) + log.Panicf("Key %s doesn't exist", value) } return value } /************************************/ -/******** ENCOGING MANAGEMENT********/ +/********* PARSING REQUEST **********/ /************************************/ // This function checks the Content-Type to select a binding engine automatically, @@ -222,10 +232,14 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { return true } +/************************************/ +/******** RESPONSE RENDERING ********/ +/************************************/ + func (c *Context) Render(code int, render render.Render, obj ...interface{}) { if err := render.Render(c.Writer, code, obj...); err != nil { c.ErrorTyped(err, ErrorTypeInternal, obj) - c.Abort(500) + c.AbortWithStatus(500) } } @@ -267,9 +281,7 @@ func (c *Context) Data(code int, contentType string, data []byte) { if len(contentType) > 0 { c.Writer.Header().Set("Content-Type", contentType) } - if code >= 0 { - c.Writer.WriteHeader(code) - } + c.Writer.WriteHeader(code) c.Writer.Write(data) } diff --git a/context_test.go b/context_test.go index 6df824c..8435ac5 100644 --- a/context_test.go +++ b/context_test.go @@ -232,13 +232,13 @@ func TestBadAbortHandlersChain(t *testing.T) { c.Next() stepsPassed += 1 // after check and abort - c.Abort(409) + c.AbortWithStatus(409) }) r.Use(func(c *Context) { stepsPassed += 1 c.Next() stepsPassed += 1 - c.Abort(403) + c.AbortWithStatus(403) }) // RUN @@ -260,7 +260,7 @@ func TestAbortHandlersChain(t *testing.T) { r := New() r.Use(func(context *Context) { stepsPassed += 1 - context.Abort(409) + context.AbortWithStatus(409) }) r.Use(func(context *Context) { stepsPassed += 1 diff --git a/gin.go b/gin.go index 1ff2acf..fcdd0a2 100644 --- a/gin.go +++ b/gin.go @@ -5,13 +5,11 @@ package gin import ( - "fmt" "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "html/template" "math" "net/http" - "path" "sync" ) @@ -28,28 +26,19 @@ const ( type ( HandlerFunc func(*Context) - // Used internally to configure router, a RouterGroup is associated with a prefix - // and an array of handlers (middlewares) - RouterGroup struct { - Handlers []HandlerFunc - prefix string - parent *RouterGroup - engine *Engine - } - // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - cache sync.Pool - finalNoRoute []HandlerFunc - noRoute []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + pool sync.Pool + allNoRoute []HandlerFunc + noRoute []HandlerFunc + router *httprouter.Router } ) func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.finalNoRoute) + c := engine.createContext(w, req, nil, engine.allNoRoute) // set 404 by default, useful for logging c.Writer.WriteHeader(404) c.Next() @@ -60,17 +49,21 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { c.Writer.WriteHeaderNow() } } - engine.cache.Put(c) + engine.reuseContext(c) } // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { engine := &Engine{} - engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} + engine.RouterGroup = &RouterGroup{ + Handlers: nil, + absolutePath: "/", + engine: engine, + } engine.router = httprouter.New() engine.router.NotFound = engine.handle404 - engine.cache.New = func() interface{} { + engine.pool.New = func() interface{} { c := &Context{Engine: engine} c.Writer = &c.writermem return c @@ -86,7 +79,7 @@ func Default() *Engine { } func (engine *Engine) LoadHTMLGlob(pattern string) { - if gin_mode == debugCode { + if IsDebugging() { render.HTMLDebug.AddGlob(pattern) engine.HTMLRender = render.HTMLDebug } else { @@ -96,7 +89,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { } func (engine *Engine) LoadHTMLFiles(files ...string) { - if gin_mode == debugCode { + if IsDebugging() { render.HTMLDebug.AddFiles(files...) engine.HTMLRender = render.HTMLDebug } else { @@ -114,151 +107,33 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) { // Adds handlers for NoRoute. It return a 404 code by default. func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.noRoute = handlers - engine.finalNoRoute = engine.combineHandlers(engine.noRoute) + engine.rebuild404Handlers() } func (engine *Engine) Use(middlewares ...HandlerFunc) { engine.RouterGroup.Use(middlewares...) - engine.finalNoRoute = engine.combineHandlers(engine.noRoute) + engine.rebuild404Handlers() +} + +func (engine *Engine) rebuild404Handlers() { + engine.allNoRoute = engine.combineHandlers(engine.noRoute) } // ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { - engine.router.ServeHTTP(w, req) +func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + engine.router.ServeHTTP(writer, request) } func (engine *Engine) Run(addr string) { - if gin_mode == debugCode { - fmt.Println("[GIN-debug] Listening and serving HTTP on " + addr) - } + debugPrint("Listening and serving HTTP on %s", addr) if err := http.ListenAndServe(addr, engine); err != nil { panic(err) } } func (engine *Engine) RunTLS(addr string, cert string, key string) { - if gin_mode == debugCode { - fmt.Println("[GIN-debug] Listening and serving HTTPS on " + addr) - } + debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { panic(err) } } - -/************************************/ -/********** ROUTES GROUPING *********/ -/************************************/ - -// Adds middlewares to the group, see example code in github. -func (group *RouterGroup) Use(middlewares ...HandlerFunc) { - group.Handlers = append(group.Handlers, middlewares...) -} - -// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. -// For example, all the routes that use a common middlware for authorization could be grouped. -func (group *RouterGroup) Group(component string, handlers ...HandlerFunc) *RouterGroup { - prefix := group.pathFor(component) - - return &RouterGroup{ - Handlers: group.combineHandlers(handlers), - parent: group, - prefix: prefix, - engine: group.engine, - } -} - -func (group *RouterGroup) pathFor(p string) string { - joined := path.Join(group.prefix, p) - // Append a '/' if the last component had one, but only if it's not there already - if len(p) > 0 && p[len(p)-1] == '/' && joined[len(joined)-1] != '/' { - return joined + "/" - } - return joined -} - -// Handle registers a new request handle and middlewares with the given path and method. -// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. -// See the example code in github. -// -// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (group *RouterGroup) Handle(method, p string, handlers []HandlerFunc) { - p = group.pathFor(p) - handlers = group.combineHandlers(handlers) - if gin_mode == debugCode { - nuHandlers := len(handlers) - name := funcName(handlers[nuHandlers-1]) - fmt.Printf("[GIN-debug] %-5s %-25s --> %s (%d handlers)\n", method, p, name, nuHandlers) - } - group.engine.router.Handle(method, p, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - c := group.engine.createContext(w, req, params, handlers) - c.Next() - c.Writer.WriteHeaderNow() - group.engine.cache.Put(c) - }) -} - -// POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(path string, handlers ...HandlerFunc) { - group.Handle("POST", path, handlers) -} - -// GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(path string, handlers ...HandlerFunc) { - group.Handle("GET", path, handlers) -} - -// DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(path string, handlers ...HandlerFunc) { - group.Handle("DELETE", path, handlers) -} - -// PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(path string, handlers ...HandlerFunc) { - group.Handle("PATCH", path, handlers) -} - -// PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(path string, handlers ...HandlerFunc) { - group.Handle("PUT", path, handlers) -} - -// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(path string, handlers ...HandlerFunc) { - group.Handle("OPTIONS", path, handlers) -} - -// HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(path string, handlers ...HandlerFunc) { - group.Handle("HEAD", path, handlers) -} - -// Static serves files from the given file system root. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use : -// router.Static("/static", "/var/www") -func (group *RouterGroup) Static(p, root string) { - prefix := group.pathFor(p) - p = path.Join(p, "/*filepath") - fileServer := http.StripPrefix(prefix, http.FileServer(http.Dir(root))) - group.GET(p, func(c *Context) { - fileServer.ServeHTTP(c.Writer, c.Request) - }) - group.HEAD(p, func(c *Context) { - fileServer.ServeHTTP(c.Writer, c.Request) - }) -} - -func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { - s := len(group.Handlers) + len(handlers) - h := make([]HandlerFunc, 0, s) - h = append(h, group.Handlers...) - h = append(h, handlers...) - return h -} diff --git a/mode.go b/mode.go index 9856789..20abd51 100644 --- a/mode.go +++ b/mode.go @@ -42,6 +42,10 @@ func Mode() string { return mode_name } +func IsDebugging() bool { + return gin_mode == debugCode +} + func init() { value := os.Getenv(GIN_MODE) if len(value) == 0 { diff --git a/recovery_test.go b/recovery_test.go index 756c7c2..f9047e2 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -39,7 +39,7 @@ func TestPanicWithAbort(t *testing.T) { r := New() r.Use(Recovery()) r.GET("/recovery", func(c *Context) { - c.Abort(400) + c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") }) diff --git a/routergroup.go b/routergroup.go new file mode 100644 index 0000000..651bf93 --- /dev/null +++ b/routergroup.go @@ -0,0 +1,137 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "github.com/julienschmidt/httprouter" + "net/http" + "path" +) + +// Used internally to configure router, a RouterGroup is associated with a prefix +// and an array of handlers (middlewares) +type RouterGroup struct { + Handlers []HandlerFunc + absolutePath string + engine *Engine +} + +// Adds middlewares to the group, see example code in github. +func (group *RouterGroup) Use(middlewares ...HandlerFunc) { + group.Handlers = append(group.Handlers, middlewares...) +} + +// Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. +// For example, all the routes that use a common middlware for authorization could be grouped. +func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { + return &RouterGroup{ + Handlers: group.combineHandlers(handlers), + absolutePath: group.calculateAbsolutePath(relativePath), + engine: group.engine, + } +} + +// Handle registers a new request handle and middlewares with the given path and method. +// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. +// See the example code in github. +// +// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut +// functions can be used. +// +// This function is intended for bulk loading and to allow the usage of less +// frequently used, non-standardized or custom methods (e.g. for internal +// communication with a proxy). +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { + absolutePath := group.calculateAbsolutePath(relativePath) + handlers = group.combineHandlers(handlers) + if IsDebugging() { + nuHandlers := len(handlers) + handlerName := nameOfFuncion(handlers[nuHandlers-1]) + debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } + + group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { + context := group.engine.createContext(w, req, params, handlers) + context.Next() + context.Writer.WriteHeaderNow() + group.engine.reuseContext(context) + }) +} + +// POST is a shortcut for router.Handle("POST", path, handle) +func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) { + group.Handle("POST", relativePath, handlers) +} + +// GET is a shortcut for router.Handle("GET", path, handle) +func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) { + group.Handle("GET", relativePath, handlers) +} + +// DELETE is a shortcut for router.Handle("DELETE", path, handle) +func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) { + group.Handle("DELETE", relativePath, handlers) +} + +// PATCH is a shortcut for router.Handle("PATCH", path, handle) +func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) { + group.Handle("PATCH", relativePath, handlers) +} + +// PUT is a shortcut for router.Handle("PUT", path, handle) +func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) { + group.Handle("PUT", relativePath, handlers) +} + +// OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) +func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) { + group.Handle("OPTIONS", relativePath, handlers) +} + +// HEAD is a shortcut for router.Handle("HEAD", path, handle) +func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { + group.Handle("HEAD", relativePath, handlers) +} + +// Static serves files from the given file system root. +// Internally a http.FileServer is used, therefore http.NotFound is used instead +// of the Router's NotFound handler. +// To use the operating system's file system implementation, +// use : +// router.Static("/static", "/var/www") +func (group *RouterGroup) Static(relativePath, root string) { + handler := group.createStaticHandler(relativePath, root) + group.GET(relativePath, handler) + group.HEAD(relativePath, handler) +} + +func (group *RouterGroup) createStaticHandler(relativePath, root string) func(*Context) { + absolutePath := group.calculateAbsolutePath(relativePath) + absolutePath = path.Join(absolutePath, "/*filepath") + fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root))) + return func(c *Context) { + fileServer.ServeHTTP(c.Writer, c.Request) + } +} + +func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { + finalSize := len(group.Handlers) + len(handlers) + mergedHandlers := make([]HandlerFunc, 0, finalSize) + mergedHandlers = append(mergedHandlers, group.Handlers...) + mergedHandlers = append(mergedHandlers, handlers...) + return mergedHandlers +} + +func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { + if len(relativePath) == 0 { + return group.absolutePath + } + absolutePath := path.Join(group.absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' + if appendSlash { + return absolutePath + "/" + } + return absolutePath +} diff --git a/utils.go b/utils.go index fee5728..69ad8fa 100644 --- a/utils.go +++ b/utils.go @@ -6,6 +6,7 @@ package gin import ( "encoding/xml" + "fmt" "reflect" "runtime" "strings" @@ -46,6 +47,12 @@ func filterFlags(content string) string { return content } +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + fmt.Printf("[GIN-debug] "+format, values) + } +} + func chooseData(custom, wildcard interface{}) interface{} { if custom == nil { if wildcard == nil { @@ -69,6 +76,14 @@ func parseAccept(accept string) []string { return parts } -func funcName(f interface{}) string { +func lastChar(str string) uint8 { + size := len(str) + if size == 0 { + panic("The length of the string can't be 0") + } + return str[size-1] +} + +func nameOfFuncion(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } From 030706c39aaa0e65c259a34d8eac354902eb1693 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Wed, 8 Oct 2014 21:49:08 +0200 Subject: [PATCH 18/36] Using absolutePath in static properly --- gin_test.go | 4 ++-- routergroup.go | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gin_test.go b/gin_test.go index 3397943..1368aa0 100644 --- a/gin_test.go +++ b/gin_test.go @@ -146,7 +146,7 @@ func TestHandleStaticFile(t *testing.T) { // TEST if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be 200, was: %d", w.Code) } if w.Body.String() != "Gin Web Framework" { t.Errorf("Response should be test, was: %s", w.Body.String()) @@ -168,7 +168,7 @@ func TestHandleStaticDir(t *testing.T) { // TEST bodyAsString := w.Body.String() if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) + t.Errorf("Response code should be 200, was: %d", w.Code) } if len(bodyAsString) == 0 { t.Errorf("Got empty body instead of file tree") diff --git a/routergroup.go b/routergroup.go index 651bf93..8163e97 100644 --- a/routergroup.go +++ b/routergroup.go @@ -102,14 +102,14 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { // use : // router.Static("/static", "/var/www") func (group *RouterGroup) Static(relativePath, root string) { - handler := group.createStaticHandler(relativePath, root) - group.GET(relativePath, handler) - group.HEAD(relativePath, handler) + absolutePath := group.calculateAbsolutePath(relativePath) + handler := group.createStaticHandler(absolutePath, root) + absolutePath = path.Join(absolutePath, "/*filepath") + group.GET(absolutePath, handler) + group.HEAD(absolutePath, handler) } -func (group *RouterGroup) createStaticHandler(relativePath, root string) func(*Context) { - absolutePath := group.calculateAbsolutePath(relativePath) - absolutePath = path.Join(absolutePath, "/*filepath") +func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) { fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root))) return func(c *Context) { fileServer.ServeHTTP(c.Writer, c.Request) From aa7b00a083a2cbdbe5b7288cf57e0d996819aac3 Mon Sep 17 00:00:00 2001 From: Manu Mtz-Almeida Date: Thu, 9 Oct 2014 01:40:42 +0200 Subject: [PATCH 19/36] General refactoring. Part 2. --- auth.go | 106 ++++++++++++++++++++++++----------------------- context.go | 11 +++++ gin.go | 42 ++++++++++--------- logger.go | 110 ++++++++++++++++++++++++------------------------- mode.go | 21 ++++++---- routergroup.go | 7 ++-- utils.go | 13 ++---- 7 files changed, 163 insertions(+), 147 deletions(-) diff --git a/auth.go b/auth.go index 248f97d..7602d72 100644 --- a/auth.go +++ b/auth.go @@ -16,70 +16,29 @@ const ( ) type ( - BasicAuthPair struct { - Code string - User string - } Accounts map[string]string - Pairs []BasicAuthPair + authPair struct { + Value string + User string + } + authPairs []authPair ) -func (a Pairs) Len() int { return len(a) } -func (a Pairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a Pairs) Less(i, j int) bool { return a[i].Code < a[j].Code } - -func processCredentials(accounts Accounts) (Pairs, error) { - if len(accounts) == 0 { - return nil, errors.New("Empty list of authorized credentials.") - } - pairs := make(Pairs, 0, len(accounts)) - for user, password := range accounts { - if len(user) == 0 || len(password) == 0 { - return nil, errors.New("User or password is empty") - } - base := user + ":" + password - code := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) - pairs = append(pairs, BasicAuthPair{code, user}) - } - // We have to sort the credentials in order to use bsearch later. - sort.Sort(pairs) - return pairs, nil -} - -func secureCompare(given, actual string) bool { - if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { - return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 - } else { - /* Securely compare actual to itself to keep constant time, but always return false */ - return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false - } -} - -func searchCredential(pairs Pairs, auth string) string { - if len(auth) == 0 { - return "" - } - // Search user in the slice of allowed credentials - r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Code >= auth }) - if r < len(pairs) && secureCompare(pairs[r].Code, auth) { - return pairs[r].User - } else { - return "" - } -} +func (a authPairs) Len() int { return len(a) } +func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } // Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where // the key is the user name and the value is the password. func BasicAuth(accounts Accounts) HandlerFunc { - - pairs, err := processCredentials(accounts) + pairs, err := processAccounts(accounts) if err != nil { panic(err) } return func(c *Context) { // Search user in the slice of allowed credentials - user := searchCredential(pairs, c.Request.Header.Get("Authorization")) - if len(user) == 0 { + user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) + if !ok { // Credentials doesn't match, we return 401 Unauthorized and abort request. c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") c.Fail(401, errors.New("Unauthorized")) @@ -90,3 +49,46 @@ func BasicAuth(accounts Accounts) HandlerFunc { } } } + +func processAccounts(accounts Accounts) (authPairs, error) { + if len(accounts) == 0 { + return nil, errors.New("Empty list of authorized credentials") + } + pairs := make(authPairs, 0, len(accounts)) + for user, password := range accounts { + if len(user) == 0 { + return nil, errors.New("User can not be empty") + } + base := user + ":" + password + value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) + pairs = append(pairs, authPair{ + Value: value, + User: user, + }) + } + // We have to sort the credentials in order to use bsearch later. + sort.Sort(pairs) + return pairs, nil +} + +func searchCredential(pairs authPairs, auth string) (string, bool) { + if len(auth) == 0 { + return "", false + } + // Search user in the slice of allowed credentials + r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth }) + if r < len(pairs) && secureCompare(pairs[r].Value, auth) { + return pairs[r].User, true + } else { + return "", false + } +} + +func secureCompare(given, actual string) bool { + if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 { + return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1 + } else { + /* 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/context.go b/context.go index 7fcdd93..8225124 100644 --- a/context.go +++ b/context.go @@ -197,6 +197,17 @@ func (c *Context) MustGet(key string) interface{} { return value } +func (c *Context) ClientIP() string { + clientIP := c.Request.Header.Get("X-Real-IP") + if len(clientIP) == 0 { + clientIP = c.Request.Header.Get("X-Forwarded-For") + } + if len(clientIP) == 0 { + clientIP = c.Request.RemoteAddr + } + return clientIP +} + /************************************/ /********* PARSING REQUEST **********/ /************************************/ diff --git a/gin.go b/gin.go index fcdd0a2..ea9345a 100644 --- a/gin.go +++ b/gin.go @@ -29,29 +29,15 @@ type ( // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. Engine struct { *RouterGroup - HTMLRender render.Render - pool sync.Pool - allNoRoute []HandlerFunc - noRoute []HandlerFunc - router *httprouter.Router + HTMLRender render.Render + Default404Body []byte + pool sync.Pool + allNoRoute []HandlerFunc + noRoute []HandlerFunc + router *httprouter.Router } ) -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) - // set 404 by default, useful for logging - c.Writer.WriteHeader(404) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 404 { - c.Data(-1, MIMEPlain, []byte("404 page not found")) - } else { - c.Writer.WriteHeaderNow() - } - } - engine.reuseContext(c) -} - // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { @@ -62,6 +48,7 @@ func New() *Engine { engine: engine, } engine.router = httprouter.New() + engine.Default404Body = []byte("404 page not found") engine.router.NotFound = engine.handle404 engine.pool.New = func() interface{} { c := &Context{Engine: engine} @@ -119,6 +106,21 @@ func (engine *Engine) rebuild404Handlers() { engine.allNoRoute = engine.combineHandlers(engine.noRoute) } +func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { + c := engine.createContext(w, req, nil, engine.allNoRoute) + // set 404 by default, useful for logging + c.Writer.WriteHeader(404) + c.Next() + if !c.Writer.Written() { + if c.Writer.Status() == 404 { + c.Data(-1, MIMEPlain, engine.Default404Body) + } else { + c.Writer.WriteHeaderNow() + } + } + engine.reuseContext(c) +} + // ServeHTTP makes the router implement the http.Handler interface. func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { engine.router.ServeHTTP(writer, request) diff --git a/logger.go b/logger.go index 5292ab8..5054f6e 100644 --- a/logger.go +++ b/logger.go @@ -10,6 +10,17 @@ import ( "time" ) +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, 55, 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}) +) + func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAll) } @@ -26,17 +37,6 @@ func ErrorLoggerT(typ uint32) HandlerFunc { } } -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, 55, 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}) -) - func Logger() HandlerFunc { stdlogger := log.New(os.Stdout, "", 0) //errlogger := log.New(os.Stderr, "", 0) @@ -48,58 +48,58 @@ func Logger() HandlerFunc { // Process request c.Next() - // save the IP of the requester - requester := c.Request.Header.Get("X-Real-IP") - // if the requester-header is empty, check the forwarded-header - if len(requester) == 0 { - requester = c.Request.Header.Get("X-Forwarded-For") - } - // if the requester is still empty, use the hard-coded address from the socket - if len(requester) == 0 { - requester = c.Request.RemoteAddr - } - - var color string - code := c.Writer.Status() - switch { - case code >= 200 && code <= 299: - color = green - case code >= 300 && code <= 399: - color = white - case code >= 400 && code <= 499: - color = yellow - default: - color = red - } - - var methodColor string - method := c.Request.Method - switch { - case method == "GET": - methodColor = blue - case method == "POST": - methodColor = cyan - case method == "PUT": - methodColor = yellow - case method == "DELETE": - methodColor = red - case method == "PATCH": - methodColor = green - case method == "HEAD": - methodColor = magenta - case method == "OPTIONS": - methodColor = white - } + // Stop timer end := time.Now() latency := end.Sub(start) + + clientIP := c.ClientIP() + method := c.Request.Method + statusCode := c.Writer.Status() + statusColor := colorForStatus(statusCode) + methodColor := colorForMethod(method) + stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), - color, code, reset, + statusColor, statusCode, reset, latency, - requester, + clientIP, methodColor, reset, method, c.Request.URL.Path, c.Errors.String(), ) } } + +func colorForStatus(code int) string { + switch { + case code >= 200 && code <= 299: + return green + case code >= 300 && code <= 399: + return white + case code >= 400 && code <= 499: + return yellow + default: + return red + } +} + +func colorForMethod(method string) string { + switch { + case method == "GET": + return blue + case method == "POST": + return cyan + case method == "PUT": + return yellow + case method == "DELETE": + return red + case method == "PATCH": + return green + case method == "HEAD": + return magenta + case method == "OPTIONS": + return white + default: + return reset + } +} diff --git a/mode.go b/mode.go index 20abd51..8ecab3d 100644 --- a/mode.go +++ b/mode.go @@ -5,6 +5,7 @@ package gin import ( + "fmt" "os" ) @@ -24,6 +25,15 @@ const ( var gin_mode int = debugCode var mode_name string = DebugMode +func init() { + value := os.Getenv(GIN_MODE) + if len(value) == 0 { + SetMode(DebugMode) + } else { + SetMode(value) + } +} + func SetMode(value string) { switch value { case DebugMode: @@ -33,7 +43,7 @@ func SetMode(value string) { case TestMode: gin_mode = testCode default: - panic("gin mode unknown, the allowed modes are: " + DebugMode + " and " + ReleaseMode) + panic("gin mode unknown: " + value) } mode_name = value } @@ -46,11 +56,8 @@ func IsDebugging() bool { return gin_mode == debugCode } -func init() { - value := os.Getenv(GIN_MODE) - if len(value) == 0 { - SetMode(DebugMode) - } else { - SetMode(value) +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + fmt.Printf("[GIN-debug] "+format, values) } } diff --git a/routergroup.go b/routergroup.go index 8163e97..8b2ebdd 100644 --- a/routergroup.go +++ b/routergroup.go @@ -48,7 +48,7 @@ func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []Han handlers = group.combineHandlers(handlers) if IsDebugging() { nuHandlers := len(handlers) - handlerName := nameOfFuncion(handlers[nuHandlers-1]) + handlerName := nameOfFunction(handlers[nuHandlers-1]) debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) } @@ -105,6 +105,8 @@ func (group *RouterGroup) Static(relativePath, root string) { absolutePath := group.calculateAbsolutePath(relativePath) handler := group.createStaticHandler(absolutePath, root) absolutePath = path.Join(absolutePath, "/*filepath") + + // Register GET and HEAD handlers group.GET(absolutePath, handler) group.HEAD(absolutePath, handler) } @@ -120,8 +122,7 @@ func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc finalSize := len(group.Handlers) + len(handlers) mergedHandlers := make([]HandlerFunc, 0, finalSize) mergedHandlers = append(mergedHandlers, group.Handlers...) - mergedHandlers = append(mergedHandlers, handlers...) - return mergedHandlers + return append(mergedHandlers, handlers...) } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { diff --git a/utils.go b/utils.go index 69ad8fa..43ddaec 100644 --- a/utils.go +++ b/utils.go @@ -6,7 +6,6 @@ package gin import ( "encoding/xml" - "fmt" "reflect" "runtime" "strings" @@ -39,20 +38,14 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error { } func filterFlags(content string) string { - for i, a := range content { - if a == ' ' || a == ';' { + for i, char := range content { + if char == ' ' || char == ';' { return content[:i] } } return content } -func debugPrint(format string, values ...interface{}) { - if IsDebugging() { - fmt.Printf("[GIN-debug] "+format, values) - } -} - func chooseData(custom, wildcard interface{}) interface{} { if custom == nil { if wildcard == nil { @@ -84,6 +77,6 @@ func lastChar(str string) uint8 { return str[size-1] } -func nameOfFuncion(f interface{}) string { +func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } From a57db1a355376c15518ff96670b3cd911c7121c9 Mon Sep 17 00:00:00 2001 From: Mirza Ceric Date: Tue, 21 Oct 2014 16:48:21 +0200 Subject: [PATCH 20/36] fixed debugPrint --- mode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mode.go b/mode.go index 8ecab3d..0495b83 100644 --- a/mode.go +++ b/mode.go @@ -58,6 +58,6 @@ func IsDebugging() bool { func debugPrint(format string, values ...interface{}) { if IsDebugging() { - fmt.Printf("[GIN-debug] "+format, values) + fmt.Printf("[GIN-debug] "+format, values...) } } From eaefeeb0fd0811899250601a40b0e4288af2ac75 Mon Sep 17 00:00:00 2001 From: Kel Cecil Date: Mon, 27 Oct 2014 20:46:23 -0400 Subject: [PATCH 21/36] Fix HTML form binding example typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43b19cc..82a1584 100644 --- a/README.md +++ b/README.md @@ -261,7 +261,7 @@ func main() { } }) - // Example for binding a HTLM form (user=manu&password=123) + // Example for binding a HTML form (user=manu&password=123) r.POST("/login", func(c *gin.Context) { var form LoginForm From f35dc49c68cdb7f1bd25b59dd145fed448225d13 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Fri, 31 Oct 2014 12:30:20 +0800 Subject: [PATCH 22/36] fix typo in comment --- deprecated.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deprecated.go b/deprecated.go index eb248dd..7188153 100644 --- a/deprecated.go +++ b/deprecated.go @@ -41,7 +41,7 @@ func (engine *Engine) LoadHTMLTemplates(pattern string) { engine.LoadHTMLGlob(pattern) } -// DEPRECATED. Use NotFound() instead +// DEPRECATED. Use NoRoute() instead func (engine *Engine) NotFound404(handlers ...HandlerFunc) { engine.NoRoute(handlers...) } From bd1b4008619fd69f9d0d8c604eb9cc5240d75692 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 2 Nov 2014 12:23:31 +0100 Subject: [PATCH 23/36] Add attribution from httprouter --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 43b19cc..44c3099 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) -Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. +Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. ![Gin console logger](http://forzefield.com/gin_example.png) From dcad0df8f777757a9a5eefb5fb0f2bd1a3a932f5 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Tue, 11 Nov 2014 08:43:48 +0100 Subject: [PATCH 24/36] Fix binding.go panic --- binding/binding.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binding/binding.go b/binding/binding.go index a40aa74..72b23df 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -87,7 +87,7 @@ func mapForm(ptr interface{}, form map[string][]string) error { return err } } - formStruct.Elem().Field(i).Set(slice) + formStruct.Field(i).Set(slice) } else { if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { return err From b8ab9554dcd768ff6d089e9a81a0646918228b8c Mon Sep 17 00:00:00 2001 From: Remco Date: Tue, 2 Dec 2014 15:28:38 +0100 Subject: [PATCH 25/36] Updated to use SetHTMLTemplate() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82a1584..a378c79 100644 --- a/README.md +++ b/README.md @@ -340,7 +340,7 @@ import "html/template" func main() { r := gin.Default() html := template.Must(template.ParseFiles("file1", "file2")) - r.HTMLTemplates = html + r.SetHTMLTemplate(html) // Listen and server on 0.0.0.0:8080 r.Run(":8080") From af9a6bcb4d87b79fb966f2b98a08adc77951911a Mon Sep 17 00:00:00 2001 From: Remco Date: Tue, 2 Dec 2014 15:39:24 +0100 Subject: [PATCH 26/36] Fixed typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a378c79..f0ef3e9 100644 --- a/README.md +++ b/README.md @@ -284,7 +284,7 @@ func main() { func main() { r := gin.Default() - // gin.H is a shortcup for map[string]interface{} + // gin.H is a shortcut for map[string]interface{} r.GET("/someJSON", func(c *gin.Context) { c.JSON(200, gin.H{"message": "hey", "status": 200}) }) From a48f83c9a1a1ed38bc7d233dc84bb0693843552f Mon Sep 17 00:00:00 2001 From: Jamie Stackhouse Date: Mon, 15 Dec 2014 13:19:51 -0400 Subject: [PATCH 27/36] Adding helper functions to router group for LINK and UNLINK. --- routergroup.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/routergroup.go b/routergroup.go index 8b2ebdd..60897b1 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,9 +5,10 @@ package gin import ( - "github.com/julienschmidt/httprouter" "net/http" "path" + + "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix @@ -95,6 +96,16 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { group.Handle("HEAD", relativePath, handlers) } +// LINK is a shortcut for router.Handle("LINK", path, handle) +func (group *RouterGroup) LINK(relativePath string, handlers ...HandlerFunc) { + group.Handle("LINK", relativePath, handlers) +} + +// UNLINK is a shortcut for router.Handle("UNLINK", path, handle) +func (group *RouterGroup) UNLINK(relativePath string, handlers ...HandlerFunc) { + group.Handle("UNLINK", relativePath, handlers) +} + // Static serves files from the given file system root. // Internally a http.FileServer is used, therefore http.NotFound is used instead // of the Router's NotFound handler. From e4f6e053d04a11e37c7b76b086167c5700e177b1 Mon Sep 17 00:00:00 2001 From: Remco Date: Sun, 21 Dec 2014 13:42:48 +0100 Subject: [PATCH 28/36] Fixed issue allowing to spoof ClientIP() The X-Forwared-For can be used to spoof the real client ip. The middleware introduced in this patch (which should only be used when having servers in front of this servers) will filter all defined proxies (or local ip addresses by default) and replace the RemoteAddr with the real client ip. --- context.go | 87 ++++++++++++++++++++++++++++++++++++++++++++----- context_test.go | 41 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 8225124..2f0e2d8 100644 --- a/context.go +++ b/context.go @@ -12,7 +12,9 @@ import ( "github.com/gin-gonic/gin/render" "github.com/julienschmidt/httprouter" "log" + "net" "net/http" + "strings" ) const ( @@ -197,15 +199,84 @@ func (c *Context) MustGet(key string) interface{} { return value } +func ipInMasks(ip net.IP, masks []interface{}) bool { + for _, proxy := range masks { + var mask *net.IPNet + var err error + + switch t := proxy.(type) { + case string: + if _, mask, err = net.ParseCIDR(t); err != nil { + panic(err) + } + case net.IP: + mask = &net.IPNet{IP: t, Mask: net.CIDRMask(len(t)*8, len(t)*8)} + case net.IPNet: + mask = &t + } + + if mask.Contains(ip) { + return true + } + } + + return false +} + +// the ForwardedFor middleware unwraps the X-Forwarded-For headers, be careful to only use this +// middleware if you've got servers in front of this server. The list with (known) proxies and +// local ips are being filtered out of the forwarded for list, giving the last not local ip being +// the real client ip. +func ForwardedFor(proxies ...interface{}) HandlerFunc { + if len(proxies) == 0 { + // default to local ips + var reservedLocalIps = []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"} + + proxies = make([]interface{}, len(reservedLocalIps)) + + for i, v := range reservedLocalIps { + proxies[i] = v + } + } + + return func(c *Context) { + // the X-Forwarded-For header contains an array with left most the client ip, then + // comma separated, all proxies the request passed. The last proxy appears + // as the remote address of the request. Returning the client + // ip to comply with default RemoteAddr response. + + // check if remoteaddr is local ip or in list of defined proxies + remoteIp := net.ParseIP(strings.Split(c.Request.RemoteAddr, ":")[0]) + + if !ipInMasks(remoteIp, proxies) { + return + } + + if forwardedFor := c.Request.Header.Get("X-Forwarded-For"); forwardedFor != "" { + parts := strings.Split(forwardedFor, ",") + + for i := len(parts) - 1; i >= 0; i-- { + part := parts[i] + + ip := net.ParseIP(strings.TrimSpace(part)) + + if ipInMasks(ip, proxies) { + continue + } + + // returning remote addr conform the original remote addr format + c.Request.RemoteAddr = ip.String() + ":0" + + // remove forwarded for address + c.Request.Header.Set("X-Forwarded-For", "") + return + } + } + } +} + func (c *Context) ClientIP() string { - clientIP := c.Request.Header.Get("X-Real-IP") - if len(clientIP) == 0 { - clientIP = c.Request.Header.Get("X-Forwarded-For") - } - if len(clientIP) == 0 { - clientIP = c.Request.RemoteAddr - } - return clientIP + return c.Request.RemoteAddr } /************************************/ diff --git a/context_test.go b/context_test.go index 8435ac5..851a56c 100644 --- a/context_test.go +++ b/context_test.go @@ -440,3 +440,44 @@ func TestBindingJSONMalformed(t *testing.T) { t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) } } + +func TestClientIP(t *testing.T) { + r := New() + + var clientIP string = "" + r.GET("/", func(c *Context) { + clientIP = c.ClientIP() + }) + + body := bytes.NewBuffer([]byte("")) + req, _ := http.NewRequest("GET", "/", body) + req.RemoteAddr = "clientip:1234" + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if clientIP != "clientip:1234" { + t.Errorf("ClientIP should not be %s, but clientip:1234", clientIP) + } +} + +func TestClientIPWithXForwardedForWithProxy(t *testing.T) { + r := New() + r.Use(ForwardedFor()) + + var clientIP string = "" + r.GET("/", func(c *Context) { + clientIP = c.ClientIP() + }) + + body := bytes.NewBuffer([]byte("")) + req, _ := http.NewRequest("GET", "/", body) + req.RemoteAddr = "172.16.8.3:1234" + req.Header.Set("X-Real-Ip", "realip") + req.Header.Set("X-Forwarded-For", "1.2.3.4, 10.10.0.4, 192.168.0.43, 172.16.8.4") + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if clientIP != "1.2.3.4:0" { + t.Errorf("ClientIP should not be %s, but 1.2.3.4:0", clientIP) + } +} From e5450a70e939938f0408d2ae9a1fd413856f4900 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 29 Dec 2014 12:49:59 +0100 Subject: [PATCH 29/36] Migrate to travis new container builds --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 98b4346..3d33833 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: go - +sudo: false go: - 1.3 + - 1.4 - tip From c2185a129ada89acfd146e806e220f1b2e1c0b74 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Thu, 1 Jan 2015 17:06:02 +0100 Subject: [PATCH 30/36] Fix some examples in README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cf2eec2..b1974ea 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ func main() { r := gin.Default() // Example for binding JSON ({"user": "manu", "password": "123"}) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginJSON", func(c *gin.Context) { var json LoginJSON c.Bind(&json) // This will infer what binder to use depending on the content-type header. @@ -262,7 +262,7 @@ func main() { }) // Example for binding a HTML form (user=manu&password=123) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginHTML", func(c *gin.Context) { var form LoginForm c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. @@ -424,7 +424,7 @@ func main() { // hit "localhost:8080/admin/secrets authorized.GET("/secrets", func(c *gin.Context) { // get user, it was setted by the BasicAuth middleware - user := c.Get(gin.AuthUserKey).(string) + user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { c.JSON(200, gin.H{"user": user, "secret": secret}) } else { From 852729e90c4339184420d30c1c6cc2202f8dcff3 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 01:48:01 +0100 Subject: [PATCH 31/36] Fix PR #71 --- examples/pluggable_renderer/example_pongo2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/pluggable_renderer/example_pongo2.go b/examples/pluggable_renderer/example_pongo2.go index 8b6fd94..9f745e1 100644 --- a/examples/pluggable_renderer/example_pongo2.go +++ b/examples/pluggable_renderer/example_pongo2.go @@ -37,7 +37,7 @@ func (p *pongoRender) Render(w http.ResponseWriter, code int, data ...interface{ t = tmpl } writeHeader(w, code, "text/html") - return t.ExecuteRW(w, ctx) + return t.ExecuteWriter(ctx, w) } func main() { From d9d83deb250a8172c9380c0daf57ceeda7feb8be Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:00:19 +0100 Subject: [PATCH 32/36] Apply gofmt to PR #179 --- routergroup.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/routergroup.go b/routergroup.go index 60897b1..8e02a40 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,10 +5,9 @@ package gin import ( + "github.com/julienschmidt/httprouter" "net/http" "path" - - "github.com/julienschmidt/httprouter" ) // Used internally to configure router, a RouterGroup is associated with a prefix From 7aa51dc3938dc404e13f948a017bb9380ca1d10f Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:23:49 +0100 Subject: [PATCH 33/36] Solve #164 --- gin.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/gin.go b/gin.go index ea9345a..37e6e4d 100644 --- a/gin.go +++ b/gin.go @@ -126,16 +126,18 @@ func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Reques engine.router.ServeHTTP(writer, request) } -func (engine *Engine) Run(addr string) { +func (engine *Engine) Run(addr string) error { debugPrint("Listening and serving HTTP on %s", addr) if err := http.ListenAndServe(addr, engine); err != nil { - panic(err) + return err } + return nil } -func (engine *Engine) RunTLS(addr string, cert string, key string) { +func (engine *Engine) RunTLS(addr string, cert string, key string) error { debugPrint("Listening and serving HTTPS on %s", addr) if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { - panic(err) + return err } + return nil } From 413d0f2296837c9317aee17ff0a4c59b28579d22 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Sun, 4 Jan 2015 02:26:33 +0100 Subject: [PATCH 34/36] Fix TestRouteNotOK2 with HTTP 405 --- Godeps/Godeps.json | 2 +- gin_test.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d963b7e..905a487 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20" + "Rev": "90d58bada7e6154006f2728ee09053271154a8f6" } ] } diff --git a/gin_test.go b/gin_test.go index 1368aa0..ba74c15 100644 --- a/gin_test.go +++ b/gin_test.go @@ -108,9 +108,8 @@ func testRouteNotOK2(method string, t *testing.T) { if passed == true { t.Errorf(method + " route handler was invoked, when it should not") } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location")) } } From 2d1291329a909e85c7a3cd762bb9e064601b2824 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Mon, 5 Jan 2015 16:15:42 +0100 Subject: [PATCH 35/36] Fix #191 outdated documentation --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1974ea..b49c9b0 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,7 @@ Using LoadHTMLTemplates() ```go func main() { r := gin.Default() - r.LoadHTMLTemplates("templates/*") + r.LoadHTMLGlob("templates/*") r.GET("/index", func(c *gin.Context) { obj := gin.H{"title": "Main website"} c.HTML(200, "index.tmpl", obj) @@ -331,6 +331,11 @@ func main() { r.Run(":8080") } ``` +```html +

+ {{ .title }} +

+``` You can also use your own html template render From d936320e0e15f008e537c952051c2d507b102ef7 Mon Sep 17 00:00:00 2001 From: Javier Provecho Fernandez Date: Wed, 4 Feb 2015 12:37:22 +0100 Subject: [PATCH 36/36] Sync master into develop - Add 265faff4bae38ebfd3c7a82c4fdbefb229f22767 - Update "github.com/julienschmidt/httprouter" version in Godeps - Add 28b9ff9e3495dabeaea2da86c100effbf1a68346 --- Godeps/Godeps.json | 2 +- README.md | 15 +++++++++++++++ binding/binding.go | 13 ++++++++++--- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 905a487..20da1fc 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -4,7 +4,7 @@ "Deps": [ { "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "90d58bada7e6154006f2728ee09053271154a8f6" + "Rev": "aeec11926f7a8fab580383810e1b1bbba99bdaa7" } ] } diff --git a/README.md b/README.md index b49c9b0..ce3ea7a 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,21 @@ func main() { } ``` +####Serving static files + +Use Engine.ServeFiles(path string, root http.FileSystem): + +```go +func main() { + r := gin.Default() + r.Static("/assets", "./assets") + + // Listen and server on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +Note: this will use `httpNotFound` instead of the Router's `NotFound` handler. ####HTML rendering diff --git a/binding/binding.go b/binding/binding.go index 72b23df..92460a5 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -155,7 +155,7 @@ func ensureNotPointer(obj interface{}) { } } -func Validate(obj interface{}) error { +func Validate(obj interface{}, parents ...string) error { typ := reflect.TypeOf(obj) val := reflect.ValueOf(obj) @@ -180,12 +180,19 @@ func Validate(obj interface{}) error { if strings.Index(field.Tag.Get("binding"), "required") > -1 { fieldType := field.Type.Kind() if fieldType == reflect.Struct { - err := Validate(fieldValue) + if reflect.DeepEqual(zero, fieldValue) { + return errors.New("Required " + field.Name) + } + err := Validate(fieldValue, field.Name) if err != nil { return err } } else if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name) + if len(parents) > 0 { + return errors.New("Required " + field.Name + " on " + parents[0]) + } else { + return errors.New("Required " + field.Name) + } } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { err := Validate(fieldValue) if err != nil {