Commit cd528ed7 authored by ale's avatar ale

replace PuerkitoBio/ghost with gorilla/handlers

The only thing we need from ghost is the GZIPHandler. The
gorilla/handlers package has a CompressHandler, and much fewer
dependencies. Also, add a test that attempts a request with
an Accept-Encoding: deflate header.
parent dd7caa6c
......@@ -5,10 +5,6 @@
"./..."
],
"Deps": [
{
"ImportPath": "github.com/PuerkitoBio/ghost",
"Rev": "a0146f2f931611b8bfe40f07018c97a7c881c76a"
},
{
"ImportPath": "github.com/aryann/difflib",
"Rev": "035af7c09b120b0909dd998c92745b82f61e0b1c"
......@@ -22,10 +18,6 @@
"Comment": "v2.0.0-22-g9847b93",
"Rev": "9847b93751a5fbaf227b893d172cee0104ac6427"
},
{
"ImportPath": "github.com/garyburd/redigo/redis",
"Rev": "08550c8cc5b9d419fc472629d749d33b98f1d785"
},
{
"ImportPath": "github.com/gonuts/commander",
"Rev": "f8ba4e959ca914268227c3ebbd7f6bf0bb35541a"
......@@ -35,8 +27,8 @@
"Rev": "741a6cbd37a30dedc93f817e7de6aaf0ca38a493"
},
{
"ImportPath": "github.com/gorilla/securecookie",
"Rev": "1b0c7f6e9ab3d7f500fd7d50c7ad835ff428139b"
"ImportPath": "github.com/gorilla/handlers",
"Rev": "40694b40f4a928c062f56849989d3e9cd0570e5f"
},
{
"ImportPath": "github.com/jmcvetta/randutil",
......@@ -46,10 +38,6 @@
"ImportPath": "github.com/miekg/dns",
"Rev": "874ec871288a738d8d87fd5cec1dd71e88fdacb2"
},
{
"ImportPath": "github.com/nu7hatch/gouuid",
"Rev": "179d4d0c4d8d407a32af483c2354df1d2c91e6c3"
},
{
"ImportPath": "github.com/ugorji/go/codec",
"Rev": "5abd4e96a45c386928ed2ca2a7ef63e2533e18ec"
......
Copyright (c) 2013, Martin Angers
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# Ghost
Ghost is a web development library loosely inspired by node's [Connect library][connect]. It provides a number of simple, single-responsibility HTTP handlers that can be combined to build a full-featured web server, and a generic template engine integration interface.
It stays close to the metal, not abstracting Go's standard library away. As a matter of fact, any stdlib handler can be used with Ghost's handlers, they simply are `net/http.Handler`'s.
## Installation and documentation
`go get github.com/PuerkitoBio/ghost`
[API reference][godoc]
*Status* : Still under development, things will change.
## Example
See the /ghostest directory for a complete working example of a website built with Ghost. It shows all handlers and template support of Ghost.
## Handlers
Ghost offers the following handlers:
* BasicAuthHandler : basic authentication support.
* ContextHandler : key-value map provider for the duration of the request.
* FaviconHandler : simple and efficient favicon renderer.
* GZIPHandler : gzip-compresser for the body of the response.
* LogHandler : fully customizable request logger.
* PanicHandler : panic-catching handler to control the error response.
* SessionHandler : store-agnostic server-side session provider.
* StaticHandler : convenience handler that wraps a call to `net/http.ServeFile`.
Two stores are provided for the session persistence, `MemoryStore`, an in-memory map that is not suited for production environment, and `RedisStore`, a more robust and scalable [redigo][]-based Redis store. Because of the generic `SessionStore` interface, custom stores can easily be created as needed.
The `handlers` package also offers the `ChainableHandler` interface, which supports combining HTTP handlers in a sequential fashion, and the `ChainHandlers()` function that creates a new handler from the sequential combination of any number of handlers.
As a convenience, all functions that take a `http.Handler` as argument also have a corresponding function with the `Func` suffix that take a `http.HandlerFunc` instead as argument. This saves the type-cast when a simple handler function is passed (for example, `SessionHandler()` and `SessionHandlerFunc()`).
### Handlers Design
The HTTP handlers such as Basic Auth and Context need to store some state information to provide their functionality. Instead of using variables and a mutex to control shared access, Ghost augments the `http.ResponseWriter` interface that is part of the Handler's `ServeHTTP()` function signature. Because this instance is unique for each request and is not shared, there is no locking involved to access the state information.
However, when combining such handlers, Ghost needs a way to move through the chain of augmented ResponseWriters. This is why these *augmented writers* need to implement the `WrapWriter` interface. A single method is required, `WrappedWriter() http.ResponseWriter`, which returns the wrapped ResponseWriter.
And to get back a specific augmented writer, the `GetResponseWriter()` function is provided. It takes a ResponseWriter and a predicate function as argument, and returns the requested specific writer using the *comma-ok* pattern. Example, for the session writer:
```Go
func getSessionWriter(w http.ResponseWriter) (*sessResponseWriter, bool) {
ss, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
_, ok := tst.(*sessResponseWriter)
return ok
})
if ok {
return ss.(*sessResponseWriter), true
}
return nil, false
}
```
Ghost does not provide a muxer, there are already many great ones available, but I would recommend Go's native `http.ServeMux` or [pat][] because it has great features and plays well with Ghost's design. Gorilla's muxer is very popular, but since it depends on Gorilla's (mutex-based) context provider, this is redundant with Ghost's context.
## Templates
Ghost supports the following template engines:
* Go's native templates (needs work, at the moment does not work with nested templates)
* [Amber][]
TODO : Go's mustache implementation.
### Templates Design
The template engines can be registered much in the same way as database drivers, just by importing for side effects (using `_ "import/path"`). The `init()` function of the template engine's package registers the template compiler with the correct file extension, and the engine can be used.
## License
The [BSD 3-Clause license][lic].
[connect]: https://github.com/senchalabs/connect
[godoc]: http://godoc.org/github.com/PuerkitoBio/ghost
[lic]: http://opensource.org/licenses/BSD-3-Clause
[redigo]: https://github.com/garyburd/redigo
[pat]: https://github.com/bmizerany/pat
[amber]: https://github.com/eknkc/amber
package ghost
import (
"log"
)
// Logging function, defaults to Go's native log.Printf function. The idea to use
// this instead of a *log.Logger struct is that it can be set to any of log.{Printf,Fatalf, Panicf},
// but also to more flexible userland loggers like SeeLog (https://github.com/cihub/seelog).
// It could be set, for example, to SeeLog's Debugf function. Any function with the
// signature func(fmt string, params ...interface{}).
var LogFn = log.Printf
<html>
<head>
<title>Ghost Test</title>
<link type="text/css" rel="stylesheet" href="/public/styles.css">
<link type="text/css" rel="stylesheet" href="/public/bootstrap-combined.min.css">
</head>
<body>
<h1>Welcome to Ghost Test</h1>
<img src="/public/logo.png" alt="peace" />
<ol>
<li><a href="/session">Session</a></li>
<li><a href="/session/auth">Authenticated Session</a></li>
<li><a href="/context">Chained Context</a></li>
<li><a href="/panic">Panic</a></li>
<li><a href="/public/styles.css">Styles.css</a></li>
<li><a href="/public/jquery-2.0.0.min.js">JQuery</a></li>
<li><a href="/public/logo.png">Logo</a></li>
</ol>
<script src="/public/jquery-2.0.0.min.js"></script>
</body>
</html>
// Ghostest is an interactive end-to-end Web site application to test
// the ghost packages. It serves the following URLs, with the specified
// features (handlers):
//
// / : panic;log;gzip;static; -> serve file index.html
// /public/styles.css : panic;log;gzip;StripPrefix;FileServer; -> serve directory public/
// /public/script.js : panic;log;gzip;StripPrefix;FileServer; -> serve directory public/
// /public/logo.pn : panic;log;gzip;StripPrefix;FileServer; -> serve directory public/
// /session : panic;log;gzip;session;context;Custom; -> serve dynamic Go template
// /session/auth : panic;log;gzip;session;context;basicAuth;Custom; -> serve dynamic template
// /panic : panic;log;gzip;Custom; -> panics
// /context : panic;log;gzip;context;Custom1;Custom2; -> serve dynamic Amber template
package main
import (
"log"
"net/http"
"time"
"git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/PuerkitoBio/ghost/handlers"
"git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/PuerkitoBio/ghost/templates"
_ "git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/PuerkitoBio/ghost/templates/amber"
_ "git.autistici.org/ale/autoradio/Godeps/_workspace/src/github.com/PuerkitoBio/ghost/templates/gotpl"
"github.com/bmizerany/pat"
)
const (
sessionPageTitle = "Session Page"
sessionPageAuthTitle = "Authenticated Session Page"
sessionPageKey = "txt"
contextPageKey = "time"
sessionExpiration = 10 // Session expires after 10 seconds
)
var (
// Create the common session store and secret
memStore = handlers.NewMemoryStore(1)
secret = "testimony of the ancients"
)
// The struct used to pass data to the session template.
type sessionPageInfo struct {
SessionID string
Title string
Text string
}
// Authenticate the Basic Auth credentials.
func authenticate(u, p string) (interface{}, bool) {
if u == "user" && p == "pwd" {
return u + p, true
}
return nil, false
}
// Handle the session page requests.
func sessionPageRenderer(w handlers.GhostWriter, r *http.Request) {
var (
txt interface{}
data sessionPageInfo
title string
)
ssn := w.Session()
if r.Method == "GET" {
txt = ssn.Data[sessionPageKey]
} else {
txt = r.FormValue(sessionPageKey)
ssn.Data[sessionPageKey] = txt
}
if r.URL.Path == "/session/auth" {
title = sessionPageAuthTitle
} else {
title = sessionPageTitle
}
if txt != nil {
data = sessionPageInfo{ssn.ID(), title, txt.(string)}
} else {
data = sessionPageInfo{ssn.ID(), title, "[nil]"}
}
err := templates.Render("templates/session.tmpl", w, data)
if err != nil {
panic(err)
}
}
// Prepare the context value for the chained handlers context page.
func setContext(w handlers.GhostWriter, r *http.Request) {
w.Context()[contextPageKey] = time.Now().String()
}
// Retrieve the context value and render the chained handlers context page.
func renderContextPage(w handlers.GhostWriter, r *http.Request) {
err := templates.Render("templates/amber/context.amber",
w, &struct{ Val string }{w.Context()[contextPageKey].(string)})
if err != nil {
panic(err)
}
}
// Prepare the web server and kick it off.
func main() {
// Blank the default logger's prefixes
log.SetFlags(0)
// Compile the dynamic templates (native Go templates and Amber
// templates are both registered via the for-side-effects-only imports)
err := templates.CompileDir("./templates/")
if err != nil {
panic(err)
}
// Set the simple routes for static files
mux := pat.New()
mux.Get("/", handlers.StaticFileHandler("./index.html"))
mux.Get("/public/", http.StripPrefix("/public/", http.FileServer(http.Dir("./public/"))))
// Set the more complex routes for session handling and dynamic page (same
// handler is used for both GET and POST).
ssnOpts := handlers.NewSessionOptions(memStore, secret)
ssnOpts.CookieTemplate.MaxAge = sessionExpiration
hSsn := handlers.SessionHandler(
handlers.ContextHandlerFunc(
handlers.GhostHandlerFunc(sessionPageRenderer),
1),
ssnOpts)
mux.Get("/session", hSsn)
mux.Post("/session", hSsn)
hAuthSsn := handlers.BasicAuthHandler(hSsn, authenticate, "")
mux.Get("/session/auth", hAuthSsn)
mux.Post("/session/auth", hAuthSsn)
// Set the handler for the chained context route
mux.Get("/context", handlers.ContextHandler(handlers.ChainHandlerFuncs(
handlers.GhostHandlerFunc(setContext),
handlers.GhostHandlerFunc(renderContextPage)),
1))
// Set the panic route, which simply panics
mux.Get("/panic", http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
panic("explicit panic")
}))
// Combine the top level handlers, that wrap around the muxer.
// Panic is the outermost, so that any panic is caught and responded to with a code 500.
// Log is next, so that every request is logged along with the URL, status code and response time.
// GZIP is then applied, so that content is compressed.
// Finally, the muxer finds the specific handler that applies to the route.
h := handlers.FaviconHandler(
handlers.PanicHandler(
handlers.LogHandler(
handlers.GZIPHandler(
mux,
nil),
handlers.NewLogOptions(nil, handlers.Ltiny)),
nil),
"./public/favicon.ico",
48*time.Hour)
// Assign the combined handler to the server.
http.Handle("/", h)
// Start it up.
if err := http.ListenAndServe(":9000", nil); err != nil {
panic(err)
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
!!! 5
html
head
title Chained Context
link[type="text/css"][rel="stylesheet"][href="/public/bootstrap-combined.min.css"]
body
h1 Chained Context
h2 Value found: #{Val}
<html>
<head>
<title>{{ .Title }}</title>
<link type="text/css" rel="stylesheet" href="/public/styles.css">
<link type="text/css" rel="stylesheet" href="/public/bootstrap-combined.min.css">
</head>
<body>
<h1>Session: {{ .SessionID }}</h1>
<ol>
<li><a href="/">Home</a></li>
<li><a href="/session">Session</a></li>
<li><a href="/session/auth">Authenticated Session</a></li>
<li><a href="/context">Chained Context</a></li>
<li><a href="/panic">Panic</a></li>
<li><a href="/public/styles.css">Styles.css</a></li>
<li><a href="/public/jquery-2.0.0.min.js">JQuery</a></li>
<li><a href="/public/logo.png">Logo</a></li>
</ol>
<h2>Current Value: {{ .Text }}</h2>
<form method="POST">
<input type="text" name="txt" placeholder="some value to save to session"></input>
<button type="submit">Submit</button>
</form>
<script src="/public/jquery-2.0.0.min.js"></script>
</body>
</html>
package handlers
// Inspired by node.js' Connect library implementation of the basicAuth middleware.
// https://github.com/senchalabs/connect
import (
"bytes"
"encoding/base64"
"fmt"
"net/http"
"strings"
)
// Internal writer that keeps track of the currently authenticated user.
type userResponseWriter struct {
http.ResponseWriter
user interface{}
userName string
}
// Implement the WrapWriter interface.
func (this *userResponseWriter) WrappedWriter() http.ResponseWriter {
return this.ResponseWriter
}
// Writes an unauthorized response to the client, specifying the expected authentication
// information.
func Unauthorized(w http.ResponseWriter, realm string) {
w.Header().Set("Www-Authenticate", fmt.Sprintf(`Basic realm="%s"`, realm))
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorized"))
}
// Writes a bad request response to the client, with an optional message.
func BadRequest(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusBadRequest)
if msg == "" {
msg = "Bad Request"
}
w.Write([]byte(msg))
}
// BasicAuthHandlerFunc is the same as BasicAuthHandler, it is just a convenience
// signature that accepts a func(http.ResponseWriter, *http.Request) instead of
// a http.Handler interface. It saves the boilerplate http.HandlerFunc() cast.
func BasicAuthHandlerFunc(h http.HandlerFunc,
authFn func(string, string) (interface{}, bool), realm string) http.HandlerFunc {
return BasicAuthHandler(h, authFn, realm)
}
// Returns a Basic Authentication handler, protecting the wrapped handler from
// being accessed if the authentication function is not successful.
func BasicAuthHandler(h http.Handler,
authFn func(string, string) (interface{}, bool), realm string) http.HandlerFunc {
if realm == "" {
realm = "Authorization Required"
}
return func(w http.ResponseWriter, r *http.Request) {
// Self-awareness
if _, ok := GetUser(w); ok {
h.ServeHTTP(w, r)
return
}
authInfo := r.Header.Get("Authorization")
if authInfo == "" {
// No authorization info, return 401
Unauthorized(w, realm)
return
}
parts := strings.Split(authInfo, " ")
if len(parts) != 2 {
BadRequest(w, "Bad authorization header")
return
}
scheme := parts[0]
creds, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
BadRequest(w, "Bad credentials encoding")
return
}
index := bytes.Index(creds, []byte(":"))
if scheme != "Basic" || index < 0 {
BadRequest(w, "Bad authorization header")
return
}
user, pwd := string(creds[:index]), string(creds[index+1:])
udata, ok := authFn(user, pwd)
if ok {
// Save user data and continue
uw := &userResponseWriter{w, udata, user}
h.ServeHTTP(uw, r)
} else {
Unauthorized(w, realm)
}
}
}
// Return the currently authenticated user. This is the same data that was returned
// by the authentication function passed to BasicAuthHandler.
func GetUser(w http.ResponseWriter) (interface{}, bool) {
usr, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
_, ok := tst.(*userResponseWriter)
return ok
})
if ok {
return usr.(*userResponseWriter).user, true
}
return nil, false
}
// Return the currently authenticated user name. This is the user name that was
// authenticated for the current request.
func GetUserName(w http.ResponseWriter) (string, bool) {
usr, ok := GetResponseWriter(w, func(tst http.ResponseWriter) bool {
_, ok := tst.(*userResponseWriter)
return ok
})
if ok {
return usr.(*userResponseWriter).userName, true
}
return "", false
}
package handlers
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestUnauth(t *testing.T) {
h := BasicAuthHandler(StaticFileHandler("./testdata/script.js"), func(u, pwd string) (interface{}, bool) {
if u == "me" && pwd == "you" {
return u, true
}
return nil, false
}, "foo")
s := httptest.NewServer(h)
defer s.Close()
res, err := http.Get(s.URL)
if err != nil {
panic(err)
}
assertStatus(http.StatusUnauthorized, res.StatusCode, t)
assertHeader("Www-Authenticate", `Basic realm="foo"`, res, t)
}
func TestGzippedAuth(t *testing.T) {
h := GZIPHandler(BasicAuthHandler(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
usr, ok := GetUser(w)
if assertTrue(ok, "expected authenticated user, got false", t) {
assertTrue(usr.(string) == "meyou", fmt.Sprintf("expected user data to be 'meyou', got '%s'", usr), t)
}
usr, ok = GetUserName(w)
if assertTrue(ok, "expected authenticated user name, got false", t) {
assertTrue(usr == "me", fmt.Sprintf("expected user name to be 'me', got '%s'", usr), t)
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(usr.(string)))
}), func(u, pwd string) (interface{}, bool) {
if u == "me" && pwd == "you" {
return u + pwd, true
}
return nil, false
}, ""), nil)
s := httptest.NewServer(h)
defer s.Close()
req, err := http.NewRequest("GET", "http://me:you@"+s.URL[7:], nil)
if err != nil {
panic(err)
}
req.Header.Set("Accept-Encoding", "gzip")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
assertStatus(http.StatusOK, res.StatusCode, t)
assertGzippedBody([]byte("me"), res, t)
}
package handlers
import (
"net/http"
)
// ChainableHandler is a valid Handler interface, and adds the possibility to
// chain other handlers.
type ChainableHandler interface {
http.Handler
Chain(http.Handler) ChainableHandler
ChainFunc(http.HandlerFunc) ChainableHandler
}
// Default implementation of a simple ChainableHandler
type chainHandler struct {
http.Handler
}
func (this *chainHandler) ChainFunc(h http.HandlerFunc) ChainableHandler {
return this.Chain(h)
}
// Implementation of the ChainableHandler interface, calls the chained handler
// after the current one (sequential).
func (this *chainHandler) Chain(h http.Handler) ChainableHandler {
return &chainHandler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add the chained handler after the call to this handler
this.ServeHTTP(w, r)
h.ServeHTTP(w, r)
}),
}
}
// Convert a standard http handler to a chainable handler interface.
func NewChainableHandler(h http.Handler) ChainableHandler {
return &chainHandler{
h,
}
}
// Helper function to chain multiple handler functions in a single call.
func ChainHandlerFuncs(h ...http.HandlerFunc) ChainableHandler {
return &chainHandler{
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, v := range h {