Merge branch 'comments' of stephen/caddy-hugo2 into master

pull/12/head
stephen 7 years ago committed by Gogs
commit c9508770a9
  1. 6
      caddyhugo.go
  2. 4
      client.go
  3. 314
      comments/comments.go
  4. 42
      http.go
  5. 19
      setup.go
  6. 4
      testdir/caddyfile

@ -10,6 +10,7 @@ import (
"sync" "sync"
"git.stephensearles.com/stephen/acedoc" "git.stephensearles.com/stephen/acedoc"
"git.stephensearles.com/stephen/caddy-hugo2/comments"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
@ -21,12 +22,10 @@ import (
func init() { func init() {
plugin := CaddyHugo{} plugin := CaddyHugo{}
// register a "generic" plugin, like a directive or middleware
caddy.RegisterPlugin("hugo", caddy.Plugin{ caddy.RegisterPlugin("hugo", caddy.Plugin{
ServerType: "http", ServerType: "http",
Action: plugin.SetupCaddy, Action: plugin.SetupCaddy,
}) })
} }
// CaddyHugo implements the plugin // CaddyHugo implements the plugin
@ -38,7 +37,8 @@ type CaddyHugo struct {
Dir string Dir string
Media *MediaSource Media *MediaSource
Comments *comments.Service
docs map[string]*editSession docs map[string]*editSession
mtx sync.Mutex mtx sync.Mutex

@ -121,3 +121,7 @@ func (ch *CaddyHugo) editSession(docName string) (*editSession, error) {
func docNameFromEditRequest(r *http.Request) string { func docNameFromEditRequest(r *http.Request) string {
return r.URL.Path[len("/hugo/edit/"):] return r.URL.Path[len("/hugo/edit/"):]
} }
func docNameFromCommentRequest(r *http.Request) string {
return r.URL.Path[1 : len(r.URL.Path)-len("/comments")]
}

@ -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, "<a href='/login'>click here to enter your name and the comments password</a>")
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(
`<style>
comment {
margin: 10px 0;
}
comment, comment name, comment text {
display: block;
}
comment name::after {
content: " says:";
}
comment text {
margin-left: 5px;
padding-left: 5px;
border-left: 2px solid gray;
}
</style>
{{range .}}{{ if .Text }}<comment>
<name>{{.Name}}</name>
<text>{{.Text}}</text>
</comment>
{{end}}{{end}}
<form onsubmit="submitComment(event)">
<p>
<div>
<label for="comment">post a comment:</label>
</div>
<div>
<textarea placeholder="comment..." name="comment"></textarea>
</div>
<div>
<button type="submit">post</button>
</div>
</p>
</form>`))
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,
}),
}
}

@ -2,7 +2,6 @@ package caddyhugo
import ( import (
"encoding/base64" "encoding/base64"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -23,8 +22,18 @@ func (ch *CaddyHugo) ServeHTTPWithNext(next httpserver.Handler, c *caddy.Control
return 200, nil return 200, nil
} }
if !ch.Auth(r) { if ch.Comments != nil && strings.HasSuffix(r.URL.Path, "/comments") {
return http.StatusUnauthorized, errors.New("not authorized") 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") { 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 { func (ch *CaddyHugo) Auth(r *http.Request) bool {
// this is handled upstream by the caddy configuration
return true return true
} }
@ -114,6 +124,14 @@ func (ch *CaddyHugo) Match(r *http.Request) bool {
return true return true
} }
if strings.HasSuffix(r.URL.Path, "/comments") {
return true
}
if r.URL.Path == "/login" {
return true
}
if r.URL.Path == "/hugo" { if r.URL.Path == "/hugo" {
return true return true
} }
@ -229,3 +247,21 @@ func (a aferoHTTP) Open(name string) (http.File, error) {
} }
return af, err 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, "<html><body>Log in with your name and the password. Ask Dan or Stephen for the password. <a href=%q>go back</a></body></html>", r.Referer())
return 200, nil
}
http.Redirect(w, r, r.Referer(), http.StatusFound)
return 200, nil
}

@ -7,6 +7,8 @@ import (
"os" "os"
"path" "path"
"git.stephensearles.com/stephen/caddy-hugo2/comments"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
@ -26,9 +28,26 @@ func (ch *CaddyHugo) SetupCaddy(c *caddy.Controller) error {
}) })
ch.Site.AddMiddleware(ch.Middleware(c)) ch.Site.AddMiddleware(ch.Middleware(c))
ch.commentsSetting(c)
return err 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 { func (ch *CaddyHugo) Setup(dir string) error {
var err error var err error

@ -1,5 +1,7 @@
localhost:8080 { localhost:8080 {
hugo hugo {
comments test
}
root ./testsite root ./testsite
errors { * } errors { * }
pprof pprof

Loading…
Cancel
Save