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
# https://github.com/github/linguist#using-gitattributes
examples/* linguist-documentation
recipes/* linguist-documentation
website/* linguist-documentation

8
.gitignore vendored
View File

@ -1,10 +1,10 @@
# Website
site/
.publish/
site
.publish
# Node.js
node_modules/
node_modules
# IntelliJ
.idea/
.idea
*.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 is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
# [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)
A fast and unfancy micro web framework for Go.
## Features
@ -20,16 +21,27 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
- `http.HandlerFunc`
- `func(http.ResponseWriter, *http.Request)`
- 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:
- Favicon
- Index file
- Static files
- WebSocket
- API to serve index and favicon.
- Centralized HTTP error handling.
- Customizable request binding function.
- Customizable response rendering function, allowing you to use any HTML template engine.
- Customizable HTTP request binding function.
- 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.
@ -75,16 +87,28 @@ BenchmarkZeus_GithubAll 2000 748827 ns/op 30068
$ 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)
- [CRUD](https://github.com/labstack/echo/tree/master/examples/crud)
- [Website](https://github.com/labstack/echo/tree/master/examples/website)
- [Middleware](https://github.com/labstack/echo/tree/master/examples/middleware)
- [Stream](https://github.com/labstack/echo/tree/master/examples/stream)
- [File Upload](http://echo.labstack.com/recipes/file-upload)
- [Streaming File Upload](http://echo.labstack.com/recipes/streaming-file-upload)
- [Streaming Response](http://echo.labstack.com/recipes/streaming-response)
- [WebSocket](http://echo.labstack.com/recipes/websocket)
- [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)
## 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
**Use issues for everything**
@ -98,7 +122,3 @@ $ go get github.com/labstack/echo
- [Vishal Rana](https://github.com/vishr) - Author
- [Nitin Rana](https://github.com/nr17) - Consultant
- [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 (
"encoding/json"
"encoding/xml"
"net/http"
"path"
"fmt"
"net/url"
"golang.org/x/net/websocket"
)
@ -16,6 +22,7 @@ type (
socket *websocket.Conn
pnames []string
pvalues []string
query url.Values
store store
echo *Echo
}
@ -69,6 +76,19 @@ func (c *Context) Param(name string) (value string) {
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.
func (c *Context) Get(key string) interface{} {
return c.store[key]
@ -76,47 +96,82 @@ func (c *Context) Get(key string) interface{} {
// Set saves data in the context.
func (c *Context) Set(key string, val interface{}) {
if c.store == nil {
c.store = make(store)
}
c.store[key] = val
}
// Bind binds the request body into specified type v. Default binder does it
// based on Content-Type header.
// Bind binds the request body into specified type `i`. The default binder does
// it based on Content-Type header.
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
// response with status code.
// Render renders a template with data and sends a text/html response with status
// code. Templates can be registered using `Echo.SetRenderer()`.
func (c *Context) Render(code int, name string, data interface{}) error {
if c.echo.renderer == nil {
return RendererNotRegistered
}
c.response.Header().Set(ContentType, TextHTML)
c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
c.response.WriteHeader(code)
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 {
c.response.Header().Set(ContentType, ApplicationJSON)
c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8)
c.response.WriteHeader(code)
return json.NewEncoder(c.response).Encode(i)
}
// String sends a text/plain response with status code.
func (c *Context) String(code int, s string) error {
c.response.Header().Set(ContentType, TextPlain)
// JSONP sends a JSONP response with status code. It uses `callback` to construct
// the JSONP payload.
func (c *Context) JSONP(code int, callback string, i interface{}) (err error) {
c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8)
c.response.WriteHeader(code)
_, err := c.response.Write([]byte(s))
return err
c.response.Write([]byte(callback + "("))
if err = json.NewEncoder(c.response).Encode(i); err == nil {
c.response.Write([]byte(");"))
}
return
}
// HTML sends a text/html response with status code.
func (c *Context) HTML(code int, html string) error {
c.response.Header().Set(ContentType, TextHTML)
// XML sends an XML response with status code.
func (c *Context) XML(code int, i interface{}) error {
c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8)
c.response.WriteHeader(code)
_, err := c.response.Write([]byte(html))
return err
c.response.Write([]byte(xml.Header))
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.
@ -126,11 +181,15 @@ func (c *Context) NoContent(code int) error {
}
// 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)
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) {
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) {
c.request = r
c.response.reset(w)
c.query = nil
c.store = nil
c.echo = e
}

View File

@ -10,6 +10,9 @@ import (
"strings"
"encoding/xml"
"net/url"
"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) {
usr := `{"id":"1","name":"Joe"}`
req, _ := http.NewRequest(POST, "/", strings.NewReader(usr))
userJSON := `{"id":"1","name":"Joe"}`
userXML := `<user><id>1</id><name>Joe</name></user>`
req, _ := http.NewRequest(POST, "/", strings.NewReader(userJSON))
rec := httptest.NewRecorder()
c := NewContext(req, NewResponse(rec), New())
@ -38,16 +43,12 @@ func TestContext(t *testing.T) {
// Socket
assert.Nil(t, c.Socket())
//-------
// Param
//-------
// By id
// Param by id
c.pnames = []string{"id"}
c.pvalues = []string{"1"}
assert.Equal(t, "1", c.P(0))
// By name
// Param by name
assert.Equal(t, "1", c.Param("id"))
// Store
@ -59,19 +60,14 @@ func TestContext(t *testing.T) {
//------
// JSON
testBind(t, c, ApplicationJSON)
testBind(t, c, "application/json")
// TODO: Form
c.request.Header.Set(ContentType, ApplicationForm)
u := new(user)
err := c.Bind(u)
assert.NoError(t, err)
// XML
c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML))
testBind(t, c, ApplicationXML)
// Unsupported
c.request.Header.Set(ContentType, "")
u = new(user)
err = c.Bind(u)
assert.Error(t, err)
testBind(t, c, "")
//--------
// Render
@ -81,7 +77,7 @@ func TestContext(t *testing.T) {
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
}
c.echo.SetRenderer(tpl)
err = c.Render(http.StatusOK, "hello", "Joe")
err := c.Render(http.StatusOK, "hello", "Joe")
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "Hello, Joe!", rec.Body.String())
@ -92,18 +88,37 @@ func TestContext(t *testing.T) {
assert.Error(t, err)
// JSON
req.Header.Set(Accept, ApplicationJSON)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.JSON(http.StatusOK, user{"1", "Joe"})
if assert.NoError(t, err) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, ApplicationJSON, rec.Header().Get(ContentType))
assert.Equal(t, usr, strings.TrimSpace(rec.Body.String()))
assert.Equal(t, ApplicationJSONCharsetUTF8, rec.Header().Get(ContentType))
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
req.Header.Set(Accept, TextPlain)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.String(http.StatusOK, "Hello, World!")
@ -114,16 +129,34 @@ func TestContext(t *testing.T) {
}
// HTML
req.Header.Set(Accept, TextHTML)
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
if assert.NoError(t, err) {
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())
}
// 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
rec = httptest.NewRecorder()
c = NewContext(req, NewResponse(rec), New())
@ -133,7 +166,7 @@ func TestContext(t *testing.T) {
// Redirect
rec = httptest.NewRecorder()
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
rec = httptest.NewRecorder()
@ -145,11 +178,42 @@ func TestContext(t *testing.T) {
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) {
c.request.Header.Set(ContentType, ct)
u := new(user)
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, "Joe", u.Name)
}

164
echo.go
View File

@ -14,8 +14,10 @@ import (
"strings"
"sync"
"encoding/xml"
"github.com/bradfitz/http2"
"github.com/mattn/go-colorable"
"github.com/labstack/gommon/color"
"golang.org/x/net/websocket"
)
@ -28,10 +30,11 @@ type (
notFoundHandler HandlerFunc
defaultHTTPErrorHandler HTTPErrorHandler
httpErrorHandler HTTPErrorHandler
binder BindFunc
binder Binder
renderer Renderer
pool sync.Pool
debug bool
stripTrailingSlash bool
router *Router
}
@ -54,12 +57,20 @@ type (
// HTTPErrorHandler is a centralized HTTP error handler.
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.
//
// Render renders the HTML template with given name and specified data.
// It writes the output to w.
Renderer interface {
Render(w io.Writer, name string, data interface{}) error
}
@ -89,27 +100,41 @@ const (
// Media types
//-------------
ApplicationJSON = "application/json"
ApplicationProtobuf = "application/protobuf"
ApplicationMsgpack = "application/msgpack"
TextPlain = "text/plain"
TextHTML = "text/html"
ApplicationForm = "application/x-www-form-urlencoded"
MultipartForm = "multipart/form-data"
ApplicationJSON = "application/json"
ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8
ApplicationJavaScript = "application/javascript"
ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8
ApplicationXML = "application/xml"
ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8
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
//---------
Accept = "Accept"
AcceptEncoding = "Accept-Encoding"
Authorization = "Authorization"
ContentDisposition = "Content-Disposition"
ContentEncoding = "Content-Encoding"
ContentLength = "Content-Length"
ContentType = "Content-Type"
Authorization = "Authorization"
Location = "Location"
Upgrade = "Upgrade"
Vary = "Vary"
WWWAuthenticate = "WWW-Authenticate"
//-----------
// Protocols
@ -139,9 +164,22 @@ var (
UnsupportedMediaType = errors.New("echo ⇒ unsupported media type")
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) {
e = &Echo{maxParam: new(int)}
e.pool.New = func() interface{} {
@ -153,10 +191,10 @@ func New() (e *Echo) {
// Defaults
//----------
e.HTTP2(false)
e.notFoundHandler = func(c *Context) error {
return NewHTTPError(http.StatusNotFound)
if runtime.GOOS == "windows" {
e.DisableColoredLog()
}
e.HTTP2()
e.defaultHTTPErrorHandler = func(err error, c *Context) {
code := http.StatusInternalServerError
msg := http.StatusText(code)
@ -167,19 +205,13 @@ func New() (e *Echo) {
if e.debug {
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.SetBinder(func(r *http.Request, v interface{}) error {
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
})
e.SetBinder(&binder{})
return
}
@ -188,9 +220,14 @@ func (e *Echo) Router() *Router {
return e.router
}
// DisableColoredLog disables colored log.
func (e *Echo) DisableColoredLog() {
color.Disable()
}
// HTTP2 enables HTTP2 support.
func (e *Echo) HTTP2(on bool) {
e.http2 = on
func (e *Echo) HTTP2() {
e.http2 = true
}
// 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().
func (e *Echo) SetBinder(b BindFunc) {
func (e *Echo) SetBinder(b Binder) {
e.binder = b
}
@ -213,14 +250,14 @@ func (e *Echo) SetRenderer(r Renderer) {
e.renderer = r
}
// SetDebug sets debug mode.
func (e *Echo) SetDebug(on bool) {
e.debug = on
// Debug enables debug mode.
func (e *Echo) Debug() {
e.debug = true
}
// Debug returns debug mode.
func (e *Echo) Debug() bool {
return e.debug
// StripTrailingSlash enables removing trailing slash from the request path.
func (e *Echo) StripTrailingSlash() {
e.stripTrailingSlash = true
}
// Use adds handler to the middleware chain.
@ -275,6 +312,20 @@ func (e *Echo) Trace(path string, h Handler) {
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.
func (e *Echo) WebSocket(path string, h HandlerFunc) {
e.Get(path, func(c *Context) (err error) {
@ -389,7 +440,7 @@ func (e *Echo) URI(h Handler, params ...interface{}) 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 {
return e.URI(h, params...)
}
@ -399,6 +450,7 @@ func (e *Echo) Routes() []Route {
return e.router.routes
}
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := e.pool.Get().(*Context)
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
}
c.reset(r, w, e)
if h == nil {
h = e.notFoundHandler
}
// Chain middleware with handler in the end
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)
}
// Server returns the internal *http.Server
// Server returns the internal *http.Server.
func (e *Echo) Server(addr string) *http.Server {
s := &http.Server{Addr: addr}
s.Handler = e
@ -446,13 +495,13 @@ func (e *Echo) RunTLS(addr, certFile, keyFile string) {
}
// RunServer runs a custom server.
func (e *Echo) RunServer(srv *http.Server) {
e.run(srv)
func (e *Echo) RunServer(s *http.Server) {
e.run(s)
}
// RunTLSServer runs a custom server with TLS configuration.
func (e *Echo) RunTLSServer(srv *http.Server, certFile, keyFile string) {
e.run(srv, certFile, keyFile)
func (e *Echo) RunTLSServer(s *http.Server, certFile, keyFile string) {
e.run(s, certFile, keyFile)
}
func (e *Echo) run(s *http.Server, files ...string) {
@ -489,7 +538,7 @@ func (e *HTTPError) Error() string {
return e.message
}
// wraps middleware
// wrapMiddleware wraps middleware.
func wrapMiddleware(m Middleware) MiddlewareFunc {
switch m := m.(type) {
case MiddlewareFunc:
@ -520,7 +569,7 @@ func wrapMiddleware(m Middleware) MiddlewareFunc {
}
}
// Wraps HandlerFunc middleware
// wrapHandlerFuncMW wraps HandlerFunc middleware.
func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
return func(h HandlerFunc) HandlerFunc {
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 {
return func(h HandlerFunc) HandlerFunc {
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 {
switch h := h.(type) {
case HandlerFunc:
@ -566,6 +615,13 @@ func wrapHandler(h Handler) HandlerFunc {
}
}
func init() {
log.SetOutput(colorable.NewColorableStdout())
func (binder) Bind(r *http.Request, i interface{}) (err error) {
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 (
user struct {
ID string `json:"id"`
Name string `json:"name"`
ID string `json:"id" xml:"id"`
Name string `json:"name" xml:"name"`
}
)
@ -33,8 +33,8 @@ func TestEcho(t *testing.T) {
assert.NotNil(t, e.Router())
// Debug
e.SetDebug(true)
assert.True(t, e.Debug())
e.Debug()
assert.True(t, e.debug)
// DefaultHTTPErrorHandler
e.DefaultHTTPErrorHandler(errors.New("error"), c)
@ -246,6 +246,20 @@ func TestEchoTrace(t *testing.T) {
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) {
e := New()
e.WebSocket("/ws", func(c *Context) error {
@ -368,6 +382,14 @@ func TestEchoNotFound(t *testing.T) {
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) {
m := http.StatusText(http.StatusBadRequest)
he := NewHTTPError(http.StatusBadRequest, m)
@ -381,12 +403,20 @@ func TestEchoServer(t *testing.T) {
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) {
m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:]))
p := reflect.ValueOf(path)
h := reflect.ValueOf(func(c *Context) error {
c.String(http.StatusOK, method)
return nil
return c.String(http.StatusOK, method)
})
i := interface{}(e)
reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h})

View File

@ -17,7 +17,7 @@ func main() {
e := echo.New()
// Debug mode
e.SetDebug(true)
e.Debug()
//------------
// Middleware
@ -37,16 +37,6 @@ func main() {
return false
}))
//-------
// Slash
//-------
e.Use(mw.StripTrailingSlash())
// or
// e.Use(mw.RedirectToSlash())
// 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 (
"encoding/base64"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"net/http"
"github.com/labstack/echo"
)
type (
BasicValidateFunc func(string, string) bool
JWTValidateFunc func(string, jwt.SigningMethod) ([]byte, error)
)
const (
Basic = "Basic"
Bearer = "Bearer"
Basic = "Basic"
)
// BasicAuth returns an HTTP basic authentication middleware.
//
// 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.
func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
@ -31,7 +28,6 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
auth := c.Request().Header.Get(echo.Authorization)
l := len(Basic)
he := echo.NewHTTPError(http.StatusBadRequest)
if len(auth) > l+1 && auth[:l] == Basic {
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:]) {
return nil
}
he.SetCode(http.StatusUnauthorized)
}
}
}
}
return he
}
}
// 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
c.Response().Header().Set(echo.WWWAuthenticate, Basic + " realm=Restricted")
return echo.NewHTTPError(http.StatusUnauthorized)
}
}

View File

@ -3,13 +3,11 @@ package middleware
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"net/http/httptest"
"time"
)
func TestBasicAuth(t *testing.T) {
@ -38,80 +36,22 @@ func TestBasicAuth(t *testing.T) {
req.Header.Set(echo.Authorization, auth)
he := ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusUnauthorized, he.Code())
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// Empty Authorization header
req.Header.Set(echo.Authorization, "")
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
auth = base64.StdEncoding.EncodeToString([]byte(" :secret"))
auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
req.Header.Set(echo.Authorization, auth)
he = ba(c).(*echo.HTTPError)
assert.Equal(t, http.StatusBadRequest, he.Code())
// 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())
assert.Equal(t, http.StatusUnauthorized, he.Code())
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
// WebSocket
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
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) {
e := echo.New()
e.SetDebug(true)
e.Debug()
req, _ := http.NewRequest(echo.GET, "/", nil)
rec := httptest.NewRecorder()
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
e := echo.New()
e.Get("/", func(c *echo.Context) error {
c.String(http.StatusOK, "Six sick bricks tick")
return nil
return c.String(http.StatusOK, "Six sick bricks tick")
})
// Use github.com/facebookgo/grace/gracehttp
gracehttp.Serve(e.Server(":1323"))
}

View File

@ -12,10 +12,8 @@ func main() {
// Setup
e := echo.New()
e.Get("/", func(c *echo.Context) error {
c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
return nil
return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
})
// Use github.com/tylerb/graceful
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() {
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().WriteHeader(http.StatusOK)
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.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 (
Router struct {
trees [21]*node
routes []Route
echo *Echo
connectTree *node
deleteTree *node
getTree *node
headTree *node
optionsTree *node
patchTree *node
postTree *node
putTree *node
traceTree *node
routes []Route
echo *Echo
}
node struct {
typ ntype
@ -28,19 +36,20 @@ const (
mtype
)
func NewRouter(e *Echo) (r *Router) {
r = &Router{
// trees: make(map[string]*node),
routes: []Route{},
echo: e,
func NewRouter(e *Echo) *Router {
return &Router{
connectTree: new(node),
deleteTree: new(node),
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) {
@ -66,7 +75,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
} else if path[i] == '*' {
r.insert(method, path[:i], nil, stype, nil, e)
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
}
}
@ -81,7 +90,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t ntype, pnames []st
*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
for {
@ -200,21 +212,89 @@ func (n *node) findChildWithType(t ntype) *node {
return nil
}
func (r *Router) treeIndex(method string) uint8 {
if method[0] == 'P' {
return method[0]%10 + method[1] - 65
} else {
return method[0] % 10
func (r *Router) findTree(method string) (n *node) {
switch method[0] {
case 'G': // GET
m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
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) {
cn := r.trees[r.treeIndex(method)] // Current node as root
search := path
h = notFoundHandler
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 (
search = path
c *node // Child node
n int // Param counter
n int // Param counter
nt ntype // Next type
nn *node // Next node
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
for {
if search == "" {
// Found
ctx.pnames = cn.pnames
h = cn.handler
e = cn.echo
if cn.handler != nil {
// Found
ctx.pnames = cn.pnames
h = cn.handler
e = cn.echo
}
return
}
@ -287,7 +369,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
}
// Param node
Param:
Param:
c = cn.findChildWithType(ptype)
if c != nil {
// Save next
@ -307,7 +389,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
}
// Match-any node
MatchAny:
MatchAny:
// c = cn.getChild()
c = cn.findChildWithType(mtype)
if c != nil {
@ -326,10 +408,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := r.echo.pool.Get().(*Context)
h, _ := r.Find(req.Method, req.URL.Path, c)
c.reset(req, w, r.echo)
if h == nil {
c.Error(NewHTTPError(http.StatusNotFound))
} else {
h(c)
if err := h(c); err != nil {
r.echo.httpErrorHandler(err, 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(1))
}
h, _ = r.Find(GET, "/users/1", c)
assert.Nil(t, h)
}
func TestRouterMatchAny(t *testing.T) {
@ -384,7 +381,10 @@ func TestRouterMultiRoute(t *testing.T) {
// Route > /user
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) {
@ -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) {
e := New()
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.
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
@ -42,54 +42,45 @@ and message `HTTPError.Message`.
### Debug
`Echo.SetDebug(on bool)`
`Echo.Debug()`
Enables debug mode.
### Disable colored log
`Echo.DisableColoredLog()`
### StripTrailingSlash
StripTrailingSlash enables removing trailing slash from the request path.
`e.StripTrailingSlash()`
## Routing
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)
data structure which makes routing lookup really fast. It leverages
[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve
zero dynamic memory allocation with no GC overhead.
flexible. It's based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree) data
structure which makes route lookup really fast. Router leverages [sync pool](https://golang.org/pkg/sync/#Pool)
to reuse memory and achieve zero dynamic memory allocation with no GC overhead.
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
`Hello!` HTTP response.
```go
echo.Get("/hello", func(c *echo.Context) error {
e.Get("/hello", func(c *echo.Context) error {
return c.String(http.StatusOK, "Hello!")
})
```
Echo's default handler is `func(*echo.Context) error` where `echo.Context`
primarily holds HTTP request and response objects. Echo also has a support for other
types 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)
})
```
Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily
holds HTTP request and response objects. Echo also has a support for other types
of handlers.
### Match-any
Matches zero or more characters in the path. For example, pattern `/users/*` will
match
match:
- `/users/`
- `/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/:id`
- `/users/1/files/*`
Routes can be written in any order.
> Routes can be written in any order.
### Group
@ -150,15 +141,15 @@ e.Use(mw.BasicAuth(func(usr, pwd string) bool {
### 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
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
// Handler
h := func(*echo.Context) error {
h := func(c *echo.Context) error {
return c.String(http.StatusOK, "OK")
}
@ -168,9 +159,9 @@ e.Get("/users/:id", h)
## 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
action for example, logging every request. Echo supports variety of [middleware](/#features).
action, for example, logging every request.
### Logger
@ -195,7 +186,7 @@ BasicAuth middleware provides an HTTP basic authentication.
*Example*
```go
echo.Group("/admin")
e.Group("/admin")
e.Use(mw.BasicAuth(func(usr, pwd string) bool {
if usr == "joe" && pwd == "secret" {
return true
@ -225,63 +216,163 @@ to the centralized [HTTPErrorHandler](#error-handling).
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*
```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
RedirectToSlash middleware redirects requests without trailing slash path to trailing
slash path.
```sh
$ curl http://localhost:1323/users/joe
```
### Query parameter
Query parameter can be retrieved by name using `Context.Query(name string)`.
*Example*
*Options*
```go
RedirectToSlashOptions struct {
Code int
e.Get("/users", func(c *echo.Context) error {
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
e.Use(mw.RedirectToSlash())
`public/views/hello.html`
```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
```go
context.JSON(code int, v interface{}) error
Context.JSON(code int, v interface{}) error
```
Sends a JSON HTTP response with status code.
### String
### XML
```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
```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.
### 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
`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.
## Deployment
*WIP*

View File

@ -1,13 +1,9 @@
# 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
- 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`
- `func(http.ResponseWriter, *http.Request)`
- 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:
- Favicon
- Index file
- Static files
- WebSocket
- API to serve index and favicon.
- Centralized HTTP error handling.
- Customizable request binding function.
- Customizable response rendering function, allowing you to use any HTML template engine.
- Customizable HTTP request binding function.
- Customizable HTTP response rendering function, allowing you to use any HTML template engine.
## 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
```
###[Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello)
### Hello, World!
Create `server.go` with the following content
Create `server.go`
```go
package main
@ -83,24 +90,7 @@ func main() {
}
```
`echo.New()` returns a new instance of Echo.
`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
Start server
```sh
$ 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.
### 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)
## 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 }}">
{% 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/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/echo.css" rel="stylesheet">
{%- for path in extra_css %}
@ -75,4 +75,4 @@
<script src="{{ path }}"></script>
{%- endfor %}
</body>
</html>
</html>

View File

@ -1,11 +1,19 @@
site_name: Echo
theme: journal
theme: flatly
theme_dir: echo
copyright: '&copy; 2015 LabStack'
repo_url: https://github.com/labstack/echo
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.