From 69754a99460898dd1e7faf8419a2d7d0cee21490 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 9 Oct 2018 18:07:43 +0200 Subject: [PATCH 01/23] Add middleware --- default.go | 18 ++++++++ group.go | 41 +++++++++-------- router.go | 133 +++++++++++++++++++++++++++++++++++------------------ 3 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 default.go diff --git a/default.go b/default.go new file mode 100644 index 0000000..99cc394 --- /dev/null +++ b/default.go @@ -0,0 +1,18 @@ +package router + +import ( + "fmt" +) + +func defaultNotFoundHandler(c *Context) error { + return c.String(404, `not found`) +} + +func defaultMethodNotAllowedHandler(c *Context) error { + return c.String(504, `method not allowed`) +} + +func defaultErrorHandler(c *Context, err interface{}) { + fmt.Println(err) + c.String(500, `internal server error`) +} diff --git a/group.go b/group.go index 2e7bb80..e9a1451 100644 --- a/group.go +++ b/group.go @@ -6,48 +6,49 @@ func join(prefix, path string) string { return urlpath.Join(prefix, urlpath.Clean(path)) } +// Group is a router group with a shared prefix and set of middlewares type Group struct { - router *Router - prefix string + router *Router + prefix string + middleware []Middleware } -func (g *Group) Group(prefix string) *Group { - return &Group{prefix: join(g.prefix, prefix), router: g.router} +// Group creates a new router group with a shared prefix and set of middlewares +func (g *Group) Group(prefix string, middleware ...Middleware) *Group { + return &Group{prefix: join(g.prefix, prefix), router: g.router, middleware: append(g.middleware, middleware...)} } // GET adds a GET route -func (g *Group) GET(path string, handle GetHandle) { - g.router.GET(join(g.prefix, path), handle) +func (g *Group) GET(path string, handle Handle, middleware ...Middleware) { + g.router.GET(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // POST adds a POST route -func (g *Group) POST(path string, handle interface{}) { - g.router.POST(join(g.prefix, path), handle) +func (g *Group) POST(path string, handle interface{}, middleware ...Middleware) { + g.router.POST(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // DELETE adds a DELETE route -func (g *Group) DELETE(path string, handle GetHandle) { - g.router.DELETE(join(g.prefix, path), handle) +func (g *Group) DELETE(path string, handle Handle, middleware ...Middleware) { + g.router.DELETE(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // PUT adds a PUT route -func (g *Group) PUT(path string, handle interface{}) { - checkInterfaceHandle(handle) - g.router.PUT(join(g.prefix, path), handle) +func (g *Group) PUT(path string, handle interface{}, middleware ...Middleware) { + g.router.PUT(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // PATCH adds a PATCH route -func (g *Group) PATCH(path string, handle interface{}) { - checkInterfaceHandle(handle) - g.router.PATCH(join(g.prefix, path), handle) +func (g *Group) PATCH(path string, handle interface{}, middleware ...Middleware) { + g.router.PATCH(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // HEAD adds a HEAD route -func (g *Group) HEAD(path string, handle GetHandle) { - g.router.HEAD(join(g.prefix, path), handle) +func (g *Group) HEAD(path string, handle Handle, middleware ...Middleware) { + g.router.HEAD(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } // OPTIONS adds a OPTIONS route -func (g *Group) OPTIONS(path string, handle GetHandle) { - g.router.OPTIONS(join(g.prefix, path), handle) +func (g *Group) OPTIONS(path string, handle Handle, middleware ...Middleware) { + g.router.OPTIONS(join(g.prefix, path), handle, append(g.middleware, middleware...)...) } diff --git a/router.go b/router.go index 4c1c616..720c417 100644 --- a/router.go +++ b/router.go @@ -2,7 +2,6 @@ package router import ( "encoding/json" - "fmt" "net/http" "reflect" @@ -10,65 +9,82 @@ import ( ) type route struct { - Method string - Path string - Handle interface{} + Method string + Path string + Handle interface{} + Middleware []Middleware } -// GetHandle handles a request that doesn't receive a body -type GetHandle func(*Context) error +// Handle handles a request +type Handle func(*Context) error + +// ErrorHandle handles a request +type ErrorHandle func(*Context, interface{}) + +// Middleware TODO: +type Middleware func(Handle) Handle // Router is the router itself type Router struct { - routes []route - Renderer Renderer + routes []route + Renderer Renderer + middleware []Middleware + NotFoundHandler Handle + MethodNotAllowedHandler Handle + ErrorHandler ErrorHandle } // New returns a new Router func New() *Router { - return &Router{} + return &Router{NotFoundHandler: defaultNotFoundHandler, MethodNotAllowedHandler: defaultMethodNotAllowedHandler, ErrorHandler: defaultErrorHandler} } -func (r *Router) Group(prefix string) *Group { - return &Group{prefix: prefix, router: r} +// Use adds a global middleware +func (r *Router) Use(m ...Middleware) { + r.middleware = append(r.middleware, m...) +} + +// Group creates a new router group with a shared prefix and set of middlewares +func (r *Router) Group(prefix string, middleware ...Middleware) *Group { + return &Group{prefix: prefix, router: r, middleware: middleware} } // GET adds a GET route -func (r *Router) GET(path string, handle GetHandle) { - r.routes = append(r.routes, route{`GET`, path, handle}) +func (r *Router) GET(path string, handle Handle, middleware ...Middleware) { + r.routes = append(r.routes, route{`GET`, path, handle, middleware}) } // POST adds a POST route -func (r *Router) POST(path string, handle interface{}) { +func (r *Router) POST(path string, handle interface{}, middleware ...Middleware) { checkInterfaceHandle(handle) - r.routes = append(r.routes, route{`POST`, path, handle}) + r.routes = append(r.routes, route{`POST`, path, handle, middleware}) } // DELETE adds a DELETE route -func (r *Router) DELETE(path string, handle GetHandle) { - r.routes = append(r.routes, route{`DELETE`, path, handle}) +func (r *Router) DELETE(path string, handle Handle, middleware ...Middleware) { + r.routes = append(r.routes, route{`DELETE`, path, handle, middleware}) } // PUT adds a PUT route -func (r *Router) PUT(path string, handle interface{}) { +func (r *Router) PUT(path string, handle interface{}, middleware ...Middleware) { checkInterfaceHandle(handle) - r.routes = append(r.routes, route{`PUT`, path, handle}) + r.routes = append(r.routes, route{`PUT`, path, handle, middleware}) } // PATCH adds a PATCH route -func (r *Router) PATCH(path string, handle interface{}) { +func (r *Router) PATCH(path string, handle interface{}, middleware ...Middleware) { checkInterfaceHandle(handle) - r.routes = append(r.routes, route{`PATCH`, path, handle}) + r.routes = append(r.routes, route{`PATCH`, path, handle, middleware}) } // HEAD adds a HEAD route -func (r *Router) HEAD(path string, handle GetHandle) { - r.routes = append(r.routes, route{`HEAD`, path, handle}) +func (r *Router) HEAD(path string, handle Handle, middleware ...Middleware) { + r.routes = append(r.routes, route{`HEAD`, path, handle, middleware}) } // OPTIONS adds a OPTIONS route -func (r *Router) OPTIONS(path string, handle GetHandle) { - r.routes = append(r.routes, route{`OPTIONS`, path, handle}) +func (r *Router) OPTIONS(path string, handle Handle, middleware ...Middleware) { + r.routes = append(r.routes, route{`OPTIONS`, path, handle, middleware}) } // Start starts the web server and binds to the given address @@ -82,19 +98,39 @@ func (r *Router) getHttpr() *httprouter.Router { httpr := httprouter.New() for _, v := range r.routes { - if handle, ok := v.Handle.(GetHandle); ok { - httpr.Handle(v.Method, v.Path, handleGET(r, handle)) - continue + handle, ok := v.Handle.(Handle) + if !ok { + handle = handlePOST(r, v.Handle) } - httpr.Handle(v.Method, v.Path, handlePOST(r, v.Handle)) + httpr.Handle(v.Method, v.Path, handleReq(r, handle, append(r.middleware, v.Middleware...))) + } + + httpr.NotFound = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + handleReq(r, r.NotFoundHandler, r.middleware)(res, req, nil) + }) + + httpr.MethodNotAllowed = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + handleReq(r, r.MethodNotAllowedHandler, r.middleware)(res, req, nil) + }) + + httpr.PanicHandler = func(res http.ResponseWriter, req *http.Request, err interface{}) { + c := newContext(r, res, req, nil) + r.ErrorHandler(c, err) } return httpr } +func handleErr(errHandler ErrorHandle, err interface{}) Handle { + return func(c *Context) error { + errHandler(c, err) + return nil + } +} + func checkInterfaceHandle(f interface{}) { - if _, ok := f.(GetHandle); ok { + if _, ok := f.(Handle); ok { return } @@ -119,34 +155,41 @@ func checkInterfaceHandle(f interface{}) { return } -func handlePOST(r *Router, f interface{}) httprouter.Handle { +func handlePOST(r *Router, f interface{}) Handle { funcRv, inputRt := reflect.ValueOf(f), reflect.TypeOf(f).In(1) - return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) { - c := newContext(r, res, req, param) - + return func(c *Context) error { data := reflect.New(inputRt) - { - err := json.NewDecoder(req.Body).Decode(data.Interface()) - req.Body.Close() - if err != nil { - c.NoContent(400) // TODO: send info about error (BindError) - return - } + + err := json.NewDecoder(c.Request.Body).Decode(data.Interface()) + c.Request.Body.Close() + if err != nil { + c.NoContent(400) // TODO: send info about error (BindError) + return nil } out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()}) - err := out[0].Interface() - _ = err + + if out[0].IsNil() { + return nil + } + return out[0].Interface().(error) } } -func handleGET(r *Router, f GetHandle) httprouter.Handle { +func handleReq(r *Router, handle Handle, m []Middleware) httprouter.Handle { return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) { c := newContext(r, res, req, param) + f := handle + for i := len(m) - 1; i >= 0; i-- { // TODO: 1,2,3 of 3,2,1 + f = m[i](f) + } + err := f(c) - fmt.Println(err) + if err != nil { + r.ErrorHandler(c, err) + } } } From 4d46f5c770dddea15e050ebf8aed709389b4427c Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 9 Oct 2018 19:18:09 +0200 Subject: [PATCH 02/23] Add Reader --- default.go | 11 +++++++++++ router.go | 14 ++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/default.go b/default.go index 99cc394..2f0aeac 100644 --- a/default.go +++ b/default.go @@ -1,6 +1,7 @@ package router import ( + "encoding/json" "fmt" ) @@ -16,3 +17,13 @@ func defaultErrorHandler(c *Context, err interface{}) { fmt.Println(err) c.String(500, `internal server error`) } + +func defaultReader(c *Context, dst interface{}) bool { + err := json.NewDecoder(c.Request.Body).Decode(dst) + if err != nil { + c.NoContent(400) + return false + } + + return true +} diff --git a/router.go b/router.go index 720c417..45d0619 100644 --- a/router.go +++ b/router.go @@ -1,7 +1,6 @@ package router import ( - "encoding/json" "net/http" "reflect" @@ -24,9 +23,13 @@ type ErrorHandle func(*Context, interface{}) // Middleware TODO: type Middleware func(Handle) Handle +// Binder reads input to dst, returns true is successful +type Reader func(c *Context, dst interface{}) bool + // Router is the router itself type Router struct { routes []route + Reader Reader Renderer Renderer middleware []Middleware NotFoundHandler Handle @@ -36,7 +39,7 @@ type Router struct { // New returns a new Router func New() *Router { - return &Router{NotFoundHandler: defaultNotFoundHandler, MethodNotAllowedHandler: defaultMethodNotAllowedHandler, ErrorHandler: defaultErrorHandler} + return &Router{Reader: defaultReader, NotFoundHandler: defaultNotFoundHandler, MethodNotAllowedHandler: defaultMethodNotAllowedHandler, ErrorHandler: defaultErrorHandler} } // Use adds a global middleware @@ -161,12 +164,11 @@ func handlePOST(r *Router, f interface{}) Handle { return func(c *Context) error { data := reflect.New(inputRt) - err := json.NewDecoder(c.Request.Body).Decode(data.Interface()) - c.Request.Body.Close() - if err != nil { - c.NoContent(400) // TODO: send info about error (BindError) + if !r.Reader(c, data.Interface()) { + c.Request.Body.Close() return nil } + c.Request.Body.Close() out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()}) From b0b25ecf9ba817dff10350b15e58530f8e258d7a Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 9 Oct 2018 19:21:07 +0200 Subject: [PATCH 03/23] Add LICENSE --- LICENSE | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1f2ef6d --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2018, Fuyu +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. From 6eeb6b932161aa87547a8afa0e901c363d4d54a2 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Wed, 10 Oct 2018 11:49:29 +0200 Subject: [PATCH 04/23] Set Content-Type header in context helpers --- context.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/context.go b/context.go index 26efcda..cf6c73d 100644 --- a/context.go +++ b/context.go @@ -29,6 +29,7 @@ func (c *Context) Bytes(code int, b []byte) error { // String returns the given status code and writes the string to the body func (c *Context) String(code int, s string) error { + c.Response.Header().Set(`Content-Type`, `text/plain`) c.Response.WriteHeader(code) _, err := c.Response.Write([]byte(s)) return err @@ -42,6 +43,7 @@ func (c *Context) NoContent(code int) error { // JSON returns the given status code and writes JSON to the body func (c *Context) JSON(code int, data interface{}) error { + c.Response.Header().Set(`Content-Type`, `application/json`) c.Response.WriteHeader(code) return json.NewEncoder(c.Response).Encode(data) // TODO: Encode to buffer first to prevent partial responses on error } @@ -51,6 +53,7 @@ func (c *Context) Render(code int, template string, data interface{}) error { panic(`Cannot call render without a renderer set`) } + c.Response.Header().Set(`Content-Type`, `text/html`) c.Response.WriteHeader(code) return c.router.Renderer.Render(c.Response, template, data, c) } From 3131ac9efbce4fb7226c7b535ecf473596e789c1 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 16 Oct 2018 18:26:06 +0200 Subject: [PATCH 05/23] Fix POST routes without input --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index 45d0619..f091723 100644 --- a/router.go +++ b/router.go @@ -15,7 +15,7 @@ type route struct { } // Handle handles a request -type Handle func(*Context) error +type Handle = func(*Context) error // ErrorHandle handles a request type ErrorHandle func(*Context, interface{}) From 45ec83b9d3f487186ec86b98cdaedd33c7d331ea Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Wed, 7 Nov 2018 22:08:22 +0100 Subject: [PATCH 06/23] Add README.md --- README.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b3b80b --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# router + +Router is a HTTP router for Golang. It's built on [httprouter](https://github.com/julienschmidt/httprouter) and takes inspiration from [labstack/echo](https://github.com/labstack/echo), but with reduced complexity and easier data binding. + +The data binding is made easier by specifying your input as a parameter to your function + +Example: + +```golang +type someType struct { + A string `json:"a"` + B int `json:"b"` +} + +func handlePOST(c *router.Context, input someType) error { + fmt.Println(input) + return c.NoContent(200) +} +``` + +### Why make data binding shorter? + +Many applications read, bind and validate data for most calls. In Echo this could mean adding boilerplate code to every call. This extra boilerplate code can make your code significantly longer and very hard to read. + +```golang +func handlePOST(c *echo.Context) error { + var input someType + err := c.Bind(&input) + if err != nil { + return c.NoContent(http.StatusBadRequest) + } + err = c.Validate(input) + if err != nil { + return c.NoContent(http.StatusBadRequest) + } + + // Your actual code +} +``` + +In router you define your code to read, bind and validate the input once, and it applies to every POST, PATCH and PUT call. + +### What about the performance of dynamic parameters? + +While using dynamic parameters takes a bit of extra processing, this barely has any impact on performance. Router can still handle tens to hunderds of thousands of requests per second. + +### How does middleware work? + +Middleware works similar to most other routers. The dynamic parameters has no effect on middleware, input data is parsed after all middlewares and right before your handler. + +## Installation + +```bash +go get git.fuyu.moe/Fuyu/router +``` + +## Getting started + +```golang +package main + +import "git.fuyu.moe/Fuyu/router" + +func main() { + // Create a router instance + r := router.New() + + // Add routes + r.GET(`/`, yourGetFunc) + r.POST(`/`, yourPostFunc) + + // Start router + panic(r.Start(`127.0.0.1:8080`)) +} +``` + +## Advice + +### Configuration + +For a serious project you should set `r.Reader`, `r.ErrorHandler`, `r.NotFoundHandler`, and `r.MethodNotAllowedHandler`. + +### Templating + +You can set `r.Renderer` and call `c.Render(code, tmplName, data)` in your handlers + + + +## Examples + +```golang +package main + +import ( + "fmt" + "time" + + "git.fuyu.moe/Fuyu/router" +) + +type product struct { + Name string `json:"name"` +} + +func main() { + r := router.New() + r.Use(accessLog) + + r.GET(`/hello`, hello) + + a := r.Group(`/api`, gandalf) + a.POST(`/product`, createProduct) + a.PATCH(`/product/:id`, updateProduct) + a.POST(`/logout`, logout) + + r.Start(`:8080`) +} + +func accessLog(next router.Handle) router.Handle { + return func(c *router.Context) error { + t := time.Now() + err := next(c) + + fmt.Println(c.Request.Method, c.Request.URL.Path, c.Request.RemoteAddr, t, time.Since(t)) + + return err + } +} + +func gandalf(next router.Handle) router.Handle { + youShallPass := false + return func(c *router.Context) error { + if !youShallPass { + return c.String(401, `You shall not pass`) + } + + return next(c) + } +} + +func hello(c *router.Context) error { + return c.String(200, `Hello`) +} + +func createProduct(c *router.Context, p product) error { + return c.JSON(200, p) +} + +func updateProduct(c *router.Context, p product) error { + productID := c.Param(`id`) + return c.String(200, fmt.Sprintf( + `ProductID %d new name %s`, productID, p.Name, + )) +} + +func logout(c *router.Context) error { + return c.String(200, `logout`) +} +``` From 2c930bacf138f3b66b7c07d9e07fdb7f543113d7 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 18 Nov 2018 15:39:46 +0100 Subject: [PATCH 07/23] Use net/http for default handler messages --- context.go | 5 +++++ default.go | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index cf6c73d..60ab4df 100644 --- a/context.go +++ b/context.go @@ -35,6 +35,11 @@ func (c *Context) String(code int, s string) error { return err } +// StatusText returns the given status code with the matching status text +func (c *Context) StatusText(code int) error { + return c.String(code, http.StatusText(code)) +} + // NoContent returns the given status code without writing anything to the body func (c *Context) NoContent(code int) error { c.Response.WriteHeader(code) diff --git a/default.go b/default.go index 2f0aeac..d36c6c8 100644 --- a/default.go +++ b/default.go @@ -2,20 +2,19 @@ package router import ( "encoding/json" - "fmt" + "net/http" ) func defaultNotFoundHandler(c *Context) error { - return c.String(404, `not found`) + return c.StatusText(http.StatusNotFound) } func defaultMethodNotAllowedHandler(c *Context) error { - return c.String(504, `method not allowed`) + return c.StatusText(http.StatusMethodNotAllowed) } func defaultErrorHandler(c *Context, err interface{}) { - fmt.Println(err) - c.String(500, `internal server error`) + _ = c.StatusText(http.StatusInternalServerError) } func defaultReader(c *Context, dst interface{}) bool { From 4c83818ecdd4c96bc639b8d3c57c173b43af2e2b Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 18 Nov 2018 16:20:13 +0100 Subject: [PATCH 08/23] Fix Middleware comment --- router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/router.go b/router.go index f091723..79bbb71 100644 --- a/router.go +++ b/router.go @@ -20,7 +20,7 @@ type Handle = func(*Context) error // ErrorHandle handles a request type ErrorHandle func(*Context, interface{}) -// Middleware TODO: +// Middleware is a function that runs before your route, it gets the next handler as a parameter type Middleware func(Handle) Handle // Binder reads input to dst, returns true is successful From a16ee2740c0db7be61bdf48d9c491988d4e6c62f Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Sun, 18 Nov 2018 16:29:06 +0100 Subject: [PATCH 09/23] Add error to return of Reader --- default.go | 7 +++---- router.go | 13 +++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/default.go b/default.go index d36c6c8..ecda050 100644 --- a/default.go +++ b/default.go @@ -17,12 +17,11 @@ func defaultErrorHandler(c *Context, err interface{}) { _ = c.StatusText(http.StatusInternalServerError) } -func defaultReader(c *Context, dst interface{}) bool { +func defaultReader(c *Context, dst interface{}) (bool, error) { err := json.NewDecoder(c.Request.Body).Decode(dst) if err != nil { - c.NoContent(400) - return false + return false, c.StatusText(http.StatusBadRequest) } - return true + return true, nil } diff --git a/router.go b/router.go index 79bbb71..04980c1 100644 --- a/router.go +++ b/router.go @@ -24,7 +24,7 @@ type ErrorHandle func(*Context, interface{}) type Middleware func(Handle) Handle // Binder reads input to dst, returns true is successful -type Reader func(c *Context, dst interface{}) bool +type Reader func(c *Context, dst interface{}) (bool, error) // Router is the router itself type Router struct { @@ -164,11 +164,16 @@ func handlePOST(r *Router, f interface{}) Handle { return func(c *Context) error { data := reflect.New(inputRt) - if !r.Reader(c, data.Interface()) { + if r.Reader != nil { + ok, err := r.Reader(c, data.Interface()) c.Request.Body.Close() - return nil + if err != nil { + return err + } + if !ok { + return nil + } } - c.Request.Body.Close() out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()}) From d30d304cea836fe6502173a96e194f596c85512d Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 30 Nov 2018 14:24:28 +0100 Subject: [PATCH 10/23] Add Redirect to Context --- context.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/context.go b/context.go index 60ab4df..21b1307 100644 --- a/context.go +++ b/context.go @@ -20,6 +20,11 @@ func newContext(router *Router, res http.ResponseWriter, req *http.Request, para return &Context{router, req, res, param.ByName, make(map[string]interface{})} } +func (c *Context) Redirect(code int, url string) error { + http.Redirect(c.Response, c.Request, url, code) + return nil +} + // String returns the given status code and writes the bytes to the body func (c *Context) Bytes(code int, b []byte) error { c.Response.WriteHeader(code) From 4cc4ce2a2e6b201a922cf952e75cc06f423ea857 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 30 Nov 2018 14:27:00 +0100 Subject: [PATCH 11/23] Add QueryParam to Context --- context.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/context.go b/context.go index 21b1307..2c65adb 100644 --- a/context.go +++ b/context.go @@ -20,6 +20,15 @@ func newContext(router *Router, res http.ResponseWriter, req *http.Request, para return &Context{router, req, res, param.ByName, make(map[string]interface{})} } +func (c *Context) QueryParam(param string) string { + params := c.Request.URL.Query()[param] + if params == nil { + return `` + } + + return params[0] +} + func (c *Context) Redirect(code int, url string) error { http.Redirect(c.Response, c.Request, url, code) return nil From 0990f5bafe386b45e5555771861af8cd79365a02 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 30 Nov 2018 14:29:59 +0100 Subject: [PATCH 12/23] Remove unused code --- router.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/router.go b/router.go index 04980c1..dbf517f 100644 --- a/router.go +++ b/router.go @@ -125,13 +125,6 @@ func (r *Router) getHttpr() *httprouter.Router { return httpr } -func handleErr(errHandler ErrorHandle, err interface{}) Handle { - return func(c *Context) error { - errHandler(c, err) - return nil - } -} - func checkInterfaceHandle(f interface{}) { if _, ok := f.(Handle); ok { return From 890ff550eb3350bba97f5d9d9135e8ca6a086ba4 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 30 Nov 2018 14:48:07 +0100 Subject: [PATCH 13/23] Fix function comments --- context.go | 6 +++++- router.go | 4 ++-- type.go | 1 + 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/context.go b/context.go index 2c65adb..ee7116a 100644 --- a/context.go +++ b/context.go @@ -20,6 +20,8 @@ func newContext(router *Router, res http.ResponseWriter, req *http.Request, para return &Context{router, req, res, param.ByName, make(map[string]interface{})} } +// QueryParam returns the specified parameter from the query string. +// Returns an empty string if it doesn't exist. Returns the first parameter if multiple instances exist func (c *Context) QueryParam(param string) string { params := c.Request.URL.Query()[param] if params == nil { @@ -29,12 +31,13 @@ func (c *Context) QueryParam(param string) string { return params[0] } +// Redirect sends a redirect to the client func (c *Context) Redirect(code int, url string) error { http.Redirect(c.Response, c.Request, url, code) return nil } -// String returns the given status code and writes the bytes to the body +// Bytes returns the given status code and writes the bytes to the body func (c *Context) Bytes(code int, b []byte) error { c.Response.WriteHeader(code) _, err := c.Response.Write(b) @@ -67,6 +70,7 @@ func (c *Context) JSON(code int, data interface{}) error { return json.NewEncoder(c.Response).Encode(data) // TODO: Encode to buffer first to prevent partial responses on error } +// Render renders a templating using the Renderer set in router func (c *Context) Render(code int, template string, data interface{}) error { if c.router.Renderer == nil { panic(`Cannot call render without a renderer set`) diff --git a/router.go b/router.go index dbf517f..a878555 100644 --- a/router.go +++ b/router.go @@ -23,7 +23,7 @@ type ErrorHandle func(*Context, interface{}) // Middleware is a function that runs before your route, it gets the next handler as a parameter type Middleware func(Handle) Handle -// Binder reads input to dst, returns true is successful +// Reader reads input to dst, returns true if successful type Reader func(c *Context, dst interface{}) (bool, error) // Router is the router itself @@ -159,7 +159,7 @@ func handlePOST(r *Router, f interface{}) Handle { if r.Reader != nil { ok, err := r.Reader(c, data.Interface()) - c.Request.Body.Close() + _ = c.Request.Body.Close() if err != nil { return err } diff --git a/type.go b/type.go index 2d5c6a5..bea7215 100644 --- a/type.go +++ b/type.go @@ -4,6 +4,7 @@ import ( "io" ) +// Renderer renders a template type Renderer interface { Render(w io.Writer, template string, data interface{}, c *Context) error } From 717c6f65fcdc2ec28f0939473f333ee4b084e4c5 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Fri, 30 Nov 2018 14:48:29 +0100 Subject: [PATCH 14/23] Use a buffer in Render --- context.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index ee7116a..e6cc4b3 100644 --- a/context.go +++ b/context.go @@ -1,7 +1,9 @@ package router import ( + "bytes" "encoding/json" + "io" "net/http" "github.com/julienschmidt/httprouter" @@ -76,9 +78,16 @@ func (c *Context) Render(code int, template string, data interface{}) error { panic(`Cannot call render without a renderer set`) } + var b bytes.Buffer + err := c.router.Renderer.Render(&b, template, data, c) + if err != nil { + return err + } + c.Response.Header().Set(`Content-Type`, `text/html`) c.Response.WriteHeader(code) - return c.router.Renderer.Render(c.Response, template, data, c) + _, _ = io.Copy(c.Response, &b) + return nil } // Set sets a value in the context. Set is not safe to be used concurrently From 15df08f21b55abfe63e42cf62ae5bae35ebd3888 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 11 Jun 2019 16:24:53 +0200 Subject: [PATCH 15/23] Add Stop function --- router.go | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index a878555..dd8cfd7 100644 --- a/router.go +++ b/router.go @@ -1,8 +1,10 @@ package router import ( + "context" "net/http" "reflect" + "time" "github.com/julienschmidt/httprouter" ) @@ -35,6 +37,7 @@ type Router struct { NotFoundHandler Handle MethodNotAllowedHandler Handle ErrorHandler ErrorHandle + server *http.Server } // New returns a new Router @@ -94,7 +97,25 @@ func (r *Router) OPTIONS(path string, handle Handle, middleware ...Middleware) { func (r *Router) Start(addr string) error { httpr := r.getHttpr() - return http.ListenAndServe(addr, httpr) + r.server = &http.Server{Addr: addr, Handler: httpr} + return r.server.ListenAndServe() +} + +// Stop stops the web server +func (r *Router) Stop() error { + if r.server == nil { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*5)) + defer cancel() + err := r.server.Shutdown(ctx) + if err == context.DeadlineExceeded { + err = r.server.Close() + } + r.server = nil + + return err } func (r *Router) getHttpr() *httprouter.Router { From 60f82143da0552ec3f3ead1df439df0fc71f689c Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 11 Jun 2019 16:30:58 +0200 Subject: [PATCH 16/23] Add TLS version of Start --- router.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/router.go b/router.go index dd8cfd7..7b50138 100644 --- a/router.go +++ b/router.go @@ -2,6 +2,7 @@ package router import ( "context" + "crypto/tls" "net/http" "reflect" "time" @@ -101,6 +102,13 @@ func (r *Router) Start(addr string) error { return r.server.ListenAndServe() } +func (r *Router) StartTLS(addr, certFile, keyFile string, conf *tls.Config) error { + httpr := r.getHttpr() + + r.server = &http.Server{Addr: addr, Handler: httpr, TLSConfig: conf} + return r.server.ListenAndServeTLS(certFile, keyFile) +} + // Stop stops the web server func (r *Router) Stop() error { if r.server == nil { From 317961ab6eb975d39636c425ae3116114c5de07d Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Wed, 3 Jul 2019 12:24:45 +0200 Subject: [PATCH 17/23] Clean up some code --- router.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/router.go b/router.go index 7b50138..584ba01 100644 --- a/router.go +++ b/router.go @@ -102,6 +102,7 @@ func (r *Router) Start(addr string) error { return r.server.ListenAndServe() } +// StartTLS starts a TLS web server using the given key, cert and config and binds to the given address func (r *Router) StartTLS(addr, certFile, keyFile string, conf *tls.Config) error { httpr := r.getHttpr() @@ -115,7 +116,7 @@ func (r *Router) Stop() error { return nil } - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Second*5)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() err := r.server.Shutdown(ctx) if err == context.DeadlineExceeded { @@ -211,7 +212,7 @@ func handleReq(r *Router, handle Handle, m []Middleware) httprouter.Handle { c := newContext(r, res, req, param) f := handle - for i := len(m) - 1; i >= 0; i-- { // TODO: 1,2,3 of 3,2,1 + for i := len(m) - 1; i >= 0; i-- { f = m[i](f) } From 2a612eb82faeea8c0d499cdde0abf54398bb39d0 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Wed, 3 Jul 2019 12:26:29 +0200 Subject: [PATCH 18/23] Fix middlewares getting overwritten --- router.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index 584ba01..1584600 100644 --- a/router.go +++ b/router.go @@ -136,7 +136,11 @@ func (r *Router) getHttpr() *httprouter.Router { handle = handlePOST(r, v.Handle) } - httpr.Handle(v.Method, v.Path, handleReq(r, handle, append(r.middleware, v.Middleware...))) + middleware := make([]Middleware, len(r.middleware)+len(v.Middleware)) + copy(middleware, r.middleware) + copy(middleware[len(r.middleware):], v.Middleware) + + httpr.Handle(v.Method, v.Path, handleReq(r, handle, middleware)) } httpr.NotFound = http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { From d504c9d2b516cca9e8a51398fce1661ffe291094 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Mon, 6 Apr 2020 16:41:26 +0200 Subject: [PATCH 19/23] Add Context.RealIP --- context.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/context.go b/context.go index e6cc4b3..6b8b7a9 100644 --- a/context.go +++ b/context.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "io" + "net" "net/http" + "strings" "github.com/julienschmidt/httprouter" ) @@ -99,3 +101,17 @@ func (c *Context) Set(key string, value interface{}) { func (c *Context) Get(key string) interface{} { return c.store[key] } + +// RealIP uses proxy headers for the real ip, if none exist the IP of the current connection is returned +func (c *Context) RealIP() string { + if ip := c.Request.Header.Get(`X-Forwarded-For`); ip != `` { + return strings.Split(ip, `, `)[0] + } + + if ip := c.Request.Header.Get(`X-Real-IP`); ip != `` { + return ip + } + + ra, _, _ := net.SplitHostPort(c.Request.RemoteAddr) + return ra +} From 5b5a102c71454d1e39a6575545fb48d0c89a36b2 Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Thu, 9 Apr 2020 10:41:23 +0200 Subject: [PATCH 20/23] Export NewContext --- context.go | 3 ++- router.go | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/context.go b/context.go index 6b8b7a9..eec329c 100644 --- a/context.go +++ b/context.go @@ -20,7 +20,8 @@ type Context struct { store map[string]interface{} } -func newContext(router *Router, res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context { +// NewContext creates a new context, this function is only exported for use in tests +func NewContext(router *Router, res http.ResponseWriter, req *http.Request, param httprouter.Params) *Context { return &Context{router, req, res, param.ByName, make(map[string]interface{})} } diff --git a/router.go b/router.go index 1584600..f53b4ca 100644 --- a/router.go +++ b/router.go @@ -152,7 +152,7 @@ func (r *Router) getHttpr() *httprouter.Router { }) httpr.PanicHandler = func(res http.ResponseWriter, req *http.Request, err interface{}) { - c := newContext(r, res, req, nil) + c := NewContext(r, res, req, nil) r.ErrorHandler(c, err) } @@ -181,8 +181,6 @@ func checkInterfaceHandle(f interface{}) { if rt.In(0) != reflect.TypeOf(&Context{}) { panic(`handle should accept Context as first argument`) } - - return } func handlePOST(r *Router, f interface{}) Handle { @@ -213,7 +211,7 @@ func handlePOST(r *Router, f interface{}) Handle { func handleReq(r *Router, handle Handle, m []Middleware) httprouter.Handle { return func(res http.ResponseWriter, req *http.Request, param httprouter.Params) { - c := newContext(r, res, req, param) + c := NewContext(r, res, req, param) f := handle for i := len(m) - 1; i >= 0; i-- { From 66ae2a435c93a4dad4bd57dc2e1fdcf5f73efedc Mon Sep 17 00:00:00 2001 From: NiseVoid Date: Tue, 19 May 2020 17:04:00 +0200 Subject: [PATCH 21/23] Add TrimTrailingSlashes option --- router.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/router.go b/router.go index f53b4ca..d869fc6 100644 --- a/router.go +++ b/router.go @@ -38,6 +38,7 @@ type Router struct { NotFoundHandler Handle MethodNotAllowedHandler Handle ErrorHandler ErrorHandle + TrimTrailingSlashes bool server *http.Server } @@ -127,7 +128,7 @@ func (r *Router) Stop() error { return err } -func (r *Router) getHttpr() *httprouter.Router { +func (r *Router) getHttpr() http.Handler { httpr := httprouter.New() for _, v := range r.routes { @@ -156,6 +157,18 @@ func (r *Router) getHttpr() *httprouter.Router { r.ErrorHandler(c, err) } + if r.TrimTrailingSlashes { + httpr.RedirectTrailingSlash = false + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + l := len(req.URL.Path) + if l > 1 && req.URL.Path[l-1] == '/' { + req.URL.Path = req.URL.Path[:l-1] + } + httpr.ServeHTTP(w, req) + }) + } + return httpr } From 283c75b32e971b100c8aa7c25c0a448f682d77d5 Mon Sep 17 00:00:00 2001 From: Elwin Tamminga Date: Tue, 20 Jul 2021 10:49:55 +0200 Subject: [PATCH 22/23] Remove port when using headers in c.RealIP Co-authored-by: Elwin Tamminga Co-committed-by: Elwin Tamminga --- context.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/context.go b/context.go index eec329c..e9c2fd6 100644 --- a/context.go +++ b/context.go @@ -105,14 +105,17 @@ func (c *Context) Get(key string) interface{} { // RealIP uses proxy headers for the real ip, if none exist the IP of the current connection is returned func (c *Context) RealIP() string { + reqIP := c.Request.RemoteAddr + if ip := c.Request.Header.Get(`X-Forwarded-For`); ip != `` { - return strings.Split(ip, `, `)[0] + reqIP = strings.Split(ip, `, `)[0] + } else if ip := c.Request.Header.Get(`X-Real-IP`); ip != `` { + reqIP = ip } - if ip := c.Request.Header.Get(`X-Real-IP`); ip != `` { - return ip + ra, _, _ := net.SplitHostPort(reqIP) + if ra != `` { + reqIP = ra } - - ra, _, _ := net.SplitHostPort(c.Request.RemoteAddr) - return ra + return reqIP } From 152bf49c4b925a761b3a0aa5c4830da3c9f5082e Mon Sep 17 00:00:00 2001 From: robinknaapen Date: Mon, 28 Mar 2022 10:38:46 +0200 Subject: [PATCH 23/23] Add support for net.Listener --- go.mod | 5 +++++ go.sum | 2 ++ router.go | 9 +++++++++ 3 files changed, 16 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dc9d16a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.fuyu.moe/Fuyu/router + +go 1.13 + +require github.com/julienschmidt/httprouter v1.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..096c54e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= diff --git a/router.go b/router.go index d869fc6..754ab42 100644 --- a/router.go +++ b/router.go @@ -3,6 +3,7 @@ package router import ( "context" "crypto/tls" + "net" "net/http" "reflect" "time" @@ -95,6 +96,14 @@ func (r *Router) OPTIONS(path string, handle Handle, middleware ...Middleware) { r.routes = append(r.routes, route{`OPTIONS`, path, handle, middleware}) } +// Serve accepts incoming HTTP connections on the listener +func (r *Router) Serve(l net.Listener) error { + httpr := r.getHttpr() + + r.server = &http.Server{Handler: httpr} + return r.server.Serve(l) +} + // Start starts the web server and binds to the given address func (r *Router) Start(addr string) error { httpr := r.getHttpr()