Skip to content
Find file
41484d0
333 lines (286 sloc) 9.04 KB
package lion
import (
"fmt"
"log"
"net/http"
"os"
"path"
"runtime"
"strings"
"time"
"github.com/fatih/color"
"golang.org/x/net/context"
)
var red = color.New(color.FgRed).SprintFunc()
var yellow = color.New(color.FgYellow).SprintFunc()
var magenta = color.New(color.FgHiMagenta).SprintFunc()
var blue = color.New(color.FgBlue).SprintFunc()
var green = color.New(color.FgGreen).SprintFunc()
var cyan = color.New(color.FgCyan).SprintFunc()
var hiBlue = color.New(color.FgHiBlue).SprintFunc()
var lionColor = color.New(color.Italic, color.FgHiGreen).SprintFunc()
// Classic creates a new router instance with default middlewares: Recovery, RealIP, Logger and Static.
// The static middleware instance is initiated with a directory named "public" located relatively to the current working directory.
func Classic() *Router {
return New(NewRecovery(), RealIP(), NewLogger(), NewStatic(http.Dir("public")))
}
var lionLogger = log.New(os.Stdout, lionColor("[lion]")+" ", log.Ldate|log.Ltime)
// Logger is a middlewares that logs incomming http requests
type Logger struct {
*log.Logger
}
// NewLogger creates a new Logger
func NewLogger() *Logger {
return &Logger{
Logger: lionLogger,
}
}
// ServeNext implements the Middleware interface for Logger.
// It wraps the corresponding http.ResponseWriter and saves statistics about the status code returned, the number of bytes written and the time that requests took.
func (l *Logger) ServeNext(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
res := WrapResponseWriter(w)
start := time.Now()
next.ServeHTTPC(c, res, r)
l.Printf("%s %s | %s | %dB in %v from %s", magenta(r.Method), hiBlue(r.URL.Path), statusColor(res.Status()), res.BytesWritten(), timeColor(time.Since(start)), r.RemoteAddr)
})
}
func statusColor(status int) string {
msg := fmt.Sprintf("%d %s", status, http.StatusText(status))
switch {
case status < 200:
return blue(msg)
case status < 300:
return green(msg)
case status < 400:
return cyan(msg)
case status < 500:
return yellow(msg)
default:
return red(msg)
}
}
func timeColor(dur time.Duration) string {
switch {
case dur < 500*time.Millisecond:
return green(dur)
case dur < 5*time.Second:
return yellow(dur)
default:
return red(dur)
}
}
// Recovery is a middleware that recovers from panics
// Taken from https://github.com/codegangsta/negroni/blob/master/recovery.go
type Recovery struct {
Logger *log.Logger
PrintStack bool
StackAll bool
StackSize int
}
// NewRecovery creates a new Recovery instance
func NewRecovery() *Recovery {
return &Recovery{
Logger: lionLogger,
PrintStack: false,
StackAll: false,
StackSize: 1024 * 8,
}
}
// ServeNext is the method responsible for recovering from a panic
func (rec *Recovery) ServeNext(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(http.StatusInternalServerError)
stack := make([]byte, rec.StackSize)
stack = stack[:runtime.Stack(stack, rec.StackAll)]
f := "PANIC: %s\n%s"
rec.Logger.Printf(f, err, stack)
if rec.PrintStack {
fmt.Fprintf(w, f, err, stack)
}
}
}()
next.ServeHTTPC(c, w, r)
})
}
func defaultPanicHandler() Handler {
return HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) {
log.Println(fmt.Sprintf("%v", ctx.Value("panic")))
http.Error(w, fmt.Sprintf("%v", ctx.Value("panic")), http.StatusInternalServerError)
})
}
// Static is a middleware handler that serves static files in the given directory/filesystem.
// Taken from https://github.com/codegangsta/negroni/blob/master/static.go
type Static struct {
// Dir is the directory to serve static files from
Dir http.FileSystem
// Prefix is the optional prefix used to serve the static directory content
Prefix string
// IndexFile defines which file to serve as index if it exists.
IndexFile string
}
// NewStatic returns a new instance of Static
func NewStatic(directory http.FileSystem) *Static {
return &Static{
Dir: directory,
Prefix: "",
IndexFile: "index.html",
}
}
// ServeNext tries to find a file in the directory
func (s *Static) ServeNext(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
next.ServeHTTPC(c, w, r)
return
}
file := r.URL.Path
// if we have a prefix, filter requests by stripping the prefix
if s.Prefix != "" {
if !strings.HasPrefix(file, s.Prefix) {
next.ServeHTTPC(c, w, r)
return
}
file = file[len(s.Prefix):]
if file != "" && file[0] != '/' {
next.ServeHTTPC(c, w, r)
return
}
}
f, err := s.Dir.Open(file)
if err != nil {
// discard the error?
next.ServeHTTPC(c, w, r)
return
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
next.ServeHTTPC(c, w, r)
return
}
// try to serve index file
if fi.IsDir() {
// redirect if missing trailing slash
if !strings.HasSuffix(r.URL.Path, "/") {
http.Redirect(w, r, r.URL.Path+"/", http.StatusFound)
return
}
file = path.Join(file, s.IndexFile)
f, err = s.Dir.Open(file)
if err != nil {
next.ServeHTTPC(c, w, r)
return
}
defer f.Close()
fi, err = f.Stat()
if err != nil || fi.IsDir() {
next.ServeHTTPC(c, w, r)
return
}
}
http.ServeContent(w, r, file, fi.ModTime(), f)
})
}
// MaxAge is a middleware that defines the max duration headers
func MaxAge(dur time.Duration) Middleware {
return MaxAgeWithFilter(dur, func(c context.Context, w http.ResponseWriter, r *http.Request) bool { return true })
}
// MaxAgeWithFilter is a middleware that defines the max duration headers with a filter function.
// If the filter returns true then the headers will be set. Otherwise, if it returns false the headers will not be set.
func MaxAgeWithFilter(dur time.Duration, filter func(c context.Context, w http.ResponseWriter, r *http.Request) bool) Middleware {
if filter == nil {
filter = func(c context.Context, w http.ResponseWriter, r *http.Request) bool { return false }
}
return MiddlewareFunc(func(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
if !filter(c, w, r) {
return
}
w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%d, public, must-revalidate, proxy-revalidate", int(dur.Seconds())))
next.ServeHTTPC(c, w, r)
})
})
}
// NoCache middleware sets headers to disable browser caching.
// Inspired by https://github.com/mytrile/nocache
func NoCache() Middleware {
var epoch = time.Unix(0, 0).Format(time.RFC1123)
return noCache{
responseHeaders: map[string]string{
"Expires": epoch,
"Cache-Control": "no-cache, private, must-revalidate, max-age=0",
"Pragma": "no-cache",
"X-Accel-Expires": "0",
},
etagHeaders: []string{
"ETag",
"If-Modified-Since",
"If-Match",
"If-Note-Match",
"If-Range",
"If-Unmodified-Since",
},
}
}
type noCache struct {
responseHeaders map[string]string
etagHeaders []string
}
func (n noCache) ServeNext(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
// Delete ETag headers
for _, v := range n.etagHeaders {
if r.Header.Get(v) != "" {
r.Header.Del(v)
}
}
// Add nocache headers
for k, v := range n.responseHeaders {
w.Header().Set(k, v)
}
})
}
var xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For")
var xRealIP = http.CanonicalHeaderKey("X-Real-IP")
// RealIP is a middleware that sets a http.Request's RemoteAddr to the results
// of parsing either the X-Forwarded-For header or the X-Real-IP header (in that
// order).
//
// This middleware should be inserted fairly early in the middleware stack to
// ensure that subsequent layers (e.g., request loggers) which examine the
// RemoteAddr will see the intended value.
//
// You should only use this middleware if you can trust the headers passed to
// you (in particular, the two headers this middleware uses), for example
// because you have placed a reverse proxy like HAProxy or nginx in front of
// Goji. If your reverse proxies are configured to pass along arbitrary header
// values from the client, or if you use this middleware without a reverse
// proxy, malicious clients will be able to make you very sad (or, depending on
// how you're using RemoteAddr, vulnerable to an attack of some sort).
// Taken from https://github.com/zenazn/goji/blob/master/web/middleware/realip.go
func RealIP() Middleware {
return MiddlewareFunc(func(next Handler) Handler {
return HandlerFunc(func(c context.Context, w http.ResponseWriter, r *http.Request) {
if rip := realIP(r); rip != "" {
r.RemoteAddr = rip
}
next.ServeHTTPC(c, w, r)
})
})
}
func realIP(r *http.Request) string {
var ip string
if xff := r.Header.Get(xForwardedFor); xff != "" {
i := strings.Index(xff, ", ")
if i == -1 {
i = len(xff)
}
ip = xff[:i]
} else if xrip := r.Header.Get(xRealIP); xrip != "" {
ip = xrip
}
return ip
}
Something went wrong with that request. Please try again.