From 50d0f535098b8a395d951c14da811c50dbe20acb Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Fri, 1 Sep 2017 22:39:20 -0600 Subject: [PATCH 1/6] comments wip --- caddyhugo.go | 6 +- client.go | 4 + comments/comments.go | 243 +++++++++++++++++++++++++++++++++++++++++++ http.go | 46 +++++++- setup.go | 19 ++++ testdir/caddyfile | 4 +- 6 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 comments/comments.go 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..10e85c9 --- /dev/null +++ b/comments/comments.go @@ -0,0 +1,243 @@ +package comments + +import ( + "bytes" + "crypto/subtle" + "encoding/json" + "fmt" + "io" + "net/http" + "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 { + w.WriteHeader(401) + return nil + } + + if r.Method == "POST" { + return cs.Comment(post, Comment{ + Name: user, + Text: r.FormValue("Text"), + IP: r.RemoteAddr, + Date: time.Now(), + }) + } + + _, err := cs.WriteTo(post, w) + if err != nil { + return err + } + + return nil +} + +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) WriteTo(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() + + var i uint64 + + prefix := post + "-" + + keys := d.KeysPrefix(prefix, nil) + for range keys { + i++ + } + + key := prefix + fmt.Sprint(i) + 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) + } + } + + 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..14849a4 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") + } + + return 200, nil + } + + if r.URL.Path == "/login" { + return ch.commentsLogin(r, w) } if strings.HasPrefix(r.URL.Path, "/hugo/publish") { @@ -106,6 +115,11 @@ 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 +} + +func (ch *CaddyHugo) CommentAuth(r *http.Request) bool { return true } @@ -114,6 +128,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 +251,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="Give your name and the password. Ask Dan or Stephen for the password.`) + return 401, nil + } + + if strings.HasSuffix(r.Referer(), "/comments") { + http.Redirect(w, r, r.Referer(), http.StatusFound) + } + + return 200, nil +} diff --git a/setup.go b/setup.go index 649279d..317f056 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.Default() + 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 From c1fc9ce6d158c2d7b85fce7b580ac4e2020f40f4 Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Sun, 3 Sep 2017 11:18:31 -0600 Subject: [PATCH 2/6] getting there --- comments/comments.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/comments/comments.go b/comments/comments.go index 10e85c9..8f3fecf 100644 --- a/comments/comments.go +++ b/comments/comments.go @@ -5,6 +5,7 @@ import ( "crypto/subtle" "encoding/json" "fmt" + "html/template" "io" "net/http" "sync" @@ -80,7 +81,8 @@ func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Req }) } - _, err := cs.WriteTo(post, w) + w.Header().Set("Content-Type", "text/html") + _, err := cs.WriteHTML(post, w) if err != nil { return err } @@ -96,7 +98,37 @@ func (cs *Service) Load(post string) ([]Comment, error) { return cs.Storage.Retreive(post) } -func (cs *Service) WriteTo(post string, w io.Writer) (int64, error) { +func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) { + buf := &bytes.Buffer{} + tmpl := template.Must(template.New("").Parse( + `
+ + + +
+ {{range $idx, $elem := .}} + $elem.Name + $elem.Text + + {{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 From 432affea76497f82f2617c5e957943a07216e100 Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Tue, 5 Sep 2017 09:20:06 -0500 Subject: [PATCH 3/6] comments appearing; need better embedding approach --- comments/comments.go | 54 ++++++++++++++++++++++++++++++++++++-------- http.go | 2 +- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/comments/comments.go b/comments/comments.go index 8f3fecf..04451e1 100644 --- a/comments/comments.go +++ b/comments/comments.go @@ -75,7 +75,7 @@ func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Req if r.Method == "POST" { return cs.Comment(post, Comment{ Name: user, - Text: r.FormValue("Text"), + Text: readCommentText(r), IP: r.RemoteAddr, Date: time.Now(), }) @@ -90,6 +90,12 @@ func (cs *Service) ServeComments(post string, w http.ResponseWriter, r *http.Req 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) } @@ -101,7 +107,28 @@ func (cs *Service) Load(post string) ([]Comment, error) { 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}} + - - -
- {{range $idx, $elem := .}} - $elem.Name - $elem.Text - - {{end}}`)) +

+

+ +
+
+ +
+
+ +
+

+ `)) + comments, err := cs.Load(post) if err != nil { return 0, err } + err = tmpl.Execute(buf, comments) if err != nil { return 0, err @@ -260,6 +293,7 @@ func (d *diskvStorage) Retreive(post string) ([]Comment, error) { if err != nil { return comments, fmt.Errorf("error unmarshaling comment: %v", err) } + comments = append(comments, comment) } return comments, nil diff --git a/http.go b/http.go index 14849a4..5bfbbe6 100644 --- a/http.go +++ b/http.go @@ -26,7 +26,7 @@ func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Control docName := docNameFromCommentRequest(r) err := ch.Comments.ServeComments(docName, w, r) if err != nil { - return 500, fmt.Errorf("couldn't load comments") + return 500, fmt.Errorf("couldn't load comments:", err) } return 200, nil From 28c081ed30b93ee84fce3d8d6451a13ba735da13 Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Tue, 5 Sep 2017 22:27:38 -0500 Subject: [PATCH 4/6] comments working; with some stuff in the theme --- comments/comments.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/comments/comments.go b/comments/comments.go index 04451e1..1ea3719 100644 --- a/comments/comments.go +++ b/comments/comments.go @@ -128,14 +128,7 @@ func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) { {{.Text}} {{end}}{{end}} -
- +

@@ -144,7 +137,7 @@ func (cs *Service) WriteHTML(post string, w io.Writer) (int64, error) {
- +

`)) From 1f3d3a21d256852bca394c7cacb1c9109ecf8606 Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Wed, 6 Sep 2017 09:01:35 -0500 Subject: [PATCH 5/6] finishing up the basic implementation of comments --- comments/comments.go | 1 + http.go | 14 +++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/comments/comments.go b/comments/comments.go index 1ea3719..3f4bc44 100644 --- a/comments/comments.go +++ b/comments/comments.go @@ -68,6 +68,7 @@ func (cs *Service) User(r *http.Request) (string, bool) { 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 } diff --git a/http.go b/http.go index 5bfbbe6..eb83273 100644 --- a/http.go +++ b/http.go @@ -119,10 +119,6 @@ func (ch *CaddyHugo) Auth(r *http.Request) bool { return true } -func (ch *CaddyHugo) CommentAuth(r *http.Request) bool { - return true -} - func (ch *CaddyHugo) Match(r *http.Request) bool { if strings.HasPrefix(r.URL.Path, "/media/") { return true @@ -259,13 +255,13 @@ func (ch *CaddyHugo) commentsLogin(r *http.Request, w http.ResponseWriter) (int, _, ok := ch.Comments.User(r) if !ok { - w.Header().Set("WWW-Authenticate", `Basic realm="Give your name and the password. Ask Dan or Stephen for the password.`) - return 401, nil + 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 } - if strings.HasSuffix(r.Referer(), "/comments") { - http.Redirect(w, r, r.Referer(), http.StatusFound) - } + http.Redirect(w, r, r.Referer(), http.StatusFound) return 200, nil } From 3f941de9c88585cc299a9201ac99f75776134860 Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Wed, 6 Sep 2017 09:11:06 -0500 Subject: [PATCH 6/6] fixing comment keying, fixing comment storage location --- comments/comments.go | 23 +++++++++++++++++------ setup.go | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/comments/comments.go b/comments/comments.go index 3f4bc44..990a711 100644 --- a/comments/comments.go +++ b/comments/comments.go @@ -8,6 +8,7 @@ import ( "html/template" "io" "net/http" + "strconv" "sync" "time" @@ -175,17 +176,27 @@ func (d *diskvStorage) nextKey(post string) (string, error) { d.keyMtx.Lock() defer d.keyMtx.Unlock() - var i uint64 - prefix := post + "-" + lastKey := prefix + keys := d.KeysPrefix(prefix, nil) - for range keys { - i++ + 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(i) - err := d.WriteStream(key, &bytes.Buffer{}, true) + key := prefix + fmt.Sprint(lastIdx+1) + err = d.WriteStream(key, &bytes.Buffer{}, true) return key, err } diff --git a/setup.go b/setup.go index 317f056..17994cc 100644 --- a/setup.go +++ b/setup.go @@ -38,7 +38,7 @@ func (ch *CaddyHugo) commentsSetting(c *caddy.Controller) { if c.Val() == "hugo" { for c.NextBlock() { if c.Val() == "comments" { - ch.Comments = comments.Default() + ch.Comments = comments.WithStorage(comments.NewDiskv(path.Join(ch.Site.Root, "comments"))) if c.NextArg() { ch.Comments.Password = c.Val() }