From 97d914bce49ca2c6d5650772b0e205c7c31db7cc Mon Sep 17 00:00:00 2001 From: ston1th Date: Wed, 19 Sep 2018 00:10:16 +0200 Subject: [PATCH] minor bugfixes and cleanup --- pkg/cache/cache.go | 2 + pkg/cmd/cmd.go | 29 +++++-------- pkg/core/const.go | 2 + pkg/core/permission.go | 2 + pkg/core/types.go | 16 ++++++- pkg/db/db.go | 20 +++------ pkg/db/helper.go | 6 ++- pkg/db/marshal.go | 28 ------------ pkg/db/page.go | 31 ++++++-------- pkg/db/user.go | 3 ++ pkg/index/index.go | 7 ++- pkg/render/render.go | 10 ----- pkg/server/context.go | 65 ++++++++++++++-------------- pkg/server/handler.go | 92 +++++++++++++++++----------------------- pkg/server/server.go | 29 +++++++++---- pkg/server/templates.go | 26 +++++++++--- pkg/server/xsrf.go | 2 + pkg/store/boltstore.go | 6 +-- pkg/store/marshal.go | 2 + pkg/store/store.go | 15 ------- templates/all.html | 2 +- templates/custom.css | 12 +++++- templates/index.html | 6 +-- templates/page.html | 2 + templates/pageShare.html | 2 + templates/search.html | 2 +- 26 files changed, 198 insertions(+), 221 deletions(-) delete mode 100644 pkg/db/marshal.go diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 5935e4c..f7efa71 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package cache import ( diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 813c420..95b74ab 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -3,7 +3,6 @@ package cmd import ( - "errors" "git.giftfish.de/ston1th/godrop" "github.com/urfave/cli" stdlog "log" @@ -13,13 +12,12 @@ import ( "syscall" "time" + "git.giftfish.de/ston1th/gowiki/pkg/core" "git.giftfish.de/ston1th/gowiki/pkg/log" "git.giftfish.de/ston1th/gowiki/pkg/server" ) const ( - blevePath = "bleve" - defDataDir = "/var/lib/gowiki" defListen = "127.0.0.1:8090" defLogFile = "gowiki.log" @@ -28,7 +26,7 @@ const ( defRunGroup = "wiki" ) -func initServer(conf serverConfig) (err error) { +func initServer(conf core.Config) (err error) { dropCfg := godrop.Config{ User: conf.User, Group: conf.Group, @@ -51,7 +49,7 @@ func initServer(conf serverConfig) (err error) { return } - srv := server.NewHTTPServer(l, conf.Version, conf.DataDir+"/"+blevePath, conf.SecureCookie) + srv := server.NewHTTPServer(l, conf) err = srv.Start() if err != nil { return @@ -63,8 +61,7 @@ func initServer(conf serverConfig) (err error) { log.Println("caught " + sig.String()) go func() { time.Sleep(time.Second * 20) - //TODO error needed? - log.Fatal(errors.New("stop timed out: killing")) + log.Fatal("stop timed out: killing") }() err = srv.Stop() if err != nil { @@ -76,7 +73,7 @@ func initServer(conf serverConfig) (err error) { var ( verbose bool - conf serverConfig + conf core.Config ) func Run(version string) { @@ -131,17 +128,6 @@ func Run(version string) { 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{ @@ -168,6 +154,11 @@ func serverFlags() []cli.Flag { Usage: "log file (leave empty for stdout, this only works in combination with -f)", Destination: &conf.LogFile, }, + cli.StringFlag{ + Name: "secret", + Usage: "static hmac secret (at least 64 chars, used for JWT signing)", + Destination: &conf.Secret, + }, cli.BoolFlag{ Name: "foreground, f", Usage: "do not fork into the background", diff --git a/pkg/core/const.go b/pkg/core/const.go index 5b1fc86..a465adb 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package core const ( diff --git a/pkg/core/permission.go b/pkg/core/permission.go index 547cbc1..2303cce 100644 --- a/pkg/core/permission.go +++ b/pkg/core/permission.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package core type Permission int diff --git a/pkg/core/types.go b/pkg/core/types.go index fc50391..714e145 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package core import "html/template" @@ -36,5 +38,17 @@ func (u Users) Less(i, j int) bool { return u[i].Username < u[j].Username } type Result struct { Title string StoreTitle string - Text string + HTML template.HTML +} + +type Config struct { + DataDir string + User string + Group string + ListenAddr string + LogFile string + Version string + Secret string + Foreground bool + SecureCookie bool } diff --git a/pkg/db/db.go b/pkg/db/db.go index be73cbf..8137bed 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -21,10 +21,7 @@ This is the [Index](/wiki/Index) page. You can customize it how you like.` ) -/*const ( - maxResult = 1e6 - maxSearchResult = 101 -)*/ +const blevePath = "bleve" func dbErr(i interface{}) error { return fmt.Errorf("db: %s", i) @@ -37,13 +34,12 @@ type DB struct { Index *index.Index } -func New(indexPath string) (db *DB, err error) { +func New(datadir string) (db *DB, err error) { db = &DB{cache: cache.NewCache()} - //TODO - //db.Index, err = index.NewIndex(indexPath) - //if err != nil { - // return - //} + db.Index, err = index.NewIndex(datadir + "/" + blevePath) + if err != nil { + return + } db.users, err = store.NewBoltStore(usersFile, nil) if err != nil { @@ -80,7 +76,5 @@ func (db *DB) Close() error { if err != nil { return err } - return nil - //TODO - //return db.Index.Close() + return db.Index.Close() } diff --git a/pkg/db/helper.go b/pkg/db/helper.go index ba527c8..2ebe1b5 100644 --- a/pkg/db/helper.go +++ b/pkg/db/helper.go @@ -1,7 +1,9 @@ +// Copyright (C) 2018 Marius Schellenberger + package db import ( - "golang.org/x/crypto/sha3" + "crypto/sha256" "time" ) @@ -31,7 +33,7 @@ func validatePassword(pw string) (b []byte) { if len(pw) <= 56 { return } - hash := sha3.New384() + hash := sha256.New() hash.Write(b) b = hash.Sum(nil) return diff --git a/pkg/db/marshal.go b/pkg/db/marshal.go deleted file mode 100644 index 13401d9..0000000 --- a/pkg/db/marshal.go +++ /dev/null @@ -1,28 +0,0 @@ -package db - -import ( - "bytes" - "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) -} diff --git a/pkg/db/page.go b/pkg/db/page.go index ce1b708..a985064 100644 --- a/pkg/db/page.go +++ b/pkg/db/page.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package db import ( @@ -81,19 +83,17 @@ func (db *DB) CreatePage(title, section, markdown, username string, p core.Permi Owner: owner, Perm: p, } - //search := render.Render(page) - render.Render(page) + search := render.Render(page) err = db.pages.Set(st, page) if err != nil { return } - //TODO - //err = db.Index.Add(title, st, search) - //if err != nil { - // return - //} + err = db.Index.Add(title, st, search) + if err != nil { + return + } db.cache.Add(st, page) return } @@ -119,20 +119,17 @@ func (db *DB) UpdatePage(title, section, markdown, username string, p core.Permi page.Markdown = markdown page.Updated = created(username) page.Perm = p - //TODO - //search := render.Render(page) - render.Render(page) + search := render.Render(page) err = db.pages.Set(st, page) if err != nil { return } - //TODO - //err = db.Index.Add(title, st, search) - //if err != nil { - // return - //} + err = db.Index.Add(title, st, search) + if err != nil { + return + } db.cache.Add(st, page) return } @@ -155,7 +152,5 @@ func (db *DB) DeletePage(title, section, username string) (err error) { return } db.cache.Delete(st) - return nil - //TODO - //return db.Index.Delete(st) + return db.Index.Delete(st) } diff --git a/pkg/db/user.go b/pkg/db/user.go index 816f796..98522a0 100644 --- a/pkg/db/user.go +++ b/pkg/db/user.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package db import ( @@ -166,6 +168,7 @@ func (db *DB) AdminUpdateUser(username, password string, admin bool) error { return db.users.Set(username, u) } +//TODO delete all user owned pages func (db *DB) DeleteUser(username string) error { if username == defUser { return errors.New("user '" + defUser + "' cannot be deleted") diff --git a/pkg/index/index.go b/pkg/index/index.go index fb44231..e4e351a 100644 --- a/pkg/index/index.go +++ b/pkg/index/index.go @@ -4,15 +4,14 @@ package index import ( "fmt" + "html/template" "os" "git.giftfish.de/ston1th/gowiki/pkg/core" "github.com/blevesearch/bleve" ) -const ( - maxSearchResult = 100 -) +const maxSearchResult = 101 type indexPage struct { Title string `json:"title"` @@ -89,7 +88,7 @@ func (i *Index) Search(search string) (results []core.Result, err error) { } if fragField == "title" || fragField == "search" { for _, f := range frags { - r.Text += f + r.HTML += template.HTML(f) } break } diff --git a/pkg/render/render.go b/pkg/render/render.go index 1c1b8ee..32960aa 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -22,7 +22,6 @@ func makeFilter() analysis.CharFilter { } var ( - //reHeader = regexp.MustCompile("((.+)()") reHeader = regexp.MustCompile("()(.+)()") reLink = regexp.MustCompile(`id="(.+?)"`) whiteSpace = regexp.MustCompile(`\s+`) @@ -64,7 +63,6 @@ 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 = reHeader.ReplaceAllString(html, `${1}${2}${3}`) html = reLink.ReplaceAllStringFunc(html, spaceReplace) for k, v := range hn { @@ -126,11 +124,3 @@ func closeTag(j int) (ret string) { } return } - -func renderResult(res []core.Result) (html string) { - for _, r := range res { - html += `` + r.Title + "" + - `
` + r.Text + "

" - } - return -} diff --git a/pkg/server/context.go b/pkg/server/context.go index a221f0c..527fe55 100644 --- a/pkg/server/context.go +++ b/pkg/server/context.go @@ -25,7 +25,7 @@ func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Con 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'") + h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';frame-ancestors 'none'") ctx = &Context{ Request: r, Response: w, @@ -101,6 +101,10 @@ func (c *Context) Exec() { c.Data.Login = c.LoggedOn() c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e5 + if c.Data.Msg != "" { + c.Status = http.StatusBadRequest + } + c.Err = c.T.Execute(c.Response, c.Data) if c.Err != nil { c.Status = http.StatusInternalServerError @@ -118,6 +122,16 @@ func (c *Context) log() { log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status) } +func (c *Context) Error(i interface{}) { + switch v := i.(type) { + case string: + c.Data.Msg = v + case error: + c.Data.Msg = v.Error() + } + c.Exec() +} + func (c *Context) NotFound() { c.Template("notFoundHandler") c.Data = webData{Title: "404"} @@ -170,17 +184,11 @@ func (c *Context) Var(name string) (ret string) { return } -// TODO remove f -// CheckToken validates the xsrf token -func (c *Context) CheckXsrf(f func()) (ok bool) { +// CheckXsrf validates the xsrf token +func (c *Context) CheckXsrf() (ok bool) { ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize]) if !ok { - if f != nil { - f() - return - } - c.Data.Msg = "wrong csrf token" - c.Exec() + c.Error("wrong csrf token") } return } @@ -190,26 +198,6 @@ func (c *Context) LoggedOn() (ok bool) { return } -func newCookie(value string, maxAge int) (cookie *http.Cookie) { - cookie = &http.Cookie{ - Name: cookieName, - Value: value, - Path: "/", - MaxAge: maxAge, - //TODO make secure configurable - Secure: false, - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - } - if maxAge > 0 { - d := time.Duration(maxAge) * time.Second - cookie.Expires = time.Now().Add(d) - } else if maxAge < 0 { - cookie.Expires = time.Unix(1, 0) - } - return -} - func (c *Context) SetCookie(claims map[string]interface{}) { t := jwt.NewToken(claims, nil) if err := c.Srv.JWT.Sign(t); err != nil { @@ -217,7 +205,22 @@ func (c *Context) SetCookie(claims map[string]interface{}) { return } c.Token = *t - http.SetCookie(c.Response, newCookie(c.Token.String(), int(jwt.DefaultExpiry.Seconds()))) + cookie := &http.Cookie{ + Name: cookieName, + Value: c.Token.String(), + Path: "/", + MaxAge: int(jwt.DefaultExpiry.Seconds()), + Secure: c.Srv.Secure, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + } + if cookie.MaxAge > 0 { + d := time.Duration(cookie.MaxAge) * time.Second + cookie.Expires = time.Now().Add(d) + } else if cookie.MaxAge < 0 { + cookie.Expires = time.Unix(1, 0) + } + http.SetCookie(c.Response, cookie) } func (c *Context) User() string { diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 2f4c58b..eca5965 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -88,21 +88,19 @@ func loginHandler(ctx *Context) { case "POST": user := ctx.Form("user") password := ctx.Form("password") - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } if user == "" || password == "" { - ctx.Data.Msg = "wrong inputs" - ctx.Exec() + ctx.Error("wrong inputs") return } u, err := ctx.Srv.DB.Login(user, password) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } - ctx.SetCookie(map[string]interface{}{userClaim: u.Username, adminClaim: u.Admin}) + ctx.SetCookie(jwt.Claims{userClaim: u.Username, adminClaim: u.Admin}) ctx.Redirect(core.IndexURI, 302) } } @@ -118,18 +116,16 @@ func searchHandler(ctx *Context) { ctx.Exec() case "POST": search := ctx.Form("search") - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } - //TODO - //res, err := ctx.Srv.DB.Index.Search(search) - //if err != nil { - // ctx.Data.Msg = err.Error() - // ctx.Exec() - // return - //} - //ctx.Data.Data = res + res, err := ctx.Srv.DB.Index.Search(search) + if err != nil { + ctx.Error(err) + return + } + ctx.Data.Data = res ctx.Data.Search = search ctx.Exec() } @@ -164,7 +160,7 @@ func pageNewHandler(ctx *Context) { Perm: perm, } ctx.Data.Data = p - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } if section != core.WikiSection { @@ -172,8 +168,7 @@ func pageNewHandler(ctx *Context) { } page, err := ctx.Srv.DB.CreatePage(title, section, markdown, ctx.User(), perm) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Redirect(page.StoreTitle, 302) @@ -242,6 +237,10 @@ func pageShareHandler(ctx *Context) { ctx.NotFound() return } + if page.Perm != core.Private { + ctx.Error("only private pages can be shared") + return + } ctx.Data.Data = st ctx.Data.Title = page.Title ctx.Data.BodyTitle = page.Title @@ -250,13 +249,12 @@ func pageShareHandler(ctx *Context) { ctx.Exec() case "POST": duration := ctx.Form("duration") - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } d, err := time.ParseDuration(duration) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } t := jwt.NewToken(map[string]interface{}{ @@ -286,36 +284,31 @@ func pageBlacklistHandler(ctx *Context) { case "GET": ctx.Exec() case "POST": - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } t, err := jwt.DecodeToken(ctx.Form("share")) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } err = ctx.Srv.JWT.Verify(t) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } user := t.Claims.GetString(sharedClaim) if user == "" { - ctx.Data.Msg = "blacklisting denied: no share token" - ctx.Exec() + ctx.Error("blacklisting denied: no share token") return } if ctx.User() != user { - ctx.Data.Msg = "blacklisting denied: token was issued by " + user - ctx.Exec() + ctx.Error("blacklisting denied: token was issued by " + user) return } err = ctx.Srv.JWT.Invalidate(t) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Exec() @@ -342,13 +335,12 @@ func pageEditHandler(ctx *Context) { page.Markdown = ctx.Form("markdown") page.Perm = core.ParsePermString(ctx.Form("perm")) ctx.Data.Data = page - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Redirect(page.StoreTitle, 302) @@ -367,12 +359,11 @@ func pageDelHandler(ctx *Context) { case "GET": ctx.Exec() case "POST": - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Redirect(core.IndexURI, 302) @@ -416,17 +407,15 @@ func userNewHandler(ctx *Context) { if admin == "0" { adm = true } - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } if user == "" || password == "" || password != repeat { - ctx.Data.Msg = "wrong inputs" - ctx.Exec() + ctx.Error("wrong inputs") return } if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Redirect("/user", 302) @@ -443,8 +432,7 @@ func userEditHandler(ctx *Context) { case "GET": u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user")) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Data.Data = u @@ -458,25 +446,22 @@ func userEditHandler(ctx *Context) { if admin == "0" { adm = true } - if !ctx.CheckXsrf(nil) { + if !ctx.CheckXsrf() { return } if user == "" || password != repeat { - ctx.Data.Msg = "wrong inputs" - ctx.Exec() + ctx.Error("wrong inputs") return } if ctx.Admin() { if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } } else { if user == ctx.User() { if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Redirect(core.IndexURI, 302) @@ -505,8 +490,7 @@ func userDelHandler(ctx *Context) { case "GET": user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user")) if err != nil { - ctx.Data.Msg = err.Error() - ctx.Exec() + ctx.Error(err) return } ctx.Data.Data = user diff --git a/pkg/server/server.go b/pkg/server/server.go index ecc23d7..2abb4fd 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -1,3 +1,5 @@ +// Copyright (C) 2018 Marius Schellenberger + package server import ( @@ -9,6 +11,7 @@ import ( "net/http" "time" + "git.giftfish.de/ston1th/gowiki/pkg/core" "git.giftfish.de/ston1th/gowiki/pkg/db" "git.giftfish.de/ston1th/gowiki/pkg/log" "git.giftfish.de/ston1th/jwt" @@ -19,10 +22,13 @@ const ( ) type HTTPServer struct { - Version string - indexPath string - listener net.Listener - srv *http.Server + Version string + Secure bool + Secret string + DataDir string + + listener net.Listener + srv *http.Server DB *db.DB JWT *jwt.JWT @@ -31,9 +37,12 @@ type HTTPServer struct { res map[string][]byte } -func NewHTTPServer(l net.Listener, version, indexPath string, secCookie bool) (srv *HTTPServer) { +func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) { srv = &HTTPServer{ - Version: version, + Version: conf.Version, + Secure: conf.SecureCookie, + Secret: conf.Secret, + DataDir: conf.DataDir, listener: l, } srv.loadTemplates() @@ -44,13 +53,15 @@ func NewHTTPServer(l net.Listener, version, indexPath string, secCookie bool) (s } func (s *HTTPServer) Start() (err error) { - //TODO make secret configurable - buf := bytes.NewBufferString("be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09") + var buf *bytes.Buffer + if s.Secret != "" { + buf = bytes.NewBufferString(s.Secret) + } s.JWT, err = jwt.New(jwt.DefaultExpiry, jwt.NewMemBlacklist(), buf) if err != nil { return } - s.DB, err = db.New(s.indexPath) + s.DB, err = db.New(s.DataDir) if err != nil { return } diff --git a/pkg/server/templates.go b/pkg/server/templates.go index 6decf95..8b2a832 100644 --- a/pkg/server/templates.go +++ b/pkg/server/templates.go @@ -28,7 +28,7 @@ const (
@@ -48,9 +48,9 @@ const ( GoWiki | {{.Title}} - + - +