diff --git a/caddyhugo.go b/caddyhugo.go
index 776d7a8..b8b9342 100644
--- a/caddyhugo.go
+++ b/caddyhugo.go
@@ -10,6 +10,7 @@ import (
"sync"
"git.stephensearles.com/stephen/acedoc"
+ "git.stephensearles.com/stephen/caddy-hugo2/comments"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugolib"
@@ -21,12 +22,10 @@ import (
func init() {
plugin := CaddyHugo{}
- // register a "generic" plugin, like a directive or middleware
caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http",
Action: plugin.SetupCaddy,
})
-
}
// CaddyHugo implements the plugin
@@ -38,7 +37,8 @@ type CaddyHugo struct {
Dir string
- Media *MediaSource
+ Media *MediaSource
+ Comments *comments.Service
docs map[string]*editSession
mtx sync.Mutex
diff --git a/client.go b/client.go
index 65ee767..e0671d0 100644
--- a/client.go
+++ b/client.go
@@ -121,3 +121,7 @@ func (ch *CaddyHugo) editSession(docName string) (*editSession, error) {
func docNameFromEditRequest(r *http.Request) string {
return r.URL.Path[len("/hugo/edit/"):]
}
+
+func docNameFromCommentRequest(r *http.Request) string {
+ return r.URL.Path[1 : len(r.URL.Path)-len("/comments")]
+}
diff --git a/comments/comments.go b/comments/comments.go
new file mode 100644
index 0000000..990a711
--- /dev/null
+++ b/comments/comments.go
@@ -0,0 +1,314 @@
+package comments
+
+import (
+ "bytes"
+ "crypto/subtle"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "io"
+ "net/http"
+ "strconv"
+ "sync"
+ "time"
+
+ "github.com/peterbourgon/diskv"
+)
+
+type Comment struct {
+ Name, Text string
+ IP string
+ Date time.Time
+ Spam bool
+}
+
+func MarshalJSON(c Comment) ([]byte, error) {
+ buf := new(bytes.Buffer)
+ err := json.NewEncoder(buf).Encode(c)
+ return buf.Bytes(), err
+}
+
+func UnmarshalJSON(b []byte) (Comment, error) {
+ var c Comment
+ err := json.NewDecoder(bytes.NewBuffer(b)).Decode(&c)
+ return c, err
+}
+
+func Default() *Service {
+ return WithStorage(NewDiskv("comments"))
+}
+
+func WithStorage(storage Storage) *Service {
+ return &Service{Storage: storage}
+}
+
+type Service struct {
+ Storage Storage
+ Password string
+}
+
+type Storage interface {
+ Store(post string, c Comment) error
+ Retreive(post string) ([]Comment, error)
+ ReadAll(post string) (io.Reader, error)
+}
+
+func (cs *Service) User(r *http.Request) (string, bool) {
+ if cs.Password == "" {
+ return "", true
+ }
+
+ user, providedPass, ok := r.BasicAuth()
+ if ok && subtle.ConstantTimeCompare([]byte(cs.Password), []byte(providedPass)) == 1 {
+ return user, true
+ }
+
+ return "", false
+}
+
+func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Request) error {
+ user, allowed := cs.User(r)
+ if !allowed {
+ fmt.Fprintf(w, "click here to enter your name and the comments password")
+ w.WriteHeader(401)
+ return nil
+ }
+
+ if r.Method == "POST" {
+ return cs.Comment(post, Comment{
+ Name: user,
+ Text: readCommentText(r),
+ IP: r.RemoteAddr,
+ Date: time.Now(),
+ })
+ }
+
+ w.Header().Set("Content-Type", "text/html")
+ _, err := cs.WriteHTML(post, w)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func readCommentText(r *http.Request) string {
+ comment := struct{ Text string }{}
+ json.NewDecoder(r.Body).Decode(&comment)
+ return comment.Text
+}
+
+func (cs *Service) Comment(post string, c Comment) error {
+ return cs.Storage.Store(post, c)
+}
+
+func (cs *Service) Load(post string) ([]Comment, error) {
+ return cs.Storage.Retreive(post)
+}
+
+func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) {
+ buf := &bytes.Buffer{}
+ tmpl := template.Must(template.New("").Parse(
+ `
+ {{range .}}{{ if .Text }}
+ {{.Name}}
+ {{.Text}}
+
+ {{end}}{{end}}
+
`))
+
+ comments, err := cs.Load(post)
+ if err != nil {
+ return 0, err
+ }
+
+ err = tmpl.Execute(buf, comments)
+ if err != nil {
+ return 0, err
+ }
+ return io.Copy(w, buf)
+}
+
+func (cs *Service) WriteJSON(post string, w io.Writer) (int64, error) {
+ r, err := cs.Storage.ReadAll(post)
+ if err != nil {
+ return 0, err
+ }
+
+ return io.Copy(w, r)
+}
+
+type diskvStorage struct {
+ *diskv.Diskv
+
+ keyMtx sync.Mutex
+ keyLastKnown int64
+}
+
+func (d *diskvStorage) nextKey(post string) (string, error) {
+ d.keyMtx.Lock()
+ defer d.keyMtx.Unlock()
+
+ prefix := post + "-"
+
+ lastKey := prefix
+
+ keys := d.KeysPrefix(prefix, nil)
+ for key := range keys {
+ lastKey = key
+ }
+
+ if lastKey == prefix {
+ return prefix + "1", nil
+ }
+
+ minusPrefix := lastKey[len(prefix):]
+ lastIdx, err := strconv.Atoi(minusPrefix)
+ if err != nil {
+ return "", fmt.Errorf("comment key %q seems malformed", lastKey)
+ }
+
+ key := prefix + fmt.Sprint(lastIdx+1)
+ err = d.WriteStream(key, &bytes.Buffer{}, true)
+
+ return key, err
+}
+
+func (d *diskvStorage) Store(post string, c Comment) error {
+ b, err := MarshalJSON(c)
+ if err != nil {
+ return fmt.Errorf("error marshaling comment for storage: %v", err)
+ }
+
+ key, err := d.nextKey(post)
+ if err != nil {
+ return fmt.Errorf("error recording comment: %v", err)
+ }
+
+ err = d.Write(key, b)
+ if err != nil {
+ return fmt.Errorf("error writing comment to storage: %v", err)
+ }
+
+ return nil
+}
+
+type multiReadCloser struct {
+ rcs []io.ReadCloser
+}
+
+func (mrc *multiReadCloser) Read(b []byte) (int, error) {
+ if len(mrc.rcs) == 0 {
+ return 0, io.EOF
+ }
+
+ n, err := mrc.rcs[0].Read(b)
+ if n == len(b) { // we read enough
+ return n, err
+ }
+
+ for (err == nil || err == io.EOF) && len(mrc.rcs) > 1 {
+ var n2 int
+ closeErr := mrc.rcs[0].Close()
+ if closeErr != nil {
+ return n, closeErr
+ }
+
+ mrc.rcs = mrc.rcs[1:]
+ n2, err = mrc.rcs[0].Read(b[n:])
+ n += n2
+ }
+
+ return n, err
+}
+
+func (mrc *multiReadCloser) Close() error {
+ var err error
+
+ for _, rc := range mrc.rcs {
+ err2 := rc.Close()
+ if err2 != nil {
+ err = err2
+ }
+ }
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *diskvStorage) ReadAll(post string) (io.Reader, error) {
+ prefix := post + "-"
+
+ var rcs []io.ReadCloser
+
+ keys := d.KeysPrefix(prefix, nil)
+ for key := range keys {
+ rc, err := d.ReadStream(key, false)
+ if err != nil {
+ return nil, err
+ }
+ rcs = append(rcs, rc)
+ }
+
+ return &multiReadCloser{rcs}, nil
+}
+
+func (d *diskvStorage) Retreive(post string) ([]Comment, error) {
+ var comments []Comment
+
+ r, err := d.ReadAll(post)
+ if err != nil {
+ return comments, fmt.Errorf("error reading comment from storage: %v", err)
+ }
+
+ dec := json.NewDecoder(r)
+ for dec.More() {
+ comment := Comment{}
+ err := dec.Decode(&comment)
+ if err != nil {
+ return comments, fmt.Errorf("error unmarshaling comment: %v", err)
+ }
+ comments = append(comments, comment)
+ }
+
+ return comments, nil
+}
+
+func NewDiskv(path string) Storage {
+ return &diskvStorage{
+ Diskv: diskv.New(diskv.Options{
+ BasePath: path,
+ CacheSizeMax: 1024 * 1024,
+ }),
+ }
+}
diff --git a/http.go b/http.go
index ca46bba..eb83273 100644
--- a/http.go
+++ b/http.go
@@ -2,7 +2,6 @@ package caddyhugo
import (
"encoding/base64"
- "errors"
"fmt"
"net/http"
"os"
@@ -23,8 +22,18 @@ func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Control
return 200, nil
}
- if !ch.Auth(r) {
- return http.StatusUnauthorized, errors.New("not authorized")
+ if ch.Comments != nil && strings.HasSuffix(r.URL.Path, "/comments") {
+ docName := docNameFromCommentRequest(r)
+ err := ch.Comments.ServeComments(docName, w, r)
+ if err != nil {
+ return 500, fmt.Errorf("couldn't load comments:", err)
+ }
+
+ return 200, nil
+ }
+
+ if r.URL.Path == "/login" {
+ return ch.commentsLogin(r, w)
}
if strings.HasPrefix(r.URL.Path, "/hugo/publish") {
@@ -106,6 +115,7 @@ func (ch *CaddyHugo) Middleware(c *caddy.Controller) httpserver.Middleware {
}
func (ch *CaddyHugo) Auth(r *http.Request) bool {
+ // this is handled upstream by the caddy configuration
return true
}
@@ -114,6 +124,14 @@ func (ch *CaddyHugo) Match(r *http.Request) bool {
return true
}
+ if strings.HasSuffix(r.URL.Path, "/comments") {
+ return true
+ }
+
+ if r.URL.Path == "/login" {
+ return true
+ }
+
if r.URL.Path == "/hugo" {
return true
}
@@ -229,3 +247,21 @@ func (a aferoHTTP) Open(name string) (http.File, error) {
}
return af, err
}
+
+func (ch *CaddyHugo) commentsLogin(r *http.Request, w http.ResponseWriter) (int, error) {
+ if ch.Comments == nil {
+ return 200, nil
+ }
+
+ _, ok := ch.Comments.User(r)
+ if !ok {
+ w.Header().Set("WWW-Authenticate", `Basic realm="Log in with your name and the password. Ask Dan or Stephen for the password."`)
+ w.WriteHeader(401)
+ fmt.Fprintf(w, "Log in with your name and the password. Ask Dan or Stephen for the password. go back", r.Referer())
+ return 200, nil
+ }
+
+ http.Redirect(w, r, r.Referer(), http.StatusFound)
+
+ return 200, nil
+}
diff --git a/setup.go b/setup.go
index 649279d..17994cc 100644
--- a/setup.go
+++ b/setup.go
@@ -7,6 +7,8 @@ import (
"os"
"path"
+ "git.stephensearles.com/stephen/caddy-hugo2/comments"
+
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
@@ -26,9 +28,26 @@ func (ch *CaddyHugo) SetupCaddy(c *caddy.Controller) error {
})
ch.Site.AddMiddleware(ch.Middleware(c))
+ ch.commentsSetting(c)
+
return err
}
+func (ch *CaddyHugo) commentsSetting(c *caddy.Controller) {
+ for c.NextLine() {
+ if c.Val() == "hugo" {
+ for c.NextBlock() {
+ if c.Val() == "comments" {
+ ch.Comments = comments.WithStorage(comments.NewDiskv(path.Join(ch.Site.Root, "comments")))
+ if c.NextArg() {
+ ch.Comments.Password = c.Val()
+ }
+ }
+ }
+ }
+ }
+}
+
func (ch *CaddyHugo) Setup(dir string) error {
var err error
diff --git a/testdir/caddyfile b/testdir/caddyfile
index cb91987..0bfaa5b 100644
--- a/testdir/caddyfile
+++ b/testdir/caddyfile
@@ -1,5 +1,7 @@
localhost:8080 {
- hugo
+ hugo {
+ comments test
+ }
root ./testsite
errors { * }
pprof