split into packages

This commit is contained in:
ston1th 2018-02-09 22:38:47 +01:00
commit c5d41b0965
26 changed files with 328 additions and 449 deletions

200
pkg/cmd/cmd.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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 = &notFoundHandler{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
}

View 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

File diff suppressed because one or more lines are too long

36
pkg/util/helper.go Normal file
View 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)
}
}