Merge branch 'develop' | Update AUTHORS and CHANGELOG

This commit is contained in:
Javier Provecho Fernandez 2015-02-04 13:18:37 +01:00
commit 5cfe2c56dd
21 changed files with 843 additions and 335 deletions

View File

@ -1,5 +1,6 @@
language: go language: go
sudo: false
go: go:
- 1.3 - 1.3
- 1.4
- tip - tip

View File

@ -1,12 +1,11 @@
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.
##gin 0.x series authors ##gin 0.x series authors
**Lead Developer:** Manu Martinez-Almeida (@manucorporat) **Original Developer:** Manu Martinez-Almeida (@manucorporat)
**Staff:** **Long-term Maintainer:** Javier Provecho (@javierprovecho)
Javier Provecho (@javierprovecho)
People and companies, who have contributed, in alphabetical order. People and companies, who have contributed, in alphabetical order.
@ -31,6 +30,14 @@ People and companies, who have contributed, in alphabetical order.
- Added travis CI integration - Added travis CI integration
**@andredublin (Andre Dublin)**
- Fix typo in comment
**@bredov (Ludwig Valda Vasquez)**
- Fix html templating in debug mode
**@bluele (Jun Kimura)** **@bluele (Jun Kimura)**
- Fixes code examples in README - Fixes code examples in README
@ -41,20 +48,38 @@ People and companies, who have contributed, in alphabetical order.
**@dickeyxxx (Jeff Dickey)** **@dickeyxxx (Jeff Dickey)**
- Typos in README - Typos in README
- Add example about serving static files
**@dutchcoders (DutchCoders)**
- ★ Fix security bug that allows client to spoof ip
- Fix typo. r.HTMLTemplates -> SetHTMLTemplate
**@fmd (Fareed Dudhia)** **@fmd (Fareed Dudhia)**
- Fix typo. SetHTTPTemplate -> SetHTMLTemplate - Fix typo. SetHTTPTemplate -> SetHTMLTemplate
**@jammie-stackhouse (Jamie Stackhouse)
- Add more shortcuts for router methods
**@jasonrhansen** **@jasonrhansen**
- Fix spelling and grammar errors in documentation - Fix spelling and grammar errors in documentation
**@JasonSoft (Jason Lee)**
- Fix typo in comment
**@julienschmidt (Julien Schmidt)** **@julienschmidt (Julien Schmidt)**
- gofmt the code examples - gofmt the code examples
**@kelcecil (Kel Cecil)**
- Fix readme typo
**@kyledinh (Kyle Dinh)** **@kyledinh (Kyle Dinh)**
- Adds RunTLS() - Adds RunTLS()
@ -63,6 +88,10 @@ People and companies, who have contributed, in alphabetical order.
- Small fixes in README - Small fixes in README
**@loongmxbt (Saint Asky)**
- Fix typo in example
**@lucas-clemente (Lucas Clemente)** **@lucas-clemente (Lucas Clemente)**
- ★ work around path.Join removing trailing slashes from routes - ★ work around path.Join removing trailing slashes from routes
@ -73,10 +102,15 @@ People and companies, who have contributed, in alphabetical order.
- Fixes Content-Type for json render - Fixes Content-Type for json render
**@mirzac (Mirza Ceric)**
- Fix debug printing
**@mopemope (Yutaka Matsubara)** **@mopemope (Yutaka Matsubara)**
- ★ Adds Godep support (Dependencies Manager) - ★ Adds Godep support (Dependencies Manager)
- Fix variadic parameter in the flexible render API - Fix variadic parameter in the flexible render API
- Fix Corrupted plain render - Fix Corrupted plain render
- Add Pluggable View Renderer Example
**@msemenistyi (Mykyta Semenistyi)** **@msemenistyi (Mykyta Semenistyi)**
@ -96,6 +130,10 @@ People and companies, who have contributed, in alphabetical order.
- Fix Port usage in README. - Fix Port usage in README.
**@se77en (Damon Zhao)**
- Improve color logging
**@silasb (Silas Baronda)** **@silasb (Silas Baronda)**
- Fixing quotes in README - Fixing quotes in README
@ -104,5 +142,17 @@ People and companies, who have contributed, in alphabetical order.
- Fixes some texts in README II - Fixes some texts in README II
**@slimmy (Jimmy Pettersson)
- Added messages for required bindings
**@smira (Andrey Smirnov)**
- Add support for ignored/unexported fields in binding
**@yosssi (Keiji Yoshida)**
- Fix link in README
**@yuyabee** **@yuyabee**
- Fixed README - Fixed README

View File

@ -1,5 +1,12 @@
#Changelog #Changelog
###Gin 0.5 (Jan 4, 2015)
- [NEW] Content Negotiation
- [FIX] Solved security bug that allow a client to spoof ip
- [FIX] Fix unexported/ignored fields in binding
###Gin 0.4 (Aug 21, 2014) ###Gin 0.4 (Aug 21, 2014)
- [NEW] Development mode - [NEW] Development mode
@ -34,7 +41,7 @@
- [NEW] New API for serving static files. gin.Static() - [NEW] New API for serving static files. gin.Static()
- [NEW] gin.H() can be serialized into XML - [NEW] gin.H() can be serialized into XML
- [NEW] Typed errors. Errors can be typed. Internet/external/custom. - [NEW] Typed errors. Errors can be typed. Internet/external/custom.
- [NEW] Support for Godebs - [NEW] Support for Godeps
- [NEW] Travis/Godocs badges in README - [NEW] Travis/Godocs badges in README
- [NEW] New Bind() and BindWith() methods for parsing request body. - [NEW] New Bind() and BindWith() methods for parsing request body.
- [NEW] Add Content.Copy() - [NEW] Add Content.Copy()

2
Godeps/Godeps.json generated
View File

@ -4,7 +4,7 @@
"Deps": [ "Deps": [
{ {
"ImportPath": "github.com/julienschmidt/httprouter", "ImportPath": "github.com/julienschmidt/httprouter",
"Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20" "Rev": "aeec11926f7a8fab580383810e1b1bbba99bdaa7"
} }
] ]
} }

View File

@ -3,8 +3,35 @@
[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![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) [![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://gin-gonic.github.io/gin/other/console.png)
![Gin console logger](https://gin-gonic.github.io/gin/other/console.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? ##Gin is new, will it be supported?
@ -24,19 +51,19 @@ Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcomin
- [x] Flexible rendering system - [x] Flexible rendering system
- [ ] More powerful validation API - [ ] More powerful validation API
- [ ] Improve documentation - [ ] Improve documentation
- [ ] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework). - [X] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework).
- [x] Continuous integration - [x] Continuous integration
## Start using it ## 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 Run this in your terminal
``` ```
go get github.com/gin-gonic/gin 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" import "github.com/gin-gonic/gin"
@ -223,7 +250,7 @@ func main() {
r := gin.Default() r := gin.Default()
// Example for binding JSON ({"user": "manu", "password": "123"}) // 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 var json LoginJSON
c.Bind(&json) // This will infer what binder to use depending on the content-type header. c.Bind(&json) // This will infer what binder to use depending on the content-type header.
@ -234,8 +261,8 @@ 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) { r.POST("/loginHTML", func(c *gin.Context) {
var form LoginForm 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. c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML.
@ -257,7 +284,7 @@ func main() {
func main() { func main() {
r := gin.Default() 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) { r.GET("/someJSON", func(c *gin.Context) {
c.JSON(200, gin.H{"message": "hey", "status": 200}) c.JSON(200, gin.H{"message": "hey", "status": 200})
}) })
@ -286,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.
####Serving static files ####Serving static files
@ -310,7 +352,7 @@ Using LoadHTMLTemplates()
```go ```go
func main() { func main() {
r := gin.Default() r := gin.Default()
r.LoadHTMLTemplates("templates/*") r.LoadHTMLGlob("templates/*")
r.GET("/index", func(c *gin.Context) { r.GET("/index", func(c *gin.Context) {
obj := gin.H{"title": "Main website"} obj := gin.H{"title": "Main website"}
c.HTML(200, "index.tmpl", obj) c.HTML(200, "index.tmpl", obj)
@ -320,6 +362,11 @@ func main() {
r.Run(":8080") r.Run(":8080")
} }
``` ```
```html
<h1>
{{ .title }}
</h1>
```
You can also use your own html template render You can also use your own html template render
@ -329,7 +376,7 @@ import "html/template"
func main() { func main() {
r := gin.Default() r := gin.Default()
html := template.Must(template.ParseFiles("file1", "file2")) html := template.Must(template.ParseFiles("file1", "file2"))
r.HTMLTemplates = html r.SetHTMLTemplate(html)
// Listen and server on 0.0.0.0:8080 // Listen and server on 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
@ -413,7 +460,7 @@ func main() {
// hit "localhost:8080/admin/secrets // hit "localhost:8080/admin/secrets
authorized.GET("/secrets", func(c *gin.Context) { authorized.GET("/secrets", func(c *gin.Context) {
// get user, it was setted by the BasicAuth middleware // 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 { if secret, ok := secrets[user]; ok {
c.JSON(200, gin.H{"user": user, "secret": secret}) c.JSON(200, gin.H{"user": user, "secret": secret})
} else { } else {

104
auth.go
View File

@ -16,70 +16,29 @@ const (
) )
type ( type (
BasicAuthPair struct { Accounts map[string]string
Code string authPair struct {
Value string
User string User string
} }
Accounts map[string]string authPairs []authPair
Pairs []BasicAuthPair
) )
func (a Pairs) Len() int { return len(a) } func (a authPairs) Len() int { return len(a) }
func (a Pairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a authPairs) 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 (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value }
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 ""
}
}
// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where // 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. // the key is the user name and the value is the password.
func BasicAuth(accounts Accounts) HandlerFunc { func BasicAuth(accounts Accounts) HandlerFunc {
pairs, err := processAccounts(accounts)
pairs, err := processCredentials(accounts)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return func(c *Context) { return func(c *Context) {
// Search user in the slice of allowed credentials // Search user in the slice of allowed credentials
user := searchCredential(pairs, c.Request.Header.Get("Authorization")) user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization"))
if len(user) == 0 { if !ok {
// Credentials doesn't match, we return 401 Unauthorized and abort request. // Credentials doesn't match, we return 401 Unauthorized and abort request.
c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
c.Fail(401, errors.New("Unauthorized")) 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
}
}

View File

@ -87,7 +87,7 @@ func mapForm(ptr interface{}, form map[string][]string) error {
return err return err
} }
} }
formStruct.Elem().Field(i).Set(slice) formStruct.Field(i).Set(slice)
} else { } else {
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err return err
@ -169,8 +169,8 @@ func Validate(obj interface{}, parents ...string) error {
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i) field := typ.Field(i)
// Allow ignored fields in the struct // Allow ignored and unexported fields in the struct
if field.Tag.Get("form") == "-" { if field.Tag.Get("form") == "-" || field.PkgPath != "" {
continue continue
} }

View File

@ -12,7 +12,9 @@ import (
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"log" "log"
"net"
"net/http" "net/http"
"strings"
) )
const ( const (
@ -67,27 +69,29 @@ type Context struct {
Engine *Engine Engine *Engine
handlers []HandlerFunc handlers []HandlerFunc
index int8 index int8
accepted []string
} }
/************************************/ /************************************/
/********** ROUTES GROUPING *********/ /********** CONTEXT CREATION ********/
/************************************/ /************************************/
func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { 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.writermem.reset(w)
c.Request = req c.Request = req
c.Params = params c.Params = params
c.handlers = handlers c.handlers = handlers
c.Keys = nil c.Keys = nil
c.index = -1 c.index = -1
c.accepted = nil
c.Errors = c.Errors[0:0] c.Errors = c.Errors[0:0]
return c return c
} }
/************************************/ func (engine *Engine) reuseContext(c *Context) {
/****** FLOW AND ERROR MANAGEMENT****/ engine.pool.Put(c)
/************************************/ }
func (c *Context) Copy() *Context { func (c *Context) Copy() *Context {
var cp Context = *c var cp Context = *c
@ -96,6 +100,10 @@ func (c *Context) Copy() *Context {
return &cp return &cp
} }
/************************************/
/*************** FLOW ***************/
/************************************/
// Next should be used only in the middlewares. // Next should be used only in the middlewares.
// It executes the pending handlers in the chain inside the calling handler. // It executes the pending handlers in the chain inside the calling handler.
// See example in github. // See example in github.
@ -107,25 +115,31 @@ func (c *Context) Next() {
} }
} }
// Forces the system to do not continue calling the pending handlers. // Forces the system to do not continue calling the pending handlers in the chain.
// For example, the first handler checks if the request is authorized. If it's not, context.Abort(401) should be called. func (c *Context) Abort() {
// 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)
}
c.index = AbortIndex 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. // Fail is the same as Abort plus an error message.
// Calling `context.Fail(500, err)` is equivalent to: // Calling `context.Fail(500, err)` is equivalent to:
// ``` // ```
// context.Error("Operation aborted", err) // context.Error("Operation aborted", err)
// context.Abort(500) // context.AbortWithStatus(500)
// ``` // ```
func (c *Context) Fail(code int, err error) { func (c *Context) Fail(code int, err error) {
c.Error(err, "Operation aborted") c.Error(err, "Operation aborted")
c.Abort(code) c.AbortWithStatus(code)
} }
func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) {
@ -144,9 +158,9 @@ func (c *Context) Error(err error, meta interface{}) {
} }
func (c *Context) LastError() error { func (c *Context) LastError() error {
s := len(c.Errors) nuErrors := len(c.Errors)
if s > 0 { if nuErrors > 0 {
return errors.New(c.Errors[s-1].Err) return errors.New(c.Errors[nuErrors-1].Err)
} else { } else {
return nil return nil
} }
@ -168,9 +182,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. // 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) { func (c *Context) Get(key string) (interface{}, error) {
if c.Keys != nil { if c.Keys != nil {
item, ok := c.Keys[key] value, ok := c.Keys[key]
if ok { if ok {
return item, nil return value, nil
} }
} }
return nil, errors.New("Key does not exist.") return nil, errors.New("Key does not exist.")
@ -180,13 +194,93 @@ func (c *Context) Get(key string) (interface{}, error) {
func (c *Context) MustGet(key string) interface{} { func (c *Context) MustGet(key string) interface{} {
value, err := c.Get(key) value, err := c.Get(key)
if err != nil || value == nil { if err != nil || value == nil {
log.Panicf("Key %s doesn't exist", key) log.Panicf("Key %s doesn't exist", value)
} }
return value 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 {
return c.Request.RemoteAddr
}
/************************************/ /************************************/
/******** ENCODING MANAGEMENT********/ /********* PARSING REQUEST **********/
/************************************/ /************************************/
// This function checks the Content-Type to select a binding engine automatically, // This function checks the Content-Type to select a binding engine automatically,
@ -220,10 +314,14 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) bool {
return true return true
} }
/************************************/
/******** RESPONSE RENDERING ********/
/************************************/
func (c *Context) Render(code int, render render.Render, obj ...interface{}) { func (c *Context) Render(code int, render render.Render, obj ...interface{}) {
if err := render.Render(c.Writer, code, obj...); err != nil { if err := render.Render(c.Writer, code, obj...); err != nil {
c.ErrorTyped(err, ErrorTypeInternal, obj) c.ErrorTyped(err, ErrorTypeInternal, obj)
c.Abort(500) c.AbortWithStatus(500)
} }
} }
@ -265,9 +363,7 @@ func (c *Context) Data(code int, contentType string, data []byte) {
if len(contentType) > 0 { if len(contentType) > 0 {
c.Writer.Header().Set("Content-Type", contentType) c.Writer.Header().Set("Content-Type", contentType)
} }
if code >= 0 {
c.Writer.WriteHeader(code) c.Writer.WriteHeader(code)
}
c.Writer.Write(data) c.Writer.Write(data)
} }
@ -275,3 +371,64 @@ func (c *Context) Data(code int, contentType string, data []byte) {
func (c *Context) File(filepath string) { func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
/************************************/
/******** CONTENT NEGOTIATION *******/
/************************************/
type Negotiate struct {
Offered []string
HTMLPath string
HTMLData interface{}
JSONData interface{}
XMLData interface{}
Data interface{}
}
func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) {
case MIMEJSON:
data := chooseData(config.JSONData, config.Data)
c.JSON(code, data)
case MIMEHTML:
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 := chooseData(config.XMLData, config.Data)
c.XML(code, data)
default:
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"))
}
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
}

View File

@ -232,13 +232,13 @@ func TestBadAbortHandlersChain(t *testing.T) {
c.Next() c.Next()
stepsPassed += 1 stepsPassed += 1
// after check and abort // after check and abort
c.Abort(409) c.AbortWithStatus(409)
}) })
r.Use(func(c *Context) { r.Use(func(c *Context) {
stepsPassed += 1 stepsPassed += 1
c.Next() c.Next()
stepsPassed += 1 stepsPassed += 1
c.Abort(403) c.AbortWithStatus(403)
}) })
// RUN // RUN
@ -260,7 +260,7 @@ func TestAbortHandlersChain(t *testing.T) {
r := New() r := New()
r.Use(func(context *Context) { r.Use(func(context *Context) {
stepsPassed += 1 stepsPassed += 1
context.Abort(409) context.AbortWithStatus(409)
}) })
r.Use(func(context *Context) { r.Use(func(context *Context) {
stepsPassed += 1 stepsPassed += 1
@ -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")) 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)
}
}

View File

@ -41,7 +41,7 @@ func (engine *Engine) LoadHTMLTemplates(pattern string) {
engine.LoadHTMLGlob(pattern) engine.LoadHTMLGlob(pattern)
} }
// DEPRECATED. Use NotFound() instead // DEPRECATED. Use NoRoute() instead
func (engine *Engine) NotFound404(handlers ...HandlerFunc) { func (engine *Engine) NotFound404(handlers ...HandlerFunc) {
engine.NoRoute(handlers...) engine.NoRoute(handlers...)
} }

View File

@ -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.ExecuteWriter(ctx, w)
}
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")
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>{{ title }}</title>
<meta name="keywords" content="">
<meta name="description" content="">
</head>
<body>
Hello {{ name }} !
</body>
</html>

209
gin.go
View File

@ -5,13 +5,11 @@
package gin package gin
import ( import (
"fmt"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/julienschmidt/httprouter" "github.com/julienschmidt/httprouter"
"html/template" "html/template"
"math" "math"
"net/http" "net/http"
"path"
"sync" "sync"
) )
@ -28,49 +26,31 @@ const (
type ( type (
HandlerFunc func(*Context) 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. // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
Engine struct { Engine struct {
*RouterGroup *RouterGroup
HTMLRender render.Render HTMLRender render.Render
cache sync.Pool Default404Body []byte
finalNoRoute []HandlerFunc pool sync.Pool
allNoRoute []HandlerFunc
noRoute []HandlerFunc noRoute []HandlerFunc
router *httprouter.Router router *httprouter.Router
} }
) )
func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
c := engine.createContext(w, req, nil, engine.finalNoRoute)
// 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.cache.Put(c)
}
// Returns a new blank Engine instance without any middleware attached. // Returns a new blank Engine instance without any middleware attached.
// The most basic configuration // The most basic configuration
func New() *Engine { func New() *Engine {
engine := &Engine{} engine := &Engine{}
engine.RouterGroup = &RouterGroup{nil, "/", nil, engine} engine.RouterGroup = &RouterGroup{
Handlers: nil,
absolutePath: "/",
engine: engine,
}
engine.router = httprouter.New() engine.router = httprouter.New()
engine.Default404Body = []byte("404 page not found")
engine.router.NotFound = engine.handle404 engine.router.NotFound = engine.handle404
engine.cache.New = func() interface{} { engine.pool.New = func() interface{} {
c := &Context{Engine: engine} c := &Context{Engine: engine}
c.Writer = &c.writermem c.Writer = &c.writermem
return c return c
@ -86,7 +66,8 @@ func Default() *Engine {
} }
func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLGlob(pattern string) {
if gin_mode == debugCode { if IsDebugging() {
render.HTMLDebug.AddGlob(pattern)
engine.HTMLRender = render.HTMLDebug engine.HTMLRender = render.HTMLDebug
} else { } else {
templ := template.Must(template.ParseGlob(pattern)) templ := template.Must(template.ParseGlob(pattern))
@ -95,7 +76,8 @@ func (engine *Engine) LoadHTMLGlob(pattern string) {
} }
func (engine *Engine) LoadHTMLFiles(files ...string) { func (engine *Engine) LoadHTMLFiles(files ...string) {
if gin_mode == debugCode { if IsDebugging() {
render.HTMLDebug.AddFiles(files...)
engine.HTMLRender = render.HTMLDebug engine.HTMLRender = render.HTMLDebug
} else { } else {
templ := template.Must(template.ParseFiles(files...)) templ := template.Must(template.ParseFiles(files...))
@ -112,151 +94,50 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
// Adds handlers for NoRoute. It return a 404 code by default. // Adds handlers for NoRoute. It return a 404 code by default.
func (engine *Engine) NoRoute(handlers ...HandlerFunc) { func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
engine.noRoute = handlers engine.noRoute = handlers
engine.finalNoRoute = engine.combineHandlers(engine.noRoute) engine.rebuild404Handlers()
} }
func (engine *Engine) Use(middlewares ...HandlerFunc) { func (engine *Engine) Use(middlewares ...HandlerFunc) {
engine.RouterGroup.Use(middlewares...) engine.RouterGroup.Use(middlewares...)
engine.finalNoRoute = engine.combineHandlers(engine.noRoute) engine.rebuild404Handlers()
}
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. // ServeHTTP makes the router implement the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
engine.router.ServeHTTP(w, req) engine.router.ServeHTTP(writer, request)
} }
func (engine *Engine) Run(addr string) { func (engine *Engine) Run(addr string) error {
if gin_mode == debugCode { debugPrint("Listening and serving HTTP on %s", addr)
fmt.Println("[GIN-debug] Listening and serving HTTP on " + addr)
}
if err := http.ListenAndServe(addr, engine); err != nil { 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 {
if gin_mode == debugCode { debugPrint("Listening and serving HTTPS on %s", addr)
fmt.Println("[GIN-debug] Listening and serving HTTPS on " + addr)
}
if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil {
panic(err) return err
} }
} return nil
/************************************/
/********** 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
} }

View File

@ -108,9 +108,8 @@ func testRouteNotOK2(method string, t *testing.T) {
if passed == true { if passed == true {
t.Errorf(method + " route handler was invoked, when it should not") t.Errorf(method + " route handler was invoked, when it should not")
} }
if w.Code != http.StatusNotFound { if w.Code != http.StatusMethodNotAllowed {
// 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.StatusMethodNotAllowed, w.Code, w.HeaderMap.Get("Location"))
t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location"))
} }
} }
@ -146,7 +145,7 @@ func TestHandleStaticFile(t *testing.T) {
// TEST // TEST
if w.Code != 200 { 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" { if w.Body.String() != "Gin Web Framework" {
t.Errorf("Response should be test, was: %s", w.Body.String()) t.Errorf("Response should be test, was: %s", w.Body.String())
@ -168,7 +167,7 @@ func TestHandleStaticDir(t *testing.T) {
// TEST // TEST
bodyAsString := w.Body.String() bodyAsString := w.Body.String()
if w.Code != 200 { 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 { if len(bodyAsString) == 0 {
t.Errorf("Got empty body instead of file tree") t.Errorf("Got empty body instead of file tree")

View File

@ -10,6 +10,17 @@ import (
"time" "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 { func ErrorLogger() HandlerFunc {
return ErrorLoggerT(ErrorTypeAll) return ErrorLoggerT(ErrorTypeAll)
} }
@ -26,14 +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})
reset = string([]byte{27, 91, 48, 109})
)
func Logger() HandlerFunc { func Logger() HandlerFunc {
stdlogger := log.New(os.Stdout, "", 0) stdlogger := log.New(os.Stdout, "", 0)
//errlogger := log.New(os.Stderr, "", 0) //errlogger := log.New(os.Stderr, "", 0)
@ -45,38 +48,58 @@ func Logger() HandlerFunc {
// Process request // Process request
c.Next() c.Next()
// save the IP of the requester // Stop timer
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
}
end := time.Now() end := time.Now()
latency := end.Sub(start) latency := end.Sub(start)
stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s %4s %s\n%s",
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"), end.Format("2006/01/02 - 15:04:05"),
color, code, reset, statusColor, statusCode, reset,
latency, latency,
requester, clientIP,
c.Request.Method, c.Request.URL.Path, methodColor, reset, method,
c.Request.URL.Path,
c.Errors.String(), 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
}
}

31
mode.go
View File

@ -5,6 +5,7 @@
package gin package gin
import ( import (
"fmt"
"os" "os"
) )
@ -22,6 +23,16 @@ const (
) )
var gin_mode int = debugCode 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) { func SetMode(value string) {
switch value { switch value {
@ -32,15 +43,21 @@ func SetMode(value string) {
case TestMode: case TestMode:
gin_mode = testCode gin_mode = testCode
default: default:
panic("gin mode unknown, the allowed modes are: " + DebugMode + " and " + ReleaseMode) panic("gin mode unknown: " + value)
} }
mode_name = value
} }
func init() { func Mode() string {
value := os.Getenv(GIN_MODE) return mode_name
if len(value) == 0 { }
SetMode(DebugMode)
} else { func IsDebugging() bool {
SetMode(value) return gin_mode == debugCode
}
func debugPrint(format string, values ...interface{}) {
if IsDebugging() {
fmt.Printf("[GIN-debug] "+format, values...)
} }
} }

View File

@ -39,7 +39,7 @@ func TestPanicWithAbort(t *testing.T) {
r := New() r := New()
r.Use(Recovery()) r.Use(Recovery())
r.GET("/recovery", func(c *Context) { r.GET("/recovery", func(c *Context) {
c.Abort(400) c.AbortWithStatus(400)
panic("Oupps, Houston, we have a problem") panic("Oupps, Houston, we have a problem")
}) })

View File

@ -30,7 +30,10 @@ type (
redirectRender struct{} redirectRender struct{}
// Redirects // Redirects
htmlDebugRender struct{} htmlDebugRender struct {
files []string
globs []string
}
// form binding // form binding
HTMLRender struct { HTMLRender struct {
@ -43,7 +46,7 @@ var (
XML = xmlRender{} XML = xmlRender{}
Plain = plainRender{} Plain = plainRender{}
Redirect = redirectRender{} Redirect = redirectRender{}
HTMLDebug = htmlDebugRender{} HTMLDebug = &htmlDebugRender{}
) )
func writeHeader(w http.ResponseWriter, code int, contentType string) { 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 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") writeHeader(w, code, "text/html")
file := data[0].(string) file := data[0].(string)
obj := data[1] obj := data[1]
t, err := template.ParseFiles(file)
if err != nil { t := template.New("")
if len(r.files) > 0 {
if _, err := t.ParseFiles(r.files...); err != nil {
return err return err
} }
}
for _, glob := range r.globs {
if _, err := t.ParseGlob(glob); err != nil {
return err
}
}
return t.ExecuteTemplate(w, file, obj) return t.ExecuteTemplate(w, file, obj)
} }

View File

@ -12,6 +12,10 @@ import (
"net/http" "net/http"
) )
const (
NoWritten = -1
)
type ( type (
ResponseWriter interface { ResponseWriter interface {
http.ResponseWriter http.ResponseWriter
@ -20,6 +24,7 @@ type (
http.CloseNotifier http.CloseNotifier
Status() int Status() int
Size() int
Written() bool Written() bool
WriteHeaderNow() WriteHeaderNow()
} }
@ -27,43 +32,49 @@ type (
responseWriter struct { responseWriter struct {
http.ResponseWriter http.ResponseWriter
status int status int
written bool size int
} }
) )
func (w *responseWriter) reset(writer http.ResponseWriter) { func (w *responseWriter) reset(writer http.ResponseWriter) {
w.ResponseWriter = writer w.ResponseWriter = writer
w.status = 200 w.status = 200
w.written = false w.size = NoWritten
} }
func (w *responseWriter) WriteHeader(code int) { func (w *responseWriter) WriteHeader(code int) {
if code > 0 { if code > 0 {
w.status = code w.status = code
if w.written { if w.Written() {
log.Println("[GIN] WARNING. Headers were already written!") log.Println("[GIN] WARNING. Headers were already written!")
} }
} }
} }
func (w *responseWriter) WriteHeaderNow() { func (w *responseWriter) WriteHeaderNow() {
if !w.written { if !w.Written() {
w.written = true w.size = 0
w.ResponseWriter.WriteHeader(w.status) w.ResponseWriter.WriteHeader(w.status)
} }
} }
func (w *responseWriter) Write(data []byte) (n int, err error) { func (w *responseWriter) Write(data []byte) (n int, err error) {
w.WriteHeaderNow() w.WriteHeaderNow()
return w.ResponseWriter.Write(data) n, err = w.ResponseWriter.Write(data)
w.size += n
return
} }
func (w *responseWriter) Status() int { func (w *responseWriter) Status() int {
return w.status return w.status
} }
func (w *responseWriter) Size() int {
return w.size
}
func (w *responseWriter) Written() bool { func (w *responseWriter) Written() bool {
return w.written return w.size != NoWritten
} }
// Implements the http.Hijacker interface // Implements the http.Hijacker interface

148
routergroup.go Normal file
View File

@ -0,0 +1,148 @@
// 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 := nameOfFunction(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)
}
// 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.
// To use the operating system's file system implementation,
// use :
// router.Static("/static", "/var/www")
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)
}
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)
}
}
func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc {
finalSize := len(group.Handlers) + len(handlers)
mergedHandlers := make([]HandlerFunc, 0, finalSize)
mergedHandlers = append(mergedHandlers, group.Handlers...)
return append(mergedHandlers, handlers...)
}
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
}

View File

@ -8,6 +8,7 @@ import (
"encoding/xml" "encoding/xml"
"reflect" "reflect"
"runtime" "runtime"
"strings"
) )
type H map[string]interface{} type H map[string]interface{}
@ -37,14 +38,45 @@ func (h H) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
} }
func filterFlags(content string) string { func filterFlags(content string) string {
for i, a := range content { for i, char := range content {
if a == ' ' || a == ';' { if char == ' ' || char == ';' {
return content[:i] return content[:i]
} }
} }
return content return content
} }
func funcName(f interface{}) string { func chooseData(custom, wildcard interface{}) interface{} {
if custom == nil {
if wildcard == nil {
panic("negotiation config is invalid")
}
return wildcard
}
return custom
}
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 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 nameOfFunction(f interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()
} }