Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/InVisionApp/rye

A tiny http middleware for Golang with added handlers for common needs.
https://github.com/InVisionApp/rye

basic-auth chain golang http-handler jwt-middleware middleware middleware-handlers rye statsd

Last synced: 3 months ago
JSON representation

A tiny http middleware for Golang with added handlers for common needs.

Awesome Lists containing this project

README

        

[![LICENSE](https://img.shields.io/badge/license-MIT-orange.svg)](LICENSE)
[![Golang](https://img.shields.io/badge/Golang-v1.7-blue.svg)](https://golang.org/dl/)
[![Godocs](https://img.shields.io/badge/golang-documentation-blue.svg)](https://godoc.org/github.com/InVisionApp/rye)
[![Go Report Card](https://goreportcard.com/badge/github.com/InVisionApp/rye)](https://goreportcard.com/report/github.com/InVisionApp/rye)
[![Travis Build Status](https://travis-ci.com/InVisionApp/rye.svg?token=qgpSBc6cjHgbnjqC45af&branch=master)](https://travis-ci.com/InVisionApp/rye)
[![codecov](https://codecov.io/gh/InVisionApp/rye/branch/master/graph/badge.svg?token=hhqA1l88kx)](https://codecov.io/gh/InVisionApp/rye)


# rye
A simple library to support http services. Currently, **rye** provides a middleware handler which can be used to chain http handlers together while providing statsd metrics for use with DataDog or other logging aggregators. In addition, **rye** comes with various pre-built middleware handlers for enabling functionality such as CORS and rate/CIDR limiting.

## Setup
In order to use **rye**, you should vendor it and the **statsd** client within your project.

```sh
govendor fetch github.com/InVisionApp/rye
govendor fetch github.com/cactus/go-statsd-client/statsd
```

## Why another middleware lib?

* `rye` is *tiny* - the core lib is ~143 lines of code (including comments)!
* Each middleware gets statsd metrics tracking for free including an overall error counter
* We wanted to have an easy way to say “run these two middlewares on this endpoint, but only one middleware on this endpoint”
* Of course, this is doable with negroni and gorilla-mux, but you’d have to use a subrouter with gorilla, which tends to end up in more code
* Bundled helper methods for standardising JSON response messages
* Unified way for handlers and middlewares to return more detailed responses via the `rye.Response` struct (if they chose to do so).
* Pre-built middlewares for things like CORS support

## Example

You can run an example locally to give it a try. The code for the example is [here](example/rye_example.go)!

```sh
cd example
go run rye_example.go
```

## Writing custom middleware handlers

Begin by importing the required libraries:

```go
import (
"github.com/cactus/go-statsd-client/statsd"
"github.com/InVisionApp/rye"
)
```

Create a statsd client (if desired) and create a rye Config in order to pass in optional dependencies:

```go
config := &rye.Config{
Statter: statsdClient,
StatRate: DEFAULT_STATSD_RATE,
}
```

Create a middleware handler. The purpose of the Handler is to keep Config and to provide an interface for chaining http handlers.

```go
middlewareHandler := rye.NewMWHandler(config)
```

Set up any global handlers by using the `Use()` method. Global handlers get pre-pended to the list of your handlers for EVERY endpoint.
They are bound to the MWHandler struct. Therefore, you could set up multiple MWHandler structs if you want to have different collections
of global handlers.

```go
middlewareHandler.Use(middleware_routelogger)
```

Build your http handlers using the Handler type from the **rye** package.

```go
type Handler func(w http.ResponseWriter, r *http.Request) *rye.Response
```

Here are some example (custom) handlers:

```go
func homeHandler(rw http.ResponseWriter, r *http.Request) *rye.Response {
fmt.Fprint(rw, "Refer to README.md for auth-api API usage")
return nil
}

func middlewareFirstHandler(rw http.ResponseWriter, r *http.Request) *rye.Response {
fmt.Fprint(rw, "This handler fires first.")
return nil
}

func errorHandler(rw http.ResponseWriter, r *http.Request) *rye.Response {
return &rye.Response{
StatusCode: http.StatusInternalServerError,
Err: errors.New(message),
}
}
```

Finally, to setup your handlers in your API (Example shown using [Gorilla](https://github.com/gorilla/mux)):
```go
routes := mux.NewRouter().StrictSlash(true)

routes.Handle("/", middlewareHandler.Handle([]rye.Handler{
a.middlewareFirstHandler,
a.homeHandler,
})).Methods("GET")

log.Infof("API server listening on %v", ListenAddress)

srv := &http.Server{
Addr: ListenAddress,
Handler: routes,
}

srv.ListenAndServe()
```

## Statsd Generated by Rye

Rye comes with built-in configurable `statsd` statistics that you could record to your favorite monitoring system. To configure that, you'll need to set up a `Statter` based on the `github.com/cactus/go-statsd-client` and set it in your instantiation of `MWHandler` through the `rye.Config`.

When a middleware is called, it's timing is recorded and a counter is recorded associated directly with the http status code returned during the call. Additionally, an `errors` counter is also sent to the statter which allows you to count any errors that occur with a code equaling or above 500.

Example: If you have a middleware handler you've created with a method named `loginHandler`, successful calls to that will be recorded to `handlers.loginHandler.2xx`. Additionally you'll receive stats such as `handlers.loginHandler.400` or `handlers.loginHandler.500`. You also will receive an increase in the `errors` count.

_If you're sending your logs into a system such as DataDog, be aware that your stats from Rye can have prefixes such as `statsd.my-service.my-k8s-cluster.handlers.loginHandler.2xx` or even `statsd.my-service.my-k8s-cluster.errors`. Just keep in mind your stats could end up in the destination sink system with prefixes._

## Using with Golang 1.7 Context

With Golang 1.7, a new feature has been added that supports a request specific context. This is a great feature that Rye supports out-of-the-box. The tricky part of this is how the context is modified on the request. In Golang, the Context is always available on a Request through `http.Request.Context()`. Great! However, if you want to add key/value pairs to the context, you will have to add the context to the request before it gets passed to the next Middleware. To support this, the `rye.Response` has a property called `Context`. This property takes a properly created context (pulled from the `request.Context()` function. When you return a `rye.Response` which has `Context`, the **rye** library will craft a new Request and make sure that the next middleware receives that request.

Here's the details of creating a middleware with a proper `Context`. You must first pull from the current request `Context`. In the example below, you see `ctx := r.Context()`. That pulls the current context. Then, you create a NEW context with your additional context key/value. Finally, you return `&rye.Response{Context:ctx}`

```go
func addContextVar(rw http.ResponseWriter, r *http.Request) *rye.Response {
// Retrieve the request's context
ctx := r.Context()

// Create a NEW context
ctx = context.WithValue(ctx,"CONTEXT_KEY","my context value")

// Return that in the Rye response
// Rye will add it to the Request to
// pass to the next middleware
return &rye.Response{Context:ctx}
}
```
Now in a later middleware, you can easily retrieve the value you set!
```go
func getContextVar(rw http.ResponseWriter, r *http.Request) *rye.Response {
// Retrieving the value is easy!
myVal := r.Context().Value("CONTEXT_KEY")

// Log it to the server log?
log.Infof("Context Value: %v", myVal)

return nil
}
```
For another simple example, look in the [JWT middleware](middleware_jwt.go) - it adds the JWT into the context for use by other middlewares. It uses the `CONTEXT_JWT` key to push the JWT token into the `Context`.

## Using built-in middleware handlers

Rye comes with various pre-built middleware handlers. Pre-built middlewares source (and docs) can be found in the package dir following the pattern `middleware_*.go`.

To use them, specify the constructor of the middleware as one of the middleware handlers when you define your routes:

```go
// example
routes.Handle("/", middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareCORS(), // to use the CORS middleware (with defaults)
a.homeHandler,
})).Methods("GET")

OR

routes.Handle("/", middlewareHandler.Handle([]rye.Handler{
rye.NewMiddlewareCORS("*", "GET, POST", "X-Access-Token"), // to use specific config when instantiating the middleware handler
a.homeHandler,
})).Methods("GET")

```

## Serving Static Files

Rye has the ability to add serving static files in the chain. Two handlers
have been provided: `StaticFilesystem` and `StaticFile`. These middlewares
should always be used at the end of the chain. Their configuration is
simply based on an absolute path on the server and possibly a skipped
path prefix.

The use case here could be a powerful one. Rye allows you to serve a filesystem
just as a whole or a single file. Used together you could facilitate an application
which does both -> fulfilling the capability to provide a single page application.
For example, if you had a webpack application which served static resources and
artifacts, you would use the `StaticFilesystem` to serve those. Then you'd use
`StaticFile` to serve the single page which refers to the single-page application
through `index.html`.

A full sample is provided in the `static-examples` folder. Here's a snippet from
the example using Gorilla:

```go
pwd, err := os.Getwd()
if err != nil {
log.Fatalf("NewStaticFile: Could not get working directory.")
}

routes.PathPrefix("/dist/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFilesystem(pwd+"/dist/", "/dist/"),
}))

routes.PathPrefix("/ui/").Handler(middlewareHandler.Handle([]rye.Handler{
rye.MiddlewareRouteLogger(),
rye.NewStaticFile(pwd + "/dist/index.html"),
}))
```

### Middleware list

| Name | Description |
|----------------------------|---------------------------------------|
| [Access Token](middleware_accesstoken.go) | Provide Access Token validation |
| [CIDR](middleware_cidr.go) | Provide request IP whitelisting |
| [CORS](middleware_cors.go) | Provide CORS functionality for routes |
| [Auth](middleware_auth.go) | Provide Authorization header validation (basic auth, JWT) |
| [Route Logger](middleware_routelogger.go) | Provide basic logging for a specific route |
| [Static File](middleware_static_file.go) | Provides serving a single file |
| [Static Filesystem](middleware_static_filesystem.go) | Provides serving a single file |

### A Note on the JWT Middleware

The [JWT Middleware](middleware_auth.go) pushes the JWT token onto the Context for use by other middlewares in the chain. This is a convenience that allows any part of your middleware chain quick access to the JWT. Example usage might include a middleware that needs access to your user id or email address stored in the JWT. To access this `Context` variable, the code is very simple:
```go
func getJWTfromContext(rw http.ResponseWriter, r *http.Request) *rye.Response {
// Retrieving the value is easy!
// Just reference the rye.CONTEXT_JWT const as a key
myVal := r.Context().Value(rye.CONTEXT_JWT)

// Log it to the server log?
log.Infof("Context Value: %v", myVal)

return nil
}
```

## API

### Config
This struct is configuration for the MWHandler. It holds references and config to dependencies such as the statsdClient.
```go
type Config struct {
Statter statsd.Statter
StatRate float32
}
```

### MWHandler
This struct is the primary handler container. It holds references to the statsd client.
```go
type MWHandler struct {
Config Config
}
```

#### Constructor
```go
func NewMWHandler(statter statsd.Statter, statrate float32) *MWHandler
```

#### Use
This method prepends a global handler for every Handle method you call.
Use this multiple times to setup global handlers for every endpoint.
Call `Use()` for each global handler before setting up additional routes.
```go
func (m *MWHandler) Use(handlers Handler)
```

#### Handle
This method chains middleware handlers in order and returns a complete `http.Handler`.
```go
func (m *MWHandler) Handle(handlers []Handler) http.Handler
```

### rye.Response
This struct is utilized by middlewares as a way to share state; ie. a middleware can return a `*rye.Response` as a way to indicate that further middleware execution should stop (without an error) or return a hard error by setting `Err` + `StatusCode` or add to the request `Context` by returning a non-nil `Context`.
```go
type Response struct {
Err error
StatusCode int
StopExecution bool
Context context.Context
}
```

### Handler
This type is used to define an http handler that can be chained using the MWHandler.Handle method. The `rye.Response` is from the **rye** package and has facilities to emit StatusCode, bubble up errors and/or stop further middleware execution chain.
```go
type Handler func(w http.ResponseWriter, r *http.Request) *rye.Response
```

## Test stuff
All interfacing with the project is done via `make`. Targets exist for all primary tasks such as:

- Testing: `make test` or `make testv` (for verbosity)
- Generate: `make generate` - this generates based on vendored libraries (from $GOPATH)
- All (test, build): `make all`
- .. and a few others. Run `make help` to see all available targets.
- You can also test the project in Docker (and Codeship) by running `jet steps`

## Contributing
Fork the repository, write a PR and we'll consider it!

## Special Thanks
Thanks go out to Justin Reyna (InVisionApp.com) for the awesome logo!