diff --git a/.gitignore b/.gitignore index 35a319bb2..eafd2badb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ # Website website/public -website/Makefile - -# Node.js -node_modules # IntelliJ .idea diff --git a/README.md b/README.md index 3d5e00c9f..53f82c2e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# [Echo](http://labstack.com/echo) [![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) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) +# [Echo v2](http://echo.labstack.com/v2) [![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) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) + +## Don't forget to try the upcoming [v3](https://github.com/labstack/echo/tree/v3) tracked [here]( https://github.com/labstack/echo/issues/665) #### Fast and unfancy HTTP server framework for Go (Golang). Up to 10x faster than the rest. @@ -32,10 +34,14 @@ ### Installation +Echo is developed and tested using Go `1.6.x` and `1.7.x` + ```sh $ go get -u github.com/labstack/echo ``` +> Ideally, you should rely on a [package manager](https://github.com/avelino/awesome-go#package-management) like glide or govendor to use a specific [version](https://github.com/labstack/echo/releases) of Echo. + ### Hello, World! Create `server.go` @@ -79,24 +85,32 @@ e.DELETE("/users/:id", deleteUser) ### Path Parameters ```go +// e.GET("/users/:id", getUser) func getUser(c echo.Context) error { // User ID from path `users/:id` id := c.Param("id") + return c.String(http.StatusOK, id) } ``` +Browse to http://localhost:1323/users/Joe and you should see 'Joe' on the page. + ### Query Parameters `/show?team=x-men&member=wolverine` ```go +//e.GET("/show", show) func show(c echo.Context) error { // Get team and member from the query string team := c.QueryParam("team") member := c.QueryParam("member") + return c.String(http.StatusOK, "team:" + team + ", member:" + member) } ``` +Browse to http://localhost:1323/show?team=x-men&member=wolverine and you should see 'team:x-men, member:wolverine' on the page. + ### Form `application/x-www-form-urlencoded` `POST` `/save` @@ -106,15 +120,21 @@ name | value name | Joe Smith email | joe@labstack.com - ```go +// e.POST("/save", save) func save(c echo.Context) error { // Get name and email name := c.FormValue("name") email := c.FormValue("email") + return c.String(http.StatusOK, "name:" + name + ", email:" + email) } ``` +Run the following command. +```sh +$ curl -F "name=Joe Smith" -F "email=joe@labstack.com" http://localhost:1323/save +// => name:Joe Smith, email:joe@labstack.com +``` ### Form `multipart/form-data` `POST` `/save` @@ -122,14 +142,13 @@ func save(c echo.Context) error { name | value :--- | :--- name | Joe Smith -email | joe@labstack.com avatar | avatar ```go +// e.POST("/save", save) func save(c echo.Context) error { - // Get name and email + // Get name name := c.FormValue("name") - email := c.FormValue("email") // Get avatar avatar, err := c.FormFile("avatar") if err != nil { @@ -155,10 +174,23 @@ func save(c echo.Context) error { return err } - return c.HTML(http.StatusOK, "Thank you!") + return c.HTML(http.StatusOK, "Thank you! " + name + "") } ``` +Run the following command. +```sh +$ curl -F "name=Joe Smith" -F "avatar=@/path/to/your/avatar.png" http://localhost:1323/save +// => Thank you! Joe Smith +``` + +For checking uploaded image, run the following command. +```sh +cd +ls avatar.png +// => avatar.png +``` + ### Handling Request - Bind `JSON` or `XML` or `form` payload into Go struct based on `Content-Type` request header. diff --git a/context.go b/context.go index ebc2ac2e2..4939236f6 100644 --- a/context.go +++ b/context.go @@ -17,20 +17,18 @@ import ( "bytes" - "github.com/labstack/echo/context" + gcontext "github.com/labstack/echo/context" ) type ( // Context represents the context of the current HTTP request. It holds request and // response objects, path, path parameters, data and registered handler. Context interface { - context.Context - // StdContext returns `context.Context`. - StdContext() context.Context + StdContext() gcontext.Context // SetStdContext sets `context.Context`. - SetStdContext(context.Context) + SetStdContext(gcontext.Context) // Request returns `engine.Request` interface. Request() engine.Request @@ -187,63 +185,50 @@ type ( Reset(engine.Request, engine.Response) } - echoContext struct { - context context.Context - request engine.Request - response engine.Response - path string - pnames []string - pvalues []string - handler HandlerFunc - echo *Echo + context struct { + stdContext gcontext.Context + request engine.Request + response engine.Response + path string + pnames []string + pvalues []string + handler HandlerFunc + store store + echo *Echo } + + store map[string]interface{} ) const ( indexPage = "index.html" ) -func (c *echoContext) StdContext() context.Context { - return c.context -} - -func (c *echoContext) SetStdContext(ctx context.Context) { - c.context = ctx -} - -func (c *echoContext) Deadline() (deadline time.Time, ok bool) { - return c.context.Deadline() -} - -func (c *echoContext) Done() <-chan struct{} { - return c.context.Done() -} - -func (c *echoContext) Err() error { - return c.context.Err() +func (c *context) StdContext() gcontext.Context { + return c.stdContext } -func (c *echoContext) Value(key interface{}) interface{} { - return c.context.Value(key) +func (c *context) SetStdContext(ctx gcontext.Context) { + c.stdContext = ctx } -func (c *echoContext) Request() engine.Request { +func (c *context) Request() engine.Request { return c.request } -func (c *echoContext) Response() engine.Response { +func (c *context) Response() engine.Response { return c.response } -func (c *echoContext) Path() string { +func (c *context) Path() string { return c.path } -func (c *echoContext) SetPath(p string) { +func (c *context) SetPath(p string) { c.path = p } -func (c *echoContext) P(i int) (value string) { +func (c *context) P(i int) (value string) { l := len(c.pnames) if i < l { value = c.pvalues[i] @@ -251,7 +236,7 @@ func (c *echoContext) P(i int) (value string) { return } -func (c *echoContext) Param(name string) (value string) { +func (c *context) Param(name string) (value string) { l := len(c.pnames) for i, n := range c.pnames { if n == name && i < l { @@ -262,71 +247,74 @@ func (c *echoContext) Param(name string) (value string) { return } -func (c *echoContext) ParamNames() []string { +func (c *context) ParamNames() []string { return c.pnames } -func (c *echoContext) SetParamNames(names ...string) { +func (c *context) SetParamNames(names ...string) { c.pnames = names } -func (c *echoContext) ParamValues() []string { +func (c *context) ParamValues() []string { return c.pvalues } -func (c *echoContext) SetParamValues(values ...string) { +func (c *context) SetParamValues(values ...string) { c.pvalues = values } -func (c *echoContext) QueryParam(name string) string { +func (c *context) QueryParam(name string) string { return c.request.URL().QueryParam(name) } -func (c *echoContext) QueryParams() map[string][]string { +func (c *context) QueryParams() map[string][]string { return c.request.URL().QueryParams() } -func (c *echoContext) FormValue(name string) string { +func (c *context) FormValue(name string) string { return c.request.FormValue(name) } -func (c *echoContext) FormParams() map[string][]string { +func (c *context) FormParams() map[string][]string { return c.request.FormParams() } -func (c *echoContext) FormFile(name string) (*multipart.FileHeader, error) { +func (c *context) FormFile(name string) (*multipart.FileHeader, error) { return c.request.FormFile(name) } -func (c *echoContext) MultipartForm() (*multipart.Form, error) { +func (c *context) MultipartForm() (*multipart.Form, error) { return c.request.MultipartForm() } -func (c *echoContext) Cookie(name string) (engine.Cookie, error) { +func (c *context) Cookie(name string) (engine.Cookie, error) { return c.request.Cookie(name) } -func (c *echoContext) SetCookie(cookie engine.Cookie) { +func (c *context) SetCookie(cookie engine.Cookie) { c.response.SetCookie(cookie) } -func (c *echoContext) Cookies() []engine.Cookie { +func (c *context) Cookies() []engine.Cookie { return c.request.Cookies() } -func (c *echoContext) Set(key string, val interface{}) { - c.context = context.WithValue(c.context, key, val) +func (c *context) Set(key string, val interface{}) { + if c.store == nil { + c.store = make(store) + } + c.store[key] = val } -func (c *echoContext) Get(key string) interface{} { - return c.context.Value(key) +func (c *context) Get(key string) interface{} { + return c.store[key] } -func (c *echoContext) Bind(i interface{}) error { +func (c *context) Bind(i interface{}) error { return c.echo.binder.Bind(i, c) } -func (c *echoContext) Render(code int, name string, data interface{}) (err error) { +func (c *context) Render(code int, name string, data interface{}) (err error) { if c.echo.renderer == nil { return ErrRendererNotRegistered } @@ -340,21 +328,21 @@ func (c *echoContext) Render(code int, name string, data interface{}) (err error return } -func (c *echoContext) HTML(code int, html string) (err error) { +func (c *context) HTML(code int, html string) (err error) { c.response.Header().Set(HeaderContentType, MIMETextHTMLCharsetUTF8) c.response.WriteHeader(code) _, err = c.response.Write([]byte(html)) return } -func (c *echoContext) String(code int, s string) (err error) { +func (c *context) String(code int, s string) (err error) { c.response.Header().Set(HeaderContentType, MIMETextPlainCharsetUTF8) c.response.WriteHeader(code) _, err = c.response.Write([]byte(s)) return } -func (c *echoContext) JSON(code int, i interface{}) (err error) { +func (c *context) JSON(code int, i interface{}) (err error) { b, err := json.Marshal(i) if c.echo.Debug() { b, err = json.MarshalIndent(i, "", " ") @@ -365,11 +353,11 @@ func (c *echoContext) JSON(code int, i interface{}) (err error) { return c.JSONBlob(code, b) } -func (c *echoContext) JSONBlob(code int, b []byte) (err error) { +func (c *context) JSONBlob(code int, b []byte) (err error) { return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b) } -func (c *echoContext) JSONP(code int, callback string, i interface{}) (err error) { +func (c *context) JSONP(code int, callback string, i interface{}) (err error) { b, err := json.Marshal(i) if err != nil { return err @@ -377,7 +365,7 @@ func (c *echoContext) JSONP(code int, callback string, i interface{}) (err error return c.JSONPBlob(code, callback, b) } -func (c *echoContext) JSONPBlob(code int, callback string, b []byte) (err error) { +func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) { c.response.Header().Set(HeaderContentType, MIMEApplicationJavaScriptCharsetUTF8) c.response.WriteHeader(code) if _, err = c.response.Write([]byte(callback + "(")); err != nil { @@ -390,7 +378,7 @@ func (c *echoContext) JSONPBlob(code int, callback string, b []byte) (err error) return } -func (c *echoContext) XML(code int, i interface{}) (err error) { +func (c *context) XML(code int, i interface{}) (err error) { b, err := xml.Marshal(i) if c.echo.Debug() { b, err = xml.MarshalIndent(i, "", " ") @@ -401,28 +389,31 @@ func (c *echoContext) XML(code int, i interface{}) (err error) { return c.XMLBlob(code, b) } -func (c *echoContext) XMLBlob(code int, b []byte) (err error) { +func (c *context) XMLBlob(code int, b []byte) (err error) { + c.response.Header().Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) + c.response.WriteHeader(code) if _, err = c.response.Write([]byte(xml.Header)); err != nil { return } - return c.Blob(code, MIMEApplicationXMLCharsetUTF8, b) + _, err = c.response.Write(b) + return } -func (c *echoContext) Blob(code int, contentType string, b []byte) (err error) { +func (c *context) Blob(code int, contentType string, b []byte) (err error) { c.response.Header().Set(HeaderContentType, contentType) c.response.WriteHeader(code) _, err = c.response.Write(b) return } -func (c *echoContext) Stream(code int, contentType string, r io.Reader) (err error) { +func (c *context) Stream(code int, contentType string, r io.Reader) (err error) { c.response.Header().Set(HeaderContentType, contentType) c.response.WriteHeader(code) _, err = io.Copy(c.response, r) return } -func (c *echoContext) File(file string) error { +func (c *context) File(file string) error { f, err := os.Open(file) if err != nil { return ErrNotFound @@ -431,11 +422,12 @@ func (c *echoContext) File(file string) error { fi, _ := f.Stat() if fi.IsDir() { - file = filepath.Join(file, "index.html") + file = filepath.Join(file, indexPage) f, err = os.Open(file) if err != nil { return ErrNotFound } + defer f.Close() if fi, err = f.Stat(); err != nil { return err } @@ -443,15 +435,15 @@ func (c *echoContext) File(file string) error { return c.ServeContent(f, fi.Name(), fi.ModTime()) } -func (c *echoContext) Attachment(r io.ReadSeeker, name string) (err error) { +func (c *context) Attachment(r io.ReadSeeker, name string) (err error) { return c.contentDisposition(r, name, "attachment") } -func (c *echoContext) Inline(r io.ReadSeeker, name string) (err error) { +func (c *context) Inline(r io.ReadSeeker, name string) (err error) { return c.contentDisposition(r, name, "inline") } -func (c *echoContext) contentDisposition(r io.ReadSeeker, name, dispositionType string) (err error) { +func (c *context) contentDisposition(r io.ReadSeeker, name, dispositionType string) (err error) { c.response.Header().Set(HeaderContentType, ContentTypeByExtension(name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name)) c.response.WriteHeader(http.StatusOK) @@ -459,12 +451,12 @@ func (c *echoContext) contentDisposition(r io.ReadSeeker, name, dispositionType return } -func (c *echoContext) NoContent(code int) error { +func (c *context) NoContent(code int) error { c.response.WriteHeader(code) return nil } -func (c *echoContext) Redirect(code int, url string) error { +func (c *context) Redirect(code int, url string) error { if code < http.StatusMultipleChoices || code > http.StatusTemporaryRedirect { return ErrInvalidRedirectCode } @@ -473,27 +465,27 @@ func (c *echoContext) Redirect(code int, url string) error { return nil } -func (c *echoContext) Error(err error) { +func (c *context) Error(err error) { c.echo.httpErrorHandler(err, c) } -func (c *echoContext) Echo() *Echo { +func (c *context) Echo() *Echo { return c.echo } -func (c *echoContext) Handler() HandlerFunc { +func (c *context) Handler() HandlerFunc { return c.handler } -func (c *echoContext) SetHandler(h HandlerFunc) { +func (c *context) SetHandler(h HandlerFunc) { c.handler = h } -func (c *echoContext) Logger() log.Logger { +func (c *context) Logger() log.Logger { return c.echo.logger } -func (c *echoContext) ServeContent(content io.ReadSeeker, name string, modtime time.Time) error { +func (c *context) ServeContent(content io.ReadSeeker, name string, modtime time.Time) error { req := c.Request() res := c.Response() @@ -520,9 +512,10 @@ func ContentTypeByExtension(name string) (t string) { return } -func (c *echoContext) Reset(req engine.Request, res engine.Response) { - c.context = context.Background() +func (c *context) Reset(req engine.Request, res engine.Response) { + c.stdContext = gcontext.Background() c.request = req c.response = res + c.store = nil c.handler = NotFoundHandler } diff --git a/context_test.go b/context_test.go index f7f8fe789..a375d7797 100644 --- a/context_test.go +++ b/context_test.go @@ -11,10 +11,10 @@ import ( "text/template" "time" - "golang.org/x/net/context" - "strings" + gcontext "github.com/labstack/echo/context" + "net/url" "encoding/xml" @@ -37,7 +37,7 @@ func TestContext(t *testing.T) { e := New() req := test.NewRequest(POST, "/", strings.NewReader(userJSON)) rec := test.NewResponseRecorder() - c := e.NewContext(req, rec).(*echoContext) + c := e.NewContext(req, rec).(*context) // Echo assert.Equal(t, e, c.Echo()) @@ -71,7 +71,7 @@ func TestContext(t *testing.T) { // JSON rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) err = c.JSON(http.StatusOK, user{1, "Jon Snow"}) if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Status()) @@ -81,13 +81,13 @@ func TestContext(t *testing.T) { // JSON (error) rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) err = c.JSON(http.StatusOK, make(chan bool)) assert.Error(t, err) // JSONP rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) callback := "callback" err = c.JSONP(http.StatusOK, callback, user{1, "Jon Snow"}) if assert.NoError(t, err) { @@ -98,23 +98,23 @@ func TestContext(t *testing.T) { // XML rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) - err = c.XML(http.StatusOK, user{1, "Jon Snow"}) + c = e.NewContext(req, rec).(*context) + err = c.XML(http.StatusCreated, user{1, "Jon Snow"}) if assert.NoError(t, err) { - assert.Equal(t, http.StatusOK, rec.Status()) + assert.Equal(t, http.StatusCreated, rec.Status()) assert.Equal(t, MIMEApplicationXMLCharsetUTF8, rec.Header().Get(HeaderContentType)) assert.Equal(t, xml.Header+userXML, rec.Body.String()) } // XML (error) rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) err = c.XML(http.StatusOK, make(chan bool)) assert.Error(t, err) // String rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) err = c.String(http.StatusOK, "Hello, World!") if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Status()) @@ -124,7 +124,7 @@ func TestContext(t *testing.T) { // HTML rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) err = c.HTML(http.StatusOK, "Hello, World!") if assert.NoError(t, err) { assert.Equal(t, http.StatusOK, rec.Status()) @@ -134,7 +134,7 @@ func TestContext(t *testing.T) { // Stream rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) r := strings.NewReader("response from a stream") err = c.Stream(http.StatusOK, "application/octet-stream", r) if assert.NoError(t, err) { @@ -145,7 +145,7 @@ func TestContext(t *testing.T) { // Attachment rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) file, err := os.Open("_fixture/images/walle.png") if assert.NoError(t, err) { err = c.Attachment(file, "walle.png") @@ -158,7 +158,7 @@ func TestContext(t *testing.T) { // Inline rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) file, err = os.Open("_fixture/images/walle.png") if assert.NoError(t, err) { err = c.Inline(file, "walle.png") @@ -171,13 +171,13 @@ func TestContext(t *testing.T) { // NoContent rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) c.NoContent(http.StatusOK) assert.Equal(t, http.StatusOK, rec.Status()) // Error rec = test.NewResponseRecorder() - c = e.NewContext(req, rec).(*echoContext) + c = e.NewContext(req, rec).(*context) c.Error(errors.New("error")) assert.Equal(t, http.StatusInternalServerError, rec.Status()) @@ -193,7 +193,7 @@ func TestContextCookie(t *testing.T) { req.Header().Add(HeaderCookie, theme) req.Header().Add(HeaderCookie, user) rec := test.NewResponseRecorder() - c := e.NewContext(req, rec).(*echoContext) + c := e.NewContext(req, rec).(*context) // Read single cookie, err := c.Cookie("theme") @@ -234,12 +234,12 @@ func TestContextPath(t *testing.T) { e := New() r := e.Router() - r.Add(GET, "/users/:id", nil, e) + r.Add(GET, "/users/:id", nil) c := e.NewContext(nil, nil) r.Find(GET, "/users/1", c) assert.Equal(t, "/users/:id", c.Path()) - r.Add(GET, "/users/:uid/files/:fid", nil, e) + r.Add(GET, "/users/:uid/files/:fid", nil) c = e.NewContext(nil, nil) r.Find(GET, "/users/1/files/1", c) assert.Equal(t, "/users/:uid/files/:fid", c.Path()) @@ -351,23 +351,16 @@ func TestContextRedirect(t *testing.T) { assert.Error(t, c.Redirect(310, "http://labstack.github.io/echo")) } -func TestContextEmbedded(t *testing.T) { - var c Context - c = new(echoContext) - c.SetStdContext(context.WithValue(c, "key", "val")) - assert.Equal(t, "val", c.Value("key")) - now := time.Now() - ctx, _ := context.WithDeadline(context.Background(), now) - c.SetStdContext(ctx) - n, _ := ctx.Deadline() - assert.Equal(t, now, n) - assert.Equal(t, context.DeadlineExceeded, c.Err()) - assert.NotNil(t, c.Done()) +func TestStdContextEmbedded(t *testing.T) { + c := new(context) + sc := gcontext.WithValue(nil, "key", "val") + c.SetStdContext(sc) + assert.NotEqual(t, c, c.StdContext()) } func TestContextStore(t *testing.T) { var c Context - c = new(echoContext) + c = new(context) c.Set("name", "Jon Snow") assert.Equal(t, "Jon Snow", c.Get("name")) } @@ -407,7 +400,7 @@ func TestContextHandler(t *testing.T) { r.Add(GET, "/handler", func(Context) error { _, err := b.Write([]byte("handler")) return err - }, e) + }) c := e.NewContext(nil, nil) r.Find(GET, "/handler", c) c.Handler()(c) diff --git a/echo.go b/echo.go index a8e9c31c8..ac471a03c 100644 --- a/echo.go +++ b/echo.go @@ -48,8 +48,7 @@ import ( "runtime" "sync" - "golang.org/x/net/context" - + gcontext "github.com/labstack/echo/context" "github.com/labstack/echo/engine" "github.com/labstack/echo/log" glog "github.com/labstack/gommon/log" @@ -238,13 +237,14 @@ func New() (e *Echo) { // NewContext returns a Context instance. func (e *Echo) NewContext(req engine.Request, res engine.Response) Context { - return &echoContext{ - context: context.Background(), - request: req, - response: res, - echo: e, - pvalues: make([]string, *e.maxParam), - handler: NotFoundHandler, + return &context{ + stdContext: gcontext.Background(), + request: req, + response: res, + store: make(store), + echo: e, + pvalues: make([]string, *e.maxParam), + handler: NotFoundHandler, } } @@ -473,7 +473,7 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl h = middleware[i](h) } return h(c) - }, e) + }) r := Route{ Method: method, Path: path, @@ -541,15 +541,15 @@ func (e *Echo) ReleaseContext(c Context) { } func (e *Echo) ServeHTTP(req engine.Request, res engine.Response) { - c := e.pool.Get().(*echoContext) + c := e.pool.Get().(*context) c.Reset(req, res) // Middleware - h := func(Context) error { + h := func(c Context) error { method := req.Method() path := req.URL().Path() e.router.Find(method, path, c) - h := c.handler + h := c.Handler() for i := len(e.middleware) - 1; i >= 0; i-- { h = e.middleware[i](h) } diff --git a/echo_test.go b/echo_test.go index 677fd17b2..bf3f4aabd 100644 --- a/echo_test.go +++ b/echo_test.go @@ -333,7 +333,7 @@ func TestEchoHTTPError(t *testing.T) { func TestEchoContext(t *testing.T) { e := New() c := e.AcquireContext() - assert.IsType(t, new(echoContext), c) + assert.IsType(t, new(context), c) e.ReleaseContext(c) } diff --git a/engine/standard/server.go b/engine/standard/server.go index b681dc2dd..fb364860e 100644 --- a/engine/standard/server.go +++ b/engine/standard/server.go @@ -112,9 +112,7 @@ func (s *Server) Start() error { if s.config.TLSCertFile != "" && s.config.TLSKeyFile != "" { // TODO: https://github.com/golang/go/commit/d24f446a90ea94b87591bf16228d7d871fec3d92 - config := &tls.Config{ - NextProtos: []string{"http/1.1"}, - } + config := &tls.Config{} if !s.config.DisableHTTP2 { config.NextProtos = append(config.NextProtos, "h2") } diff --git a/glide.lock b/glide.lock index 552b29d15..b973a59fa 100644 --- a/glide.lock +++ b/glide.lock @@ -1,14 +1,39 @@ -hash: dcb2a2984f2e724f0419065a77a4c8a6a52539349e8994c347f0efdb1fca972c -updated: 2016-09-08T08:30:16.896450504-07:00 +hash: 72f3bb61d0485275144e593f73de0b18217afecac7eaea1bfe1147d106182eca +updated: 2016-10-20T09:17:44.443328154-07:00 imports: +- name: github.com/daaku/go.zipexe + version: a5fe2436ffcb3236e175e5149162b41cd28bd27d - name: github.com/davecgh/go-spew version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9 subpackages: - spew - name: github.com/dgrijalva/jwt-go version: 24c63f56522a87ec5339cc3567883f1039378fdb +- name: github.com/facebookgo/clock + version: 600d898af40aa09a7a93ecb9265d87b0504b6f03 +- name: github.com/facebookgo/grace + version: 5729e484473f52048578af1b80d0008c7024089b + subpackages: + - gracehttp + - gracenet +- name: github.com/facebookgo/httpdown + version: a3b1354551a26449fbe05f5d855937f6e7acbd71 +- name: github.com/facebookgo/stats + version: 1b76add642e42c6ffba7211ad7b3939ce654526e +- name: github.com/GeertJohan/go.rice + version: 9fdfd46f9806a9228aae341d65ab75c5235c383c + subpackages: + - embedded +- name: github.com/golang/protobuf + version: 98fa357170587e470c5f27d3c3ea0947b71eb455 + subpackages: + - proto +- name: github.com/gorilla/websocket + version: 5df680c89f2a84ad9cb16cf143557a0853cfaabc +- name: github.com/kardianos/osext + version: c2c54e542fb797ad986b31721e1baedf214ca413 - name: github.com/klauspost/compress - version: 08fe86a420401e830c24114bdf9f1ba91331407a + version: 1e658061989f47658e69492cf63a839630a25eba subpackages: - flate - gzip @@ -16,40 +41,53 @@ imports: - name: github.com/klauspost/cpuid version: 09cded8978dc9e80714c4d85b0322337b0a1e5e0 - name: github.com/klauspost/crc32 - version: 19b0b332c9e4516a6370a0456e6182c3b5036720 + version: cb6bfca970f6908083f26f39a79009d608efd5cd - name: github.com/labstack/gommon - version: 431777a5117c8de4352a400dad1e2a55f484b189 + version: f3b1a1b3bd4726161e1200863d278df7da5e66ff subpackages: - bytes - color - log - random - name: github.com/mattn/go-colorable - version: ed8eb9e318d7a84ce5915b495b7d35e0cfe7b5a8 + version: 6c903ff4aa50920ca86087a280590b36b3152b9c - name: github.com/mattn/go-isatty version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8 - name: github.com/pmezard/go-difflib - version: 792786c7400a136282c1664665ae0a8db921c6c2 + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: - difflib - name: github.com/stretchr/testify - version: d77da356e56a7428ad25149ca77381849a6a5232 + version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506 subpackages: - assert +- name: github.com/tylerb/graceful + version: 50a48b6e73fcc75b45e22c05b79629a67c79e938 - name: github.com/valyala/bytebufferpool version: e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7 - name: github.com/valyala/fasthttp - version: 0503f9d38c2eb8793a0210f950a815810ffd1b64 + version: 204fed91879e3476bedd4d9ce79e765a824a9b31 subpackages: - fasthttputil - name: github.com/valyala/fasttemplate version: 3b874956e03f1636d171bda64b130f9135f42cff - name: golang.org/x/net - version: 9313baa13d9262e49d07b20ed57dceafcd7240cc + version: daba796358cd2742b75aae05761f1b898c9f6a5c subpackages: - context + - websocket - name: golang.org/x/sys - version: 30de6d19a3bd89a5f38ae4028e23aaa5582648af + version: 002cbb5f952456d0c50e0d2aff17ea5eca716979 subpackages: - unix +- name: google.golang.org/appengine + version: 5b8c3b819891014a2d12354528f7d046dd53c89e + subpackages: + - internal + - internal/app_identity + - internal/base + - internal/datastore + - internal/log + - internal/modules + - internal/remote_api testImports: [] diff --git a/glide.yaml b/glide.yaml index e94edd2ff..2c7e20c4a 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,6 +1,11 @@ package: github.com/labstack/echo import: +- package: github.com/GeertJohan/go.rice - package: github.com/dgrijalva/jwt-go +- package: github.com/facebookgo/grace + subpackages: + - gracehttp +- package: github.com/gorilla/websocket - package: github.com/labstack/gommon subpackages: - bytes @@ -11,8 +16,11 @@ import: - package: github.com/stretchr/testify subpackages: - assert +- package: github.com/tylerb/graceful - package: github.com/valyala/fasthttp - package: github.com/valyala/fasttemplate - package: golang.org/x/net subpackages: - context + - websocket +- package: google.golang.org/appengine diff --git a/group.go b/group.go index bd467a4f9..c25e687b4 100644 --- a/group.go +++ b/group.go @@ -137,14 +137,14 @@ func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group { // Static implements `Echo#Static()` for sub-routes within the Group. func (g *Group) Static(prefix, root string) { - g.GET(g.prefix+prefix+"*", func(c Context) error { + g.GET(prefix+"*", func(c Context) error { return c.File(path.Join(root, c.P(0))) }) } // File implements `Echo#File()` for sub-routes within the Group. func (g *Group) File(path, file string) { - g.GET(g.prefix+path, func(c Context) error { + g.GET(path, func(c Context) error { return c.File(file) }) } diff --git a/middleware/cors.go b/middleware/cors.go index 69300c4f3..3182a5b9d 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -75,6 +75,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc { if len(config.AllowMethods) == 0 { config.AllowMethods = DefaultCORSConfig.AllowMethods } + allowOrigins := strings.Join(config.AllowOrigins, ",") allowMethods := strings.Join(config.AllowMethods, ",") allowHeaders := strings.Join(config.AllowHeaders, ",") exposeHeaders := strings.Join(config.ExposeHeaders, ",") @@ -88,25 +89,11 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc { req := c.Request() res := c.Response() - origin := req.Header().Get(echo.HeaderOrigin) - originSet := req.Header().Contains(echo.HeaderOrigin) // Issue #517 - - // Check allowed origins - allowedOrigin := "" - for _, o := range config.AllowOrigins { - if o == "*" || o == origin { - allowedOrigin = o - break - } - } // Simple request if req.Method() != echo.OPTIONS { res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) - if !originSet || allowedOrigin == "" { - return next(c) - } - res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowedOrigin) + res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigins) if config.AllowCredentials { res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") } @@ -120,10 +107,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc { res.Header().Add(echo.HeaderVary, echo.HeaderOrigin) res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestMethod) res.Header().Add(echo.HeaderVary, echo.HeaderAccessControlRequestHeaders) - if !originSet || allowedOrigin == "" { - return next(c) - } - res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowedOrigin) + res.Header().Set(echo.HeaderAccessControlAllowOrigin, allowOrigins) res.Header().Set(echo.HeaderAccessControlAllowMethods, allowMethods) if config.AllowCredentials { res.Header().Set(echo.HeaderAccessControlAllowCredentials, "true") diff --git a/middleware/cors_test.go b/middleware/cors_test.go index 846c4b340..0ab9e3499 100644 --- a/middleware/cors_test.go +++ b/middleware/cors_test.go @@ -21,18 +21,6 @@ func TestCORS(t *testing.T) { return c.String(http.StatusOK, "test") }) - // No origin header - h(c) - assert.Equal(t, "", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) - - // Empty origin header - req = test.NewRequest(echo.GET, "/", nil) - rec = test.NewResponseRecorder() - c = e.NewContext(req, rec) - req.Header().Set(echo.HeaderOrigin, "") - h(c) - assert.Equal(t, "*", rec.Header().Get(echo.HeaderAccessControlAllowOrigin)) - // Wildcard origin req = test.NewRequest(echo.GET, "/", nil) rec = test.NewResponseRecorder() diff --git a/recipe/cors/server.go b/recipe/cors/server.go new file mode 100644 index 000000000..e3cadb68b --- /dev/null +++ b/recipe/cors/server.go @@ -0,0 +1,38 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +var ( + users = []string{"Joe", "Veer", "Zion"} +) + +func getUsers(c echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // CORS default + // Allows requests from any origin wth GET, HEAD, PUT, POST or DELETE method. + // e.Use(middleware.CORS()) + + // CORS restricted + // Allows requests from any `https://labstack.com` or `https://labstack.net` origin + // wth GET, PUT, POST or DELETE method. + e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, + AllowMethods: []string{echo.GET, echo.PUT, echo.POST, echo.DELETE}, + })) + + e.GET("/api/users", getUsers) + e.Run(standard.New(":1323")) +} diff --git a/recipe/crud/server.go b/recipe/crud/server.go new file mode 100644 index 000000000..f92c31031 --- /dev/null +++ b/recipe/crud/server.go @@ -0,0 +1,76 @@ +package main + +import ( + "net/http" + "strconv" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +type ( + user struct { + ID int `json:"id"` + Name string `json:"name"` + } +) + +var ( + users = map[int]*user{} + seq = 1 +) + +//---------- +// Handlers +//---------- + +func createUser(c echo.Context) error { + u := &user{ + ID: seq, + } + if err := c.Bind(u); err != nil { + return err + } + users[u.ID] = u + seq++ + return c.JSON(http.StatusCreated, u) +} + +func getUser(c echo.Context) error { + id, _ := strconv.Atoi(c.Param("id")) + return c.JSON(http.StatusOK, users[id]) +} + +func updateUser(c echo.Context) error { + u := new(user) + if err := c.Bind(u); err != nil { + return err + } + id, _ := strconv.Atoi(c.Param("id")) + users[id].Name = u.Name + return c.JSON(http.StatusOK, users[id]) +} + +func deleteUser(c echo.Context) error { + id, _ := strconv.Atoi(c.Param("id")) + delete(users, id) + return c.NoContent(http.StatusNoContent) +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Routes + e.POST("/users", createUser) + e.GET("/users/:id", getUser) + e.PUT("/users/:id", updateUser) + e.DELETE("/users/:id", deleteUser) + + // Start server + e.Run(standard.New(":1323")) +} diff --git a/recipe/embed-resources/.gitignore b/recipe/embed-resources/.gitignore new file mode 100644 index 000000000..9524d94fa --- /dev/null +++ b/recipe/embed-resources/.gitignore @@ -0,0 +1,2 @@ +rice +app.rice-box.go diff --git a/recipe/embed-resources/app/index.html b/recipe/embed-resources/app/index.html new file mode 100644 index 000000000..66aac4465 --- /dev/null +++ b/recipe/embed-resources/app/index.html @@ -0,0 +1,11 @@ + + + + + go.rice Example + + + +

go.rice Example

+ + diff --git a/recipe/embed-resources/app/main.js b/recipe/embed-resources/app/main.js new file mode 100644 index 000000000..f888dc5ca --- /dev/null +++ b/recipe/embed-resources/app/main.js @@ -0,0 +1 @@ +alert("main.js"); diff --git a/recipe/embed-resources/server.go b/recipe/embed-resources/server.go new file mode 100644 index 000000000..e923194f6 --- /dev/null +++ b/recipe/embed-resources/server.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + + "github.com/GeertJohan/go.rice" + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +func main() { + e := echo.New() + // the file server for rice. "app" is the folder where the files come from. + assetHandler := http.FileServer(rice.MustFindBox("app").HTTPBox()) + // serves the index.html from rice + e.GET("/", standard.WrapHandler(assetHandler)) + + // servers other static files + e.GET("/static/*", standard.WrapHandler(http.StripPrefix("/static/", assetHandler))) + e.Run(standard.New(":3000")) +} diff --git a/recipe/file-upload/multiple/public/index.html b/recipe/file-upload/multiple/public/index.html new file mode 100644 index 000000000..b8463601a --- /dev/null +++ b/recipe/file-upload/multiple/public/index.html @@ -0,0 +1,17 @@ + + + + + Multiple file upload + + +

Upload multiple files with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + diff --git a/recipe/file-upload/multiple/server.go b/recipe/file-upload/multiple/server.go new file mode 100644 index 000000000..f9d31ff04 --- /dev/null +++ b/recipe/file-upload/multiple/server.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "io" + "os" + + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func upload(c echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //------------ + // Read files + //------------ + + // Multipart form + form, err := c.MultipartForm() + if err != nil { + return err + } + files := form.File["files"] + + for _, file := range files { + // Source + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

Uploaded successfully %d files with fields name=%s and email=%s.

", len(files), name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.Static("public")) + + e.POST("/upload", upload) + + e.Run(standard.New(":1323")) +} diff --git a/recipe/file-upload/single/public/index.html b/recipe/file-upload/single/public/index.html new file mode 100644 index 000000000..993579983 --- /dev/null +++ b/recipe/file-upload/single/public/index.html @@ -0,0 +1,17 @@ + + + + + Single file upload + + +

Upload single file with fields

+ +
+ Name:
+ Email:
+ Files:

+ +
+ + diff --git a/recipe/file-upload/single/server.go b/recipe/file-upload/single/server.go new file mode 100644 index 000000000..cc59ea5f6 --- /dev/null +++ b/recipe/file-upload/single/server.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "io" + "os" + + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func upload(c echo.Context) error { + // Read form fields + name := c.FormValue("name") + email := c.FormValue("email") + + //----------- + // Read file + //----------- + + // Source + file, err := c.FormFile("file") + if err != nil { + return err + } + src, err := file.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(file.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + return c.HTML(http.StatusOK, fmt.Sprintf("

File %s uploaded successfully with fields name=%s and email=%s.

", file.Filename, name, email)) +} + +func main() { + e := echo.New() + + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.Static("public")) + + e.POST("/upload", upload) + + e.Run(standard.New(":1323")) +} diff --git a/recipe/google-app-engine/Dockerfile b/recipe/google-app-engine/Dockerfile new file mode 100644 index 000000000..5d1c13e53 --- /dev/null +++ b/recipe/google-app-engine/Dockerfile @@ -0,0 +1,7 @@ +# Dockerfile extending the generic Go image with application files for a +# single application. +FROM gcr.io/google_appengine/golang + +COPY . /go/src/app +RUN go-wrapper download +RUN go-wrapper install -tags appenginevm \ No newline at end of file diff --git a/recipe/google-app-engine/app-engine.go b/recipe/google-app-engine/app-engine.go new file mode 100644 index 000000000..7b369770a --- /dev/null +++ b/recipe/google-app-engine/app-engine.go @@ -0,0 +1,21 @@ +// +build appengine + +package main + +import ( + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "net/http" +) + +func createMux() *echo.Echo { + e := echo.New() + + // note: we don't need to provide the middleware or static handlers, that's taken care of by the platform + // app engine has it's own "main" wrapper - we just need to hook echo into the default handler + s := standard.New("") + s.SetHandler(e) + http.Handle("/", s) + + return e +} diff --git a/recipe/google-app-engine/app-engine.yaml b/recipe/google-app-engine/app-engine.yaml new file mode 100644 index 000000000..e8f5bf050 --- /dev/null +++ b/recipe/google-app-engine/app-engine.yaml @@ -0,0 +1,36 @@ +application: my-application-id # defined when you create your app using google dev console +module: default # see https://cloud.google.com/appengine/docs/go/ +version: alpha # you can run multiple versions of an app and A/B test +runtime: go # see https://cloud.google.com/appengine/docs/go/ +api_version: go1 # used when appengine supports different go versions + +default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) + +handlers: +# all the static files that we normally serve ourselves are defined here and Google will handle +# serving them for us from it's own CDN / edge locations. For all the configuration options see: +# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers +- url: / + mime_type: text/html + static_files: public/index.html + upload: public/index.html + +- url: /favicon.ico + mime_type: image/x-icon + static_files: public/favicon.ico + upload: public/favicon.ico + +- url: /scripts + mime_type: text/javascript + static_dir: public/scripts + +# static files normally don't touch the server that the app runs on but server-side template files +# needs to be readable by the app. The application_readable option makes sure they are available as +# part of the app deployment onto the instance. +- url: /templates + static_dir: /templates + application_readable: true + +# finally, we route all other requests to our application. The script name just means "the go app" +- url: /.* + script: _go_app \ No newline at end of file diff --git a/recipe/google-app-engine/app-managed.go b/recipe/google-app-engine/app-managed.go new file mode 100644 index 000000000..d6a41c816 --- /dev/null +++ b/recipe/google-app-engine/app-managed.go @@ -0,0 +1,29 @@ +// +build appenginevm + +package main + +import ( + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "google.golang.org/appengine" + "net/http" + "runtime" +) + +func createMux() *echo.Echo { + e := echo.New() + + // note: we don't need to provide the middleware or static handlers + // for the appengine vm version - that's taken care of by the platform + + return e +} + +func main() { + // the appengine package provides a convenient method to handle the health-check requests + // and also run the app on the correct port. We just need to add Echo to the default handler + s := standard.New(":8080") + s.SetHandler(e) + http.Handle("/", s) + appengine.Main() +} diff --git a/recipe/google-app-engine/app-managed.yaml b/recipe/google-app-engine/app-managed.yaml new file mode 100644 index 000000000..d5da4cd94 --- /dev/null +++ b/recipe/google-app-engine/app-managed.yaml @@ -0,0 +1,37 @@ +application: my-application-id # defined when you create your app using google dev console +module: default # see https://cloud.google.com/appengine/docs/go/ +version: alpha # you can run multiple versions of an app and A/B test +runtime: go # see https://cloud.google.com/appengine/docs/go/ +api_version: go1 # used when appengine supports different go versions +vm: true # for managed VMs only, remove for appengine classic + +default_expiration: "1d" # for CDN serving of static files (use url versioning if long!) + +handlers: +# all the static files that we normally serve ourselves are defined here and Google will handle +# serving them for us from it's own CDN / edge locations. For all the configuration options see: +# https://cloud.google.com/appengine/docs/go/config/appconfig#Go_app_yaml_Static_file_handlers +- url: / + mime_type: text/html + static_files: public/index.html + upload: public/index.html + +- url: /favicon.ico + mime_type: image/x-icon + static_files: public/favicon.ico + upload: public/favicon.ico + +- url: /scripts + mime_type: text/javascript + static_dir: public/scripts + +# static files normally don't touch the server that the app runs on but server-side template files +# needs to be readable by the app. The application_readable option makes sure they are available as +# part of the app deployment onto the instance. +- url: /templates + static_dir: /templates + application_readable: true + +# finally, we route all other requests to our application. The script name just means "the go app" +- url: /.* + script: _go_app \ No newline at end of file diff --git a/recipe/google-app-engine/app-standalone.go b/recipe/google-app-engine/app-standalone.go new file mode 100644 index 000000000..92615b459 --- /dev/null +++ b/recipe/google-app-engine/app-standalone.go @@ -0,0 +1,25 @@ +// +build !appengine,!appenginevm + +package main + +import ( + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func createMux() *echo.Echo { + e := echo.New() + + e.Use(middleware.Recover()) + e.Use(middleware.Logger()) + e.Use(middleware.Gzip()) + + e.Use(middleware.Static("public")) + + return e +} + +func main() { + e.Run(standard.New(":8080")) +} diff --git a/recipe/google-app-engine/app.go b/recipe/google-app-engine/app.go new file mode 100644 index 000000000..8d4d97a2e --- /dev/null +++ b/recipe/google-app-engine/app.go @@ -0,0 +1,4 @@ +package main + +// reference our echo instance and create it early +var e = createMux() diff --git a/recipe/google-app-engine/public/favicon.ico b/recipe/google-app-engine/public/favicon.ico new file mode 100644 index 000000000..d939ddca1 Binary files /dev/null and b/recipe/google-app-engine/public/favicon.ico differ diff --git a/recipe/google-app-engine/public/index.html b/recipe/google-app-engine/public/index.html new file mode 100644 index 000000000..aed4f4668 --- /dev/null +++ b/recipe/google-app-engine/public/index.html @@ -0,0 +1,15 @@ + + + + + + Echo + + + + + +

Echo!

+ + + diff --git a/recipe/google-app-engine/public/scripts/main.js b/recipe/google-app-engine/public/scripts/main.js new file mode 100644 index 000000000..62a4c8f1f --- /dev/null +++ b/recipe/google-app-engine/public/scripts/main.js @@ -0,0 +1 @@ +console.log("Echo!"); diff --git a/recipe/google-app-engine/templates/welcome.html b/recipe/google-app-engine/templates/welcome.html new file mode 100644 index 000000000..5dc667c36 --- /dev/null +++ b/recipe/google-app-engine/templates/welcome.html @@ -0,0 +1 @@ +{{define "welcome"}}Hello, {{.}}!{{end}} diff --git a/recipe/google-app-engine/users.go b/recipe/google-app-engine/users.go new file mode 100644 index 000000000..b7687181f --- /dev/null +++ b/recipe/google-app-engine/users.go @@ -0,0 +1,54 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/middleware" +) + +type ( + user struct { + ID string `json:"id"` + Name string `json:"name"` + } +) + +var ( + users map[string]user +) + +func init() { + users = map[string]user{ + "1": user{ + ID: "1", + Name: "Wreck-It Ralph", + }, + } + + // hook into the echo instance to create an endpoint group + // and add specific middleware to it plus handlers + g := e.Group("/users") + g.Use(middleware.CORS()) + + g.POST("", createUser) + g.GET("", getUsers) + g.GET("/:id", getUser) +} + +func createUser(c echo.Context) error { + u := new(user) + if err := c.Bind(u); err != nil { + return err + } + users[u.ID] = *u + return c.JSON(http.StatusCreated, u) +} + +func getUsers(c echo.Context) error { + return c.JSON(http.StatusOK, users) +} + +func getUser(c echo.Context) error { + return c.JSON(http.StatusOK, users[c.P(0)]) +} diff --git a/recipe/google-app-engine/welcome.go b/recipe/google-app-engine/welcome.go new file mode 100644 index 000000000..dc23f28b3 --- /dev/null +++ b/recipe/google-app-engine/welcome.go @@ -0,0 +1,31 @@ +package main + +import ( + "html/template" + "io" + "net/http" + + "github.com/labstack/echo" +) + +type ( + Template struct { + templates *template.Template + } +) + +func init() { + t := &Template{ + templates: template.Must(template.ParseFiles("templates/welcome.html")), + } + e.SetRenderer(t) + e.GET("/welcome", welcome) +} + +func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) +} + +func welcome(c echo.Context) error { + return c.Render(http.StatusOK, "welcome", "Joe") +} diff --git a/recipe/graceful-shutdown/grace/server.go b/recipe/graceful-shutdown/grace/server.go new file mode 100644 index 000000000..1eeab5312 --- /dev/null +++ b/recipe/graceful-shutdown/grace/server.go @@ -0,0 +1,19 @@ +package main + +import ( + "net/http" + + "github.com/facebookgo/grace/gracehttp" + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +func main() { + e := echo.New() + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Six sick bricks tick") + }) + std := standard.New(":1323") + std.SetHandler(e) + gracehttp.Serve(std.Server) +} diff --git a/recipe/graceful-shutdown/graceful/server.go b/recipe/graceful-shutdown/graceful/server.go new file mode 100644 index 000000000..c41ff8b04 --- /dev/null +++ b/recipe/graceful-shutdown/graceful/server.go @@ -0,0 +1,21 @@ +package main + +import ( + "net/http" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "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 joe crows nose") + }) + std := standard.New(":1323") + std.SetHandler(e) + graceful.ListenAndServe(std.Server, 5*time.Second) +} diff --git a/recipe/hello-world/server.go b/recipe/hello-world/server.go new file mode 100644 index 000000000..0ba8287ed --- /dev/null +++ b/recipe/hello-world/server.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func main() { + // Echo instance + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Route => handler + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!\n") + }) + + // Start server + e.Run(standard.New(":1323")) +} diff --git a/recipe/http2/cert.pem b/recipe/http2/cert.pem new file mode 100644 index 000000000..34d6746d6 --- /dev/null +++ b/recipe/http2/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+DCCAeCgAwIBAgIPFB3cowaziqTVN3I2utsEMA0GCSqGSIb3DQEBCwUAMBIx +EDAOBgNVBAoTB0FjbWUgQ28wHhcNMTYwNTA5MTUwNDQ2WhcNMTcwNTA5MTUwNDQ2 +WjASMRAwDgYDVQQKEwdBY21lIENvMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEA2Q55Uf4UX00jIvyeu8RF4BQC6RCqwiQeg+k7a9LvQyIOQpaw+jr8IJ7W +aUUYe3vF1fIgtSsHVGnGtfYcTmgOCbqhQnU7LYuDYpVfXemNTvMkbT2iqxyrrRNy +n3FVMnf1O9HrRol8mfOCVf5j3GeqyCw1PXH1OgnNLDsdz7DoNUXt334Q0SMGF+Yg +RnlAIzRmye1UPBPVupcSbQBPGEtBstBQMYtKQDf0bPfciV6nHMhTrmw3Xc/d96OY +35iZ0zveWUYVJfyi8xqPBtfjnomUs8UmQ47aZdL6mrWaHXinIbTI4phKmB22qnlg +PY//yrMM2BgEMGGi8FOFQG/n5f0zzQIDAQABo0swSTAOBgNVHQ8BAf8EBAMCBaAw +EwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAUBgNVHREEDTALggls +b2NhbGhvc3QwDQYJKoZIhvcNAQELBQADggEBAL2SqeH2Z6zKje9XC6O/TC0DKRvE +L/d/1NVz5nr52Gqk+7DzU1QCWHD9+FLEX+wvzcploNoqcR+Xrh7N/2kNGymDdxP+ +UlpmpzLQ6BwcjWgwMa4GMn3L/aczFFuhG9dZdcy9UV4fXtreLO0FMt4BuBQyKMK2 +ZkWgw2rx3fMBgR4B99JpRr0wgqPHbFndvPYbMAEiYB4baRiIpkyBC6/JyZTNTBBH ++WyvuQKbip5WvVST+PjIy2eywJ2yGXKek3TOS8GTulVqUE5u7utzdOLJaLXIvF2K +BAbRa6CZ8+bZWRYpBujlu1mKjedzi/pS5BK5q/gPUWe8YcqEJVZWEHMakZ0= +-----END CERTIFICATE----- diff --git a/recipe/http2/key.pem b/recipe/http2/key.pem new file mode 100644 index 000000000..f6c5a0b7d --- /dev/null +++ b/recipe/http2/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA2Q55Uf4UX00jIvyeu8RF4BQC6RCqwiQeg+k7a9LvQyIOQpaw ++jr8IJ7WaUUYe3vF1fIgtSsHVGnGtfYcTmgOCbqhQnU7LYuDYpVfXemNTvMkbT2i +qxyrrRNyn3FVMnf1O9HrRol8mfOCVf5j3GeqyCw1PXH1OgnNLDsdz7DoNUXt334Q +0SMGF+YgRnlAIzRmye1UPBPVupcSbQBPGEtBstBQMYtKQDf0bPfciV6nHMhTrmw3 +Xc/d96OY35iZ0zveWUYVJfyi8xqPBtfjnomUs8UmQ47aZdL6mrWaHXinIbTI4phK +mB22qnlgPY//yrMM2BgEMGGi8FOFQG/n5f0zzQIDAQABAoIBABvDpTMeu/5gwJDW +HXvJJGMATEsBrcX9lsWqzMYDCkXdElPkMKkhaGTHzIdUpkgNUgYG3bYu1dYvXZKi +84X3+2u1KrF0i2hNAzZWAwfhOwkRQuSSUL9wgGk4Jp5Mgehdvcm0FVNFfOG4a0CS +tLVZsVe/h+PfMs4EuPDicvLBH0LI/dE+GeXGIhuF8/g7EN3S+f/WtCejrzah7Ghf +OsaBaBL0O5s82wU9PX0ZFxo4FADfwKLL0Zmbno8NC0cMsNOM2/B2dRF0eTlCDeAy +rxIXL7HEz+fWK96jCbDiMPyP9XstQxO2atZa2gC5vOJCwPpVVDbqW7FNop4VwUOW +bTtDrcUCgYEA6+hyuN0DcaAOM64rVyBbcMOD4L6Q00A1+9I1wBJtx1yNNfTS4iJc +EXxkLS8ae9xXYEGhiU0PyOufs+iTwLwvlbWcZUJailEoBG5EzC1BDZPE20hNcXav +zxxyQxRrzdItmMj8wa4TETVlcPGcKxNUw/UXqFIyNShtALse2mnYQQcCgYEA64sA +3WC+/e0jVoaPiIuCg2vt5QUiyfkblSUjsdBTlMWYIjlJNcGv81vepk3NIAPkTs71 +HZuRwEDENm/7HDSG2SpyB9de8/b061Uo/o+ZhWqPOHx1m7mD9x96CZk4wtxwL14u +SJuwyujqmSYoUYW8FKjHS5n74hP9YUT7hXKWs4sCgYAx2eAMUp/8rd7yata7xZmt +HZPLtVlzWrlNqqEzInHSVCt/AGpj4PDlvQyKQ87r56cLLzNMiV1RjwEjin1WmC3S +DButhjUNz5KORSMCpnl9vgE2eXPsCzGhqZg3tqQFTWnXRHmtD/T1iPwTvurKa35Z +HnzOU/hKJW3LXr9pVj6dlwKBgQCXVaEBm1Y7GbB5uEziIxiAzch0O9+FOxsgsVME +vN/mlynO21WRR1eAGUetPBGN/1Ih3FCabEix6Cro+vuwvILjZqULKrIkN0hXJ0kG +fUba9IL+fOCnZANItJ2ZKyvP7wfZNz6vgfCN/iY0rdJ7xlv4PhSGG3I9aDCE3Who +7h2rTwKBgQCQ5s0JdS/EDX5h7LPPqpIV1kqMmcqvBonZN4TySfqxKSlhasO3oGgC +rYkygiYGU8WdooavGNSmY4+sCBY002PZQ8EcSp+KySpYLeiVrJZTc+ehzk93t2to +b1p1mmLFCM6SVJAIQ+y9jt5GCSEka0BlVoNfLM06OLcC9j8iIGsjrw== +-----END RSA PRIVATE KEY----- diff --git a/recipe/http2/server.go b/recipe/http2/server.go new file mode 100644 index 000000000..9f66e46dc --- /dev/null +++ b/recipe/http2/server.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine" + "github.com/labstack/echo/engine/standard" +) + +func request(c echo.Context) error { + req := c.Request().(*standard.Request).Request + format := "
Request Information\n\nProtocol: %s\nHost: %s\nRemote Address: %s\nMethod: %s\nPath: %s\n
" + return c.HTML(http.StatusOK, fmt.Sprintf(format, req.Proto, req.Host, req.RemoteAddr, req.Method, req.URL.Path)) +} + +func stream(c echo.Context) error { + res := c.Response().(*standard.Response).ResponseWriter + gone := res.(http.CloseNotifier).CloseNotify() + res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) + res.WriteHeader(http.StatusOK) + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + fmt.Fprint(res, "
Clock Stream\n\n")
+	for {
+		fmt.Fprintf(res, "%v\n", time.Now())
+		res.(http.Flusher).Flush()
+		select {
+		case <-ticker.C:
+		case <-gone:
+			break
+		}
+	}
+}
+
+func main() {
+	e := echo.New()
+	e.GET("/request", request)
+	e.GET("/stream", stream)
+	e.Run(standard.WithConfig(engine.Config{
+		Address:     ":1323",
+		TLSCertFile: "cert.pem",
+		TLSKeyFile:  "key.pem",
+	}))
+}
diff --git a/recipe/jsonp/public/index.html b/recipe/jsonp/public/index.html
new file mode 100644
index 000000000..b26791ef2
--- /dev/null
+++ b/recipe/jsonp/public/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+    
+    
+    JSONP
+    
+    
+
+
+
+
+    
+ +

+


+        

+
+ + + diff --git a/recipe/jsonp/server.go b/recipe/jsonp/server.go new file mode 100644 index 000000000..bee3327b4 --- /dev/null +++ b/recipe/jsonp/server.go @@ -0,0 +1,35 @@ +package main + +import ( + "math/rand" + "net/http" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.Static("public")) + + // JSONP + e.GET("/jsonp", func(c echo.Context) error { + callback := c.QueryParam("callback") + var content struct { + Response string `json:"response"` + Timestamp time.Time `json:"timestamp"` + Random int `json:"random"` + } + content.Response = "Sent via JSONP" + content.Timestamp = time.Now().UTC() + content.Random = rand.Intn(1000) + return c.JSONP(http.StatusOK, callback, &content) + }) + + // Start server + e.Run(standard.New(":1323")) +} diff --git a/recipe/jwt/custom-claims/server.go b/recipe/jwt/custom-claims/server.go new file mode 100644 index 000000000..d0eb51398 --- /dev/null +++ b/recipe/jwt/custom-claims/server.go @@ -0,0 +1,87 @@ +package main + +import ( + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +// jwtCustomClaims are custom claims extending default ones. +type jwtCustomClaims struct { + Name string `json:"name"` + Admin bool `json:"admin"` + jwt.StandardClaims +} + +func login(c echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + if username == "jon" && password == "shhh!" { + + // Set custom claims + claims := &jwtCustomClaims{ + "Jon Snow", + true, + jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Hour * 72).Unix(), + }, + } + + // Create token with claims + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]string{ + "token": t, + }) + } + + return echo.ErrUnauthorized +} + +func accessible(c echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c echo.Context) error { + user := c.Get("user").(*jwt.Token) + claims := user.Claims.(*jwtCustomClaims) + name := claims.Name + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Login route + e.POST("/login", login) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + + // Configure middleware with the custom claims type + config := middleware.JWTConfig{ + Claims: &jwtCustomClaims{}, + SigningKey: []byte("secret"), + } + r.Use(middleware.JWTWithConfig(config)) + r.GET("", restricted) + + e.Run(standard.New(":1323")) +} diff --git a/recipe/jwt/map-claims/server.go b/recipe/jwt/map-claims/server.go new file mode 100644 index 000000000..267e045e3 --- /dev/null +++ b/recipe/jwt/map-claims/server.go @@ -0,0 +1,70 @@ +package main + +import ( + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +func login(c echo.Context) error { + username := c.FormValue("username") + password := c.FormValue("password") + + if username == "jon" && password == "shhh!" { + // Create token + token := jwt.New(jwt.SigningMethodHS256) + + // Set claims + claims := token.Claims.(jwt.MapClaims) + claims["name"] = "Jon Snow" + claims["admin"] = true + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return err + } + return c.JSON(http.StatusOK, map[string]string{ + "token": t, + }) + } + + return echo.ErrUnauthorized +} + +func accessible(c echo.Context) error { + return c.String(http.StatusOK, "Accessible") +} + +func restricted(c echo.Context) error { + user := c.Get("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + name := claims["name"].(string) + return c.String(http.StatusOK, "Welcome "+name+"!") +} + +func main() { + e := echo.New() + + // Middleware + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + + // Login route + e.POST("/login", login) + + // Unauthenticated route + e.GET("/", accessible) + + // Restricted group + r := e.Group("/restricted") + r.Use(middleware.JWT([]byte("secret"))) + r.GET("", restricted) + + e.Run(standard.New(":1323")) +} diff --git a/recipe/middleware/server.go b/recipe/middleware/server.go new file mode 100644 index 000000000..d8bf6420c --- /dev/null +++ b/recipe/middleware/server.go @@ -0,0 +1,83 @@ +package main + +import ( + "net/http" + "strconv" + "sync" + "time" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +type ( + Stats struct { + Uptime time.Time `json:"uptime"` + RequestCount uint64 `json:"requestCount"` + Statuses map[string]int `json:"statuses"` + mutex sync.RWMutex + } +) + +func NewStats() *Stats { + return &Stats{ + Uptime: time.Now(), + Statuses: make(map[string]int), + } +} + +// Process is the middleware function. +func (s *Stats) Process(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if err := next(c); err != nil { + c.Error(err) + } + s.mutex.Lock() + defer s.mutex.Unlock() + s.RequestCount++ + status := strconv.Itoa(c.Response().Status()) + s.Statuses[status]++ + return nil + } +} + +// Handle is the endpoint to get stats. +func (s *Stats) Handle(c echo.Context) error { + s.mutex.RLock() + defer s.mutex.RUnlock() + return c.JSON(http.StatusOK, s) +} + +// ServerHeader middleware adds a `Server` header to the response. +func ServerHeader(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + c.Response().Header().Set(echo.HeaderServer, "Echo/2.0") + return next(c) + } +} + +func main() { + e := echo.New() + + // Debug mode + e.SetDebug(true) + + //------------------- + // Custom middleware + //------------------- + // Stats + s := NewStats() + e.Use(s.Process) + e.GET("/stats", s.Handle) // Endpoint to get stats + + // Server header + e.Use(ServerHeader) + + // Handler + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + + // Start server + e.Run(standard.New(":1323")) +} diff --git a/recipe/streaming-response/server.go b/recipe/streaming-response/server.go new file mode 100644 index 000000000..ab51c9d80 --- /dev/null +++ b/recipe/streaming-response/server.go @@ -0,0 +1,46 @@ +package main + +import ( + "net/http" + "time" + + "encoding/json" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +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.HeaderContentType, echo.MIMEApplicationJSON) + c.Response().WriteHeader(http.StatusOK) + for _, l := range locations { + if err := json.NewEncoder(c.Response()).Encode(l); err != nil { + return err + } + c.Response().(http.Flusher).Flush() + time.Sleep(1 * time.Second) + } + return nil + }) + e.Run(standard.New(":1323")) +} diff --git a/recipe/subdomains/server.go b/recipe/subdomains/server.go new file mode 100644 index 000000000..e16e2fb4c --- /dev/null +++ b/recipe/subdomains/server.go @@ -0,0 +1,79 @@ +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +type ( + Host struct { + Echo *echo.Echo + } +) + +func main() { + // Hosts + hosts := make(map[string]*Host) + + //----- + // API + //----- + + api := echo.New() + api.Use(middleware.Logger()) + api.Use(middleware.Recover()) + + hosts["api.localhost:1323"] = &Host{api} + + api.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "API") + }) + + //------ + // Blog + //------ + + blog := echo.New() + blog.Use(middleware.Logger()) + blog.Use(middleware.Recover()) + + hosts["blog.localhost:1323"] = &Host{blog} + + blog.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Blog") + }) + + //--------- + // Website + //--------- + + site := echo.New() + site.Use(middleware.Logger()) + site.Use(middleware.Recover()) + + hosts["localhost:1323"] = &Host{site} + + site.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Website") + }) + + // Server + e := echo.New() + e.Any("/*", func(c echo.Context) (err error) { + req := c.Request() + res := c.Response() + host := hosts[req.Host()] + + if host == nil { + err = echo.ErrNotFound + } else { + host.Echo.ServeHTTP(req, res) + } + + return + }) + e.Run(standard.New(":1323")) +} diff --git a/recipe/websocket/gorilla/server.go b/recipe/websocket/gorilla/server.go new file mode 100644 index 000000000..a84f24946 --- /dev/null +++ b/recipe/websocket/gorilla/server.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/labstack/echo" + + "github.com/gorilla/websocket" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" +) + +var ( + upgrader = websocket.Upgrader{} +) + +func hello() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + defer c.Close() + + for { + // Write + err := c.WriteMessage(websocket.TextMessage, []byte("Hello, Client!")) + if err != nil { + log.Fatal(err) + } + + // Read + _, msg, err := c.ReadMessage() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", msg) + } + } +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.Static("../public")) + e.GET("/ws", standard.WrapHandler(http.HandlerFunc(hello()))) + e.Run(standard.New(":1323")) +} diff --git a/recipe/websocket/net/server.go b/recipe/websocket/net/server.go new file mode 100644 index 000000000..f9deabfd8 --- /dev/null +++ b/recipe/websocket/net/server.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "log" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/labstack/echo/middleware" + "golang.org/x/net/websocket" +) + +func hello() websocket.Handler { + return websocket.Handler(func(ws *websocket.Conn) { + for { + // Write + err := websocket.Message.Send(ws, "Hello, Client!") + if err != nil { + log.Fatal(err) + } + + // Read + msg := "" + err = websocket.Message.Receive(ws, &msg) + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", msg) + } + }) +} + +func main() { + e := echo.New() + e.Use(middleware.Logger()) + e.Use(middleware.Recover()) + e.Use(middleware.Static("../public")) + e.GET("/ws", standard.WrapHandler(hello())) + e.Run(standard.New(":1323")) +} diff --git a/recipe/websocket/public/index.html b/recipe/websocket/public/index.html new file mode 100644 index 000000000..33cabb1ba --- /dev/null +++ b/recipe/websocket/public/index.html @@ -0,0 +1,39 @@ + + + + + + WebSocket + + + +

+ + + + + diff --git a/router.go b/router.go index 7be4f4a9e..769af1a6b 100644 --- a/router.go +++ b/router.go @@ -51,7 +51,7 @@ func NewRouter(e *Echo) *Router { } // Add registers a new route for method and path with matching handler. -func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { +func (r *Router) Add(method, path string, h HandlerFunc) { // Validate path if path == "" { panic("echo: path cannot be empty") @@ -66,7 +66,7 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { if path[i] == ':' { j := i + 1 - r.insert(method, path[:i], nil, skind, "", nil, e) + r.insert(method, path[:i], nil, skind, "", nil) for ; i < l && path[i] != '/'; i++ { } @@ -75,26 +75,26 @@ func (r *Router) Add(method, path string, h HandlerFunc, e *Echo) { i, l = j, len(path) if i == l { - r.insert(method, path[:i], h, pkind, ppath, pnames, e) + r.insert(method, path[:i], h, pkind, ppath, pnames) return } - r.insert(method, path[:i], nil, pkind, ppath, pnames, e) + r.insert(method, path[:i], nil, pkind, ppath, pnames) } else if path[i] == '*' { - r.insert(method, path[:i], nil, skind, "", nil, e) + r.insert(method, path[:i], nil, skind, "", nil) pnames = append(pnames, "_*") - r.insert(method, path[:i+1], h, akind, ppath, pnames, e) + r.insert(method, path[:i+1], h, akind, ppath, pnames) return } } - r.insert(method, path, h, skind, ppath, pnames, e) + r.insert(method, path, h, skind, ppath, pnames) } -func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string, e *Echo) { +func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string, pnames []string) { // Adjust max param l := len(pnames) - if *e.maxParam < l { - *e.maxParam = l + if *r.echo.maxParam < l { + *r.echo.maxParam = l } cn := r.tree // Current node as root @@ -343,7 +343,7 @@ func (r *Router) Find(method, path string, context Context) { // Static node if c = cn.findChild(search[0], skind); c != nil { // Save next - if cn.label == '/' { + if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623 nk = pkind nn = cn ns = search @@ -361,7 +361,7 @@ func (r *Router) Find(method, path string, context Context) { } // Save next - if cn.label == '/' { + if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623 nk = akind nn = cn ns = search diff --git a/router_test.go b/router_test.go index b099d5e95..387399d34 100644 --- a/router_test.go +++ b/router_test.go @@ -280,8 +280,8 @@ func TestRouterStatic(t *testing.T) { r.Add(GET, path, func(c Context) error { c.Set("path", path) return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, path, c) c.handler(c) assert.Equal(t, path, c.Get("path")) @@ -292,8 +292,8 @@ func TestRouterParam(t *testing.T) { r := e.router r.Add(GET, "/users/:id", func(c Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/1", c) assert.Equal(t, "1", c.P(0)) } @@ -303,8 +303,8 @@ func TestRouterTwoParam(t *testing.T) { r := e.router r.Add(GET, "/users/:uid/files/:fid", func(Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/1/files/1", c) assert.Equal(t, "1", c.P(0)) @@ -318,13 +318,13 @@ func TestRouterParamWithSlash(t *testing.T) { r.Add(GET, "/a/:b/c/d/:e", func(c Context) error { return nil - }, e) + }) r.Add(GET, "/a/:b/c/:d/:f", func(c Context) error { return nil - }, e) + }) - c := e.NewContext(nil, nil).(*echoContext) + c := e.NewContext(nil, nil).(*context) assert.NotPanics(t, func() { r.Find(GET, "/a/1/c/d/2/3", c) }) @@ -337,14 +337,14 @@ func TestRouterMatchAny(t *testing.T) { // Routes r.Add(GET, "/", func(Context) error { return nil - }, e) + }) r.Add(GET, "/*", func(Context) error { return nil - }, e) + }) r.Add(GET, "/users/*", func(Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, "/", c) assert.Equal(t, "", c.P(0)) @@ -361,8 +361,8 @@ func TestRouterMicroParam(t *testing.T) { r := e.router r.Add(GET, "/:a/:b/:c", func(c Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, "/1/2/3", c) assert.Equal(t, "1", c.P(0)) assert.Equal(t, "2", c.P(1)) @@ -376,8 +376,8 @@ func TestRouterMixParamMatchAny(t *testing.T) { // Route r.Add(GET, "/users/:id/*", func(c Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) r.Find(GET, "/users/joe/comments", c) c.handler(c) @@ -392,11 +392,11 @@ func TestRouterMultiRoute(t *testing.T) { r.Add(GET, "/users", func(c Context) error { c.Set("path", "/users") return nil - }, e) + }) r.Add(GET, "/users/:id", func(c Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) @@ -408,7 +408,7 @@ func TestRouterMultiRoute(t *testing.T) { assert.Equal(t, "1", c.P(0)) // Route > /user - c = e.NewContext(nil, nil).(*echoContext) + c = e.NewContext(nil, nil).(*context) r.Find(GET, "/user", c) he := c.handler(c).(*HTTPError) assert.Equal(t, http.StatusNotFound, he.Code) @@ -422,32 +422,32 @@ func TestRouterPriority(t *testing.T) { r.Add(GET, "/users", func(c Context) error { c.Set("a", 1) return nil - }, e) + }) r.Add(GET, "/users/new", func(c Context) error { c.Set("b", 2) return nil - }, e) + }) r.Add(GET, "/users/:id", func(c Context) error { c.Set("c", 3) return nil - }, e) + }) r.Add(GET, "/users/dew", func(c Context) error { c.Set("d", 4) return nil - }, e) + }) r.Add(GET, "/users/:id/files", func(c Context) error { c.Set("e", 5) return nil - }, e) + }) r.Add(GET, "/users/newsee", func(c Context) error { c.Set("f", 6) return nil - }, e) + }) r.Add(GET, "/users/*", func(c Context) error { c.Set("g", 7) return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) @@ -490,17 +490,17 @@ func TestRouterPriority(t *testing.T) { func TestRouterPriorityNotFound(t *testing.T) { e := New() r := e.router - c := e.NewContext(nil, nil).(*echoContext) + c := e.NewContext(nil, nil).(*context) // Add r.Add(GET, "/a/foo", func(c Context) error { c.Set("a", 1) return nil - }, e) + }) r.Add(GET, "/a/bar", func(c Context) error { c.Set("b", 2) return nil - }, e) + }) // Find r.Find(GET, "/a/foo", c) @@ -511,7 +511,7 @@ func TestRouterPriorityNotFound(t *testing.T) { c.handler(c) assert.Equal(t, 2, c.Get("b")) - c = e.NewContext(nil, nil).(*echoContext) + c = e.NewContext(nil, nil).(*context) r.Find(GET, "/abc/def", c) he := c.handler(c).(*HTTPError) assert.Equal(t, http.StatusNotFound, he.Code) @@ -525,14 +525,14 @@ func TestRouterParamNames(t *testing.T) { r.Add(GET, "/users", func(c Context) error { c.Set("path", "/users") return nil - }, e) + }) r.Add(GET, "/users/:id", func(c Context) error { return nil - }, e) + }) r.Add(GET, "/users/:uid/files/:fid", func(c Context) error { return nil - }, e) - c := e.NewContext(nil, nil).(*echoContext) + }) + c := e.NewContext(nil, nil).(*context) // Route > /users r.Find(GET, "/users", c) @@ -552,6 +552,38 @@ func TestRouterParamNames(t *testing.T) { assert.Equal(t, "1", c.P(1)) } +// Issue #623 +func TestRouterStaticDynamicConflict(t *testing.T) { + e := New() + r := e.router + c := e.NewContext(nil, nil) + + r.Add(GET, "/dictionary/skills", func(c Context) error { + c.Set("a", 1) + return nil + }) + r.Add(GET, "/dictionary/:name", func(c Context) error { + c.Set("b", 2) + return nil + }) + r.Add(GET, "/server", func(c Context) error { + c.Set("c", 3) + return nil + }) + + r.Find(GET, "/dictionary/skills", c) + c.Handler()(c) + assert.Equal(t, 1, c.Get("a")) + c = e.NewContext(nil, nil) + r.Find(GET, "/dictionary/type", c) + c.Handler()(c) + assert.Equal(t, 2, c.Get("b")) + c = e.NewContext(nil, nil) + r.Find(GET, "/server", c) + c.Handler()(c) + assert.Equal(t, 3, c.Get("c")) +} + func TestRouterAPI(t *testing.T) { e := New() r := e.router @@ -559,9 +591,9 @@ func TestRouterAPI(t *testing.T) { for _, route := range api { r.Add(route.Method, route.Path, func(c Context) error { return nil - }, e) + }) } - c := e.NewContext(nil, nil).(*echoContext) + c := e.NewContext(nil, nil).(*context) for _, route := range api { r.Find(route.Method, route.Path, c) for i, n := range c.pnames { @@ -581,7 +613,7 @@ func BenchmarkRouterGitHubAPI(b *testing.B) { for _, route := range api { r.Add(route.Method, route.Path, func(c Context) error { return nil - }, e) + }) } // Find routes diff --git a/test/response.go b/test/response.go index dc6146932..02809524b 100644 --- a/test/response.go +++ b/test/response.go @@ -55,6 +55,9 @@ func (r *Response) WriteHeader(code int) { } func (r *Response) Write(b []byte) (n int, err error) { + if !r.committed { + r.WriteHeader(http.StatusOK) + } n, err = r.writer.Write(b) r.size += int64(n) return diff --git a/website/Makefile b/website/Makefile new file mode 100644 index 000000000..0f190a63f --- /dev/null +++ b/website/Makefile @@ -0,0 +1,2 @@ +build: + rm -rf public/v2 && hugo diff --git a/website/config.json b/website/config.json new file mode 100644 index 000000000..51438d2b7 --- /dev/null +++ b/website/config.json @@ -0,0 +1,44 @@ +{ + "baseurl": "https://echo.labstack.com/v2/", + "languageCode": "en-us", + "title": "Echo - Fast and unfancy HTTP server framework for Go (Golang)", + "canonifyurls": true, + "googleAnalytics": "UA-85059636-2", + "publishdir": "public/v2", + "permalinks": { + "guide": "/guide/:filename", + "middleware": "/middleware/:filename", + "recipes": "/recipes/:filename" + }, + "menu": { + "side": [{ + "name": "Guide", + "pre": "", + "weight": 1, + "identifier": "guide", + "url": "guide" + },{ + "name": "Middleware", + "pre": "", + "weight": 1, + "identifier": "middleware", + "url": "middleware" + }, { + "name": "Recipes", + "pre": "", + "weight": 2, + "identifier": "recipes", + "url": "recipes" + }, { + "name": "Godoc", + "pre": "", + "weight": 3, + "identifier": "godoc", + "url": "godoc" + }] + }, + "params": { + "image": "https://echo.labstack.com/images/logo.png", + "description": "Echo is micro web framework for Go (Golang), high performance, minimalistic and fast." + } +} diff --git a/website/content/godoc/echo.md b/website/content/godoc/echo.md new file mode 100644 index 000000000..6fa6ab8c7 --- /dev/null +++ b/website/content/godoc/echo.md @@ -0,0 +1,8 @@ ++++ +title = "echo" +[menu.side] + name = "echo" + parent = "godoc" + weight = 1 + url = "https://godoc.org/github.com/labstack/echo" ++++ diff --git a/website/content/godoc/engine.md b/website/content/godoc/engine.md new file mode 100644 index 000000000..785e48b8e --- /dev/null +++ b/website/content/godoc/engine.md @@ -0,0 +1,8 @@ ++++ +title = "engine" +[menu.side] + name = "engine" + parent = "godoc" + weight = 3 + url = "https://godoc.org/github.com/labstack/echo/engine" ++++ diff --git a/website/content/godoc/fasthttp.md b/website/content/godoc/fasthttp.md new file mode 100644 index 000000000..0d1b9a7ce --- /dev/null +++ b/website/content/godoc/fasthttp.md @@ -0,0 +1,8 @@ ++++ +title = "fasthttp" +[menu.side] + name = "engine/fasthttp" + parent = "godoc" + weight = 5 + url = "https://godoc.org/github.com/labstack/echo/engine/fasthttp" ++++ diff --git a/website/content/godoc/middleware.md b/website/content/godoc/middleware.md new file mode 100644 index 000000000..b9a3a8b58 --- /dev/null +++ b/website/content/godoc/middleware.md @@ -0,0 +1,9 @@ ++++ +title = "middleware" +[menu.side] + name = "middleware" + identifier = "godoc-middleware" + parent = "godoc" + weight = 1 + url = "https://godoc.org/github.com/labstack/echo/middleware" ++++ diff --git a/website/content/godoc/standard.md b/website/content/godoc/standard.md new file mode 100644 index 000000000..19e5e92dc --- /dev/null +++ b/website/content/godoc/standard.md @@ -0,0 +1,8 @@ ++++ +title = "standard" +[menu.side] + name = "engine/standard" + parent = "godoc" + weight = 4 + url = "https://godoc.org/github.com/labstack/echo/engine/standard" ++++ diff --git a/website/content/guide/context.md b/website/content/guide/context.md new file mode 100644 index 000000000..957aa3cd6 --- /dev/null +++ b/website/content/guide/context.md @@ -0,0 +1,58 @@ ++++ +title = "Context" +description = "Context in Echo" +[menu.side] + name = "Context" + identifier = "context" + parent = "guide" + weight = 5 ++++ + +## Context + +`echo.Context` represents the context of the current HTTP request. It holds request and +response reference, path, path parameters, data, registered handler and APIs to read +request and write response. As Context is an interface, it is easy to extend it with +custom APIs. + +#### Extending Context + +**Define a custom context** + +```go +type CustomContext struct { + echo.Context +} + +func (c *CustomContext) Foo() { + println("foo") +} + +func (c *CustomContext) Bar() { + println("bar") +} +``` + +**Create a middleware to extend default context** + +```go +e.Use(func(h echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + cc := &CustomContext{c} + return h(cc) + } +}) +``` + +> This middleware should be registered before any other middleware. + +**Use in handler** + +```go +e.Get("/", func(c echo.Context) error { + cc := c.(*CustomContext) + cc.Foo() + cc.Bar() + return cc.String(200, "OK") +}) +``` diff --git a/website/content/guide/cookies.md b/website/content/guide/cookies.md new file mode 100644 index 000000000..f7d4894c4 --- /dev/null +++ b/website/content/guide/cookies.md @@ -0,0 +1,78 @@ ++++ +title = "Cookies" +description = "Handling cookie in Echo" +[menu.side] + name = "Cookies" + parent = "guide" + weight = 6 ++++ + +## Cookies + +Cookie is a small piece of data sent from a website and stored in the user's web +browser while the user is browsing. Every time the user loads the website, the browser +sends the cookie back to the server to notify the user's previous activity. +Cookies were designed to be a reliable mechanism for websites to remember stateful +information (such as items added in the shopping cart in an online store) or to +record the user's browsing activity (including clicking particular buttons, logging +in, or recording which pages were visited in the past). Cookies can also store +passwords and form content a user has previously entered, such as a credit card +number or an address. + +### Cookie Attributes + +Attribute | Optional +:--- | :--- +`Name` | No +`Value` | No +`Path` | Yes +`Domain` | Yes +`Expires` | Yes +`Secure` | Yes +`HTTPOnly` | Yes + +### Create a Cookie + +```go +func writeCookie(c echo.Context) error { + cookie := new(echo.Cookie) + cookie.SetName("username") + cookie.SetValue("jon") + cookie.SetExpires(time.Now().Add(24 * time.Hour)) + c.SetCookie(cookie) + return c.String(http.StatusOK, "write a cookie") +} +``` + +- Cookie is created using `new(echo.Cookie)`. +- Attributes for the cookie are set using `Setter` functions. +- Finally `c.SetCookie(cookies)` adds a `Set-Cookie` header in HTTP response. + +### Read a Cookie + +```go +func readCookie(c echo.Context) error { + cookie, err := c.Cookie("username") + if err != nil { + return err + } + fmt.Println(cookie.Name()) + fmt.Println(cookie.Value()) + return c.String(http.StatusOK, "read a cookie") +} +``` + +- Cookie is read by name using `c.Cookie("username")` from the HTTP request. +- Cookie attributes are accessed using `Getter` function. + +### Read all Cookies + +```go +func readAllCookies(c echo.Context) error { + for _, cookie := range c.Cookies() { + fmt.Println(cookie.Name()) + fmt.Println(cookie.Value()) + } + return c.String(http.StatusOK, "read all cookie") +} +``` diff --git a/website/content/guide/customization.md b/website/content/guide/customization.md new file mode 100644 index 000000000..a555cb960 --- /dev/null +++ b/website/content/guide/customization.md @@ -0,0 +1,98 @@ ++++ +title = "Customization" +description = "Customizing Echo" +[menu.side] + name = "Customization" + parent = "guide" + weight = 3 ++++ + +## Customization + +### HTTP Error Handler + +`Echo#SetHTTPErrorHandler(h HTTPErrorHandler)` registers a custom `Echo#HTTPErrorHandler`. + +Default HTTP error handler rules: + +- If error is of type `Echo#HTTPError` it sends HTTP response with status code `HTTPError.Code` +and message `HTTPError.Message`. +- Else it sends `500 - Internal Server Error`. +- If debug mode is enabled, it uses `error.Error()` as status message. + +### Debug + +`Echo#SetDebug(on bool)` enable/disable debug mode. + +### Logging + +#### Custom Logger + +`Echo#SetLogger(l log.Logger)` + +SetLogger defines a custom logger. + +#### Log Output + +`Echo#SetLogOutput(w io.Writer)` sets the output destination for the logger. Default +value `os.Stdout` + +To completely disable logs use `Echo#SetLogOutput(io.Discard)` + +#### Log Level + +`Echo#SetLogLevel(l log.Level)` + +SetLogLevel sets the log level for the logger. Default value `5` (OFF). +Possible values: + +- `0` (DEBUG) +- `1` (INFO) +- `2` (WARN) +- `3` (ERROR) +- `4` (FATAL) +- `5` (OFF) + +### HTTP Engine + +Echo currently supports standard and [fasthttp](https://github.com/valyala/fasthttp) +server engines. Echo utilizes interfaces to abstract the internal implementation +of these servers so you can seamlessly switch from one engine to another based on +your preference. + +#### Running a standard HTTP server + +`e.Run(standard.New(":1323"))` + +#### Running a fasthttp server + +`e.Run(fasthttp.New(":1323"))` + +#### Running a server with TLS configuration + +`e.Run(.WithTLS(":1323", "", ""))` + +#### Running a server with engine configuration + +`e.Run(.WithConfig())` + +##### Configuration + +```go +Config struct { + Address string // TCP address to listen on. + Listener net.Listener // Custom `net.Listener`. If set, server accepts connections on it. + TLSCertFile string // TLS certificate file path. + TLSKeyFile string // TLS key file path. + ReadTimeout time.Duration // Maximum duration before timing out read of the request. + WriteTimeout time.Duration // Maximum duration before timing out write of the response. +} +``` + +#### Access internal server instance and configure its properties + +```go +s := standard.New(":1323") +s.MaxHeaderBytes = 1 << 20 +e.Run(s) +``` diff --git a/website/content/guide/error-handling.md b/website/content/guide/error-handling.md new file mode 100644 index 000000000..ab9c054cd --- /dev/null +++ b/website/content/guide/error-handling.md @@ -0,0 +1,49 @@ ++++ +title = "Error Handling" +description = "Error handling in Echo" +[menu.side] + name = "Error Handling" + parent = "guide" + weight = 8 ++++ + +## Error Handling + +Echo advocates centralized HTTP error handling by returning error from middleware +or handlers. + +- Log errors from a unified location +- Send customized HTTP responses + +For example, when basic auth middleware finds invalid credentials it returns +`401 - Unauthorized` error, aborting the current HTTP request. + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +func main() { + e := echo.New() + e.Use(echo.WrapMiddleware(func(c echo.Context) error { + // Extract the credentials from HTTP request header and perform a security + // check + + // For invalid credentials + return echo.NewHTTPError(http.StatusUnauthorized) + })) + e.GET("/", welcome) + e.Run(standard.New(":1323")) +} + +func welcome(c echo.Context) error { + return c.String(http.StatusOK, "Welcome!") +} +``` + +See how [HTTPErrorHandler](/guide/customization#http-error-handler) handles it. diff --git a/website/content/guide/faq.md b/website/content/guide/faq.md new file mode 100644 index 000000000..394dbb8ca --- /dev/null +++ b/website/content/guide/faq.md @@ -0,0 +1,90 @@ ++++ +title = "FAQ" +description = "Frequently asked questions in Echo" +[menu.side] + name = "FAQ" + parent = "guide" + weight = 20 ++++ + +## FAQ + +Q: **How to retrieve `*http.Request` and `http.ResponseWriter` from `echo.Context`?** + +- `http.Request` > `c.Request().(*standard.Request).Request` +- `http.ResponseWriter` > `c.Response()` + +> Standard engine only + +Q: **How to use standard handler `func(http.ResponseWriter, *http.Request)` with Echo?** + +```go +func handler(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, "Handler!") +} + +func main() { + e := echo.New() + e.GET("/", standard.WrapHandler(http.HandlerFunc(handler))) + e.Run(standard.New(":1323")) +} +``` + +Q: **How to use fasthttp handler `func(fasthttp.RequestCtx)` with Echo?** + +```go +func handler(c *fh.RequestCtx) { + io.WriteString(c, "Handler!") +} + +func main() { + e := echo.New() + e.GET("/", fasthttp.WrapHandler(handler)) + e.Run(fasthttp.New(":1323")) +} +``` + +Q: **How to use standard middleware `func(http.Handler) http.Handler` with Echo?** + +```go +func middleware(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + println("Middleware!") + h.ServeHTTP(w, r) + }) +} + +func main() { + e := echo.New() + e.Use(standard.WrapMiddleware(middleware)) + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "OK") + }) + e.Run(standard.New(":1323")) +} +``` + +Q: **How to use fasthttp middleware `func(http.Handler) http.Handler` with Echo?** + +```go +func middleware(h fh.RequestHandler) fh.RequestHandler { + return func(ctx *fh.RequestCtx) { + println("Middleware!") + h(ctx) + } +} + +func main() { + e := echo.New() + e.Use(fasthttp.WrapMiddleware(middleware)) + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "OK") + }) + e.Run(fasthttp.New(":1323")) +} +``` + + diff --git a/website/content/guide/installation.md b/website/content/guide/installation.md new file mode 100644 index 000000000..78449b9b6 --- /dev/null +++ b/website/content/guide/installation.md @@ -0,0 +1,23 @@ ++++ +title = "Installation" +description = "Installing Echo" +[menu.side] + name = "Installation" + parent = "guide" + weight = 1 ++++ + +## Installation + +Echo is developed and tested using Go `1.6.x` and `1.7.x` + +```sh +$ go get -u github.com/labstack/echo +``` + +> Ideally, you should rely on a [package manager](https://github.com/avelino/awesome-go#package-management) like glide or govendor to use a specific [version](https://github.com/labstack/echo/releases) of Echo. + +### [Migrating from v1](/guide/migrating) + +Echo follows [semantic versioning](http://semver.org) managed through GitHub releases. +Specific version of Echo can be installed using a [package manager](https://github.com/avelino/awesome-go#package-management). diff --git a/website/content/guide/migrating.md b/website/content/guide/migrating.md new file mode 100644 index 000000000..0baea8c18 --- /dev/null +++ b/website/content/guide/migrating.md @@ -0,0 +1,93 @@ ++++ +title = "Migrating" +description = "Migrating from Echo v1 to v2" +[menu.side] + name = "Migrating" + parent = "guide" + weight = 2 ++++ + +## Migrating from v1 + +### Change Log + +- Good news, 85% of the API remains the same. +- `Engine` interface to abstract `HTTP` server implementation, allowing +us to use HTTP servers beyond Go standard library. It currently supports standard and [fasthttp](https://github.com/valyala/fasthttp) server. +- Context, Request and Response are converted to interfaces. [More...](https://github.com/labstack/echo/issues/146) +- Handler signature is changed to `func (c echo.Context) error`. +- Dropped auto wrapping of handler and middleware to enforce compile time check. +- APIs to run middleware before or after the router, which doesn't require `Echo#Hook` API now. +- Ability to define middleware at route level. +- `Echo#HTTPError` exposed it's fields `Code` and `Message`. +- Option to specify log format in logger middleware and default logger. + +#### API + +v1 | v2 +--- | --- +`Context#Query()` | `Context#QueryParam()` +`Context#Form()` | `Context#FormValue()` + +### FAQ + +Q. How to access original objects from interfaces? + +A. Only if you need to... + +```go +// `*http.Request` +c.Request().(*standard.Request).Request + +// `*http.URL` +c.Request().URL().(*standard.URL).URL + +// Request `http.Header` +c.Request().Header().(*standard.Header).Header + +// `http.ResponseWriter` +c.Response().(*standard.Response).ResponseWriter + +// Response `http.Header` +c.Response().Header().(*standard.Header).Header +``` + +Q. How to use standard handler and middleware? + +A. + +```go +package main + +import ( + "net/http" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +// Standard middleware +func middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + println("standard middleware") + next.ServeHTTP(w, r) + }) +} + +// Standard handler +func handler(w http.ResponseWriter, r *http.Request) { + println("standard handler") +} + +func main() { + e := echo.New() + e.Use(standard.WrapMiddleware(middleware)) + e.GET("/", standard.WrapHandler(http.HandlerFunc(handler))) + e.Run(standard.New(":1323")) +} +``` + +### Next? + +- Browse through [recipes](/recipes/hello-world) freshly converted to v2. +- Read documentation and dig into test cases. diff --git a/website/content/guide/request.md b/website/content/guide/request.md new file mode 100644 index 000000000..f4ec131eb --- /dev/null +++ b/website/content/guide/request.md @@ -0,0 +1,98 @@ ++++ +title = "HTTP Request" +description = "Handling HTTP request in Echo" +[menu.side] + name = "Request" + parent = "guide" + weight = 6 ++++ + +## Request + +### Bind Request Body + +To bind request body into a provided Go type use `Context#Bind(interface{})`. +The default binder supports decoding application/json, application/xml and +application/x-www-form-urlencoded payload based on Context-Type header. + +*Example* + +TODO + +> Custom binder can be registered via `Echo#SetBinder(Binder)` + +### Query Parameter + +Query parameter can be retrieved by name using `Context#QueryParam(name string)`. + +*Example* + +```go +e.GET("/users", func(c echo.Context) error { + name := c.QueryParam("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#FormValue(name string)`. + +*Example* + +```go +e.POST("/users", func(c echo.Context) error { + name := c.FormValue("name") + return c.String(http.StatusOK, name) +}) +``` + +```sh +$ curl -d "name=joe" http://localhost:1323/users +``` + +### Path Parameter + +Registered 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.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) +}) +``` + +```sh +$ curl http://localhost:1323/users/joe +``` + + +### Handler Path + +`Context#Path()` returns the registered path for the handler, it can be used in the +middleware for logging purpose. + +*Example* + +```go +e.Use(func(c echo.Context) error { + println(c.Path()) // Prints `/users/:name` + return nil +}) +e.GET("/users/:name", func(c echo.Context) error) { + return c.String(http.StatusOK, name) +}) +``` diff --git a/website/content/guide/routing.md b/website/content/guide/routing.md new file mode 100644 index 000000000..3fa02126b --- /dev/null +++ b/website/content/guide/routing.md @@ -0,0 +1,115 @@ ++++ +title = "HTTP Routing" +description = "Routing HTTP request in Echo" +[menu.side] + name = "Routing" + parent = "guide" + weight = 4 ++++ + +## Routing + +Echo's router is [fast, optimized]({{< ref "index.md#performance">}}) and +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 matching handler. +For example, code below registers a route for method `GET`, path `/hello` and a +handler which sends `Hello, World!` HTTP response. + +```go +// Handler +func hello(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") +} + +// Route +e.GET("/hello", hello) +``` + +You can use `Echo.Any(path string, h Handler)` to register a handler for all HTTP methods. +If you want to register it for some methods use `Echo.Match(methods []string, path string, h Handler)`. + +Echo defines handler function as `func(echo.Context) error` where `echo.Context` primarily +holds HTTP request and response interfaces. + +### Match-any + +Matches zero or more characters in the path. For example, pattern `/users/*` will +match: + +- `/users/` +- `/users/1` +- `/users/1/files/1` +- `/users/anything...` + +### Path matching order + +- Static +- Param +- Match any + +#### Example + +```go +e.GET("/users/:id", func(c echo.Context) error { + return c.String(http.StatusOK, "/users/:id") +}) + +e.GET("/users/new", func(c echo.Context) error { + return c.String(http.StatusOK, "/users/new") +}) + +e.GET("/users/1/files/*", func(c echo.Context) error { + return c.String(http.StatusOK, "/users/1/files/*") +}) +``` + +Above routes would resolve in the following order: + +- `/users/new` +- `/users/:id` +- `/users/1/files/*` + +> Routes can be written in any order. + +### Group + +`Echo#Group(prefix string, m ...Middleware) *Group` + +Routes with common prefix can be grouped to define a new sub-router with optional +middleware. In addition to specified middleware group also inherits parent middleware. +To add middleware later in the group you can use `Group.Use(m ...Middleware)`. +Groups can also be nested. + +In the code below, we create an admin group which requires basic HTTP authentication +for routes `/admin/*`. + +```go +g := e.Group("/admin") +g.Use(middleware.BasicAuth(func(username, password string) bool { + if username == "joe" && password == "secret" { + return true + } + return false +})) +``` + +### URI building + +`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. + +`e.URI(h, 1)` will generate `/users/1` for the route registered below + +```go +// Handler +h := func(c echo.Context) error { + return c.String(http.StatusOK, "OK") +} + +// Route +e.GET("/users/:id", h) +``` diff --git a/website/content/guide/static-files.md b/website/content/guide/static-files.md new file mode 100644 index 000000000..e15afc855 --- /dev/null +++ b/website/content/guide/static-files.md @@ -0,0 +1,60 @@ ++++ +title = "Static Files" +description = "Serving static files in Echo" +[menu.side] + name = "Static Files" + parent = "guide" + weight = 3 ++++ + +Images, JavaScript, CSS, PDF, Fonts and so on... + +## Static Files + +### [Using Static Middleware]({{< ref "middleware/static.md">}}) + +### Using `Echo#Static()` + +`Echo#Static(prefix, root string)` registers a new route with path prefix to serve +static files from the provided root directory. + +*Usage 1* + +```go +e := echo.New() +e.Static("/static", "assets") +``` + +Example above will serve any file from the assets directory for path `/static/*`. For example, +a request to `/static/js/main.js` will fetch and serve `assets/js/main.js` file. + +*Usage 2* + +```go +e := echo.New() +e.Static("/", "assets") +``` + +Example above will serve any file from the assets directory for path `/*`. For example, +a request to `/js/main.js` will fetch and serve `assets/js/main.js` file. + +### Using `Echo#File()` + +`Echo#File(path, file string)` registers a new route with path to serve a static +file. + +*Usage 1* + +Serving an index page from `public/index.html` + +```go +e.File("/", "public/index.html") +``` + +*Usage 2* + +Serving a favicon from `images/favicon.ico` + +```go +e.File("/favicon.ico", "images/favicon.ico") +``` diff --git a/website/content/guide/templates.md b/website/content/guide/templates.md new file mode 100644 index 000000000..4c0c7d3a0 --- /dev/null +++ b/website/content/guide/templates.md @@ -0,0 +1,60 @@ ++++ +title = "Templates" +description = "How to use templates in Echo" +[menu.side] + name = "Templates" + parent = "guide" + weight = 3 ++++ + +## Templates + +### Template Rendering + +`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. + +Example below shows how to use Go `html/template`: + +1. Implement `echo.Renderer` interface + + ```go + type Template struct { + templates *template.Template + } + + func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error { + return t.templates.ExecuteTemplate(w, name, data) + } + ``` + +2. Pre-compile templates + + `public/views/hello.html` + + ```html + {{define "hello"}}Hello, {{.}}!{{end}} + ``` + + ```go + t := &Template{ + templates: template.Must(template.ParseGlob("public/views/*.html")), + } + ``` + +3. Register templates + + ```go + e := echo.New() + e.SetRenderer(t) + e.GET("/hello", Hello) + ``` + +4. Render a template inside your handler + + ```go + func Hello(c echo.Context) error { + return c.Render(http.StatusOK, "hello", "World") + } + ``` diff --git a/website/content/guide/testing.md b/website/content/guide/testing.md new file mode 100644 index 000000000..5b6af23cb --- /dev/null +++ b/website/content/guide/testing.md @@ -0,0 +1,161 @@ ++++ +title = "Testing" +description = "Testing handler and middleware in Echo" +[menu.side] + name = "Testing" + parent = "guide" + weight = 9 ++++ + +## Testing + +### Testing Handler + +`GET` `/users/:id` + +Handler below retrieves user by id from the database. If user is not found it returns +`404` error with a message. + +#### CreateUser + +`POST` `/users` + +- Accepts JSON payload +- On success `201 - Created` +- On error `500 - Internal Server Error` + +#### GetUser + +`GET` `/users/:email` + +- On success `200 - OK` +- On error `404 - Not Found` if user is not found otherwise `500 - Internal Server Error` + +`handler.go` + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo" +) + +type ( + User struct { + Name string `json:"name" form:"name"` + Email string `json:"email" form:"email"` + } + handler struct { + db map[string]*User + } +) + +func (h *handler) createUser(c echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) +} + +func (h *handler) getUser(c echo.Context) error { + email := c.Param("email") + user := h.db[email] + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "user not found") + } + return c.JSON(http.StatusOK, user) +} +``` + +`handler_test.go` + +```go +package handler + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" + "github.com/stretchr/testify/assert" +) + +var ( + mockDB = map[string]*User{ + "jon@labstack.com": &User{"Jon Snow", "jon@labstack.com"}, + } + userJSON = `{"name":"Jon Snow","email":"jon@labstack.com"}` +) + +func TestCreateUser(t *testing.T) { + // Setup + e := echo.New() + req, err := http.NewRequest(echo.POST, "/users", strings.NewReader(userJSON)) + if assert.NoError(t, err) { + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := e.NewContext(standard.NewRequest(req, e.Logger()), standard.NewResponse(rec, e.Logger())) + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.createUser(c)) { + assert.Equal(t, http.StatusCreated, rec.Code) + assert.Equal(t, userJSON, rec.Body.String()) + } + } +} + +func TestGetUser(t *testing.T) { + // Setup + e := echo.New() + req := new(http.Request) + rec := httptest.NewRecorder() + c := e.NewContext(standard.NewRequest(req, e.Logger()), standard.NewResponse(rec, e.Logger())) + c.SetPath("/users/:email") + c.SetParamNames("email") + c.SetParamValues("jon@labstack.com") + h := &handler{mockDB} + + // Assertions + if assert.NoError(t, h.getUser(c)) { + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, userJSON, rec.Body.String()) + } +} +``` + +#### Using Form Payload + +```go +f := make(url.Values) +f.Set("name", "Jon Snow") +f.Set("email", "jon@labstack.com") +req, err := http.NewRequest(echo.POST, "/", strings.NewReader(f.Encode())) +``` + +#### Setting Path Params + +```go +c.SetParamNames("id", "email") +c.SetParamValues("1", "jon@labstack.com") +``` + +#### Setting Query Params + +```go +q := make(url.Values) +q.Set("email", "jon@labstack.com") +req, err := http.NewRequest(echo.POST, "/?"+q.Encode(), nil) +``` + +### Testing Middleware + +*TBD* + +You can looking to built-in middleware [test cases](https://github.com/labstack/echo/tree/master/middleware). diff --git a/website/content/index.md b/website/content/index.md new file mode 100644 index 000000000..3c9115376 --- /dev/null +++ b/website/content/index.md @@ -0,0 +1,291 @@ ++++ +title = "Index" ++++ + +# Fast and unfancy HTTP server framework for Go (Golang). + +## Feature Overview + +- Optimized HTTP router which smartly prioritize routes +- Build robust and scalable RESTful APIs +- Run with standard HTTP server or FastHTTP server +- Group APIs +- Extensible middleware framework +- Define middleware at root, group or route level +- Data binding for JSON, XML and form payload +- Handy functions to send variety of HTTP responses +- Centralized HTTP error handling +- Template rendering with any template engine +- Define your format for the logger +- Highly customizable + + +## Performance + +- Environment: + - Go 1.6 + - wrk 4.0.0 + - 2 GB, 2 Core (DigitalOcean) +- Test Suite: https://github.com/vishr/web-framework-benchmark +- Date: 4/4/2016 + +Performance + +## Quick Start + +### Installation + +```sh +$ go get -u github.com/labstack/echo +``` + +### Hello, World! + +Create `server.go` + +```go +package main + +import ( + "net/http" + "github.com/labstack/echo" + "github.com/labstack/echo/engine/standard" +) + +func main() { + e := echo.New() + e.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Hello, World!") + }) + e.Run(standard.New(":1323")) +} +``` + +Start server + +```sh +$ go run server.go +``` + +Browse to [http://localhost:1323](http://localhost:1323) and you should see +Hello, World! on the page. + +### Routing + +```go +e.POST("/users", saveUser) +e.GET("/users/:id", getUser) +e.PUT("/users/:id", updateUser) +e.DELETE("/users/:id", deleteUser) +``` + +### Path Parameters + +```go +func getUser(c echo.Context) error { + // User ID from path `users/:id` + id := c.Param("id") +} +``` + +### Query Parameters + +`/show?team=x-men&member=wolverine` + +```go +func show(c echo.Context) error { + // Get team and member from the query string + team := c.QueryParam("team") + member := c.QueryParam("member") +} +``` + +### Form `application/x-www-form-urlencoded` + +`POST` `/save` + +name | value +:--- | :--- +name | Joe Smith +email | joe@labstack.com + +```go +func save(c echo.Context) error { + // Get name and email + name := c.FormValue("name") + email := c.FormValue("email") +} +``` + +### Form `multipart/form-data` + +`POST` `/save` + +name | value +:--- | :--- +name | Joe Smith +email | joe@labstack.com +avatar | avatar + +```go +func save(c echo.Context) error { + // Get name and email + name := c.FormValue("name") + email := c.FormValue("email") + // Get avatar + avatar, err := c.FormFile("avatar") + if err != nil { + return err + } + + // Source + src, err := avatar.Open() + if err != nil { + return err + } + defer src.Close() + + // Destination + dst, err := os.Create(avatar.Filename) + if err != nil { + return err + } + defer dst.Close() + + // Copy + if _, err = io.Copy(dst, src); err != nil { + return err + } + + return c.HTML(http.StatusOK, "Thank you!") +} +``` + +### Handling Request + +- Bind `JSON` or `XML` or `form` payload into Go struct based on `Content-Type` request header. +- Render response as `JSON` or `XML` with status code. + +```go +type User struct { + Name string `json:"name" xml:"name" form:"name"` + Email string `json:"email" xml:"email" form:"email"` +} + +e.POST("/users", func(c echo.Context) error { + u := new(User) + if err := c.Bind(u); err != nil { + return err + } + return c.JSON(http.StatusCreated, u) + // or + // return c.XML(http.StatusCreated, u) +}) +``` + +### Static Content + +Server any file from static directory for path `/static/*`. + +```go +e.Static("/static", "static") +``` + +##### [Learn More](https://echo.labstack.com/guide/static-files) + +### [Template Rendering](https://echo.labstack.com/guide/templates) + +### Middleware + +```go +// Root level middleware +e.Use(middleware.Logger()) +e.Use(middleware.Recover()) + +// Group level middleware +g := e.Group("/admin") +g.Use(middleware.BasicAuth(func(username, password string) bool { + if username == "joe" && password == "secret" { + return true + } + return false +})) + +// Route level middleware +track := func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + println("request to /users") + return next(c) + } +} +e.GET("/users", func(c echo.Context) error { + return c.String(http.StatusOK, "/users") +}, track) +``` + +#### Built-in Middleware + +Middleware | Description +:--- | :--- +[BodyLimit]({{< ref "middleware/body-limit.md">}}) | Limit request body +[Logger]({{< ref "middleware/logger.md">}}) | Log HTTP requests +[Recover]({{< ref "middleware/recover.md">}}) | Recover from panics +[Gzip]({{< ref "middleware/gzip.md">}}) | Send gzip HTTP response +[BasicAuth]({{< ref "middleware/basic-auth.md">}}) | HTTP basic authentication +[JWTAuth]({{< ref "middleware/jwt.md">}}) | JWT authentication +[Secure]({{< ref "middleware/secure.md">}}) | Protection against attacks +[CORS]({{< ref "middleware/cors.md">}}) | Cross-Origin Resource Sharing +[CSRF]({{< ref "middleware/csrf.md">}}) | Cross-Site Request Forgery +[Static]({{< ref "middleware/static.md">}}) | Serve static files +[HTTPSRedirect]({{< ref "middleware/redirect.md#httpsredirect-middleware">}}) | Redirect HTTP requests to HTTPS +[HTTPSWWWRedirect]({{< ref "middleware/redirect.md#httpswwwredirect-middleware">}}) | Redirect HTTP requests to WWW HTTPS +[WWWRedirect]({{< ref "middleware/redirect.md#wwwredirect-middleware">}}) | Redirect non WWW requests to WWW +[NonWWWRedirect]({{< ref "middleware/redirect.md#nonwwwredirect-middleware">}}) | Redirect WWW requests to non WWW +[AddTrailingSlash]({{< ref "middleware/trailing-slash.md#addtrailingslash-middleware">}}) | Add trailing slash to the request URI +[RemoveTrailingSlash]({{< ref "middleware/trailing-slash.md#removetrailingslash-middleware">}}) | Remove trailing slash from the request URI +[MethodOverride]({{< ref "middleware/method-override.md">}}) | Override request method + +#### Third-party Middleware + +Middleware | Description +:--- | :--- +[echoperm](https://github.com/xyproto/echoperm) | Keeping track of users, login states and permissions. +[echopprof](https://github.com/mtojek/echopprof) | Adapt net/http/pprof to labstack/echo. + +##### [Learn More](https://echo.labstack.com/middleware/overview) + +### Next + +- Head over to [guide](https://echo.labstack.com/guide/installation) +- Browse [recipes](https://echo.labstack.com/recipes/hello-world) + +### Need help? + +- [Hop on to chat](https://gitter.im/labstack/echo) +- [Open an issue](https://github.com/labstack/echo/issues/new) + +## Support Echo + +- ☆ the project +- [Donate](https://echo.labstack.com/support-echo) +- 🌐 spread the word +- [Contribute](#contribute:d680e8a854a7cbad6d490c445cba2eba) to the project + +## Contribute + +**Use issues for everything** + +- Report issues +- Discuss on chat before sending a pull request +- Suggest new features or enhancements +- Improve/fix documentation + +## Credits + +- [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/website/content/middleware/basic-auth.md b/website/content/middleware/basic-auth.md new file mode 100644 index 000000000..6bf24c01b --- /dev/null +++ b/website/content/middleware/basic-auth.md @@ -0,0 +1,59 @@ ++++ +title = "BasicAuth Middleware" +description = "Basic auth middleware for Echo" +[menu.side] + name = "BasicAuth" + parent = "middleware" + weight = 5 ++++ + +## BasicAuth Middleware + +BasicAuth middleware provides an HTTP basic authentication. + +- For valid credentials it calls the next handler. +- For invalid credentials, it sends "401 - Unauthorized" response. +- For empty or invalid `Authorization` header, it sends "400 - Bad Request" response. + +*Usage* + +```go +e := echo.New() +e.Use(middleware.BasicAuth(func(username, password string) bool { + if username == "joe" && password == "secret" { + return true + } + return false +})) +``` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.BasicAuthWithConfig(middleware.BasicAuthConfig{}, +})) +``` + +### Configuration + +```go +BasicAuthConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Validator is a function to validate BasicAuth credentials. + // Required. + Validator BasicAuthValidator +} +``` + +*Default Configuration* + +```go +DefaultBasicAuthConfig = BasicAuthConfig{ + Skipper: defaultSkipper, +} +``` diff --git a/website/content/middleware/body-limit.md b/website/content/middleware/body-limit.md new file mode 100644 index 000000000..4d342b0fc --- /dev/null +++ b/website/content/middleware/body-limit.md @@ -0,0 +1,55 @@ ++++ +title = "BodyLimit Middleware" +description = "Body limit middleware for Echo" +[menu.side] + name = "BodyLimit" + parent = "middleware" + weight = 5 ++++ + +## BodyLimit Middleware + +BodyLimit middleware sets the maximum allowed size for a request body, if the +size exceeds the configured limit, it sends "413 - Request Entity Too Large" +response. The body limit is determined based on both `Content-Length` request +header and actual content read, which makes it super secure. + +Limit can be specified as `4x` or `4xB`, where x is one of the multiple from K, M, +G, T or P. + +*Usage* + +```go +e := echo.New() +e.Use(middleware.BodyLimit("2M")) +``` +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.BodyLimitWithConfig(middleware.BodyLimitConfig{}, +})) +``` + +### Configuration + +```go +BodyLimitConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Maximum allowed size for a request body, it can be specified + // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. + Limit string `json:"limit"` +} +``` + +*Default Configuration* + +```go +DefaultBodyLimitConfig = BodyLimitConfig{ + Skipper: defaultSkipper, +} +``` diff --git a/website/content/middleware/cors.md b/website/content/middleware/cors.md new file mode 100644 index 000000000..d2a1c0fe8 --- /dev/null +++ b/website/content/middleware/cors.md @@ -0,0 +1,80 @@ ++++ +title = "CORS Middleware" +description = "CORS middleware for Echo" +[menu.side] + name = "CORS" + parent = "middleware" + weight = 5 ++++ + +## CORS Middleware + +CORS middleware implements [CORS](http://www.w3.org/TR/cors) specification. +CORS gives web servers cross-domain access controls, which enable secure cross-domain +data transfers. + +*Usage* + +`e.Use(middleware.CORS())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ + AllowOrigins: []string{"https://labstack.com", "https://labstack.net"}, + AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept}, +})) +``` + +### Configuration + +```go +CORSConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // AllowOrigin defines a list of origins that may access the resource. + // Optional. Default value []string{"*"}. + AllowOrigins []string `json:"allow_origins"` + + // AllowMethods defines a list methods allowed when accessing the resource. + // This is used in response to a preflight request. + // Optional. Default value DefaultCORSConfig.AllowMethods. + AllowMethods []string `json:"allow_methods"` + + // AllowHeaders defines a list of request headers that can be used when + // making the actual request. This in response to a preflight request. + // Optional. Default value []string{}. + AllowHeaders []string `json:"allow_headers"` + + // AllowCredentials indicates whether or not the response to the request + // can be exposed when the credentials flag is true. When used as part of + // a response to a preflight request, this indicates whether or not the + // actual request can be made using credentials. + // Optional. Default value false. + AllowCredentials bool `json:"allow_credentials"` + + // ExposeHeaders defines a whitelist headers that clients are allowed to + // access. + // Optional. Default value []string{}. + ExposeHeaders []string `json:"expose_headers"` + + // MaxAge indicates how long (in seconds) the results of a preflight request + // can be cached. + // Optional. Default value 0. + MaxAge int `json:"max_age"` +} +``` + +*Default Configuration* + +```go +DefaultCORSConfig = CORSConfig{ + Skipper: defaultSkipper, + AllowOrigins: []string{"*"}, + AllowMethods: []string{echo.GET, echo.HEAD, echo.PUT, echo.PATCH, echo.POST, echo.DELETE}, +} +``` diff --git a/website/content/middleware/csrf.md b/website/content/middleware/csrf.md new file mode 100644 index 000000000..2a9d4f874 --- /dev/null +++ b/website/content/middleware/csrf.md @@ -0,0 +1,107 @@ ++++ +title = "CSRF Middleware" +description = "CSRF middleware for Echo" +[menu.side] + name = "CSRF" + parent = "middleware" + weight = 5 ++++ + +## CSRF Middleware + +Cross-site request forgery, also known as one-click attack or session riding and +abbreviated as CSRF (sometimes pronounced sea-surf) or XSRF, is a type of malicious +exploit of a website where unauthorized commands are transmitted from a user that +the website trusts. + +*Usage* + +`e.Use(middleware.CSRF())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "header:X-XSRF-TOKEN", +})) +``` + +Example above uses `X-XSRF-TOKEN` request header to extract CSRF token. + +### Accessing CSRF Token + +#### Server-side + +CSRF token can be accessed from `Echo#Context` using `ContextKey` and passed to +the client via template. + +#### Client-side + +CSRF token can be accessed from CSRF cookie. + +### Configuration + +```go +// CSRFConfig defines the config for CSRF middleware. +CSRFConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // TokenLength is the length of the generated token. + TokenLength uint8 `json:"token_length"` + // Optional. Default value 32. + + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:X-CSRF-Token". + // Possible values: + // - "header:" + // - "form:" + // - "query:" + TokenLookup string `json:"token_lookup"` + + // Context key to store generated CSRF token into context. + // Optional. Default value "csrf". + ContextKey string `json:"context_key"` + + // Name of the CSRF cookie. This cookie will store CSRF token. + // Optional. Default value "csrf". + CookieName string `json:"cookie_name"` + + // Domain of the CSRF cookie. + // Optional. Default value none. + CookieDomain string `json:"cookie_domain"` + + // Path of the CSRF cookie. + // Optional. Default value none. + CookiePath string `json:"cookie_path"` + + // Max age (in seconds) of the CSRF cookie. + // Optional. Default value 86400 (24hr). + CookieMaxAge int `json:"cookie_max_age"` + + // Indicates if CSRF cookie is secure. + // Optional. Default value false. + CookieSecure bool `json:"cookie_secure"` + + // Indicates if CSRF cookie is HTTP only. + // Optional. Default value false. + CookieHTTPOnly bool `json:"cookie_http_only"` +} +``` + +*Default Configuration* + +```go +DefaultCSRFConfig = CSRFConfig{ + Skipper: defaultSkipper, + TokenLength: 32, + TokenLookup: "header:" + echo.HeaderXCSRFToken, + ContextKey: "csrf", + CookieName: "_csrf", + CookieMaxAge: 86400, +} +``` diff --git a/website/content/middleware/gzip.md b/website/content/middleware/gzip.md new file mode 100644 index 000000000..97ab46b35 --- /dev/null +++ b/website/content/middleware/gzip.md @@ -0,0 +1,49 @@ ++++ +title = "Gzip Middleware" +description = "Gzip middleware for Echo" +[menu.side] + name = "Gzip" + parent = "middleware" + weight = 5 ++++ + +## Gzip Middleware + +Gzip middleware compresses HTTP response using gzip compression scheme. + +*Usage* + +`e.Use(middleware.Gzip())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, +})) +``` + +### Configuration + +```go +GzipConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Gzip compression level. + // Optional. Default value -1. + Level int `json:"level"` +} +``` + +*Default Configuration* + +```go +DefaultGzipConfig = GzipConfig{ + Skipper: defaultSkipper, + Level: -1, +} +``` diff --git a/website/content/middleware/jwt.md b/website/content/middleware/jwt.md new file mode 100644 index 000000000..393227d0e --- /dev/null +++ b/website/content/middleware/jwt.md @@ -0,0 +1,81 @@ ++++ +title = "JWT Middleware" +description = "JWT middleware for Echo" +[menu.side] + name = "JWT" + parent = "middleware" + weight = 5 ++++ + +## JWT Middleware + +JWT provides a JSON Web Token (JWT) authentication middleware. + +- For valid token, it sets the user in context and calls next handler. +- For invalid token, it sends "401 - Unauthorized" response. +- For empty or invalid `Authorization` header, it sends "400 - Bad Request". + +*Usage* + +`e.Use(middleware.JWT([]byte("secret"))` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.JWTWithConfig(middleware.JWTConfig{ + SigningKey: []byte("secret"), + TokenLookup: "query:token", +})) +``` + +### Configuration + +```go +// JWTConfig defines the config for JWT middleware. +JWTConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Signing key to validate token. + // Required. + SigningKey interface{} `json:"signing_key"` + + // Signing method, used to check token signing method. + // Optional. Default value HS256. + SigningMethod string `json:"signing_method"` + + // Context key to store user information from the token into context. + // Optional. Default value "user". + ContextKey string `json:"context_key"` + + // Claims are extendable claims data defining token content. + // Optional. Default value jwt.MapClaims + Claims jwt.Claims + + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "cookie:" + TokenLookup string `json:"token_lookup"` +} +``` + +*Default Configuration* + +```go +DefaultJWTConfig = JWTConfig{ + Skipper: defaultSkipper, + SigningMethod: AlgorithmHS256, + ContextKey: "user", + TokenLookup: "header:" + echo.HeaderAuthorization, + Claims: jwt.MapClaims{}, +} +``` + +### [Recipe]({{< ref "recipes/jwt.md">}}) diff --git a/website/content/middleware/logger.md b/website/content/middleware/logger.md new file mode 100644 index 000000000..337544550 --- /dev/null +++ b/website/content/middleware/logger.md @@ -0,0 +1,89 @@ ++++ +title = "Logger Middleware" +description = "Logger middleware for Echo" +[menu.side] + name = "Logger" + parent = "middleware" + weight = 5 ++++ + +## Logger Middleware + +Logger middleware logs the information about each HTTP request. + +*Usage* + +`e.Use(middleware.Logger())` + +*Sample Output* + +```js +{"time":"2016-05-10T07:02:25-07:00","remote_ip":"::1","method":"GET","uri":"/","status":200, "latency":55653,"latency_human":"55.653µs","rx_bytes":0,"tx_bytes":13} +``` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Format: "method=${method}, uri=${uri}, status=${status}\n", +})) +``` + +Example above uses a `Format` which logs request method and request URI. + +*Sample Output* + +```sh +method=GET, uri=/hello, status=200 +``` + +### Configuration + +```go +LoggerConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Log format which can be constructed using the following tags: + // + // - time_rfc3339 + // - id (Request ID - Not implemented) + // - remote_ip + // - uri + // - host + // - method + // - path + // - referer + // - user_agent + // - status + // - latency (In microseconds) + // - latency_human (Human readable) + // - bytes_in (Bytes received) + // - bytes_out (Bytes sent) + // + // Example "${remote_ip} ${status}" + // + // Optional. Default value DefaultLoggerConfig.Format. + Format string `json:"format"` + + // Output is a writer where logs are written. + // Optional. Default value os.Stdout. + Output io.Writer +} +``` + +*Default Configuration* + +```go +DefaultLoggerConfig = LoggerConfig{ + Skipper: defaultSkipper, + Format: `{"time":"${time_rfc3339}","remote_ip":"${remote_ip}",` + + `"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` + + `"bytes_out":${bytes_out}}` + "\n", + Output: os.Stdout, +} +``` diff --git a/website/content/middleware/method-override.md b/website/content/middleware/method-override.md new file mode 100644 index 000000000..13f016812 --- /dev/null +++ b/website/content/middleware/method-override.md @@ -0,0 +1,52 @@ ++++ +title = "MethodOverride Middleware" +description = "Method override middleware for Echo" +[menu.side] + name = "MethodOverride" + parent = "middleware" + weight = 5 ++++ + +## MethodOverride Middleware + +MethodOverride middleware checks for the overridden method from the request and +uses it instead of the original method. + +For security reasons, only `POST` method can be overridden. + +*Usage* + +`e.Pre(middleware.MethodOverride())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Pre(middleware.MethodOverrideWithConfig(middleware.MethodOverrideConfig{ + Getter: middleware.MethodFromForm("_method"), +})) +``` + +### Configuration + +```go +MethodOverrideConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Getter is a function that gets overridden method from the request. + // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). + Getter MethodOverrideGetter +} +``` + +*Default Configuration* + +```go +DefaultMethodOverrideConfig = MethodOverrideConfig{ + Skipper: defaultSkipper, + Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), +} +``` diff --git a/website/content/middleware/overview.md b/website/content/middleware/overview.md new file mode 100644 index 000000000..58e3b9dd6 --- /dev/null +++ b/website/content/middleware/overview.md @@ -0,0 +1,105 @@ ++++ +title = "Overview" +description = "Overview of Echo middleware" +[menu.side] + name = "Overview" + parent = "middleware" + weight = 1 ++++ + +## Middleware Overview + +Middleware is a function chained in the HTTP request-response cycle with access +to `Echo#Context` which it uses to perform a specific action, for example, logging +every request or limiting the number of requests. + +Handler is processed in the end after all middleware are finished executing. + +### Middleware Levels + +#### Root Level (Before router) + +`Echo#Pre()` can be used to register a middleware which is executed before router +processes the request. It is helpful to make any changes to the request properties, +for example, adding or removing a trailing slash from the path so it matches the +route. + +The following built-in middleware should be registered at this level: + +- HTTPSRedirect +- HTTPSWWWRedirect +- WWWRedirect +- NonWWWRedirect +- AddTrailingSlash +- RemoveTrailingSlash +- MethodOverride + +> As router has not processed the request, middleware at this level won't +have access to any path related API from `echo.Context`. + +#### Root Level (After router) + +Most of the time you will register a middleware at this level using `Echo#Use()`. +This middleware is executed after router processes the request and has full access +to `echo.Context` API. + +The following built-in middleware should be registered at this level: + +- BodyLimit +- Logger +- Gzip +- Recover +- BasicAuth +- JWTAuth +- Secure +- CORS +- Static + +#### Group Level + +When creating a new group, you can register middleware just for that group. For +example, you can have an admin group which is secured by registering a BasicAuth +middleware for it. + +*Usage* + +```go +e := echo.New() +admin := e.Group("/admin", middleware.BasicAuth()) +``` + +You can also add a middleware after creating a group via `admin.Use()`. + +#### Route Level + +When defining a new route, you can optionally register middleware just for it. + +*Usage* + +```go +e := echo.New() +e.GET("/", , ) +``` + +### Skipping Middleware + +There are cases when you would like to skip a middleware based on some condition, +for that each middleware has an option to define a function `Skipper func(c echo.Context) bool`. + +*Usage* + +```go +e := echo.New() +e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ + Skipper: func(c echo.Context) bool { + if strings.HasPrefix(c.Request().Host(), "localhost") { + return true + } + return false + }, +})) +``` + +Example above skips Logger middleware when request host starts with localhost. + +### [Writing Custom Middleware]({{< ref "recipes/middleware.md">}}) diff --git a/website/content/middleware/recover.md b/website/content/middleware/recover.md new file mode 100644 index 000000000..912e04d04 --- /dev/null +++ b/website/content/middleware/recover.md @@ -0,0 +1,65 @@ ++++ +title = "Recover Middleware" +description = "Recover middleware for Echo" +[menu.side] + name = "Recover" + parent = "middleware" + weight = 5 ++++ + +## Recover Middleware + +Recover middleware recovers from panics anywhere in the chain, prints stack trace +and handles the control to the centralized +[HTTPErrorHandler]({{< ref "guide/customization.md#http-error-handler">}}). + +*Usage* + +`e.Use(middleware.Recover())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{ + StackSize: 1 << 10, // 1 KB +})) +``` + +Example above uses a `StackSize` of 1 KB and default values for `DisableStackAll` +and `DisablePrintStack`. + +### Configuration + +```go +RecoverConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Size of the stack to be printed. + // Optional. Default value 4KB. + StackSize int `json:"stack_size"` + + // DisableStackAll disables formatting stack traces of all other goroutines + // into buffer after the trace for the current goroutine. + // Optional. Default value false. + DisableStackAll bool `json:"disable_stack_all"` + + // DisablePrintStack disables printing stack trace. + // Optional. Default value as false. + DisablePrintStack bool `json:"disable_print_stack"` +} +``` + +*Default Configuration* + +```go +DefaultRecoverConfig = RecoverConfig{ + Skipper: defaultSkipper, + StackSize: 4 << 10, // 4 KB + DisableStackAll: false, + DisablePrintStack: false, +} +``` diff --git a/website/content/middleware/redirect.md b/website/content/middleware/redirect.md new file mode 100644 index 000000000..4735f637d --- /dev/null +++ b/website/content/middleware/redirect.md @@ -0,0 +1,104 @@ ++++ +title = "Redirect Middleware" +description = "Redirect middleware for Echo" +[menu.side] + name = "Redirect" + parent = "middleware" + weight = 5 ++++ + +## HTTPSRedirect Middleware + +HTTPSRedirect middleware redirects http requests to https. +For example, http://labstack.com will be redirected to https://labstack.com. + +*Usage* + +```go +e := echo.New() +e.Pre(middleware.HTTPSRedirect()) +``` + +## HTTPSWWWRedirect Middleware + +HTTPSWWWRedirect redirects http requests to www https. +For example, http://labstack.com will be redirected to https://www.labstack.com. + +*Usage* + +```go +e := echo.New() +e.Pre(middleware.HTTPSWWWRedirect()) +``` + +## HTTPSNonWWWRedirect Middleware + +HTTPSNonWWWRedirect redirects http requests to https non www. +For example, http://www.labstack.com will be redirect to https://labstack.com. + +*Usage* + +```go +e := echo.New() +e.Pre(HTTPSNonWWWRedirect()) +``` + +## WWWRedirect Middleware + +WWWRedirect redirects non www requests to www. + +For example, http://labstack.com will be redirected to http://www.labstack.com. + +*Usage* + +```go +e := echo.New() +e.Pre(middleware.WWWRedirect()) +``` + +## NonWWWRedirect Middleware + +NonWWWRedirect redirects www requests to non www. +For example, http://www.labstack.com will be redirected to http://labstack.com. + +*Usage* + +```go +e := echo.New() +e.Pre(middleware.NonWWWRedirect()) +``` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.HTTPSRedirectWithConfig(middleware.RedirectConfig{ + Code: http.StatusTemporaryRedirect, +})) +``` + +Example above will redirect the request HTTP to HTTPS with status code `307 - StatusTemporaryRedirect`. + +### Configuration + +```go +RedirectConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // Status code to be used when redirecting the request. + // Optional. Default value http.StatusMovedPermanently. + Code int `json:"code"` +} +``` + +*Default Configuration* + +```go +DefaultRedirectConfig = RedirectConfig{ + Skipper: defaultSkipper, + Code: http.StatusMovedPermanently, +} +``` diff --git a/website/content/middleware/secure.md b/website/content/middleware/secure.md new file mode 100644 index 000000000..7f54dca33 --- /dev/null +++ b/website/content/middleware/secure.md @@ -0,0 +1,98 @@ ++++ +title = "Secure Middleware" +description = "Secure middleware for Echo" +[menu.side] + name = "Secure" + parent = "middleware" + weight = 5 ++++ + +## Secure Middleware + +Secure middleware provides protection against cross-site scripting (XSS) attack, +content type sniffing, clickjacking, insecure connection and other code injection +attacks. + +*Usage* + +`e.Use(middleware.Secure())` + +### Custom Configuration + +*Usage* + +```go +e := echo.New() +e.Use(middleware.SecureWithConfig(middleware.SecureConfig{ + XSSProtection: "", + ContentTypeNosniff: "", + XFrameOptions: "", + HSTSMaxAge: 3600, + ContentSecurityPolicy: "default-src 'self'", +})) +``` + +Passing empty `XSSProtection`, `ContentTypeNosniff`, `XFrameOptions` or `ContentSecurityPolicy` +disables that protection. + +### Configuration + +```go +SecureConfig struct { + // Skipper defines a function to skip middleware. + Skipper Skipper + + // XSSProtection provides protection against cross-site scripting attack (XSS) + // by setting the `X-XSS-Protection` header. + // Optional. Default value "1; mode=block". + XSSProtection string `json:"xss_protection"` + + // ContentTypeNosniff provides protection against overriding Content-Type + // header by setting the `X-Content-Type-Options` header. + // Optional. Default value "nosniff". + ContentTypeNosniff string `json:"content_type_nosniff"` + + // XFrameOptions can be used to indicate whether or not a browser should + // be allowed to render a page in a ,