Compare commits

...

17 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
5 changed files with 286 additions and 33 deletions

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)
@ -35,6 +57,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)
@ -48,14 +75,22 @@ 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`)
}
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
@ -67,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
}

View File

@ -2,28 +2,26 @@ 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 {
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
}

View File

@ -1,8 +1,11 @@
package router
import (
"context"
"crypto/tls"
"net/http"
"reflect"
"time"
"github.com/julienschmidt/httprouter"
)
@ -20,11 +23,11 @@ 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
type Reader func(c *Context, dst interface{}) bool
// 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 {
@ -35,6 +38,8 @@ type Router struct {
NotFoundHandler Handle
MethodNotAllowedHandler Handle
ErrorHandler ErrorHandle
TrimTrailingSlashes bool
server *http.Server
}
// New returns a new Router
@ -94,10 +99,36 @@ 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()
}
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 {
@ -106,7 +137,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) {
@ -118,18 +153,23 @@ 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)
}
return httpr
}
if r.TrimTrailingSlashes {
httpr.RedirectTrailingSlash = false
func handleErr(errHandler ErrorHandle, err interface{}) Handle {
return func(c *Context) error {
errHandler(c, err)
return nil
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{}) {
@ -154,8 +194,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 {
@ -164,11 +202,16 @@ func handlePOST(r *Router, f interface{}) Handle {
return func(c *Context) error {
data := reflect.New(inputRt)
if !r.Reader(c, data.Interface()) {
c.Request.Body.Close()
return nil
if r.Reader != nil {
ok, err := r.Reader(c, data.Interface())
_ = c.Request.Body.Close()
if err != nil {
return err
}
if !ok {
return nil
}
}
c.Request.Body.Close()
out := funcRv.Call([]reflect.Value{reflect.ValueOf(c), data.Elem()})
@ -181,10 +224,10 @@ 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-- { // TODO: 1,2,3 of 3,2,1
for i := len(m) - 1; i >= 0; i-- {
f = m[i](f)
}

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
}