Permalink
| 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 | |
| } |