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
|
# Exclude `website` and `examples` from Github's language statistics
|
||||||
# https://github.com/github/linguist#using-gitattributes
|
# https://github.com/github/linguist#using-gitattributes
|
||||||
examples/* linguist-documentation
|
examples/* linguist-documentation
|
||||||
|
recipes/* linguist-documentation
|
||||||
website/* linguist-documentation
|
website/* linguist-documentation
|
||||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,10 +1,10 @@
|
|||||||
# Website
|
# Website
|
||||||
site/
|
site
|
||||||
.publish/
|
.publish
|
||||||
|
|
||||||
# Node.js
|
# Node.js
|
||||||
node_modules/
|
node_modules
|
||||||
|
|
||||||
# IntelliJ
|
# IntelliJ
|
||||||
.idea/
|
.idea
|
||||||
*.iml
|
*.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](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
|
||||||
Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
|
|
||||||
|
A fast and unfancy micro web framework for Go.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -20,16 +21,27 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
|
|||||||
- `http.HandlerFunc`
|
- `http.HandlerFunc`
|
||||||
- `func(http.ResponseWriter, *http.Request)`
|
- `func(http.ResponseWriter, *http.Request)`
|
||||||
- Sub-router/Groups
|
- Sub-router/Groups
|
||||||
- Handy encoding/decoding functions.
|
- Handy functions to send variety of HTTP response:
|
||||||
|
- HTML
|
||||||
|
- HTML via templates
|
||||||
|
- String
|
||||||
|
- JSON
|
||||||
|
- JSONP
|
||||||
|
- XML
|
||||||
|
- File
|
||||||
|
- NoContent
|
||||||
|
- Redirect
|
||||||
|
- Error
|
||||||
- Build-in support for:
|
- Build-in support for:
|
||||||
|
- Favicon
|
||||||
|
- Index file
|
||||||
- Static files
|
- Static files
|
||||||
- WebSocket
|
- WebSocket
|
||||||
- API to serve index and favicon.
|
|
||||||
- Centralized HTTP error handling.
|
- Centralized HTTP error handling.
|
||||||
- Customizable request binding function.
|
- Customizable HTTP request binding function.
|
||||||
- Customizable response rendering function, allowing you to use any HTML template engine.
|
- Customizable HTTP response rendering function, allowing you to use any HTML template engine.
|
||||||
|
|
||||||
## Benchmark
|
## Performance
|
||||||
|
|
||||||
Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015.
|
Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015.
|
||||||
|
|
||||||
@ -75,16 +87,28 @@ BenchmarkZeus_GithubAll 2000 748827 ns/op 30068
|
|||||||
$ go get github.com/labstack/echo
|
$ go get github.com/labstack/echo
|
||||||
```
|
```
|
||||||
|
|
||||||
##[Examples](https://github.com/labstack/echo/tree/master/examples)
|
## [Recipes](https://github.com/labstack/echo/tree/master/recipes)
|
||||||
|
|
||||||
- [Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello)
|
- [File Upload](http://echo.labstack.com/recipes/file-upload)
|
||||||
- [CRUD](https://github.com/labstack/echo/tree/master/examples/crud)
|
- [Streaming File Upload](http://echo.labstack.com/recipes/streaming-file-upload)
|
||||||
- [Website](https://github.com/labstack/echo/tree/master/examples/website)
|
- [Streaming Response](http://echo.labstack.com/recipes/streaming-response)
|
||||||
- [Middleware](https://github.com/labstack/echo/tree/master/examples/middleware)
|
- [WebSocket](http://echo.labstack.com/recipes/websocket)
|
||||||
- [Stream](https://github.com/labstack/echo/tree/master/examples/stream)
|
- [Subdomains](http://echo.labstack.com/recipes/subdomains)
|
||||||
|
- [JWT Authentication](http://echo.labstack.com/recipes/jwt-authentication)
|
||||||
|
- [Graceful Shutdown](http://echo.labstack.com/recipes/graceful-shutdown)
|
||||||
|
|
||||||
##[Guide](http://echo.labstack.com/guide)
|
##[Guide](http://echo.labstack.com/guide)
|
||||||
|
|
||||||
|
## Echo System
|
||||||
|
|
||||||
|
Community created packages for Echo
|
||||||
|
|
||||||
|
- [echo-logrus](https://github.com/deoxxa/echo-logrus)
|
||||||
|
- [go_middleware](https://github.com/rightscale/go_middleware)
|
||||||
|
- [permissions2](https://github.com/xyproto/permissions2)
|
||||||
|
- [permissionbolt](https://github.com/xyproto/permissionbolt)
|
||||||
|
- [echo-middleware](https://github.com/syntaqx/echo-middleware)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
|
||||||
**Use issues for everything**
|
**Use issues for everything**
|
||||||
@ -98,7 +122,3 @@ $ go get github.com/labstack/echo
|
|||||||
- [Vishal Rana](https://github.com/vishr) - Author
|
- [Vishal Rana](https://github.com/vishr) - Author
|
||||||
- [Nitin Rana](https://github.com/nr17) - Consultant
|
- [Nitin Rana](https://github.com/nr17) - Consultant
|
||||||
- [Contributors](https://github.com/labstack/echo/graphs/contributors)
|
- [Contributors](https://github.com/labstack/echo/graphs/contributors)
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
[MIT](https://github.com/labstack/echo/blob/master/LICENSE)
|
|
||||||
|
101
context.go
101
context.go
@ -2,7 +2,13 @@ package echo
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
@ -16,6 +22,7 @@ type (
|
|||||||
socket *websocket.Conn
|
socket *websocket.Conn
|
||||||
pnames []string
|
pnames []string
|
||||||
pvalues []string
|
pvalues []string
|
||||||
|
query url.Values
|
||||||
store store
|
store store
|
||||||
echo *Echo
|
echo *Echo
|
||||||
}
|
}
|
||||||
@ -69,6 +76,19 @@ func (c *Context) Param(name string) (value string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Query returns query parameter by name.
|
||||||
|
func (c *Context) Query(name string) string {
|
||||||
|
if c.query == nil {
|
||||||
|
c.query = c.request.URL.Query()
|
||||||
|
}
|
||||||
|
return c.query.Get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form returns form parameter by name.
|
||||||
|
func (c *Context) Form(name string) string {
|
||||||
|
return c.request.FormValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
// Get retrieves data from the context.
|
// Get retrieves data from the context.
|
||||||
func (c *Context) Get(key string) interface{} {
|
func (c *Context) Get(key string) interface{} {
|
||||||
return c.store[key]
|
return c.store[key]
|
||||||
@ -76,47 +96,82 @@ func (c *Context) Get(key string) interface{} {
|
|||||||
|
|
||||||
// Set saves data in the context.
|
// Set saves data in the context.
|
||||||
func (c *Context) Set(key string, val interface{}) {
|
func (c *Context) Set(key string, val interface{}) {
|
||||||
|
if c.store == nil {
|
||||||
|
c.store = make(store)
|
||||||
|
}
|
||||||
c.store[key] = val
|
c.store[key] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bind binds the request body into specified type v. Default binder does it
|
// Bind binds the request body into specified type `i`. The default binder does
|
||||||
// based on Content-Type header.
|
// it based on Content-Type header.
|
||||||
func (c *Context) Bind(i interface{}) error {
|
func (c *Context) Bind(i interface{}) error {
|
||||||
return c.echo.binder(c.request, i)
|
return c.echo.binder.Bind(c.request, i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render invokes the registered HTML template renderer and sends a text/html
|
// Render renders a template with data and sends a text/html response with status
|
||||||
// response with status code.
|
// code. Templates can be registered using `Echo.SetRenderer()`.
|
||||||
func (c *Context) Render(code int, name string, data interface{}) error {
|
func (c *Context) Render(code int, name string, data interface{}) error {
|
||||||
if c.echo.renderer == nil {
|
if c.echo.renderer == nil {
|
||||||
return RendererNotRegistered
|
return RendererNotRegistered
|
||||||
}
|
}
|
||||||
c.response.Header().Set(ContentType, TextHTML)
|
c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
|
||||||
c.response.WriteHeader(code)
|
c.response.WriteHeader(code)
|
||||||
return c.echo.renderer.Render(c.response, name, data)
|
return c.echo.renderer.Render(c.response, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON sends an application/json response with status code.
|
// HTML formats according to a format specifier and sends HTML response with
|
||||||
|
// status code.
|
||||||
|
func (c *Context) HTML(code int, format string, a ...interface{}) (err error) {
|
||||||
|
c.response.Header().Set(ContentType, TextHTMLCharsetUTF8)
|
||||||
|
c.response.WriteHeader(code)
|
||||||
|
_, err = fmt.Fprintf(c.response, format, a...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// String formats according to a format specifier and sends text response with status
|
||||||
|
// code.
|
||||||
|
func (c *Context) String(code int, format string, a ...interface{}) (err error) {
|
||||||
|
c.response.Header().Set(ContentType, TextPlain)
|
||||||
|
c.response.WriteHeader(code)
|
||||||
|
_, err = fmt.Fprintf(c.response, format, a...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON sends a JSON response with status code.
|
||||||
func (c *Context) JSON(code int, i interface{}) error {
|
func (c *Context) JSON(code int, i interface{}) error {
|
||||||
c.response.Header().Set(ContentType, ApplicationJSON)
|
c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8)
|
||||||
c.response.WriteHeader(code)
|
c.response.WriteHeader(code)
|
||||||
return json.NewEncoder(c.response).Encode(i)
|
return json.NewEncoder(c.response).Encode(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
// String sends a text/plain response with status code.
|
// JSONP sends a JSONP response with status code. It uses `callback` to construct
|
||||||
func (c *Context) String(code int, s string) error {
|
// the JSONP payload.
|
||||||
c.response.Header().Set(ContentType, TextPlain)
|
func (c *Context) JSONP(code int, callback string, i interface{}) (err error) {
|
||||||
|
c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8)
|
||||||
c.response.WriteHeader(code)
|
c.response.WriteHeader(code)
|
||||||
_, err := c.response.Write([]byte(s))
|
c.response.Write([]byte(callback + "("))
|
||||||
return err
|
if err = json.NewEncoder(c.response).Encode(i); err == nil {
|
||||||
|
c.response.Write([]byte(");"))
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML sends a text/html response with status code.
|
// XML sends an XML response with status code.
|
||||||
func (c *Context) HTML(code int, html string) error {
|
func (c *Context) XML(code int, i interface{}) error {
|
||||||
c.response.Header().Set(ContentType, TextHTML)
|
c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8)
|
||||||
c.response.WriteHeader(code)
|
c.response.WriteHeader(code)
|
||||||
_, err := c.response.Write([]byte(html))
|
c.response.Write([]byte(xml.Header))
|
||||||
return err
|
return xml.NewEncoder(c.response).Encode(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// File sends a response with the content of the file. If attachment is true, the
|
||||||
|
// client is prompted to save the file.
|
||||||
|
func (c *Context) File(name string, attachment bool) error {
|
||||||
|
dir, file := path.Split(name)
|
||||||
|
if attachment {
|
||||||
|
c.response.Header().Set(ContentDisposition, "attachment; filename="+file)
|
||||||
|
}
|
||||||
|
return serveFile(dir, file, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NoContent sends a response with no body and a status code.
|
// NoContent sends a response with no body and a status code.
|
||||||
@ -126,11 +181,15 @@ func (c *Context) NoContent(code int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect redirects the request using http.Redirect with status code.
|
// Redirect redirects the request using http.Redirect with status code.
|
||||||
func (c *Context) Redirect(code int, url string) {
|
func (c *Context) Redirect(code int, url string) error {
|
||||||
|
if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect {
|
||||||
|
return InvalidRedirectCode
|
||||||
|
}
|
||||||
http.Redirect(c.response, c.request, url, code)
|
http.Redirect(c.response, c.request, url, code)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error invokes the registered HTTP error handler. Usually used by middleware.
|
// Error invokes the registered HTTP error handler. Generally used by middleware.
|
||||||
func (c *Context) Error(err error) {
|
func (c *Context) Error(err error) {
|
||||||
c.echo.httpErrorHandler(err, c)
|
c.echo.httpErrorHandler(err, c)
|
||||||
}
|
}
|
||||||
@ -138,5 +197,7 @@ func (c *Context) Error(err error) {
|
|||||||
func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) {
|
func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) {
|
||||||
c.request = r
|
c.request = r
|
||||||
c.response.reset(w)
|
c.response.reset(w)
|
||||||
|
c.query = nil
|
||||||
|
c.store = nil
|
||||||
c.echo = e
|
c.echo = e
|
||||||
}
|
}
|
||||||
|
118
context_test.go
118
context_test.go
@ -10,6 +10,9 @@ import (
|
|||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"encoding/xml"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -24,8 +27,10 @@ func (t *Template) Render(w io.Writer, name string, data interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestContext(t *testing.T) {
|
func TestContext(t *testing.T) {
|
||||||
usr := `{"id":"1","name":"Joe"}`
|
userJSON := `{"id":"1","name":"Joe"}`
|
||||||
req, _ := http.NewRequest(POST, "/", strings.NewReader(usr))
|
userXML := `<user><id>1</id><name>Joe</name></user>`
|
||||||
|
|
||||||
|
req, _ := http.NewRequest(POST, "/", strings.NewReader(userJSON))
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
c := NewContext(req, NewResponse(rec), New())
|
c := NewContext(req, NewResponse(rec), New())
|
||||||
|
|
||||||
@ -38,16 +43,12 @@ func TestContext(t *testing.T) {
|
|||||||
// Socket
|
// Socket
|
||||||
assert.Nil(t, c.Socket())
|
assert.Nil(t, c.Socket())
|
||||||
|
|
||||||
//-------
|
// Param by id
|
||||||
// Param
|
|
||||||
//-------
|
|
||||||
|
|
||||||
// By id
|
|
||||||
c.pnames = []string{"id"}
|
c.pnames = []string{"id"}
|
||||||
c.pvalues = []string{"1"}
|
c.pvalues = []string{"1"}
|
||||||
assert.Equal(t, "1", c.P(0))
|
assert.Equal(t, "1", c.P(0))
|
||||||
|
|
||||||
// By name
|
// Param by name
|
||||||
assert.Equal(t, "1", c.Param("id"))
|
assert.Equal(t, "1", c.Param("id"))
|
||||||
|
|
||||||
// Store
|
// Store
|
||||||
@ -59,19 +60,14 @@ func TestContext(t *testing.T) {
|
|||||||
//------
|
//------
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
testBind(t, c, ApplicationJSON)
|
testBind(t, c, "application/json")
|
||||||
|
|
||||||
// TODO: Form
|
// XML
|
||||||
c.request.Header.Set(ContentType, ApplicationForm)
|
c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML))
|
||||||
u := new(user)
|
testBind(t, c, ApplicationXML)
|
||||||
err := c.Bind(u)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Unsupported
|
// Unsupported
|
||||||
c.request.Header.Set(ContentType, "")
|
testBind(t, c, "")
|
||||||
u = new(user)
|
|
||||||
err = c.Bind(u)
|
|
||||||
assert.Error(t, err)
|
|
||||||
|
|
||||||
//--------
|
//--------
|
||||||
// Render
|
// Render
|
||||||
@ -81,7 +77,7 @@ func TestContext(t *testing.T) {
|
|||||||
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
|
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
|
||||||
}
|
}
|
||||||
c.echo.SetRenderer(tpl)
|
c.echo.SetRenderer(tpl)
|
||||||
err = c.Render(http.StatusOK, "hello", "Joe")
|
err := c.Render(http.StatusOK, "hello", "Joe")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Equal(t, "Hello, Joe!", rec.Body.String())
|
assert.Equal(t, "Hello, Joe!", rec.Body.String())
|
||||||
@ -92,18 +88,37 @@ func TestContext(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
req.Header.Set(Accept, ApplicationJSON)
|
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
c = NewContext(req, NewResponse(rec), New())
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
err = c.JSON(http.StatusOK, user{"1", "Joe"})
|
err = c.JSON(http.StatusOK, user{"1", "Joe"})
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Equal(t, ApplicationJSON, rec.Header().Get(ContentType))
|
assert.Equal(t, ApplicationJSONCharsetUTF8, rec.Header().Get(ContentType))
|
||||||
assert.Equal(t, usr, strings.TrimSpace(rec.Body.String()))
|
assert.Equal(t, userJSON+"\n", rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONP
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
|
callback := "callback"
|
||||||
|
err = c.JSONP(http.StatusOK, callback, user{"1", "Joe"})
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, ApplicationJavaScriptCharsetUTF8, rec.Header().Get(ContentType))
|
||||||
|
assert.Equal(t, callback+"("+userJSON+"\n);", rec.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// XML
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
|
err = c.XML(http.StatusOK, user{"1", "Joe"})
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, ApplicationXMLCharsetUTF8, rec.Header().Get(ContentType))
|
||||||
|
assert.Equal(t, xml.Header, xml.Header, rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// String
|
// String
|
||||||
req.Header.Set(Accept, TextPlain)
|
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
c = NewContext(req, NewResponse(rec), New())
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
err = c.String(http.StatusOK, "Hello, World!")
|
err = c.String(http.StatusOK, "Hello, World!")
|
||||||
@ -114,16 +129,34 @@ func TestContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HTML
|
// HTML
|
||||||
req.Header.Set(Accept, TextHTML)
|
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
c = NewContext(req, NewResponse(rec), New())
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
|
err = c.HTML(http.StatusOK, "Hello, <strong>World!</strong>")
|
||||||
if assert.NoError(t, err) {
|
if assert.NoError(t, err) {
|
||||||
assert.Equal(t, http.StatusOK, rec.Code)
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
assert.Equal(t, TextHTML, rec.Header().Get(ContentType))
|
assert.Equal(t, TextHTMLCharsetUTF8, rec.Header().Get(ContentType))
|
||||||
assert.Equal(t, "Hello, <strong>World!</strong>", rec.Body.String())
|
assert.Equal(t, "Hello, <strong>World!</strong>", rec.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
|
err = c.File("test/fixture/walle.png", false)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, 219885, rec.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
// File as attachment
|
||||||
|
rec = httptest.NewRecorder()
|
||||||
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
|
err = c.File("test/fixture/walle.png", true)
|
||||||
|
if assert.NoError(t, err) {
|
||||||
|
assert.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
assert.Equal(t, rec.Header().Get(ContentDisposition), "attachment; filename=walle.png")
|
||||||
|
assert.Equal(t, 219885, rec.Body.Len())
|
||||||
|
}
|
||||||
|
|
||||||
// NoContent
|
// NoContent
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
c = NewContext(req, NewResponse(rec), New())
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
@ -133,7 +166,7 @@ func TestContext(t *testing.T) {
|
|||||||
// Redirect
|
// Redirect
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
c = NewContext(req, NewResponse(rec), New())
|
c = NewContext(req, NewResponse(rec), New())
|
||||||
c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo")
|
assert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo"))
|
||||||
|
|
||||||
// Error
|
// Error
|
||||||
rec = httptest.NewRecorder()
|
rec = httptest.NewRecorder()
|
||||||
@ -145,11 +178,42 @@ func TestContext(t *testing.T) {
|
|||||||
c.reset(req, NewResponse(httptest.NewRecorder()), New())
|
c.reset(req, NewResponse(httptest.NewRecorder()), New())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextQuery(t *testing.T) {
|
||||||
|
q := make(url.Values)
|
||||||
|
q.Set("name", "joe")
|
||||||
|
q.Set("email", "joe@labstack.com")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(GET, "/", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
c := NewContext(req, nil, New())
|
||||||
|
assert.Equal(t, "joe", c.Query("name"))
|
||||||
|
assert.Equal(t, "joe@labstack.com", c.Query("email"))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContextForm(t *testing.T) {
|
||||||
|
f := make(url.Values)
|
||||||
|
f.Set("name", "joe")
|
||||||
|
f.Set("email", "joe@labstack.com")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(POST, "/", strings.NewReader(f.Encode()))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req.Header.Add(ContentType, ApplicationForm)
|
||||||
|
|
||||||
|
c := NewContext(req, nil, New())
|
||||||
|
assert.Equal(t, "joe", c.Form("name"))
|
||||||
|
assert.Equal(t, "joe@labstack.com", c.Form("email"))
|
||||||
|
}
|
||||||
|
|
||||||
func testBind(t *testing.T, c *Context, ct string) {
|
func testBind(t *testing.T, c *Context, ct string) {
|
||||||
c.request.Header.Set(ContentType, ct)
|
c.request.Header.Set(ContentType, ct)
|
||||||
u := new(user)
|
u := new(user)
|
||||||
err := c.Bind(u)
|
err := c.Bind(u)
|
||||||
if assert.NoError(t, err) {
|
if ct == "" {
|
||||||
|
assert.Error(t, UnsupportedMediaType)
|
||||||
|
} else if assert.NoError(t, err) {
|
||||||
assert.Equal(t, "1", u.ID)
|
assert.Equal(t, "1", u.ID)
|
||||||
assert.Equal(t, "Joe", u.Name)
|
assert.Equal(t, "Joe", u.Name)
|
||||||
}
|
}
|
||||||
|
164
echo.go
164
echo.go
@ -14,8 +14,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"encoding/xml"
|
||||||
|
|
||||||
"github.com/bradfitz/http2"
|
"github.com/bradfitz/http2"
|
||||||
"github.com/mattn/go-colorable"
|
"github.com/labstack/gommon/color"
|
||||||
"golang.org/x/net/websocket"
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,10 +30,11 @@ type (
|
|||||||
notFoundHandler HandlerFunc
|
notFoundHandler HandlerFunc
|
||||||
defaultHTTPErrorHandler HTTPErrorHandler
|
defaultHTTPErrorHandler HTTPErrorHandler
|
||||||
httpErrorHandler HTTPErrorHandler
|
httpErrorHandler HTTPErrorHandler
|
||||||
binder BindFunc
|
binder Binder
|
||||||
renderer Renderer
|
renderer Renderer
|
||||||
pool sync.Pool
|
pool sync.Pool
|
||||||
debug bool
|
debug bool
|
||||||
|
stripTrailingSlash bool
|
||||||
router *Router
|
router *Router
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,12 +57,20 @@ type (
|
|||||||
// HTTPErrorHandler is a centralized HTTP error handler.
|
// HTTPErrorHandler is a centralized HTTP error handler.
|
||||||
HTTPErrorHandler func(error, *Context)
|
HTTPErrorHandler func(error, *Context)
|
||||||
|
|
||||||
BindFunc func(*http.Request, interface{}) error
|
// Binder is the interface that wraps the Bind method.
|
||||||
|
Binder interface {
|
||||||
|
Bind(*http.Request, interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
binder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validator is the interface that wraps the Validate method.
|
||||||
|
Validator interface {
|
||||||
|
Validate() error
|
||||||
|
}
|
||||||
|
|
||||||
// Renderer is the interface that wraps the Render method.
|
// Renderer is the interface that wraps the Render method.
|
||||||
//
|
|
||||||
// Render renders the HTML template with given name and specified data.
|
|
||||||
// It writes the output to w.
|
|
||||||
Renderer interface {
|
Renderer interface {
|
||||||
Render(w io.Writer, name string, data interface{}) error
|
Render(w io.Writer, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
@ -89,27 +100,41 @@ const (
|
|||||||
// Media types
|
// Media types
|
||||||
//-------------
|
//-------------
|
||||||
|
|
||||||
ApplicationJSON = "application/json"
|
ApplicationJSON = "application/json"
|
||||||
ApplicationProtobuf = "application/protobuf"
|
ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8
|
||||||
ApplicationMsgpack = "application/msgpack"
|
ApplicationJavaScript = "application/javascript"
|
||||||
TextPlain = "text/plain"
|
ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8
|
||||||
TextHTML = "text/html"
|
ApplicationXML = "application/xml"
|
||||||
ApplicationForm = "application/x-www-form-urlencoded"
|
ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8
|
||||||
MultipartForm = "multipart/form-data"
|
ApplicationForm = "application/x-www-form-urlencoded"
|
||||||
|
ApplicationProtobuf = "application/protobuf"
|
||||||
|
ApplicationMsgpack = "application/msgpack"
|
||||||
|
TextHTML = "text/html"
|
||||||
|
TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8
|
||||||
|
TextPlain = "text/plain"
|
||||||
|
TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8
|
||||||
|
MultipartForm = "multipart/form-data"
|
||||||
|
|
||||||
|
//---------
|
||||||
|
// Charset
|
||||||
|
//---------
|
||||||
|
|
||||||
|
CharsetUTF8 = "charset=utf-8"
|
||||||
|
|
||||||
//---------
|
//---------
|
||||||
// Headers
|
// Headers
|
||||||
//---------
|
//---------
|
||||||
|
|
||||||
Accept = "Accept"
|
|
||||||
AcceptEncoding = "Accept-Encoding"
|
AcceptEncoding = "Accept-Encoding"
|
||||||
|
Authorization = "Authorization"
|
||||||
ContentDisposition = "Content-Disposition"
|
ContentDisposition = "Content-Disposition"
|
||||||
ContentEncoding = "Content-Encoding"
|
ContentEncoding = "Content-Encoding"
|
||||||
ContentLength = "Content-Length"
|
ContentLength = "Content-Length"
|
||||||
ContentType = "Content-Type"
|
ContentType = "Content-Type"
|
||||||
Authorization = "Authorization"
|
Location = "Location"
|
||||||
Upgrade = "Upgrade"
|
Upgrade = "Upgrade"
|
||||||
Vary = "Vary"
|
Vary = "Vary"
|
||||||
|
WWWAuthenticate = "WWW-Authenticate"
|
||||||
|
|
||||||
//-----------
|
//-----------
|
||||||
// Protocols
|
// Protocols
|
||||||
@ -139,9 +164,22 @@ var (
|
|||||||
|
|
||||||
UnsupportedMediaType = errors.New("echo ⇒ unsupported media type")
|
UnsupportedMediaType = errors.New("echo ⇒ unsupported media type")
|
||||||
RendererNotRegistered = errors.New("echo ⇒ renderer not registered")
|
RendererNotRegistered = errors.New("echo ⇒ renderer not registered")
|
||||||
|
InvalidRedirectCode = errors.New("echo ⇒ invalid redirect status code")
|
||||||
|
|
||||||
|
//----------------
|
||||||
|
// Error handlers
|
||||||
|
//----------------
|
||||||
|
|
||||||
|
notFoundHandler = func(c *Context) error {
|
||||||
|
return NewHTTPError(http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
badRequestHandler = func(c *Context) error {
|
||||||
|
return NewHTTPError(http.StatusBadRequest)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates an Echo instance.
|
// New creates an instance of Echo.
|
||||||
func New() (e *Echo) {
|
func New() (e *Echo) {
|
||||||
e = &Echo{maxParam: new(int)}
|
e = &Echo{maxParam: new(int)}
|
||||||
e.pool.New = func() interface{} {
|
e.pool.New = func() interface{} {
|
||||||
@ -153,10 +191,10 @@ func New() (e *Echo) {
|
|||||||
// Defaults
|
// Defaults
|
||||||
//----------
|
//----------
|
||||||
|
|
||||||
e.HTTP2(false)
|
if runtime.GOOS == "windows" {
|
||||||
e.notFoundHandler = func(c *Context) error {
|
e.DisableColoredLog()
|
||||||
return NewHTTPError(http.StatusNotFound)
|
|
||||||
}
|
}
|
||||||
|
e.HTTP2()
|
||||||
e.defaultHTTPErrorHandler = func(err error, c *Context) {
|
e.defaultHTTPErrorHandler = func(err error, c *Context) {
|
||||||
code := http.StatusInternalServerError
|
code := http.StatusInternalServerError
|
||||||
msg := http.StatusText(code)
|
msg := http.StatusText(code)
|
||||||
@ -167,19 +205,13 @@ func New() (e *Echo) {
|
|||||||
if e.debug {
|
if e.debug {
|
||||||
msg = err.Error()
|
msg = err.Error()
|
||||||
}
|
}
|
||||||
http.Error(c.response, msg, code)
|
if !c.response.committed {
|
||||||
|
http.Error(c.response, msg, code)
|
||||||
|
}
|
||||||
|
log.Println(err)
|
||||||
}
|
}
|
||||||
e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler)
|
e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler)
|
||||||
e.SetBinder(func(r *http.Request, v interface{}) error {
|
e.SetBinder(&binder{})
|
||||||
ct := r.Header.Get(ContentType)
|
|
||||||
err := UnsupportedMediaType
|
|
||||||
if strings.HasPrefix(ct, ApplicationJSON) {
|
|
||||||
err = json.NewDecoder(r.Body).Decode(v)
|
|
||||||
} else if strings.HasPrefix(ct, ApplicationForm) {
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,9 +220,14 @@ func (e *Echo) Router() *Router {
|
|||||||
return e.router
|
return e.router
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableColoredLog disables colored log.
|
||||||
|
func (e *Echo) DisableColoredLog() {
|
||||||
|
color.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
// HTTP2 enables HTTP2 support.
|
// HTTP2 enables HTTP2 support.
|
||||||
func (e *Echo) HTTP2(on bool) {
|
func (e *Echo) HTTP2() {
|
||||||
e.http2 = on
|
e.http2 = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultHTTPErrorHandler invokes the default HTTP error handler.
|
// DefaultHTTPErrorHandler invokes the default HTTP error handler.
|
||||||
@ -204,7 +241,7 @@ func (e *Echo) SetHTTPErrorHandler(h HTTPErrorHandler) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetBinder registers a custom binder. It's invoked by Context.Bind().
|
// SetBinder registers a custom binder. It's invoked by Context.Bind().
|
||||||
func (e *Echo) SetBinder(b BindFunc) {
|
func (e *Echo) SetBinder(b Binder) {
|
||||||
e.binder = b
|
e.binder = b
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,14 +250,14 @@ func (e *Echo) SetRenderer(r Renderer) {
|
|||||||
e.renderer = r
|
e.renderer = r
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDebug sets debug mode.
|
// Debug enables debug mode.
|
||||||
func (e *Echo) SetDebug(on bool) {
|
func (e *Echo) Debug() {
|
||||||
e.debug = on
|
e.debug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug returns debug mode.
|
// StripTrailingSlash enables removing trailing slash from the request path.
|
||||||
func (e *Echo) Debug() bool {
|
func (e *Echo) StripTrailingSlash() {
|
||||||
return e.debug
|
e.stripTrailingSlash = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use adds handler to the middleware chain.
|
// Use adds handler to the middleware chain.
|
||||||
@ -275,6 +312,20 @@ func (e *Echo) Trace(path string, h Handler) {
|
|||||||
e.add(TRACE, path, h)
|
e.add(TRACE, path, h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any adds a route > handler to the router for all HTTP methods.
|
||||||
|
func (e *Echo) Any(path string, h Handler) {
|
||||||
|
for _, m := range methods {
|
||||||
|
e.add(m, path, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match adds a route > handler to the router for multiple HTTP methods provided.
|
||||||
|
func (e *Echo) Match(methods []string, path string, h Handler) {
|
||||||
|
for _, m := range methods {
|
||||||
|
e.add(m, path, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocket adds a WebSocket route > handler to the router.
|
// WebSocket adds a WebSocket route > handler to the router.
|
||||||
func (e *Echo) WebSocket(path string, h HandlerFunc) {
|
func (e *Echo) WebSocket(path string, h HandlerFunc) {
|
||||||
e.Get(path, func(c *Context) (err error) {
|
e.Get(path, func(c *Context) (err error) {
|
||||||
@ -389,7 +440,7 @@ func (e *Echo) URI(h Handler, params ...interface{}) string {
|
|||||||
return uri.String()
|
return uri.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL is an alias for URI.
|
// URL is an alias for `URI` function.
|
||||||
func (e *Echo) URL(h Handler, params ...interface{}) string {
|
func (e *Echo) URL(h Handler, params ...interface{}) string {
|
||||||
return e.URI(h, params...)
|
return e.URI(h, params...)
|
||||||
}
|
}
|
||||||
@ -399,6 +450,7 @@ func (e *Echo) Routes() []Route {
|
|||||||
return e.router.routes
|
return e.router.routes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
|
||||||
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
c := e.pool.Get().(*Context)
|
c := e.pool.Get().(*Context)
|
||||||
h, echo := e.router.Find(r.Method, r.URL.Path, c)
|
h, echo := e.router.Find(r.Method, r.URL.Path, c)
|
||||||
@ -406,9 +458,6 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
e = echo
|
e = echo
|
||||||
}
|
}
|
||||||
c.reset(r, w, e)
|
c.reset(r, w, e)
|
||||||
if h == nil {
|
|
||||||
h = e.notFoundHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chain middleware with handler in the end
|
// Chain middleware with handler in the end
|
||||||
for i := len(e.middleware) - 1; i >= 0; i-- {
|
for i := len(e.middleware) - 1; i >= 0; i-- {
|
||||||
@ -423,7 +472,7 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
e.pool.Put(c)
|
e.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server returns the internal *http.Server
|
// Server returns the internal *http.Server.
|
||||||
func (e *Echo) Server(addr string) *http.Server {
|
func (e *Echo) Server(addr string) *http.Server {
|
||||||
s := &http.Server{Addr: addr}
|
s := &http.Server{Addr: addr}
|
||||||
s.Handler = e
|
s.Handler = e
|
||||||
@ -446,13 +495,13 @@ func (e *Echo) RunTLS(addr, certFile, keyFile string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RunServer runs a custom server.
|
// RunServer runs a custom server.
|
||||||
func (e *Echo) RunServer(srv *http.Server) {
|
func (e *Echo) RunServer(s *http.Server) {
|
||||||
e.run(srv)
|
e.run(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunTLSServer runs a custom server with TLS configuration.
|
// RunTLSServer runs a custom server with TLS configuration.
|
||||||
func (e *Echo) RunTLSServer(srv *http.Server, certFile, keyFile string) {
|
func (e *Echo) RunTLSServer(s *http.Server, certFile, keyFile string) {
|
||||||
e.run(srv, certFile, keyFile)
|
e.run(s, certFile, keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Echo) run(s *http.Server, files ...string) {
|
func (e *Echo) run(s *http.Server, files ...string) {
|
||||||
@ -489,7 +538,7 @@ func (e *HTTPError) Error() string {
|
|||||||
return e.message
|
return e.message
|
||||||
}
|
}
|
||||||
|
|
||||||
// wraps middleware
|
// wrapMiddleware wraps middleware.
|
||||||
func wrapMiddleware(m Middleware) MiddlewareFunc {
|
func wrapMiddleware(m Middleware) MiddlewareFunc {
|
||||||
switch m := m.(type) {
|
switch m := m.(type) {
|
||||||
case MiddlewareFunc:
|
case MiddlewareFunc:
|
||||||
@ -520,7 +569,7 @@ func wrapMiddleware(m Middleware) MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wraps HandlerFunc middleware
|
// wrapHandlerFuncMW wraps HandlerFunc middleware.
|
||||||
func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
|
func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
|
||||||
return func(h HandlerFunc) HandlerFunc {
|
return func(h HandlerFunc) HandlerFunc {
|
||||||
return func(c *Context) error {
|
return func(c *Context) error {
|
||||||
@ -532,7 +581,7 @@ func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wraps http.HandlerFunc middleware
|
// wrapHTTPHandlerFuncMW wraps http.HandlerFunc middleware.
|
||||||
func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
|
func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
|
||||||
return func(h HandlerFunc) HandlerFunc {
|
return func(h HandlerFunc) HandlerFunc {
|
||||||
return func(c *Context) error {
|
return func(c *Context) error {
|
||||||
@ -544,7 +593,7 @@ func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wraps handler
|
// wrapHandler wraps handler.
|
||||||
func wrapHandler(h Handler) HandlerFunc {
|
func wrapHandler(h Handler) HandlerFunc {
|
||||||
switch h := h.(type) {
|
switch h := h.(type) {
|
||||||
case HandlerFunc:
|
case HandlerFunc:
|
||||||
@ -566,6 +615,13 @@ func wrapHandler(h Handler) HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func (binder) Bind(r *http.Request, i interface{}) (err error) {
|
||||||
log.SetOutput(colorable.NewColorableStdout())
|
ct := r.Header.Get(ContentType)
|
||||||
|
err = UnsupportedMediaType
|
||||||
|
if strings.HasPrefix(ct, ApplicationJSON) {
|
||||||
|
err = json.NewDecoder(r.Body).Decode(i)
|
||||||
|
} else if strings.HasPrefix(ct, ApplicationXML) {
|
||||||
|
err = xml.NewDecoder(r.Body).Decode(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
42
echo_test.go
42
echo_test.go
@ -18,8 +18,8 @@ import (
|
|||||||
|
|
||||||
type (
|
type (
|
||||||
user struct {
|
user struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id" xml:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" xml:"name"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -33,8 +33,8 @@ func TestEcho(t *testing.T) {
|
|||||||
assert.NotNil(t, e.Router())
|
assert.NotNil(t, e.Router())
|
||||||
|
|
||||||
// Debug
|
// Debug
|
||||||
e.SetDebug(true)
|
e.Debug()
|
||||||
assert.True(t, e.Debug())
|
assert.True(t, e.debug)
|
||||||
|
|
||||||
// DefaultHTTPErrorHandler
|
// DefaultHTTPErrorHandler
|
||||||
e.DefaultHTTPErrorHandler(errors.New("error"), c)
|
e.DefaultHTTPErrorHandler(errors.New("error"), c)
|
||||||
@ -246,6 +246,20 @@ func TestEchoTrace(t *testing.T) {
|
|||||||
testMethod(t, TRACE, "/", e)
|
testMethod(t, TRACE, "/", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEchoAny(t *testing.T) { // JFC
|
||||||
|
e := New()
|
||||||
|
e.Any("/", func(c *Context) error {
|
||||||
|
return c.String(http.StatusOK, "Any")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEchoMatch(t *testing.T) { // JFC
|
||||||
|
e := New()
|
||||||
|
e.Match([]string{GET, POST}, "/", func(c *Context) error {
|
||||||
|
return c.String(http.StatusOK, "Match")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestEchoWebSocket(t *testing.T) {
|
func TestEchoWebSocket(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
e.WebSocket("/ws", func(c *Context) error {
|
e.WebSocket("/ws", func(c *Context) error {
|
||||||
@ -368,6 +382,14 @@ func TestEchoNotFound(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEchoBadRequest(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r, _ := http.NewRequest("INVALID", "/files", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(w, r)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestEchoHTTPError(t *testing.T) {
|
func TestEchoHTTPError(t *testing.T) {
|
||||||
m := http.StatusText(http.StatusBadRequest)
|
m := http.StatusText(http.StatusBadRequest)
|
||||||
he := NewHTTPError(http.StatusBadRequest, m)
|
he := NewHTTPError(http.StatusBadRequest, m)
|
||||||
@ -381,12 +403,20 @@ func TestEchoServer(t *testing.T) {
|
|||||||
assert.IsType(t, &http.Server{}, s)
|
assert.IsType(t, &http.Server{}, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStripTrailingSlash(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
e.StripTrailingSlash()
|
||||||
|
r, _ := http.NewRequest(GET, "/users/", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
e.ServeHTTP(w, r)
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func testMethod(t *testing.T, method, path string, e *Echo) {
|
func testMethod(t *testing.T, method, path string, e *Echo) {
|
||||||
m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:]))
|
m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:]))
|
||||||
p := reflect.ValueOf(path)
|
p := reflect.ValueOf(path)
|
||||||
h := reflect.ValueOf(func(c *Context) error {
|
h := reflect.ValueOf(func(c *Context) error {
|
||||||
c.String(http.StatusOK, method)
|
return c.String(http.StatusOK, method)
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
i := interface{}(e)
|
i := interface{}(e)
|
||||||
reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h})
|
reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h})
|
||||||
|
@ -17,7 +17,7 @@ func main() {
|
|||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
// Debug mode
|
// Debug mode
|
||||||
e.SetDebug(true)
|
e.Debug()
|
||||||
|
|
||||||
//------------
|
//------------
|
||||||
// Middleware
|
// Middleware
|
||||||
@ -37,16 +37,6 @@ func main() {
|
|||||||
return false
|
return false
|
||||||
}))
|
}))
|
||||||
|
|
||||||
//-------
|
|
||||||
// Slash
|
|
||||||
//-------
|
|
||||||
|
|
||||||
e.Use(mw.StripTrailingSlash())
|
|
||||||
|
|
||||||
// or
|
|
||||||
|
|
||||||
// e.Use(mw.RedirectToSlash())
|
|
||||||
|
|
||||||
// Gzip
|
// Gzip
|
||||||
e.Use(mw.Gzip())
|
e.Use(mw.Gzip())
|
||||||
|
|
||||||
|
@ -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 (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"github.com/labstack/echo"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/labstack/echo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
BasicValidateFunc func(string, string) bool
|
BasicValidateFunc func(string, string) bool
|
||||||
JWTValidateFunc func(string, jwt.SigningMethod) ([]byte, error)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Basic = "Basic"
|
Basic = "Basic"
|
||||||
Bearer = "Bearer"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// BasicAuth returns an HTTP basic authentication middleware.
|
// BasicAuth returns an HTTP basic authentication middleware.
|
||||||
//
|
//
|
||||||
// For valid credentials it calls the next handler.
|
// For valid credentials it calls the next handler.
|
||||||
// For invalid Authorization header it sends "404 - Bad Request" response.
|
|
||||||
// For invalid credentials, it sends "401 - Unauthorized" response.
|
// For invalid credentials, it sends "401 - Unauthorized" response.
|
||||||
func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
|
func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
|
||||||
return func(c *echo.Context) error {
|
return func(c *echo.Context) error {
|
||||||
@ -31,7 +28,6 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
|
|||||||
|
|
||||||
auth := c.Request().Header.Get(echo.Authorization)
|
auth := c.Request().Header.Get(echo.Authorization)
|
||||||
l := len(Basic)
|
l := len(Basic)
|
||||||
he := echo.NewHTTPError(http.StatusBadRequest)
|
|
||||||
|
|
||||||
if len(auth) > l+1 && auth[:l] == Basic {
|
if len(auth) > l+1 && auth[:l] == Basic {
|
||||||
b, err := base64.StdEncoding.DecodeString(auth[l+1:])
|
b, err := base64.StdEncoding.DecodeString(auth[l+1:])
|
||||||
@ -43,47 +39,11 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc {
|
|||||||
if fn(cred[:i], cred[i+1:]) {
|
if fn(cred[:i], cred[i+1:]) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
he.SetCode(http.StatusUnauthorized)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return he
|
c.Response().Header().Set(echo.WWWAuthenticate, Basic + " realm=Restricted")
|
||||||
}
|
return echo.NewHTTPError(http.StatusUnauthorized)
|
||||||
}
|
|
||||||
|
|
||||||
// JWTAuth returns a JWT authentication middleware.
|
|
||||||
//
|
|
||||||
// For valid token it sets JWT claims in the context with key `_claims` and calls
|
|
||||||
// the next handler.
|
|
||||||
// For invalid Authorization header it sends "404 - Bad Request" response.
|
|
||||||
// For invalid credentials, it sends "401 - Unauthorized" response.
|
|
||||||
func JWTAuth(fn JWTValidateFunc) echo.HandlerFunc {
|
|
||||||
return func(c *echo.Context) error {
|
|
||||||
// Skip WebSocket
|
|
||||||
if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
auth := c.Request().Header.Get("Authorization")
|
|
||||||
l := len(Bearer)
|
|
||||||
he := echo.NewHTTPError(http.StatusBadRequest)
|
|
||||||
|
|
||||||
if len(auth) > l+1 && auth[:l] == Bearer {
|
|
||||||
t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) {
|
|
||||||
// Lookup key and verify method
|
|
||||||
if kid := token.Header["kid"]; kid != nil {
|
|
||||||
return fn(kid.(string), token.Method)
|
|
||||||
}
|
|
||||||
return fn("", token.Method)
|
|
||||||
})
|
|
||||||
if err == nil && t.Valid {
|
|
||||||
c.Set("_claims", t.Claims)
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
he.SetCode(http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return he
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,13 +3,11 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/dgrijalva/jwt-go"
|
|
||||||
"github.com/labstack/echo"
|
"github.com/labstack/echo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"net/http/httptest"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBasicAuth(t *testing.T) {
|
func TestBasicAuth(t *testing.T) {
|
||||||
@ -38,80 +36,22 @@ func TestBasicAuth(t *testing.T) {
|
|||||||
req.Header.Set(echo.Authorization, auth)
|
req.Header.Set(echo.Authorization, auth)
|
||||||
he := ba(c).(*echo.HTTPError)
|
he := ba(c).(*echo.HTTPError)
|
||||||
assert.Equal(t, http.StatusUnauthorized, he.Code())
|
assert.Equal(t, http.StatusUnauthorized, he.Code())
|
||||||
|
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
|
||||||
|
|
||||||
// Empty Authorization header
|
// Empty Authorization header
|
||||||
req.Header.Set(echo.Authorization, "")
|
req.Header.Set(echo.Authorization, "")
|
||||||
he = ba(c).(*echo.HTTPError)
|
he = ba(c).(*echo.HTTPError)
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
assert.Equal(t, http.StatusUnauthorized, he.Code())
|
||||||
|
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
|
||||||
|
|
||||||
// Invalid Authorization header
|
// Invalid Authorization header
|
||||||
auth = base64.StdEncoding.EncodeToString([]byte(" :secret"))
|
auth = base64.StdEncoding.EncodeToString([]byte("invalid"))
|
||||||
req.Header.Set(echo.Authorization, auth)
|
req.Header.Set(echo.Authorization, auth)
|
||||||
he = ba(c).(*echo.HTTPError)
|
he = ba(c).(*echo.HTTPError)
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
assert.Equal(t, http.StatusUnauthorized, he.Code())
|
||||||
|
assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate))
|
||||||
// Invalid scheme
|
|
||||||
auth = "Base " + base64.StdEncoding.EncodeToString([]byte(" :secret"))
|
|
||||||
req.Header.Set(echo.Authorization, auth)
|
|
||||||
he = ba(c).(*echo.HTTPError)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
|
||||||
|
|
||||||
// WebSocket
|
// WebSocket
|
||||||
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
|
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
|
||||||
assert.NoError(t, ba(c))
|
assert.NoError(t, ba(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJWTAuth(t *testing.T) {
|
|
||||||
req, _ := http.NewRequest(echo.GET, "/", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
c := echo.NewContext(req, echo.NewResponse(rec), echo.New())
|
|
||||||
key := []byte("key")
|
|
||||||
fn := func(kid string, method jwt.SigningMethod) ([]byte, error) {
|
|
||||||
return key, nil
|
|
||||||
}
|
|
||||||
ja := JWTAuth(fn)
|
|
||||||
token := jwt.New(jwt.SigningMethodHS256)
|
|
||||||
token.Claims["foo"] = "bar"
|
|
||||||
token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
|
|
||||||
ts, err := token.SignedString(key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
// Valid credentials
|
|
||||||
auth := Bearer + " " + ts
|
|
||||||
req.Header.Set(echo.Authorization, auth)
|
|
||||||
assert.NoError(t, ja(c))
|
|
||||||
|
|
||||||
//---------------------
|
|
||||||
// Invalid credentials
|
|
||||||
//---------------------
|
|
||||||
|
|
||||||
// Expired token
|
|
||||||
token.Claims["exp"] = time.Now().Add(-time.Second).Unix()
|
|
||||||
ts, err = token.SignedString(key)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
auth = Bearer + " " + ts
|
|
||||||
req.Header.Set(echo.Authorization, auth)
|
|
||||||
he := ja(c).(*echo.HTTPError)
|
|
||||||
assert.Equal(t, http.StatusUnauthorized, he.Code())
|
|
||||||
|
|
||||||
// Empty Authorization header
|
|
||||||
req.Header.Set(echo.Authorization, "")
|
|
||||||
he = ja(c).(*echo.HTTPError)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
|
||||||
|
|
||||||
// Invalid Authorization header
|
|
||||||
auth = "token"
|
|
||||||
req.Header.Set(echo.Authorization, auth)
|
|
||||||
he = ja(c).(*echo.HTTPError)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
|
||||||
|
|
||||||
// Invalid scheme
|
|
||||||
auth = "Bear token"
|
|
||||||
req.Header.Set(echo.Authorization, auth)
|
|
||||||
he = ja(c).(*echo.HTTPError)
|
|
||||||
assert.Equal(t, http.StatusBadRequest, he.Code())
|
|
||||||
|
|
||||||
// WebSocket
|
|
||||||
c.Request().Header.Set(echo.Upgrade, echo.WebSocket)
|
|
||||||
assert.NoError(t, ja(c))
|
|
||||||
}
|
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func TestRecover(t *testing.T) {
|
func TestRecover(t *testing.T) {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.SetDebug(true)
|
e.Debug()
|
||||||
req, _ := http.NewRequest(echo.GET, "/", nil)
|
req, _ := http.NewRequest(echo.GET, "/", nil)
|
||||||
rec := httptest.NewRecorder()
|
rec := httptest.NewRecorder()
|
||||||
c := echo.NewContext(req, echo.NewResponse(rec), e)
|
c := echo.NewContext(req, echo.NewResponse(rec), e)
|
||||||
|
@ -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
|
// Setup
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Get("/", func(c *echo.Context) error {
|
e.Get("/", func(c *echo.Context) error {
|
||||||
c.String(http.StatusOK, "Six sick bricks tick")
|
return c.String(http.StatusOK, "Six sick bricks tick")
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use github.com/facebookgo/grace/gracehttp
|
|
||||||
gracehttp.Serve(e.Server(":1323"))
|
gracehttp.Serve(e.Server(":1323"))
|
||||||
}
|
}
|
@ -12,10 +12,8 @@ func main() {
|
|||||||
// Setup
|
// Setup
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Get("/", func(c *echo.Context) error {
|
e.Get("/", func(c *echo.Context) error {
|
||||||
c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
|
return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose")
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use github.com/tylerb/graceful
|
|
||||||
graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
|
graceful.ListenAndServe(e.Server(":1323"), 5*time.Second)
|
||||||
}
|
}
|
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() {
|
func main() {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.Get("/stream", func(c *echo.Context) error {
|
e.Get("/", func(c *echo.Context) error {
|
||||||
c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
|
c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON)
|
||||||
c.Response().WriteHeader(http.StatusOK)
|
c.Response().WriteHeader(http.StatusOK)
|
||||||
for _, l := range locations {
|
for _, l := range locations {
|
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.status = http.StatusOK
|
||||||
r.committed = false
|
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 (
|
type (
|
||||||
Router struct {
|
Router struct {
|
||||||
trees [21]*node
|
connectTree *node
|
||||||
routes []Route
|
deleteTree *node
|
||||||
echo *Echo
|
getTree *node
|
||||||
|
headTree *node
|
||||||
|
optionsTree *node
|
||||||
|
patchTree *node
|
||||||
|
postTree *node
|
||||||
|
putTree *node
|
||||||
|
traceTree *node
|
||||||
|
routes []Route
|
||||||
|
echo *Echo
|
||||||
}
|
}
|
||||||
node struct {
|
node struct {
|
||||||
typ ntype
|
typ ntype
|
||||||
@ -28,19 +36,20 @@ const (
|
|||||||
mtype
|
mtype
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewRouter(e *Echo) (r *Router) {
|
func NewRouter(e *Echo) *Router {
|
||||||
r = &Router{
|
return &Router{
|
||||||
// trees: make(map[string]*node),
|
connectTree: new(node),
|
||||||
routes: []Route{},
|
deleteTree: new(node),
|
||||||
echo: e,
|
getTree: new(node),
|
||||||
|
headTree: new(node),
|
||||||
|
optionsTree: new(node),
|
||||||
|
patchTree: new(node),
|
||||||
|
postTree: new(node),
|
||||||
|
putTree: new(node),
|
||||||
|
traceTree: new(node),
|
||||||
|
routes: []Route{},
|
||||||
|
echo: e,
|
||||||
}
|
}
|
||||||
for _, m := range methods {
|
|
||||||
r.trees[r.treeIndex(m)] = &node{
|
|
||||||
prefix: "",
|
|
||||||
children: children{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
|
func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
|
||||||
@ -66,7 +75,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) {
|
|||||||
} else if path[i] == '*' {
|
} else if path[i] == '*' {
|
||||||
r.insert(method, path[:i], nil, stype, nil, e)
|
r.insert(method, path[:i], nil, stype, nil, e)
|
||||||
pnames = append(pnames, "_name")
|
pnames = append(pnames, "_name")
|
||||||
r.insert(method, path[:i+1], h, mtype, pnames, e)
|
r.insert(method, path[:i + 1], h, mtype, pnames, e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,7 +90,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t ntype, pnames []st
|
|||||||
*e.maxParam = l
|
*e.maxParam = l
|
||||||
}
|
}
|
||||||
|
|
||||||
cn := r.trees[r.treeIndex(method)] // Current node as root
|
cn := r.findTree(method) // Current node as root
|
||||||
|
if cn == nil {
|
||||||
|
panic("echo => invalid method")
|
||||||
|
}
|
||||||
search := path
|
search := path
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@ -200,21 +212,89 @@ func (n *node) findChildWithType(t ntype) *node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) treeIndex(method string) uint8 {
|
func (r *Router) findTree(method string) (n *node) {
|
||||||
if method[0] == 'P' {
|
switch method[0] {
|
||||||
return method[0]%10 + method[1] - 65
|
case 'G': // GET
|
||||||
} else {
|
m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
|
||||||
return method[0] % 10
|
if m == 0x47455400 {
|
||||||
|
n = r.getTree
|
||||||
|
}
|
||||||
|
case 'P': // POST, PUT or PATCH
|
||||||
|
switch method[1] {
|
||||||
|
case 'O': // POST
|
||||||
|
m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
|
||||||
|
uint32(method[0]) << 24
|
||||||
|
if m == 0x504f5354 {
|
||||||
|
n = r.postTree
|
||||||
|
}
|
||||||
|
case 'U': // PUT
|
||||||
|
m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24
|
||||||
|
if m == 0x50555400 {
|
||||||
|
n = r.putTree
|
||||||
|
}
|
||||||
|
case 'A': // PATCH
|
||||||
|
m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
|
||||||
|
uint64(method[1]) << 48 | uint64(method[0]) << 56
|
||||||
|
if m == 0x5041544348000000 {
|
||||||
|
n = r.patchTree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'D': // DELETE
|
||||||
|
m := uint64(method[5]) << 16 | uint64(method[4]) << 24 | uint64(method[3]) << 32 |
|
||||||
|
uint64(method[2]) << 40 | uint64(method[1]) << 48 | uint64(method[0]) << 56
|
||||||
|
if m == 0x44454c4554450000 {
|
||||||
|
n = r.deleteTree
|
||||||
|
}
|
||||||
|
case 'C': // CONNECT
|
||||||
|
m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
|
||||||
|
uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
|
||||||
|
uint64(method[0]) << 56
|
||||||
|
if m == 0x434f4e4e45435400 {
|
||||||
|
n = r.connectTree
|
||||||
|
}
|
||||||
|
case 'H': // HEAD
|
||||||
|
m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 |
|
||||||
|
uint32(method[0]) << 24
|
||||||
|
if m == 0x48454144 {
|
||||||
|
n = r.headTree
|
||||||
|
}
|
||||||
|
case 'O': // OPTIONS
|
||||||
|
m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 |
|
||||||
|
uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 |
|
||||||
|
uint64(method[0]) << 56
|
||||||
|
if m == 0x4f5054494f4e5300 {
|
||||||
|
n = r.optionsTree
|
||||||
|
}
|
||||||
|
case 'T': // TRACE
|
||||||
|
m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 |
|
||||||
|
uint64(method[1]) << 48 | uint64(method[0]) << 56
|
||||||
|
if m == 0x5452414345000000 {
|
||||||
|
n = r.traceTree
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) {
|
func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) {
|
||||||
cn := r.trees[r.treeIndex(method)] // Current node as root
|
h = notFoundHandler
|
||||||
search := path
|
cn := r.findTree(method) // Current node as root
|
||||||
|
if cn == nil {
|
||||||
|
h = badRequestHandler
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip trailing slash
|
||||||
|
if r.echo.stripTrailingSlash {
|
||||||
|
l := len(path)
|
||||||
|
if path[l - 1] == '/' {
|
||||||
|
path = path[:l - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
search = path
|
||||||
c *node // Child node
|
c *node // Child node
|
||||||
n int // Param counter
|
n int // Param counter
|
||||||
nt ntype // Next type
|
nt ntype // Next type
|
||||||
nn *node // Next node
|
nn *node // Next node
|
||||||
ns string // Next search
|
ns string // Next search
|
||||||
@ -225,10 +305,12 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
|
|||||||
// Search order static > param > match-any
|
// Search order static > param > match-any
|
||||||
for {
|
for {
|
||||||
if search == "" {
|
if search == "" {
|
||||||
// Found
|
if cn.handler != nil {
|
||||||
ctx.pnames = cn.pnames
|
// Found
|
||||||
h = cn.handler
|
ctx.pnames = cn.pnames
|
||||||
e = cn.echo
|
h = cn.handler
|
||||||
|
e = cn.echo
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -287,7 +369,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Param node
|
// Param node
|
||||||
Param:
|
Param:
|
||||||
c = cn.findChildWithType(ptype)
|
c = cn.findChildWithType(ptype)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
// Save next
|
// Save next
|
||||||
@ -307,7 +389,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Match-any node
|
// Match-any node
|
||||||
MatchAny:
|
MatchAny:
|
||||||
// c = cn.getChild()
|
// c = cn.getChild()
|
||||||
c = cn.findChildWithType(mtype)
|
c = cn.findChildWithType(mtype)
|
||||||
if c != nil {
|
if c != nil {
|
||||||
@ -326,10 +408,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
c := r.echo.pool.Get().(*Context)
|
c := r.echo.pool.Get().(*Context)
|
||||||
h, _ := r.Find(req.Method, req.URL.Path, c)
|
h, _ := r.Find(req.Method, req.URL.Path, c)
|
||||||
c.reset(req, w, r.echo)
|
c.reset(req, w, r.echo)
|
||||||
if h == nil {
|
if err := h(c); err != nil {
|
||||||
c.Error(NewHTTPError(http.StatusNotFound))
|
r.echo.httpErrorHandler(err, c)
|
||||||
} else {
|
|
||||||
h(c)
|
|
||||||
}
|
}
|
||||||
r.echo.pool.Put(c)
|
r.echo.pool.Put(c)
|
||||||
}
|
}
|
||||||
|
@ -316,9 +316,6 @@ func TestRouterTwoParam(t *testing.T) {
|
|||||||
assert.Equal(t, "1", c.P(0))
|
assert.Equal(t, "1", c.P(0))
|
||||||
assert.Equal(t, "1", c.P(1))
|
assert.Equal(t, "1", c.P(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
h, _ = r.Find(GET, "/users/1", c)
|
|
||||||
assert.Nil(t, h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouterMatchAny(t *testing.T) {
|
func TestRouterMatchAny(t *testing.T) {
|
||||||
@ -384,7 +381,10 @@ func TestRouterMultiRoute(t *testing.T) {
|
|||||||
|
|
||||||
// Route > /user
|
// Route > /user
|
||||||
h, _ = r.Find(GET, "/user", c)
|
h, _ = r.Find(GET, "/user", c)
|
||||||
assert.Nil(t, h)
|
if assert.IsType(t, new(HTTPError), h(c)) {
|
||||||
|
he := h(c).(*HTTPError)
|
||||||
|
assert.Equal(t, http.StatusNotFound, he.code)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRouterPriority(t *testing.T) {
|
func TestRouterPriority(t *testing.T) {
|
||||||
@ -537,6 +537,16 @@ func TestRouterAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouterAddInvalidMethod(t *testing.T) {
|
||||||
|
e := New()
|
||||||
|
r := e.router
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
r.Add("INVALID", "/", func(*Context) error {
|
||||||
|
return nil
|
||||||
|
}, e)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestRouterServeHTTP(t *testing.T) {
|
func TestRouterServeHTTP(t *testing.T) {
|
||||||
e := New()
|
e := New()
|
||||||
r := e.router
|
r := e.router
|
||||||
|
BIN
test/fixture/walle.png
Normal file
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.
|
Echo follows [semantic versioning](http://semver.org) managed through GitHub releases.
|
||||||
Specific version of Echo can be installed using any [package manager](https://github.com/avelino/awesome-go#package-management).
|
Specific version of Echo can be installed using a [package manager](https://github.com/avelino/awesome-go#package-management).
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
@ -42,54 +42,45 @@ and message `HTTPError.Message`.
|
|||||||
|
|
||||||
### Debug
|
### Debug
|
||||||
|
|
||||||
`Echo.SetDebug(on bool)`
|
`Echo.Debug()`
|
||||||
|
|
||||||
Enables debug mode.
|
Enables debug mode.
|
||||||
|
|
||||||
|
### Disable colored log
|
||||||
|
|
||||||
|
`Echo.DisableColoredLog()`
|
||||||
|
|
||||||
|
### StripTrailingSlash
|
||||||
|
|
||||||
|
StripTrailingSlash enables removing trailing slash from the request path.
|
||||||
|
|
||||||
|
`e.StripTrailingSlash()`
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and
|
Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and
|
||||||
flexible. It's based on [redix tree](http://en.wikipedia.org/wiki/Radix_tree)
|
flexible. It's based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree) data
|
||||||
data structure which makes routing lookup really fast. It leverages
|
structure which makes route lookup really fast. Router leverages [sync pool](https://golang.org/pkg/sync/#Pool)
|
||||||
[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve
|
to reuse memory and achieve zero dynamic memory allocation with no GC overhead.
|
||||||
zero dynamic memory allocation with no GC overhead.
|
|
||||||
|
|
||||||
Routes can be registered by specifying HTTP method, path and a handler. For example,
|
Routes can be registered by specifying HTTP method, path and a handler. For example,
|
||||||
code below registers a route for method `GET`, path `/hello` and a handler which sends
|
code below registers a route for method `GET`, path `/hello` and a handler which sends
|
||||||
`Hello!` HTTP response.
|
`Hello!` HTTP response.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
echo.Get("/hello", func(c *echo.Context) error {
|
e.Get("/hello", func(c *echo.Context) error {
|
||||||
return c.String(http.StatusOK, "Hello!")
|
return c.String(http.StatusOK, "Hello!")
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Echo's default handler is `func(*echo.Context) error` where `echo.Context`
|
Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily
|
||||||
primarily holds HTTP request and response objects. Echo also has a support for other
|
holds HTTP request and response objects. Echo also has a support for other types
|
||||||
types of handlers.
|
of handlers.
|
||||||
|
|
||||||
### Path parameter
|
|
||||||
|
|
||||||
Request path parameters can be extracted either by name `Echo.Context.Param(name string) string`
|
|
||||||
or by index `Echo.Context.P(i int) string`. Getting parameter by index gives a
|
|
||||||
slightly better performance.
|
|
||||||
|
|
||||||
```go
|
|
||||||
echo.Get("/users/:id", func(c *echo.Context) error {
|
|
||||||
// By name
|
|
||||||
id := c.Param("id")
|
|
||||||
|
|
||||||
// By index
|
|
||||||
id := c.P(0)
|
|
||||||
|
|
||||||
return c.String(http.StatusOK, id)
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### Match-any
|
### Match-any
|
||||||
|
|
||||||
Matches zero or more characters in the path. For example, pattern `/users/*` will
|
Matches zero or more characters in the path. For example, pattern `/users/*` will
|
||||||
match
|
match:
|
||||||
|
|
||||||
- `/users/`
|
- `/users/`
|
||||||
- `/users/1`
|
- `/users/1`
|
||||||
@ -118,13 +109,13 @@ e.Get("/users/1/files/*", func(c *echo.Context) error {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Above routes would resolve in order
|
Above routes would resolve in the following order:
|
||||||
|
|
||||||
- `/users/new`
|
- `/users/new`
|
||||||
- `/users/:id`
|
- `/users/:id`
|
||||||
- `/users/1/files/*`
|
- `/users/1/files/*`
|
||||||
|
|
||||||
Routes can be written in any order.
|
> Routes can be written in any order.
|
||||||
|
|
||||||
### Group
|
### Group
|
||||||
|
|
||||||
@ -150,15 +141,15 @@ e.Use(mw.BasicAuth(func(usr, pwd string) bool {
|
|||||||
|
|
||||||
### URI building
|
### URI building
|
||||||
|
|
||||||
`Echo.URI` can be used generate URI for any handler with specified path parameters.
|
`Echo.URI` can be used to generate URI for any handler with specified path parameters.
|
||||||
It's helpful to centralize all your URI patterns which ease in refactoring your
|
It's helpful to centralize all your URI patterns which ease in refactoring your
|
||||||
application.
|
application.
|
||||||
|
|
||||||
`echo.URI(h, 1)` will generate `/users/1` for the route registered below
|
`e.URI(h, 1)` will generate `/users/1` for the route registered below
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Handler
|
// Handler
|
||||||
h := func(*echo.Context) error {
|
h := func(c *echo.Context) error {
|
||||||
return c.String(http.StatusOK, "OK")
|
return c.String(http.StatusOK, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,9 +159,9 @@ e.Get("/users/:id", h)
|
|||||||
|
|
||||||
## Middleware
|
## Middleware
|
||||||
|
|
||||||
Middleware is function which is chained in the HTTP request-response cycle. Middleware
|
Middleware is a function which is chained in the HTTP request-response cycle. Middleware
|
||||||
has access to the request and response objects which it utilizes to perform a specific
|
has access to the request and response objects which it utilizes to perform a specific
|
||||||
action for example, logging every request. Echo supports variety of [middleware](/#features).
|
action, for example, logging every request.
|
||||||
|
|
||||||
### Logger
|
### Logger
|
||||||
|
|
||||||
@ -195,7 +186,7 @@ BasicAuth middleware provides an HTTP basic authentication.
|
|||||||
*Example*
|
*Example*
|
||||||
|
|
||||||
```go
|
```go
|
||||||
echo.Group("/admin")
|
e.Group("/admin")
|
||||||
e.Use(mw.BasicAuth(func(usr, pwd string) bool {
|
e.Use(mw.BasicAuth(func(usr, pwd string) bool {
|
||||||
if usr == "joe" && pwd == "secret" {
|
if usr == "joe" && pwd == "secret" {
|
||||||
return true
|
return true
|
||||||
@ -225,63 +216,163 @@ to the centralized [HTTPErrorHandler](#error-handling).
|
|||||||
e.Use(mw.Recover())
|
e.Use(mw.Recover())
|
||||||
```
|
```
|
||||||
|
|
||||||
### StripTrailingSlash
|
[Examples](https://github.com/labstack/echo/tree/master/examples/middleware)
|
||||||
|
|
||||||
StripTrailingSlash middleware removes the trailing slash from request path.
|
## Request
|
||||||
|
|
||||||
|
### Path parameter
|
||||||
|
|
||||||
|
Path parameter can be retrieved either by name `Context.Param(name string) string`
|
||||||
|
or by index `Context.P(i int) string`. Getting parameter by index gives a slightly
|
||||||
|
better performance.
|
||||||
|
|
||||||
*Example*
|
*Example*
|
||||||
|
|
||||||
```go
|
```go
|
||||||
e.Use(mw.StripTrailingSlash())
|
e.Get("/users/:name", func(c *echo.Context) error {
|
||||||
|
// By name
|
||||||
|
name := c.Param("name")
|
||||||
|
|
||||||
|
// By index
|
||||||
|
name := c.P(0)
|
||||||
|
|
||||||
|
return c.String(http.StatusOK, name)
|
||||||
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
### RedirectToSlash
|
```sh
|
||||||
RedirectToSlash middleware redirects requests without trailing slash path to trailing
|
$ curl http://localhost:1323/users/joe
|
||||||
slash path.
|
```
|
||||||
|
|
||||||
|
### Query parameter
|
||||||
|
|
||||||
|
Query parameter can be retrieved by name using `Context.Query(name string)`.
|
||||||
|
|
||||||
|
*Example*
|
||||||
|
|
||||||
*Options*
|
|
||||||
```go
|
```go
|
||||||
RedirectToSlashOptions struct {
|
e.Get("/users", func(c *echo.Context) error {
|
||||||
Code int
|
name := c.Query("name")
|
||||||
|
return c.String(http.StatusOK, name)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl -G -d "name=joe" http://localhost:1323/users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Form parameter
|
||||||
|
|
||||||
|
Form parameter can be retrieved by name using `Context.Form(name string)`.
|
||||||
|
|
||||||
|
*Example*
|
||||||
|
|
||||||
|
```go
|
||||||
|
e.Post("/users", func(c *echo.Context) error {
|
||||||
|
name := c.Form("name")
|
||||||
|
return c.String(http.StatusOK, name)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ curl -d "name=joe" http://localhost:1323/users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
```go
|
||||||
|
Context.Render(code int, name string, data interface{}) error
|
||||||
|
```
|
||||||
|
Renders a template with data and sends a text/html response with status code. Templates
|
||||||
|
can be registered using `Echo.SetRenderer()`, allowing us to use any template engine.
|
||||||
|
|
||||||
|
Below is an example using Go `html/template`
|
||||||
|
|
||||||
|
- Implement `echo.Render` interface
|
||||||
|
|
||||||
|
```go
|
||||||
|
Template struct {
|
||||||
|
templates *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Template) Render(w io.Writer, name string, data interface{}) error {
|
||||||
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
*Example*
|
- Pre-compile templates
|
||||||
|
|
||||||
```go
|
`public/views/hello.html`
|
||||||
e.Use(mw.RedirectToSlash())
|
|
||||||
|
```html
|
||||||
|
{{define "hello"}}Hello, {{.}}!{{end}}
|
||||||
```
|
```
|
||||||
|
|
||||||
> StripTrailingSlash and RedirectToSlash middleware should not be used together.
|
```go
|
||||||
|
t := &Template{
|
||||||
|
templates: template.Must(template.ParseGlob("public/views/*.html")),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
[Examples](https://github.com/labstack/echo/tree/master/examples/middleware)
|
- Register templates
|
||||||
|
|
||||||
## Response
|
```go
|
||||||
|
e := echo.New()
|
||||||
|
e.SetRenderer(t)
|
||||||
|
e.Get("/hello", Hello)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Render template
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Hello(c *echo.Context) error {
|
||||||
|
return c.Render(http.StatusOK, "hello", "World")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### JSON
|
### JSON
|
||||||
|
|
||||||
```go
|
```go
|
||||||
context.JSON(code int, v interface{}) error
|
Context.JSON(code int, v interface{}) error
|
||||||
```
|
```
|
||||||
|
|
||||||
Sends a JSON HTTP response with status code.
|
Sends a JSON HTTP response with status code.
|
||||||
|
|
||||||
### String
|
### XML
|
||||||
|
|
||||||
```go
|
```go
|
||||||
context.String(code int, s string) error
|
Context.XML(code int, v interface{}) error
|
||||||
```
|
```
|
||||||
|
|
||||||
Sends a text/plain HTTP response with status code.
|
Sends an XML HTTP response with status code.
|
||||||
|
|
||||||
### HTML
|
### HTML
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func (c *Context) HTML(code int, html string) error
|
Context.HTML(code int, html string) error
|
||||||
```
|
```
|
||||||
|
|
||||||
Sends an HTML HTTP response with status code.
|
Sends an HTML HTTP response with status code.
|
||||||
|
|
||||||
|
### String
|
||||||
|
|
||||||
|
```go
|
||||||
|
Context.String(code int, s string) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Sends a text/plain HTTP response with status code.
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
```go
|
||||||
|
Context.File(name string, attachment bool) error
|
||||||
|
```
|
||||||
|
|
||||||
|
File sends a response with the content of the file. If attachment is `true`, the client
|
||||||
|
is prompted to save the file.
|
||||||
|
|
||||||
### Static files
|
### Static files
|
||||||
|
|
||||||
`Echo.Static(path, root string)` serves static files. For example, code below serves
|
`Echo.Static(path, root string)` serves static files. For example, code below serves
|
||||||
@ -360,7 +451,3 @@ func welcome(c *echo.Context) error {
|
|||||||
```
|
```
|
||||||
|
|
||||||
See how [HTTPErrorHandler](#customization) handles it.
|
See how [HTTPErrorHandler](#customization) handles it.
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
*WIP*
|
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
# Echo
|
# Echo
|
||||||
|
|
||||||
Build simple and performant systems!
|
A fast and unfancy micro web framework for Go.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Fast HTTP router which smartly prioritize routes.
|
- Fast HTTP router which smartly prioritize routes.
|
||||||
@ -27,14 +23,25 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
|
|||||||
- `http.HandlerFunc`
|
- `http.HandlerFunc`
|
||||||
- `func(http.ResponseWriter, *http.Request)`
|
- `func(http.ResponseWriter, *http.Request)`
|
||||||
- Sub-router/Groups
|
- Sub-router/Groups
|
||||||
- Handy encoding/decoding functions.
|
- Handy functions to send variety of HTTP response:
|
||||||
|
- HTML
|
||||||
|
- HTML via templates
|
||||||
|
- String
|
||||||
|
- JSON
|
||||||
|
- JSONP
|
||||||
|
- XML
|
||||||
|
- File
|
||||||
|
- NoContent
|
||||||
|
- Redirect
|
||||||
|
- Error
|
||||||
- Build-in support for:
|
- Build-in support for:
|
||||||
|
- Favicon
|
||||||
|
- Index file
|
||||||
- Static files
|
- Static files
|
||||||
- WebSocket
|
- WebSocket
|
||||||
- API to serve index and favicon.
|
|
||||||
- Centralized HTTP error handling.
|
- Centralized HTTP error handling.
|
||||||
- Customizable request binding function.
|
- Customizable HTTP request binding function.
|
||||||
- Customizable response rendering function, allowing you to use any HTML template engine.
|
- Customizable HTTP response rendering function, allowing you to use any HTML template engine.
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
@ -48,9 +55,9 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew
|
|||||||
$ go get github.com/labstack/echo
|
$ go get github.com/labstack/echo
|
||||||
```
|
```
|
||||||
|
|
||||||
###[Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello)
|
### Hello, World!
|
||||||
|
|
||||||
Create `server.go` with the following content
|
Create `server.go`
|
||||||
|
|
||||||
```go
|
```go
|
||||||
package main
|
package main
|
||||||
@ -83,24 +90,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`echo.New()` returns a new instance of Echo.
|
Start server
|
||||||
|
|
||||||
`e.Use(mw.Logger())` adds logging middleware to the chain. It logs every HTTP request
|
|
||||||
made to the server, producing output
|
|
||||||
|
|
||||||
```sh
|
|
||||||
2015/06/07 18:16:16 GET / 200 13.238µs 14
|
|
||||||
```
|
|
||||||
|
|
||||||
`e.Get("/", hello)` Registers hello handler for HTTP method `GET` and path `/`, so
|
|
||||||
whenever server receives an HTTP request at `/`, hello function is called.
|
|
||||||
|
|
||||||
In hello handler `c.String(http.StatusOK, "Hello, World!\n")` sends a text/plain
|
|
||||||
HTTP response to the client with 200 status code.
|
|
||||||
|
|
||||||
`e.Run(":1323")` Starts HTTP server at network address `:1323`.
|
|
||||||
|
|
||||||
Now start the server using command
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ go run server.go
|
$ go run server.go
|
||||||
@ -110,7 +100,7 @@ Browse to [http://localhost:1323](http://localhost:1323) and you should see
|
|||||||
Hello, World! on the page.
|
Hello, World! on the page.
|
||||||
|
|
||||||
### Next?
|
### Next?
|
||||||
- Browse [examples](https://github.com/labstack/echo/tree/master/examples)
|
- Browse [recipes](https://github.com/labstack/echo/tree/master/recipes)
|
||||||
- Head over to [Guide](guide.md)
|
- Head over to [Guide](guide.md)
|
||||||
|
|
||||||
## Contribute
|
## Contribute
|
||||||
|
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 }}">
|
{% if favicon %}<link rel="shortcut icon" href="{{ favicon }}">
|
||||||
{% else %}<link rel="shortcut icon" href="{{ base_url }}/img/favicon.ico">{% endif %}
|
{% else %}<link rel="shortcut icon" href="{{ base_url }}/img/favicon.ico">{% endif %}
|
||||||
|
|
||||||
<title>{% if page_title %}{{ page_title }} - {% endif %}{{ site_name }}</title>
|
<title>{% if page_title %}{{ page_title }} - {% endif %}{{ config.extra.site_title }}</title>
|
||||||
|
|
||||||
<link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet">
|
<link href="{{ base_url }}/css/bootstrap-custom.min.css" rel="stylesheet">
|
||||||
<link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet">
|
<link href="{{ base_url }}/css/font-awesome-4.0.3.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="{{ base_url }}/css/highlight.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/8.6/styles/tomorrow-night.min.css">
|
||||||
<link href="{{ base_url }}/css/base.css" rel="stylesheet">
|
<link href="{{ base_url }}/css/base.css" rel="stylesheet">
|
||||||
<link href="{{ base_url }}/css/echo.css" rel="stylesheet">
|
<link href="{{ base_url }}/css/echo.css" rel="stylesheet">
|
||||||
{%- for path in extra_css %}
|
{%- for path in extra_css %}
|
||||||
@ -75,4 +75,4 @@
|
|||||||
<script src="{{ path }}"></script>
|
<script src="{{ path }}"></script>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
site_name: Echo
|
site_name: Echo
|
||||||
|
theme: flatly
|
||||||
theme: journal
|
|
||||||
|
|
||||||
theme_dir: echo
|
theme_dir: echo
|
||||||
|
|
||||||
copyright: '© 2015 LabStack'
|
copyright: '© 2015 LabStack'
|
||||||
|
|
||||||
repo_url: https://github.com/labstack/echo
|
repo_url: https://github.com/labstack/echo
|
||||||
|
|
||||||
google_analytics: ['UA-51208124-3', 'auto']
|
google_analytics: ['UA-51208124-3', 'auto']
|
||||||
|
pages:
|
||||||
|
- Home: index.md
|
||||||
|
- Guide: guide.md
|
||||||
|
- Recipes:
|
||||||
|
- File Upload: recipes/file-upload.md
|
||||||
|
- Streaming File Upload: recipes/streaming-file-upload.md
|
||||||
|
- Streaming Response: recipes/streaming-response.md
|
||||||
|
- WebSocket: recipes/websocket.md
|
||||||
|
- Subdomains: recipes/subdomains.md
|
||||||
|
- JWT Authentication: recipes/jwt-authentication.md
|
||||||
|
- Graceful Shutdown: recipes/graceful-shutdown.md
|
||||||
|
extra:
|
||||||
|
site_title: Echo, a fast and unfancy micro web framework for Go.
|
||||||
|
Loading…
Reference in New Issue
Block a user