From 50d0f535098b8a395d951c14da811c50dbe20acb Mon Sep 17 00:00:00 2001 From: Stephen Searles Date: Fri, 1 Sep 2017 22:39:20 -0600 Subject: [PATCH] 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