Browse Source

Merge branch 'develop' | v0.5 release

Javier Provecho Fernandez 10 years ago
parent
commit
e2fa89777e
13 changed files with 402 additions and 64 deletions
  1. 56 0
      AUTHORS.md
  2. 8 1
      CHANGELOG.md
  3. 1 1
      Godeps/Godeps.json
  4. 107 25
      README.md
  5. 15 4
      auth.go
  6. 24 0
      auth_test.go
  7. 82 14
      binding/binding.go
  8. 8 1
      context.go
  9. 36 0
      context_test.go
  10. 45 15
      gin.go
  11. 2 2
      logger.go
  12. 1 1
      recovery.go
  13. 17 0
      render/render.go

+ 56 - 0
AUTHORS.md

@@ -9,6 +9,10 @@ List of all the awesome people working to make Gin the best Web Framework in Go.
 
 People and companies, who have contributed, in alphabetical order.
 
+**@858806258 (杰哥)**
+- Fix typo in example
+
+
 **@achedeuzot (Klemen Sever)**
 - Fix newline debug printing
 
@@ -21,6 +25,10 @@ People and companies, who have contributed, in alphabetical order.
 - Typos in README
 
 
+**@alexanderdidenko (Aleksandr Didenko)**
+- Add support multipart/form-data
+
+
 **@alexandernyquist (Alexander Nyquist)**
 - Using template.Must to fix multiple return issue
 - ★ Added support for OPTIONS verb
@@ -55,15 +63,39 @@ People and companies, who have contributed, in alphabetical order.
 - Add example about serving static files
 
 
+**@donileo (Adonis)**
+- Add NoMethod handler
+
+
 **@dutchcoders (DutchCoders)**
 - ★ Fix security bug that allows client to spoof ip
 - Fix typo. r.HTMLTemplates -> SetHTMLTemplate
 
 
+**@el3ctro- (Joshua Loper)**
+- Fix typo in example
+
+
+**@ethankan (Ethan Kan)**
+- Unsigned integers in binding
+
+
+**(Evgeny Persienko)**
+- Validate sub structures
+
+
+**@frankbille (Frank Bille)**
+- Add support for HTTP Realm Auth
+
+
 **@fmd (Fareed Dudhia)**
 - Fix typo. SetHTTPTemplate -> SetHTMLTemplate
 
 
+**@ironiridis (Christopher Harrington)**
+- Remove old reference
+
+
 **@jammie-stackhouse (Jamie Stackhouse)**
 - Add more shortcuts for router methods
 
@@ -104,6 +136,10 @@ People and companies, who have contributed, in alphabetical order.
 - ★ work around path.Join removing trailing slashes from routes
 
 
+**@mattn (Yasuhiro Matsumoto)**
+- Improve color logger
+
+
 **@mdigger (Dmitry Sedykh)**
 - Fixes Form binding when content-type is x-www-form-urlencoded
 - No repeat call c.Writer.Status() in gin.Logger
@@ -138,10 +174,22 @@ People and companies, who have contributed, in alphabetical order.
 - Fix Port usage in README.
 
 
+**@rayrod2030 (Ray Rodriguez)**
+- Fix typo in example
+
+
+**@rns**
+- Fix typo in example
+
+
 **@RobAWilkinson (Robert Wilkinson)**
 - Add example of forms and params
 
 
+**@rogierlommers (Rogier Lommers)**
+- Add updated static serve example
+
+
 **@se77en (Damon Zhao)**
 - Improve color logging
 
@@ -166,6 +214,14 @@ People and companies, who have contributed, in alphabetical order.
 - Update httprouter godeps
 
 
+**@tebeka (Miki Tebeka)**
+- Use net/http constants instead of numeric values
+
+
+**@techjanitor**
+- Update context.go reserved IPs
+
+
 **@yosssi (Keiji Yoshida)**
 - Fix link in README
 

+ 8 - 1
CHANGELOG.md

@@ -1,6 +1,13 @@
 #Changelog
 
-###Gin 0.6 (Mar 7, 2015)
+###Gin 0.6 (Mar 9, 2015)
+
+- [ADD] Support multipart/form-data
+- [ADD] NoMethod handler
+- [ADD] Validate sub structures
+- [ADD] Support for HTTP Realm Auth
+- [FIX] Unsigned integers in binding
+- [FIX] Improve color logger
 
 
 ###Gin 0.5 (Feb 7, 2015)

+ 1 - 1
Godeps/Godeps.json

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

+ 107 - 25
README.md

@@ -1,5 +1,7 @@
 #Gin Web Framework [![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)
 
+[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
 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](https://gin-gonic.github.io/gin/other/console.png)
@@ -10,21 +12,24 @@ $ cat test.go
 ```go
 package main
 
-import "github.com/gin-gonic/gin"
+import (
+	"net/http"
+	"github.com/gin-gonic/gin"
+)
 
 func main() {
 	router := gin.Default()
 	router.GET("/", func(c *gin.Context) {
-		c.String(200, "hello world")
+		c.String(http.StatusOK, "hello world")
 	})
 	router.GET("/ping", func(c *gin.Context) {
-		c.String(200, "pong")
+		c.String(http.StatusOK, "pong")
 	})
 	router.POST("/submit", func(c *gin.Context) {
-		c.String(401, "not authorized")
+		c.String(http.StatusUnauthorized, "not authorized")
 	})
 	router.PUT("/error", func(c *gin.Context) {
-		c.String(500, "and error hapenned :(")
+		c.String(http.StatusInternalServerError, "and error happened :(")
 	})
 	router.Run(":8080")
 }
@@ -85,15 +90,18 @@ If you'd like to help out with the project, there's a mailing list and IRC chann
 ```go 
 package main
 
-import "github.com/gin-gonic/gin"
+import (
+	"net/http"
+	"github.com/gin-gonic/gin"
+)
 
 func main() {
 	r := gin.Default()
 	r.GET("/ping", func(c *gin.Context) {
-		c.String(200, "pong")
+		c.String(http.StatusOK, "pong")
 	})
 
-	// Listen and server on 0.0.0.0:8080
+	// Listen and serve on 0.0.0.0:8080
 	r.Run(":8080")
 }
 ```
@@ -128,7 +136,7 @@ func main() {
 	r.GET("/user/:name", func(c *gin.Context) {
 		name := c.Params.ByName("name")
 		message := "Hello "+name
-		c.String(200, message)
+		c.String(http.StatusOK, message)
 	})
 
 	// However, this one will match /user/john/ and also /user/john/send
@@ -137,7 +145,7 @@ func main() {
 		name := c.Params.ByName("name")
 		action := c.Params.ByName("action")
 		message := name + " is " + action
-		c.String(200, message)
+		c.String(http.StatusOK, message)
 	})
 	
 	// Listen and server on 0.0.0.0:8080
@@ -155,15 +163,56 @@ func main() {
 		c.Request.ParseForm()
 		
 		firstname := c.Request.Form.Get("firstname")
-		lastname := c.Request.Form.get("lastname")
+		lastname := c.Request.Form.Get("lastname")
 
 		message := "Hello "+ firstname + lastname
-		c.String(200, message)
+		c.String(http.StatusOK, message)
+	})
+	r.Run(":8080")
+}
+```
+
+###Multipart Form
+```go
+package main
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/gin-gonic/gin/binding"
+)
+
+type LoginForm struct {
+	User     string `form:"user" binding:"required"`
+	Password string `form:"password" binding:"required"`
+}
+
+func main() {
+
+	r := gin.Default()
+
+	r.POST("/login", func(c *gin.Context) {
+
+		var form LoginForm
+		c.BindWith(&form, binding.MultipartForm)
+
+		if form.User == "user" && form.Password == "password" {
+			c.JSON(200, gin.H{"status": "you are logged in"})
+		} else {
+			c.JSON(401, gin.H{"status": "unauthorized"})
+		}
+
 	})
+
 	r.Run(":8080")
+
 }
 ```
 
+Test it with:
+```bash
+$ curl -v --form user=user --form password=password http://localhost:8080/login
+```
+
 #### Grouping routes
 ```go
 func main() {
@@ -272,9 +321,9 @@ func main() {
 
         c.Bind(&json) // This will infer what binder to use depending on the content-type header.
         if json.User == "manu" && json.Password == "123" {
-            c.JSON(200, gin.H{"status": "you are logged in"})
+            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
         } else {
-            c.JSON(401, gin.H{"status": "unauthorized"})
+            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
         }
 	})
 
@@ -284,9 +333,9 @@ func main() {
 
         c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML.
         if form.User == "manu" && form.Password == "123" {
-            c.JSON(200, gin.H{"status": "you are logged in"})
+            c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
         } else {
-            c.JSON(401, gin.H{"status": "unauthorized"})
+            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
         }
     })
 
@@ -303,7 +352,7 @@ func main() {
 
 	// 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})
+		c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
 	})
 
 	r.GET("/moreJSON", func(c *gin.Context) {
@@ -318,11 +367,11 @@ func main() {
 		msg.Number = 123
 		// Note that msg.Name becomes "user" in the JSON
 		// Will output  :   {"user": "Lena", "Message": "hey", "Number": 123}
-		c.JSON(200, msg)
+		c.JSON(http.StatusOK, msg)
 	})
 
 	r.GET("/someXML", func(c *gin.Context) {
-		c.XML(200, gin.H{"message": "hey", "status": 200})
+		c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK})
 	})
 
 	// Listen and server on 0.0.0.0:8080
@@ -331,7 +380,6 @@ func main() {
 ```
 
 ####Serving static files
-
 Use Engine.ServeFiles(path string, root http.FileSystem):
 
 ```go
@@ -344,6 +392,13 @@ func main() {
 }
 ```
 
+Use the following example to serve static files at top level route of your domain. Files are being served from directory ./html.
+
+```
+r := gin.Default()
+r.Use(static.Serve("/", static.LocalFile("html", false)))
+```
+
 Note: this will use `httpNotFound` instead of the Router's `NotFound` handler.
 
 ####HTML rendering
@@ -356,7 +411,7 @@ func main() {
 	r.LoadHTMLGlob("templates/*")
 	r.GET("/index", func(c *gin.Context) {
 		obj := gin.H{"title": "Main website"}
-		c.HTML(200, "index.tmpl", obj)
+		c.HTML(http.StatusOK, "index.tmpl", obj)
 	})
 
 	// Listen and server on 0.0.0.0:8080
@@ -384,13 +439,40 @@ func main() {
 }
 ```
 
+#####Using layout files with templates
+```go
+var baseTemplate = "main.tmpl"
+
+r.GET("/", func(c *gin.Context) {
+    r.SetHTMLTemplate(template.Must(template.ParseFiles(baseTemplate, "whatever.tmpl")))
+    c.HTML(200, "base", data)
+})
+```
+main.tmpl
+```html
+{{define "base"}}
+<html>
+    <head></head>
+    <body>
+        {{template "content" .}}
+    </body>
+</html>
+{{end}}
+```
+whatever.tmpl
+```html
+{{define "content"}}
+<h1>Hello World!</h1>
+{{end}}
+```
+
 #### Redirects
 
 Issuing a HTTP redirect is easy:
 
 ```go
 r.GET("/test", func(c *gin.Context) {
-	c.Redirect(301, "http://www.google.com/")
+	c.Redirect(http.StatusMovedPermanently, "http://www.google.com/")
 })
 ```
 Both internal and external locations are supported.
@@ -438,7 +520,7 @@ func main() {
 
 #### Using BasicAuth() middleware
 ```go
-// similate some private data
+// simulate some private data
 var secrets = gin.H{
 	"foo":    gin.H{"email": "foo@bar.com", "phone": "123433"},
 	"austin": gin.H{"email": "austin@example.com", "phone": "666"},
@@ -463,9 +545,9 @@ func main() {
 		// get user, it was setted by the BasicAuth middleware
 		user := c.MustGet(gin.AuthUserKey).(string)
 		if secret, ok := secrets[user]; ok {
-			c.JSON(200, gin.H{"user": user, "secret": secret})
+			c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret})
 		} else {
-			c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("})
+			c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("})
 		}
 	})
 

+ 15 - 4
auth.go

@@ -8,6 +8,7 @@ import (
 	"crypto/subtle"
 	"encoding/base64"
 	"errors"
+	"fmt"
 	"sort"
 )
 
@@ -28,9 +29,10 @@ 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 {
+// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where
+// the key is the user name and the value is the password, as well as the name of the Realm
+// (see http://tools.ietf.org/html/rfc2617#section-1.2)
+func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc {
 	pairs, err := processAccounts(accounts)
 	if err != nil {
 		panic(err)
@@ -40,7 +42,10 @@ func BasicAuth(accounts Accounts) HandlerFunc {
 		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\"")
+			if realm == "" {
+				realm = "Authorization Required"
+			}
+			c.Writer.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", realm))
 			c.Fail(401, errors.New("Unauthorized"))
 		} else {
 			// user is allowed, set UserId to key "user" in this context, the userId can be read later using
@@ -50,6 +55,12 @@ func BasicAuth(accounts Accounts) HandlerFunc {
 	}
 }
 
+// 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 {
+	return BasicAuthForRealm(accounts, "")
+}
+
 func processAccounts(accounts Accounts) (authPairs, error) {
 	if len(accounts) == 0 {
 		return nil, errors.New("Empty list of authorized credentials")

+ 24 - 0
auth_test.go

@@ -59,3 +59,27 @@ func TestBasicAuth401(t *testing.T) {
 		t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
 	}
 }
+
+func TestBasicAuth401WithCustomRealm(t *testing.T) {
+	req, _ := http.NewRequest("GET", "/login", nil)
+	w := httptest.NewRecorder()
+
+	r := New()
+	accounts := Accounts{"foo": "bar"}
+	r.Use(BasicAuthForRealm(accounts, "My Custom Realm"))
+
+	r.GET("/login", func(c *Context) {
+		c.String(200, "autorized")
+	})
+
+	req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password")))
+	r.ServeHTTP(w, req)
+
+	if w.Code != 401 {
+		t.Errorf("Response code should be Not autorized, was: %s", w.Code)
+	}
+
+	if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"My Custom Realm\"" {
+		t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type"))
+	}
+}

+ 82 - 14
binding/binding.go

@@ -25,14 +25,20 @@ type (
 	// XML binding
 	xmlBinding struct{}
 
-	// // form binding
+	// form binding
 	formBinding struct{}
+
+	// multipart form binding
+	multipartFormBinding struct{}
 )
 
+const MAX_MEMORY = 1 * 1024 * 1024
+
 var (
-	JSON = jsonBinding{}
-	XML  = xmlBinding{}
-	Form = formBinding{} // todo
+	JSON          = jsonBinding{}
+	XML           = xmlBinding{}
+	Form          = formBinding{} // todo
+	MultipartForm = multipartFormBinding{}
 )
 
 func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error {
@@ -63,6 +69,16 @@ func (_ formBinding) Bind(req *http.Request, obj interface{}) error {
 	return Validate(obj)
 }
 
+func (_ multipartFormBinding) Bind(req *http.Request, obj interface{}) error {
+	if err := req.ParseMultipartForm(MAX_MEMORY); err != nil {
+		return err
+	}
+	if err := mapForm(obj, req.Form); err != nil {
+		return err
+	}
+	return Validate(obj)
+}
+
 func mapForm(ptr interface{}, form map[string][]string) error {
 	typ := reflect.TypeOf(ptr).Elem()
 	formStruct := reflect.ValueOf(ptr).Elem()
@@ -98,18 +114,54 @@ func mapForm(ptr interface{}, form map[string][]string) error {
 	return nil
 }
 
+func setIntField(val string, bitSize int, structField reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+
+	intVal, err := strconv.ParseInt(val, 10, bitSize)
+	if err == nil {
+		structField.SetInt(intVal)
+	}
+
+	return err
+}
+
+func setUintField(val string, bitSize int, structField reflect.Value) error {
+	if val == "" {
+		val = "0"
+	}
+
+	uintVal, err := strconv.ParseUint(val, 10, bitSize)
+	if err == nil {
+		structField.SetUint(uintVal)
+	}
+
+	return err
+}
+
 func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
 	switch valueKind {
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
-		if val == "" {
-			val = "0"
-		}
-		intVal, err := strconv.Atoi(val)
-		if err != nil {
-			return err
-		} else {
-			structField.SetInt(int64(intVal))
-		}
+	case reflect.Int:
+		return setIntField(val, 0, structField)
+	case reflect.Int8:
+		return setIntField(val, 8, structField)
+	case reflect.Int16:
+		return setIntField(val, 16, structField)
+	case reflect.Int32:
+		return setIntField(val, 32, structField)
+	case reflect.Int64:
+		return setIntField(val, 64, structField)
+	case reflect.Uint:
+		return setUintField(val, 0, structField)
+	case reflect.Uint8:
+		return setUintField(val, 8, structField)
+	case reflect.Uint16:
+		return setUintField(val, 16, structField)
+	case reflect.Uint32:
+		return setUintField(val, 32, structField)
+	case reflect.Uint64:
+		return setUintField(val, 64, structField)
 	case reflect.Bool:
 		if val == "" {
 			val = "false"
@@ -199,6 +251,22 @@ func Validate(obj interface{}, parents ...string) error {
 						return err
 					}
 				}
+			} else {
+				fieldType := field.Type.Kind()
+				if fieldType == reflect.Struct {
+					if reflect.DeepEqual(zero, fieldValue) {
+						continue
+					}
+					err := Validate(fieldValue, field.Name)
+					if err != nil {
+						return err
+					}
+				} else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct {
+					err := Validate(fieldValue, field.Name)
+					if err != nil {
+						return err
+					}
+				}
 			}
 		}
 	case reflect.Slice:

+ 8 - 1
context.go

@@ -230,7 +230,7 @@ func ipInMasks(ip net.IP, masks []interface{}) bool {
 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"}
+		var reservedLocalIps = []string{"10.0.0.0/8", "127.0.0.1/32", "172.16.0.0/12", "192.168.0.0/16"}
 
 		proxies = make([]interface{}, len(reservedLocalIps))
 
@@ -295,6 +295,8 @@ func (c *Context) Bind(obj interface{}) bool {
 	switch {
 	case c.Request.Method == "GET" || ctype == MIMEPOSTForm:
 		b = binding.Form
+	case ctype == MIMEMultipartPOSTForm:
+		b = binding.MultipartForm
 	case ctype == MIMEJSON:
 		b = binding.JSON
 	case ctype == MIMEXML || ctype == MIMEXML2:
@@ -349,6 +351,11 @@ func (c *Context) String(code int, format string, values ...interface{}) {
 	c.Render(code, render.Plain, format, values)
 }
 
+// Writes the given string into the response body and sets the Content-Type to "text/html" without template.
+func (c *Context) HTMLString(code int, format string, values ...interface{}) {
+	c.Render(code, render.HTMLPlain, format, values)
+}
+
 // Returns a HTTP redirect to the specific location.
 func (c *Context) Redirect(code int, location string) {
 	if code >= 300 && code <= 308 {

+ 36 - 0
context_test.go

@@ -441,6 +441,42 @@ func TestBindingJSONMalformed(t *testing.T) {
 	}
 }
 
+func TestBindingForm(t *testing.T) {
+
+	body := bytes.NewBuffer([]byte("foo=bar&num=123&unum=1234567890"))
+
+	r := New()
+	r.POST("/binding/form", func(c *Context) {
+		var body struct {
+			Foo  string `form:"foo"`
+			Num  int    `form:"num"`
+			Unum uint   `form:"unum"`
+		}
+		if c.Bind(&body) {
+			c.JSON(200, H{"foo": body.Foo, "num": body.Num, "unum": body.Unum})
+		}
+	})
+
+	req, _ := http.NewRequest("POST", "/binding/form", body)
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	w := httptest.NewRecorder()
+
+	r.ServeHTTP(w, req)
+
+	if w.Code != 200 {
+		t.Errorf("Response code should be Ok, was: %d", w.Code)
+	}
+
+	expected := "{\"foo\":\"bar\",\"num\":123,\"unum\":1234567890}\n"
+	if w.Body.String() != expected {
+		t.Errorf("Response should be %s, was %s", expected, w.Body.String())
+	}
+
+	if w.HeaderMap.Get("Content-Type") != "application/json; charset=utf-8" {
+		t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type"))
+	}
+}
+
 func TestClientIP(t *testing.T) {
 	r := New()
 

+ 45 - 15
gin.go

@@ -14,13 +14,14 @@ import (
 )
 
 const (
-	AbortIndex   = math.MaxInt8 / 2
-	MIMEJSON     = "application/json"
-	MIMEHTML     = "text/html"
-	MIMEXML      = "application/xml"
-	MIMEXML2     = "text/xml"
-	MIMEPlain    = "text/plain"
-	MIMEPOSTForm = "application/x-www-form-urlencoded"
+	AbortIndex            = math.MaxInt8 / 2
+	MIMEJSON              = "application/json"
+	MIMEHTML              = "text/html"
+	MIMEXML               = "application/xml"
+	MIMEXML2              = "text/xml"
+	MIMEPlain             = "text/plain"
+	MIMEPOSTForm          = "application/x-www-form-urlencoded"
+	MIMEMultipartPOSTForm = "multipart/form-data"
 )
 
 type (
@@ -29,12 +30,14 @@ type (
 	// Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares.
 	Engine struct {
 		*RouterGroup
-		HTMLRender     render.Render
-		Default404Body []byte
-		pool           sync.Pool
-		allNoRoute     []HandlerFunc
-		noRoute        []HandlerFunc
-		router         *httprouter.Router
+		HTMLRender         render.Render
+		Default404Body     []byte
+		Default405Body     []byte
+		pool               sync.Pool
+		allNoRouteNoMethod []HandlerFunc
+		noRoute            []HandlerFunc
+		noMethod           []HandlerFunc
+		router             *httprouter.Router
 	}
 )
 
@@ -49,7 +52,9 @@ func New() *Engine {
 	}
 	engine.router = httprouter.New()
 	engine.Default404Body = []byte("404 page not found")
+	engine.Default405Body = []byte("405 method not allowed")
 	engine.router.NotFound = engine.handle404
+	engine.router.MethodNotAllowed = engine.handle405
 	engine.pool.New = func() interface{} {
 		c := &Context{Engine: engine}
 		c.Writer = &c.writermem
@@ -97,17 +102,27 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) {
 	engine.rebuild404Handlers()
 }
 
+func (engine *Engine) NoMethod(handlers ...HandlerFunc) {
+	engine.noMethod = handlers
+	engine.rebuild405Handlers()
+}
+
 func (engine *Engine) Use(middlewares ...HandlerFunc) {
 	engine.RouterGroup.Use(middlewares...)
 	engine.rebuild404Handlers()
+	engine.rebuild405Handlers()
 }
 
 func (engine *Engine) rebuild404Handlers() {
-	engine.allNoRoute = engine.combineHandlers(engine.noRoute)
+	engine.allNoRouteNoMethod = engine.combineHandlers(engine.noRoute)
+}
+
+func (engine *Engine) rebuild405Handlers() {
+	engine.allNoRouteNoMethod = engine.combineHandlers(engine.noMethod)
 }
 
 func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
-	c := engine.createContext(w, req, nil, engine.allNoRoute)
+	c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
 	// set 404 by default, useful for logging
 	c.Writer.WriteHeader(404)
 	c.Next()
@@ -121,6 +136,21 @@ func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) {
 	engine.reuseContext(c)
 }
 
+func (engine *Engine) handle405(w http.ResponseWriter, req *http.Request) {
+	c := engine.createContext(w, req, nil, engine.allNoRouteNoMethod)
+	// set 405 by default, useful for logging
+	c.Writer.WriteHeader(405)
+	c.Next()
+	if !c.Writer.Written() {
+		if c.Writer.Status() == 405 {
+			c.Data(-1, MIMEPlain, engine.Default405Body)
+		} 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)

+ 2 - 2
logger.go

@@ -5,8 +5,8 @@
 package gin
 
 import (
+	"github.com/mattn/go-colorable"
 	"log"
-	"os"
 	"time"
 )
 
@@ -38,7 +38,7 @@ func ErrorLoggerT(typ uint32) HandlerFunc {
 }
 
 func Logger() HandlerFunc {
-	stdlogger := log.New(os.Stdout, "", 0)
+	stdlogger := log.New(colorable.NewColorableStdout(), "", 0)
 	//errlogger := log.New(os.Stderr, "", 0)
 
 	return func(c *Context) {

+ 1 - 1
recovery.go

@@ -82,7 +82,7 @@ func function(pc uintptr) []byte {
 }
 
 // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
-// While Martini is in development mode, Recovery will also output the panic as HTML.
+// While Gin is in development mode, Recovery will also output the panic as HTML.
 func Recovery() HandlerFunc {
 	return func(c *Context) {
 		defer func() {

+ 17 - 0
render/render.go

@@ -26,6 +26,9 @@ type (
 	// Plain text
 	plainRender struct{}
 
+	// HTML Plain text
+	htmlPlainRender struct{}
+
 	// Redirects
 	redirectRender struct{}
 
@@ -45,6 +48,7 @@ var (
 	JSON      = jsonRender{}
 	XML       = xmlRender{}
 	Plain     = plainRender{}
+	HTMLPlain = htmlPlainRender{}
 	Redirect  = redirectRender{}
 	HTMLDebug = &htmlDebugRender{}
 )
@@ -85,6 +89,19 @@ func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}
 	return err
 }
 
+func (_ htmlPlainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error {
+	writeHeader(w, code, "text/html")
+	format := data[0].(string)
+	args := data[1].([]interface{})
+	var err error
+	if len(args) > 0 {
+		_, err = w.Write([]byte(fmt.Sprintf(format, args...)))
+	} else {
+		_, err = w.Write([]byte(format))
+	}
+	return err
+}
+
 func (r *htmlDebugRender) AddGlob(pattern string) {
 	r.globs = append(r.globs, pattern)
 }