// Efficient favicon handler, mostly a port of node's Connect library implementation // of the favicon middleware. // https://github.com/senchalabs/connect func FaviconHandler(h http.Handler, path string, maxAge time.Duration) http.HandlerFunc { var buf []byte var hash string return func(w http.ResponseWriter, r *http.Request) { var err error if r.URL.Path == "/favicon.ico" { if buf == nil { // Read from file and cache ghost.LogFn("ghost.favicon : serving from %s", path) buf, err = ioutil.ReadFile(path) if err != nil { ghost.LogFn("ghost.favicon : error reading file : %s", err) http.NotFound(w, r) return } hash = hashContent(buf) } writeHeaders(w.Header(), buf, maxAge, hash) writeBody(w, r, buf) } else { h.ServeHTTP(w, r) } } }
// Write the content of the favicon, or respond with a 404 not found // in case of error (hardly a critical error). func writeBody(w http.ResponseWriter, r *http.Request, buf []byte) { _, err := w.Write(buf) if err != nil { ghost.LogFn("ghost.favicon : error writing response : %s", err) http.NotFound(w, r) } }
// Compute a CRC32 hash of the session's JSON-encoded contents. func hash(s *Session) uint32 { data, err := json.Marshal(s) if err != nil { ghost.LogFn("ghost.session : error hash : %s", err) return 0 // 0 is always treated as "modified" session content } return crc32.ChecksumIEEE(data) }
// Compile the specified template file if there is a matching compiler. func compileTemplate(p, base string) error { ext := path.Ext(p) c, ok := compilers[ext] // Ignore file if no template compiler exist for this extension if ok { t, err := c.Compile(p) if err != nil { return err } key, err := filepath.Rel(base, p) if err != nil { return err } ghost.LogFn("ghost.templates : storing template for file %s", key) templaters[key] = t } return nil }
// Compile all templates that have a matching compiler (based on their extension) in the // specified directory. func CompileDir(dir string) error { mu.Lock() defer mu.Unlock() return filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { if fi == nil { return ErrDirNotExist } if !fi.IsDir() { err = compileTemplate(path, dir) if err != nil { ghost.LogFn("ghost.templates : error compiling template %s : %s", path, err) return err } } return nil }) }
// Create a Session handler to offer the Session behaviour to the specified handler. func SessionHandler(h http.Handler, opts *SessionOptions) http.HandlerFunc { // Make sure the required cookie fields are set if opts.CookieTemplate.Name == "" { opts.CookieTemplate.Name = defaultCookieName } if opts.CookieTemplate.Path == "" { opts.CookieTemplate.Path = "/" } // Secret is required if opts.Secret == "" { panic(ErrSessionSecretMissing) } // Return the actual handler return func(w http.ResponseWriter, r *http.Request) { if _, ok := getSessionWriter(w); ok { // Self-awareness h.ServeHTTP(w, r) return } if strings.Index(r.URL.Path, opts.CookieTemplate.Path) != 0 { // Session does not apply to this path h.ServeHTTP(w, r) return } // Create a new Session or retrieve the existing session based on the // session cookie received. var sess *Session var ckSessId string exCk, err := r.Cookie(opts.CookieTemplate.Name) if err != nil { sess = newSession(opts.CookieTemplate.MaxAge) ghost.LogFn("ghost.session : error getting session cookie : %s", err) } else { ckSessId, err = parseSignedCookie(exCk, opts.Secret) if err != nil { sess = newSession(opts.CookieTemplate.MaxAge) ghost.LogFn("ghost.session : error parsing signed cookie : %s", err) } else if ckSessId == "" { sess = newSession(opts.CookieTemplate.MaxAge) ghost.LogFn("ghost.session : no existing session ID") } else { // Get the session sess, err = opts.Store.Get(ckSessId) if err != nil { sess = newSession(opts.CookieTemplate.MaxAge) ghost.LogFn("ghost.session : error getting session from store : %s", err) } else if sess == nil { sess = newSession(opts.CookieTemplate.MaxAge) ghost.LogFn("ghost.session : nil session") } } } // Save the original hash of the session, used to compare if the contents // have changed during the handling of the request, so that it has to be // saved to the stored. oriHash := hash(sess) // Create the augmented ResponseWriter. srw := &sessResponseWriter{w, sess, opts.Store, false, func() { // This function is called when the header is about to be written, so that // the session cookie is correctly set. // Check if the connection is secure proto := strings.Trim(strings.ToLower(r.Header.Get("X-Forwarded-Proto")), " ") tls := r.TLS != nil || (strings.HasPrefix(proto, "https") && opts.TrustProxy) if opts.CookieTemplate.Secure && !tls { ghost.LogFn("ghost.session : secure cookie on a non-secure connection, cookie not sent") return } if !sess.IsNew() { // If this is not a new session, no need to send back the cookie // TODO : Handle expires? return } // Send the session cookie ck := opts.CookieTemplate ck.Value = sess.ID() err := signCookie(&ck, opts.Secret) if err != nil { ghost.LogFn("ghost.session : error signing cookie : %s", err) return } http.SetCookie(w, &ck) }} // Call wrapped handler h.ServeHTTP(srw, r) // TODO : Expiration management? srw.sess.resetMaxAge() // Do not save if content is the same, unless session is new (to avoid // creating a new session and sending a cookie on each successive request). if newHash := hash(sess); !sess.IsNew() && oriHash == newHash && newHash != 0 { // No changes to the session, no need to save ghost.LogFn("ghost.session : no changes to save to store") return } err = opts.Store.Set(sess) if err != nil { ghost.LogFn("ghost.session : error saving session to store : %s", err) } } }