1
0
mirror of https://github.com/labstack/echo.git synced 2024-12-24 20:14:31 +02:00
This commit is contained in:
Tyler Bunnell 2015-09-11 16:24:26 -06:00
commit 1d040969ad
41 changed files with 1789 additions and 489 deletions

1
.gitattributes vendored
View File

@ -17,4 +17,5 @@ LICENSE text eol=lf
# Exclude `website` and `examples` from Github's language statistics # Exclude `website` and `examples` from Github's language statistics
# https://github.com/github/linguist#using-gitattributes # https://github.com/github/linguist#using-gitattributes
examples/* linguist-documentation examples/* linguist-documentation
recipes/* linguist-documentation
website/* linguist-documentation website/* linguist-documentation

8
.gitignore vendored
View File

@ -1,10 +1,10 @@
# Website # Website
site/ site
.publish/ .publish
# Node.js # Node.js
node_modules/ node_modules
# IntelliJ # IntelliJ
.idea/ .idea
*.iml *.iml

View File

@ -1,5 +1,6 @@
# [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) # [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
A fast and unfancy micro web framework for Go.
## Features ## Features
@ -20,16 +21,27 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
- `http.HandlerFunc` - `http.HandlerFunc`
- `func(http.ResponseWriter, *http.Request)` - `func(http.ResponseWriter, *http.Request)`
- Sub-router/Groups - Sub-router/Groups
- Handy encoding/decoding functions. - Handy functions to send variety of HTTP response:
- HTML
- HTML via templates
- String
- JSON
- JSONP
- XML
- File
- NoContent
- Redirect
- Error
- Build-in support for: - Build-in support for:
- Favicon
- Index file
- Static files - Static files
- WebSocket - WebSocket
- API to serve index and favicon.
- Centralized HTTP error handling. - Centralized HTTP error handling.
- Customizable request binding function. - Customizable HTTP request binding function.
- Customizable response rendering function, allowing you to use any HTML template engine. - Customizable HTTP response rendering function, allowing you to use any HTML template engine.
## Benchmark ## Performance
Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015. Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015.
@ -75,16 +87,28 @@ BenchmarkZeus_GithubAll 2000 748827 ns/op 30068
$ go get github.com/labstack/echo $ go get github.com/labstack/echo
``` ```
##[Examples](https://github.com/labstack/echo/tree/master/examples) ## [Recipes](https://github.com/labstack/echo/tree/master/recipes)
- [Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello) - [File Upload](http://echo.labstack.com/recipes/file-upload)
- [CRUD](https://github.com/labstack/echo/tree/master/examples/crud) - [Streaming File Upload](http://echo.labstack.com/recipes/streaming-file-upload)
- [Website](https://github.com/labstack/echo/tree/master/examples/website) - [Streaming Response](http://echo.labstack.com/recipes/streaming-response)
- [Middleware](https://github.com/labstack/echo/tree/master/examples/middleware) - [WebSocket](http://echo.labstack.com/recipes/websocket)
- [Stream](https://github.com/labstack/echo/tree/master/examples/stream) - [Subdomains](http://echo.labstack.com/recipes/subdomains)
- [JWT Authentication](http://echo.labstack.com/recipes/jwt-authentication)
- [Graceful Shutdown](http://echo.labstack.com/recipes/graceful-shutdown)
##[Guide](http://echo.labstack.com/guide) ##[Guide](http://echo.labstack.com/guide)
## Echo System
Community created packages for Echo
- [echo-logrus](https://github.com/deoxxa/echo-logrus)
- [go_middleware](https://github.com/rightscale/go_middleware)
- [permissions2](https://github.com/xyproto/permissions2)
- [permissionbolt](https://github.com/xyproto/permissionbolt)
- [echo-middleware](https://github.com/syntaqx/echo-middleware)
## Contribute ## Contribute
**Use issues for everything** **Use issues for everything**
@ -98,7 +122,3 @@ $ go get github.com/labstack/echo
- [Vishal Rana](https://github.com/vishr) - Author - [Vishal Rana](https://github.com/vishr) - Author
- [Nitin Rana](https://github.com/nr17) - Consultant - [Nitin Rana](https://github.com/nr17) - Consultant
- [Contributors](https://github.com/labstack/echo/graphs/contributors) - [Contributors](https://github.com/labstack/echo/graphs/contributors)
## License
[MIT](https://github.com/labstack/echo/blob/master/LICENSE)

View File

@ -2,7 +2,13 @@ package echo
import ( import (
"encoding/json" "encoding/json"
"encoding/xml"
"net/http" "net/http"
"path"
"fmt"
"net/url"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
@ -16,6 +22,7 @@ type (
socket *websocket.Conn socket *websocket.Conn
pnames []string pnames []string
pvalues []string pvalues []string
query url.Values
store store store store
echo *Echo echo *Echo
} }
@ -69,6 +76,19 @@ func (c *Context) Param(name string) (value string) {
return return
} }
// Query returns query parameter by name.
func (c *Context) Query(name string) string {
if c.query == nil {
c.query = c.request.URL.Query()
}
return c.query.Get(name)
}
// Form returns form parameter by name.
func (c *Context) Form(name string) string {
return c.request.FormValue(name)
}
// Get retrieves data from the context. // Get retrieves data from the context.
func (c *Context) Get(key string) interface{} { func (c *Context) Get(key string) interface{} {
return c.store[key] return c.store[key]
@ -76,47 +96,82 @@ func (c *Context) Get(key string) interface{} {
// Set saves data in the context. // Set saves data in the context.
func (c *Context) Set(key string, val interface{}) { func (c *Context) Set(key string, val interface{}) {
if c.store == nil {
c.store = make(store)
}
c.store[key] = val c.store[key] = val
} }
// Bind binds the request body into specified type v. Default binder does it // Bind binds the request body into specified type `i`. The default binder does
// based on Content-Type header. // it based on Content-Type header.
func (c *Context) Bind(i interface{}) error { func (c *Context) Bind(i interface{}) error {
return c.echo.binder(c.request, i) return c.echo.binder.Bind(c.request, i)
} }
// Render invokes the registered HTML template renderer and sends a text/html // Render renders a template with data and sends a text/html response with status
// response with status code. // code. Templates can be registered using `Echo.SetRenderer()`.
func (c *Context) Render(code int, name string, data interface{}) error { func (c *Context) Render(code int, name string, data interface{}) error {
if c.echo.renderer == nil { if c.echo.renderer == nil {
return RendererNotRegistered return RendererNotRegistered
} }
c.response.Header().Set(ContentType, TextHTML) c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
return c.echo.renderer.Render(c.response, name, data) return c.echo.renderer.Render(c.response, name, data)
} }
// JSON sends an application/json response with status code. // HTML formats according to a format specifier and sends HTML response with
// status code.
func (c *Context) HTML(code int, format string, a ...interface{}) (err error) {
c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
c.response.WriteHeader(code)
_, err = fmt.Fprintf(c.response, format, a...)
return
}
// String formats according to a format specifier and sends text response with status
// code.
func (c *Context) String(code int, format string, a ...interface{}) (err error) {
c.response.Header().Set(ContentType, TextPlain)
c.response.WriteHeader(code)
_, err = fmt.Fprintf(c.response, format, a...)
return
}
// JSON sends a JSON response with status code.
func (c *Context) JSON(code int, i interface{}) error { func (c *Context) JSON(code int, i interface{}) error {
c.response.Header().Set(ContentType, ApplicationJSON) c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
return json.NewEncoder(c.response).Encode(i) return json.NewEncoder(c.response).Encode(i)
} }
// String sends a text/plain response with status code. // JSONP sends a JSONP response with status code. It uses `callback` to construct
func (c *Context) String(code int, s string) error { // the JSONP payload.
c.response.Header().Set(ContentType, TextPlain) func (c *Context) JSONP(code int, callback string, i interface{}) (err error) {
c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
_, err := c.response.Write([]byte(s)) c.response.Write([]byte(callback + "("))
return err if err = json.NewEncoder(c.response).Encode(i); err == nil {
c.response.Write([]byte(");"))
}
return
} }
// HTML sends a text/html response with status code. // XML sends an XML response with status code.
func (c *Context) HTML(code int, html string) error { func (c *Context) XML(code int, i interface{}) error {
c.response.Header().Set(ContentType, TextHTML) c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
_, err := c.response.Write([]byte(html)) c.response.Write([]byte(xml.Header))
return err return xml.NewEncoder(c.response).Encode(i)
}
// File sends a response with the content of the file. If attachment is true, the
// client is prompted to save the file.
func (c *Context) File(name string, attachment bool) error {
dir, file := path.Split(name)
if attachment {
c.response.Header().Set(ContentDisposition, "attachment; filename="+file)
}
return serveFile(dir, file, c)
} }
// NoContent sends a response with no body and a status code. // NoContent sends a response with no body and a status code.
@ -126,11 +181,15 @@ func (c *Context) NoContent(code int) error {
} }
// Redirect redirects the request using http.Redirect with status code. // Redirect redirects the request using http.Redirect with status code.
func (c *Context) Redirect(code int, url string) { func (c *Context) Redirect(code int, url string) error {
if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect {
return InvalidRedirectCode
}
http.Redirect(c.response, c.request, url, code) http.Redirect(c.response, c.request, url, code)
return nil
} }
// Error invokes the registered HTTP error handler. Usually used by middleware. // Error invokes the registered HTTP error handler. Generally used by middleware.
func (c *Context) Error(err error) { func (c *Context) Error(err error) {
c.echo.httpErrorHandler(err, c) c.echo.httpErrorHandler(err, c)
} }
@ -138,5 +197,7 @@ func (c *Context) Error(err error) {
func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) { func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) {
c.request = r c.request = r
c.response.reset(w) c.response.reset(w)
c.query = nil
c.store = nil
c.echo = e c.echo = e
} }

View File

@ -10,6 +10,9 @@ import (
"strings" "strings"
"encoding/xml"
"net/url"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -24,8 +27,10 @@ func (t *Template) Render(w io.Writer, name string, data interface{}) error {
} }
func TestContext(t *testing.T) { func TestContext(t *testing.T) {
usr := `{"id":"1","name":"Joe"}` userJSON := `{"id":"1","name":"Joe"}`
req, _ := http.NewRequest(POST, "/", strings.NewReader(usr)) userXML := `<user><id>1</id><name>Joe</name></user>`
req, _ := http.NewRequest(POST, "/", strings.NewReader(userJSON))
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := NewContext(req, NewResponse(rec), New()) c := NewContext(req, NewResponse(rec), New())
@ -38,16 +43,12 @@ func TestContext(t *testing.T) {
// Socket // Socket
assert.Nil(t, c.Socket()) assert.Nil(t, c.Socket())
//------- // Param by id
// Param
//-------
// By id
c.pnames = []string{"id"} c.pnames = []string{"id"}
c.pvalues = []string{"1"} c.pvalues = []string{"1"}
assert.Equal(t, "1", c.P(0)) assert.Equal(t, "1", c.P(0))
// By name // Param by name
assert.Equal(t, "1", c.Param("id")) assert.Equal(t, "1", c.Param("id"))
// Store // Store
@ -59,19 +60,14 @@ func TestContext(t *testing.T) {
//------ //------
// JSON // JSON
testBind(t, c, ApplicationJSON) testBind(t, c, "application/json")
// TODO: Form // XML
c.request.Header.Set(ContentType, ApplicationForm) c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML))
u := new(user) testBind(t, c, ApplicationXML)
err := c.Bind(u)
assert.NoError(t, err)
// Unsupported // Unsupported
c.request.Header.Set(ContentType, "") testBind(t, c, "")
u = new(user)
err = c.Bind(u)
assert.Error(t, err)
//-------- //--------
// Render // Render
@ -81,7 +77,7 @@ func TestContext(t *testing.T) {
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")), templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
} }
c.echo.SetRenderer(tpl) c.echo.SetRenderer(tpl)
err = c.Render(http.StatusOK, "hello", "Joe") err := c.Render(http.StatusOK, "hello", "Joe")
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, Joe!", rec.Body.String()) assert.Equal(t, "Hello, Joe!", rec.Body.String())
@ -92,18 +88,37 @@ func TestContext(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
// JSON // JSON
req.Header.Set(Accept, ApplicationJSON)
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New()) c = NewContext(req, NewResponse(rec), New())
err = c.JSON(http.StatusOK, user{"1", "Joe"}) err = c.JSON(http.StatusOK, user{"1", "Joe"})
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, ApplicationJSON, rec.Header().Get(ContentType)) assert.Equal(t, ApplicationJSONCharsetUTF8, rec.Header().Get(ContentType))
assert.Equal(t, usr, strings.TrimSpace(rec.Body.String())) assert.Equal(t, userJSON+"\n", rec.Body.String())
}
// JSONP
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
callback := "callback"
err = c.JSONP(http.StatusOK, callback, user{"1", "Joe"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, ApplicationJavaScriptCharsetUTF8, rec.Header().Get(ContentType))
assert.Equal(t, callback+"("+userJSON+"\n);", rec.Body.String())
}
// XML
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.XML(http.StatusOK, user{"1", "Joe"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, ApplicationXMLCharsetUTF8, rec.Header().Get(ContentType))
assert.Equal(t, xml.Header, xml.Header, rec.Body.String())
} }
// String // String
req.Header.Set(Accept, TextPlain)
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New()) c = NewContext(req, NewResponse(rec), New())
err = c.String(http.StatusOK, "Hello, World!") err = c.String(http.StatusOK, "Hello, World!")
@ -114,16 +129,34 @@ func TestContext(t *testing.T) {
} }
// HTML // HTML
req.Header.Set(Accept, TextHTML)
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New()) c = NewContext(req, NewResponse(rec), New())
err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>") err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
if assert.NoError(t, err) { if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, TextHTML, rec.Header().Get(ContentType)) assert.Equal(t, TextHTMLCharsetUTF8, rec.Header().Get(ContentType))
assert.Equal(t, "Hello, <strong>World!</strong>", rec.Body.String()) assert.Equal(t, "Hello, <strong>World!</strong>", rec.Body.String())
} }
// File
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.File("test/fixture/walle.png", false)
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, 219885, rec.Body.Len())
}
// File as attachment
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.File("test/fixture/walle.png", true)
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, rec.Header().Get(ContentDisposition), "attachment; filename=walle.png")
assert.Equal(t, 219885, rec.Body.Len())
}
// NoContent // NoContent
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New()) c = NewContext(req, NewResponse(rec), New())
@ -133,7 +166,7 @@ func TestContext(t *testing.T) {
// Redirect // Redirect
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New()) c = NewContext(req, NewResponse(rec), New())
c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo") assert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo"))
// Error // Error
rec = httptest.NewRecorder() rec = httptest.NewRecorder()
@ -145,11 +178,42 @@ func TestContext(t *testing.T) {
c.reset(req, NewResponse(httptest.NewRecorder()), New()) c.reset(req, NewResponse(httptest.NewRecorder()), New())
} }
func TestContextQuery(t *testing.T) {
q := make(url.Values)
q.Set("name", "joe")
q.Set("email", "joe@labstack.com")
req, err := http.NewRequest(GET, "/", nil)
assert.NoError(t, err)
req.URL.RawQuery = q.Encode()
c := NewContext(req, nil, New())
assert.Equal(t, "joe", c.Query("name"))
assert.Equal(t, "joe@labstack.com", c.Query("email"))
}
func TestContextForm(t *testing.T) {
f := make(url.Values)
f.Set("name", "joe")
f.Set("email", "joe@labstack.com")
req, err := http.NewRequest(POST, "/", strings.NewReader(f.Encode()))
assert.NoError(t, err)
req.Header.Add(ContentType, ApplicationForm)
c := NewContext(req, nil, New())
assert.Equal(t, "joe", c.Form("name"))
assert.Equal(t, "joe@labstack.com", c.Form("email"))
}
func testBind(t *testing.T, c *Context, ct string) { func testBind(t *testing.T, c *Context, ct string) {
c.request.Header.Set(ContentType, ct) c.request.Header.Set(ContentType, ct)
u := new(user) u := new(user)
err := c.Bind(u) err := c.Bind(u)
if assert.NoError(t, err) { if ct == "" {
assert.Error(t, UnsupportedMediaType)
} else if assert.NoError(t, err) {
assert.Equal(t, "1", u.ID) assert.Equal(t, "1", u.ID)
assert.Equal(t, "Joe", u.Name) assert.Equal(t, "Joe", u.Name)
} }

164
echo.go
View File

@ -14,8 +14,10 @@ import (
"strings" "strings"
"sync" "sync"
"encoding/xml"
"github.com/bradfitz/http2" "github.com/bradfitz/http2"
"github.com/mattn/go-colorable" "github.com/labstack/gommon/color"
"golang.org/x/net/websocket" "golang.org/x/net/websocket"
) )
@ -28,10 +30,11 @@ type (
notFoundHandler HandlerFunc notFoundHandler HandlerFunc
defaultHTTPErrorHandler HTTPErrorHandler defaultHTTPErrorHandler HTTPErrorHandler
httpErrorHandler HTTPErrorHandler httpErrorHandler HTTPErrorHandler
binder BindFunc binder Binder
renderer Renderer renderer Renderer
pool sync.Pool pool sync.Pool
debug bool debug bool
stripTrailingSlash bool
router *Router router *Router
} }
@ -54,12 +57,20 @@ type (
// HTTPErrorHandler is a centralized HTTP error handler. // HTTPErrorHandler is a centralized HTTP error handler.
HTTPErrorHandler func(error, *Context) HTTPErrorHandler func(error, *Context)
BindFunc func(*http.Request, interface{}) error // Binder is the interface that wraps the Bind method.
Binder interface {
Bind(*http.Request, interface{}) error
}
binder struct {
}
// Validator is the interface that wraps the Validate method.
Validator interface {
Validate() error
}
// Renderer is the interface that wraps the Render method. // Renderer is the interface that wraps the Render method.
//
// Render renders the HTML template with given name and specified data.
// It writes the output to w.
Renderer interface { Renderer interface {
Render(w io.Writer, name string, data interface{}) error Render(w io.Writer, name string, data interface{}) error
} }
@ -89,27 +100,41 @@ const (
// Media types // Media types
//------------- //-------------
ApplicationJSON = "application/json" ApplicationJSON = "application/json"
ApplicationProtobuf = "application/protobuf" ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8
ApplicationMsgpack = "application/msgpack" ApplicationJavaScript = "application/javascript"
TextPlain = "text/plain" ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8
TextHTML = "text/html" ApplicationXML = "application/xml"
ApplicationForm = "application/x-www-form-urlencoded" ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8
MultipartForm = "multipart/form-data" ApplicationForm = "application/x-www-form-urlencoded"
ApplicationProtobuf = "application/protobuf"
ApplicationMsgpack = "application/msgpack"
TextHTML = "text/html"
TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8
TextPlain = "text/plain"
TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8
MultipartForm = "multipart/form-data"
//---------
// Charset
//---------
CharsetUTF8 = "charset=utf-8"
//--------- //---------
// Headers // Headers
//--------- //---------
Accept = "Accept"
AcceptEncoding = "Accept-Encoding" AcceptEncoding = "Accept-Encoding"
Authorization = "Authorization"
ContentDisposition = "Content-Disposition" ContentDisposition = "Content-Disposition"
ContentEncoding = "Content-Encoding" ContentEncoding = "Content-Encoding"
ContentLength = "Content-Length" ContentLength = "Content-Length"
ContentType = "Content-Type" ContentType = "Content-Type"
Authorization = "Authorization" Location = "Location"
Upgrade = "Upgrade" Upgrade = "Upgrade"
Vary = "Vary" Vary = "Vary"
WWWAuthenticate = "WWW-Authenticate"
//----------- //-----------
// Protocols // Protocols
@ -139,9 +164,22 @@ var (
UnsupportedMediaType = errors.New("echo ⇒ unsupported media type") UnsupportedMediaType = errors.New("echo ⇒ unsupported media type")
RendererNotRegistered = errors.New("echo ⇒ renderer not registered") RendererNotRegistered = errors.New("echo ⇒ renderer not registered")
InvalidRedirectCode = errors.New("echo ⇒ invalid redirect status code")
//----------------
// Error handlers
//----------------
notFoundHandler = func(c *Context) error {
return NewHTTPError(http.StatusNotFound)
}
badRequestHandler = func(c *Context) error {
return NewHTTPError(http.StatusBadRequest)
}
) )
// New creates an Echo instance. // New creates an instance of Echo.
func New() (e *Echo) { func New() (e *Echo) {
e = &Echo{maxParam: new(int)} e = &Echo{maxParam: new(int)}
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
@ -153,10 +191,10 @@ func New() (e *Echo) {
// Defaults // Defaults
//---------- //----------
e.HTTP2(false) if runtime.GOOS == "windows" {
e.notFoundHandler = func(c *Context) error { e.DisableColoredLog()
return NewHTTPError(http.StatusNotFound)
} }
e.HTTP2()
e.defaultHTTPErrorHandler = func(err error, c *Context) { e.defaultHTTPErrorHandler = func(err error, c *Context) {
code := http.StatusInternalServerError code := http.StatusInternalServerError
msg := http.StatusText(code) msg := http.StatusText(code)
@ -167,19 +205,13 @@ func New() (e *Echo) {
if e.debug { if e.debug {
msg = err.Error() msg = err.Error()
} }
http.Error(c.response, msg, code) if !c.response.committed {
http.Error(c.response, msg, code)
}
log.Println(err)
} }
e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler) e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler)
e.SetBinder(func(r *http.Request, v interface{}) error { e.SetBinder(&binder{})
ct := r.Header.Get(ContentType)
err := UnsupportedMediaType
if strings.HasPrefix(ct, ApplicationJSON) {
err = json.NewDecoder(r.Body).Decode(v)
} else if strings.HasPrefix(ct, ApplicationForm) {
err = nil
}
return err
})
return return
} }
@ -188,9 +220,14 @@ func (e *Echo) Router() *Router {
return e.router return e.router
} }
// DisableColoredLog disables colored log.
func (e *Echo) DisableColoredLog() {
color.Disable()
}
// HTTP2 enables HTTP2 support. // HTTP2 enables HTTP2 support.
func (e *Echo) HTTP2(on bool) { func (e *Echo) HTTP2() {
e.http2 = on e.http2 = true
} }
// DefaultHTTPErrorHandler invokes the default HTTP error handler. // DefaultHTTPErrorHandler invokes the default HTTP error handler.
@ -204,7 +241,7 @@ func (e *Echo) SetHTTPErrorHandler(h HTTPErrorHandler) {
} }
// SetBinder registers a custom binder. It's invoked by Context.Bind(). // SetBinder registers a custom binder. It's invoked by Context.Bind().
func (e *Echo) SetBinder(b BindFunc) { func (e *Echo) SetBinder(b Binder) {
e.binder = b e.binder = b
} }
@ -213,14 +250,14 @@ func (e *Echo) SetRenderer(r Renderer) {
e.renderer = r e.renderer = r
} }
// SetDebug sets debug mode. // Debug enables debug mode.
func (e *Echo) SetDebug(on bool) { func (e *Echo) Debug() {
e.debug = on e.debug = true
} }
// Debug returns debug mode. // StripTrailingSlash enables removing trailing slash from the request path.
func (e *Echo) Debug() bool { func (e *Echo) StripTrailingSlash() {
return e.debug e.stripTrailingSlash = true
} }
// Use adds handler to the middleware chain. // Use adds handler to the middleware chain.
@ -275,6 +312,20 @@ func (e *Echo) Trace(path string, h Handler) {
e.add(TRACE, path, h) e.add(TRACE, path, h)
} }
// Any adds a route > handler to the router for all HTTP methods.
func (e *Echo) Any(path string, h Handler) {
for _, m := range methods {
e.add(m, path, h)
}
}
// Match adds a route > handler to the router for multiple HTTP methods provided.
func (e *Echo) Match(methods []string, path string, h Handler) {
for _, m := range methods {
e.add(m, path, h)
}
}
// WebSocket adds a WebSocket route > handler to the router. // WebSocket adds a WebSocket route > handler to the router.
func (e *Echo) WebSocket(path string, h HandlerFunc) { func (e *Echo) WebSocket(path string, h HandlerFunc) {
e.Get(path, func(c *Context) (err error) { e.Get(path, func(c *Context) (err error) {
@ -389,7 +440,7 @@ func (e *Echo) URI(h Handler, params ...interface{}) string {
return uri.String() return uri.String()
} }
// URL is an alias for URI. // URL is an alias for `URI` function.
func (e *Echo) URL(h Handler, params ...interface{}) string { func (e *Echo) URL(h Handler, params ...interface{}) string {
return e.URI(h, params...) return e.URI(h, params...)
} }
@ -399,6 +450,7 @@ func (e *Echo) Routes() []Route {
return e.router.routes return e.router.routes
} }
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := e.pool.Get().(*Context) c := e.pool.Get().(*Context)
h, echo := e.router.Find(r.Method, r.URL.Path, c) h, echo := e.router.Find(r.Method, r.URL.Path, c)
@ -406,9 +458,6 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e = echo e = echo
} }
c.reset(r, w, e) c.reset(r, w, e)
if h == nil {
h = e.notFoundHandler
}
// Chain middleware with handler in the end // Chain middleware with handler in the end
for i := len(e.middleware) - 1; i >= 0; i-- { for i := len(e.middleware) - 1; i >= 0; i-- {
@ -423,7 +472,7 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
e.pool.Put(c) e.pool.Put(c)
} }
// Server returns the internal *http.Server // Server returns the internal *http.Server.
func (e *Echo) Server(addr string) *http.Server { func (e *Echo) Server(addr string) *http.Server {
s := &http.Server{Addr: addr} s := &http.Server{Addr: addr}
s.Handler = e s.Handler = e
@ -446,13 +495,13 @@ func (e *Echo) RunTLS(addr, certFile, keyFile string) {
} }
// RunServer runs a custom server. // RunServer runs a custom server.
func (e *Echo) RunServer(srv *http.Server) { func (e *Echo) RunServer(s *http.Server) {
e.run(srv) e.run(s)
} }
// RunTLSServer runs a custom server with TLS configuration. // RunTLSServer runs a custom server with TLS configuration.
func (e *Echo) RunTLSServer(srv *http.Server, certFile, keyFile string) { func (e *Echo) RunTLSServer(s *http.Server, certFile, keyFile string) {
e.run(srv, certFile, keyFile) e.run(s, certFile, keyFile)
} }
func (e *Echo) run(s *http.Server, files ...string) { func (e *Echo) run(s *http.Server, files ...string) {
@ -489,7 +538,7 @@ func (e *HTTPError) Error() string {
return e.message return e.message
} }
// wraps middleware // wrapMiddleware wraps middleware.
func wrapMiddleware(m Middleware) MiddlewareFunc { func wrapMiddleware(m Middleware) MiddlewareFunc {
switch m := m.(type) { switch m := m.(type) {
case MiddlewareFunc: case MiddlewareFunc:
@ -520,7 +569,7 @@ func wrapMiddleware(m Middleware) MiddlewareFunc {
} }
} }
// Wraps HandlerFunc middleware // wrapHandlerFuncMW wraps HandlerFunc middleware.
func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc { func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
return func(h HandlerFunc) HandlerFunc { return func(h HandlerFunc) HandlerFunc {
return func(c *Context) error { return func(c *Context) error {
@ -532,7 +581,7 @@ func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
} }
} }
// Wraps http.HandlerFunc middleware // wrapHTTPHandlerFuncMW wraps http.HandlerFunc middleware.
func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc { func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
return func(h HandlerFunc) HandlerFunc { return func(h HandlerFunc) HandlerFunc {
return func(c *Context) error { return func(c *Context) error {
@ -544,7 +593,7 @@ func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
} }
} }
// wraps handler // wrapHandler wraps handler.
func wrapHandler(h Handler) HandlerFunc { func wrapHandler(h Handler) HandlerFunc {
switch h := h.(type) { switch h := h.(type) {
case HandlerFunc: case HandlerFunc:
@ -566,6 +615,13 @@ func wrapHandler(h Handler) HandlerFunc {
} }
} }
func init() { func (binder) Bind(r *http.Request, i interface{}) (err error) {
log.SetOutput(colorable.NewColorableStdout()) ct := r.Header.Get(ContentType)
err = UnsupportedMediaType
if strings.HasPrefix(ct, ApplicationJSON) {
err = json.NewDecoder(r.Body).Decode(i)
} else if strings.HasPrefix(ct, ApplicationXML) {
err = xml.NewDecoder(r.Body).Decode(i)
}
return
} }

View File

@ -18,8 +18,8 @@ import (
type ( type (
user struct { user struct {
ID string `json:"id"` ID string `json:"id" xml:"id"`
Name string `json:"name"` Name string `json:"name" xml:"name"`
} }
) )
@ -33,8 +33,8 @@ func TestEcho(t *testing.T) {
assert.NotNil(t, e.Router()) assert.NotNil(t, e.Router())
// Debug // Debug
e.SetDebug(true) e.Debug()
assert.True(t, e.Debug()) assert.True(t, e.debug)
// DefaultHTTPErrorHandler // DefaultHTTPErrorHandler
e.DefaultHTTPErrorHandler(errors.New("error"), c) e.DefaultHTTPErrorHandler(errors.New("error"), c)
@ -246,6 +246,20 @@ func TestEchoTrace(t *testing.T) {
testMethod(t, TRACE, "/", e) testMethod(t, TRACE, "/", e)
} }
func TestEchoAny(t *testing.T) { // JFC
e := New()
e.Any("/", func(c *Context) error {
return c.String(http.StatusOK, "Any")
})
}
func TestEchoMatch(t *testing.T) { // JFC
e := New()
e.Match([]string{GET, POST}, "/", func(c *Context) error {
return c.String(http.StatusOK, "Match")
})
}
func TestEchoWebSocket(t *testing.T) { func TestEchoWebSocket(t *testing.T) {
e := New() e := New()
e.WebSocket("/ws", func(c *Context) error { e.WebSocket("/ws", func(c *Context) error {
@ -368,6 +382,14 @@ func TestEchoNotFound(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code) assert.Equal(t, http.StatusNotFound, w.Code)
} }
func TestEchoBadRequest(t *testing.T) {
e := New()
r, _ := http.NewRequest("INVALID", "/files", nil)
w := httptest.NewRecorder()
e.ServeHTTP(w, r)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestEchoHTTPError(t *testing.T) { func TestEchoHTTPError(t *testing.T) {
m := http.StatusText(http.StatusBadRequest) m := http.StatusText(http.StatusBadRequest)
he := NewHTTPError(http.StatusBadRequest, m) he := NewHTTPError(http.StatusBadRequest, m)
@ -381,12 +403,20 @@ func TestEchoServer(t *testing.T) {
assert.IsType(t, &http.Server{}, s) assert.IsType(t, &http.Server{}, s)
} }
func TestStripTrailingSlash(t *testing.T) {
e := New()
e.StripTrailingSlash()
r, _ := http.NewRequest(GET, "/users/", nil)
w := httptest.NewRecorder()
e.ServeHTTP(w, r)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func testMethod(t *testing.T, method, path string, e *Echo) { func testMethod(t *testing.T, method, path string, e *Echo) {
m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:])) m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:]))
p := reflect.ValueOf(path) p := reflect.ValueOf(path)
h := reflect.ValueOf(func(c *Context) error { h := reflect.ValueOf(func(c *Context) error {
c.String(http.StatusOK, method) return c.String(http.StatusOK, method)
return nil
}) })
i := interface{}(e) i := interface{}(e)
reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h}) reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h})

View File

@ -17,7 +17,7 @@ func main() {
e := echo.New() e := echo.New()
// Debug mode // Debug mode
e.SetDebug(true) e.Debug()
//------------ //------------
// Middleware // Middleware
@ -37,16 +37,6 @@ func main() {
return false return false
})) }))
//-------
// Slash
//-------
e.Use(mw.StripTrailingSlash())
// or
// e.Use(mw.RedirectToSlash())
// Gzip // Gzip
e.Use(mw.Gzip()) e.Use(mw.Gzip())

View File

@ -1,18 +0,0 @@
package main
import (
"io"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
func main() {
e := echo.New()
e.Use(mw.Logger())
e.WebSocket("/ws", func(c *echo.Context) error {
io.Copy(c.Socket(), c.Socket())
return nil
})
e.Run(":1323")
}

View File

@ -2,25 +2,22 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"net/http" "net/http"
"github.com/labstack/echo"
) )
type ( type (
BasicValidateFunc func(string, string) bool BasicValidateFunc func(string, string) bool
JWTValidateFunc func(string, jwt.SigningMethod) ([]byte, error)
) )
const ( const (
Basic = "Basic" Basic = "Basic"
Bearer = "Bearer"
) )
// BasicAuth returns an HTTP basic authentication middleware. // BasicAuth returns an HTTP basic authentication middleware.
// //
// For valid credentials it calls the next handler. // For valid credentials it calls the next handler.
// For invalid Authorization header it sends "404 - Bad Request" response.
// For invalid credentials, it sends "401 - Unauthorized" response. // For invalid credentials, it sends "401 - Unauthorized" response.
func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc { func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
return func(c *echo.Context) error { return func(c *echo.Context) error {
@ -31,7 +28,6 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
auth := c.Request().Header.Get(echo.Authorization) auth := c.Request().Header.Get(echo.Authorization)
l := len(Basic) l := len(Basic)
he := echo.NewHTTPError(http.StatusBadRequest)
if len(auth) > l+1 && auth[:l] == Basic { if len(auth) > l+1 && auth[:l] == Basic {
b, err := base64.StdEncoding.DecodeString(auth[l+1:]) b, err := base64.StdEncoding.DecodeString(auth[l+1:])
@ -43,47 +39,11 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
if fn(cred[:i], cred[i+1:]) { if fn(cred[:i], cred[i+1:]) {
return nil return nil
} }
he.SetCode(http.StatusUnauthorized)
} }
} }
} }
} }
return he c.Response().Header().Set(echo.WWWAuthenticate, Basic + " realm=Restricted")
} return echo.NewHTTPError(http.StatusUnauthorized)
}
// JWTAuth returns a JWT authentication middleware.
//
// For valid token it sets JWT claims in the context with key `_claims` and calls
// the next handler.
// For invalid Authorization header it sends "404 - Bad Request" response.
// For invalid credentials, it sends "401 - Unauthorized" response.
func JWTAuth(fn JWTValidateFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
// Skip WebSocket
if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
return nil
}
auth := c.Request().Header.Get("Authorization")
l := len(Bearer)
he := echo.NewHTTPError(http.StatusBadRequest)
if len(auth) > l+1 && auth[:l] == Bearer {
t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
// Lookup key and verify method
if kid := token.Header["kid"]; kid != nil {
return fn(kid.(string), token.Method)
}
return fn("", token.Method)
})
if err == nil && t.Valid {
c.Set("_claims", t.Claims)
return nil
} else {
he.SetCode(http.StatusUnauthorized)
}
}
return he
} }
} }

View File

@ -3,13 +3,11 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"net/http/httptest"
"testing" "testing"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"net/http/httptest"
"time"
) )
func TestBasicAuth(t *testing.T) { func TestBasicAuth(t *testing.T) {
@ -38,80 +36,22 @@ func TestBasicAuth(t *testing.T) {
req.Header.Set(echo.Authorization, auth) req.Header.Set(echo.Authorization, auth)
he := ba(c).(*echo.HTTPError) he := ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusUnauthorized, he.Code()) assert.Equal(t, http.StatusUnauthorized, he.Code())
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Empty Authorization header // Empty Authorization header
req.Header.Set(echo.Authorization, "") req.Header.Set(echo.Authorization, "")
he = ba(c).(*echo.HTTPError) he = ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code()) assert.Equal(t, http.StatusUnauthorized, he.Code())
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Invalid Authorization header // Invalid Authorization header
auth = base64.StdEncoding.EncodeToString([]byte(" :secret")) auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
req.Header.Set(echo.Authorization, auth) req.Header.Set(echo.Authorization, auth)
he = ba(c).(*echo.HTTPError) he = ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code()) assert.Equal(t, http.StatusUnauthorized, he.Code())
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Invalid scheme
auth = "Base " + base64.StdEncoding.EncodeToString([]byte(" :secret"))
req.Header.Set(echo.Authorization, auth)
he = ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code())
// WebSocket // WebSocket
c.Request().Header.Set(echo.Upgrade, echo.WebSocket) c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
assert.NoError(t, ba(c)) assert.NoError(t, ba(c))
} }
func TestJWTAuth(t *testing.T) {
req, _ := http.NewRequest(echo.GET, "/", nil)
rec := httptest.NewRecorder()
c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
key := []byte("key")
fn := func(kid string, method jwt.SigningMethod) ([]byte, error) {
return key, nil
}
ja := JWTAuth(fn)
token := jwt.New(jwt.SigningMethodHS256)
token.Claims["foo"] = "bar"
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
ts, err := token.SignedString(key)
assert.NoError(t, err)
// Valid credentials
auth := Bearer + " " + ts
req.Header.Set(echo.Authorization, auth)
assert.NoError(t, ja(c))
//---------------------
// Invalid credentials
//---------------------
// Expired token
token.Claims["exp"] = time.Now().Add(-time.Second).Unix()
ts, err = token.SignedString(key)
assert.NoError(t, err)
auth = Bearer + " " + ts
req.Header.Set(echo.Authorization, auth)
he := ja(c).(*echo.HTTPError)
assert.Equal(t, http.StatusUnauthorized, he.Code())
// Empty Authorization header
req.Header.Set(echo.Authorization, "")
he = ja(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code())
// Invalid Authorization header
auth = "token"
req.Header.Set(echo.Authorization, auth)
he = ja(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code())
// Invalid scheme
auth = "Bear token"
req.Header.Set(echo.Authorization, auth)
he = ja(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code())
// WebSocket
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
assert.NoError(t, ja(c))
}

View File

@ -11,7 +11,7 @@ import (
func TestRecover(t *testing.T) { func TestRecover(t *testing.T) {
e := echo.New() e := echo.New()
e.SetDebug(true) e.Debug()
req, _ := http.NewRequest(echo.GET, "/", nil) req, _ := http.NewRequest(echo.GET, "/", nil)
rec := httptest.NewRecorder() rec := httptest.NewRecorder()
c := echo.NewContext(req, echo.NewResponse(rec), e) c := echo.NewContext(req, echo.NewResponse(rec), e)

View File

@ -1,46 +0,0 @@
package middleware
import (
"net/http"
"github.com/labstack/echo"
)
type (
RedirectToSlashOptions struct {
Code int
}
)
// StripTrailingSlash returns a middleware which removes trailing slash from request
// path.
func StripTrailingSlash() echo.HandlerFunc {
return func(c *echo.Context) error {
p := c.Request().URL.Path
l := len(p)
if p[l-1] == '/' {
c.Request().URL.Path = p[:l-1]
}
return nil
}
}
// RedirectToSlash returns a middleware which redirects requests without trailing
// slash path to trailing slash path.
func RedirectToSlash(opts ...RedirectToSlashOptions) echo.HandlerFunc {
code := http.StatusMovedPermanently
if len(opts) > 0 {
o := opts[0]
code = o.Code
}
return func(c *echo.Context) error {
p := c.Request().URL.Path
l := len(p)
if p[l-1] != '/' {
c.Redirect(code, p+"/")
}
return nil
}
}

View File

@ -1,27 +0,0 @@
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
)
func TestStripTrailingSlash(t *testing.T) {
req, _ := http.NewRequest(echo.GET, "/users/", nil)
rec := httptest.NewRecorder()
c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
StripTrailingSlash()(c)
assert.Equal(t, "/users", c.Request().URL.Path)
}
func TestRedirectToSlash(t *testing.T) {
req, _ := http.NewRequest(echo.GET, "/users", nil)
rec := httptest.NewRecorder()
c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
RedirectToSlash(RedirectToSlashOptions{Code: http.StatusTemporaryRedirect})(c)
assert.Equal(t, http.StatusTemporaryRedirect, rec.Code)
assert.Equal(t, "/users/", c.Response().Header().Get("Location"))
}

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>File Upload</title>
</head>
<body>
<h1>Upload Files</h1>
<form action="/upload" method=post enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,56 @@
package main
import (
"io"
"os"
"net/http"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
func upload(c *echo.Context) error {
req := c.Request()
// req.ParseMultipartForm(16 << 20) // Max memory 16 MiB
// Read form fields
name := c.Form("name")
email := c.Form("email")
// Read files
files := req.MultipartForm.File["files"]
for _, f := range files {
// Source file
src, err := f.Open()
if err != nil {
return err
}
defer src.Close()
// Destination file
dst, err := os.Create(f.Filename)
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
}
return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
name, email, len(files))
}
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.Post("/upload", upload)
e.Run(":1323")
}

View File

@ -11,10 +11,8 @@ func main() {
// Setup // Setup
e := echo.New() e := echo.New()
e.Get("/", func(c *echo.Context) error { e.Get("/", func(c *echo.Context) error {
c.String(http.StatusOK, "Six sick bricks tick") return c.String(http.StatusOK, "Six sick bricks tick")
return nil
}) })
// Use github.com/facebookgo/grace/gracehttp
gracehttp.Serve(e.Server(":1323")) gracehttp.Serve(e.Server(":1323"))
} }

View File

@ -12,10 +12,8 @@ func main() {
// Setup // Setup
e := echo.New() e := echo.New()
e.Get("/", func(c *echo.Context) error { e.Get("/", func(c *echo.Context) error {
c.String(http.StatusOK, "Sue sews rose on slow jor crows nose") return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
return nil
}) })
// Use github.com/tylerb/graceful
graceful.ListenAndServe(e.Server(":1323"), 5*time.Second) graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
} }

View File

@ -0,0 +1,76 @@
package main
import (
"fmt"
"net/http"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
const (
Bearer = "Bearer"
SigningKey = "somethingsupersecret"
)
// A JSON Web Token middleware
func JWTAuth(key string) echo.HandlerFunc {
return func(c *echo.Context) error {
// Skip WebSocket
if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
return nil
}
auth := c.Request().Header.Get("Authorization")
l := len(Bearer)
he := echo.NewHTTPError(http.StatusUnauthorized)
if len(auth) > l+1 && auth[:l] == Bearer {
t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
// Always check the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// Return the key for validation
return []byte(key), nil
})
if err == nil && t.Valid {
// Store token claims in echo.Context
c.Set("claims", t.Claims)
return nil
}
}
return he
}
}
func accessible(c *echo.Context) error {
return c.String(http.StatusOK, "No auth required for this route.\n")
}
func restricted(c *echo.Context) error {
return c.String(http.StatusOK, "Access granted with JWT.\n")
}
func main() {
// Echo instance
e := echo.New()
// Logger
e.Use(mw.Logger())
// Unauthenticated route
e.Get("/", accessible)
// Restricted group
r := e.Group("/restricted")
r.Use(JWTAuth(SigningKey))
r.Get("", restricted)
// Start server
e.Run(":1323")
}

View File

@ -0,0 +1,24 @@
package main
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
const SigningKey = "somethingsupersecret"
func main() {
// New web token.
token := jwt.New(jwt.SigningMethodHS256)
// Set a header and a claim
token.Header["typ"] = "JWT"
token.Claims["exp"] = time.Now().Add(time.Hour * 96).Unix()
// Generate encoded token
t, _ := token.SignedString([]byte(SigningKey))
fmt.Println(t)
}

View File

@ -0,0 +1,16 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>File Upload</title>
</head>
<body>
<h1>Upload Files</h1>
<form action="/upload" method=post enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>

View File

@ -0,0 +1,79 @@
package main
import (
"io/ioutil"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
"io"
"net/http"
"os"
)
func upload(c *echo.Context) error {
mr, err := c.Request().MultipartReader()
if err != nil {
return err
}
// Read form field `name`
part, err := mr.NextPart()
if err != nil {
return err
}
defer part.Close()
b, err := ioutil.ReadAll(part)
if err != nil {
return err
}
name := string(b)
// Read form field `email`
part, err = mr.NextPart()
if err != nil {
return err
}
defer part.Close()
b, err = ioutil.ReadAll(part)
if err != nil {
return err
}
email := string(b)
// Read files
i := 0
for {
part, err := mr.NextPart()
if err != nil {
if err == io.EOF {
break
}
return err
}
defer part.Close()
file, err := os.Create(part.FileName())
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, part); err != nil {
return err
}
i++
}
return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
name, email, i)
}
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.Post("/upload", upload)
e.Run(":1323")
}

View File

@ -29,7 +29,7 @@ var (
func main() { func main() {
e := echo.New() e := echo.New()
e.Get("/stream", func(c *echo.Context) error { e.Get("/", func(c *echo.Context) error {
c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON) c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
c.Response().WriteHeader(http.StatusOK) c.Response().WriteHeader(http.StatusOK)
for _, l := range locations { for _, l := range locations {

View File

@ -0,0 +1,67 @@
package main
import (
"net/http"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
type Hosts map[string]http.Handler
func (h Hosts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler := h[r.Host]; handler != nil {
handler.ServeHTTP(w, r)
} else {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
}
func main() {
// Host map
hosts := make(Hosts)
//-----
// API
//-----
api := echo.New()
api.Use(mw.Logger())
api.Use(mw.Recover())
hosts["api.localhost:1323"] = api
api.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "API")
})
//------
// Blog
//------
blog := echo.New()
blog.Use(mw.Logger())
blog.Use(mw.Recover())
hosts["blog.localhost:1323"] = blog
blog.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Blog")
})
//---------
// Website
//---------
site := echo.New()
site.Use(mw.Logger())
site.Use(mw.Recover())
hosts["localhost:1323"] = site
site.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Welcome!")
})
http.ListenAndServe(":1323", hosts)
}

View File

@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebSocket</title>
</head>
<body>
<p id="output"></p>
<script>
var loc = window.location;
var uri = 'ws:';
if (loc.protocol === 'https:') {
uri = 'wss:';
}
uri += '//' + loc.host;
uri += loc.pathname + 'ws';
ws = new WebSocket(uri)
ws.onopen = function() {
console.log('Connected')
}
ws.onmessage = function(evt) {
var out = document.getElementById('output');
out.innerHTML += evt.data + '<br>';
}
setInterval(function() {
ws.send('Hello, Server!');
}, 1000);
</script>
</body>
</html>

View File

@ -0,0 +1,35 @@
package main
import (
"fmt"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
"golang.org/x/net/websocket"
)
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.WebSocket("/ws", func(c *echo.Context) (err error) {
ws := c.Socket()
msg := ""
for {
if err = websocket.Message.Send(ws, "Hello, Client!"); err != nil {
return
}
if err = websocket.Message.Receive(ws, &msg); err != nil {
return
}
fmt.Println(msg)
}
return
})
e.Run(":1323")
}

View File

@ -80,3 +80,8 @@ func (r *Response) reset(w http.ResponseWriter) {
r.status = http.StatusOK r.status = http.StatusOK
r.committed = false r.committed = false
} }
//func (r *Response) clear() {
// r.Header().Del(ContentType)
// r.committed = false
//}

150
router.go
View File

@ -4,9 +4,17 @@ import "net/http"
type ( type (
Router struct { Router struct {
trees [21]*node connectTree *node
routes []Route deleteTree *node
echo *Echo getTree *node
headTree *node
optionsTree *node
patchTree *node
postTree *node
putTree *node
traceTree *node
routes []Route
echo *Echo
} }
node struct { node struct {
typ ntype typ ntype
@ -28,19 +36,20 @@ const (
mtype mtype
) )
func NewRouter(e *Echo) (r *Router) { func NewRouter(e *Echo) *Router {
r = &Router{ return &Router{
// trees: make(map[string]*node), connectTree: new(node),
routes: []Route{}, deleteTree: new(node),
echo: e, getTree: new(node),
headTree: new(node),
optionsTree: new(node),
patchTree: new(node),
postTree: new(node),
putTree: new(node),
traceTree: new(node),
routes: []Route{},
echo: e,
} }
for _, m := range methods {
r.trees[r.treeIndex(m)] = &node{
prefix: "",
children: children{},
}
}
return
} }
func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
@ -66,7 +75,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
} else if path[i] == '*' { } else if path[i] == '*' {
r.insert(method, path[:i], nil, stype, nil, e) r.insert(method, path[:i], nil, stype, nil, e)
pnames = append(pnames, "_name") pnames = append(pnames, "_name")
r.insert(method, path[:i+1], h, mtype, pnames, e) r.insert(method, path[:i + 1], h, mtype, pnames, e)
return return
} }
} }
@ -81,7 +90,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t ntype, pnames []st
*e.maxParam = l *e.maxParam = l
} }
cn := r.trees[r.treeIndex(method)] // Current node as root cn := r.findTree(method) // Current node as root
if cn == nil {
panic("echo => invalid method")
}
search := path search := path
for { for {
@ -200,21 +212,89 @@ func (n *node) findChildWithType(t ntype) *node {
return nil return nil
} }
func (r *Router) treeIndex(method string) uint8 { func (r *Router) findTree(method string) (n *node) {
if method[0] == 'P' { switch method[0] {
return method[0]%10 + method[1] - 65 case 'G': // GET
} else { m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
return method[0] % 10 if m == 0x47455400 {
n = r.getTree
}
case 'P': // POST, PUT or PATCH
switch method[1] {
case 'O': // POST
m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
uint32(method[0]) << 24
if m == 0x504f5354 {
n = r.postTree
}
case 'U': // PUT
m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
if m == 0x50555400 {
n = r.putTree
}
case 'A': // PATCH
m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
uint64(method[1]) << 48 | uint64(method[0]) << 56
if m == 0x5041544348000000 {
n = r.patchTree
}
}
case 'D': // DELETE
m := uint64(method[5]) << 16 | uint64(method[4]) << 24 | uint64(method[3]) << 32 |
uint64(method[2]) << 40 | uint64(method[1]) << 48 | uint64(method[0]) << 56
if m == 0x44454c4554450000 {
n = r.deleteTree
}
case 'C': // CONNECT
m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
uint64(method[0]) << 56
if m == 0x434f4e4e45435400 {
n = r.connectTree
}
case 'H': // HEAD
m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
uint32(method[0]) << 24
if m == 0x48454144 {
n = r.headTree
}
case 'O': // OPTIONS
m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
uint64(method[0]) << 56
if m == 0x4f5054494f4e5300 {
n = r.optionsTree
}
case 'T': // TRACE
m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
uint64(method[1]) << 48 | uint64(method[0]) << 56
if m == 0x5452414345000000 {
n = r.traceTree
}
} }
return
} }
func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) { func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) {
cn := r.trees[r.treeIndex(method)] // Current node as root h = notFoundHandler
search := path cn := r.findTree(method) // Current node as root
if cn == nil {
h = badRequestHandler
return
}
// Strip trailing slash
if r.echo.stripTrailingSlash {
l := len(path)
if path[l - 1] == '/' {
path = path[:l - 1]
}
}
var ( var (
search = path
c *node // Child node c *node // Child node
n int // Param counter n int // Param counter
nt ntype // Next type nt ntype // Next type
nn *node // Next node nn *node // Next node
ns string // Next search ns string // Next search
@ -225,10 +305,12 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
// Search order static > param > match-any // Search order static > param > match-any
for { for {
if search == "" { if search == "" {
// Found if cn.handler != nil {
ctx.pnames = cn.pnames // Found
h = cn.handler ctx.pnames = cn.pnames
e = cn.echo h = cn.handler
e = cn.echo
}
return return
} }
@ -287,7 +369,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
} }
// Param node // Param node
Param: Param:
c = cn.findChildWithType(ptype) c = cn.findChildWithType(ptype)
if c != nil { if c != nil {
// Save next // Save next
@ -307,7 +389,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
} }
// Match-any node // Match-any node
MatchAny: MatchAny:
// c = cn.getChild() // c = cn.getChild()
c = cn.findChildWithType(mtype) c = cn.findChildWithType(mtype)
if c != nil { if c != nil {
@ -326,10 +408,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := r.echo.pool.Get().(*Context) c := r.echo.pool.Get().(*Context)
h, _ := r.Find(req.Method, req.URL.Path, c) h, _ := r.Find(req.Method, req.URL.Path, c)
c.reset(req, w, r.echo) c.reset(req, w, r.echo)
if h == nil { if err := h(c); err != nil {
c.Error(NewHTTPError(http.StatusNotFound)) r.echo.httpErrorHandler(err, c)
} else {
h(c)
} }
r.echo.pool.Put(c) r.echo.pool.Put(c)
} }

View File

@ -316,9 +316,6 @@ func TestRouterTwoParam(t *testing.T) {
assert.Equal(t, "1", c.P(0)) assert.Equal(t, "1", c.P(0))
assert.Equal(t, "1", c.P(1)) assert.Equal(t, "1", c.P(1))
} }
h, _ = r.Find(GET, "/users/1", c)
assert.Nil(t, h)
} }
func TestRouterMatchAny(t *testing.T) { func TestRouterMatchAny(t *testing.T) {
@ -384,7 +381,10 @@ func TestRouterMultiRoute(t *testing.T) {
// Route > /user // Route > /user
h, _ = r.Find(GET, "/user", c) h, _ = r.Find(GET, "/user", c)
assert.Nil(t, h) if assert.IsType(t, new(HTTPError), h(c)) {
he := h(c).(*HTTPError)
assert.Equal(t, http.StatusNotFound, he.code)
}
} }
func TestRouterPriority(t *testing.T) { func TestRouterPriority(t *testing.T) {
@ -537,6 +537,16 @@ func TestRouterAPI(t *testing.T) {
} }
} }
func TestRouterAddInvalidMethod(t *testing.T) {
e := New()
r := e.router
assert.Panics(t, func() {
r.Add("INVALID", "/", func(*Context) error {
return nil
}, e)
})
}
func TestRouterServeHTTP(t *testing.T) { func TestRouterServeHTTP(t *testing.T) {
e := New() e := New()
r := e.router r := e.router

BIN
test/fixture/walle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

@ -23,7 +23,7 @@ $ go get -u github.com/labstack/echo
``` ```
Echo follows [semantic versioning](http://semver.org) managed through GitHub releases. Echo follows [semantic versioning](http://semver.org) managed through GitHub releases.
Specific version of Echo can be installed using any [package manager](https://github.com/avelino/awesome-go#package-management). Specific version of Echo can be installed using a [package manager](https://github.com/avelino/awesome-go#package-management).
## Customization ## Customization
@ -42,54 +42,45 @@ and message `HTTPError.Message`.
### Debug ### Debug
`Echo.SetDebug(on bool)` `Echo.Debug()`
Enables debug mode. Enables debug mode.
### Disable colored log
`Echo.DisableColoredLog()`
### StripTrailingSlash
StripTrailingSlash enables removing trailing slash from the request path.
`e.StripTrailingSlash()`
## Routing ## Routing
Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and
flexible. It's based on [redix tree](http://en.wikipedia.org/wiki/Radix_tree) flexible. It's based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree) data
data structure which makes routing lookup really fast. It leverages structure which makes route lookup really fast. Router leverages [sync pool](https://golang.org/pkg/sync/#Pool)
[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve to reuse memory and achieve zero dynamic memory allocation with no GC overhead.
zero dynamic memory allocation with no GC overhead.
Routes can be registered by specifying HTTP method, path and a handler. For example, Routes can be registered by specifying HTTP method, path and a handler. For example,
code below registers a route for method `GET`, path `/hello` and a handler which sends code below registers a route for method `GET`, path `/hello` and a handler which sends
`Hello!` HTTP response. `Hello!` HTTP response.
```go ```go
echo.Get("/hello", func(c *echo.Context) error { e.Get("/hello", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello!") return c.String(http.StatusOK, "Hello!")
}) })
``` ```
Echo's default handler is `func(*echo.Context) error` where `echo.Context` Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily
primarily holds HTTP request and response objects. Echo also has a support for other holds HTTP request and response objects. Echo also has a support for other types
types of handlers. of handlers.
### Path parameter
Request path parameters can be extracted either by name `Echo.Context.Param(name string) string`
or by index `Echo.Context.P(i int) string`. Getting parameter by index gives a
slightly better performance.
```go
echo.Get("/users/:id", func(c *echo.Context) error {
// By name
id := c.Param("id")
// By index
id := c.P(0)
return c.String(http.StatusOK, id)
})
```
### Match-any ### Match-any
Matches zero or more characters in the path. For example, pattern `/users/*` will Matches zero or more characters in the path. For example, pattern `/users/*` will
match match:
- `/users/` - `/users/`
- `/users/1` - `/users/1`
@ -118,13 +109,13 @@ e.Get("/users/1/files/*", func(c *echo.Context) error {
}) })
``` ```
Above routes would resolve in order Above routes would resolve in the following order:
- `/users/new` - `/users/new`
- `/users/:id` - `/users/:id`
- `/users/1/files/*` - `/users/1/files/*`
Routes can be written in any order. > Routes can be written in any order.
### Group ### Group
@ -150,15 +141,15 @@ e.Use(mw.BasicAuth(func(usr, pwd string) bool {
### URI building ### URI building
`Echo.URI` can be used generate URI for any handler with specified path parameters. `Echo.URI` can be used to generate URI for any handler with specified path parameters.
It's helpful to centralize all your URI patterns which ease in refactoring your It's helpful to centralize all your URI patterns which ease in refactoring your
application. application.
`echo.URI(h, 1)` will generate `/users/1` for the route registered below `e.URI(h, 1)` will generate `/users/1` for the route registered below
```go ```go
// Handler // Handler
h := func(*echo.Context) error { h := func(c *echo.Context) error {
return c.String(http.StatusOK, "OK") return c.String(http.StatusOK, "OK")
} }
@ -168,9 +159,9 @@ e.Get("/users/:id", h)
## Middleware ## Middleware
Middleware is function which is chained in the HTTP request-response cycle. Middleware Middleware is a function which is chained in the HTTP request-response cycle. Middleware
has access to the request and response objects which it utilizes to perform a specific has access to the request and response objects which it utilizes to perform a specific
action for example, logging every request. Echo supports variety of [middleware](/#features). action, for example, logging every request.
### Logger ### Logger
@ -195,7 +186,7 @@ BasicAuth middleware provides an HTTP basic authentication.
*Example* *Example*
```go ```go
echo.Group("/admin") e.Group("/admin")
e.Use(mw.BasicAuth(func(usr, pwd string) bool { e.Use(mw.BasicAuth(func(usr, pwd string) bool {
if usr == "joe" && pwd == "secret" { if usr == "joe" && pwd == "secret" {
return true return true
@ -225,63 +216,163 @@ to the centralized [HTTPErrorHandler](#error-handling).
e.Use(mw.Recover()) e.Use(mw.Recover())
``` ```
### StripTrailingSlash [Examples](https://github.com/labstack/echo/tree/master/examples/middleware)
StripTrailingSlash middleware removes the trailing slash from request path. ## Request
### Path parameter
Path parameter can be retrieved either by name `Context.Param(name string) string`
or by index `Context.P(i int) string`. Getting parameter by index gives a slightly
better performance.
*Example* *Example*
```go ```go
e.Use(mw.StripTrailingSlash()) e.Get("/users/:name", func(c *echo.Context) error {
// By name
name := c.Param("name")
// By index
name := c.P(0)
return c.String(http.StatusOK, name)
})
``` ```
### RedirectToSlash ```sh
RedirectToSlash middleware redirects requests without trailing slash path to trailing $ curl http://localhost:1323/users/joe
slash path. ```
### Query parameter
Query parameter can be retrieved by name using `Context.Query(name string)`.
*Example*
*Options*
```go ```go
RedirectToSlashOptions struct { e.Get("/users", func(c *echo.Context) error {
Code int name := c.Query("name")
return c.String(http.StatusOK, name)
})
```
```sh
$ curl -G -d "name=joe" http://localhost:1323/users
```
### Form parameter
Form parameter can be retrieved by name using `Context.Form(name string)`.
*Example*
```go
e.Post("/users", func(c *echo.Context) error {
name := c.Form("name")
return c.String(http.StatusOK, name)
})
```
```sh
$ curl -d "name=joe" http://localhost:1323/users
```
## Response
### Template
```go
Context.Render(code int, name string, data interface{}) error
```
Renders a template with data and sends a text/html response with status code. Templates
can be registered using `Echo.SetRenderer()`, allowing us to use any template engine.
Below is an example using Go `html/template`
- Implement `echo.Render` interface
```go
Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}) error {
return t.templates.ExecuteTemplate(w, name, data)
} }
``` ```
*Example* - Pre-compile templates
```go `public/views/hello.html`
e.Use(mw.RedirectToSlash())
```html
{{define "hello"}}Hello, {{.}}!{{end}}
``` ```
> StripTrailingSlash and RedirectToSlash middleware should not be used together. ```go
t := &Template{
templates: template.Must(template.ParseGlob("public/views/*.html")),
}
```
[Examples](https://github.com/labstack/echo/tree/master/examples/middleware) - Register templates
## Response ```go
e := echo.New()
e.SetRenderer(t)
e.Get("/hello", Hello)
```
- Render template
```go
func Hello(c *echo.Context) error {
return c.Render(http.StatusOK, "hello", "World")
}
```
### JSON ### JSON
```go ```go
context.JSON(code int, v interface{}) error Context.JSON(code int, v interface{}) error
``` ```
Sends a JSON HTTP response with status code. Sends a JSON HTTP response with status code.
### String ### XML
```go ```go
context.String(code int, s string) error Context.XML(code int, v interface{}) error
``` ```
Sends a text/plain HTTP response with status code. Sends an XML HTTP response with status code.
### HTML ### HTML
```go ```go
func (c *Context) HTML(code int, html string) error Context.HTML(code int, html string) error
``` ```
Sends an HTML HTTP response with status code. Sends an HTML HTTP response with status code.
### String
```go
Context.String(code int, s string) error
```
Sends a text/plain HTTP response with status code.
### File
```go
Context.File(name string, attachment bool) error
```
File sends a response with the content of the file. If attachment is `true`, the client
is prompted to save the file.
### Static files ### Static files
`Echo.Static(path, root string)` serves static files. For example, code below serves `Echo.Static(path, root string)` serves static files. For example, code below serves
@ -360,7 +451,3 @@ func welcome(c *echo.Context) error {
``` ```
See how [HTTPErrorHandler](#customization) handles it. See how [HTTPErrorHandler](#customization) handles it.
## Deployment
*WIP*

View File

@ -1,13 +1,9 @@
# Echo # Echo
Build simple and performant systems! A fast and unfancy micro web framework for Go.
--- ---
## Overview
Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
## Features ## Features
- Fast HTTP router which smartly prioritize routes. - Fast HTTP router which smartly prioritize routes.
@ -27,14 +23,25 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
- `http.HandlerFunc` - `http.HandlerFunc`
- `func(http.ResponseWriter, *http.Request)` - `func(http.ResponseWriter, *http.Request)`
- Sub-router/Groups - Sub-router/Groups
- Handy encoding/decoding functions. - Handy functions to send variety of HTTP response:
- HTML
- HTML via templates
- String
- JSON
- JSONP
- XML
- File
- NoContent
- Redirect
- Error
- Build-in support for: - Build-in support for:
- Favicon
- Index file
- Static files - Static files
- WebSocket - WebSocket
- API to serve index and favicon.
- Centralized HTTP error handling. - Centralized HTTP error handling.
- Customizable request binding function. - Customizable HTTP request binding function.
- Customizable response rendering function, allowing you to use any HTML template engine. - Customizable HTTP response rendering function, allowing you to use any HTML template engine.
## Performance ## Performance
@ -48,9 +55,9 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
$ go get github.com/labstack/echo $ go get github.com/labstack/echo
``` ```
###[Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello) ### Hello, World!
Create `server.go` with the following content Create `server.go`
```go ```go
package main package main
@ -83,24 +90,7 @@ func main() {
} }
``` ```
`echo.New()` returns a new instance of Echo. Start server
`e.Use(mw.Logger())` adds logging middleware to the chain. It logs every HTTP request
made to the server, producing output
```sh
2015/06/07 18:16:16 GET / 200 13.238µs 14
```
`e.Get("/", hello)` Registers hello handler for HTTP method `GET` and path `/`, so
whenever server receives an HTTP request at `/`, hello function is called.
In hello handler `c.String(http.StatusOK, "Hello, World!\n")` sends a text/plain
HTTP response to the client with 200 status code.
`e.Run(":1323")` Starts HTTP server at network address `:1323`.
Now start the server using command
```sh ```sh
$ go run server.go $ go run server.go
@ -110,7 +100,7 @@ Browse to [http://localhost:1323](http://localhost:1323) and you should see
Hello, World! on the page. Hello, World! on the page.
### Next? ### Next?
- Browse [examples](https://github.com/labstack/echo/tree/master/examples) - Browse [recipes](https://github.com/labstack/echo/tree/master/recipes)
- Head over to [Guide](guide.md) - Head over to [Guide](guide.md)
## Contribute ## Contribute

View File

@ -0,0 +1,96 @@
## File Upload
- Multipart/form-data file upload
- Multiple form fields and files
Use `req.ParseMultipartForm(16 << 20)` for manually parsing multipart form. It gives
us an option to specify the maximum memory used while parsing the request body.
## Server
`server.go`
```go
package main
import (
"io"
"os"
"net/http"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
func upload(c *echo.Context) error {
req := c.Request()
// req.ParseMultipartForm(16 << 20) // Max memory 16 MiB
// Read form fields
name := c.Form("name")
email := c.Form("email")
// Read files
files := req.MultipartForm.File["files"]
for _, f := range files {
// Source file
src, err := f.Open()
if err != nil {
return err
}
defer src.Close()
// Destination file
dst, err := os.Create(f.Filename)
if err != nil {
return err
}
defer dst.Close()
if _, err = io.Copy(dst, src); err != nil {
return err
}
}
return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
name, email, len(files))
}
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.Post("/upload", upload)
e.Run(":1323")
}
```
## Client
`index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>File Upload</title>
</head>
<body>
<h1>Upload Files</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/file-upload)

View File

@ -0,0 +1,58 @@
## Graceful Shutdown
### With [graceful](https://github.com/tylerb/graceful)
`server.go`
```go
package main
import (
"net/http"
"time"
"github.com/labstack/echo"
"github.com/tylerb/graceful"
)
func main() {
// Setup
e := echo.New()
e.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
})
graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
}
```
### With [grace](https://github.com/facebookgo/grace)
`server.go`
```go
package main
import (
"net/http"
"github.com/facebookgo/grace/gracehttp"
"github.com/labstack/echo"
)
func main() {
// Setup
e := echo.New()
e.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Six sick bricks tick")
})
gracehttp.Serve(e.Server(":1323"))
}
```
## Source Code
[`graceful`](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/graceful)
[`grace`](https://github.com/labstack/echo/blob/master/recipes/graceful-shutdown/grace)

View File

@ -0,0 +1,156 @@
## JWT Authentication
Most applications dealing with client authentication will require a more secure
mechanism than that provided by [basic authentication](https://github.com/labstack/echo/blob/master/middleware/auth.go). [JSON Web Tokens](http://jwt.io/)
are one such mechanism - JWTs are a compact means of transferring cryptographically
signed claims between the client and server.
This recipe demonstrates the use of a simple JWT authentication Echo middleware
using Dave Grijalva's [jwt-go](https://github.com/dgrijalva/jwt-go). This middleware
expects the token to be present in an Authorization HTTP header using the method
"Bearer", although JWTs are also frequently sent using cookies, the request URL,
or even the request body. We will use the HS236 signing method, note that several
other algorithms are available.
`server.go`
```go
package main
import (
"fmt"
"net/http"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
const (
Bearer = "Bearer"
SigningKey = "somethingsupersecret"
)
// A JSON Web Token middleware
func JWTAuth(key string) echo.HandlerFunc {
return func(c *echo.Context) error {
// Skip WebSocket
if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
return nil
}
auth := c.Request().Header.Get("Authorization")
l := len(Bearer)
he := echo.NewHTTPError(http.StatusUnauthorized)
if len(auth) > l+1 && auth[:l] == Bearer {
t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
// Always check the signing method
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
// Return the key for validation
return []byte(key), nil
})
if err == nil && t.Valid {
// Store token claims in echo.Context
c.Set("claims", t.Claims)
return nil
}
}
return he
}
}
func accessible(c *echo.Context) error {
return c.String(http.StatusOK, "No auth required for this route.\n")
}
func restricted(c *echo.Context) error {
return c.String(http.StatusOK, "Access granted with JWT.\n")
}
func main() {
// Echo instance
e := echo.New()
// Logger
e.Use(mw.Logger())
// Unauthenticated route
e.Get("/", accessible)
// Restricted group
r := e.Group("/restricted")
r.Use(JWTAuth(SigningKey))
r.Get("", restricted)
// Start server
e.Run(":1323")
}
```
Run `server.go` and making a request to the root path `/` returns a 200 OK response,
as this route does not use our JWT authentication middleware. Sending requests to
`/restricted` (our authenticated route) with either no Authorization header or invalid
Authorization headers / tokens will return 401 Unauthorized.
```sh
# Unauthenticated route
$ curl localhost:1323/ => No auth required for this route.
# No Authentication header
$ curl localhost:1323/restricted => Unauthorized
# Invalid Authentication method
$ curl localhost:1323/restricted -H "Authorization: Invalid " => Unauthorized
# Invalid token
$ curl localhost:1323/restricted -H "Authorization: Bearer InvalidToken" => Unauthorized
```
Running `token.go` (source) will print JWT that is valid against this middleware
to stdout. You can use this token to test succesful authentication on the `/restricted` path.
```go
package main
import (
"fmt"
"time"
"github.com/dgrijalva/jwt-go"
)
const SigningKey = "somethingsupersecret"
func main() {
// New web token.
token := jwt.New(jwt.SigningMethodHS256)
// Set a header and a claim
token.Header["typ"] = "JWT"
token.Claims["exp"] = time.Now().Add(time.Hour * 96).Unix()
// Generate encoded token
t, _ := token.SignedString([]byte(SigningKey))
fmt.Println(t)
}
```
```sh
# Valid token
$ curl localhost:1323/restricted -H "Authorization: Bearer <token>" => Access granted with JWT.
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/jwt-authentication)

View File

@ -0,0 +1,117 @@
## Streaming File Upload
- Streaming multipart/form-data file upload
- Multiple form fields and files
## Server
`server.go`
```go
package main
import (
"io/ioutil"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
"io"
"net/http"
"os"
)
func upload(c *echo.Context) error {
mr, err := c.Request().MultipartReader()
if err != nil {
return err
}
// Read form field `name`
part, err := mr.NextPart()
if err != nil {
return err
}
defer part.Close()
b, err := ioutil.ReadAll(part)
if err != nil {
return err
}
name := string(b)
// Read form field `email`
part, err = mr.NextPart()
if err != nil {
return err
}
defer part.Close()
b, err = ioutil.ReadAll(part)
if err != nil {
return err
}
email := string(b)
// Read files
i := 0
for {
part, err := mr.NextPart()
if err != nil {
if err == io.EOF {
break
}
return err
}
defer part.Close()
file, err := os.Create(part.FileName())
if err != nil {
return err
}
defer file.Close()
if _, err := io.Copy(file, part); err != nil {
return err
}
i++
}
return c.String(http.StatusOK, "Thank You! %s <%s>, %d files uploaded successfully.",
name, email, i)
}
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.Post("/upload", upload)
e.Run(":1323")
}
```
## Client
`index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>File Upload</title>
</head>
<body>
<h1>Upload Files</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
Name: <input type="text" name="name"><br>
Email: <input type="email" name="email"><br>
Files: <input type="file" name="files" multiple><br><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-file-upload)

View File

@ -0,0 +1,75 @@
## Streaming Response
- Send data as it is produced
- Streaming JSON response with chunked transfer encoding
## Server
`server.go`
```go
package main
import (
"net/http"
"time"
"encoding/json"
"github.com/labstack/echo"
)
type (
Geolocation struct {
Altitude float64
Latitude float64
Longitude float64
}
)
var (
locations = []Geolocation{
{-97, 37.819929, -122.478255},
{1899, 39.096849, -120.032351},
{2619, 37.865101, -119.538329},
{42, 33.812092, -117.918974},
{15, 37.77493, -122.419416},
}
)
func main() {
e := echo.New()
e.Get("/", func(c *echo.Context) error {
c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
for _, l := range locations {
if err := json.NewEncoder(c.Response()).Encode(l); err != nil {
return err
}
c.Response().Flush()
time.Sleep(1 * time.Second)
}
return nil
})
e.Run(":1323")
}
```
## Client
```sh
$ curl localhost:1323
```
## Output
```sh
{"Altitude":-97,"Latitude":37.819929,"Longitude":-122.478255}
{"Altitude":1899,"Latitude":39.096849,"Longitude":-120.032351}
{"Altitude":2619,"Latitude":37.865101,"Longitude":-119.538329}
{"Altitude":42,"Latitude":33.812092,"Longitude":-117.918974}
{"Altitude":15,"Latitude":37.77493,"Longitude":-122.419416}
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-response)

View File

@ -0,0 +1,75 @@
## Subdomains
`server.go`
```go
package main
import (
"net/http"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
)
type Hosts map[string]http.Handler
func (h Hosts) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler := h[r.Host]; handler != nil {
handler.ServeHTTP(w, r)
} else {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
}
}
func main() {
// Host map
hosts := make(Hosts)
//-----
// API
//-----
api := echo.New()
api.Use(mw.Logger())
api.Use(mw.Recover())
hosts["api.localhost:1323"] = api
api.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "API")
})
//------
// Blog
//------
blog := echo.New()
blog.Use(mw.Logger())
blog.Use(mw.Recover())
hosts["blog.localhost:1323"] = blog
blog.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Blog")
})
//---------
// Website
//---------
site := echo.New()
site.Use(mw.Logger())
site.Use(mw.Recover())
hosts["localhost:1323"] = site
site.Get("/", func(c *echo.Context) error {
return c.String(http.StatusOK, "Welcome!")
})
http.ListenAndServe(":1323", hosts)
}
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/subdomains)

View File

@ -0,0 +1,111 @@
## WebSocket
## Server
`server.go`
```go
package main
import (
"fmt"
"github.com/labstack/echo"
mw "github.com/labstack/echo/middleware"
"golang.org/x/net/websocket"
)
func main() {
e := echo.New()
e.Use(mw.Logger())
e.Use(mw.Recover())
e.Static("/", "public")
e.WebSocket("/ws", func(c *echo.Context) (err error) {
ws := c.Socket()
msg := ""
for {
if err = websocket.Message.Send(ws, "Hello, Client!"); err != nil {
return
}
if err = websocket.Message.Receive(ws, &msg); err != nil {
return
}
fmt.Println(msg)
}
return
})
e.Run(":1323")
}
```
## Client
`index.html`
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>WebSocket</title>
</head>
<body>
<p id="output"></p>
<script>
var loc = window.location;
var uri = 'ws:';
if (loc.protocol === 'https:') {
uri = 'wss:';
}
uri += '//' + loc.host;
uri += loc.pathname + 'ws';
ws = new WebSocket(uri)
ws.onopen = function() {
console.log('Connected')
}
ws.onmessage = function(evt) {
var out = document.getElementById('output');
out.innerHTML += evt.data + '<br>';
}
setInterval(function() {
ws.send('Hello, Server!');
}, 1000);
</script>
</body>
</html>
```
## Output
`Client`
```sh
Hello, Client!
Hello, Client!
Hello, Client!
Hello, Client!
Hello, Client!
```
`Server`
```sh
Hello, Server!
Hello, Server!
Hello, Server!
Hello, Server!
Hello, Server!
```
## [Source Code](https://github.com/labstack/echo/blob/master/recipes/websocket)

View File

@ -10,11 +10,11 @@
{% if favicon %}<link rel="shortcut icon" href="{{ favicon }}"> {% if favicon %}<link rel="shortcut icon" href="{{ favicon }}">
{% else %}<link rel="shortcut icon" href="{{ base_url }}/img/favicon.ico">{% endif %} {% else %}<link rel="shortcut icon" href="{{ base_url }}/img/favicon.ico">{% endif %}
<title>{% if page_title %}{{ page_title }} - {% endif %}{{ site_name }}</title> <title>{% if page_title %}{{ page_title }} - {% endif %}{{ config.extra.site_title }}</title>
<link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet"> <link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet">
<link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet"> <link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet">
<link rel="stylesheet" href="{{ base_url }}/css/highlight.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/tomorrow-night.min.css">
<link href="{{ base_url }}/css/base.css" rel="stylesheet"> <link href="{{ base_url }}/css/base.css" rel="stylesheet">
<link href="{{ base_url }}/css/echo.css" rel="stylesheet"> <link href="{{ base_url }}/css/echo.css" rel="stylesheet">
{%- for path in extra_css %} {%- for path in extra_css %}
@ -75,4 +75,4 @@
<script src="{{ path }}"></script> <script src="{{ path }}"></script>
{%- endfor %} {%- endfor %}
</body> </body>
</html> </html>

View File

@ -1,11 +1,19 @@
site_name: Echo site_name: Echo
theme: flatly
theme: journal
theme_dir: echo theme_dir: echo
copyright: '&copy; 2015 LabStack' copyright: '&copy; 2015 LabStack'
repo_url: https://github.com/labstack/echo repo_url: https://github.com/labstack/echo
google_analytics: ['UA-51208124-3', 'auto'] google_analytics: ['UA-51208124-3', 'auto']
pages:
- Home: index.md
- Guide: guide.md
- Recipes:
- File Upload: recipes/file-upload.md
- Streaming File Upload: recipes/streaming-file-upload.md
- Streaming Response: recipes/streaming-response.md
- WebSocket: recipes/websocket.md
- Subdomains: recipes/subdomains.md
- JWT Authentication: recipes/jwt-authentication.md
- Graceful Shutdown: recipes/graceful-shutdown.md
extra:
site_title: Echo, a fast and unfancy micro web framework for Go.