diff --git a/.gitattributes b/.gitattributes index 79acd4a2..a9609ad0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,4 +17,5 @@ LICENSE text eol=lf # Exclude `website` and `examples` from Github's language statistics # https://github.com/github/linguist#using-gitattributes examples/* linguist-documentation +recipes/* linguist-documentation website/* linguist-documentation diff --git a/.gitignore b/.gitignore index 01f796c2..45cf8e1f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ # Website -site/ -.publish/ +site +.publish # Node.js -node_modules/ +node_modules # IntelliJ -.idea/ +.idea *.iml diff --git a/README.md b/README.md index 2e2840ed..2f4e4fdb 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ -# [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) -Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go. +# [Echo](http://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) + +A fast and unfancy micro web framework for Go. ## Features @@ -20,16 +21,27 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew - `http.HandlerFunc` - `func(http.ResponseWriter, *http.Request)` - Sub-router/Groups -- Handy encoding/decoding functions. +- Handy functions to send variety of HTTP response: + - HTML + - HTML via templates + - String + - JSON + - JSONP + - XML + - File + - NoContent + - Redirect + - Error - Build-in support for: + - Favicon + - Index file - Static files - WebSocket -- API to serve index and favicon. - Centralized HTTP error handling. -- Customizable request binding function. -- Customizable response rendering function, allowing you to use any HTML template engine. +- Customizable HTTP request binding function. +- Customizable HTTP response rendering function, allowing you to use any HTML template engine. -## Benchmark +## Performance Based on [vishr/go-http-routing-benchmark] (https://github.com/vishr/go-http-routing-benchmark), June 5, 2015. @@ -75,16 +87,28 @@ BenchmarkZeus_GithubAll 2000 748827 ns/op 30068 $ go get github.com/labstack/echo ``` -##[Examples](https://github.com/labstack/echo/tree/master/examples) +## [Recipes](https://github.com/labstack/echo/tree/master/recipes) -- [Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello) -- [CRUD](https://github.com/labstack/echo/tree/master/examples/crud) -- [Website](https://github.com/labstack/echo/tree/master/examples/website) -- [Middleware](https://github.com/labstack/echo/tree/master/examples/middleware) -- [Stream](https://github.com/labstack/echo/tree/master/examples/stream) +- [File Upload](http://echo.labstack.com/recipes/file-upload) +- [Streaming File Upload](http://echo.labstack.com/recipes/streaming-file-upload) +- [Streaming Response](http://echo.labstack.com/recipes/streaming-response) +- [WebSocket](http://echo.labstack.com/recipes/websocket) +- [Subdomains](http://echo.labstack.com/recipes/subdomains) +- [JWT Authentication](http://echo.labstack.com/recipes/jwt-authentication) +- [Graceful Shutdown](http://echo.labstack.com/recipes/graceful-shutdown) ##[Guide](http://echo.labstack.com/guide) +## Echo System + +Community created packages for Echo + +- [echo-logrus](https://github.com/deoxxa/echo-logrus) +- [go_middleware](https://github.com/rightscale/go_middleware) +- [permissions2](https://github.com/xyproto/permissions2) +- [permissionbolt](https://github.com/xyproto/permissionbolt) +- [echo-middleware](https://github.com/syntaqx/echo-middleware) + ## Contribute **Use issues for everything** @@ -98,7 +122,3 @@ $ go get github.com/labstack/echo - [Vishal Rana](https://github.com/vishr) - Author - [Nitin Rana](https://github.com/nr17) - Consultant - [Contributors](https://github.com/labstack/echo/graphs/contributors) - -## License - -[MIT](https://github.com/labstack/echo/blob/master/LICENSE) diff --git a/context.go b/context.go index bc9a64f0..c7beed9e 100644 --- a/context.go +++ b/context.go @@ -2,7 +2,13 @@ package echo import ( "encoding/json" + "encoding/xml" "net/http" + "path" + + "fmt" + + "net/url" "golang.org/x/net/websocket" ) @@ -16,6 +22,7 @@ type ( socket *websocket.Conn pnames []string pvalues []string + query url.Values store store echo *Echo } @@ -69,6 +76,19 @@ func (c *Context) Param(name string) (value string) { return } +// Query returns query parameter by name. +func (c *Context) Query(name string) string { + if c.query == nil { + c.query = c.request.URL.Query() + } + return c.query.Get(name) +} + +// Form returns form parameter by name. +func (c *Context) Form(name string) string { + return c.request.FormValue(name) +} + // Get retrieves data from the context. func (c *Context) Get(key string) interface{} { return c.store[key] @@ -76,47 +96,82 @@ func (c *Context) Get(key string) interface{} { // Set saves data in the context. func (c *Context) Set(key string, val interface{}) { + if c.store == nil { + c.store = make(store) + } c.store[key] = val } -// Bind binds the request body into specified type v. Default binder does it -// based on Content-Type header. +// Bind binds the request body into specified type `i`. The default binder does +// it based on Content-Type header. func (c *Context) Bind(i interface{}) error { - return c.echo.binder(c.request, i) + return c.echo.binder.Bind(c.request, i) } -// Render invokes the registered HTML template renderer and sends a text/html -// response with status code. +// Render renders a template with data and sends a text/html response with status +// code. Templates can be registered using `Echo.SetRenderer()`. func (c *Context) Render(code int, name string, data interface{}) error { if c.echo.renderer == nil { return RendererNotRegistered } - c.response.Header().Set(ContentType, TextHTML) + c.response.Header().Set(ContentType, TextHTMLCharsetUTF8) c.response.WriteHeader(code) return c.echo.renderer.Render(c.response, name, data) } -// JSON sends an application/json response with status code. +// HTML formats according to a format specifier and sends HTML response with +// status code. +func (c *Context) HTML(code int, format string, a ...interface{}) (err error) { + c.response.Header().Set(ContentType, TextHTMLCharsetUTF8) + c.response.WriteHeader(code) + _, err = fmt.Fprintf(c.response, format, a...) + return +} + +// String formats according to a format specifier and sends text response with status +// code. +func (c *Context) String(code int, format string, a ...interface{}) (err error) { + c.response.Header().Set(ContentType, TextPlain) + c.response.WriteHeader(code) + _, err = fmt.Fprintf(c.response, format, a...) + return +} + +// JSON sends a JSON response with status code. func (c *Context) JSON(code int, i interface{}) error { - c.response.Header().Set(ContentType, ApplicationJSON) + c.response.Header().Set(ContentType, ApplicationJSONCharsetUTF8) c.response.WriteHeader(code) return json.NewEncoder(c.response).Encode(i) } -// String sends a text/plain response with status code. -func (c *Context) String(code int, s string) error { - c.response.Header().Set(ContentType, TextPlain) +// JSONP sends a JSONP response with status code. It uses `callback` to construct +// the JSONP payload. +func (c *Context) JSONP(code int, callback string, i interface{}) (err error) { + c.response.Header().Set(ContentType, ApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) - _, err := c.response.Write([]byte(s)) - return err + c.response.Write([]byte(callback + "(")) + if err = json.NewEncoder(c.response).Encode(i); err == nil { + c.response.Write([]byte(");")) + } + return } -// HTML sends a text/html response with status code. -func (c *Context) HTML(code int, html string) error { - c.response.Header().Set(ContentType, TextHTML) +// XML sends an XML response with status code. +func (c *Context) XML(code int, i interface{}) error { + c.response.Header().Set(ContentType, ApplicationXMLCharsetUTF8) c.response.WriteHeader(code) - _, err := c.response.Write([]byte(html)) - return err + c.response.Write([]byte(xml.Header)) + return xml.NewEncoder(c.response).Encode(i) +} + +// File sends a response with the content of the file. If attachment is true, the +// client is prompted to save the file. +func (c *Context) File(name string, attachment bool) error { + dir, file := path.Split(name) + if attachment { + c.response.Header().Set(ContentDisposition, "attachment; filename="+file) + } + return serveFile(dir, file, c) } // NoContent sends a response with no body and a status code. @@ -126,11 +181,15 @@ func (c *Context) NoContent(code int) error { } // Redirect redirects the request using http.Redirect with status code. -func (c *Context) Redirect(code int, url string) { +func (c *Context) Redirect(code int, url string) error { + if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { + return InvalidRedirectCode + } http.Redirect(c.response, c.request, url, code) + return nil } -// Error invokes the registered HTTP error handler. Usually used by middleware. +// Error invokes the registered HTTP error handler. Generally used by middleware. func (c *Context) Error(err error) { c.echo.httpErrorHandler(err, c) } @@ -138,5 +197,7 @@ func (c *Context) Error(err error) { func (c *Context) reset(r *http.Request, w http.ResponseWriter, e *Echo) { c.request = r c.response.reset(w) + c.query = nil + c.store = nil c.echo = e } diff --git a/context_test.go b/context_test.go index 1a40351f..77e4b7d8 100644 --- a/context_test.go +++ b/context_test.go @@ -10,6 +10,9 @@ import ( "strings" + "encoding/xml" + "net/url" + "github.com/stretchr/testify/assert" ) @@ -24,8 +27,10 @@ func (t *Template) Render(w io.Writer, name string, data interface{}) error { } func TestContext(t *testing.T) { - usr := `{"id":"1","name":"Joe"}` - req, _ := http.NewRequest(POST, "/", strings.NewReader(usr)) + userJSON := `{"id":"1","name":"Joe"}` + userXML := `1Joe` + + req, _ := http.NewRequest(POST, "/", strings.NewReader(userJSON)) rec := httptest.NewRecorder() c := NewContext(req, NewResponse(rec), New()) @@ -38,16 +43,12 @@ func TestContext(t *testing.T) { // Socket assert.Nil(t, c.Socket()) - //------- - // Param - //------- - - // By id + // Param by id c.pnames = []string{"id"} c.pvalues = []string{"1"} assert.Equal(t, "1", c.P(0)) - // By name + // Param by name assert.Equal(t, "1", c.Param("id")) // Store @@ -59,19 +60,14 @@ func TestContext(t *testing.T) { //------ // JSON - testBind(t, c, ApplicationJSON) + testBind(t, c, "application/json") - // TODO: Form - c.request.Header.Set(ContentType, ApplicationForm) - u := new(user) - err := c.Bind(u) - assert.NoError(t, err) + // XML + c.request, _ = http.NewRequest(POST, "/", strings.NewReader(userXML)) + testBind(t, c, ApplicationXML) // Unsupported - c.request.Header.Set(ContentType, "") - u = new(user) - err = c.Bind(u) - assert.Error(t, err) + testBind(t, c, "") //-------- // Render @@ -81,7 +77,7 @@ func TestContext(t *testing.T) { templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")), } c.echo.SetRenderer(tpl) - err = c.Render(http.StatusOK, "hello", "Joe") + err := c.Render(http.StatusOK, "hello", "Joe") if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) assert.Equal(t, "Hello, Joe!", rec.Body.String()) @@ -92,18 +88,37 @@ func TestContext(t *testing.T) { assert.Error(t, err) // JSON - req.Header.Set(Accept, ApplicationJSON) rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec), New()) err = c.JSON(http.StatusOK, user{"1", "Joe"}) if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, ApplicationJSON, rec.Header().Get(ContentType)) - assert.Equal(t, usr, strings.TrimSpace(rec.Body.String())) + assert.Equal(t, ApplicationJSONCharsetUTF8, rec.Header().Get(ContentType)) + assert.Equal(t, userJSON+"\n", rec.Body.String()) + } + + // JSONP + rec = httptest.NewRecorder() + c = NewContext(req, NewResponse(rec), New()) + callback := "callback" + err = c.JSONP(http.StatusOK, callback, user{"1", "Joe"}) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, ApplicationJavaScriptCharsetUTF8, rec.Header().Get(ContentType)) + assert.Equal(t, callback+"("+userJSON+"\n);", rec.Body.String()) + } + + // XML + rec = httptest.NewRecorder() + c = NewContext(req, NewResponse(rec), New()) + err = c.XML(http.StatusOK, user{"1", "Joe"}) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, ApplicationXMLCharsetUTF8, rec.Header().Get(ContentType)) + assert.Equal(t, xml.Header, xml.Header, rec.Body.String()) } // String - req.Header.Set(Accept, TextPlain) rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec), New()) err = c.String(http.StatusOK, "Hello, World!") @@ -114,16 +129,34 @@ func TestContext(t *testing.T) { } // HTML - req.Header.Set(Accept, TextHTML) rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec), New()) err = c.HTML(http.StatusOK, "Hello, World!") if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Code) - assert.Equal(t, TextHTML, rec.Header().Get(ContentType)) + assert.Equal(t, TextHTMLCharsetUTF8, rec.Header().Get(ContentType)) assert.Equal(t, "Hello, World!", rec.Body.String()) } + // File + rec = httptest.NewRecorder() + c = NewContext(req, NewResponse(rec), New()) + err = c.File("test/fixture/walle.png", false) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, 219885, rec.Body.Len()) + } + + // File as attachment + rec = httptest.NewRecorder() + c = NewContext(req, NewResponse(rec), New()) + err = c.File("test/fixture/walle.png", true) + if assert.NoError(t, err) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, rec.Header().Get(ContentDisposition), "attachment; filename=walle.png") + assert.Equal(t, 219885, rec.Body.Len()) + } + // NoContent rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec), New()) @@ -133,7 +166,7 @@ func TestContext(t *testing.T) { // Redirect rec = httptest.NewRecorder() c = NewContext(req, NewResponse(rec), New()) - c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo") + assert.Equal(t, nil, c.Redirect(http.StatusMovedPermanently, "http://labstack.github.io/echo")) // Error rec = httptest.NewRecorder() @@ -145,11 +178,42 @@ func TestContext(t *testing.T) { c.reset(req, NewResponse(httptest.NewRecorder()), New()) } +func TestContextQuery(t *testing.T) { + q := make(url.Values) + q.Set("name", "joe") + q.Set("email", "joe@labstack.com") + + req, err := http.NewRequest(GET, "/", nil) + assert.NoError(t, err) + req.URL.RawQuery = q.Encode() + + c := NewContext(req, nil, New()) + assert.Equal(t, "joe", c.Query("name")) + assert.Equal(t, "joe@labstack.com", c.Query("email")) + +} + +func TestContextForm(t *testing.T) { + f := make(url.Values) + f.Set("name", "joe") + f.Set("email", "joe@labstack.com") + + req, err := http.NewRequest(POST, "/", strings.NewReader(f.Encode())) + assert.NoError(t, err) + req.Header.Add(ContentType, ApplicationForm) + + c := NewContext(req, nil, New()) + assert.Equal(t, "joe", c.Form("name")) + assert.Equal(t, "joe@labstack.com", c.Form("email")) +} + func testBind(t *testing.T, c *Context, ct string) { c.request.Header.Set(ContentType, ct) u := new(user) err := c.Bind(u) - if assert.NoError(t, err) { + if ct == "" { + assert.Error(t, UnsupportedMediaType) + } else if assert.NoError(t, err) { assert.Equal(t, "1", u.ID) assert.Equal(t, "Joe", u.Name) } diff --git a/echo.go b/echo.go index d226e62f..0dc66abf 100644 --- a/echo.go +++ b/echo.go @@ -14,8 +14,10 @@ import ( "strings" "sync" + "encoding/xml" + "github.com/bradfitz/http2" - "github.com/mattn/go-colorable" + "github.com/labstack/gommon/color" "golang.org/x/net/websocket" ) @@ -28,10 +30,11 @@ type ( notFoundHandler HandlerFunc defaultHTTPErrorHandler HTTPErrorHandler httpErrorHandler HTTPErrorHandler - binder BindFunc + binder Binder renderer Renderer pool sync.Pool debug bool + stripTrailingSlash bool router *Router } @@ -54,12 +57,20 @@ type ( // HTTPErrorHandler is a centralized HTTP error handler. HTTPErrorHandler func(error, *Context) - BindFunc func(*http.Request, interface{}) error + // Binder is the interface that wraps the Bind method. + Binder interface { + Bind(*http.Request, interface{}) error + } + + binder struct { + } + + // Validator is the interface that wraps the Validate method. + Validator interface { + Validate() error + } // Renderer is the interface that wraps the Render method. - // - // Render renders the HTML template with given name and specified data. - // It writes the output to w. Renderer interface { Render(w io.Writer, name string, data interface{}) error } @@ -89,27 +100,41 @@ const ( // Media types //------------- - ApplicationJSON = "application/json" - ApplicationProtobuf = "application/protobuf" - ApplicationMsgpack = "application/msgpack" - TextPlain = "text/plain" - TextHTML = "text/html" - ApplicationForm = "application/x-www-form-urlencoded" - MultipartForm = "multipart/form-data" + ApplicationJSON = "application/json" + ApplicationJSONCharsetUTF8 = ApplicationJSON + "; " + CharsetUTF8 + ApplicationJavaScript = "application/javascript" + ApplicationJavaScriptCharsetUTF8 = ApplicationJavaScript + "; " + CharsetUTF8 + ApplicationXML = "application/xml" + ApplicationXMLCharsetUTF8 = ApplicationXML + "; " + CharsetUTF8 + ApplicationForm = "application/x-www-form-urlencoded" + ApplicationProtobuf = "application/protobuf" + ApplicationMsgpack = "application/msgpack" + TextHTML = "text/html" + TextHTMLCharsetUTF8 = TextHTML + "; " + CharsetUTF8 + TextPlain = "text/plain" + TextPlainCharsetUTF8 = TextPlain + "; " + CharsetUTF8 + MultipartForm = "multipart/form-data" + + //--------- + // Charset + //--------- + + CharsetUTF8 = "charset=utf-8" //--------- // Headers //--------- - Accept = "Accept" AcceptEncoding = "Accept-Encoding" + Authorization = "Authorization" ContentDisposition = "Content-Disposition" ContentEncoding = "Content-Encoding" ContentLength = "Content-Length" ContentType = "Content-Type" - Authorization = "Authorization" + Location = "Location" Upgrade = "Upgrade" Vary = "Vary" + WWWAuthenticate = "WWW-Authenticate" //----------- // Protocols @@ -139,9 +164,22 @@ var ( UnsupportedMediaType = errors.New("echo ⇒ unsupported media type") RendererNotRegistered = errors.New("echo ⇒ renderer not registered") + InvalidRedirectCode = errors.New("echo ⇒ invalid redirect status code") + + //---------------- + // Error handlers + //---------------- + + notFoundHandler = func(c *Context) error { + return NewHTTPError(http.StatusNotFound) + } + + badRequestHandler = func(c *Context) error { + return NewHTTPError(http.StatusBadRequest) + } ) -// New creates an Echo instance. +// New creates an instance of Echo. func New() (e *Echo) { e = &Echo{maxParam: new(int)} e.pool.New = func() interface{} { @@ -153,10 +191,10 @@ func New() (e *Echo) { // Defaults //---------- - e.HTTP2(false) - e.notFoundHandler = func(c *Context) error { - return NewHTTPError(http.StatusNotFound) + if runtime.GOOS == "windows" { + e.DisableColoredLog() } + e.HTTP2() e.defaultHTTPErrorHandler = func(err error, c *Context) { code := http.StatusInternalServerError msg := http.StatusText(code) @@ -167,19 +205,13 @@ func New() (e *Echo) { if e.debug { msg = err.Error() } - http.Error(c.response, msg, code) + if !c.response.committed { + http.Error(c.response, msg, code) + } + log.Println(err) } e.SetHTTPErrorHandler(e.defaultHTTPErrorHandler) - e.SetBinder(func(r *http.Request, v interface{}) error { - ct := r.Header.Get(ContentType) - err := UnsupportedMediaType - if strings.HasPrefix(ct, ApplicationJSON) { - err = json.NewDecoder(r.Body).Decode(v) - } else if strings.HasPrefix(ct, ApplicationForm) { - err = nil - } - return err - }) + e.SetBinder(&binder{}) return } @@ -188,9 +220,14 @@ func (e *Echo) Router() *Router { return e.router } +// DisableColoredLog disables colored log. +func (e *Echo) DisableColoredLog() { + color.Disable() +} + // HTTP2 enables HTTP2 support. -func (e *Echo) HTTP2(on bool) { - e.http2 = on +func (e *Echo) HTTP2() { + e.http2 = true } // DefaultHTTPErrorHandler invokes the default HTTP error handler. @@ -204,7 +241,7 @@ func (e *Echo) SetHTTPErrorHandler(h HTTPErrorHandler) { } // SetBinder registers a custom binder. It's invoked by Context.Bind(). -func (e *Echo) SetBinder(b BindFunc) { +func (e *Echo) SetBinder(b Binder) { e.binder = b } @@ -213,14 +250,14 @@ func (e *Echo) SetRenderer(r Renderer) { e.renderer = r } -// SetDebug sets debug mode. -func (e *Echo) SetDebug(on bool) { - e.debug = on +// Debug enables debug mode. +func (e *Echo) Debug() { + e.debug = true } -// Debug returns debug mode. -func (e *Echo) Debug() bool { - return e.debug +// StripTrailingSlash enables removing trailing slash from the request path. +func (e *Echo) StripTrailingSlash() { + e.stripTrailingSlash = true } // Use adds handler to the middleware chain. @@ -275,6 +312,20 @@ func (e *Echo) Trace(path string, h Handler) { e.add(TRACE, path, h) } +// Any adds a route > handler to the router for all HTTP methods. +func (e *Echo) Any(path string, h Handler) { + for _, m := range methods { + e.add(m, path, h) + } +} + +// Match adds a route > handler to the router for multiple HTTP methods provided. +func (e *Echo) Match(methods []string, path string, h Handler) { + for _, m := range methods { + e.add(m, path, h) + } +} + // WebSocket adds a WebSocket route > handler to the router. func (e *Echo) WebSocket(path string, h HandlerFunc) { e.Get(path, func(c *Context) (err error) { @@ -389,7 +440,7 @@ func (e *Echo) URI(h Handler, params ...interface{}) string { return uri.String() } -// URL is an alias for URI. +// URL is an alias for `URI` function. func (e *Echo) URL(h Handler, params ...interface{}) string { return e.URI(h, params...) } @@ -399,6 +450,7 @@ func (e *Echo) Routes() []Route { return e.router.routes } +// ServeHTTP implements `http.Handler` interface, which serves HTTP requests. func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { c := e.pool.Get().(*Context) h, echo := e.router.Find(r.Method, r.URL.Path, c) @@ -406,9 +458,6 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { e = echo } c.reset(r, w, e) - if h == nil { - h = e.notFoundHandler - } // Chain middleware with handler in the end for i := len(e.middleware) - 1; i >= 0; i-- { @@ -423,7 +472,7 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { e.pool.Put(c) } -// Server returns the internal *http.Server +// Server returns the internal *http.Server. func (e *Echo) Server(addr string) *http.Server { s := &http.Server{Addr: addr} s.Handler = e @@ -446,13 +495,13 @@ func (e *Echo) RunTLS(addr, certFile, keyFile string) { } // RunServer runs a custom server. -func (e *Echo) RunServer(srv *http.Server) { - e.run(srv) +func (e *Echo) RunServer(s *http.Server) { + e.run(s) } // RunTLSServer runs a custom server with TLS configuration. -func (e *Echo) RunTLSServer(srv *http.Server, certFile, keyFile string) { - e.run(srv, certFile, keyFile) +func (e *Echo) RunTLSServer(s *http.Server, certFile, keyFile string) { + e.run(s, certFile, keyFile) } func (e *Echo) run(s *http.Server, files ...string) { @@ -489,7 +538,7 @@ func (e *HTTPError) Error() string { return e.message } -// wraps middleware +// wrapMiddleware wraps middleware. func wrapMiddleware(m Middleware) MiddlewareFunc { switch m := m.(type) { case MiddlewareFunc: @@ -520,7 +569,7 @@ func wrapMiddleware(m Middleware) MiddlewareFunc { } } -// Wraps HandlerFunc middleware +// wrapHandlerFuncMW wraps HandlerFunc middleware. func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc { return func(h HandlerFunc) HandlerFunc { return func(c *Context) error { @@ -532,7 +581,7 @@ func wrapHandlerFuncMW(m HandlerFunc) MiddlewareFunc { } } -// Wraps http.HandlerFunc middleware +// wrapHTTPHandlerFuncMW wraps http.HandlerFunc middleware. func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc { return func(h HandlerFunc) HandlerFunc { return func(c *Context) error { @@ -544,7 +593,7 @@ func wrapHTTPHandlerFuncMW(m http.HandlerFunc) MiddlewareFunc { } } -// wraps handler +// wrapHandler wraps handler. func wrapHandler(h Handler) HandlerFunc { switch h := h.(type) { case HandlerFunc: @@ -566,6 +615,13 @@ func wrapHandler(h Handler) HandlerFunc { } } -func init() { - log.SetOutput(colorable.NewColorableStdout()) +func (binder) Bind(r *http.Request, i interface{}) (err error) { + ct := r.Header.Get(ContentType) + err = UnsupportedMediaType + if strings.HasPrefix(ct, ApplicationJSON) { + err = json.NewDecoder(r.Body).Decode(i) + } else if strings.HasPrefix(ct, ApplicationXML) { + err = xml.NewDecoder(r.Body).Decode(i) + } + return } diff --git a/echo_test.go b/echo_test.go index 1b4279c1..aec0b056 100644 --- a/echo_test.go +++ b/echo_test.go @@ -18,8 +18,8 @@ import ( type ( user struct { - ID string `json:"id"` - Name string `json:"name"` + ID string `json:"id" xml:"id"` + Name string `json:"name" xml:"name"` } ) @@ -33,8 +33,8 @@ func TestEcho(t *testing.T) { assert.NotNil(t, e.Router()) // Debug - e.SetDebug(true) - assert.True(t, e.Debug()) + e.Debug() + assert.True(t, e.debug) // DefaultHTTPErrorHandler e.DefaultHTTPErrorHandler(errors.New("error"), c) @@ -246,6 +246,20 @@ func TestEchoTrace(t *testing.T) { testMethod(t, TRACE, "/", e) } +func TestEchoAny(t *testing.T) { // JFC + e := New() + e.Any("/", func(c *Context) error { + return c.String(http.StatusOK, "Any") + }) +} + +func TestEchoMatch(t *testing.T) { // JFC + e := New() + e.Match([]string{GET, POST}, "/", func(c *Context) error { + return c.String(http.StatusOK, "Match") + }) +} + func TestEchoWebSocket(t *testing.T) { e := New() e.WebSocket("/ws", func(c *Context) error { @@ -368,6 +382,14 @@ func TestEchoNotFound(t *testing.T) { assert.Equal(t, http.StatusNotFound, w.Code) } +func TestEchoBadRequest(t *testing.T) { + e := New() + r, _ := http.NewRequest("INVALID", "/files", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, r) + assert.Equal(t, http.StatusBadRequest, w.Code) +} + func TestEchoHTTPError(t *testing.T) { m := http.StatusText(http.StatusBadRequest) he := NewHTTPError(http.StatusBadRequest, m) @@ -381,12 +403,20 @@ func TestEchoServer(t *testing.T) { assert.IsType(t, &http.Server{}, s) } +func TestStripTrailingSlash(t *testing.T) { + e := New() + e.StripTrailingSlash() + r, _ := http.NewRequest(GET, "/users/", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, r) + assert.Equal(t, http.StatusNotFound, w.Code) +} + func testMethod(t *testing.T, method, path string, e *Echo) { m := fmt.Sprintf("%c%s", method[0], strings.ToLower(method[1:])) p := reflect.ValueOf(path) h := reflect.ValueOf(func(c *Context) error { - c.String(http.StatusOK, method) - return nil + return c.String(http.StatusOK, method) }) i := interface{}(e) reflect.ValueOf(i).MethodByName(m).Call([]reflect.Value{p, h}) diff --git a/examples/middleware/server.go b/examples/middleware/server.go index 503210c3..98fa0d75 100644 --- a/examples/middleware/server.go +++ b/examples/middleware/server.go @@ -17,7 +17,7 @@ func main() { e := echo.New() // Debug mode - e.SetDebug(true) + e.Debug() //------------ // Middleware @@ -37,16 +37,6 @@ func main() { return false })) - //------- - // Slash - //------- - - e.Use(mw.StripTrailingSlash()) - - // or - - // e.Use(mw.RedirectToSlash()) - // Gzip e.Use(mw.Gzip()) diff --git a/examples/websocket/server.go b/examples/websocket/server.go deleted file mode 100644 index 1b8894b2..00000000 --- a/examples/websocket/server.go +++ /dev/null @@ -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") -} diff --git a/middleware/auth.go b/middleware/auth.go index 9f471084..c47890e8 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -2,25 +2,22 @@ package middleware import ( "encoding/base64" - "github.com/dgrijalva/jwt-go" - "github.com/labstack/echo" "net/http" + + "github.com/labstack/echo" ) type ( BasicValidateFunc func(string, string) bool - JWTValidateFunc func(string, jwt.SigningMethod) ([]byte, error) ) const ( - Basic = "Basic" - Bearer = "Bearer" + Basic = "Basic" ) // BasicAuth returns an HTTP basic authentication middleware. // // For valid credentials it calls the next handler. -// For invalid Authorization header it sends "404 - Bad Request" response. // For invalid credentials, it sends "401 - Unauthorized" response. func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc { return func(c *echo.Context) error { @@ -31,7 +28,6 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc { auth := c.Request().Header.Get(echo.Authorization) l := len(Basic) - he := echo.NewHTTPError(http.StatusBadRequest) if len(auth) > l+1 && auth[:l] == Basic { b, err := base64.StdEncoding.DecodeString(auth[l+1:]) @@ -43,47 +39,11 @@ func BasicAuth(fn BasicValidateFunc) echo.HandlerFunc { if fn(cred[:i], cred[i+1:]) { return nil } - he.SetCode(http.StatusUnauthorized) } } } } - return he - } -} - -// JWTAuth returns a JWT authentication middleware. -// -// For valid token it sets JWT claims in the context with key `_claims` and calls -// the next handler. -// For invalid Authorization header it sends "404 - Bad Request" response. -// For invalid credentials, it sends "401 - Unauthorized" response. -func JWTAuth(fn JWTValidateFunc) echo.HandlerFunc { - return func(c *echo.Context) error { - // Skip WebSocket - if (c.Request().Header.Get(echo.Upgrade)) == echo.WebSocket { - return nil - } - - auth := c.Request().Header.Get("Authorization") - l := len(Bearer) - he := echo.NewHTTPError(http.StatusBadRequest) - - if len(auth) > l+1 && auth[:l] == Bearer { - t, err := jwt.Parse(auth[l+1:], func(token *jwt.Token) (interface{}, error) { - // Lookup key and verify method - if kid := token.Header["kid"]; kid != nil { - return fn(kid.(string), token.Method) - } - return fn("", token.Method) - }) - if err == nil && t.Valid { - c.Set("_claims", t.Claims) - return nil - } else { - he.SetCode(http.StatusUnauthorized) - } - } - return he + c.Response().Header().Set(echo.WWWAuthenticate, Basic + " realm=Restricted") + return echo.NewHTTPError(http.StatusUnauthorized) } } diff --git a/middleware/auth_test.go b/middleware/auth_test.go index d251be19..278a75ef 100644 --- a/middleware/auth_test.go +++ b/middleware/auth_test.go @@ -3,13 +3,11 @@ package middleware import ( "encoding/base64" "net/http" + "net/http/httptest" "testing" - "github.com/dgrijalva/jwt-go" "github.com/labstack/echo" "github.com/stretchr/testify/assert" - "net/http/httptest" - "time" ) func TestBasicAuth(t *testing.T) { @@ -38,80 +36,22 @@ func TestBasicAuth(t *testing.T) { req.Header.Set(echo.Authorization, auth) he := ba(c).(*echo.HTTPError) assert.Equal(t, http.StatusUnauthorized, he.Code()) + assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate)) // Empty Authorization header req.Header.Set(echo.Authorization, "") he = ba(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) + assert.Equal(t, http.StatusUnauthorized, he.Code()) + assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate)) // Invalid Authorization header - auth = base64.StdEncoding.EncodeToString([]byte(" :secret")) + auth = base64.StdEncoding.EncodeToString([]byte("invalid")) req.Header.Set(echo.Authorization, auth) he = ba(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) - - // Invalid scheme - auth = "Base " + base64.StdEncoding.EncodeToString([]byte(" :secret")) - req.Header.Set(echo.Authorization, auth) - he = ba(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) + assert.Equal(t, http.StatusUnauthorized, he.Code()) + assert.Equal(t, Basic + " realm=Restricted", rec.Header().Get(echo.WWWAuthenticate)) // WebSocket c.Request().Header.Set(echo.Upgrade, echo.WebSocket) assert.NoError(t, ba(c)) } - -func TestJWTAuth(t *testing.T) { - req, _ := http.NewRequest(echo.GET, "/", nil) - rec := httptest.NewRecorder() - c := echo.NewContext(req, echo.NewResponse(rec), echo.New()) - key := []byte("key") - fn := func(kid string, method jwt.SigningMethod) ([]byte, error) { - return key, nil - } - ja := JWTAuth(fn) - token := jwt.New(jwt.SigningMethodHS256) - token.Claims["foo"] = "bar" - token.Claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - ts, err := token.SignedString(key) - assert.NoError(t, err) - - // Valid credentials - auth := Bearer + " " + ts - req.Header.Set(echo.Authorization, auth) - assert.NoError(t, ja(c)) - - //--------------------- - // Invalid credentials - //--------------------- - - // Expired token - token.Claims["exp"] = time.Now().Add(-time.Second).Unix() - ts, err = token.SignedString(key) - assert.NoError(t, err) - auth = Bearer + " " + ts - req.Header.Set(echo.Authorization, auth) - he := ja(c).(*echo.HTTPError) - assert.Equal(t, http.StatusUnauthorized, he.Code()) - - // Empty Authorization header - req.Header.Set(echo.Authorization, "") - he = ja(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) - - // Invalid Authorization header - auth = "token" - req.Header.Set(echo.Authorization, auth) - he = ja(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) - - // Invalid scheme - auth = "Bear token" - req.Header.Set(echo.Authorization, auth) - he = ja(c).(*echo.HTTPError) - assert.Equal(t, http.StatusBadRequest, he.Code()) - - // WebSocket - c.Request().Header.Set(echo.Upgrade, echo.WebSocket) - assert.NoError(t, ja(c)) -} diff --git a/middleware/recover_test.go b/middleware/recover_test.go index 003c7519..0dd2f5f9 100644 --- a/middleware/recover_test.go +++ b/middleware/recover_test.go @@ -11,7 +11,7 @@ import ( func TestRecover(t *testing.T) { e := echo.New() - e.SetDebug(true) + e.Debug() req, _ := http.NewRequest(echo.GET, "/", nil) rec := httptest.NewRecorder() c := echo.NewContext(req, echo.NewResponse(rec), e) diff --git a/middleware/slash.go b/middleware/slash.go deleted file mode 100644 index a768a8ec..00000000 --- a/middleware/slash.go +++ /dev/null @@ -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 - } -} diff --git a/middleware/slash_test.go b/middleware/slash_test.go deleted file mode 100644 index 59eaf238..00000000 --- a/middleware/slash_test.go +++ /dev/null @@ -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")) -} diff --git a/recipes/file-upload/public/index.html b/recipes/file-upload/public/index.html new file mode 100644 index 00000000..82f38b49 --- /dev/null +++ b/recipes/file-upload/public/index.html @@ -0,0 +1,16 @@ + + + + + File Upload + + +

Upload Files

+
+ Name:
+ Email:
+ Files:

+ +
+ + diff --git a/recipes/file-upload/server.go b/recipes/file-upload/server.go new file mode 100644 index 00000000..fef5c853 --- /dev/null +++ b/recipes/file-upload/server.go @@ -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") +} diff --git a/examples/grace/server.go b/recipes/graceful-shutdown/grace/server.go similarity index 68% rename from examples/grace/server.go rename to recipes/graceful-shutdown/grace/server.go index fee4ffae..9dbeb7d4 100644 --- a/examples/grace/server.go +++ b/recipes/graceful-shutdown/grace/server.go @@ -11,10 +11,8 @@ func main() { // Setup e := echo.New() e.Get("/", func(c *echo.Context) error { - c.String(http.StatusOK, "Six sick bricks tick") - return nil + return c.String(http.StatusOK, "Six sick bricks tick") }) - // Use github.com/facebookgo/grace/gracehttp gracehttp.Serve(e.Server(":1323")) } diff --git a/examples/graceful/server.go b/recipes/graceful-shutdown/graceful/server.go similarity index 69% rename from examples/graceful/server.go rename to recipes/graceful-shutdown/graceful/server.go index 2c862c92..3a872691 100644 --- a/examples/graceful/server.go +++ b/recipes/graceful-shutdown/graceful/server.go @@ -12,10 +12,8 @@ func main() { // Setup e := echo.New() e.Get("/", func(c *echo.Context) error { - c.String(http.StatusOK, "Sue sews rose on slow jor crows nose") - return nil + return c.String(http.StatusOK, "Sue sews rose on slow jor crows nose") }) - // Use github.com/tylerb/graceful graceful.ListenAndServe(e.Server(":1323"), 5*time.Second) } diff --git a/recipes/jwt-authentication/server.go b/recipes/jwt-authentication/server.go new file mode 100644 index 00000000..96db0bf8 --- /dev/null +++ b/recipes/jwt-authentication/server.go @@ -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") +} diff --git a/recipes/jwt-authentication/token/token.go b/recipes/jwt-authentication/token/token.go new file mode 100644 index 00000000..0e309ab1 --- /dev/null +++ b/recipes/jwt-authentication/token/token.go @@ -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) +} diff --git a/recipes/streaming-file-upload/public/index.html b/recipes/streaming-file-upload/public/index.html new file mode 100644 index 00000000..b5cfe81e --- /dev/null +++ b/recipes/streaming-file-upload/public/index.html @@ -0,0 +1,16 @@ + + + + + File Upload + + +

Upload Files

+
+ Name:
+ Email:
+ Files:

+ +
+ + diff --git a/recipes/streaming-file-upload/server.go b/recipes/streaming-file-upload/server.go new file mode 100644 index 00000000..6a826439 --- /dev/null +++ b/recipes/streaming-file-upload/server.go @@ -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") +} diff --git a/examples/stream/server.go b/recipes/streaming-response/server.go similarity index 93% rename from examples/stream/server.go rename to recipes/streaming-response/server.go index 3d88e37f..0dbb1808 100644 --- a/examples/stream/server.go +++ b/recipes/streaming-response/server.go @@ -29,7 +29,7 @@ var ( func main() { e := echo.New() - e.Get("/stream", func(c *echo.Context) error { + e.Get("/", func(c *echo.Context) error { c.Response().Header().Set(echo.ContentType, echo.ApplicationJSON) c.Response().WriteHeader(http.StatusOK) for _, l := range locations { diff --git a/recipes/subdomains/server.go b/recipes/subdomains/server.go new file mode 100644 index 00000000..2da3cf26 --- /dev/null +++ b/recipes/subdomains/server.go @@ -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) +} diff --git a/recipes/websocket/public/index.html b/recipes/websocket/public/index.html new file mode 100644 index 00000000..3a307896 --- /dev/null +++ b/recipes/websocket/public/index.html @@ -0,0 +1,36 @@ + + + + + WebSocket + + +

+ + + + diff --git a/recipes/websocket/server.go b/recipes/websocket/server.go new file mode 100644 index 00000000..b98acb85 --- /dev/null +++ b/recipes/websocket/server.go @@ -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") +} diff --git a/response.go b/response.go index 84de7c9c..715df158 100644 --- a/response.go +++ b/response.go @@ -80,3 +80,8 @@ func (r *Response) reset(w http.ResponseWriter) { r.status = http.StatusOK r.committed = false } + +//func (r *Response) clear() { +// r.Header().Del(ContentType) +// r.committed = false +//} diff --git a/router.go b/router.go index 70ca7595..82a57282 100644 --- a/router.go +++ b/router.go @@ -4,9 +4,17 @@ import "net/http" type ( Router struct { - trees [21]*node - routes []Route - echo *Echo + connectTree *node + deleteTree *node + getTree *node + headTree *node + optionsTree *node + patchTree *node + postTree *node + putTree *node + traceTree *node + routes []Route + echo *Echo } node struct { typ ntype @@ -28,19 +36,20 @@ const ( mtype ) -func NewRouter(e *Echo) (r *Router) { - r = &Router{ - // trees: make(map[string]*node), - routes: []Route{}, - echo: e, +func NewRouter(e *Echo) *Router { + return &Router{ + connectTree: new(node), + deleteTree: new(node), + getTree: new(node), + headTree: new(node), + optionsTree: new(node), + patchTree: new(node), + postTree: new(node), + putTree: new(node), + traceTree: new(node), + routes: []Route{}, + echo: e, } - for _, m := range methods { - r.trees[r.treeIndex(m)] = &node{ - prefix: "", - children: children{}, - } - } - return } func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { @@ -66,7 +75,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { } else if path[i] == '*' { r.insert(method, path[:i], nil, stype, nil, e) pnames = append(pnames, "_name") - r.insert(method, path[:i+1], h, mtype, pnames, e) + r.insert(method, path[:i + 1], h, mtype, pnames, e) return } } @@ -81,7 +90,10 @@ func (r *Router) insert(method, path string, h HandlerFunc, t ntype, pnames []st *e.maxParam = l } - cn := r.trees[r.treeIndex(method)] // Current node as root + cn := r.findTree(method) // Current node as root + if cn == nil { + panic("echo => invalid method") + } search := path for { @@ -200,21 +212,89 @@ func (n *node) findChildWithType(t ntype) *node { return nil } -func (r *Router) treeIndex(method string) uint8 { - if method[0] == 'P' { - return method[0]%10 + method[1] - 65 - } else { - return method[0] % 10 +func (r *Router) findTree(method string) (n *node) { + switch method[0] { + case 'G': // GET + m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24 + if m == 0x47455400 { + n = r.getTree + } + case 'P': // POST, PUT or PATCH + switch method[1] { + case 'O': // POST + m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 | + uint32(method[0]) << 24 + if m == 0x504f5354 { + n = r.postTree + } + case 'U': // PUT + m := uint32(method[2]) << 8 | uint32(method[1]) << 16 | uint32(method[0]) << 24 + if m == 0x50555400 { + n = r.putTree + } + case 'A': // PATCH + m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 | + uint64(method[1]) << 48 | uint64(method[0]) << 56 + if m == 0x5041544348000000 { + n = r.patchTree + } + } + case 'D': // DELETE + m := uint64(method[5]) << 16 | uint64(method[4]) << 24 | uint64(method[3]) << 32 | + uint64(method[2]) << 40 | uint64(method[1]) << 48 | uint64(method[0]) << 56 + if m == 0x44454c4554450000 { + n = r.deleteTree + } + case 'C': // CONNECT + m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 | + uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 | + uint64(method[0]) << 56 + if m == 0x434f4e4e45435400 { + n = r.connectTree + } + case 'H': // HEAD + m := uint32(method[3]) | uint32(method[2]) << 8 | uint32(method[1]) << 16 | + uint32(method[0]) << 24 + if m == 0x48454144 { + n = r.headTree + } + case 'O': // OPTIONS + m := uint64(method[6]) << 8 | uint64(method[5]) << 16 | uint64(method[4]) << 24 | + uint64(method[3]) << 32 | uint64(method[2]) << 40 | uint64(method[1]) << 48 | + uint64(method[0]) << 56 + if m == 0x4f5054494f4e5300 { + n = r.optionsTree + } + case 'T': // TRACE + m := uint64(method[4]) << 24 | uint64(method[3]) << 32 | uint64(method[2]) << 40 | + uint64(method[1]) << 48 | uint64(method[0]) << 56 + if m == 0x5452414345000000 { + n = r.traceTree + } } + return } func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo) { - cn := r.trees[r.treeIndex(method)] // Current node as root - search := path + h = notFoundHandler + cn := r.findTree(method) // Current node as root + if cn == nil { + h = badRequestHandler + return + } + + // Strip trailing slash + if r.echo.stripTrailingSlash { + l := len(path) + if path[l - 1] == '/' { + path = path[:l - 1] + } + } var ( + search = path c *node // Child node - n int // Param counter + n int // Param counter nt ntype // Next type nn *node // Next node ns string // Next search @@ -225,10 +305,12 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo // Search order static > param > match-any for { if search == "" { - // Found - ctx.pnames = cn.pnames - h = cn.handler - e = cn.echo + if cn.handler != nil { + // Found + ctx.pnames = cn.pnames + h = cn.handler + e = cn.echo + } return } @@ -287,7 +369,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo } // Param node - Param: + Param: c = cn.findChildWithType(ptype) if c != nil { // Save next @@ -307,7 +389,7 @@ func (r *Router) Find(method, path string, ctx *Context) (h HandlerFunc, e *Echo } // Match-any node - MatchAny: + MatchAny: // c = cn.getChild() c = cn.findChildWithType(mtype) if c != nil { @@ -326,10 +408,8 @@ func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := r.echo.pool.Get().(*Context) h, _ := r.Find(req.Method, req.URL.Path, c) c.reset(req, w, r.echo) - if h == nil { - c.Error(NewHTTPError(http.StatusNotFound)) - } else { - h(c) + if err := h(c); err != nil { + r.echo.httpErrorHandler(err, c) } r.echo.pool.Put(c) } diff --git a/router_test.go b/router_test.go index caaadd74..a9a622c8 100644 --- a/router_test.go +++ b/router_test.go @@ -316,9 +316,6 @@ func TestRouterTwoParam(t *testing.T) { assert.Equal(t, "1", c.P(0)) assert.Equal(t, "1", c.P(1)) } - - h, _ = r.Find(GET, "/users/1", c) - assert.Nil(t, h) } func TestRouterMatchAny(t *testing.T) { @@ -384,7 +381,10 @@ func TestRouterMultiRoute(t *testing.T) { // Route > /user h, _ = r.Find(GET, "/user", c) - assert.Nil(t, h) + if assert.IsType(t, new(HTTPError), h(c)) { + he := h(c).(*HTTPError) + assert.Equal(t, http.StatusNotFound, he.code) + } } func TestRouterPriority(t *testing.T) { @@ -537,6 +537,16 @@ func TestRouterAPI(t *testing.T) { } } +func TestRouterAddInvalidMethod(t *testing.T) { + e := New() + r := e.router + assert.Panics(t, func() { + r.Add("INVALID", "/", func(*Context) error { + return nil + }, e) + }) +} + func TestRouterServeHTTP(t *testing.T) { e := New() r := e.router diff --git a/test/fixture/walle.png b/test/fixture/walle.png new file mode 100644 index 00000000..493985d4 Binary files /dev/null and b/test/fixture/walle.png differ diff --git a/website/docs/guide.md b/website/docs/guide.md index a20bd7a9..790b5e8b 100644 --- a/website/docs/guide.md +++ b/website/docs/guide.md @@ -23,7 +23,7 @@ $ go get -u github.com/labstack/echo ``` Echo follows [semantic versioning](http://semver.org) managed through GitHub releases. -Specific version of Echo can be installed using any [package manager](https://github.com/avelino/awesome-go#package-management). +Specific version of Echo can be installed using a [package manager](https://github.com/avelino/awesome-go#package-management). ## Customization @@ -42,54 +42,45 @@ and message `HTTPError.Message`. ### Debug -`Echo.SetDebug(on bool)` +`Echo.Debug()` Enables debug mode. +### Disable colored log + +`Echo.DisableColoredLog()` + +### StripTrailingSlash + +StripTrailingSlash enables removing trailing slash from the request path. + +`e.StripTrailingSlash()` + ## Routing Echo's router is [fast, optimized](https://github.com/labstack/echo#benchmark) and -flexible. It's based on [redix tree](http://en.wikipedia.org/wiki/Radix_tree) -data structure which makes routing lookup really fast. It leverages -[sync pool](https://golang.org/pkg/sync/#Pool) to reuse memory and achieve -zero dynamic memory allocation with no GC overhead. +flexible. It's based on [radix tree](http://en.wikipedia.org/wiki/Radix_tree) data +structure which makes route lookup really fast. Router leverages [sync pool](https://golang.org/pkg/sync/#Pool) +to reuse memory and achieve zero dynamic memory allocation with no GC overhead. Routes can be registered by specifying HTTP method, path and a handler. For example, code below registers a route for method `GET`, path `/hello` and a handler which sends `Hello!` HTTP response. ```go -echo.Get("/hello", func(c *echo.Context) error { +e.Get("/hello", func(c *echo.Context) error { return c.String(http.StatusOK, "Hello!") }) ``` -Echo's default handler is `func(*echo.Context) error` where `echo.Context` -primarily holds HTTP request and response objects. Echo also has a support for other -types of handlers. - -### Path parameter - -Request path parameters can be extracted either by name `Echo.Context.Param(name string) string` -or by index `Echo.Context.P(i int) string`. Getting parameter by index gives a -slightly better performance. - -```go -echo.Get("/users/:id", func(c *echo.Context) error { - // By name - id := c.Param("id") - - // By index - id := c.P(0) - - return c.String(http.StatusOK, id) -}) -``` +Echo's default handler is `func(*echo.Context) error` where `echo.Context` primarily +holds HTTP request and response objects. Echo also has a support for other types +of handlers. ### Match-any Matches zero or more characters in the path. For example, pattern `/users/*` will -match +match: - `/users/` - `/users/1` @@ -118,13 +109,13 @@ e.Get("/users/1/files/*", func(c *echo.Context) error { }) ``` -Above routes would resolve in order +Above routes would resolve in the following order: - `/users/new` - `/users/:id` - `/users/1/files/*` -Routes can be written in any order. +> Routes can be written in any order. ### Group @@ -150,15 +141,15 @@ e.Use(mw.BasicAuth(func(usr, pwd string) bool { ### URI building -`Echo.URI` can be used generate URI for any handler with specified path parameters. +`Echo.URI` can be used to generate URI for any handler with specified path parameters. It's helpful to centralize all your URI patterns which ease in refactoring your application. -`echo.URI(h, 1)` will generate `/users/1` for the route registered below +`e.URI(h, 1)` will generate `/users/1` for the route registered below ```go // Handler -h := func(*echo.Context) error { +h := func(c *echo.Context) error { return c.String(http.StatusOK, "OK") } @@ -168,9 +159,9 @@ e.Get("/users/:id", h) ## Middleware -Middleware is function which is chained in the HTTP request-response cycle. Middleware +Middleware is a function which is chained in the HTTP request-response cycle. Middleware has access to the request and response objects which it utilizes to perform a specific -action for example, logging every request. Echo supports variety of [middleware](/#features). +action, for example, logging every request. ### Logger @@ -195,7 +186,7 @@ BasicAuth middleware provides an HTTP basic authentication. *Example* ```go -echo.Group("/admin") +e.Group("/admin") e.Use(mw.BasicAuth(func(usr, pwd string) bool { if usr == "joe" && pwd == "secret" { return true @@ -225,63 +216,163 @@ to the centralized [HTTPErrorHandler](#error-handling). e.Use(mw.Recover()) ``` -### StripTrailingSlash +[Examples](https://github.com/labstack/echo/tree/master/examples/middleware) -StripTrailingSlash middleware removes the trailing slash from request path. +## Request + +### Path parameter + +Path parameter can be retrieved either by name `Context.Param(name string) string` +or by index `Context.P(i int) string`. Getting parameter by index gives a slightly +better performance. *Example* ```go -e.Use(mw.StripTrailingSlash()) +e.Get("/users/:name", func(c *echo.Context) error { + // By name + name := c.Param("name") + + // By index + name := c.P(0) + + return c.String(http.StatusOK, name) +}) ``` -### RedirectToSlash -RedirectToSlash middleware redirects requests without trailing slash path to trailing -slash path. +```sh +$ curl http://localhost:1323/users/joe +``` + +### Query parameter + +Query parameter can be retrieved by name using `Context.Query(name string)`. + +*Example* -*Options* ```go -RedirectToSlashOptions struct { - Code int +e.Get("/users", func(c *echo.Context) error { + name := c.Query("name") + return c.String(http.StatusOK, name) +}) +``` + +```sh +$ curl -G -d "name=joe" http://localhost:1323/users +``` + +### Form parameter + +Form parameter can be retrieved by name using `Context.Form(name string)`. + +*Example* + +```go +e.Post("/users", func(c *echo.Context) error { + name := c.Form("name") + return c.String(http.StatusOK, name) +}) +``` + +```sh +$ curl -d "name=joe" http://localhost:1323/users +``` + +## Response + +### Template + +```go +Context.Render(code int, name string, data interface{}) error +``` +Renders a template with data and sends a text/html response with status code. Templates +can be registered using `Echo.SetRenderer()`, allowing us to use any template engine. + +Below is an example using Go `html/template` + +- Implement `echo.Render` interface + +```go +Template struct { + templates *template.Template +} + +func (t *Template) Render(w io.Writer, name string, data interface{}) error { + return t.templates.ExecuteTemplate(w, name, data) } ``` -*Example* +- Pre-compile templates -```go -e.Use(mw.RedirectToSlash()) +`public/views/hello.html` + +```html +{{define "hello"}}Hello, {{.}}!{{end}} ``` -> StripTrailingSlash and RedirectToSlash middleware should not be used together. +```go +t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), +} +``` -[Examples](https://github.com/labstack/echo/tree/master/examples/middleware) +- Register templates -## Response +```go +e := echo.New() +e.SetRenderer(t) +e.Get("/hello", Hello) +``` + +- Render template + +```go +func Hello(c *echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") +} +``` ### JSON ```go -context.JSON(code int, v interface{}) error +Context.JSON(code int, v interface{}) error ``` Sends a JSON HTTP response with status code. -### String +### XML ```go -context.String(code int, s string) error +Context.XML(code int, v interface{}) error ``` -Sends a text/plain HTTP response with status code. +Sends an XML HTTP response with status code. ### HTML ```go -func (c *Context) HTML(code int, html string) error +Context.HTML(code int, html string) error ``` Sends an HTML HTTP response with status code. +### String + +```go +Context.String(code int, s string) error +``` + +Sends a text/plain HTTP response with status code. + +### File + +```go +Context.File(name string, attachment bool) error +``` + +File sends a response with the content of the file. If attachment is `true`, the client +is prompted to save the file. + ### Static files `Echo.Static(path, root string)` serves static files. For example, code below serves @@ -360,7 +451,3 @@ func welcome(c *echo.Context) error { ``` See how [HTTPErrorHandler](#customization) handles it. - -## Deployment - -*WIP* diff --git a/website/docs/index.md b/website/docs/index.md index 1f2f633a..0bd0bbac 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -1,13 +1,9 @@ # Echo -Build simple and performant systems! +A fast and unfancy micro web framework for Go. --- -## Overview - -Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framework in Go. - ## Features - Fast HTTP router which smartly prioritize routes. @@ -27,14 +23,25 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew - `http.HandlerFunc` - `func(http.ResponseWriter, *http.Request)` - Sub-router/Groups -- Handy encoding/decoding functions. +- Handy functions to send variety of HTTP response: + - HTML + - HTML via templates + - String + - JSON + - JSONP + - XML + - File + - NoContent + - Redirect + - Error - Build-in support for: + - Favicon + - Index file - Static files - WebSocket -- API to serve index and favicon. - Centralized HTTP error handling. -- Customizable request binding function. -- Customizable response rendering function, allowing you to use any HTML template engine. +- Customizable HTTP request binding function. +- Customizable HTTP response rendering function, allowing you to use any HTML template engine. ## Performance @@ -48,9 +55,9 @@ Echo is a fast HTTP router (zero dynamic memory allocation) and micro web framew $ go get github.com/labstack/echo ``` -###[Hello, World!](https://github.com/labstack/echo/tree/master/examples/hello) +### Hello, World! -Create `server.go` with the following content +Create `server.go` ```go package main @@ -83,24 +90,7 @@ func main() { } ``` -`echo.New()` returns a new instance of Echo. - -`e.Use(mw.Logger())` adds logging middleware to the chain. It logs every HTTP request -made to the server, producing output - -```sh -2015/06/07 18:16:16 GET / 200 13.238µs 14 -``` - -`e.Get("/", hello)` Registers hello handler for HTTP method `GET` and path `/`, so -whenever server receives an HTTP request at `/`, hello function is called. - -In hello handler `c.String(http.StatusOK, "Hello, World!\n")` sends a text/plain -HTTP response to the client with 200 status code. - -`e.Run(":1323")` Starts HTTP server at network address `:1323`. - -Now start the server using command +Start server ```sh $ go run server.go @@ -110,7 +100,7 @@ Browse to [http://localhost:1323](http://localhost:1323) and you should see Hello, World! on the page. ### Next? -- Browse [examples](https://github.com/labstack/echo/tree/master/examples) +- Browse [recipes](https://github.com/labstack/echo/tree/master/recipes) - Head over to [Guide](guide.md) ## Contribute diff --git a/website/docs/recipes/file-upload.md b/website/docs/recipes/file-upload.md new file mode 100644 index 00000000..a8628ac5 --- /dev/null +++ b/website/docs/recipes/file-upload.md @@ -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 + + + + + File Upload + + +

Upload Files

+
+ Name:
+ Email:
+ Files:

+ +
+ + + +``` + +## [Source Code](https://github.com/labstack/echo/blob/master/recipes/file-upload) diff --git a/website/docs/recipes/graceful-shutdown.md b/website/docs/recipes/graceful-shutdown.md new file mode 100644 index 00000000..ed0134b1 --- /dev/null +++ b/website/docs/recipes/graceful-shutdown.md @@ -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) diff --git a/website/docs/recipes/jwt-authentication.md b/website/docs/recipes/jwt-authentication.md new file mode 100644 index 00000000..59efe776 --- /dev/null +++ b/website/docs/recipes/jwt-authentication.md @@ -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 " => Access granted with JWT. +``` + +## [Source Code](https://github.com/labstack/echo/blob/master/recipes/jwt-authentication) + + + + + diff --git a/website/docs/recipes/streaming-file-upload.md b/website/docs/recipes/streaming-file-upload.md new file mode 100644 index 00000000..8d9699fe --- /dev/null +++ b/website/docs/recipes/streaming-file-upload.md @@ -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 + + + + + File Upload + + +

Upload Files

+
+ Name:
+ Email:
+ Files:

+ +
+ + + +``` + +## [Source Code](https://github.com/labstack/echo/blob/master/recipes/streaming-file-upload) + diff --git a/website/docs/recipes/streaming-response.md b/website/docs/recipes/streaming-response.md new file mode 100644 index 00000000..7aa77cf8 --- /dev/null +++ b/website/docs/recipes/streaming-response.md @@ -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) + diff --git a/website/docs/recipes/subdomains.md b/website/docs/recipes/subdomains.md new file mode 100644 index 00000000..ea4e7246 --- /dev/null +++ b/website/docs/recipes/subdomains.md @@ -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) diff --git a/website/docs/recipes/websocket.md b/website/docs/recipes/websocket.md new file mode 100644 index 00000000..78f19478 --- /dev/null +++ b/website/docs/recipes/websocket.md @@ -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 + + + + + WebSocket + + +

+ + + + +``` + +## 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) + diff --git a/website/echo/base.html b/website/echo/base.html index 4f0260c1..75df7a29 100644 --- a/website/echo/base.html +++ b/website/echo/base.html @@ -10,11 +10,11 @@ {% if favicon %} {% else %}{% endif %} - {% if page_title %}{{ page_title }} - {% endif %}{{ site_name }} + {% if page_title %}{{ page_title }} - {% endif %}{{ config.extra.site_title }} - + {%- for path in extra_css %} @@ -75,4 +75,4 @@ {%- endfor %} - \ No newline at end of file + diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 13d59c70..4378cfcf 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -1,11 +1,19 @@ site_name: Echo - -theme: journal - +theme: flatly theme_dir: echo - copyright: '© 2015 LabStack' - repo_url: https://github.com/labstack/echo - google_analytics: ['UA-51208124-3', 'auto'] +pages: + - Home: index.md + - Guide: guide.md + - Recipes: + - File Upload: recipes/file-upload.md + - Streaming File Upload: recipes/streaming-file-upload.md + - Streaming Response: recipes/streaming-response.md + - WebSocket: recipes/websocket.md + - Subdomains: recipes/subdomains.md + - JWT Authentication: recipes/jwt-authentication.md + - Graceful Shutdown: recipes/graceful-shutdown.md +extra: + site_title: Echo, a fast and unfancy micro web framework for Go.