Compare commits

...

22 Commits

Author SHA1 Message Date
Elwin Tamminga 283c75b32e Remove port when using headers in c.RealIP
Co-authored-by: Elwin Tamminga <elwintamminga@gmail.com>
Co-committed-by: Elwin Tamminga <elwintamminga@gmail.com>
2021-07-20 10:49:55 +02:00
Nise Void 66ae2a435c
Add TrimTrailingSlashes option 2020-05-19 17:04:00 +02:00
Nise Void 5b5a102c71
Export NewContext 2020-04-09 10:41:23 +02:00
Nise Void d504c9d2b5
Add Context.RealIP 2020-04-06 16:41:26 +02:00
Nise Void 2a612eb82f
Fix middlewares getting overwritten 2019-07-03 12:26:29 +02:00
Nise Void 317961ab6e
Clean up some code 2019-07-03 12:24:45 +02:00
Nise Void 60f82143da
Add TLS version of Start 2019-06-11 16:30:58 +02:00
Nise Void 15df08f21b
Add Stop function 2019-06-11 16:24:53 +02:00
Nise Void 717c6f65fc
Use a buffer in Render 2018-12-03 13:19:57 +01:00
Nise Void 890ff550eb
Fix function comments 2018-12-03 13:19:57 +01:00
Nise Void 0990f5bafe
Remove unused code 2018-12-03 13:19:57 +01:00
Nise Void 4cc4ce2a2e
Add QueryParam to Context 2018-12-03 13:19:52 +01:00
Nise Void d30d304cea
Add Redirect to Context 2018-11-30 14:24:28 +01:00
Nise Void a16ee2740c
Add error to return of Reader 2018-11-18 16:29:06 +01:00
Nise Void 4c83818ecd
Fix Middleware comment 2018-11-18 16:20:13 +01:00
Nise Void 2c930bacf1
Use net/http for default handler messages 2018-11-18 15:39:46 +01:00
Nise Void 45ec83b9d3
Add README.md 2018-11-07 22:09:11 +01:00
Nise Void 3131ac9efb
Fix POST routes without input 2018-10-16 18:26:06 +02:00
Nise Void 6eeb6b9321
Set Content-Type header in context helpers 2018-10-10 11:49:29 +02:00
Nise Void b0b25ecf9b
Add LICENSE 2018-10-09 19:21:07 +02:00
Nise Void 4d46f5c770
Add Reader 2018-10-09 19:18:09 +02:00
Nise Void 69754a9946
Add middleware 2018-10-09 19:17:38 +02:00
7 changed files with 432 additions and 72 deletions

29
LICENSE Normal file
View File

@ -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.

159
README.md Normal file
View File

@ -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`)
}
```

View File

@ -1,8 +1,12 @@
package router
import (
"bytes"
"encoding/json"
"io"
"net"
"net/http"
"strings"
"github.com/julienschmidt/httprouter"
)
@ -16,11 +20,29 @@ 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{})}
}
// String returns the given status code and writes the bytes to the body
// 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 {
return ``
}
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
}
// 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)
@ -29,11 +51,17 @@ 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
}
// 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)
@ -42,17 +70,27 @@ 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
}
// 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`)
}
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
@ -64,3 +102,20 @@ 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 {
reqIP := c.Request.RemoteAddr
if ip := c.Request.Header.Get(`X-Forwarded-For`); ip != `` {
reqIP = strings.Split(ip, `, `)[0]
} else if ip := c.Request.Header.Get(`X-Real-IP`); ip != `` {
reqIP = ip
}
ra, _, _ := net.SplitHostPort(reqIP)
if ra != `` {
reqIP = ra
}
return reqIP
}

27
default.go Normal file
View File

@ -0,0 +1,27 @@
package router
import (
"encoding/json"
"net/http"
)
func defaultNotFoundHandler(c *Context) error {
return c.StatusText(http.StatusNotFound)
}
func defaultMethodNotAllowedHandler(c *Context) error {
return c.StatusText(http.StatusMethodNotAllowed)
}
func defaultErrorHandler(c *Context, err interface{}) {
_ = c.StatusText(http.StatusInternalServerError)
}
func defaultReader(c *Context, dst interface{}) (bool, error) {
err := json.NewDecoder(c.Request.Body).Decode(dst)
if err != nil {
return false, c.StatusText(http.StatusBadRequest)
}
return true, nil
}

View File

@ -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...)...)
}

186
router.go
View File

@ -1,100 +1,179 @@
package router
import (
"encoding/json"
"fmt"
"context"
"crypto/tls"
"net/http"
"reflect"
"time"
"github.com/julienschmidt/httprouter"
)
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 is a function that runs before your route, it gets the next handler as a parameter
type Middleware func(Handle) Handle
// Reader reads input to dst, returns true if successful
type Reader func(c *Context, dst interface{}) (bool, error)
// Router is the router itself
type Router struct {
routes []route
Renderer Renderer
routes []route
Reader Reader
Renderer Renderer
middleware []Middleware
NotFoundHandler Handle
MethodNotAllowedHandler Handle
ErrorHandler ErrorHandle
TrimTrailingSlashes bool
server *http.Server
}
// New returns a new Router
func New() *Router {
return &Router{}
return &Router{Reader: defaultReader, 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
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()
}
func (r *Router) getHttpr() *httprouter.Router {
// 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()
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 {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 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() http.Handler {
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))
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) {
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)
}
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
}
func checkInterfaceHandle(f interface{}) {
if _, ok := f.(GetHandle); ok {
if _, ok := f.(Handle); ok {
return
}
@ -115,38 +194,47 @@ 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{}) 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 r.Reader != nil {
ok, err := r.Reader(c, data.Interface())
_ = c.Request.Body.Close()
if err != nil {
c.NoContent(400) // TODO: send info about error (BindError)
return
return err
}
if !ok {
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)
c := NewContext(r, res, req, param)
f := handle
for i := len(m) - 1; i >= 0; i-- {
f = m[i](f)
}
err := f(c)
fmt.Println(err)
if err != nil {
r.ErrorHandler(c, err)
}
}
}

View File

@ -4,6 +4,7 @@ import (
"io"
)
// Renderer renders a template
type Renderer interface {
Render(w io.Writer, template string, data interface{}, c *Context) error
}