├── whcompat ├── compat_nae.go ├── compat_ae.go ├── pkg.go ├── close18.go ├── close17.go ├── compat17.go └── compat16.go ├── README.md ├── gensym.go ├── pkg.go ├── examples_test.go ├── wherr ├── example_test.go └── errors.go ├── whlog ├── serve.go └── logging.go ├── whauth └── auth.go ├── whmon ├── rid.go └── writer.go ├── gensym_test.go ├── whjson └── json.go ├── whcache └── cache.go ├── whroute └── routes.go ├── whsess ├── store.go └── cookie.go ├── whgls └── gls.go ├── whparse └── query.go ├── whfatal └── fatal.go ├── whredir └── redirect.go ├── whtmpl └── tmpl.go ├── whmux ├── argmux.go └── mux.go └── LICENSE /whcompat/compat_nae.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // +build !appengine 5 | // +build !go1.7 6 | 7 | package whcompat 8 | 9 | import ( 10 | "net/http" 11 | 12 | "golang.org/x/net/context" 13 | ) 14 | 15 | func new16Context(r *http.Request) context.Context { 16 | return context.Background() 17 | } 18 | -------------------------------------------------------------------------------- /whcompat/compat_ae.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // +build appengine 5 | // +build !go1.7 6 | 7 | package whcompat 8 | 9 | import ( 10 | "net/http" 11 | 12 | "golang.org/x/net/context" 13 | "google.golang.org/appengine" 14 | ) 15 | 16 | func new16Context(r *http.Request) context.Context { 17 | return appengine.NewContext(r) 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webhelp 2 | 3 | A bunch of useful utilities for whenever I do web programming in Go. Like a framework, but better, cause it's not. 4 | 5 | ## Docs 6 | 7 | https://godoc.org/gopkg.in/webhelp.v1 8 | 9 | Recently, I wrote a long blog post about how to use webhelp: 10 | http://www.jtolds.com/writing/2017/01/writing-advanced-web-applications-with-go/ 11 | 12 | See also the OAuth2 extensions (https://github.com/go-webhelp/whoauth2) or 13 | the Goth extensions (https://github.com/go-webhelp/whgoth). 14 | -------------------------------------------------------------------------------- /whcompat/pkg.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whcompat provides webhelp compatibility across different Go 5 | // releases. 6 | // 7 | // The webhelp suite depends heavily on Go 1.7 style http.Request contexts, 8 | // which aren't available in earlier Go releases. This package backports all 9 | // of the functionality in a forwards-compatible way. You can use this package 10 | // to get the desired behavior for all Go releases. 11 | package whcompat // import "gopkg.in/webhelp.v1/whcompat" 12 | -------------------------------------------------------------------------------- /whcompat/close18.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // +build go1.8 5 | 6 | package whcompat 7 | 8 | import ( 9 | "net/http" 10 | ) 11 | 12 | // CloseNotify causes a handler to have its request.Context() canceled the 13 | // second the client TCP connection goes away by hooking the http.CloseNotifier 14 | // logic into the context. Prior to Go 1.8, this costs an extra goroutine in 15 | // a read loop. Go 1.8 and on, this behavior happens automatically with or 16 | // without this wrapper. 17 | func CloseNotify(h http.Handler) http.Handler { 18 | return h 19 | } 20 | -------------------------------------------------------------------------------- /gensym.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package webhelp 5 | 6 | import ( 7 | "sync" 8 | ) 9 | 10 | var ( 11 | keyMtx sync.Mutex 12 | keyCounter uint64 13 | ) 14 | 15 | // ContextKey is only useful via the GenSym() constructor. See GenSym() for 16 | // more documentation 17 | type ContextKey struct { 18 | id uint64 19 | } 20 | 21 | // GenSym generates a brand new, never-before-seen ContextKey for use as a 22 | // Context.WithValue key. Please see the example. 23 | func GenSym() ContextKey { 24 | keyMtx.Lock() 25 | defer keyMtx.Unlock() 26 | keyCounter += 1 27 | return ContextKey{id: keyCounter} 28 | } 29 | -------------------------------------------------------------------------------- /pkg.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // package webhelp is a bunch of useful utilities for doing web programming 5 | // in Go. webhelp encourages you to use the standard library for web 6 | // programming, but provides some oft-needed tools to help simplify the task. 7 | // 8 | // webhelp tightly integrates with the new Go 1.7 Request Context support, 9 | // but has backported the functionality to previous Go releases in the whcompat 10 | // subpackage. 11 | // 12 | // Recently I wrote a long blog post about how to use webhelp: 13 | // http://www.jtolds.com/writing/2017/01/writing-advanced-web-applications-with-go/ 14 | package webhelp // import "gopkg.in/webhelp.v1" 15 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package webhelp_test 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "gopkg.in/webhelp.v1/whcompat" 11 | "gopkg.in/webhelp.v1/whlog" 12 | "gopkg.in/webhelp.v1/whmux" 13 | ) 14 | 15 | var ( 16 | pageName = whmux.NewStringArg() 17 | ) 18 | 19 | func page(w http.ResponseWriter, r *http.Request) { 20 | name := pageName.Get(whcompat.Context(r)) 21 | 22 | w.Header().Set("Content-Type", "text/plain") 23 | fmt.Fprintf(w, "Welcome to %s", name) 24 | } 25 | 26 | func Example() { 27 | pageHandler := pageName.Shift(whmux.Exact(http.HandlerFunc(page))) 28 | 29 | whlog.ListenAndServe(":0", whmux.Dir{ 30 | "wiki": pageHandler, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /whcompat/close17.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // +build !go1.8 5 | 6 | package whcompat 7 | 8 | import ( 9 | "net/http" 10 | 11 | "golang.org/x/net/context" 12 | "gopkg.in/webhelp.v1/whroute" 13 | ) 14 | 15 | // CloseNotify causes a handler to have its request.Context() canceled the 16 | // second the client TCP connection goes away by hooking the http.CloseNotifier 17 | // logic into the context. Prior to Go 1.8, this costs an extra goroutine in 18 | // a read loop. Go 1.8 and on, this behavior happens automatically with or 19 | // without this wrapper. 20 | func CloseNotify(h http.Handler) http.Handler { 21 | return whroute.HandlerFunc(h, 22 | func(w http.ResponseWriter, r *http.Request) { 23 | if cnw, ok := w.(http.CloseNotifier); ok { 24 | doneChan := make(chan bool) 25 | defer close(doneChan) 26 | 27 | closeChan := cnw.CloseNotify() 28 | ctx, cancelFunc := context.WithCancel(Context(r)) 29 | r = WithContext(r, ctx) 30 | 31 | go func() { 32 | select { 33 | case <-doneChan: 34 | cancelFunc() 35 | case <-closeChan: 36 | cancelFunc() 37 | } 38 | }() 39 | } 40 | h.ServeHTTP(w, r) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /wherr/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package wherr_test 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "gopkg.in/webhelp.v1/wherr" 11 | "gopkg.in/webhelp.v1/whlog" 12 | "gopkg.in/webhelp.v1/whmux" 13 | "github.com/spacemonkeygo/errors/errhttp" 14 | ) 15 | 16 | func PageName(r *http.Request) (string, error) { 17 | if r.FormValue("name") == "" { 18 | return "", wherr.BadRequest.New("No page name supplied") 19 | } 20 | return r.FormValue("name"), nil 21 | } 22 | 23 | func Page(w http.ResponseWriter, r *http.Request) { 24 | name, err := PageName(r) 25 | if err != nil { 26 | // This will use our error handler! 27 | wherr.Handle(w, r, err) 28 | return 29 | } 30 | 31 | fmt.Fprintf(w, name) 32 | // do more stuff 33 | } 34 | 35 | func Routes() http.Handler { 36 | return whmux.Dir{ 37 | "page": http.HandlerFunc(Page), 38 | } 39 | } 40 | 41 | func ErrorHandler(w http.ResponseWriter, r *http.Request, err error) { 42 | http.Error(w, "some error happened!", errhttp.GetStatusCode(err, 500)) 43 | } 44 | 45 | func Example() { 46 | // If we didn't register our error handler, we'd end up using a default one. 47 | whlog.ListenAndServe(":0", wherr.HandleWith(wherr.HandlerFunc(ErrorHandler), 48 | Routes())) 49 | } 50 | -------------------------------------------------------------------------------- /whlog/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package whlog 5 | 6 | import ( 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "gopkg.in/webhelp.v1/whcompat" 13 | ) 14 | 15 | const ( 16 | keepAlivePeriod = 3 * time.Minute 17 | ) 18 | 19 | func serve(l net.Listener, handler http.Handler) error { 20 | if tcp_l, ok := l.(*net.TCPListener); ok { 21 | l = tcpKeepAliveListener{TCPListener: tcp_l} 22 | } 23 | return (&http.Server{Handler: handler}).Serve(l) 24 | } 25 | 26 | type tcpKeepAliveListener struct { 27 | *net.TCPListener 28 | } 29 | 30 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 31 | tc, err := ln.AcceptTCP() 32 | if err != nil { 33 | return nil, err 34 | } 35 | tc.SetKeepAlive(true) 36 | tc.SetKeepAlivePeriod(keepAlivePeriod) 37 | return tc, nil 38 | } 39 | 40 | // ListenAndServe creates a TCP listener prior to calling Serve. It also logs 41 | // the address it listens on, and wraps given handlers in whcompat.DoneNotify. 42 | // Like the standard library, it sets TCP keepalive semantics on. 43 | func ListenAndServe(addr string, handler http.Handler) error { 44 | l, err := net.Listen("tcp", addr) 45 | if err != nil { 46 | return err 47 | } 48 | log.Printf("listening on %s", l.Addr()) 49 | return serve(l, whcompat.DoneNotify(handler)) 50 | } 51 | -------------------------------------------------------------------------------- /whauth/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whauth provides some helper methods and handlers for dealing with 5 | // HTTP basic auth 6 | package whauth // import "gopkg.in/webhelp.v1/whauth" 7 | 8 | import ( 9 | "net/http" 10 | 11 | "golang.org/x/net/context" 12 | "gopkg.in/webhelp.v1" 13 | "gopkg.in/webhelp.v1/whcompat" 14 | "gopkg.in/webhelp.v1/wherr" 15 | "gopkg.in/webhelp.v1/whroute" 16 | ) 17 | 18 | var ( 19 | BasicAuthUser = webhelp.GenSym() 20 | ) 21 | 22 | // RequireBasicAuth ensures that a valid user is provided, calling 23 | // wherr.Handle with wherr.Unauthorized if not. 24 | func RequireBasicAuth(h http.Handler, realm string, 25 | valid func(ctx context.Context, user, pass string) bool) http.Handler { 26 | return whroute.HandlerFunc(h, 27 | func(w http.ResponseWriter, r *http.Request) { 28 | user, pass, ok := r.BasicAuth() 29 | if !ok { 30 | w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) 31 | wherr.Handle(w, r, wherr.Unauthorized.New("basic auth required")) 32 | return 33 | } 34 | if !valid(whcompat.Context(r), user, pass) { 35 | w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) 36 | wherr.Handle(w, r, 37 | wherr.Unauthorized.New("invalid username or password")) 38 | return 39 | } 40 | h.ServeHTTP(w, whcompat.WithContext(r, context.WithValue( 41 | whcompat.Context(r), BasicAuthUser, user))) 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /whmon/rid.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package whmon 5 | 6 | import ( 7 | "math/rand" 8 | "net/http" 9 | "sync/atomic" 10 | "time" 11 | 12 | "gopkg.in/webhelp.v1" 13 | "gopkg.in/webhelp.v1/whcompat" 14 | "gopkg.in/webhelp.v1/whroute" 15 | "golang.org/x/net/context" 16 | ) 17 | 18 | var ( 19 | RequestId = webhelp.GenSym() 20 | 21 | idCounter uint64 22 | inc uint64 23 | ) 24 | 25 | // RequestIds generates a new request id for the request if one does not 26 | // already exist under the Request Context Key RequestId. 27 | // The RequestId can be retrieved using: 28 | // 29 | // rid := whcompat.Context(req).Value(whmon.RequestId).(int64) 30 | // 31 | func RequestIds(h http.Handler) http.Handler { 32 | return addKey(h, RequestId, func(r *http.Request) interface{} { 33 | rid, ok := whcompat.Context(r).Value(RequestId).(int64) 34 | if !ok { 35 | rid = newId() 36 | } 37 | return rid 38 | }) 39 | } 40 | 41 | func addKey(h http.Handler, key interface{}, 42 | val func(r *http.Request) interface{}) http.Handler { 43 | return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { 44 | h.ServeHTTP(w, whcompat.WithContext(r, 45 | context.WithValue(whcompat.Context(r), key, val(r)))) 46 | }) 47 | } 48 | 49 | func init() { 50 | rng := rand.New(rand.NewSource(time.Now().Unix())) 51 | idCounter = uint64(rng.Int63()) 52 | inc = uint64(rng.Int63() | 3) 53 | } 54 | 55 | func newId() int64 { 56 | id := atomic.AddUint64(&idCounter, inc) 57 | return int64(id >> 1) 58 | } 59 | -------------------------------------------------------------------------------- /whcompat/compat17.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // This file can go once everything uses go1.7 context semantics 5 | 6 | // +build go1.7 7 | 8 | package whcompat 9 | 10 | import ( 11 | "context" 12 | "net/http" 13 | ) 14 | 15 | // Context is a light wrapper around the behavior of Go 1.7's 16 | // (*http.Request).Context method, except this version works with earlier Go 17 | // releases, too. In Go 1.7 and on, this simply calls r.Context(). See the 18 | // note for WithContext for how this works on previous Go releases. 19 | // If building with the appengine tag, when needed, fresh contexts will be 20 | // generated with appengine.NewContext(). 21 | func Context(r *http.Request) context.Context { 22 | return r.Context() 23 | } 24 | 25 | // WithContext is a light wrapper around the behavior of Go 1.7's 26 | // (*http.Request).WithContext method, except this version works with earlier 27 | // Go releases, too. IMPORTANT CAVEAT: to get this to work for Go 1.6 and 28 | // earlier, a few tricks are pulled, such as expecting the returned r.URL to 29 | // never change what object it points to, and a finalizer is set on the 30 | // returned request. 31 | func WithContext(r *http.Request, ctx context.Context) *http.Request { 32 | return r.WithContext(ctx) 33 | } 34 | 35 | // DoneNotify cancels request contexts when the http.Handler returns in Go 36 | // releases prior to Go 1.7. In Go 1.7 and forward, this is a no-op. 37 | // You get this behavior for free if you use whlog.ListenAndServe. 38 | func DoneNotify(h http.Handler) http.Handler { 39 | return h 40 | } 41 | -------------------------------------------------------------------------------- /gensym_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package webhelp_test 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "golang.org/x/net/context" 11 | "gopkg.in/webhelp.v1" 12 | "gopkg.in/webhelp.v1/whcompat" 13 | "gopkg.in/webhelp.v1/wherr" 14 | "gopkg.in/webhelp.v1/whlog" 15 | "gopkg.in/webhelp.v1/whroute" 16 | ) 17 | 18 | var ( 19 | UserKey = webhelp.GenSym() 20 | ) 21 | 22 | type User struct { 23 | Name string 24 | } 25 | 26 | func loadUser(r *http.Request) (user *User, err error) { 27 | return nil, wherr.InternalServerError.New("not implemented yet") 28 | } 29 | 30 | // myWrapper will load the user from a request, serving any detected errors, 31 | // and otherwise passing the request along to the wrapped handler with the 32 | // user bound inside the context. 33 | func myWrapper(h http.Handler) http.Handler { 34 | return whroute.HandlerFunc(h, 35 | func(w http.ResponseWriter, r *http.Request) { 36 | 37 | user, err := loadUser(r) 38 | if err != nil { 39 | wherr.Handle(w, r, err) 40 | return 41 | } 42 | 43 | h.ServeHTTP(w, whcompat.WithContext(r, 44 | context.WithValue(whcompat.Context(r), UserKey, user))) 45 | }) 46 | } 47 | 48 | // myHandler is a standard http.HandlerFunc that expects to be able to load 49 | // a user out of the request context. 50 | func myHandler(w http.ResponseWriter, r *http.Request) { 51 | ctx := whcompat.Context(r) 52 | if user, ok := ctx.Value(UserKey).(*User); ok { 53 | // do something with the user 54 | fmt.Fprint(w, user.Name) 55 | } 56 | } 57 | 58 | // Routes returns an http.Handler. You might have a whmux.Dir or something 59 | // in here. 60 | func Routes() http.Handler { 61 | return myWrapper(http.HandlerFunc(myHandler)) 62 | } 63 | 64 | func ExampleGenSym() { 65 | whlog.ListenAndServe(":0", Routes()) 66 | } 67 | -------------------------------------------------------------------------------- /whlog/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whlog provides functionality to log incoming requests and results. 5 | package whlog // import "gopkg.in/webhelp.v1/whlog" 6 | 7 | import ( 8 | "log" 9 | "net/http" 10 | "time" 11 | 12 | "gopkg.in/webhelp.v1/whmon" 13 | "gopkg.in/webhelp.v1/whroute" 14 | ) 15 | 16 | type Loggerf func(format string, arg ...interface{}) 17 | 18 | var ( 19 | Default Loggerf = log.Printf 20 | ) 21 | 22 | // LogResponses takes a Handler and makes it log responses. LogResponses uses 23 | // whmon's ResponseWriter to keep track of activity. whfatal.Catch should be 24 | // placed *inside* if applicable. whlog.Default makes a good default logger. 25 | func LogResponses(logger Loggerf, h http.Handler) http.Handler { 26 | return whmon.MonitorResponse(whroute.HandlerFunc(h, 27 | func(w http.ResponseWriter, r *http.Request) { 28 | method, requestURI := r.Method, r.RequestURI 29 | rw := w.(whmon.ResponseWriter) 30 | start := time.Now() 31 | 32 | defer func() { 33 | rec := recover() 34 | if rec != nil { 35 | log.Printf("Panic: %v", rec) 36 | panic(rec) 37 | } 38 | }() 39 | h.ServeHTTP(rw, r) 40 | 41 | if !rw.WroteHeader() { 42 | rw.WriteHeader(http.StatusOK) 43 | } 44 | 45 | code := rw.StatusCode() 46 | 47 | logger(`%s %#v %d %d %d %v`, method, requestURI, code, 48 | r.ContentLength, rw.Written(), time.Since(start)) 49 | })) 50 | } 51 | 52 | // LogRequests takes a Handler and makes it log requests (prior to request 53 | // handling). whlog.Default makes a good default logger. 54 | func LogRequests(logger Loggerf, h http.Handler) http.Handler { 55 | return whroute.HandlerFunc(h, 56 | func(w http.ResponseWriter, r *http.Request) { 57 | logger(`%s %#v %d`, r.Method, r.RequestURI, r.ContentLength) 58 | h.ServeHTTP(w, r) 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /whjson/json.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whjson provides some nice utilities for dealing with JSON-based 5 | // APIs, such as a good JSON wherr.Handler. 6 | package whjson // import "gopkg.in/webhelp.v1/whjson" 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "log" 12 | "net/http" 13 | 14 | "github.com/spacemonkeygo/errors/errhttp" 15 | "gopkg.in/webhelp.v1/whcompat" 16 | "gopkg.in/webhelp.v1/wherr" 17 | ) 18 | 19 | var ( 20 | // ErrHandler provides a good wherr.Handler. It will return a JSON object 21 | // like `{"err": "message"}` where message is filled in with 22 | // errhttp.GetErrorBody. The status code is set with errhttp.GetStatusCode. 23 | ErrHandler = wherr.HandlerFunc(errHandler) 24 | ) 25 | 26 | func errHandler(w http.ResponseWriter, r *http.Request, handledErr error) { 27 | log.Printf("error: %v", handledErr) 28 | data, err := json.MarshalIndent(map[string]string{ 29 | "err": errhttp.GetErrorBody(handledErr)}, "", " ") 30 | if err != nil { 31 | log.Printf("failed serializing error: %v", handledErr) 32 | data = []byte(`{"err": "Internal Server Error"}`) 33 | } 34 | w.Header().Set("Content-Type", "application/json") 35 | w.Header().Set("Content-Length", fmt.Sprint(len(data))) 36 | w.WriteHeader(errhttp.GetStatusCode(handledErr, 37 | http.StatusInternalServerError)) 38 | w.Write(data) 39 | } 40 | 41 | // Render will render JSON `value` like `{"resp": }`, falling back to 42 | // ErrHandler if no error handler was registered and an error is 43 | // encountered. This is good for making sure your API is always returning 44 | // usefully namespaced JSON objects that are clearly differentiated from error 45 | // responses. 46 | func Render(w http.ResponseWriter, r *http.Request, value interface{}) { 47 | data, err := json.MarshalIndent( 48 | map[string]interface{}{"resp": value}, "", " ") 49 | if err != nil { 50 | if handler := wherr.HandlingWith(whcompat.Context(r)); handler != nil { 51 | handler.HandleError(w, r, err) 52 | return 53 | } 54 | errHandler(w, r, err) 55 | return 56 | } 57 | 58 | w.Header().Set("Content-Type", "application/json") 59 | w.Header().Set("Content-Length", fmt.Sprint(len(data))) 60 | w.Write(data) 61 | } 62 | -------------------------------------------------------------------------------- /whcache/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2017 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whcache provides a mechanism for per-request computation caching 5 | // 6 | // Sometimes you have a helper method that performs a computation or interacts 7 | // with a datastore or remote resource, and that helper method gets called 8 | // repeatedly. With simple context values, since the helper method is the most 9 | // descendent frame in the stack, there isn't a context specific place 10 | // (besides maybe the session, which would be a bad choice) to cache context 11 | // specific values. With this cache helper, there now is. 12 | // 13 | // The cache must be registered up the handler chain with Register, and then 14 | // helper methods can use Set/Get/Remove to interact with the cache (if 15 | // available). If no cache was registered, Set/Get/Remove will still work, but 16 | // Get will never return a value. 17 | package whcache // import "gopkg.in/webhelp.v1/whcache" 18 | 19 | import ( 20 | "net/http" 21 | 22 | "golang.org/x/net/context" 23 | "gopkg.in/webhelp.v1" 24 | "gopkg.in/webhelp.v1/whcompat" 25 | "gopkg.in/webhelp.v1/whroute" 26 | ) 27 | 28 | var ( 29 | cacheKey = webhelp.GenSym() 30 | ) 31 | 32 | type reqCache map[interface{}]interface{} 33 | 34 | // Register installs a cache in the handler chain. 35 | func Register(h http.Handler) http.Handler { 36 | return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { 37 | ctx := whcompat.Context(r) 38 | if _, ok := ctx.Value(cacheKey).(reqCache); ok { 39 | h.ServeHTTP(w, r) 40 | return 41 | } 42 | h.ServeHTTP(w, whcompat.WithContext(r, 43 | context.WithValue(ctx, cacheKey, reqCache{}))) 44 | }) 45 | } 46 | 47 | // Set stores the key/val pair in the context specific cache, if possible. 48 | func Set(ctx context.Context, key, val interface{}) { 49 | cache, ok := ctx.Value(cacheKey).(reqCache) 50 | if !ok { 51 | return 52 | } 53 | cache[key] = val 54 | } 55 | 56 | // Remove removes any values stored with key from the context specific cache, 57 | // if possible. 58 | func Remove(ctx context.Context, key interface{}) { 59 | cache, ok := ctx.Value(cacheKey).(reqCache) 60 | if !ok { 61 | return 62 | } 63 | delete(cache, key) 64 | } 65 | 66 | // Get returns previously stored key/value pairs from the context specific 67 | // cache if one is registered and the value is found, and returns nil 68 | // otherwise. 69 | func Get(ctx context.Context, key interface{}) interface{} { 70 | cache, ok := ctx.Value(cacheKey).(reqCache) 71 | if !ok { 72 | return nil 73 | } 74 | val, _ := cache[key] 75 | return val 76 | } 77 | -------------------------------------------------------------------------------- /whroute/routes.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whroute provides utilities to implement route listing, whereby 5 | // http.Handlers that opt in can list what routes they understand. 6 | package whroute // import "gopkg.in/webhelp.v1/whroute" 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "net/http" 12 | "sort" 13 | ) 14 | 15 | const ( 16 | // AllMethods should be returned from a whroute.Lister when all methods are 17 | // successfully handled. 18 | AllMethods = "ALL" 19 | 20 | // AllPaths should be returned from a whroute.Lister when all paths are 21 | // successfully handled. 22 | AllPaths = "[/<*>]" 23 | ) 24 | 25 | // Lister is an interface handlers can implement if they want the Routes 26 | // method to work. All http.Handlers in the webhelp package implement Routes. 27 | type Lister interface { 28 | Routes(cb func(method, path string, annotations map[string]string)) 29 | } 30 | 31 | // Routes will call cb with all routes known to h. 32 | func Routes(h http.Handler, 33 | cb func(method, path string, annotations map[string]string)) { 34 | if rl, ok := h.(Lister); ok { 35 | rl.Routes(cb) 36 | } else { 37 | cb(AllMethods, AllPaths, nil) 38 | } 39 | } 40 | 41 | // PrintRoutes will write all routes of h to out, using the Routes method. 42 | func PrintRoutes(out io.Writer, h http.Handler) (err error) { 43 | Routes(h, func(method, path string, annotations map[string]string) { 44 | if err != nil { 45 | return 46 | } 47 | if host, ok := annotations["Host"]; ok { 48 | _, err = fmt.Fprintf(out, "%s\t%s%s\n", method, host, path) 49 | } else { 50 | _, err = fmt.Fprintf(out, "%s\t%s\n", method, path) 51 | } 52 | annotationKeys := make([]string, 0, len(annotations)) 53 | for key := range annotations { 54 | annotationKeys = append(annotationKeys, key) 55 | } 56 | sort.Strings(annotationKeys) 57 | for _, key := range annotationKeys { 58 | if err != nil { 59 | return 60 | } 61 | if key == "Host" { 62 | continue 63 | } 64 | _, err = fmt.Fprintf(out, " %s: %s\n", key, annotations[key]) 65 | } 66 | }) 67 | return err 68 | } 69 | 70 | type routeHandlerFunc struct { 71 | routes http.Handler 72 | fn func(http.ResponseWriter, *http.Request) 73 | } 74 | 75 | // HandlerFunc advertises the routes from routes, but serves content using 76 | // fn. 77 | func HandlerFunc(routes http.Handler, 78 | fn func(http.ResponseWriter, *http.Request)) http.Handler { 79 | return routeHandlerFunc{ 80 | routes: routes, 81 | fn: fn} 82 | } 83 | 84 | func (rhf routeHandlerFunc) Routes( 85 | cb func(method, path string, annotations map[string]string)) { 86 | Routes(rhf.routes, cb) 87 | } 88 | 89 | func (rhf routeHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { 90 | rhf.fn(w, r) 91 | } 92 | -------------------------------------------------------------------------------- /whcompat/compat16.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // This file can go once everything uses go1.7 context semantics 5 | 6 | // +build !go1.7 7 | 8 | package whcompat 9 | 10 | import ( 11 | "net/http" 12 | "net/url" 13 | "runtime" 14 | "sync" 15 | 16 | "gopkg.in/webhelp.v1/whroute" 17 | "golang.org/x/net/context" 18 | ) 19 | 20 | type reqInfo struct { 21 | ctx context.Context 22 | } 23 | 24 | var ( 25 | reqCtxMappingsMtx sync.Mutex 26 | reqCtxMappings = map[*url.URL]reqInfo{} 27 | ) 28 | 29 | // Context is a light wrapper around the behavior of Go 1.7's 30 | // (*http.Request).Context method, except this version works with earlier Go 31 | // releases, too. In Go 1.7 and on, this simply calls r.Context(). See the 32 | // note for WithContext for how this works on previous Go releases. 33 | // If building with the appengine tag, when needed, fresh contexts will be 34 | // generated with appengine.NewContext(). 35 | func Context(r *http.Request) context.Context { 36 | reqCtxMappingsMtx.Lock() 37 | info, ok := reqCtxMappings[r.URL] 38 | if ok { 39 | reqCtxMappingsMtx.Unlock() 40 | return info.ctx 41 | } 42 | 43 | ctx := new16Context(r) 44 | bindContextAndUnlock(r, ctx) 45 | 46 | return ctx 47 | } 48 | 49 | func bindContextAndUnlock(r *http.Request, ctx context.Context) { 50 | reqCtxMappings[r.URL] = reqInfo{ctx: ctx} 51 | reqCtxMappingsMtx.Unlock() 52 | 53 | runtime.SetFinalizer(r, func(r *http.Request) { 54 | reqCtxMappingsMtx.Lock() 55 | delete(reqCtxMappings, r.URL) 56 | reqCtxMappingsMtx.Unlock() 57 | }) 58 | } 59 | 60 | func copyReqAndURL(r *http.Request) (c *http.Request) { 61 | c = &http.Request{} 62 | *c = *r 63 | c.URL = &url.URL{} 64 | *(c.URL) = *(r.URL) 65 | return c 66 | } 67 | 68 | // WithContext is a light wrapper around the behavior of Go 1.7's 69 | // (*http.Request).WithContext method, except this version works with earlier 70 | // Go releases, too. IMPORTANT CAVEAT: to get this to work for Go 1.6 and 71 | // earlier, a few tricks are pulled, such as expecting the returned r.URL to 72 | // never change what object it points to, and a finalizer is set on the 73 | // returned request. 74 | func WithContext(r *http.Request, ctx context.Context) *http.Request { 75 | if ctx == nil { 76 | panic("nil ctx") 77 | } 78 | r = copyReqAndURL(r) 79 | reqCtxMappingsMtx.Lock() 80 | bindContextAndUnlock(r, ctx) 81 | return r 82 | } 83 | 84 | // DoneNotify cancels request contexts when the http.Handler returns in Go 85 | // releases prior to Go 1.7. In Go 1.7 and forward, this is a no-op. 86 | // You get this behavior for free if you use whlog.ListenAndServe. 87 | func DoneNotify(h http.Handler) http.Handler { 88 | return whroute.HandlerFunc(h, 89 | func(w http.ResponseWriter, r *http.Request) { 90 | ctx, cancel := context.WithCancel(Context(r)) 91 | defer cancel() 92 | h.ServeHTTP(w, WithContext(r, ctx)) 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /whsess/store.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // package whsess is a lightweight session storage mechanism for the webhelp 5 | // package. Attempting to be a combination of minimal and useful. Implementing 6 | // the Store interface is all one must do to provide a different session 7 | // storage mechanism. 8 | package whsess // import "gopkg.in/webhelp.v1/whsess" 9 | 10 | import ( 11 | "net/http" 12 | 13 | "github.com/spacemonkeygo/errors" 14 | "golang.org/x/net/context" 15 | "gopkg.in/webhelp.v1/whcompat" 16 | "gopkg.in/webhelp.v1/whroute" 17 | ) 18 | 19 | type ctxKey int 20 | 21 | var ( 22 | reqCtxKey ctxKey = 1 23 | 24 | SessionError = errors.NewClass("session") 25 | ) 26 | 27 | type SessionData struct { 28 | New bool 29 | Values map[interface{}]interface{} 30 | } 31 | 32 | type Session struct { 33 | SessionData 34 | 35 | store Store 36 | namespace string 37 | } 38 | 39 | type Store interface { 40 | Load(ctx context.Context, r *http.Request, namespace string) ( 41 | SessionData, error) 42 | Save(ctx context.Context, w http.ResponseWriter, 43 | namespace string, s SessionData) error 44 | Clear(ctx context.Context, w http.ResponseWriter, namespace string) error 45 | } 46 | 47 | type reqCtx struct { 48 | s Store 49 | r *http.Request 50 | cache map[string]*Session 51 | } 52 | 53 | // HandlerWithStore wraps a webhelp.Handler such that Load works with contexts 54 | // provided in that Handler. 55 | func HandlerWithStore(s Store, h http.Handler) http.Handler { 56 | return whroute.HandlerFunc(h, 57 | func(w http.ResponseWriter, r *http.Request) { 58 | h.ServeHTTP(w, whcompat.WithContext(r, context.WithValue( 59 | whcompat.Context(r), reqCtxKey, &reqCtx{s: s, r: r}))) 60 | }) 61 | } 62 | 63 | // Load will return the current session, creating one if necessary. This will 64 | // fail if a store wasn't installed with HandlerWithStore somewhere up the 65 | // call chain. 66 | func Load(ctx context.Context, namespace string) (*Session, error) { 67 | rc, ok := ctx.Value(reqCtxKey).(*reqCtx) 68 | if !ok { 69 | return nil, SessionError.New( 70 | "no session store handler wrapper installed") 71 | } 72 | if rc.cache != nil { 73 | if session, exists := rc.cache[namespace]; exists { 74 | return session, nil 75 | } 76 | } 77 | sessiondata, err := rc.s.Load(ctx, rc.r, namespace) 78 | if err != nil { 79 | return nil, err 80 | } 81 | session := &Session{ 82 | SessionData: sessiondata, 83 | store: rc.s, 84 | namespace: namespace} 85 | if rc.cache == nil { 86 | rc.cache = map[string]*Session{namespace: session} 87 | } else { 88 | rc.cache[namespace] = session 89 | } 90 | return session, nil 91 | } 92 | 93 | // Save saves the session using the appropriate mechanism. 94 | func (s *Session) Save(ctx context.Context, w http.ResponseWriter) error { 95 | err := s.store.Save(ctx, w, s.namespace, s.SessionData) 96 | if err == nil { 97 | s.SessionData.New = false 98 | } 99 | return err 100 | } 101 | 102 | // Clear clears the session using the appropriate mechanism. 103 | func (s *Session) Clear(ctx context.Context, w http.ResponseWriter) error { 104 | // clear out the cache 105 | for name := range s.Values { 106 | delete(s.Values, name) 107 | } 108 | return s.store.Clear(ctx, w, s.namespace) 109 | } 110 | -------------------------------------------------------------------------------- /whgls/gls.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whgls provides webhelp tools that use grossness enabled by 5 | // the github.com/jtolds/gls package. No other webhelp packages use 6 | // github.com/jtolds/gls. 7 | // 8 | // The predominant use case for github.com/jtolds/gls is to attach a current 9 | // request's contextual information to all log lines kicked off by the request. 10 | package whgls // import "gopkg.in/webhelp.v1/whgls" 11 | 12 | import ( 13 | "log" 14 | "net/http" 15 | 16 | "github.com/jtolds/gls" 17 | "golang.org/x/net/context" 18 | "gopkg.in/webhelp.v1/whcompat" 19 | "gopkg.in/webhelp.v1/whmon" 20 | "gopkg.in/webhelp.v1/whroute" 21 | ) 22 | 23 | var ( 24 | ctxMgr = gls.NewContextManager() 25 | reqSym = gls.GenSym() 26 | ) 27 | 28 | // Bind will make sure that Load works from any callstacks kicked off by this 29 | // handler, via the magic of github.com/jtolds/gls. It is worthwhile to call 30 | // Bind at the base of your handler stack and again after attaching any useful 31 | // values you might want to include in logs to the request context. 32 | func Bind(h http.Handler) http.Handler { 33 | return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { 34 | ctxMgr.SetValues(gls.Values{reqSym: r}, func() { 35 | h.ServeHTTP(w, r) 36 | }) 37 | }) 38 | } 39 | 40 | // Load will return the *http.Request bound to the current call stack by a 41 | // Bind handler further up the stack. 42 | func Load() *http.Request { 43 | if val, ok := ctxMgr.GetValue(reqSym); ok { 44 | if r, ok := val.(*http.Request); ok { 45 | return r 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | // SetLogOutput will configure the standard library's logger to use the 52 | // provided logger that requires a context, such as AppEngine's loggers. 53 | // This requires that the handler was wrapped with Bind. Note that this will 54 | // cause all log messages without a context to be silently swallowed! 55 | // 56 | // If whmon.RequestIds was in the handler callchain prior to Bind, this logger 57 | // will also attach the Request ID to all log lines. 58 | // 59 | // The benefit of this is that the standard library's logger (or some other 60 | // logger that doesn't use contexts) can now be used naturally on a platform 61 | // that requires contexts (like App Engine). 62 | // 63 | // App Engine Example: 64 | // 65 | // import ( 66 | // "net/http" 67 | // 68 | // "gopkg.in/webhelp.v1/whgls" 69 | // "google.golang.org/appengine/log" 70 | // ) 71 | // 72 | // var ( 73 | // handler = ... 74 | // ) 75 | // 76 | // func init() { 77 | // whgls.SetLogOutput(log.Infof) 78 | // http.Handle("/", whmon.RequestIds(whgls.Bind(handler))) 79 | // } 80 | // 81 | func SetLogOutput( 82 | logger func(ctx context.Context, format string, args ...interface{})) { 83 | log.SetOutput(writerFunc(func(p []byte) (n int, err error) { 84 | r := Load() 85 | if r == nil { 86 | return len(p), nil 87 | } 88 | ctx := whcompat.Context(r) 89 | 90 | if rid, ok := ctx.Value(whmon.RequestId).(int64); ok { 91 | logger(whcompat.Context(r), "[R:%d] %s", rid, p) 92 | } else { 93 | // what if p has some format specifiers in it? we need to use "%s" as the 94 | // format string. 95 | logger(whcompat.Context(r), "%s", p) 96 | } 97 | 98 | return len(p), nil 99 | })) 100 | } 101 | 102 | type writerFunc func([]byte) (int, error) 103 | 104 | func (w writerFunc) Write(p []byte) (int, error) { return w(p) } 105 | -------------------------------------------------------------------------------- /whparse/query.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whparse provides some convenient input parsing helpers. 5 | package whparse // import "gopkg.in/webhelp.v1/whparse" 6 | 7 | import ( 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // ParseBool is like strconv.ParseBool but also understands yes/no 13 | func ParseBool(val string) (bool, error) { 14 | val = strings.ToLower(val) 15 | switch val { 16 | case "no", "n": 17 | return false, nil 18 | case "yes", "y": 19 | return true, nil 20 | } 21 | return strconv.ParseBool(val) 22 | } 23 | 24 | // OptInt64 parses val and returns the integer value in question, unless 25 | // parsing fails, in which case the default def is returned. 26 | func OptInt64(val string, def int64) int64 { 27 | if val == "" { 28 | return def 29 | } 30 | rv, err := strconv.ParseInt(val, 10, 64) 31 | if err != nil { 32 | return def 33 | } 34 | return rv 35 | } 36 | 37 | // OptUint64 parses val and returns the integer value in question, unless 38 | // parsing fails, in which case the default def is returned. 39 | func OptUint64(val string, def uint64) uint64 { 40 | if val == "" { 41 | return def 42 | } 43 | rv, err := strconv.ParseUint(val, 10, 64) 44 | if err != nil { 45 | return def 46 | } 47 | return rv 48 | } 49 | 50 | // OptInt32 parses val and returns the integer value in question, unless 51 | // parsing fails, in which case the default def is returned. 52 | func OptInt32(val string, def int32) int32 { 53 | if val == "" { 54 | return def 55 | } 56 | rv, err := strconv.ParseInt(val, 10, 32) 57 | if err != nil { 58 | return def 59 | } 60 | return int32(rv) 61 | } 62 | 63 | // OptUint32 parses val and returns the integer value in question, unless 64 | // parsing fails, in which case the default def is returned. 65 | func OptUint32(val string, def uint32) uint32 { 66 | if val == "" { 67 | return def 68 | } 69 | rv, err := strconv.ParseUint(val, 10, 32) 70 | if err != nil { 71 | return def 72 | } 73 | return uint32(rv) 74 | } 75 | 76 | // OptInt parses val and returns the integer value in question, unless 77 | // parsing fails, in which case the default def is returned. 78 | func OptInt(val string, def int) int { 79 | if val == "" { 80 | return def 81 | } 82 | rv, err := strconv.ParseInt(val, 10, 0) 83 | if err != nil { 84 | return def 85 | } 86 | return int(rv) 87 | } 88 | 89 | // OptUint parses val and returns the integer value in question, unless 90 | // parsing fails, in which case the default def is returned. 91 | func OptUint(val string, def uint) uint { 92 | if val == "" { 93 | return def 94 | } 95 | rv, err := strconv.ParseUint(val, 10, 0) 96 | if err != nil { 97 | return def 98 | } 99 | return uint(rv) 100 | } 101 | 102 | // OptBool parses val and returns the bool value in question, unless 103 | // parsing fails, in which case the default def is returned. 104 | func OptBool(val string, def bool) bool { 105 | if val == "" { 106 | return def 107 | } 108 | rv, err := ParseBool(val) 109 | if err != nil { 110 | return def 111 | } 112 | return rv 113 | } 114 | 115 | // OptFloat64 parses val and returns the float value in question, unless 116 | // parsing fails, in which case the default def is returned. 117 | func OptFloat64(val string, def float64) float64 { 118 | if val == "" { 119 | return def 120 | } 121 | rv, err := strconv.ParseFloat(val, 64) 122 | if err != nil { 123 | return def 124 | } 125 | return rv 126 | } 127 | 128 | // OptFloat32 parses val and returns the float value in question, unless 129 | // parsing fails, in which case the default def is returned. 130 | func OptFloat32(val string, def float32) float32 { 131 | if val == "" { 132 | return def 133 | } 134 | rv, err := strconv.ParseFloat(val, 32) 135 | if err != nil { 136 | return def 137 | } 138 | return float32(rv) 139 | } 140 | -------------------------------------------------------------------------------- /whfatal/fatal.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whfatal uses panics to make early termination of http.Handlers 5 | // easier. No other webhelp package depends on or uses this one. 6 | package whfatal // import "gopkg.in/webhelp.v1/whfatal" 7 | 8 | import ( 9 | "net/http" 10 | 11 | "gopkg.in/webhelp.v1/wherr" 12 | "gopkg.in/webhelp.v1/whmon" 13 | "gopkg.in/webhelp.v1/whredir" 14 | "gopkg.in/webhelp.v1/whroute" 15 | ) 16 | 17 | type fatalBehavior func(w http.ResponseWriter, r *http.Request) 18 | 19 | // Catch takes a Handler and returns a new one that works with Fatal, 20 | // whfatal.Redirect, and whfatal.Error. Catch will also catch panics that are 21 | // wherr.HTTPError errors. Catch should be placed *inside* a whlog.LogRequests 22 | // handler, wherr.HandleWith handlers, and a few other handlers. Otherwise, 23 | // the wrapper will be one of the things interrupted by Fatal calls. 24 | func Catch(h http.Handler) http.Handler { 25 | return whmon.MonitorResponse(whroute.HandlerFunc(h, 26 | func(w http.ResponseWriter, r *http.Request) { 27 | rw := w.(whmon.ResponseWriter) 28 | defer func() { 29 | rec := recover() 30 | if rec == nil { 31 | return 32 | } 33 | behavior, ok := rec.(fatalBehavior) 34 | if !ok { 35 | perr, ok := rec.(error) 36 | if !ok || !wherr.HTTPError.Contains(perr) { 37 | panic(rec) 38 | } 39 | behavior = func(w http.ResponseWriter, r *http.Request) { 40 | wherr.Handle(w, r, perr) 41 | } 42 | } 43 | if behavior != nil { 44 | behavior(rw, r) 45 | } 46 | if !rw.WroteHeader() { 47 | rw.WriteHeader(http.StatusInternalServerError) 48 | } 49 | }() 50 | h.ServeHTTP(rw, r) 51 | })) 52 | } 53 | 54 | // Redirect is like whredir.Redirect but panics so all additional request 55 | // processing terminates. Implemented with Fatal(). 56 | // 57 | // IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter 58 | // won't be able to be obtained. Because this requires whfatal.Catch, if 59 | // you're writing a library intended to be used by others, please avoid this 60 | // and other Fatal* methods. If you are writing a library intended to be used 61 | // by yourself, you should probably avoid these methods anyway. 62 | func Redirect(redirectTo string) { 63 | Fatal(func(w http.ResponseWriter, r *http.Request) { 64 | whredir.Redirect(w, r, redirectTo) 65 | }) 66 | } 67 | 68 | // Error is like wherr.Handle but panics so that all additional request 69 | // processing terminates. Implemented with Fatal() 70 | // 71 | // IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter 72 | // won't be able to be obtained. Because this requires whfatal.Catch, if 73 | // you're writing a library intended to be used by others, please avoid this 74 | // and other Fatal* methods. If you are writing a library intended to be used 75 | // by yourself, you should probably avoid these methods anyway. 76 | func Error(err error) { 77 | Fatal(func(w http.ResponseWriter, r *http.Request) { 78 | wherr.Handle(w, r, err) 79 | }) 80 | } 81 | 82 | // Fatal panics in a way that Catch understands to abort all additional 83 | // request processing. Once request processing has been aborted, handler is 84 | // called, if not nil. If handler doesn't write a response, a 500 will 85 | // automatically be returned. whfatal.Error and whfatal.Redirect are 86 | // implemented using this method. 87 | // 88 | // IMPORTANT: must be used with whfatal.Catch, or else the http.ResponseWriter 89 | // won't be able to be obtained. Because this requires whfatal.Catch, if 90 | // you're writing a library intended to be used by others, please avoid this 91 | // and other Fatal* methods. If you are writing a library intended to be used 92 | // by yourself, you should probably avoid these methods anyway. 93 | func Fatal(handler func(w http.ResponseWriter, r *http.Request)) { 94 | // even if handler == nil, this is NOT the same as panic(nil), so we're okay. 95 | panic(fatalBehavior(handler)) 96 | } 97 | -------------------------------------------------------------------------------- /wherr/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package wherr provides a unified error handling framework for http.Handlers. 5 | package wherr // import "gopkg.in/webhelp.v1/wherr" 6 | 7 | import ( 8 | "log" 9 | "net/http" 10 | 11 | "github.com/spacemonkeygo/errors" 12 | "github.com/spacemonkeygo/errors/errhttp" 13 | "golang.org/x/net/context" 14 | "gopkg.in/webhelp.v1" 15 | "gopkg.in/webhelp.v1/whcompat" 16 | "gopkg.in/webhelp.v1/whroute" 17 | ) 18 | 19 | var ( 20 | HTTPError = errors.NewClass("HTTP Error", errors.NoCaptureStack()) 21 | 22 | BadRequest = ErrorClass(http.StatusBadRequest) 23 | Unauthorized = ErrorClass(http.StatusUnauthorized) 24 | Forbidden = ErrorClass(http.StatusForbidden) 25 | NotFound = ErrorClass(http.StatusNotFound) 26 | MethodNotAllowed = ErrorClass(http.StatusMethodNotAllowed) 27 | NotAcceptable = ErrorClass(http.StatusNotAcceptable) 28 | RequestTimeout = ErrorClass(http.StatusRequestTimeout) 29 | Conflict = ErrorClass(http.StatusConflict) 30 | Gone = ErrorClass(http.StatusGone) 31 | LengthRequired = ErrorClass(http.StatusLengthRequired) 32 | PreconditionFailed = ErrorClass(http.StatusPreconditionFailed) 33 | RequestEntityTooLarge = ErrorClass(http.StatusRequestEntityTooLarge) 34 | RequestURITooLong = ErrorClass(http.StatusRequestURITooLong) 35 | UnsupportedMediaType = ErrorClass(http.StatusUnsupportedMediaType) 36 | RequestedRangeNotSatisfiable = ErrorClass(http.StatusRequestedRangeNotSatisfiable) 37 | ExpectationFailed = ErrorClass(http.StatusExpectationFailed) 38 | Teapot = ErrorClass(http.StatusTeapot) 39 | InternalServerError = ErrorClass(http.StatusInternalServerError) 40 | NotImplemented = ErrorClass(http.StatusNotImplemented) 41 | BadGateway = ErrorClass(http.StatusBadGateway) 42 | ServiceUnavailable = ErrorClass(http.StatusServiceUnavailable) 43 | GatewayTimeout = ErrorClass(http.StatusGatewayTimeout) 44 | 45 | errHandler = webhelp.GenSym() 46 | ) 47 | 48 | // ErrorClass creates a new subclass of HTTPError using the given HTTP status 49 | // code 50 | func ErrorClass(code int) *errors.ErrorClass { 51 | msg := http.StatusText(code) 52 | if msg == "" { 53 | msg = "Unknown error" 54 | } 55 | return HTTPError.NewClass(msg, errhttp.SetStatusCode(code)) 56 | } 57 | 58 | // Handle uses the provided error handler given via HandleWith 59 | // to handle the error, falling back to a built in default if not provided. 60 | func Handle(w http.ResponseWriter, r *http.Request, err error) { 61 | if handler, ok := whcompat.Context(r).Value(errHandler).(Handler); ok { 62 | handler.HandleError(w, r, err) 63 | return 64 | } 65 | log.Printf("error: %v", err) 66 | http.Error(w, errhttp.GetErrorBody(err), 67 | errhttp.GetStatusCode(err, http.StatusInternalServerError)) 68 | } 69 | 70 | // Handlers handle errors. After HandleError returns, it's assumed a response 71 | // has been written out and all error handling has completed. 72 | type Handler interface { 73 | HandleError(w http.ResponseWriter, r *http.Request, err error) 74 | } 75 | 76 | // HandleWith binds the given eror Handler to the request contexts that pass 77 | // through the given http.Handler. wherr.Handle will use this error Handler 78 | // for handling errors. If you're using the whfatal package, you should place 79 | // a whfatal.Catch inside this handler, so this error handler can deal 80 | // with Fatal requests. 81 | func HandleWith(eh Handler, h http.Handler) http.Handler { 82 | return whroute.HandlerFunc(h, 83 | func(w http.ResponseWriter, r *http.Request) { 84 | ctx := context.WithValue(whcompat.Context(r), errHandler, eh) 85 | h.ServeHTTP(w, whcompat.WithContext(r, ctx)) 86 | }) 87 | } 88 | 89 | // HandlingWith returns the error handler if registered, or nil if no error 90 | // handler is registered and the default should be used. 91 | func HandlingWith(ctx context.Context) Handler { 92 | handler, _ := ctx.Value(errHandler).(Handler) 93 | return handler 94 | } 95 | 96 | type HandlerFunc func(w http.ResponseWriter, r *http.Request, err error) 97 | 98 | func (f HandlerFunc) HandleError(w http.ResponseWriter, r *http.Request, 99 | err error) { 100 | f(w, r, err) 101 | } 102 | -------------------------------------------------------------------------------- /whmon/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whmon provides a means to monitor various aspects of how the request 5 | // and response is going. 6 | package whmon // import "gopkg.in/webhelp.v1/whmon" 7 | 8 | import ( 9 | "net/http" 10 | 11 | "gopkg.in/webhelp.v1/whroute" 12 | ) 13 | 14 | type rWriter struct { 15 | w http.ResponseWriter 16 | wroteHeader bool 17 | sc int 18 | written int64 19 | } 20 | 21 | var _ http.ResponseWriter = (*rWriter)(nil) 22 | 23 | func (r *rWriter) Header() http.Header { return r.w.Header() } 24 | 25 | func (r *rWriter) Write(p []byte) (int, error) { 26 | if !r.wroteHeader { 27 | r.wroteHeader = true 28 | r.sc = 200 29 | } 30 | n, err := r.w.Write(p) 31 | r.written += int64(n) 32 | return n, err 33 | } 34 | 35 | func (r *rWriter) WriteHeader(sc int) { 36 | if !r.wroteHeader { 37 | r.wroteHeader = true 38 | r.sc = sc 39 | } 40 | r.w.WriteHeader(sc) 41 | } 42 | 43 | func (r *rWriter) StatusCode() int { 44 | if !r.wroteHeader { 45 | return 200 46 | } 47 | return r.sc 48 | } 49 | 50 | func (r *rWriter) WroteHeader() bool { return r.wroteHeader } 51 | func (r *rWriter) Written() int64 { return r.written } 52 | 53 | type ResponseWriter interface { 54 | // Header, Write, and WriteHeader are exactly like http.ResponseWriter 55 | Header() http.Header 56 | Write([]byte) (int, error) 57 | WriteHeader(int) 58 | 59 | // WroteHeader returns true if the Header was sent out. Note that this can 60 | // happen if only Write is called. 61 | WroteHeader() bool 62 | // StatusCode returns the HTTP status code the Header sent. 63 | StatusCode() int 64 | // Written returns the total amount of bytes successfully passed through the 65 | // Write call. This does not include the header. 66 | Written() int64 67 | } 68 | 69 | // MonitorResponse wraps all incoming http.ResponseWriters with a 70 | // monitored ResponseWriter that keeps track of additional status information 71 | // about the outgoing response. It preserves whether or not the passed in 72 | // response writer is an http.Flusher, http.CloseNotifier, or an http.Hijacker. 73 | // whlog.LogRequests and whfatal.Catch also do this for you. 74 | func MonitorResponse(h http.Handler) http.Handler { 75 | return whroute.HandlerFunc(h, 76 | func(w http.ResponseWriter, r *http.Request) { 77 | h.ServeHTTP(wrapResponseWriter(w), r) 78 | }) 79 | } 80 | 81 | // wrapResponseWriter will wrap an http.ResponseWriter with the instrumentation 82 | // to turn it into a whmon.ResponseWriter. An http.ResponseWriter must be 83 | // turned into a whmon.ResponseWriter before being used. It's much better 84 | // to use MonitorResponse instead of WrapResponseWriter. 85 | func wrapResponseWriter(w http.ResponseWriter) ResponseWriter { 86 | if ww, ok := w.(ResponseWriter); ok { 87 | // don't do it if we already have the methods we need 88 | return ww 89 | } 90 | fw, fok := w.(http.Flusher) 91 | cnw, cnok := w.(http.CloseNotifier) 92 | hw, hok := w.(http.Hijacker) 93 | rw := &rWriter{w: w} 94 | if fok { 95 | if cnok { 96 | if hok { 97 | return struct { 98 | ResponseWriter 99 | http.Flusher 100 | http.CloseNotifier 101 | http.Hijacker 102 | }{ 103 | ResponseWriter: rw, 104 | Flusher: fw, 105 | CloseNotifier: cnw, 106 | Hijacker: hw, 107 | } 108 | } else { 109 | return struct { 110 | ResponseWriter 111 | http.Flusher 112 | http.CloseNotifier 113 | }{ 114 | ResponseWriter: rw, 115 | Flusher: fw, 116 | CloseNotifier: cnw, 117 | } 118 | } 119 | } else { 120 | if hok { 121 | return struct { 122 | ResponseWriter 123 | http.Flusher 124 | http.Hijacker 125 | }{ 126 | ResponseWriter: rw, 127 | Flusher: fw, 128 | Hijacker: hw, 129 | } 130 | } else { 131 | return struct { 132 | ResponseWriter 133 | http.Flusher 134 | }{ 135 | ResponseWriter: rw, 136 | Flusher: fw, 137 | } 138 | } 139 | } 140 | } else { 141 | if cnok { 142 | if hok { 143 | return struct { 144 | ResponseWriter 145 | http.CloseNotifier 146 | http.Hijacker 147 | }{ 148 | ResponseWriter: rw, 149 | CloseNotifier: cnw, 150 | Hijacker: hw, 151 | } 152 | } else { 153 | return struct { 154 | ResponseWriter 155 | http.CloseNotifier 156 | }{ 157 | ResponseWriter: rw, 158 | CloseNotifier: cnw, 159 | } 160 | } 161 | } else { 162 | if hok { 163 | return struct { 164 | ResponseWriter 165 | http.Hijacker 166 | }{ 167 | ResponseWriter: rw, 168 | Hijacker: hw, 169 | } 170 | } else { 171 | return rw 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /whredir/redirect.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whredir provides some helper methods and handlers for redirecting 5 | // incoming requests to other URLs. 6 | package whredir // import "gopkg.in/webhelp.v1/whredir" 7 | 8 | import ( 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | 13 | "gopkg.in/webhelp.v1/wherr" 14 | "gopkg.in/webhelp.v1/whmux" 15 | "gopkg.in/webhelp.v1/whroute" 16 | ) 17 | 18 | // Redirect is just http.Redirect with http.StatusSeeOther which I always 19 | // forget. 20 | func Redirect(w http.ResponseWriter, r *http.Request, redirectTo string) { 21 | http.Redirect(w, r, redirectTo, http.StatusSeeOther) 22 | } 23 | 24 | type redirectHandler string 25 | 26 | // RedirectHandler returns an http.Handler that redirects all requests to url. 27 | func RedirectHandler(url string) http.Handler { 28 | return redirectHandler(url) 29 | } 30 | 31 | // ServeHTTP implements http.handler 32 | func (t redirectHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 33 | Redirect(w, r, string(t)) 34 | } 35 | 36 | // Routes implements whroute.Lister 37 | func (t redirectHandler) Routes( 38 | cb func(method, path string, annotations map[string]string)) { 39 | cb(whroute.AllMethods, whroute.AllPaths, 40 | map[string]string{"Redirect": string(t)}) 41 | } 42 | 43 | var _ http.Handler = redirectHandler("") 44 | var _ whroute.Lister = redirectHandler("") 45 | 46 | // RedirectHandlerFunc is an http.Handler that redirects all requests to the 47 | // returned URL. 48 | type RedirectHandlerFunc func(r *http.Request) string 49 | 50 | // ServeHTTP implements http.handler 51 | func (f RedirectHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { 52 | Redirect(w, r, f(r)) 53 | } 54 | 55 | // Routes implements whroute.Lister 56 | func (f RedirectHandlerFunc) Routes( 57 | cb func(method, path string, annotations map[string]string)) { 58 | cb(whroute.AllMethods, whroute.AllPaths, 59 | map[string]string{"Redirect": "f(req)"}) 60 | } 61 | 62 | var _ http.Handler = RedirectHandlerFunc(nil) 63 | var _ whroute.Lister = RedirectHandlerFunc(nil) 64 | 65 | // FullURL returns the full url from the incoming request, regardless of 66 | // what current whmux.Dir is involved or how req.URL.Path has been edited. 67 | func FullURL(r *http.Request) (*url.URL, error) { 68 | u, err := url.ParseRequestURI(r.RequestURI) 69 | if err != nil { 70 | return nil, err 71 | } 72 | u.Scheme = r.URL.Scheme 73 | u.Host = r.URL.Host 74 | u.User = r.URL.User 75 | return u, nil 76 | } 77 | 78 | // RequireHTTPS returns a handler that will redirect to the same path but using 79 | // https if https was not already used. 80 | func RequireHTTPS(handler http.Handler) http.Handler { 81 | return whroute.HandlerFunc(handler, 82 | func(w http.ResponseWriter, r *http.Request) { 83 | if r.URL.Scheme == "https" { 84 | handler.ServeHTTP(w, r) 85 | return 86 | } 87 | u, err := FullURL(r) 88 | if err != nil { 89 | wherr.Handle(w, r, err) 90 | return 91 | } 92 | u.Scheme = "https" 93 | Redirect(w, r, u.String()) 94 | }) 95 | } 96 | 97 | // RequireHost returns a handler that will redirect to the same path but using 98 | // the given host if the given host was not specifically requested. 99 | func RequireHost(host string, handler http.Handler) http.Handler { 100 | if host == "*" { 101 | return handler 102 | } 103 | return whmux.Host{ 104 | host: handler, 105 | "*": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 106 | u, err := FullURL(r) 107 | if err != nil { 108 | wherr.Handle(w, r, err) 109 | return 110 | } 111 | u.Host = host 112 | Redirect(w, r, u.String()) 113 | })} 114 | } 115 | 116 | func addTrailingSlash(w http.ResponseWriter, r *http.Request, h http.Handler) { 117 | u, err := FullURL(r) 118 | if err != nil { 119 | wherr.Handle(w, r, err) 120 | return 121 | } 122 | 123 | if strings.HasSuffix(u.Path, "/") { 124 | h.ServeHTTP(w, r) 125 | return 126 | } 127 | 128 | u.Path += "/" 129 | Redirect(w, r, u.String()) 130 | } 131 | 132 | // RequireTrailingSlash makes sure all handled paths have a trailing slash. 133 | // This helps with relative URLs for other resources. 134 | func RequireTrailingSlash(h http.Handler) http.Handler { 135 | return whroute.HandlerFunc(h, 136 | func(w http.ResponseWriter, r *http.Request) { 137 | if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") { 138 | h.ServeHTTP(w, r) 139 | return 140 | } 141 | 142 | addTrailingSlash(w, r, h) 143 | }) 144 | } 145 | 146 | func RequireNextSlash(h http.Handler) http.Handler { 147 | return whroute.HandlerFunc(h, func(w http.ResponseWriter, r *http.Request) { 148 | if r.URL.Path != "/" && r.URL.Path != "" { 149 | h.ServeHTTP(w, r) 150 | return 151 | } 152 | 153 | addTrailingSlash(w, r, h) 154 | }) 155 | } 156 | -------------------------------------------------------------------------------- /whsess/cookie.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package whsess 5 | 6 | import ( 7 | "bytes" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/base64" 11 | "encoding/gob" 12 | "net/http" 13 | "sync" 14 | 15 | "golang.org/x/crypto/nacl/secretbox" 16 | "golang.org/x/net/context" 17 | "gopkg.in/webhelp.v1/whcompat" 18 | "gopkg.in/webhelp.v1/wherr" 19 | ) 20 | 21 | const ( 22 | nonceLength = 24 23 | keyLength = 32 24 | minKeyLength = 10 25 | ) 26 | 27 | type CookieOptions struct { 28 | Path string 29 | Domain string 30 | MaxAge int 31 | Secure bool 32 | HttpOnly bool 33 | } 34 | 35 | type CookieStore struct { 36 | Options CookieOptions 37 | secretCB func(context.Context) ([]byte, error) 38 | 39 | secretMtx sync.Mutex 40 | secretSetup bool 41 | secret [keyLength]byte 42 | } 43 | 44 | var _ Store = (*CookieStore)(nil) 45 | 46 | // NewCookieStore creates a secure cookie store with default settings. 47 | // Configure the Options field further if additional settings are required. 48 | func NewCookieStore(secretKey []byte) *CookieStore { 49 | rv := &CookieStore{ 50 | Options: CookieOptions{ 51 | Path: "/", 52 | MaxAge: 86400 * 30}, 53 | secretCB: func(context.Context) ([]byte, error) { 54 | return secretKey, nil 55 | }, 56 | } 57 | return rv 58 | } 59 | 60 | // NewLazyCookieStore is like NewCookieStore but loads the secretKey using 61 | // the provided callback once. This is useful for delayed initialization after 62 | // the first request for something like App Engine where you can't interact 63 | // with a database without a context. 64 | func NewLazyCookieStore(secretKey func(context.Context) ([]byte, error)) ( 65 | cs *CookieStore) { 66 | rv := &CookieStore{ 67 | Options: CookieOptions{ 68 | Path: "/", 69 | MaxAge: 86400 * 30}, 70 | secretCB: secretKey, 71 | } 72 | return rv 73 | } 74 | 75 | func (cs *CookieStore) getSecret(ctx context.Context) ( 76 | *[keyLength]byte, error) { 77 | cs.secretMtx.Lock() 78 | defer cs.secretMtx.Unlock() 79 | if cs.secretSetup { 80 | return &cs.secret, nil 81 | } 82 | secret, err := cs.secretCB(ctx) 83 | if err != nil { 84 | return nil, err 85 | } 86 | if len(secret) < minKeyLength { 87 | return nil, wherr.InternalServerError.New("cookie secret not long enough") 88 | } 89 | secretHash := sha256.Sum256(secret) 90 | copy(cs.secret[:], secretHash[:]) 91 | cs.secretSetup = true 92 | return &cs.secret, nil 93 | } 94 | 95 | // Load implements the Store interface. Not expected to be used directly. 96 | func (cs *CookieStore) Load(ctx context.Context, r *http.Request, 97 | namespace string) (rv SessionData, err error) { 98 | empty := SessionData{New: true, Values: map[interface{}]interface{}{}} 99 | secret, err := cs.getSecret(whcompat.Context(r)) 100 | if err != nil { 101 | return empty, err 102 | } 103 | c, err := r.Cookie(namespace) 104 | if err != nil || c.Value == "" { 105 | return empty, nil 106 | } 107 | data, err := base64.URLEncoding.DecodeString(c.Value) 108 | if err != nil { 109 | return empty, nil 110 | } 111 | var nonce [nonceLength]byte 112 | copy(nonce[:], data[:nonceLength]) 113 | decrypted, ok := secretbox.Open(nil, data[nonceLength:], &nonce, 114 | secret) 115 | if !ok { 116 | return empty, nil 117 | } 118 | err = gob.NewDecoder(bytes.NewReader(decrypted)).Decode(&rv.Values) 119 | if err != nil { 120 | return empty, nil 121 | } 122 | return rv, nil 123 | } 124 | 125 | // Save implements the Store interface. Not expected to be used directly. 126 | func (cs *CookieStore) Save(ctx context.Context, w http.ResponseWriter, 127 | namespace string, s SessionData) error { 128 | secret, err := cs.getSecret(ctx) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | var out bytes.Buffer 134 | err = gob.NewEncoder(&out).Encode(&s.Values) 135 | if err != nil { 136 | return err 137 | } 138 | var nonce [nonceLength]byte 139 | _, err = rand.Read(nonce[:]) 140 | if err != nil { 141 | return err 142 | } 143 | value := base64.URLEncoding.EncodeToString( 144 | secretbox.Seal(nonce[:], out.Bytes(), &nonce, secret)) 145 | 146 | return setCookie(w, &http.Cookie{ 147 | Name: namespace, 148 | Value: value, 149 | Path: cs.Options.Path, 150 | Domain: cs.Options.Domain, 151 | MaxAge: cs.Options.MaxAge, 152 | Secure: cs.Options.Secure, 153 | HttpOnly: cs.Options.HttpOnly}) 154 | } 155 | 156 | func setCookie(w http.ResponseWriter, cookie *http.Cookie) error { 157 | v := cookie.String() 158 | if v == "" { 159 | return SessionError.New("invalid cookie %#v", cookie.Name) 160 | } 161 | w.Header().Add("Set-Cookie", v) 162 | return nil 163 | } 164 | 165 | // Clear implements the Store interface. Not expected to be used directly. 166 | func (cs *CookieStore) Clear(ctx context.Context, w http.ResponseWriter, 167 | namespace string) error { 168 | return setCookie(w, &http.Cookie{ 169 | Name: namespace, 170 | Value: "", 171 | Path: cs.Options.Path, 172 | Domain: cs.Options.Domain, 173 | MaxAge: -1, 174 | Secure: cs.Options.Secure, 175 | HttpOnly: cs.Options.HttpOnly}) 176 | } 177 | -------------------------------------------------------------------------------- /whtmpl/tmpl.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whtmpl provides some helpful utilities for constructing and using 5 | // lots of html/templates 6 | package whtmpl // import "gopkg.in/webhelp.v1/whtmpl" 7 | 8 | import ( 9 | "fmt" 10 | "html/template" 11 | "net/http" 12 | "path/filepath" 13 | "runtime" 14 | "strings" 15 | 16 | "gopkg.in/webhelp.v1/wherr" 17 | ) 18 | 19 | // Pair is a useful type that allows for passing more than one current template 20 | // variable into a sub-template. 21 | // 22 | // Expected usage within a template like: 23 | // 24 | // {{ template "subtemplate" (makepair $val1 $val2) }} 25 | // 26 | // Expected usage within the subtemplate like 27 | // 28 | // {{ $val1 := .First }} 29 | // {{ $val2 := .Second }} 30 | // 31 | // "makepair" is registered as a template function inside a Collection 32 | type Pair struct { 33 | First, Second interface{} 34 | } 35 | 36 | // Collection is a useful type that helps when defining a bunch of html 37 | // inside of Go files. Assuming you want to define a template called "landing" 38 | // that references another template called "header". With a template 39 | // collection, you would make three files: 40 | // 41 | // pkg.go: 42 | // 43 | // package views 44 | // 45 | // import "gopkg.in/webhelp.v1/whtmpl" 46 | // 47 | // var Templates = whtmpl.NewCollection() 48 | // 49 | // landing.go: 50 | // 51 | // package views 52 | // 53 | // var _ = Templates.MustParse(`{{ template "header" . }} 54 | // 55 | //

Landing!

`) 56 | // 57 | // header.go: 58 | // 59 | // package views 60 | // 61 | // var _ = Templates.MustParse(`My website!`) 62 | // 63 | // Note that MustParse determines the name of the template based on the 64 | // go filename. 65 | // 66 | // A template collection by default has two additional helper functions defined 67 | // within templates: 68 | // 69 | // * makemap: creates a map out of the even number of arguments given. 70 | // * makepair: creates a Pair type of its two given arguments. 71 | // * makeslice: creates a slice of the given arguments. 72 | // * safeurl: calls template.URL with its first argument and returns the 73 | // result. 74 | // 75 | type Collection struct { 76 | group *template.Template 77 | } 78 | 79 | // Creates a new Collection. 80 | func NewCollection() *Collection { 81 | return &Collection{group: template.New("").Funcs( 82 | template.FuncMap{ 83 | "makepair": func(first, second interface{}) Pair { 84 | return Pair{First: first, Second: second} 85 | }, 86 | "makemap": makemap, 87 | "makeslice": func(args ...interface{}) []interface{} { return args }, 88 | "safeurl": func(val string) template.URL { 89 | return template.URL(val) 90 | }, 91 | "safehtml": func(val string) template.HTML { 92 | return template.HTML(val) 93 | }, 94 | })} 95 | } 96 | 97 | func makemap(vals ...interface{}) map[interface{}]interface{} { 98 | if len(vals)%2 != 0 { 99 | panic("need an even amount of values") 100 | } 101 | rv := make(map[interface{}]interface{}, len(vals)/2) 102 | for i := 0; i < len(vals); i += 2 { 103 | rv[vals[i]] = vals[i+1] 104 | } 105 | return rv 106 | } 107 | 108 | // Allows you to add and overwrite template function definitions. Mutates 109 | // called collection and returns self. 110 | func (tc *Collection) Funcs(m template.FuncMap) *Collection { 111 | tc.group = tc.group.Funcs(m) 112 | return tc 113 | } 114 | 115 | // MustParse parses template source "tmpl" and stores it in the 116 | // Collection using the name of the go file that MustParse is called 117 | // from. 118 | func (tc *Collection) MustParse(tmpl string) *template.Template { 119 | _, filename, _, ok := runtime.Caller(1) 120 | if !ok { 121 | panic("unable to determine template name") 122 | } 123 | name := strings.TrimSuffix(filepath.Base(filename), ".go") 124 | parsed, err := tc.Parse(name, tmpl) 125 | if err != nil { 126 | panic(err) 127 | } 128 | return parsed 129 | } 130 | 131 | // Parse parses the source "tmpl" and stores it in the template collection 132 | // using name "name". 133 | func (tc *Collection) Parse(name string, tmpl string) ( 134 | *template.Template, error) { 135 | if tc.group.Lookup(name) != nil { 136 | return nil, fmt.Errorf("template %#v already registered", name) 137 | } 138 | 139 | return tc.group.New(name).Parse(tmpl) 140 | } 141 | 142 | // Lookup a template by name. Returns nil if not found. 143 | func (tc *Collection) Lookup(name string) *template.Template { 144 | return tc.group.Lookup(name) 145 | } 146 | 147 | // Render writes the template out to the response writer (or any errors that 148 | // come up), with value as the template value. 149 | func (tc *Collection) Render(w http.ResponseWriter, r *http.Request, 150 | template string, values interface{}) { 151 | tmpl := tc.Lookup(template) 152 | if tmpl == nil { 153 | wherr.Handle(w, r, wherr.InternalServerError.New( 154 | "no template %#v registered", template)) 155 | return 156 | } 157 | w.Header().Set("Content-Type", "text/html") 158 | err := tmpl.Execute(w, values) 159 | if err != nil { 160 | wherr.Handle(w, r, err) 161 | return 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /whmux/argmux.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | package whmux 5 | 6 | import ( 7 | "net/http" 8 | "strconv" 9 | 10 | "golang.org/x/net/context" 11 | 12 | "gopkg.in/webhelp.v1" 13 | "gopkg.in/webhelp.v1/whcompat" 14 | "gopkg.in/webhelp.v1/wherr" 15 | "gopkg.in/webhelp.v1/whroute" 16 | ) 17 | 18 | // StringArg is a way to pull off arbitrary path elements from an incoming 19 | // URL. You'll need to create one with NewStringArg. 20 | type StringArg webhelp.ContextKey 21 | 22 | func NewStringArg() StringArg { 23 | return StringArg(webhelp.GenSym()) 24 | } 25 | 26 | // Shift takes an http.Handler and returns a new http.Handler that does 27 | // additional request processing. When an incoming request is processed, the 28 | // new http.Handler pulls the next path element off of the incoming request 29 | // path and puts the value in the current Context. It then passes processing 30 | // off to the wrapped http.Handler. The value will be an empty string if no 31 | // argument is found. 32 | func (a StringArg) Shift(h http.Handler) http.Handler { 33 | return a.ShiftOpt(h, notFoundHandler{}) 34 | } 35 | 36 | type stringOptShift struct { 37 | a StringArg 38 | found, notfound http.Handler 39 | } 40 | 41 | // ShiftOpt is like Shift but the first handler is used only if there's an 42 | // argument found and the second handler is used if there isn't. 43 | func (a StringArg) ShiftOpt(found, notfound http.Handler) http.Handler { 44 | return stringOptShift{a: a, found: found, notfound: notfound} 45 | } 46 | 47 | func (ssi stringOptShift) ServeHTTP(w http.ResponseWriter, r *http.Request) { 48 | arg, newpath := Shift(r.URL.Path) 49 | if arg == "" { 50 | ssi.notfound.ServeHTTP(w, r) 51 | return 52 | } 53 | r.URL.Path = newpath 54 | ctx := context.WithValue(whcompat.Context(r), ssi.a, arg) 55 | ssi.found.ServeHTTP(w, whcompat.WithContext(r, ctx)) 56 | } 57 | 58 | func (ssi stringOptShift) Routes(cb func(string, string, map[string]string)) { 59 | whroute.Routes(ssi.found, 60 | func(method, path string, annotations map[string]string) { 61 | cb(method, "/"+path, annotations) 62 | }) 63 | whroute.Routes(ssi.notfound, 64 | func(method, path string, annotations map[string]string) { 65 | switch path { 66 | case whroute.AllPaths, "/": 67 | cb(method, "/", annotations) 68 | } 69 | }) 70 | } 71 | 72 | var _ http.Handler = stringOptShift{} 73 | var _ whroute.Lister = stringOptShift{} 74 | 75 | // Get returns a stored value for the Arg from the Context, or "" if no value 76 | // was found (which won't be the case if a higher-level handler was this 77 | // arg) 78 | func (a StringArg) Get(ctx context.Context) (val string) { 79 | if val, ok := ctx.Value(a).(string); ok { 80 | return val 81 | } 82 | return "" 83 | } 84 | 85 | // IntArg is a way to pull off numeric path elements from an incoming 86 | // URL. You'll need to create one with NewIntArg. 87 | type IntArg webhelp.ContextKey 88 | 89 | func NewIntArg() IntArg { 90 | return IntArg(webhelp.GenSym()) 91 | } 92 | 93 | type notFoundHandler struct{} 94 | 95 | func (notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 96 | wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", r.URL.Path)) 97 | } 98 | 99 | func (notFoundHandler) Routes(cb func(string, string, map[string]string)) {} 100 | 101 | var _ http.Handler = notFoundHandler{} 102 | var _ whroute.Lister = notFoundHandler{} 103 | 104 | // Shift takes an http.Handler and returns a new http.Handler that does 105 | // additional request processing. When an incoming request is processed, the 106 | // new http.Handler pulls the next path element off of the incoming request 107 | // path and puts the value in the current Context. It then passes processing 108 | // off to the wrapped http.Handler. It responds with a 404 if no numeric value 109 | // is found. 110 | func (a IntArg) Shift(h http.Handler) http.Handler { 111 | return a.ShiftOpt(h, notFoundHandler{}) 112 | } 113 | 114 | type intOptShift struct { 115 | a IntArg 116 | found, notfound http.Handler 117 | } 118 | 119 | // ShiftOpt is like Shift but will only use the first handler if there's a 120 | // numeric argument found and the second handler otherwise. 121 | func (a IntArg) ShiftOpt(found, notfound http.Handler) http.Handler { 122 | return intOptShift{a: a, found: found, notfound: notfound} 123 | } 124 | 125 | func (isi intOptShift) ServeHTTP(w http.ResponseWriter, r *http.Request) { 126 | str, newpath := Shift(r.URL.Path) 127 | val, err := strconv.ParseInt(str, 10, 64) 128 | if err != nil { 129 | isi.notfound.ServeHTTP(w, r) 130 | return 131 | } 132 | r.URL.Path = newpath 133 | ctx := context.WithValue(whcompat.Context(r), isi.a, val) 134 | isi.found.ServeHTTP(w, whcompat.WithContext(r, ctx)) 135 | } 136 | 137 | func (isi intOptShift) Routes(cb func(string, string, map[string]string)) { 138 | whroute.Routes(isi.found, func(method, path string, 139 | annotations map[string]string) { 140 | cb(method, "/"+path, annotations) 141 | }) 142 | whroute.Routes(isi.notfound, cb) 143 | } 144 | 145 | var _ http.Handler = intOptShift{} 146 | var _ whroute.Lister = intOptShift{} 147 | 148 | // Get returns a stored value for the Arg from the Context and ok = true if 149 | // found, or ok = false if no value was found (which won't be the case if a 150 | // higher-level handler was this arg) 151 | func (a IntArg) Get(ctx context.Context) (val int64, ok bool) { 152 | if val, ok := ctx.Value(a).(int64); ok { 153 | return val, true 154 | } 155 | return 0, false 156 | } 157 | 158 | // MustGet is like Get but panics in cases when ok would be false. If used with 159 | // whfatal.Catch, will return a 404 to the user. 160 | func (a IntArg) MustGet(ctx context.Context) (val int64) { 161 | val, ok := ctx.Value(a).(int64) 162 | if !ok { 163 | panic(wherr.NotFound.New("Required argument missing")) 164 | } 165 | return val 166 | } 167 | -------------------------------------------------------------------------------- /whmux/mux.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2016 JT Olds 2 | // See LICENSE for copying information 3 | 4 | // Package whmux provides some useful request mux helpers for demultiplexing 5 | // requests to one of a number of handlers. 6 | package whmux // import "gopkg.in/webhelp.v1/whmux" 7 | 8 | import ( 9 | "net/http" 10 | "sort" 11 | "strings" 12 | 13 | "gopkg.in/webhelp.v1/wherr" 14 | "gopkg.in/webhelp.v1/whroute" 15 | ) 16 | 17 | // Dir is an http.Handler that mimics a directory. It mutates an incoming 18 | // request's URL.Path to properly namespace handlers. This way a handler can 19 | // assume it has the root of its section. If you want the original URL, use 20 | // req.RequestURI (but don't modify it). 21 | type Dir map[string]http.Handler 22 | 23 | // ServeHTTP implements http.handler 24 | func (d Dir) ServeHTTP(w http.ResponseWriter, r *http.Request) { 25 | dir, left := Shift(r.URL.Path) 26 | handler, ok := d[dir] 27 | if !ok { 28 | wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", dir)) 29 | return 30 | } 31 | if left == "" { 32 | left = "/" 33 | } 34 | r.URL.Path = left 35 | handler.ServeHTTP(w, r) 36 | } 37 | 38 | // Routes implements whroute.Lister 39 | func (d Dir) Routes( 40 | cb func(method, path string, annotations map[string]string)) { 41 | keys := make([]string, 0, len(d)) 42 | for element := range d { 43 | keys = append(keys, element) 44 | } 45 | sort.Strings(keys) 46 | for _, element := range keys { 47 | whroute.Routes(d[element], 48 | func(method, path string, annotations map[string]string) { 49 | if element == "" { 50 | cb(method, "/", annotations) 51 | } else { 52 | cb(method, "/"+element+path, annotations) 53 | } 54 | }) 55 | } 56 | } 57 | 58 | var _ http.Handler = Dir(nil) 59 | var _ whroute.Lister = Dir(nil) 60 | 61 | // Shift pulls the first directory out of the path and returns the remainder. 62 | func Shift(path string) (dir, left string) { 63 | // slice off the first "/"s if they exists 64 | path = strings.TrimLeft(path, "/") 65 | 66 | if len(path) == 0 { 67 | return "", "" 68 | } 69 | 70 | // find the first '/' after the initial one 71 | split := strings.Index(path, "/") 72 | if split == -1 { 73 | return path, "" 74 | } 75 | return path[:split], path[split:] 76 | } 77 | 78 | // Method is an http.Handler muxer that keys off of the given HTTP request 79 | // method. 80 | type Method map[string]http.Handler 81 | 82 | // ServeHTTP implements http.handler 83 | func (m Method) ServeHTTP(w http.ResponseWriter, r *http.Request) { 84 | if handler, found := m[r.Method]; found { 85 | handler.ServeHTTP(w, r) 86 | return 87 | } 88 | wherr.Handle(w, r, 89 | wherr.MethodNotAllowed.New("bad method: %#v", r.Method)) 90 | } 91 | 92 | // Routes implements whroute.Lister 93 | func (m Method) Routes( 94 | cb func(method, path string, annotations map[string]string)) { 95 | keys := make([]string, 0, len(m)) 96 | for method := range m { 97 | keys = append(keys, method) 98 | } 99 | sort.Strings(keys) 100 | for _, method := range keys { 101 | handler := m[method] 102 | whroute.Routes(handler, 103 | func(_, path string, annotations map[string]string) { 104 | cb(method, path, annotations) 105 | }) 106 | } 107 | } 108 | 109 | var _ http.Handler = Method(nil) 110 | var _ whroute.Lister = Method(nil) 111 | 112 | // ExactPath takes an http.Handler that returns a new http.Handler that doesn't 113 | // accept any more path elements and returns a 404 if more are provided. 114 | func ExactPath(h http.Handler) http.Handler { 115 | return Dir{"": h} 116 | } 117 | 118 | // RequireMethod takes an http.Handler and returns a new http.Handler that only 119 | // works with the given HTTP method. If a different method is used, a 405 is 120 | // returned. 121 | func RequireMethod(method string, h http.Handler) http.Handler { 122 | return Method{method: h} 123 | } 124 | 125 | // RequireGet is simply RequireMethod but called with "GET" as the first 126 | // argument. 127 | func RequireGet(h http.Handler) http.Handler { 128 | return RequireMethod("GET", h) 129 | } 130 | 131 | // Exact is simply RequireGet and ExactPath called together. 132 | func Exact(h http.Handler) http.Handler { 133 | return RequireGet(ExactPath(h)) 134 | } 135 | 136 | // Host is an http.Handler that chooses a subhandler based on the request 137 | // Host header. The star host ("*") is a default handler. 138 | type Host map[string]http.Handler 139 | 140 | // ServeHTTP implements http.handler 141 | func (h Host) ServeHTTP(w http.ResponseWriter, r *http.Request) { 142 | handler, ok := h[r.Host] 143 | if !ok { 144 | handler, ok = h["*"] 145 | if !ok { 146 | wherr.Handle(w, r, wherr.NotFound.New("host: %#v", r.Host)) 147 | return 148 | } 149 | } 150 | handler.ServeHTTP(w, r) 151 | } 152 | 153 | // Routes implements whroute.Lister 154 | func (h Host) Routes( 155 | cb func(method, path string, annotations map[string]string)) { 156 | keys := make([]string, 0, len(h)) 157 | for element := range h { 158 | keys = append(keys, element) 159 | } 160 | sort.Strings(keys) 161 | for _, host := range keys { 162 | whroute.Routes(h[host], 163 | func(method, path string, annotations map[string]string) { 164 | cp := make(map[string]string, len(annotations)+1) 165 | for key, val := range annotations { 166 | cp[key] = val 167 | } 168 | cp["Host"] = host 169 | cb(method, path, cp) 170 | }) 171 | } 172 | } 173 | 174 | var _ http.Handler = Host(nil) 175 | var _ whroute.Lister = Host(nil) 176 | 177 | // Overlay is essentially a Dir that you can put in front of another 178 | // http.Handler. If the requested entry isn't in the overlay Dir, the 179 | // Default will be used. If no Default is specified this works exactly the 180 | // same as a Dir. 181 | type Overlay struct { 182 | Default http.Handler 183 | Overlay Dir 184 | } 185 | 186 | // ServeHTTP implements http.handler 187 | func (o Overlay) ServeHTTP(w http.ResponseWriter, r *http.Request) { 188 | dir, left := Shift(r.URL.Path) 189 | handler, ok := o.Overlay[dir] 190 | if !ok { 191 | if o.Default == nil { 192 | wherr.Handle(w, r, wherr.NotFound.New("resource: %#v", dir)) 193 | return 194 | } 195 | o.Default.ServeHTTP(w, r) 196 | return 197 | } 198 | r.URL.Path = left 199 | handler.ServeHTTP(w, r) 200 | } 201 | 202 | // Routes implements whroute.Lister 203 | func (o Overlay) Routes( 204 | cb func(method, path string, annotations map[string]string)) { 205 | whroute.Routes(o.Overlay, cb) 206 | if o.Default != nil { 207 | whroute.Routes(o.Default, cb) 208 | } 209 | } 210 | 211 | var _ http.Handler = Overlay{} 212 | var _ whroute.Lister = Overlay{} 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------