├── .gitignore ├── README.md ├── log └── log.go ├── server.go ├── LICENSE ├── handlers_test.go ├── context_test.go ├── httputil ├── response_test.go └── response.go ├── handler.go ├── context.go └── doc.go /.gitignore: -------------------------------------------------------------------------------- 1 | _cgo_defun.c 2 | _cgo_export.* 3 | _cgo_gotypes.go 4 | _obj 5 | _test 6 | _testmain.go 7 | ._* 8 | .AppleDouble 9 | .DS_Store 10 | .LSOverride 11 | [568vq].out 12 | *.[568vq] 13 | *.a 14 | *.cgo1.go 15 | *.cgo2.c 16 | *.exe 17 | *.o 18 | *.prof 19 | *.so 20 | *.test 21 | ehthumbs.db 22 | Thumbs.db 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Package core provides a pure handlers (or middlewares) stack so you can perform actions downstream, then filter and manipulate the response upstream. 4 | 5 | [](https://godoc.org/github.com/volatile/core) 6 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // Package log implements a simple logging package for handlers usage. 2 | package log 3 | 4 | import ( 5 | "log" 6 | "runtime" 7 | "strings" 8 | ) 9 | 10 | // Stack logs the error err with the stack trace. 11 | func Stack(err interface{}) { 12 | stack := make([]byte, 64<<10) 13 | stack = stack[:runtime.Stack(stack, false)] 14 | 15 | log.Printf("%v\n%s", err, stack) 16 | } 17 | 18 | // StackWithCaller logs the error err with the caller package name and the stack trace. 19 | func StackWithCaller(err interface{}) { 20 | stack := make([]byte, 64<<10) 21 | stack = stack[:runtime.Stack(stack, false)] 22 | 23 | if pack, ok := callerPackage(); ok { 24 | log.Printf("%s: %v\n%s", pack, err, stack) 25 | } else { 26 | log.Printf("%v\n%s", err, stack) 27 | } 28 | } 29 | 30 | func callerPackage() (pack string, ok bool) { 31 | var pc uintptr 32 | if pc, _, _, ok = runtime.Caller(2); !ok { 33 | return 34 | } 35 | path := strings.Split(runtime.FuncForPC(pc).Name(), "/") 36 | pack = strings.Split(path[len(path)-1], ".")[0] 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "flag" 5 | "net/http" 6 | ) 7 | 8 | var ( 9 | // Production allows handlers know whether the server is running in a production environment. 10 | Production bool 11 | 12 | // Address is the TCP network address on which the server is listening and serving. Default is ":8080". 13 | Address = ":8080" 14 | 15 | // beforeRun stores a set of functions that are triggered just before running the server. 16 | beforeRun []func() 17 | ) 18 | 19 | func init() { 20 | flag.BoolVar(&Production, "production", Production, "run the server in production environment") 21 | flag.StringVar(&Address, "address", Address, "the address to listen and serving on") 22 | } 23 | 24 | // BeforeRun adds a function that will be triggered just before running the server. 25 | func BeforeRun(f func()) { 26 | beforeRun = append(beforeRun, f) 27 | } 28 | 29 | // Run starts the server for listening and serving. 30 | func Run() { 31 | for _, f := range beforeRun { 32 | f() 33 | } 34 | 35 | panic(http.ListenAndServe(Address, defaultHandlersStack)) 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 WHITE devOPS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /handlers_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestServeHTTP(t *testing.T) { 10 | statusWant := http.StatusForbidden 11 | headerKey := "foo" 12 | headerValueWant := "bar" 13 | bodyWant := "foobar" 14 | 15 | hs := NewHandlersStack() 16 | hs.Use(func(c *Context) { c.Next() }) 17 | hs.Use(func(c *Context) { c.Next() }) 18 | hs.Use(func(c *Context) { 19 | c.ResponseWriter.Header().Set(headerKey, headerValueWant) 20 | c.ResponseWriter.WriteHeader(statusWant) 21 | c.ResponseWriter.Write([]byte(bodyWant)) 22 | }) 23 | hs.Use(func(c *Context) { 24 | c.ResponseWriter.Write([]byte("baz")) 25 | }) 26 | 27 | r, _ := http.NewRequest("GET", "", nil) 28 | w := httptest.NewRecorder() 29 | hs.ServeHTTP(w, r) 30 | 31 | statusGot := w.Code 32 | if statusWant != statusGot { 33 | t.Errorf("status code: want %d, got %d", statusWant, statusGot) 34 | } 35 | 36 | headerValueGot := w.Header().Get(headerKey) 37 | if headerValueWant != headerValueGot { 38 | t.Errorf("header: want %q, got %q", headerValueWant, headerValueGot) 39 | } 40 | 41 | bodyGot := w.Body.String() 42 | if bodyWant != bodyGot { 43 | t.Errorf("body: want %q, got %q", bodyWant, bodyGot) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | func TestRecover(t *testing.T) { 13 | statusWant := http.StatusInternalServerError 14 | bodyWant := http.StatusText(http.StatusInternalServerError) + "\n" 15 | 16 | hs := NewHandlersStack() 17 | hs.Use(func(c *Context) { 18 | panic("") 19 | }) 20 | 21 | oldOut := os.Stdout 22 | log.SetOutput(ioutil.Discard) 23 | 24 | r, _ := http.NewRequest("GET", "", nil) 25 | w := httptest.NewRecorder() 26 | hs.ServeHTTP(w, r) 27 | 28 | log.SetOutput(oldOut) 29 | 30 | statusGot := w.Code 31 | if statusWant != statusGot { 32 | t.Errorf("status code: want %d, got %d", statusWant, statusGot) 33 | } 34 | 35 | bodyGot := w.Body.String() 36 | if bodyWant != bodyGot { 37 | t.Errorf("body: want %q, got %q", bodyWant, bodyGot) 38 | } 39 | } 40 | 41 | func TestRecoverCustom(t *testing.T) { 42 | statusWant := http.StatusServiceUnavailable 43 | bodyWant := http.StatusText(http.StatusServiceUnavailable) 44 | var errorWant, errorGot interface{} 45 | errorWant = "foobar" 46 | 47 | hs := NewHandlersStack() 48 | hs.HandlePanic(func(c *Context) { 49 | errorGot = c.Data["panic"] 50 | c.ResponseWriter.WriteHeader(statusWant) 51 | c.ResponseWriter.Write([]byte(bodyWant)) 52 | }) 53 | hs.Use(func(c *Context) { 54 | defer c.Recover() 55 | panic(errorWant) 56 | }) 57 | 58 | oldOut := os.Stdout 59 | log.SetOutput(ioutil.Discard) 60 | 61 | r, _ := http.NewRequest("GET", "", nil) 62 | w := httptest.NewRecorder() 63 | hs.ServeHTTP(w, r) 64 | 65 | log.SetOutput(oldOut) 66 | 67 | if errorWant != errorGot { 68 | t.Errorf("panic error: want %q, got %q", errorWant, errorGot) 69 | } 70 | 71 | statusGot := w.Code 72 | if statusWant != statusGot { 73 | t.Errorf("status code: want %d, got %d", statusWant, statusGot) 74 | } 75 | 76 | bodyGot := w.Body.String() 77 | if bodyWant != bodyGot { 78 | t.Errorf("body: want %q, got %q", bodyWant, bodyGot) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /httputil/response_test.go: -------------------------------------------------------------------------------- 1 | package httputil 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/volatile/core" 9 | ) 10 | 11 | func TestResponseWriterBinder(t *testing.T) { 12 | headerKey := "foo" 13 | headerValueWant := "bar" 14 | bodyWant := "foobar" 15 | 16 | w := httptest.NewRecorder() 17 | c := &core.Context{ 18 | ResponseWriter: w, 19 | } 20 | 21 | BindResponseWriter(c.ResponseWriter, c, func(p []byte) { 22 | c.ResponseWriter.Header().Set(headerKey, headerValueWant) 23 | }) 24 | 25 | c.ResponseWriter.Write([]byte(bodyWant)) 26 | 27 | headerValueGot := w.Header().Get(headerKey) 28 | if headerValueWant != headerValueGot { 29 | t.Errorf("header: want %q, got %q", headerValueWant, headerValueGot) 30 | } 31 | 32 | bodyGot := w.Body.String() 33 | if bodyWant != bodyGot { 34 | t.Errorf("body: want %q, got %q", bodyWant, bodyGot) 35 | } 36 | } 37 | 38 | func TestResponseStatus(t *testing.T) { 39 | statusWant := http.StatusForbidden 40 | var statusGot, customStatusGot int 41 | 42 | type CustomResponseWriter struct { 43 | http.ResponseWriter 44 | } 45 | 46 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 47 | http.Error(w, http.StatusText(statusWant), statusWant) 48 | statusGot = ResponseStatus(w) 49 | customStatusGot = ResponseStatus(CustomResponseWriter{w}) 50 | })) 51 | defer ts.Close() 52 | 53 | if _, err := http.Get(ts.URL); err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | if statusWant != statusGot { 58 | t.Errorf("http.ResponseWriter: want %d, got %d", statusWant, statusGot) 59 | } 60 | 61 | if customStatusGot != statusGot { 62 | t.Errorf("CustomResponseWriter: want %d, got %d", customStatusGot, statusGot) 63 | } 64 | } 65 | 66 | func TestSetDetectedContentType(t *testing.T) { 67 | headerKey := "Content-Type" 68 | headerValueWant := "text/html; charset=utf-8" 69 | 70 | w := httptest.NewRecorder() 71 | SetDetectedContentType(w, []byte("")) 72 | 73 | headerValueGot := w.Header().Get(headerKey) 74 | if headerValueWant != headerValueGot { 75 | t.Errorf("set detected content type: want %q, got %q", headerValueWant, headerValueGot) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import "net/http" 4 | 5 | // HandlersStack contains a set of handlers. 6 | type HandlersStack struct { 7 | Handlers []func(*Context) // The handlers stack. 8 | PanicHandler func(*Context) // The handler called in case of panic. Useful to send custom server error information. Context.Data["panic"] contains the panic error. 9 | } 10 | 11 | // defaultHandlersStack contains the default handlers stack used for serving. 12 | var defaultHandlersStack = NewHandlersStack() 13 | 14 | // NewHandlersStack returns a new NewHandlersStack. 15 | func NewHandlersStack() *HandlersStack { 16 | return new(HandlersStack) 17 | } 18 | 19 | // Use adds a handler to the handlers stack. 20 | func (hs *HandlersStack) Use(h func(*Context)) { 21 | hs.Handlers = append(hs.Handlers, h) 22 | } 23 | 24 | // Use adds a handler to the default handlers stack. 25 | func Use(h func(*Context)) { 26 | defaultHandlersStack.Use(h) 27 | } 28 | 29 | // HandlePanic sets the panic handler of the handlers stack. 30 | // 31 | // Context.Data["panic"] contains the panic error. 32 | func (hs *HandlersStack) HandlePanic(h func(*Context)) { 33 | hs.PanicHandler = h 34 | } 35 | 36 | // HandlePanic sets the panic handler of the default handlers stack. 37 | // 38 | // Context.Data["panic"] contains the panic error. 39 | func HandlePanic(h func(*Context)) { 40 | defaultHandlersStack.HandlePanic(h) 41 | } 42 | 43 | // ServeHTTP makes a context for the request, sets some good practice default headers and enters the handlers stack. 44 | func (hs *HandlersStack) ServeHTTP(w http.ResponseWriter, r *http.Request) { 45 | // Make a context for the request. 46 | c := &Context{ 47 | Request: r, 48 | Data: make(map[string]interface{}), 49 | index: -1, // Begin with -1 because Next will increment the index before calling the first handler. 50 | handlersStack: hs, 51 | } 52 | c.ResponseWriter = contextWriter{w, c} // Use a binder to set the context's written flag on the first write. 53 | 54 | // Set some "good practice" default headers. 55 | c.ResponseWriter.Header().Set("Cache-Control", "no-cache") 56 | c.ResponseWriter.Header().Set("Connection", "keep-alive") 57 | c.ResponseWriter.Header().Set("Vary", "Accept-Encoding") 58 | 59 | defer c.Recover() // Always recover form panics. 60 | c.Next() // Enter the handlers stack. 61 | } 62 | -------------------------------------------------------------------------------- /httputil/response.go: -------------------------------------------------------------------------------- 1 | // Package httputil provides HTTP utility functions, complementing the most common ones in package net/http. 2 | package httputil 3 | 4 | import ( 5 | "io" 6 | "net/http" 7 | "reflect" 8 | 9 | "github.com/volatile/core" 10 | ) 11 | 12 | // responseWriterBinder represents a binder that catches a downstream response writing and transfer its content to a writer (that will normally take care to use the original ResponseWriter). 13 | type responseWriterBinder struct { 14 | io.Writer 15 | http.ResponseWriter 16 | before []func([]byte) // A set of functions that will be triggered just before writing the response. 17 | } 18 | 19 | // Write calls the writer upstream after executing the functions in the before field. 20 | func (w responseWriterBinder) Write(p []byte) (int, error) { 21 | for _, f := range w.before { 22 | f(p) 23 | } 24 | return w.Writer.Write(p) 25 | } 26 | 27 | // BindResponseWriter catches a downstream response writing and transfer its content to the writer w (that will normally take care to use the original ResponseWriter). 28 | // The before variadic is a set of functions that will be triggered just before writing the response. 29 | func BindResponseWriter(w io.Writer, c *core.Context, before ...func([]byte)) { 30 | c.ResponseWriter = responseWriterBinder{w, c.ResponseWriter, before} 31 | } 32 | 33 | // ResponseStatus returns the HTTP response status. 34 | // Remember that the status is only set by the server after WriteHeader has been called. 35 | func ResponseStatus(w http.ResponseWriter) int { 36 | return int(httpResponseStruct(reflect.ValueOf(w)).FieldByName("status").Int()) 37 | } 38 | 39 | // httpResponseStruct returns the response structure after going trough all the intermediary response writers. 40 | func httpResponseStruct(v reflect.Value) reflect.Value { 41 | if v.Kind() == reflect.Ptr { 42 | v = v.Elem() 43 | } 44 | 45 | if v.Type().String() == "http.response" { 46 | return v 47 | } 48 | 49 | return httpResponseStruct(v.FieldByName("ResponseWriter").Elem()) 50 | } 51 | 52 | // SetDetectedContentType detects, sets and returns the response Conten-Type header value. 53 | func SetDetectedContentType(w http.ResponseWriter, p []byte) string { 54 | ct := w.Header().Get("Content-Type") 55 | if ct == "" { 56 | ct = http.DetectContentType(p) 57 | w.Header().Set("Content-Type", ct) 58 | } 59 | return ct 60 | } 61 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "runtime" 7 | ) 8 | 9 | // Context contains all the data needed during the serving flow, including the standard http.ResponseWriter and *http.Request. 10 | // 11 | // The Data field can be used to pass all kind of data through the handlers stack. 12 | type Context struct { 13 | ResponseWriter http.ResponseWriter 14 | Request *http.Request 15 | Data map[string]interface{} 16 | index int // Keeps the actual handler index. 17 | handlersStack *HandlersStack // Keeps the reference to the actual handlers stack. 18 | written bool // A flag to know if the response has been written. 19 | } 20 | 21 | // Written tells if the response has been written. 22 | func (c *Context) Written() bool { 23 | return c.written 24 | } 25 | 26 | // Next calls the next handler in the stack, but only if the response isn't already written. 27 | func (c *Context) Next() { 28 | // Call the next handler only if there is one and the response hasn't been written. 29 | if !c.Written() && c.index < len(c.handlersStack.Handlers)-1 { 30 | c.index++ 31 | c.handlersStack.Handlers[c.index](c) 32 | } 33 | } 34 | 35 | // Recover recovers form panics. 36 | // It logs the stack and uses the PanicHandler (or a classic Internal Server Error) to write the response. 37 | // 38 | // Usage: 39 | // 40 | // defer c.Recover() 41 | func (c *Context) Recover() { 42 | if err := recover(); err != nil { 43 | stack := make([]byte, 64<<10) 44 | stack = stack[:runtime.Stack(stack, false)] 45 | log.Printf("%v\n%s", err, stack) 46 | 47 | if !c.Written() { 48 | c.ResponseWriter.Header().Del("Content-Type") 49 | 50 | if c.handlersStack.PanicHandler != nil { 51 | c.Data["panic"] = err 52 | c.handlersStack.PanicHandler(c) 53 | } else { 54 | http.Error(c.ResponseWriter, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 55 | } 56 | } 57 | } 58 | } 59 | 60 | // contextWriter represents a binder that catches a downstream response writing and sets the context's written flag on the first write. 61 | type contextWriter struct { 62 | http.ResponseWriter 63 | context *Context 64 | } 65 | 66 | // Write sets the context's written flag before writing the response. 67 | func (w contextWriter) Write(p []byte) (int, error) { 68 | w.context.written = true 69 | return w.ResponseWriter.Write(p) 70 | } 71 | 72 | // WriteHeader sets the context's written flag before writing the response header. 73 | func (w contextWriter) WriteHeader(code int) { 74 | w.context.written = true 75 | w.ResponseWriter.WriteHeader(code) 76 | } 77 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package core provides a pure handlers (or middlewares) stack so you can perform actions downstream, then filter and manipulate the response upstream. 3 | 4 | The handlers stack 5 | 6 | A handler is a function that receives a Context (which contains the response writer and the request). 7 | It can be registered with Use and has the possibility to break the stream or to continue with the next handler of the stack. 8 | 9 | Example of a logger, followed by a security headers setter, followed by a response writer: 10 | 11 | // Log 12 | core.Use(func(c *core.Context) { 13 | // Before the response. 14 | start := time.Now() 15 | 16 | // Execute the next handler in the stack. 17 | c.Next() 18 | 19 | // After the response. 20 | log.Printf(" %s %s %s", c.Request.Method, c.Request.URL, time.Since(start)) 21 | }) 22 | 23 | // Secure 24 | core.Use(func(c *core.Context) { 25 | c.ResponseWriter.Header().Set("X-Frame-Options", "SAMEORIGIN") 26 | c.ResponseWriter.Header().Set("X-Content-Type-Options", "nosniff") 27 | c.ResponseWriter.Header().Set("X-XSS-Protection", "1; mode=block") 28 | 29 | // Execute the next handler in the stack. 30 | c.Next() 31 | }) 32 | 33 | // Response 34 | core.Use(func(c *core.Context) { 35 | fmt.Fprint(c.ResponseWriter, "Hello, World!") 36 | }) 37 | 38 | // Run server 39 | core.Run() 40 | 41 | A clearer visualization of this serving flow: 42 | 43 | request open 44 | |— log start 45 | |——— secure start 46 | |————— response write 47 | |——— secure end 48 | |— log end 49 | request close 50 | 51 | When using Run, your app is reachable at http://localhost:8080 by default. 52 | 53 | If you need more flexibility, you can make a new handlers stack, which is fully compatible with the net/http.Handler interface: 54 | 55 | hs := core.NewHandlersStack() 56 | 57 | hs.Use(func(c *core.Context) { 58 | fmt.Fprint(c.ResponseWriter, "Hello, World!") 59 | }) 60 | 61 | http.ListenAndServe(":8080", hs) 62 | 63 | Flags 64 | 65 | These flags are predefined: 66 | 67 | -address 68 | The address to listen and serving on. 69 | Value is saved in Address. 70 | -production 71 | Run the server in production environment. 72 | Some third-party handlers may have different behaviors 73 | depending on the environment. 74 | Value is saved in Production. 75 | 76 | It's up to you to call 77 | flag.Parse() 78 | in your main function if you want to use them. 79 | 80 | Panic recovering 81 | 82 | When using Run, your server always recovers from panics, logs the error with stack, and sends a 500 Internal Server Error. 83 | If you want to use a custom handler on panic, give one to HandlePanic. 84 | 85 | Handlers and helpers 86 | 87 | No handlers or helpers are bundled in the core: it does one thing and does it well. 88 | That's why you have to import all and only the handlers or helpers you need: 89 | 90 | compress 91 | Clever response compressing 92 | https://godoc.org/github.com/volatile/compress 93 | cors 94 | Cross-Origin Resource Sharing support 95 | https://godoc.org/github.com/volatile/cors 96 | i18n 97 | Simple internationalization 98 | https://godoc.org/github.com/volatile/i18n 99 | log 100 | Requests logging 101 | https://godoc.org/github.com/volatile/log 102 | response 103 | Readable response helper 104 | https://godoc.org/github.com/volatile/response 105 | route 106 | Flexible routing helper 107 | https://godoc.org/github.com/volatile/route 108 | secure 109 | Quick security wins 110 | https://godoc.org/github.com/volatile/secure 111 | static 112 | Simple assets serving 113 | https://godoc.org/github.com/volatile/static 114 | */ 115 | package core 116 | --------------------------------------------------------------------------------