bastion

package module
v3.1.0+incompatible Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 2, 2019 License: MIT Imports: 16 Imported by: 8

README

Bastion

Documentation Coverage Status Go Report Card CircleCI

Defend your API from the sieges. Bastion offers an "augmented" Router instance.

It has the minimal necessary to create an API with default handlers and middleware that help you raise your API easy and fast. Allows to have commons handlers and middleware between projects with the need for each one to do so. It's also included some useful/optional subpackages: middleware and render. We hope you enjoy it too!

Installation

go get -u github.com/ifreddyrondon/bastion

Examples

See _examples/ for a variety of examples.

As easy as:

package main

import (
    "net/http"

    "github.com/ifreddyrondon/bastion"
    "github.com/ifreddyrondon/bastion/render"
)

func handler(w http.ResponseWriter, r *http.Request) {
	render.JSON.Send(w, map[string]string{"message": "hello bastion"})
}

func main() {
	app := bastion.New()
	app.Get("/hello", handler)
	// By default it serves on :8080 unless a
	// ADDR environment variable was defined.
	app.Serve()
	// app.Serve(":3000") for a hard coded port
}

Router

Bastion use go-chi as a router making it easy to modularize the applications. Each Bastion instance accepts a URL pattern and chain of handlers. The URL pattern supports named params (ie. /users/{userID}) and wildcards (ie. /admin/*). URL parameters can be fetched at runtime by calling chi.URLParam(r, "userID") for named parameters and chi.URLParam(r, "*") for a wildcard parameter.

NewRouter

NewRouter return a router as a subrouter along a routing path.

It's very useful to split up a large API as many independent routers and compose them as a single service.

package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/go-chi/chi"
	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/render"
)

// Routes creates a REST router for the todos resource
func routes() http.Handler {
	r := bastion.NewRouter()

	r.Get("/", list)    // GET /todos - read a list of todos
	r.Post("/", create) // POST /todos - create a new todo and persist it
	r.Route("/{id}", func(r chi.Router) {
		r.Get("/", get)       // GET /todos/{id} - read a single todo by :id
		r.Put("/", update)    // PUT /todos/{id} - update a single todo by :id
		r.Delete("/", delete) // DELETE /todos/{id} - delete a single todo by :id
	})

	return r
}

func list(w http.ResponseWriter, r *http.Request) {
	render.Text.Response(w, http.StatusOK, "todos list of stuff..")
}

func create(w http.ResponseWriter, r *http.Request) {
	render.Text.Response(w, http.StatusOK, "todos create")
}

func get(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	render.Text.Response(w, http.StatusOK, fmt.Sprintf("get todo with id %v", id))
}

func update(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	render.Text.Response(w, http.StatusOK, fmt.Sprintf("update todo with id %v", id))
}

func delete(w http.ResponseWriter, r *http.Request) {
	id := chi.URLParam(r, "id")
	render.Text.Response(w, http.StatusOK, fmt.Sprintf("delete todo with id %v", id))
}

func main() {
	app := bastion.New()
	app.Mount("/todo/", routes())
	fmt.Fprintln(os.Stderr, app.Serve())
}

Middlewares

Bastion comes equipped with a set of commons middleware handlers, providing a suite of standard net/http middleware. They are just stdlib net/http middleware handlers. There is nothing special about them, which means the router and all the tooling is designed to be compatible and friendly with any middleware in the community.

Core middleware
Name Description
Logger Logs the start and end of each request with the elapsed processing time.
RequestID Injects a request ID into the context of each request.
Recovery Gracefully absorb panics and prints the stack trace.
InternalError Intercept responses to verify if his status code is >= 500. If status is >= 500, it'll response with a default error. IT allows to response with the same error without disclosure internal information, also the real error is logged.
Auxiliary middleware
Name Description
Listing Parses the url from a request and stores a listing.Listing on the context, it can be accessed through middleware.GetListing.
WrapResponseWriter provides an easy way to capture http related metrics from your application's http.Handlers or event hijack the response.

Checkout for references, examples, options and docu in middleware or chi for more middlewares.

Register on shutdown

You can register a function to call on shutdown. This can be used to gracefully shutdown connections. By default the shutdown execute the server shutdown.

Bastion listens if any SIGINT, SIGTERM or SIGKILL signal is emitted and performs a graceful shutdown.

It can be added with RegisterOnShutdown method of the bastion instance, it can accept variable number of functions.

Register on shutdown example
package main

import (
    "log"

    "github.com/ifreddyrondon/bastion"
)

func onShutdown() {
    log.Printf("My registered on shutdown. Doing something...")
}

func main() {
    app := bastion.New()
    app.RegisterOnShutdown(onShutdown)
    app.Serve(":8080")
}

Options

Options are used to define how the application should run, it can be set through optionals functions when using bastion.New().

package main

import (
    "github.com/ifreddyrondon/bastion"
)

func main() {
	// turn off pretty print logger and sets 500 errors message
	bastion.New(bastion.DisablePrettyLogging(), bastion.InternalErrMsg(`Just another "500 - internal error"`))
}
InternalErrMsg

Represent the message returned to the user when a http 500 error is caught by the InternalError middleware. Default looks like something went wrong.

  • InternalErrMsg(msg string) set the message returned to the user when catch a 500 status error.
DisableInternalErrorMiddleware

Boolean flag to disable the internal error middleware. Default false.

  • DisableInternalErrorMiddleware() turn off internal error middleware.
DisableRecoveryMiddleware

Boolean flag to disable recovery middleware. Default false.

  • DisableRecoveryMiddleware() turn off recovery middleware.
DisablePingRouter

Boolean flag to disable the ping route. Default false.

  • DisablePingRouter() turn off ping route.
DisableLoggerMiddleware

Boolean flag to disable the logger middleware. Default false.

  • DisableLoggerMiddleware() turn off logger middleware.
DisablePrettyLogging

Boolean flag to don't output a colored human readable version on the out writer. Default false.

  • DisablePrettyLogging() turn off the pretty logging.
LoggerLevel

Defines log level. Default debug. Allows for logging at the following levels (from highest to lowest):

  • panic, 5

  • fatal, 4

  • error, 3

  • warn, 2

  • info, 1

  • debug, 0

  • LoggerLevel(lvl string) set the logger level.

package main

import (
    "github.com/ifreddyrondon/bastion"
)

func main() {
	bastion.New(bastion.LoggerLevel(bastion.ErrorLevel))
	// or
	bastion.New(bastion.LoggerLevel("error"))
}
LoggerOutput

Where the logger output write. Default os.Stdout.

  • LoggerOutput(w io.Writer) set the logger output writer.
ProfilerRoutePrefix

Optional path prefix for profiler subrouter. If left unspecified, /debug/ is used as the default path prefix.

  • ProfilerRoutePrefix(prefix string) set the prefix path for the profile router.
EnableProfiler

Boolean flag to enable the profiler subrouter in production mode.

  • EnableProfiler() turn on profiler subrouter.
Mode

Mode in which the App is running. Default is "debug". Can be set using Mode(string) option or with ENV vars GO_ENV or GO_ENVIRONMENT. Mode(mode string) has more priority than the ENV variables.

When production mode is on, the request logger IP, UserAgent and Referer are enable, the logger level is set to error (is not set with LoggerLevel option), the profiler routes are disable (is not set with EnableProfiler option) and the logging pretty print is disabled.

  • Mode(mode string) set the mode in which the App is running.
package main

import (
    "github.com/ifreddyrondon/bastion"
)

func main() {
	bastion.New(bastion.Mode(bastion.DebugMode))
	// or
	bastion.New(bastion.Mode("production"))
}

Testing

Bastion comes with battery included testing tools to perform End-to-end test over your endpoint/handlers.

It uses github.com/gavv/httpexpect to incrementally build HTTP requests, inspect HTTP responses and inspect response payload recursively.

Quick start
  1. Create the bastion instance with the handler you want to test.
  2. Import from bastion.Tester
  3. It receive a *testing.T and *bastion.Bastion instances as params.
  4. Build http request.
  5. Inspect http response.
  6. Inspect response payload.
package main_test

import (
	"net/http"
	"testing"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/_examples/todo-rest/todo"
    "github.com/ifreddyrondon/bastion/render"
)

func setup() *bastion.Bastion {
	app := bastion.New()
	app.Mount("/todo/", todo.Routes())
	return app
}

func TestHandlerCreate(t *testing.T) {
	app := setup()
	payload := map[string]interface{}{
		"description": "new description",
	}

	e := bastion.Tester(t, app)
	e.POST("/todo/").WithJSON(payload).Expect().
		Status(http.StatusCreated).
		JSON().Object().
		ContainsKey("id").ValueEqual("id", 0).
		ContainsKey("description").ValueEqual("description", "new description")
}

Go and check the full test for handler and complete app 🤓

Render

Easily rendering JSON, XML, binary data, and HTML templates responses

Usage

It can be used with pretty much any web framework providing you can access the http.ResponseWriter from your handler. The rendering functions simply wraps Go's existing functionality for marshaling and rendering data.

package main

import (
	"encoding/xml"
	"net/http"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/render"
)

type ExampleXML struct {
	XMLName xml.Name `xml:"example"`
	One     string   `xml:"one,attr"`
	Two     string   `xml:"two,attr"`
}

func main() {
	app := bastion.New()

	app.Get("/data", func(w http.ResponseWriter, req *http.Request) {
		render.Data.Response(w, http.StatusOK, []byte("Some binary data here."))
	})

	app.Get("/text", func(w http.ResponseWriter, req *http.Request) {
		render.Text.Response(w, http.StatusOK, "Plain text here")
	})

	app.Get("/html", func(w http.ResponseWriter, req *http.Request) {
		render.HTML.Response(w, http.StatusOK, "<h1>Hello World</h1>")
	})

	app.Get("/json", func(w http.ResponseWriter, req *http.Request) {
		render.JSON.Response(w, http.StatusOK, map[string]string{"hello": "json"})
	})

	app.Get("/json-ok", func(w http.ResponseWriter, req *http.Request) {
		// with implicit status 200
		render.JSON.Send(w, map[string]string{"hello": "json"})
	})

	app.Get("/xml", func(w http.ResponseWriter, req *http.Request) {
		render.XML.Response(w, http.StatusOK, ExampleXML{One: "hello", Two: "xml"})
	})

	app.Get("/xml-ok", func(w http.ResponseWriter, req *http.Request) {
		// with implicit status 200
		render.XML.Send(w, ExampleXML{One: "hello", Two: "xml"})
	})

	app.Serve()
}

Checkout more references, examples, options and implementations in render.

Binder

To bind a request body or a source input into a type, use a binder. It's currently support binding of JSON, XML and YAML. The binding execute Validate() if the type implements the binder.Validate interface after successfully bind the type.

The goal of implement Validate is to endorse the values linked to the type. This library intends for you to handle your own validations error.

Usage
package main

import (
	"net/http"

	"github.com/pkg/errors"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/binder"
	"github.com/ifreddyrondon/bastion/render"
)

type address struct {
	Address *string `json:"address" xml:"address" yaml:"address"`
	Lat     float64 `json:"lat" xml:"lat" yaml:"lat"`
	Lng     float64 `json:"lng" xml:"lng" yaml:"lng"`
}

func (a *address) Validate() error {
	if a.Address == nil || *a.Address == "" {
		return errors.New("missing address field")
	}
	return nil
}

func main() {
	app := bastion.New()
	app.Post("/decode-json", func(w http.ResponseWriter, r *http.Request) {
		var a address
		if err := binder.JSON.FromReq(r, &a); err != nil {
			render.JSON.BadRequest(w, err)
			return
		}
		render.JSON.Send(w, a)
	})
	app.Post("/decode-xml", func(w http.ResponseWriter, r *http.Request) {
		var a address
		if err := binder.XML.FromReq(r, &a); err != nil {
			render.JSON.BadRequest(w, err)
			return
		}
		render.JSON.Send(w, a)
	})
	app.Post("/decode-yaml", func(w http.ResponseWriter, r *http.Request) {
		var a address
		if err := binder.YAML.FromReq(r, &a); err != nil {
			render.JSON.BadRequest(w, err)
			return
		}
		render.JSON.Send(w, a)
	})
	app.Serve()
}

Checkout more references, examples, options and implementations in binder.

Logger

Bastion have an internal JSON structured logger powered by github.com/rs/zerolog. It can be accessed from the context of each request l := bastion.LoggerFromCtx(ctx). The request id is logged for every call to the logger.

package main

import (
	"net/http"

	"github.com/ifreddyrondon/bastion"
	"github.com/ifreddyrondon/bastion/render"
)

func handler(w http.ResponseWriter, r *http.Request) {
	l := bastion.LoggerFromCtx(r.Context())
	l.Info().Msg("handler")

	render.JSON.Send(w, map[string]string{"message": "hello bastion"})
}

func main() {
	app := bastion.New()
	app.Get("/hello", handler)
	app.Serve()
}

Documentation

Index

Constants

View Source
const (
	DebugLevel = "debug"
	InfoLevel  = "info"
	WarnLevel  = "warn"
	ErrorLevel = "error"
	FatalLevel = "fatal"
	PanicLevel = "panic"
)
View Source
const (
	DebugMode      = "debug"
	ProductionMode = "production"
)

Variables

This section is empty.

Functions

func LoggerFromCtx added in v1.4.0

func LoggerFromCtx(ctx context.Context) *zerolog.Logger

LoggerFromCtx returns the Logger associated with the ctx.

func NewRouter added in v1.1.0

func NewRouter() *chi.Mux

NewRouter return a router as a subrouter along a routing path. It's very useful to split up a large API as many independent routers and compose them as a single service.

func Tester added in v1.2.4

func Tester(t *testing.T, bastion *Bastion) *httpexpect.Expect

Tester is an end-to-end testing helper for bastion handlers. It receives a reporter testing.T and http.Handler as params.

Types

type Bastion

type Bastion struct {
	Options
	*chi.Mux
	// contains filtered or unexported fields
}

Bastion offers an "augmented" Router instance. It has the minimal necessary to create an API with default handlers and middleware. Allows to have commons handlers and middleware between projects with the need for each one to do so. Mounted Routers It use go-chi router to modularize the applications. Each instance of Bastion, will have the possibility of mounting an API router, it will define the routes and middleware of the application with the app logic. Without a Bastion you can't do much!

func New

func New(opts ...Opt) *Bastion

New returns a new instance of Bastion and adds some sane, and useful, defaults.

func (*Bastion) RegisterOnShutdown added in v1.3.0

func (app *Bastion) RegisterOnShutdown(fs ...OnShutdown)

RegisterOnShutdown registers a function to call on Shutdown. This can be used to gracefully shutdown connections that have undergone NPN/ALPN protocol upgrade or that have been hijacked. This function should start protocol-specific graceful shutdown, but should not wait for shutdown to complete.

func (*Bastion) Serve

func (app *Bastion) Serve(addr ...string) error

Serve accepts incoming connections coming from the specified address/port. It is a shortcut for http.ListenAndServe(addr, router). Note: this method will block the calling goroutine indefinitely unless an error happens.

func (Bastion) String

func (mode Bastion) String() string

type OnShutdown

type OnShutdown func()

OnShutdown is a function to be implemented when is necessary to run something before a shutdown of the server or in graceful shutdown.

type Opt

type Opt func(*Bastion)

Opt helper type to create functional options

func DisableInternalErrorMiddleware

func DisableInternalErrorMiddleware() Opt

DisableInternalErrorMiddleware turn off internal error middleware.

func DisableLoggerMiddleware

func DisableLoggerMiddleware() Opt

func DisablePingRouter

func DisablePingRouter() Opt

DisablePingRouter turn off ping route.

func DisablePrettyLogging

func DisablePrettyLogging() Opt

DisablePrettyLogging turn off the pretty logging.

func DisableRecoveryMiddleware

func DisableRecoveryMiddleware() Opt

DisableRecoveryMiddleware turn off recovery middleware.

func EnableProfiler

func EnableProfiler() Opt

EnableProfiler turn on the profiler router.

func InternalErrMsg

func InternalErrMsg(msg string) Opt

InternalErrMsg set the message returned to the user when catch a 500 status error.

func LoggerLevel

func LoggerLevel(lvl string) Opt

LoggerLevel set the logger level.

func LoggerOutput

func LoggerOutput(w io.Writer) Opt

LoggerOutput set the logger output writer

func Mode

func Mode(mode string) Opt

Mode set the mode in which the App is running.

func ProfilerRoutePrefix

func ProfilerRoutePrefix(prefix string) Opt

ProfilerRoutePrefix set the prefix path for the profile router.

type Options added in v1.3.3

type Options struct {
	// InternalErrMsg message returned to the user when catch a 500 status error.
	InternalErrMsg string
	// DisableInternalErrorMiddleware boolean flag to disable the internal error middleware.
	DisableInternalErrorMiddleware bool
	// DisableRecoveryMiddleware boolean flag to disable the recovery middleware.
	DisableRecoveryMiddleware bool
	// DisablePingRouter boolean flag to disable the ping router.
	DisablePingRouter bool
	// DisableLoggerMiddleware boolean flag to disable the logger middleware.
	DisableLoggerMiddleware bool
	// DisablePrettyLogging don't output a colored human readable version on the out writer.
	DisablePrettyLogging bool
	// LoggerOutput logger output writer. Default os.Stdout
	LoggerOutput io.Writer
	// LoggerLevel defines log levels. Default "debug".
	LoggerLevel string

	// Mode in which the App is running. Default is "debug".
	Mode string

	// ProfilerRoutePrefix is an optional path prefix for profiler subrouter. If left unspecified, `/debug/`
	// is used as the default path prefix.
	ProfilerRoutePrefix string
	// EnableProfiler boolean flag to enable the profiler router in production mode.
	EnableProfiler bool
	// contains filtered or unexported fields
}

Options are used to define how the application should run.

func (Options) IsDebug

func (opts Options) IsDebug() bool

IsDebug check if app is running in debug mode

func (Options) String

func (mode Options) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL