split into packages
This commit is contained in:
parent
2b56832843
commit
c5d41b0965
26 changed files with 328 additions and 449 deletions
200
pkg/cmd/cmd.go
Normal file
200
pkg/cmd/cmd.go
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/godrop"
|
||||
"github.com/urfave/cli"
|
||||
logger "log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
blevePath = "bleve"
|
||||
|
||||
defDataDir = "/var/lib/gowiki"
|
||||
defListen = "127.0.0.1:8090"
|
||||
defLogFile = "gowiki.log"
|
||||
|
||||
defRunUser = "wiki"
|
||||
defRunGroup = "wiki"
|
||||
)
|
||||
|
||||
func server(conf serverConfig) (err error) {
|
||||
dropCfg := godrop.Config{
|
||||
User: conf.User,
|
||||
Group: conf.Group,
|
||||
Foreground: conf.Foreground,
|
||||
}
|
||||
err = godrop.Drop(dropCfg, func() (net.Listener, error) {
|
||||
return net.Listen("tcp", conf.ListenAddr)
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = os.Chdir(conf.DataDir); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.NewLogger(conf.LogFile)
|
||||
|
||||
l, err := godrop.GetListener()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
srv := NewHTTPServer(l, conf.Version, conf.SecureCookie)
|
||||
err = srv.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("gowiki started")
|
||||
sigs := make(chan os.Signal)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
log.Println("caught " + sig.String())
|
||||
go func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
//TODO error needed?
|
||||
log.Fatal(errors.New("stop timed out: killing"))
|
||||
}()
|
||||
err = srv.Stop()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println("gowiki stopped")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
conf serverConfig
|
||||
)
|
||||
|
||||
func Run(version string) {
|
||||
app := cli.NewApp()
|
||||
app.Name = "gowiki"
|
||||
app.Usage = "a simple wiki engine"
|
||||
app.Version = version
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "start gowiki server",
|
||||
Flags: defaultFlags(serverFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
conf.Version = app.Version
|
||||
if err := server(conf); err != nil {
|
||||
logger.Fatal("server: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dump",
|
||||
Usage: "create a json dump of all articles in the data directory (dump is written to stdout) gowiki needs to be stopped",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := os.Chdir(conf.DataDir); err != nil {
|
||||
logger.Fatal("dump: ", err)
|
||||
}
|
||||
if err := dump(blevePath, verbose); err != nil {
|
||||
logger.Fatal("dump: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Usage: "restore a json dump to the data directory (dump is read from stdin) gowiki needs to be stopped",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := os.Chdir(conf.DataDir); err != nil {
|
||||
logger.Fatal("restore: ", err)
|
||||
}
|
||||
if err := restore(blevePath, verbose); err != nil {
|
||||
logger.Fatal("restore: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
type serverConfig struct {
|
||||
DataDir string
|
||||
User string
|
||||
Group string
|
||||
ListenAddr string
|
||||
LogFile string
|
||||
Version string
|
||||
Foreground bool
|
||||
SecureCookie bool
|
||||
}
|
||||
|
||||
func serverFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Value: defRunUser,
|
||||
Usage: "drop privileges to user",
|
||||
Destination: &conf.User,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "group, g",
|
||||
Value: defRunGroup,
|
||||
Usage: "drop privileges to group",
|
||||
Destination: &conf.Group,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "listen, l",
|
||||
Value: defListen,
|
||||
Usage: "listening <address>:<port>",
|
||||
Destination: &conf.ListenAddr,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defLogFile,
|
||||
Usage: "log file (leave empty for stdout, this only works in combination with -f)",
|
||||
Destination: &conf.LogFile,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "foreground, f",
|
||||
Usage: "do not fork into the background",
|
||||
Destination: &conf.Foreground,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "secure, s",
|
||||
Usage: "enable secure cookie",
|
||||
Destination: &conf.SecureCookie,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dumpRestoreFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "verbose, v",
|
||||
Usage: "enable verbose mode",
|
||||
Destination: &verbose,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFlags(f []cli.Flag) []cli.Flag {
|
||||
return append([]cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "data, d",
|
||||
Value: defDataDir,
|
||||
Usage: "data directory",
|
||||
Destination: &conf.DataDir,
|
||||
},
|
||||
}, f...)
|
||||
}
|
||||
71
pkg/db/db.go
Normal file
71
pkg/db/db.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
dbFile = "gowiki.db"
|
||||
defUser = "admin"
|
||||
defPassword = "gowiki"
|
||||
userTable = "user"
|
||||
pageTable = "page"
|
||||
bcryptCost = 13
|
||||
indexName = "Index"
|
||||
welcome = `# Welcome to GoWiki
|
||||
This is the [Index](/wiki/Index) page.
|
||||
You can customize it how you like.`
|
||||
)
|
||||
const (
|
||||
maxResult = 1e6
|
||||
maxSearchResult = 101
|
||||
)
|
||||
|
||||
var (
|
||||
errUserNotExists = dbErr("user does not exist")
|
||||
errUserExists = dbErr("user already exist")
|
||||
)
|
||||
|
||||
func dbErr(i interface{}) error {
|
||||
return fmt.Errorf("db: %s", i)
|
||||
}
|
||||
|
||||
type BoltStore struct {
|
||||
Marshaler
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func NewBoltStore() (bs *BoltStore, err error) {
|
||||
db, err := bolt.Open(dbFile, 0666, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = db.Update(func(tx *bolt.Tx) error {
|
||||
if _, err := tx.CreateBucketIfNotExists([]byte(userTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.CreateBucketIfNotExists([]byte(pagesTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
bs = &BoltStore{NewGOB(), db}
|
||||
if !bs.UserExists(defUser) {
|
||||
err = bs.CreateUser(defUser, defPassword, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
bs.UnlockUser(defUser)
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Close() error {
|
||||
return bs.db.Close()
|
||||
}
|
||||
103
pkg/db/dump.go
Normal file
103
pkg/db/dump.go
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Dump struct {
|
||||
Articles []DumpArticle `json:"articles"`
|
||||
Snippets []Snippet `json:"snippets"`
|
||||
Users []User `json:"users"`
|
||||
}
|
||||
|
||||
type DumpArticle struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string `json:"link_title"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
func verboseLog(prefix, title string, v bool) {
|
||||
if v {
|
||||
fmt.Fprintln(os.Stderr, prefix, title)
|
||||
}
|
||||
}
|
||||
|
||||
func dump(path string, v bool) (err error) {
|
||||
i, err := NewDumpIndex(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer i.Close()
|
||||
bs, err := NewBoltStore()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
art, err := i.Get(false, indexName)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dump Dump
|
||||
titles, err := i.Dump()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, title := range titles {
|
||||
art, err = i.Get(false, title)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
verboseLog("dump:", art.Title, v)
|
||||
dump.Articles = append(dump.Articles, DumpArticle{
|
||||
Title: art.Title,
|
||||
LinkTitle: art.LinkTitle,
|
||||
MD: art.MD,
|
||||
Created: art.Created,
|
||||
Updated: art.Updated,
|
||||
Public: art.Public,
|
||||
})
|
||||
}
|
||||
//TODO snippets
|
||||
//verboseLog("dump:", "snippets", v)
|
||||
verboseLog("dump:", "users", v)
|
||||
dump.Users, err = bs.DumpUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(dump)
|
||||
}
|
||||
|
||||
func restore(path string, v bool) (err error) {
|
||||
i, err := NewIndex(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer i.Close()
|
||||
bs, err := NewBoltStore()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var dump Dump
|
||||
err = json.NewDecoder(os.Stdin).Decode(&dump)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, art := range dump.Articles {
|
||||
verboseLog("restore:", art.Title, v)
|
||||
err = i.Restore(&art)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
//TODO snippets
|
||||
//verboseLog("restore:", "snippets", v)
|
||||
verboseLog("restore:", "users", v)
|
||||
err = bs.RestoreUsers(dump.Users)
|
||||
return
|
||||
}
|
||||
14
pkg/db/helper.go
Normal file
14
pkg/db/helper.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package db
|
||||
|
||||
import "golang.org/x/crypto/sha3"
|
||||
|
||||
func validatePassword(pw string) (b []byte) {
|
||||
b = []byte(pw)
|
||||
if len(pw) <= 56 {
|
||||
return
|
||||
}
|
||||
hash := sha3.New384()
|
||||
hash.Write(b)
|
||||
b = hash.Sum(nil)
|
||||
return
|
||||
}
|
||||
25
pkg/db/marshal.go
Normal file
25
pkg/db/marshal.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package db
|
||||
|
||||
import "encoding/gob"
|
||||
|
||||
type Marshaler interface {
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
Unmarshal(data []byte, v interface{}) error
|
||||
}
|
||||
|
||||
type gobMarshaler struct{}
|
||||
|
||||
func NewGOB() Marshaler {
|
||||
return gobMarshaler{}
|
||||
}
|
||||
|
||||
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(buf).Encode(v)
|
||||
b = buf.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||
}
|
||||
42
pkg/db/page.go
Normal file
42
pkg/db/page.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package db
|
||||
|
||||
type Article struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string `json:"-"`
|
||||
Search string `json:"search"`
|
||||
Index template.HTML `json:"index"`
|
||||
Text template.HTML `json:"text"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Public Permission = iota
|
||||
Internal
|
||||
Private
|
||||
)
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
Markdown string `json:"markdown"`
|
||||
Index template.HTML `json:"index"`
|
||||
Text template.HTML `json:"text"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Perm Permission `json:"perm"`
|
||||
}
|
||||
|
||||
func (bs *BoltStore) GetPage(title string) (p Page, err error) {
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket([]byte(pageTable)).Get([]byte(username))
|
||||
if v == nil {
|
||||
return errUserNotExists
|
||||
}
|
||||
return bs.Unmarshal(v, &u)
|
||||
})
|
||||
return
|
||||
}
|
||||
148
pkg/db/snippet.go
Normal file
148
pkg/db/snippet.go
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/boltdb/bolt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Snippet struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string `json:"-"`
|
||||
Index template.HTML `json:"index"`
|
||||
Text template.HTML `json:"text"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Permission int `json:"permission"`
|
||||
}
|
||||
|
||||
func copyBuf(buf []byte) (b []byte) {
|
||||
b = make([]byte, len(buf))
|
||||
copy(b, buf)
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
errSnippetNotExists = dbErr("snippet does not exist")
|
||||
errSnippetExists = dbErr("snippet already exist")
|
||||
errInvalidPermission = dbErr("invalid permission")
|
||||
)
|
||||
|
||||
func (bs *BoltStore) GetSnippets(username string) (snip []Snippet, err error) {
|
||||
user := []byte(username)
|
||||
var buf [][]byte
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(snippetTable)).ForEach(func(k, v []byte) error {
|
||||
if !bytes.HasPrefix(k, user) {
|
||||
return nil
|
||||
}
|
||||
buf = append(buf, copyBuf(v))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, b := range buf {
|
||||
s := new(Snippet)
|
||||
if err = bs.Unmarshal(b, s); err != nil {
|
||||
return
|
||||
}
|
||||
snip = append(snip, *s)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) GetSnippet(username, title string, perm int) (*Snippet, error) {
|
||||
s := new(Snippet)
|
||||
err := bs.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket([]byte(snippetTable)).Get([]byte(title))
|
||||
if v == nil {
|
||||
return errSnippetNotExists
|
||||
}
|
||||
return bs.Unmarshal(v, s)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch s.Permission {
|
||||
case 0:
|
||||
return s, nil
|
||||
case 1:
|
||||
if username != "" {
|
||||
return s, nil
|
||||
}
|
||||
case 2:
|
||||
if strings.HasPrefix(title, username) {
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return nil, errSnippetNotExists
|
||||
}
|
||||
|
||||
func (bs *BoltStore) snippetExists(username, title string, perm int) (err error) {
|
||||
_, err = bs.GetSnippet(username, title, perm)
|
||||
return
|
||||
}
|
||||
|
||||
func makeKey(username, title string) string {
|
||||
return username + title
|
||||
}
|
||||
|
||||
func checkPerm(p int) error {
|
||||
switch p {
|
||||
case 0, 1, 2:
|
||||
return nil
|
||||
}
|
||||
return errInvalidPermission
|
||||
}
|
||||
|
||||
func (bs *BoltStore) CreateSnippet(username, title, text string, perm int) (newTitle string, err error) {
|
||||
newTitle, title = makeLinkTitle(title)
|
||||
err = checkPerm(perm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = bs.snippetExists(username, title, perm)
|
||||
if err == nil {
|
||||
err = errSnippetExists
|
||||
return
|
||||
}
|
||||
err = bs.db.Update(func(tx *bolt.Tx) error {
|
||||
snip := render(text).Snippet()
|
||||
snip.Title = title
|
||||
snip.Created = username + " " + now()
|
||||
snip.Permission = perm
|
||||
v, err := bs.Marshal(snip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(snippetTable)).Put([]byte(makeKey(username, title)), v)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) EditSnippet(username, title, text string, perm int) (err error) {
|
||||
err = checkPerm(perm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = bs.snippetExists(username, title, perm)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = bs.db.Update(func(tx *bolt.Tx) error {
|
||||
snip := render(text).Snippet()
|
||||
snip.Title = title
|
||||
snip.Created = username + " " + now()
|
||||
snip.Permission = perm
|
||||
v, err := bs.Marshal(snip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(snippetTable)).Put([]byte(makeKey(username, title)), v)
|
||||
})
|
||||
return
|
||||
}
|
||||
224
pkg/db/user.go
Normal file
224
pkg/db/user.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func (bs *BoltStore) GetUsers() (users []User, err error) {
|
||||
users, err = bs.DumpUsers()
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) GetUserWithoutPassword(username string) (u User, err error) {
|
||||
u, err = bs.GetUser(username)
|
||||
u.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) GetUser(username string) (u User, err error) {
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
v := tx.Bucket([]byte(userTable)).Get([]byte(username))
|
||||
if v == nil {
|
||||
return errUserNotExists
|
||||
}
|
||||
return bs.Unmarshal(v, &u)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) UserIsAdmin(username string) (admin bool) {
|
||||
bs.db.View(func(tx *bolt.Tx) error {
|
||||
u, _ := bs.GetUser(username)
|
||||
admin = u.Admin
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) UserExists(username string) error {
|
||||
return bs.db.View(func(tx *bolt.Tx) error {
|
||||
if tx.Bucket([]byte(userTable)).Get([]byte(username)) == nil {
|
||||
return errUserNotExists
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) DumpUsers() (users []User, err error) {
|
||||
var buf [][]byte
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(userTable)).ForEach(func(k, v []byte) error {
|
||||
buf = append(buf, copyBuf(v))
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, b := range buf {
|
||||
u := new(User)
|
||||
if err = bs.Unmarshal(b, u); err != nil {
|
||||
return
|
||||
}
|
||||
users = append(users, *u)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) RestoreUsers(users []User) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(userTable))
|
||||
err := b.ForEach(func(k, v []byte) error {
|
||||
return b.Delete(k)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, u := range users {
|
||||
v, err := bs.Marshal(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.Put([]byte(u.Username), v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) CreateUser(username, password string, admin bool) error {
|
||||
if err := bs.UserExists(username); err == nil {
|
||||
return errUserExists
|
||||
}
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
v, err := bs.Marshal(&User{
|
||||
Username: username,
|
||||
Password: string(hash),
|
||||
Admin: admin,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(userTable)).Put([]byte(username), v)
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Login(username, password string) error {
|
||||
var e error
|
||||
err := bs.db.Update(func(tx *bolt.Tx) error {
|
||||
us, err := bs.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if us.Locked == 3 {
|
||||
return errors.New("db: user locked")
|
||||
}
|
||||
b := tx.Bucket([]byte(userTable))
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(us.Password), validatePassword(password)); err != nil {
|
||||
us.Locked++
|
||||
v, err := bs.Marshal(us)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = b.Put([]byte(username), v); err != nil {
|
||||
return err
|
||||
}
|
||||
e = errors.New("db: wrong login")
|
||||
return nil
|
||||
}
|
||||
us.Locked = 0
|
||||
v, err := bs.Marshal(us)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Put([]byte(username), v)
|
||||
})
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (bs *BoltStore) UnlockUser(username string) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
u, err := bs.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Locked < 3 {
|
||||
return nil
|
||||
}
|
||||
u.Locked = 0
|
||||
v, err := bs.Marshal(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(userTable)).Put([]byte(username), v)
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) UpdateUserPassword(username, password string) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
if password != "" {
|
||||
us, err := bs.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
us.Password = string(hash)
|
||||
v, err := bs.Marshal(us)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(userTable)).Put([]byte(username), v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) AdminUpdateUser(username, password string, admin bool) error {
|
||||
if username == defUser && !admin {
|
||||
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
||||
}
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
us, err := bs.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
us.Admin = admin
|
||||
if password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
us.Password = string(hash)
|
||||
}
|
||||
v, err := bs.Marshal(us)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Bucket([]byte(userTable)).Put([]byte(username), v)
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) DeleteUser(username string) error {
|
||||
if username == defUser {
|
||||
return errors.New("user '" + defUser + "' cannot be deleted")
|
||||
}
|
||||
if err := bs.UserExists(username); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(userTable)).Delete([]byte(username))
|
||||
})
|
||||
}
|
||||
112
pkg/index/index.go
Normal file
112
pkg/index/index.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/document"
|
||||
)
|
||||
|
||||
type IndexPage struct {
|
||||
Title string `json:"title"`
|
||||
Search string `json:"search"`
|
||||
}
|
||||
|
||||
func indexErr(i interface{}) error {
|
||||
return fmt.Errorf("index: %s", i)
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
i bleve.Index
|
||||
}
|
||||
|
||||
func NewIndex(path string) (i *Index, err error) {
|
||||
var bi bleve.Index
|
||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
bi, err = bleve.New(path, mapping)
|
||||
if err != nil {
|
||||
err = indexErr(err)
|
||||
return
|
||||
}
|
||||
i = &Index{index: bi}
|
||||
_, err = i.Index(indexName, welcome, defUser, true)
|
||||
if err != nil {
|
||||
err = indexErr(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
bi, err = bleve.Open(path)
|
||||
if err != nil {
|
||||
err = indexErr(err)
|
||||
return
|
||||
}
|
||||
i = &Index{index: bi}
|
||||
}
|
||||
i.cache = NewIndexCache()
|
||||
return
|
||||
}
|
||||
|
||||
func NewDumpIndex(path string) (i *Index, err error) {
|
||||
bi, err := bleve.OpenUsing(path, map[string]interface{}{"read_only": true})
|
||||
if err != nil {
|
||||
err = indexErr(err)
|
||||
return
|
||||
}
|
||||
i = &Index{index: bi}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Index) Close() error {
|
||||
return i.i.Close()
|
||||
}
|
||||
|
||||
func (i *Index) Add(ip IndexPage) error {
|
||||
doc, _ := i.i.Document(ip.Title)
|
||||
if doc != nil {
|
||||
err := i.Delete(ip.Title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return i.i.Index(ip.Title, ip)
|
||||
}
|
||||
|
||||
func (i *Index) Delete(title string) error {
|
||||
return i.i.Delete(title)
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Title string
|
||||
Text string
|
||||
}
|
||||
|
||||
func (i *Index) Search(search string) (results []Result, err error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
|
||||
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||
req.Size = maxSearchResult
|
||||
res, err := i.i.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, hit := range res.Hits {
|
||||
if hit == nil {
|
||||
continue
|
||||
}
|
||||
title := hit.ID
|
||||
text := ""
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == "title" || fragField == "search" {
|
||||
for _, f := range frags {
|
||||
text += f
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
results = append(results, Result{title, text})
|
||||
}
|
||||
return
|
||||
}
|
||||
154
pkg/index/render.go
Normal file
154
pkg/index/render.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve/analysis"
|
||||
htmlFilter "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
var filter = makeFilter()
|
||||
|
||||
func makeFilter() analysis.CharFilter {
|
||||
f, _ := htmlFilter.CharFilterConstructor(nil, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
reHeader = regexp.MustCompile("(<h[1-6])>(.+)(</h[1-6]>)")
|
||||
reLink = regexp.MustCompile(`id="(.+)">`)
|
||||
whiteSpace = regexp.MustCompile(`\s+`)
|
||||
reIndex = regexp.MustCompile("<h([1-6])>(.+)</h[1-6]>")
|
||||
titleRe = regexp.MustCompile("[^a-zA-Z0-9 -]+")
|
||||
)
|
||||
|
||||
type rendered struct {
|
||||
Index template.HTML
|
||||
Text template.HTML
|
||||
MD string
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
func (r *rendered) Article() *Article {
|
||||
return &Article{
|
||||
Index: r.Index,
|
||||
Text: r.Text,
|
||||
MD: r.MD,
|
||||
Search: whiteSpace.ReplaceAllString(string(filter.Filter(r.Raw)), " "),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rendered) Snippet() *Snippet {
|
||||
return &Snippet{
|
||||
Index: r.Index,
|
||||
Text: r.Text,
|
||||
MD: r.MD,
|
||||
}
|
||||
}
|
||||
|
||||
func render(text string) *rendered {
|
||||
rend := blackfriday.MarkdownCommon([]byte(text))
|
||||
index, html := buildIndex(string(rend))
|
||||
html = strings.Replace(html, "<table>", `<table class="table">`, -1)
|
||||
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
|
||||
html = strings.Replace(html, "<a", `<a target="_blank"`, -1)
|
||||
return &rendered{
|
||||
Index: template.HTML(index),
|
||||
Text: template.HTML(html),
|
||||
MD: text,
|
||||
Raw: rend,
|
||||
}
|
||||
}
|
||||
|
||||
func spaceReplace(s string) string {
|
||||
return whiteSpace.ReplaceAllString(s, "+")
|
||||
//TODO remove
|
||||
//return strings.Replace(s, " ", "+", -1)
|
||||
}
|
||||
|
||||
func makeLinkTitle(t string) (newTitle string, title string) {
|
||||
title = titleRe.ReplaceAllString(t, "")
|
||||
newTitle = spaceReplace(title)
|
||||
return
|
||||
}
|
||||
|
||||
func buildIndex(html string) (string, string) {
|
||||
var ret string
|
||||
m := make(map[string]int)
|
||||
hn := reIndex.FindAllStringSubmatch(html, -1)
|
||||
html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
|
||||
html = reLink.ReplaceAllStringFunc(html, spaceReplace)
|
||||
for k, v := range hn {
|
||||
link, _ := makeLinkTitle(v[2])
|
||||
i := m[link]
|
||||
m[link] = i + 1
|
||||
newLink := link + strconv.Itoa(i)
|
||||
html = strings.Replace(html, link+`"`, newLink+`"`, 1)
|
||||
ret += before(hn, k)
|
||||
ret += `<li><a href="#` + newLink + `">` + v[2] + "</a>"
|
||||
ret += after(hn, k)
|
||||
}
|
||||
return ret, html
|
||||
}
|
||||
|
||||
func before(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if c == 1 {
|
||||
return `<ul type="circle">`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func after(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if len(arr) <= i+1 {
|
||||
return closeTag(c-1) + "</ul>"
|
||||
}
|
||||
n, _ := strconv.Atoi(arr[i+1][1])
|
||||
if n == 1 {
|
||||
return closeTag(c-n) + "</ul>"
|
||||
} else if n < c {
|
||||
return closeTag(c - n)
|
||||
} else if n > c {
|
||||
return openTag(n - c)
|
||||
} else if n == c {
|
||||
return "</li>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += `<ul type="circle"><li>`
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "circle", "none", j-1)
|
||||
}
|
||||
ret = ret[0 : len(ret)-4]
|
||||
return
|
||||
}
|
||||
|
||||
func closeTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += "</li></ul></li>"
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "</li></li>", "</li>", -1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func renderResult(res []Result) (html string) {
|
||||
//TODO fmt.Sprintf ?
|
||||
for r := range res {
|
||||
html += `<a href="/` + r.LinkTitle + `"><b>` + r.Title + "</b></a>" +
|
||||
`<pre style="white-space: pre-wrap">` + r.Text + "</pre><br>"
|
||||
|
||||
}
|
||||
}
|
||||
35
pkg/log/log.go
Normal file
35
pkg/log/log.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
logger "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var log *logger.Logger
|
||||
|
||||
func newLogger(file string) {
|
||||
if file == "" {
|
||||
log = logger.New(os.Stdout, "", logger.LstdFlags)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
logger.SetOutput(f)
|
||||
log = logger.New(f, "", logger.LstdFlags)
|
||||
}
|
||||
|
||||
func Println(v ...interface{}) {
|
||||
log.Println(v...)
|
||||
}
|
||||
|
||||
func Printf(fmt string, v ...interface{}) {
|
||||
log.Printf(fmt, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
log.Fatal(v...)
|
||||
}
|
||||
49
pkg/server/cache.go
Normal file
49
pkg/server/cache.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CachedArticle struct {
|
||||
Article *Article
|
||||
CacheTime time.Time
|
||||
}
|
||||
|
||||
type IndexCache struct {
|
||||
sync.RWMutex
|
||||
Cache map[string]*CachedArticle
|
||||
}
|
||||
|
||||
func NewIndexCache() *IndexCache {
|
||||
return &IndexCache{Cache: make(map[string]*CachedArticle)}
|
||||
}
|
||||
|
||||
func (ic IndexCache) Add(title string, a *Article) {
|
||||
now := time.Now()
|
||||
ic.Lock()
|
||||
defer ic.Unlock()
|
||||
c, ok := ic.Cache[title]
|
||||
if ok && now.After(c.CacheTime) {
|
||||
c.Article = a
|
||||
c.CacheTime = now
|
||||
ic.Cache[title] = c
|
||||
return
|
||||
}
|
||||
ic.Cache[title] = &CachedArticle{a, now}
|
||||
}
|
||||
|
||||
func (ic IndexCache) Get(title string) *Article {
|
||||
ic.RLock()
|
||||
defer ic.RUnlock()
|
||||
if a, ok := ic.Cache[title]; ok {
|
||||
return a.Article
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ic *IndexCache) Delete(title string) {
|
||||
ic.Lock()
|
||||
delete(ic.Cache, title)
|
||||
ic.Unlock()
|
||||
}
|
||||
250
pkg/server/context.go
Normal file
250
pkg/server/context.go
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
Srv *HTTPServer
|
||||
T *template.Template
|
||||
Status int
|
||||
Err error
|
||||
Time time.Time
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
type webData struct {
|
||||
Version string
|
||||
Time int64
|
||||
|
||||
Title string
|
||||
BodyTitle string
|
||||
|
||||
Admin bool
|
||||
Login bool
|
||||
|
||||
User string
|
||||
Token string
|
||||
Msg string
|
||||
Search string
|
||||
|
||||
Editor *Editor
|
||||
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
defer c.log()
|
||||
if c.T == nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
c.Err = errors.New("template is nil")
|
||||
return
|
||||
}
|
||||
c.newToken()
|
||||
c.Data.Version = c.Srv.Version
|
||||
if c.Data.BodyTitle == "" {
|
||||
c.Data.BodyTitle = c.Data.Title
|
||||
}
|
||||
c.Data.Admin = c.GetAdmin()
|
||||
c.Data.User = c.GetUser()
|
||||
c.Data.Login = c.LoggedOn()
|
||||
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
|
||||
c.Err = c.T.Execute(c.Response, c.Data)
|
||||
if c.Err != nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
}
|
||||
if c.Status == 0 {
|
||||
c.Status = http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) log() {
|
||||
if c.Err != nil {
|
||||
log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
|
||||
}
|
||||
|
||||
func (c *Context) NotFound() {
|
||||
c.Template("notFoundHandler")
|
||||
c.Data = webData{Title: "404"}
|
||||
c.Status = http.StatusNotFound
|
||||
c.Response.WriteHeader(http.StatusNotFound)
|
||||
c.Exec()
|
||||
}
|
||||
func (c *Context) Template(name string) {
|
||||
c.T = c.Srv.templ[name]
|
||||
}
|
||||
|
||||
func (c *Context) Write(buf []byte) (err error) {
|
||||
_, err = c.Response.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, code int) {
|
||||
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
|
||||
http.Redirect(c.Response, c.Request, uri, code)
|
||||
}
|
||||
|
||||
func (c *Context) Method() string {
|
||||
return c.Request.Method
|
||||
}
|
||||
|
||||
func (c *Context) Path() string {
|
||||
return c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (c *Context) FormSlice(name string) []string {
|
||||
// does nothing if called twice
|
||||
c.Request.ParseForm()
|
||||
return c.Request.PostForm[name]
|
||||
}
|
||||
|
||||
func (c *Context) Form(name string) (ret string) {
|
||||
ret = c.Request.PostFormValue(name)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) Var(name string) (ret string) {
|
||||
ret, _ = mux.Vars(c.Request)[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) initToken() {
|
||||
token := c.getToken()
|
||||
if token == nil || len(token) != keyLen {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values["token"] = genKey(keyLen)
|
||||
s.Options = c.Srv.Store.Options
|
||||
err := s.Save(c.Request, c.Response)
|
||||
if err != nil {
|
||||
log.Println("InitToken: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) newToken() {
|
||||
c.initToken()
|
||||
rnd := genKey(keyLen)
|
||||
c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...))
|
||||
}
|
||||
|
||||
// CheckToken validates the xsrf token
|
||||
func (c *Context) CheckToken(f func()) bool {
|
||||
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
|
||||
if err != nil {
|
||||
log.Println("CheckToken: ", err)
|
||||
return false
|
||||
}
|
||||
if len(token) != authLen {
|
||||
return false
|
||||
}
|
||||
ok := subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
|
||||
if !ok {
|
||||
if f != nil {
|
||||
f()
|
||||
return ok
|
||||
}
|
||||
c.Data.Msg = "wrong csrf token"
|
||||
c.Exec()
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
_, ok = s.Values["user"]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetUdata(user string, admin bool) {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values["user"] = user
|
||||
s.Values["admin"] = admin
|
||||
s.Options = c.Srv.Store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) UnsetUdata() {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
s.Values = make(map[interface{}]interface{})
|
||||
s.Options = c.Srv.Store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) GetUser() string {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["user"].(string); ok {
|
||||
return ret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context) GetAdmin() bool {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["admin"].(bool); ok {
|
||||
return ret
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) getToken() []byte {
|
||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
||||
t, ok := s.Values["token"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b, ok := t.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func xor(a, b []byte) (c []byte) {
|
||||
n := len(a)
|
||||
if len(b) != n {
|
||||
return
|
||||
}
|
||||
c = make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
c[i] = a[i] ^ b[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
43
pkg/server/editor.go
Normal file
43
pkg/server/editor.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import "sync"
|
||||
|
||||
type Editor struct {
|
||||
Username string
|
||||
Time string
|
||||
}
|
||||
|
||||
type EditorStore struct {
|
||||
sync.RWMutex
|
||||
m map[string]*Editor
|
||||
}
|
||||
|
||||
func NewEditorStore() *EditorStore {
|
||||
return &EditorStore{m: make(map[string]*Editor)}
|
||||
}
|
||||
|
||||
func (e *EditorStore) Set(page, username string) *Editor {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
editor := e.m[page]
|
||||
if editor != nil {
|
||||
return editor
|
||||
}
|
||||
e.m[page] = &Editor{username, now()}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EditorStore) RemoveEditor(page, username string) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
if editor, ok := e.m[page]; ok {
|
||||
if editor.Username == username {
|
||||
delete(e.m, page)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (e *EditorStore) Remove(page string) {
|
||||
e.Lock()
|
||||
defer e.Unlock()
|
||||
delete(e.m, page)
|
||||
}
|
||||
447
pkg/server/handler.go
Normal file
447
pkg/server/handler.go
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type notFoundHandler struct {
|
||||
s *HTTPServer
|
||||
}
|
||||
|
||||
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
newContext(w, r, nf.s).NotFound()
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if ctx.GetUser() == ctx.Var("user") {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
func authHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.NotFound()
|
||||
}
|
||||
}
|
||||
|
||||
func staticHandler(ctx *Context) {
|
||||
var err error
|
||||
ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
|
||||
ctx.SetHeader("Expires", time.Now().Add(time.Hour*12).Format(http.TimeFormat))
|
||||
switch ctx.Path() {
|
||||
case "/bootstrap.css":
|
||||
err = ctx.Write(ctx.Srv.res["bootstrap.css"])
|
||||
case "/custom.css":
|
||||
err = ctx.Write(ctx.Srv.res["custom.css"])
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("static:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.UnsetUdata()
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func loginHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Login",
|
||||
BodyTitle: "Login",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || submit != "Login" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.BS.Login(user, password)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.SetUdata(user, ctx.Srv.BS.UserIsAdmin(user))
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(ctx *Context) {
|
||||
ctx.Template("searchHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Search",
|
||||
BodyTitle: "Search",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
search := ctx.Form("search")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if submit != "Search" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := ctx.Srv.Index.Search(!ctx.LoggedOn(), search)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Search = search
|
||||
ctx.Data.Data = template.HTML(res)
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func allHandler(ctx *Context) {
|
||||
ctx.Template("allHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Articles",
|
||||
BodyTitle: "All Articles",
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
all, err := ctx.Srv.Index.GetAll(!ctx.LoggedOn())
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = all
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiNewHandler(ctx *Context) {
|
||||
ctx.Template("wikiNewHandler")
|
||||
ctx.Data = webData{Title: "New Article"}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
title := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
art := Article{
|
||||
Title: title,
|
||||
MD: text,
|
||||
}
|
||||
ctx.Data.Data = art
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
newTitle, err := ctx.Srv.Index.Index(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/"+newTitle, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiHandler(ctx *Context) {
|
||||
if ctx.T == nil {
|
||||
ctx.Template("wikiHandler")
|
||||
}
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
title := ctx.Var("title")
|
||||
art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("wikiMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiEditHandler(ctx *Context) {
|
||||
ctx.Template("wikiEditHandler")
|
||||
ctx.Data = webData{}
|
||||
title := ctx.Var("title")
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Data.Editor = ctx.Srv.Editors.Set(title, ctx.GetUser())
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
ftitle := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
ctx.Data.Data = Article{
|
||||
Title: ftitle,
|
||||
LinkTitle: title,
|
||||
MD: text,
|
||||
Public: pub,
|
||||
}
|
||||
ctx.Data.Title = ftitle
|
||||
ctx.Data.BodyTitle = ftitle
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if submit == "Back" {
|
||||
ctx.Srv.Editors.RemoveEditor(title, ctx.GetUser())
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.Index.Update(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Srv.Editors.Remove(title)
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiDelHandler(ctx *Context) {
|
||||
title := ctx.Var("title")
|
||||
submit := ctx.Form("submit")
|
||||
f := func() {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
if !ctx.CheckToken(f) {
|
||||
return
|
||||
}
|
||||
if submit != "Delete" {
|
||||
f()
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.Index.Delete(title); err != nil {
|
||||
log.Println("delete:", err)
|
||||
f()
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func usersHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Template("usersHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Users",
|
||||
BodyTitle: "Users",
|
||||
User: ctx.GetUser(),
|
||||
}
|
||||
us, err := ctx.Srv.BS.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = us
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func userNewHandler(ctx *Context) {
|
||||
ctx.Template("userNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New User",
|
||||
BodyTitle: "New User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.BS.CreateUser(user, password, adm); err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditHandler(ctx *Context) {
|
||||
ctx.Template("userEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
u, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken(nil) {
|
||||
return
|
||||
}
|
||||
if user == "" || password != repeat || submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if ctx.GetAdmin() {
|
||||
if err := ctx.Srv.BS.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if user == ctx.GetUser() {
|
||||
if err := ctx.Srv.BS.UpdateUserPassword(user, password); err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.BS.UnlockUser(user); err != nil {
|
||||
log.Println("unlock:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
|
||||
func userDelHandler(ctx *Context) {
|
||||
ctx.Template("userDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete User",
|
||||
BodyTitle: "Delete User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
user, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = user
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if err := ctx.Srv.BS.DeleteUser(ctx.Var("user")); err != nil {
|
||||
log.Println("delete:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
ctx.UnsetUdata()
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
127
pkg/server/routes.go
Normal file
127
pkg/server/routes.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler ctxHandler
|
||||
Methods []string
|
||||
}
|
||||
|
||||
var routes = []route{
|
||||
{
|
||||
"/",
|
||||
indexHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/bootstrap.css",
|
||||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/custom.css",
|
||||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
loginHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/search",
|
||||
searchHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/new",
|
||||
authHandler(
|
||||
wikiNewHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/all",
|
||||
allHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+-]+$}",
|
||||
wikiHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+-]+}/md",
|
||||
wikiMDHandler(
|
||||
wikiHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+-]+}/edit",
|
||||
authHandler(
|
||||
wikiEditHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+-]+}/del",
|
||||
authHandler(
|
||||
wikiDelHandler),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/users",
|
||||
adminAuthHandler(
|
||||
usersHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+$}",
|
||||
wikiHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/md",
|
||||
wikiMDHandler(
|
||||
wikiHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/edit",
|
||||
wikiEditHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/del",
|
||||
wikiDelHandler,
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/user/new",
|
||||
adminAuthHandler(
|
||||
userNewHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/del/{user:[a-zA-Z0-9]+$}",
|
||||
adminAuthHandler(
|
||||
userDelHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/unlock/{user:[a-zA-Z0-9]+$}",
|
||||
adminAuthHandler(
|
||||
userUnlockHandler),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/user/edit/{user:[a-zA-Z0-9]+$}",
|
||||
userAuthHandler(
|
||||
userEditHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/logout",
|
||||
logoutHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
}
|
||||
118
pkg/server/server.go
Normal file
118
pkg/server/server.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"html/template"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLen = 32
|
||||
authLen = keyLen * 2
|
||||
cookieName = "gosession"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
Version string
|
||||
SecCookie bool
|
||||
Store *sessions.CookieStore
|
||||
Editors *EditorStore
|
||||
BS *BoltStore
|
||||
Index *Index
|
||||
listener net.Listener
|
||||
srv *http.Server
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
}
|
||||
|
||||
func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) {
|
||||
srv = &HTTPServer{
|
||||
Version: version,
|
||||
SecCookie: secCookie,
|
||||
listener: l,
|
||||
}
|
||||
srv.Editors = NewEditorStore()
|
||||
srv.cookieStore()
|
||||
srv.loadTemplates()
|
||||
srv.srv = &http.Server{
|
||||
Handler: srv.buildRoutes(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Start() (err error) {
|
||||
s.BS, err = NewBoltStore()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.Index, err = NewIndex(blevePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
log.Println(s.srv.Serve(s.listener))
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Stop() error {
|
||||
s.BS.Close()
|
||||
s.Index.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func (s *HTTPServer) buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = ¬FoundHandler{s}
|
||||
for _, v := range routes {
|
||||
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r, s))
|
||||
}
|
||||
}
|
||||
func (s *HTTPServer) cookieStore() {
|
||||
options := &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 12,
|
||||
Secure: s.SecCookie,
|
||||
HttpOnly: true,
|
||||
}
|
||||
authKey := genKey(authLen)
|
||||
encKey := genKey(keyLen)
|
||||
s.Store = sessions.NewCookieStore(authKey, encKey)
|
||||
s.Store.Options = options
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Hour * 12)
|
||||
newAuthKey := genKey(authLen)
|
||||
newEncKey := genKey(keyLen)
|
||||
newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
|
||||
newStore.Options = options
|
||||
|
||||
s.Store = newStore
|
||||
authKey = newAuthKey
|
||||
encKey = newEncKey
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
155
pkg/server/snippet_handler.go
Normal file
155
pkg/server/snippet_handler.go
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
package main
|
||||
|
||||
func allSnippetHandler(ctx *Context) {
|
||||
ctx.Template("allSnippetHandler")
|
||||
ctx.Data = webData{Title: "All Snippets"}
|
||||
all, err := ctx.Srv.BS.GetSnippets(ctx.GetUser())
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = all
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func snippetNewHandler(ctx *Context) {
|
||||
ctx.Template("snippetNewHandler")
|
||||
ctx.Data = webData{Title: "New Snippet"}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.GetUser()
|
||||
title := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
perm := ctx.Form("permission")
|
||||
submit := ctx.Form("submit")
|
||||
snip := Snippet{
|
||||
Title: title,
|
||||
MD: text,
|
||||
}
|
||||
ctx.Data.Data = art
|
||||
if !ctx.CheckToken() {
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
iperm, err := strconv.Atoi(perm)
|
||||
if err != nil || submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
newTitle, err := ctx.Srv.BS.CreateSnippet(user, title, text, iperm)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/users/"+user+"/"+newTitle, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiHandler(ctx *Context) {
|
||||
if ctx.T == nil {
|
||||
ctx.Template("wikiHandler")
|
||||
}
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
title := ctx.Var("title")
|
||||
art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("wikiMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiEditHandler(ctx *Context) {
|
||||
ctx.Template("wikiEditHandler")
|
||||
ctx.Data = webData{}
|
||||
title := ctx.Var("title")
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
art, err := ctx.Srv.Index.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Data.Editor = ctx.Srv.Editors.Set(title, ctx.GetUser())
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
ftitle := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
ctx.Data.Data = Article{
|
||||
Title: ftitle,
|
||||
LinkTitle: title,
|
||||
MD: text,
|
||||
Public: pub,
|
||||
}
|
||||
ctx.Data.Title = ftitle
|
||||
ctx.Data.BodyTitle = ftitle
|
||||
if !ctx.CheckToken() {
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if submit == "Back" {
|
||||
ctx.Srv.Editors.RemoveEditor(title, ctx.GetUser())
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.Index.Update(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Srv.Editors.Remove(title)
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiDelHandler(ctx *Context) {
|
||||
title := ctx.Var("title")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if submit != "Delete" {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.Index.Delete(title); err != nil {
|
||||
log.Println("delete:", err)
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
693
pkg/server/templates.go
Normal file
693
pkg/server/templates.go
Normal file
File diff suppressed because one or more lines are too long
36
pkg/util/helper.go
Normal file
36
pkg/util/helper.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const timeFmt = "2006-01-02 15:04:05"
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
|
||||
func since(t time.Time) string {
|
||||
s := time.Since(t)
|
||||
u := uint64(s)
|
||||
if u <= 0 {
|
||||
return "0s"
|
||||
}
|
||||
str := s.String()
|
||||
switch {
|
||||
case u >= uint64(time.Second):
|
||||
f, _ := strconv.ParseFloat(str[:len(str)-1], 64)
|
||||
return fmt.Sprintf("%.fs", f)
|
||||
case u < uint64(time.Microsecond):
|
||||
f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
|
||||
return fmt.Sprintf("%.fns", f)
|
||||
case u < uint64(time.Millisecond):
|
||||
f, _ := strconv.ParseFloat(str[:len(str)-3], 64)
|
||||
return fmt.Sprintf("%.fµs", f)
|
||||
default:
|
||||
f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
|
||||
return fmt.Sprintf("%.fms", f)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue