Comments: fixes #9 #10
+3
-3
@@ -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,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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,11 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ch *CaddyHugo) CommentAuth(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +128,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 +251,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="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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.Default()
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
localhost:8080 {
|
localhost:8080 {
|
||||||
hugo
|
hugo {
|
||||||
|
comments test
|
||||||
|
}
|
||||||
root ./testsite
|
root ./testsite
|
||||||
errors { * }
|
errors { * }
|
||||||
pprof
|
pprof
|
||||||
|
|||||||
Reference in New Issue
Block a user