mirror of
https://github.com/labstack/echo.git
synced 2024-12-24 20:14:31 +02:00
Merge branch 'master' of https://github.com/labstack/echo
This commit is contained in:
commit
1d040969ad
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -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
8
.gitignore
vendored
@ -1,10 +1,10 @@
|
||||
# Website
|
||||
site/
|
||||
.publish/
|
||||
site
|
||||
.publish
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
node_modules
|
||||
|
||||
# IntelliJ
|
||||
.idea/
|
||||
.idea
|
||||
*.iml
|
||||
|
54
README.md
54
README.md
@ -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)
|
||||
|
101
context.go
101
context.go
@ -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
|
||||
}
|
||||
|
118
context_test.go
118
context_test.go
@ -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
164
echo.go
@ -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
|
||||
}
|
||||
|
42
echo_test.go
42
echo_test.go
@ -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})
|
||||
|
@ -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())
|
||||
|
||||
|
@ -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")
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
16
recipes/file-upload/public/index.html
Normal file
16
recipes/file-upload/public/index.html
Normal 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>
|
56
recipes/file-upload/server.go
Normal file
56
recipes/file-upload/server.go
Normal 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")
|
||||
}
|
@ -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"))
|
||||
}
|
@ -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)
|
||||
}
|
76
recipes/jwt-authentication/server.go
Normal file
76
recipes/jwt-authentication/server.go
Normal 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")
|
||||
}
|
24
recipes/jwt-authentication/token/token.go
Normal file
24
recipes/jwt-authentication/token/token.go
Normal 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)
|
||||
}
|
16
recipes/streaming-file-upload/public/index.html
Normal file
16
recipes/streaming-file-upload/public/index.html
Normal 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>
|
79
recipes/streaming-file-upload/server.go
Normal file
79
recipes/streaming-file-upload/server.go
Normal 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")
|
||||
}
|
@ -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 {
|
67
recipes/subdomains/server.go
Normal file
67
recipes/subdomains/server.go
Normal 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)
|
||||
}
|
36
recipes/websocket/public/index.html
Normal file
36
recipes/websocket/public/index.html
Normal 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>
|
35
recipes/websocket/server.go
Normal file
35
recipes/websocket/server.go
Normal 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")
|
||||
}
|
@ -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
150
router.go
@ -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)
|
||||
}
|
||||
|
@ -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
BIN
test/fixture/walle.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 215 KiB |
@ -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*
|
||||
|
@ -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
|
||||
|
96
website/docs/recipes/file-upload.md
Normal file
96
website/docs/recipes/file-upload.md
Normal 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)
|
58
website/docs/recipes/graceful-shutdown.md
Normal file
58
website/docs/recipes/graceful-shutdown.md
Normal 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)
|
156
website/docs/recipes/jwt-authentication.md
Normal file
156
website/docs/recipes/jwt-authentication.md
Normal 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)
|
||||
|
||||
|
||||
|
||||
|
||||
|
117
website/docs/recipes/streaming-file-upload.md
Normal file
117
website/docs/recipes/streaming-file-upload.md
Normal 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)
|
||||
|
75
website/docs/recipes/streaming-response.md
Normal file
75
website/docs/recipes/streaming-response.md
Normal 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)
|
||||
|
75
website/docs/recipes/subdomains.md
Normal file
75
website/docs/recipes/subdomains.md
Normal 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)
|
111
website/docs/recipes/websocket.md
Normal file
111
website/docs/recipes/websocket.md
Normal 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)
|
||||
|
@ -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 %}
|
||||
|
@ -1,11 +1,19 @@
|
||||
site_name: Echo
|
||||
|
||||
theme: journal
|
||||
|
||||
theme: flatly
|
||||
theme_dir: echo
|
||||
|
||||
copyright: '© 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.
|
||||
|
Loading…
Reference in New Issue
Block a user