diff --git a/VERSION b/VERSION index c0ec837..6b3126c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v2.0 +v1.0 diff --git a/docstore b/docstore new file mode 100755 index 0000000..981b967 Binary files /dev/null and b/docstore differ diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 2ab3ba1..86c8f85 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -14,17 +14,18 @@ import ( "net" "os" "os/signal" + "path/filepath" "syscall" "time" ) const ( - defDataDir = "/var/lib/docstore" + defRunDir = "/var/lib/docstore" defListen = "127.0.0.1:8080" defLogFile = "docstore.log" - defRunUser = "wiki" - defRunGroup = "wiki" + defRunUser = "docstore" + defRunGroup = "docstore" ) var ( @@ -50,7 +51,7 @@ func initServer(cfg core.Config) (err error) { if err != nil { return } - if err = os.Chdir(cfg.DataDir); err != nil { + if err = os.Chdir(cfg.RunDir); err != nil { return } @@ -65,6 +66,11 @@ func initServer(cfg core.Config) (err error) { log.InitLogger(cfg) + cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir) + err = godrop.Unveil(cfg.DataDir, "rwc") + if err != nil { + return + } srv := server.NewHTTPServer(cfg, l) err = srv.Start() if err != nil { @@ -105,7 +111,7 @@ func loadConfig() error { func Run(version string) { app := cli.NewApp() app.Name = "docstore" - app.Usage = "a simple wiki engine" + app.Usage = "file server and indexing engine" app.Version = version app.Commands = []cli.Command{ { @@ -125,7 +131,7 @@ func Run(version string) { }, { Name: "dump", - Usage: "create a json dump of all articles in the data directory (docstore needs to be stopped)", + Usage: "create a json dump of all users (docstore needs to be stopped)", Flags: defaultFlags(dumpRestoreFlags()), Action: func(c *cli.Context) error { var ( @@ -158,7 +164,7 @@ func Run(version string) { }, { Name: "restore", - Usage: "restore a json dump to the data directory (docstore needs to be stopped)", + Usage: "restore a json dump (docstore needs to be stopped)", Flags: defaultFlags(dumpRestoreFlags()), Action: func(c *cli.Context) error { var ( @@ -224,11 +230,6 @@ func serverFlags() []cli.Flag { Usage: "static hmac secret (at least 64 chars, used for JWT signing)", Destination: &config.Secret, }, - cli.BoolTFlag{ - Name: "admin, a", - Usage: "use -a=false to disable the admin user", - Destination: &config.Admin, - }, cli.BoolFlag{ Name: "foreground, f", Usage: "do not fork into the background", @@ -266,10 +267,10 @@ func defaultFlags(f []cli.Flag) []cli.Flag { Destination: &configFile, }, cli.StringFlag{ - Name: "data, d", - Value: defDataDir, - Usage: "data directory", - Destination: &config.DataDir, + Name: "run, r", + Value: defRunDir, + Usage: "run directory", + Destination: &config.RunDir, }, }, f...) } diff --git a/pkg/core/const.go b/pkg/core/const.go index 31a130a..85fda03 100644 --- a/pkg/core/const.go +++ b/pkg/core/const.go @@ -8,6 +8,16 @@ var ( LoginURI = "/login" LogoutURI = "/logout" + RawPrefix = "/raw" + IndexPrefix = "/index" + MovePrefix = "/move" + DeletePrefix = "/delete" + + Favicon = "/favicon.ico" + BootstrapCSS = "/bootstrap.css" + CustomCSS = "/custom.css" + + DataDir = "data" AutoImport = "auto_import" ManualImport = "manual_import" Imported = "imported" diff --git a/pkg/core/helper.go b/pkg/core/helper.go index 500f4dd..3671f0b 100644 --- a/pkg/core/helper.go +++ b/pkg/core/helper.go @@ -2,16 +2,6 @@ package core import "time" -// Contains checks if slice a contains string str -func Contains(str string, a []string) (int, bool) { - for i, s := range a { - if s == str { - return i, true - } - } - return -1, false -} - const ( timeFmt = "2006-01-02 15:04:05" fileFmt = "20060102_150405" diff --git a/pkg/core/permission.go b/pkg/core/permission.go deleted file mode 100644 index 01fd8cb..0000000 --- a/pkg/core/permission.go +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2019 Marius Schellenberger - -package core - -type Permission int - -const ( - Invalid Permission = iota - Public - Private -) - -func ParsePermString(p string) Permission { - switch p { - case "1": - return Public - case "2": - return Private - } - return Invalid -} - -func ReadPerm(username string, p Permission, s Section) bool { - switch p { - case Public: - return true - case Private: - _, c := Contains(username, s.Members) - return c - } - return false -} - -func WritePerm(username string, p Permission, s Section) bool { - switch p { - case Public, Private: - _, c := Contains(username, s.Members) - return c - } - return false -} diff --git a/pkg/core/permission_test.go b/pkg/core/permission_test.go deleted file mode 100644 index 40b205e..0000000 --- a/pkg/core/permission_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2019 Marius Schellenberger - -package core - -import ( - "fmt" - "testing" -) - -func TestParsePermString(t *testing.T) { - data := map[string]Permission{ - "": Invalid, - "1": Public, - "2": Private, - "123": Invalid, - "test": Invalid, - } - for k, v := range data { - t.Run(k, func(t *testing.T) { - if ParsePermString(k) != v { - t.Fail() - } - }) - } -} - -func TestReadPerm(t *testing.T) { - data := []struct { - Username string - Perm Permission - Section Section - OK bool - }{ - {"", Invalid, Section{Members: []string{"admin"}}, false}, - {"", Public, Section{Members: []string{"admin"}}, true}, - {"user", Public, Section{Members: []string{"admin"}}, true}, - {"", Private, Section{Members: []string{"admin"}}, false}, - {"user", Private, Section{Members: []string{"admin"}}, false}, - {"", Private, Section{Members: []string{"user"}}, false}, - {"user", Private, Section{Members: []string{"user"}}, true}, - {"user", Private, Section{Members: []string{"user", "admin"}}, true}, - {"admin", Private, Section{Members: []string{"user"}}, false}, - } - for i, v := range data { - t.Run(fmt.Sprintf("Read:%d", i), func(t *testing.T) { - if ReadPerm(v.Username, v.Perm, v.Section) != v.OK { - t.Fail() - } - }) - } -} - -func TestWritePerm(t *testing.T) { - data := []struct { - Username string - Perm Permission - Section Section - OK bool - }{ - {"", Invalid, Section{Members: []string{"user"}}, false}, - {"", Public, Section{Members: []string{"user"}}, false}, - {"user", Public, Section{Members: []string{"user"}}, true}, - {"user", Public, Section{Members: []string{"admin"}}, false}, - {"user", Private, Section{Members: []string{"user"}}, true}, - {"", Private, Section{Members: []string{"user"}}, false}, - {"", Private, Section{Members: []string{"user", "admin"}}, false}, - {"user", Private, Section{Members: []string{"user", "admin"}}, true}, - {"admin", Private, Section{Members: []string{"user", "admin"}}, true}, - } - for i, v := range data { - t.Run(fmt.Sprintf("Write:%d", i), func(t *testing.T) { - if WritePerm(v.Username, v.Perm, v.Section) != v.OK { - t.Fail() - } - }) - } -} diff --git a/pkg/core/timeout.go b/pkg/core/timeout.go index ad92a42..771603e 100644 --- a/pkg/core/timeout.go +++ b/pkg/core/timeout.go @@ -1,6 +1,7 @@ package core import ( + "errors" "os/exec" "time" ) @@ -24,7 +25,12 @@ func Timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) { } err = <-done case e := <-done: - err = e + exerr, ok := e.(*exec.ExitError) + if ok { + err = errors.New(e.Error() + "\n" + string(exerr.Stderr)) + } else { + err = e + } output = <-out } return diff --git a/pkg/core/types.go b/pkg/core/types.go index 8444ef9..864bb34 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -4,52 +4,6 @@ package core import "html/template" -type Page struct { - Title string `json:"title"` - StoreTitle string `json:"store_title"` - Markdown string `json:"markdown"` - TOC template.HTML `json:"toc"` - HTML template.HTML `json:"html"` - Created string `json:"created"` - Updated string `json:"updated"` - Perm Permission `json:"perm"` -} - -type Pages []Page - -func (p Pages) Len() int { return len(p) } -func (p Pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (p Pages) Less(i, j int) bool { return p[i].StoreTitle < p[j].StoreTitle } - -type UpdatedPages []Page - -func (p UpdatedPages) Len() int { return len(p) } -func (p UpdatedPages) Swap(i, j int) { p[i], p[j] = p[j], p[i] } -func (p UpdatedPages) Less(i, j int) bool { return p[i].Updated > p[j].Updated } - -type Section struct { - Name string - User bool - Members []string -} - -type Sections []Section - -func (s Sections) Len() int { return len(s) } -func (s Sections) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s Sections) Less(i, j int) bool { return s[i].Name < s[j].Name } - -type SectionPage struct { - Section string - Pages UpdatedPages -} - -type SectionPages []SectionPage - -func (s SectionPages) Len() int { return len(s) } -func (s SectionPages) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s SectionPages) Less(i, j int) bool { return s[i].Section < s[j].Section } - type User struct { Username string Password string @@ -59,11 +13,25 @@ type User struct { type Result struct { ID string + Path string HTML template.HTML } +type Path struct { + Name string + Abs string + Indexed bool +} + +type Paths []Path + +func (p Paths) Len() int { return len(p) } +func (p Paths) Swap(i, j int) { p[i], p[j] = p[j], p[i] } +func (p Paths) Less(i, j int) bool { return p[i].Name < p[j].Name } + type Config struct { - DataDir string `json:"data_dir,omitempty"` + RunDir string `json:"run_dir,omitempty"` + DataDir string `json:"-"` User string `json:"user,omitempty"` Group string `json:"group,omitempty"` ListenAddr string `json:"listen_addr,omitempty"` diff --git a/pkg/db/db.go b/pkg/db/db.go index d8ae0be..cfda4ec 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -25,7 +25,7 @@ type DB struct { func New(cfg core.Config) (db *DB, err error) { db = new(DB) - dbFile := filepath.Join(cfg.DataDir, storeFile) + dbFile := filepath.Join(cfg.RunDir, storeFile) err = godrop.Unveil(dbFile, "rwc") if err != nil { return @@ -34,12 +34,12 @@ func New(cfg core.Config) (db *DB, err error) { if err != nil { return } - db.Index, err = index.NewIndex(filepath.Join(cfg.DataDir, blevePath)) + db.Index, err = index.NewIndex(filepath.Join(cfg.RunDir, blevePath)) if err != nil { return } err = db.UnlockUser() - if err != nil { + if err != nil && err != store.ErrKeyNotFound { return } err = db.RunMigrations() diff --git a/pkg/db/index.go b/pkg/db/index.go new file mode 100644 index 0000000..343e5f2 --- /dev/null +++ b/pkg/db/index.go @@ -0,0 +1,80 @@ +// Copyright (C) 2019 Marius Schellenberger + +package db + +import ( + "git.giftfish.de/ston1th/docstore/pkg/core" +) + +const ( + idPrefix = "id/" + pathPrefix = "path/" +) + +func (db *DB) GetPaths(res []core.Result) error { + for i, v := range res { + err := db.store.Get(pathPrefix+v.ID, &res[i].Path) + if err != nil { + return err + } + } + return nil +} + +func (db *DB) GetIndexed(p core.Paths) { + for i, v := range p { + p[i].Indexed = db.IsIndexed(v.Abs) + } +} + +func (db *DB) IsIndexed(path string) bool { + return db.store.Get(idPrefix+path, nil) == nil +} + +func (db *DB) GetPath(id string) (path string, err error) { + err = db.store.Get(pathPrefix+id, &path) + return +} + +func (db *DB) GetID(path string) (id string, err error) { + err = db.store.Get(idPrefix+path, &id) + return +} + +func (db *DB) DeletePath(id string) error { + return db.store.Delete(pathPrefix + id) +} + +func (db *DB) DeleteID(path string) error { + return db.store.Delete(idPrefix + path) +} + +func (db *DB) NewFile(id, path string) (err error) { + err = db.store.Set(idPrefix+path, id) + if err != nil { + return + } + err = db.store.Set(pathPrefix+id, path) + return +} + +func (db *DB) MoveFile(op, np string) error { + id, err := db.DeleteFile(op) + if err != nil { + return err + } + return db.NewFile(id, np) +} + +func (db *DB) DeleteFile(path string) (id string, err error) { + id, err = db.GetID(path) + if err != nil { + return + } + err = db.DeleteID(path) + if err != nil { + return + } + err = db.DeletePath(id) + return +} diff --git a/pkg/fs/file.go b/pkg/fs/file.go new file mode 100644 index 0000000..dce9997 --- /dev/null +++ b/pkg/fs/file.go @@ -0,0 +1,72 @@ +package fs + +import ( + "errors" + "git.giftfish.de/ston1th/docstore/pkg/core" + "os" + "path/filepath" + "strings" +) + +func (fs *Filesystem) GetFile(path string) (f *os.File, fi os.FileInfo, err error) { + path = strings.TrimPrefix(filepath.Clean(path), core.RawPrefix) + p := filepath.Join(fs.Base, path) + f, err = os.Open(p) + if err != nil { + return + } + fi, err = f.Stat() + if err != nil { + return + } + if !fi.Mode().IsRegular() { + err = os.ErrNotExist + } + return +} + +func (fs *Filesystem) Mode(path string) (mode FileMode, err error) { + path = filepath.Clean(path) + p := filepath.Join(fs.Base, path) + fi, err := os.Stat(p) + if err != nil { + return + } + m := fi.Mode() + if m.IsDir() { + mode = IsDir + } else if m.IsRegular() { + mode = IsFile + } + return +} + +func (fs *Filesystem) Move(path, dest, name string) (string, error) { + path = filepath.Clean(path) + if name == "" { + _, name = filepath.Split(path) + } + newfile := filepath.Clean(filepath.Join(dest, name)) + return newfile, os.Rename(filepath.Join(fs.Base, path), filepath.Join(fs.Base, newfile)) +} + +func (fs *Filesystem) Delete(path string) (removed []string, err error) { + path = filepath.Clean(path) + fullpath := filepath.Join(fs.Base, path) + if fullpath == fs.Base { + return nil, errors.New("the root path can not be deleted") + } + fi, err := os.Stat(path) + if err != nil { + return + } + m := fi.Mode() + if m.IsDir() { + err = os.RemoveAll(fullpath) + removed = fs.RecursiveFiles(path) + } else if m.IsRegular() { + err = os.Remove(fullpath) + removed = []string{path} + } + return +} diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go new file mode 100644 index 0000000..ab90599 --- /dev/null +++ b/pkg/fs/fs.go @@ -0,0 +1,120 @@ +package fs + +import ( + "errors" + "git.giftfish.de/ston1th/docstore/pkg/core" + "os" + "path/filepath" + "sort" + "strings" +) + +type FileMode int + +const ( + Undefined FileMode = iota + IsDir + IsFile +) + +// Directory represents a directory and its contents +type Directory struct { + Dirs core.Paths + Files core.Paths +} + +type Filesystem struct { + Base string +} + +func NewFilesystem(base string) (fs *Filesystem, err error) { + fi, err := os.Stat(base) + if err != nil { + return + } + if !fi.IsDir() { + err = errors.New("base dir '" + base + "' is not a directory") + } + fs = &Filesystem{base} + return +} + +func (fs *Filesystem) ReadDir(path string) (d *Directory) { + path = filepath.Clean(path) + combined := filepath.Join(fs.Base, path) + f, err := os.Open(combined) + if err != nil { + return + } + names, err := f.Readdirnames(0) + if err != nil { + return + } + d = new(Directory) + for _, n := range names { + fi, err := os.Stat(filepath.Join(combined, n)) + if err != nil { + continue + } + m := fi.Mode() + if m.IsDir() { + d.Dirs = append(d.Dirs, core.Path{Name: n, Abs: filepath.Join(path, n)}) + continue + } + if m.IsRegular() { + d.Files = append(d.Files, core.Path{Name: n, Abs: filepath.Join(path, n)}) + } + } + sort.Sort(d.Dirs) + sort.Sort(d.Files) + return +} + +func (fs *Filesystem) Recursive() (p core.Paths) { + filepath.Walk(fs.Base, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if info.IsDir() { + a := strings.TrimPrefix(path, fs.Base) + if a == "" { + a = "/" + } + p = append(p, core.Path{Abs: a}) + } + return nil + }) + sort.Sort(p) + return +} + +func (fs *Filesystem) RecursiveFiles(p string) (files []string) { + p = filepath.Clean(p) + fullpath := filepath.Join(fs.Base, p) + filepath.Walk(fullpath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil + } + if info.Mode().IsRegular() { + files = append(files, strings.TrimPrefix(path, fullpath)) + } + return nil + }) + return +} + +func Paths(path string) (p core.Paths) { + path = filepath.Clean(path) + s := strings.Split(path, string(filepath.Separator)) + for i, v := range s[1:] { + if v == "" { + continue + } + if i == 0 { + p = append(p, core.Path{Name: v, Abs: "/" + v}) + continue + } + p = append(p, core.Path{Name: v, Abs: filepath.Join(p[i-1].Abs, v)}) + } + return +} diff --git a/pkg/index/id.go b/pkg/index/id.go new file mode 100644 index 0000000..7f846d3 --- /dev/null +++ b/pkg/index/id.go @@ -0,0 +1,12 @@ +package index + +import ( + "crypto/rand" + "encoding/hex" +) + +func newID() string { + b := make([]byte, 16) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/pkg/index/index.go b/pkg/index/index.go index 18b703b..db47e9d 100644 --- a/pkg/index/index.go +++ b/pkg/index/index.go @@ -51,15 +51,17 @@ func (i *Index) Close() error { return i.i.Close() } -func (i *Index) Add(id, text, docType string) error { - doc, _ := i.i.Document(id) - if doc != nil { - err := i.Delete(id) - if err != nil { - return err +func (i *Index) Add(text, docType string) (id string, err error) { + id = newID() + for { + doc, _ := i.i.Document(id) + if doc == nil { + break } + id = newID() } - return i.i.Index(id, indexDocument{text, core.Now(), docType}) + err = i.i.Index(id, indexDocument{text, core.Now(), docType}) + return } func (i *Index) Delete(id string) error { diff --git a/pkg/server/context.go b/pkg/server/context.go index 39b66db..5e617a0 100644 --- a/pkg/server/context.go +++ b/pkg/server/context.go @@ -11,6 +11,7 @@ import ( "html/template" "net/http" "net/url" + "reflect" "time" ) @@ -21,59 +22,29 @@ const ( refererClaim = "referer" ) -func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) { +func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context { h := w.Header() - h.Set("X-Frame-Options", "DENY") + h.Set("X-Frame-Options", "sameorigin") 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';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'") + h.Set("Content-Security-Policy", "default-src 'none';object-src 'self';frame-src 'self';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'self'") h.Set("Referrer-Policy", "same-origin") - ctx = &Context{ + return &Context{ Request: r, Response: w, Srv: s, Time: time.Now(), } - path := ctx.Path() - if path == "/bootstrap.css" || path == "/custom.css" || path == "/favicon.ico" { - return - } - - if c, err := r.Cookie(cookieName); err == nil { - t, err := jwt.DecodeToken(c.Value) - if err != nil { - ctx.LogSetCookie("DecodeToken:", err) - return - } - if err = s.JWT.Verify(t); err != nil { - ctx.LogSetCookie("VerifyToken:", err) - return - } - if t.Claims.GetString(totpClaim) != "" { - if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI { - ctx.Token = *t - return - } - ctx.Redirect(core.TotpURI, http.StatusFound) - return nil - - } - if err = s.JWT.Invalidate(t); err != nil { - log.Println("Invalidate:", err) - } - } - ctx.SetCookie(nil, 0) - return } type Context struct { - Request *http.Request - Response http.ResponseWriter - Srv *HTTPServer - T *template.Template - Status int - Err error - Time time.Time + Request *http.Request + Response http.ResponseWriter + Srv *HTTPServer + T *template.Template + HTTPStatus int + Err error + Time time.Time Token jwt.Token @@ -96,7 +67,9 @@ type webData struct { Msg string Search string - Data interface{} + Path string + Paths core.Paths + Data interface{} } type loginData struct { @@ -104,20 +77,10 @@ type loginData struct { Referer string } -type sectionData struct { - Name string - Members map[string]bool -} - -type pageData struct { - Page core.Page - Sections map[string]bool -} - func (c *Context) Exec() { - defer c.log() + defer c.Log() if c.T == nil { - c.Status = http.StatusInternalServerError + c.Status(http.StatusInternalServerError) c.Err = errors.New("template is nil") return } @@ -132,24 +95,24 @@ func (c *Context) Exec() { c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6 if c.Data.Msg != "" { - c.Status = http.StatusBadRequest + c.Status(http.StatusBadRequest) } 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 + c.Status(http.StatusInternalServerError) } } -func (c *Context) log() { +func (c *Context) Log() { + if c.HTTPStatus == 0 { + c.HTTPStatus = int(reflect.Indirect(reflect.ValueOf(c.Response)).FieldByName("status").Int()) + } 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) + log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus, c.Err) return } - log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status) + log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus) } func (c *Context) Error(i interface{}) { @@ -165,20 +128,19 @@ func (c *Context) Error(i interface{}) { func (c *Context) NotFound() { c.Template("notFoundHandler") c.Data = webData{Title: "404"} - c.Status = http.StatusNotFound - c.Response.WriteHeader(http.StatusNotFound) + c.Status(http.StatusNotFound) c.Exec() } func (c *Context) Forbidden() { c.Template("forbiddenHandler") c.Data = webData{Title: "403"} - c.Status = http.StatusForbidden - c.Response.WriteHeader(http.StatusForbidden) + c.Status(http.StatusForbidden) c.Exec() } func (c *Context) SwitchHandler() { + log.Debug("server: switching to normal handler") c.Srv.srv.Handler = c.Srv.handler c.Redirect(core.IndexURI, http.StatusFound) } @@ -192,6 +154,11 @@ func (c *Context) Write(buf []byte) (err error) { return } +func (c *Context) Status(status int) { + c.HTTPStatus = status + c.Response.WriteHeader(status) +} + func (c *Context) SetHeader(name, value string) { c.Response.Header().Set(name, value) } diff --git a/pkg/server/handler.go b/pkg/server/handler.go index 9e1d136..ef3dd53 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -4,13 +4,20 @@ package server import ( "git.giftfish.de/ston1th/docstore/pkg/core" + "git.giftfish.de/ston1th/docstore/pkg/fs" "git.giftfish.de/ston1th/docstore/pkg/log" "git.giftfish.de/ston1th/docstore/pkg/otp" "git.giftfish.de/ston1th/jwt/v3" "net/http" + "path/filepath" + "strings" "time" ) +//TODO add missing CSRF checks + +const noSuchFile = "no such file or directory" + type notFoundHandler struct { s *HTTPServer } @@ -29,13 +36,66 @@ func totpAuthHandler(h ctxHandler) ctxHandler { } } +func jwtRegisterHandler(h ctxHandler) ctxHandler { + return func(ctx *Context) { + if c, err := ctx.Request.Cookie(cookieName); err == nil { + t, err := jwt.DecodeToken(c.Value) + if err != nil { + ctx.LogSetCookie("DecodeToken:", err) + return + } + if err = ctx.Srv.JWT.Verify(t); err != nil { + ctx.LogSetCookie("VerifyToken:", err) + return + } + ctx.Token = *t + h(ctx) + return + } + ctx.SetCookie(nil, 0) + h(ctx) + } +} + +func jwtHandler(h ctxHandler) ctxHandler { + return func(ctx *Context) { + if c, err := ctx.Request.Cookie(cookieName); err == nil { + t, err := jwt.DecodeToken(c.Value) + if err != nil { + ctx.LogSetCookie("DecodeToken:", err) + return + } + if err = ctx.Srv.JWT.Verify(t); err != nil { + ctx.LogSetCookie("VerifyToken:", err) + return + } + path := ctx.Path() + if t.Claims.GetString(totpClaim) != "" { + if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI { + ctx.Token = *t + h(ctx) + return + } + ctx.Redirect(core.TotpURI, http.StatusFound) + return + } + ctx.Token = *t + h(ctx) + return + } + ctx.SetCookie(nil, 0) + h(ctx) + } +} + func authHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.LoggedOn() { h(ctx) return } - ctx.Forbidden() + //ctx.Forbidden() + ctx.Redirect(core.LoginURI, http.StatusFound) } } @@ -44,11 +104,11 @@ func staticHandler(ctx *Context) { ctx.SetHeader("Content-Type", "text/css; charset=utf-8") ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat)) switch ctx.Path() { - case "/bootstrap.css": + case core.BootstrapCSS: err = ctx.Write(ctx.Srv.res["bootstrap.css"]) - case "/custom.css": + case core.CustomCSS: err = ctx.Write(ctx.Srv.res["custom.css"]) - case "/favicon.ico": + case core.Favicon: ctx.SetHeader("Content-Type", "image/x-icon") err = ctx.Write(ctx.Srv.res["favicon.ico"]) } @@ -75,12 +135,14 @@ func registerHandler(ctx *Context) { case "GET": ctx.Exec() case "POST": - if !ctx.CheckXsrf() { - return - } user := ctx.Form("user") password := ctx.Form("password") repeat := ctx.Form("repeat") + ctx.Data.Data = user + + if !ctx.CheckXsrf() { + return + } if user == "" || password == "" { ctx.Error("empty user or password") @@ -98,8 +160,156 @@ func registerHandler(ctx *Context) { } } +func fileHandler(ctx *Context) { + defer ctx.Log() + f, fi, err := ctx.Srv.FS.GetFile(ctx.Path()) + if err != nil { + ctx.Status(http.StatusNotFound) + err = ctx.Write([]byte(noSuchFile)) + if err != nil { + log.Println("file:", err) + } + return + } + http.ServeContent(ctx.Response, ctx.Request, fi.Name(), fi.ModTime(), f) +} + func indexHandler(ctx *Context) { - ctx.Redirect(core.IndexURI, http.StatusFound) + path := strings.TrimPrefix(ctx.Path(), core.IndexPrefix) + switch ctx.Method() { + case "GET": + go func() { + file, txt, err := ctx.Srv.Tesseract.Scan(path) + if err != nil { + log.Printf("scan: %s: %s", path, err) + return + } + id, err := ctx.Srv.DB.Index.Add(txt, "pdf") + if err != nil { + log.Printf("index: %s: %s", path, err) + return + } + err = ctx.Srv.DB.NewFile(id, file) + if err != nil { + log.Printf("newFile: %s: %s", path, err) + } + }() + p, _ := filepath.Split(path) + ctx.Redirect(p, http.StatusFound) + } +} + +func moveHandler(ctx *Context) { + path := strings.TrimPrefix(ctx.Path(), core.MovePrefix) + ctx.Data = webData{ + Title: "Move File", + BodyTitle: "Move File", + } + ctx.Template("moveHandler") + switch ctx.Method() { + case "GET": + ctx.Data.Path = path + ctx.Data.Paths = ctx.Srv.FS.Recursive() + _, ctx.Data.Data = filepath.Split(path) + ctx.Exec() + case "POST": + name := ctx.Form("name") + dest := ctx.Form("destination") + newfile, err := ctx.Srv.FS.Move(path, dest, name) + if err != nil { + ctx.Error(err) + return + } + err = ctx.Srv.DB.MoveFile(path, newfile) + if err != nil { + ctx.Error(err) + return + } + ctx.Redirect(newfile, http.StatusFound) + } +} + +func deleteHandler(ctx *Context) { + path := strings.TrimPrefix(ctx.Path(), core.DeletePrefix) + ctx.Template("deleteHandler") + switch ctx.Method() { + case "GET": + mode, err := ctx.Srv.FS.Mode(path) + if err != nil { + ctx.Error(err) + return + } + switch mode { + case fs.IsFile: + ctx.Data = webData{ + Title: "Delete File", + BodyTitle: "Delete File", + } + case fs.IsDir: + ctx.Data = webData{ + Title: "Delete Directory", + BodyTitle: "Delete Directory", + } + default: + ctx.Data = webData{ + Title: "Delete", + BodyTitle: "Delete", + } + ctx.Error(noSuchFile) + return + } + ctx.Data.Path = path + ctx.Data.Data = mode + ctx.Exec() + case "POST": + files, err := ctx.Srv.FS.Delete(path) + if err != nil { + ctx.Error(err) + return + } + for _, f := range files { + id, err := ctx.Srv.DB.DeleteFile(f) + if err != nil { + log.Printf("db: delete: %s %s", f, err) + } + err = ctx.Srv.DB.Index.Delete(id) + if err != nil { + log.Printf("index: delete: %s %s", f, err) + } + } + ctx.Redirect(core.IndexURI, http.StatusFound) + } +} + +func dirHandler(ctx *Context) { + ctx.Template("dirHandler") + ctx.Data = webData{ + Title: "Directory Viewer", + BodyTitle: "Directory Viewer", + } + path := ctx.Path() + switch ctx.Method() { + case "GET": + mode, err := ctx.Srv.FS.Mode(path) + if err != nil { + ctx.Error(err) + return + } + switch mode { + case fs.IsDir: + dir := ctx.Srv.FS.ReadDir(path) + ctx.Srv.DB.GetIndexed(dir.Files) + ctx.Data.Data = dir + ctx.Data.Paths = fs.Paths(path) + ctx.Exec() + case fs.IsFile: + ctx.Template("fileHandler") + ctx.Data.Data = core.RawPrefix + path + ctx.Data.Paths = fs.Paths(path) + ctx.Exec() + } + case "POST": + } } func loginHandler(ctx *Context) { @@ -201,492 +411,23 @@ func searchHandler(ctx *Context) { if !ctx.CheckXsrf() { return } - search := ctx.Form("search") - - res, err := ctx.Srv.DB.Index.Search(search) + ctx.Data.Search = ctx.Form("search") + if ctx.Data.Search == "" { + ctx.Error("empty search request") + return + } + res, err := ctx.Srv.DB.Index.Search(ctx.Data.Search) if err != nil { ctx.Error(err) return } - user := ctx.User() - r := make([]core.Result, len(res)) - i := 0 - for _, v := range res { - if ctx.Srv.DB.ValidatePage(v.StoreTitle, user) { - r[i] = v - i++ - } - } - - ctx.Data.Data = r[:i] - ctx.Data.Search = search - ctx.Exec() - } -} - -func sectionsHandler(ctx *Context) { - ctx.Template("sectionsHandler") - ctx.Data = webData{ - Title: "All Sections", - BodyTitle: "All Sections", - Admin: ctx.Admin(), - } - ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin()) - ctx.Exec() -} - -func sectionHandler(ctx *Context) { - ctx.Template("sectionHandler") - section := ctx.Var("section") - ctx.Data = webData{ - Title: section, - BodyTitle: section, - Admin: ctx.Admin(), - } - var err error - ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin()) - if err != nil { - ctx.NotFound() - return - } - ctx.Exec() -} - -func sectionNewHandler(ctx *Context) { - ctx.Template("sectionNewHandler") - ctx.Data = webData{ - Title: "New Section", - BodyTitle: "New Section", - } - users, err := ctx.Srv.DB.GetUsers() - if err != nil { - ctx.Error(err) - return - } - sd := sectionData{Members: make(map[string]bool)} - for _, u := range users { - sd.Members[u.Username] = false - } - switch ctx.Method() { - case "GET": - ctx.Data.Data = sd - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - section := ctx.Form("section") - members := ctx.FormSlice("members") - for _, m := range members { - sd.Members[m] = true - } - ctx.Data.Data = sd - if section == "" { - ctx.Error("empty section") - return - } - err = ctx.Srv.DB.CreateSection(section, members, false) + err = ctx.Srv.DB.GetPaths(res) if err != nil { ctx.Error(err) return } - log.Printf("create: section %s created by %s\n", section, ctx.User()) - ctx.Redirect(core.SectionsURI, http.StatusFound) - } -} - -func sectionEditHandler(ctx *Context) { - ctx.Template("sectionEditHandler") - ctx.Data = webData{ - Title: "Edit Section", - BodyTitle: "Edit Section", - } - users, err := ctx.Srv.DB.GetUsers() - if err != nil { - ctx.Error(err) - return - } - sd := sectionData{Members: make(map[string]bool)} - for _, u := range users { - sd.Members[u.Username] = false - } - s, err := ctx.Srv.DB.GetSection(ctx.Var("section")) - if err != nil { - ctx.Error(err) - return - } - sd.Name = s.Name - switch ctx.Method() { - case "GET": - for _, m := range s.Members { - sd.Members[m] = true - } - ctx.Data.Data = sd + ctx.Data.Data = res ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - members := ctx.FormSlice("members") - for _, m := range members { - sd.Members[m] = true - } - ctx.Data.Data = sd - err = ctx.Srv.DB.UpdateSection(s.Name, members, s.User) - if err != nil { - ctx.Error(err) - return - } - log.Printf("update: section %s updated by %s\n", s.Name, ctx.User()) - ctx.Redirect(core.SectionsURI, http.StatusFound) - } -} - -func sectionDelHandler(ctx *Context) { - ctx.Template("sectionDelHandler") - ctx.Data = webData{ - Title: "Delete Section", - BodyTitle: "Delete Section", - } - s, err := ctx.Srv.DB.GetSection(ctx.Var("section")) - if err != nil { - ctx.Error(err) - return - } - if s.User { - ctx.Error("user section can not be deleted") - return - } - switch ctx.Method() { - case "GET": - ctx.Data.Data = s.Name - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - err := ctx.Srv.DB.DeleteSection(s.Name) - if err != nil { - ctx.Error(err) - return - } - log.Printf("delete: section %s deleted by %s\n", s.Name, ctx.User()) - ctx.Redirect(core.SectionsURI, http.StatusFound) - } -} - -func pageNewHandler(ctx *Context) { - ctx.Template("pageNewHandler") - ctx.Data = webData{Title: "New Page"} - user := ctx.User() - secs, err := ctx.Srv.DB.GetUserSections(user) - if err != nil { - ctx.Error(err) - return - } - switch ctx.Method() { - case "GET": - secs[user] = true - ctx.Data.Data = pageData{core.Page{Perm: core.Public}, secs} - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - section := ctx.Form("section") - title := ctx.Form("title") - markdown := ctx.Form("markdown") - perm := core.ParsePermString(ctx.Form("perm")) - p := core.Page{ - Title: title, - Markdown: markdown, - Perm: perm, - } - secs[section] = true - ctx.Data.Data = pageData{p, secs} - if title == "" { - ctx.Error("empty title") - return - } - if perm == core.Invalid { - ctx.Error("invalid permission") - return - } - - page, err := ctx.Srv.DB.CreatePage(title, section, markdown, user, perm) - if err != nil { - ctx.Error(err) - return - } - ctx.Redirect(page.StoreTitle, http.StatusFound) - } -} - -func pageHandler(ctx *Context) { - var pageCreated string - ctx.Data = webData{ - Admin: ctx.Admin(), - } - section := ctx.Var("section") - title := ctx.Var("title") - user := ctx.User() - key := ctx.Var("key") - if key != "" { - t, err := jwt.DecodeToken(key) - if err != nil { - log.Println("share: DecodeToken:", err) - ctx.NotFound() - return - } - if err = ctx.Srv.JWT.Verify(t); err != nil { - log.Println("share: VerifyToken:", err) - ctx.NotFound() - return - } - user = t.Claims.GetString(sharedClaim) - if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(userCreatedClaim)) { - if err = ctx.Srv.JWT.Invalidate(t); err != nil { - log.Println("share: Invalidate:", err) - } - ctx.NotFound() - return - } - section = t.Claims.GetString(sectionClaim) - title = t.Claims.GetString(titleClaim) - pageCreated = t.Claims.GetString(pageCreatedClaim) - ctx.Data.Key = key - if ctx.T == nil { - ctx.Template("pageViewHandler") - } - } else { - if ctx.T == nil { - ctx.Template("pageHandler") - } - } - page, err := ctx.Srv.DB.GetPage(section, title, user) - if err != nil { - log.Println(err) - ctx.NotFound() - return - } - if key != "" && page.Created != pageCreated { - ctx.NotFound() - return - } - ctx.Data.Data = page - ctx.Data.Title = page.Title - ctx.Data.BodyTitle = page.Title - ctx.Exec() -} - -func pageMDHandler(f ctxHandler) ctxHandler { - return func(ctx *Context) { - ctx.Template("pageMDHandler") - f(ctx) - } -} - -func pageShareHandler(ctx *Context) { - ctx.Template("pageShareHandler") - ctx.Data = webData{} - section := ctx.Var("section") - title := ctx.Var("title") - user := ctx.User() - page, err := ctx.Srv.DB.GetPage(section, title, user) - if err != nil { - ctx.NotFound() - return - } - if page.Perm != core.Private { - ctx.Error("only private pages can be shared") - return - } - ctx.Data.Data = page.StoreTitle - ctx.Data.Title = page.Title - ctx.Data.BodyTitle = page.Title - switch ctx.Method() { - case "GET": - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - - d, err := duration(ctx.Form("duration"), ctx.Form("mode")) - if err != nil { - ctx.Error(err) - return - } - t := jwt.NewToken(map[string]interface{}{ - sharedClaim: user, - userCreatedClaim: ctx.Token.Claims.GetString(userCreatedClaim), - pageCreatedClaim: page.Created, - sectionClaim: section, - titleClaim: title, - jwt.ExpClaim: jwt.NewExp(d), - }, nil) - - if err = ctx.Srv.JWT.Sign(t); err != nil { - log.Println("share: SignToken:", err) - ctx.NotFound() - return - } - ctx.Data.Key = t.String() - ctx.Exec() - } -} - -func pageBlacklistHandler(ctx *Context) { - ctx.Template("pageBlacklistHandler") - ctx.Data = webData{ - Title: "Blacklist", - BodyTitle: "Blacklist", - } - switch ctx.Method() { - case "GET": - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - - t, err := jwt.DecodeToken(ctx.Form("share")) - if err != nil { - ctx.Error(err) - return - } - err = ctx.Srv.JWT.Verify(t) - if err != nil { - ctx.Error(err) - return - } - user := t.Claims.GetString(sharedClaim) - if user == "" { - ctx.Error("blacklisting denied: no share token") - return - } - if ctx.User() != user { - ctx.Error("blacklisting denied: token was issued by " + user) - return - } - err = ctx.Srv.JWT.Invalidate(t) - if err != nil { - ctx.Error(err) - return - } - ctx.Exec() - } -} - -func pageEditHandler(ctx *Context) { - ctx.Template("pageEditHandler") - ctx.Data = webData{} - section := ctx.Var("section") - title := ctx.Var("title") - page, err := ctx.Srv.DB.GetPage(section, title, ctx.User()) - if err != nil { - ctx.NotFound() - return - } - ctx.Data.Title = page.Title - ctx.Data.BodyTitle = page.Title - switch ctx.Method() { - case "GET": - ctx.Data.Data = page - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - page.Markdown = ctx.Form("markdown") - page.Perm = core.ParsePermString(ctx.Form("perm")) - ctx.Data.Data = page - - err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm) - if err != nil { - ctx.Error(err) - return - } - ctx.Redirect(page.StoreTitle, http.StatusFound) - } -} - -func pageDelHandler(ctx *Context) { - section := ctx.Var("section") - title := ctx.Var("title") - ctx.Template("pageDelHandler") - ctx.Data.Title = title - ctx.Data.BodyTitle = title - switch ctx.Method() { - case "GET": - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil { - ctx.Error(err) - return - } - ctx.Redirect(core.IndexURI, http.StatusFound) - } -} - -func usersHandler(ctx *Context) { - if !ctx.LoggedOn() { - ctx.NotFound() - return - } - ctx.Template("usersHandler") - ctx.Data = webData{ - Title: "Users", - BodyTitle: "Users", - User: ctx.User(), - } - us, err := ctx.Srv.DB.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": - if !ctx.CheckXsrf() { - return - } - user := ctx.Form("user") - password := ctx.Form("password") - repeat := ctx.Form("repeat") - admin := ctx.Form("admin") - adm := false - if admin == "0" { - adm = true - } - - if user == "" || password == "" { - ctx.Error("empty user or password") - return - } - if password != repeat { - ctx.Error("passwords do not match") - return - } - if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil { - ctx.Error(err) - return - } - log.Printf("create: user %s created by %s\n", user, ctx.User()) - ctx.Redirect("/users", http.StatusFound) } } @@ -696,7 +437,7 @@ func userEditHandler(ctx *Context) { Title: "Edit User", BodyTitle: "Edit User", } - u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user")) + u, err := ctx.Srv.DB.GetUserWithoutPassword() if err != nil { ctx.Error(err) return @@ -709,33 +450,18 @@ func userEditHandler(ctx *Context) { if !ctx.CheckXsrf() { return } - user := ctx.Var("user") password := ctx.Form("password") repeat := ctx.Form("repeat") - admin := ctx.Form("admin") - adm := false - if admin == "0" { - adm = true - } if password != repeat { ctx.Error("passwords do not match") return } - if ctx.Admin() { - if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil { - ctx.Error(err) - return - } - log.Printf("update: user %s updated by %s\n", user, ctx.User()) - ctx.Redirect("/users", http.StatusFound) - return - } - if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil { + if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil { ctx.Error(err) return } - log.Printf("update: %s updated by %s\n", user, ctx.User()) + log.Printf("update: %s updated\n", ctx.User()) ctx.Redirect(core.IndexURI, http.StatusFound) } } @@ -747,7 +473,7 @@ func userTotpHandler(ctx *Context) { BodyTitle: "TOTP", } req := otp.Request{Username: ctx.Var("user")} - u, err := ctx.Srv.DB.GetUser(req.Username) + u, err := ctx.Srv.DB.GetUser() if err != nil { ctx.Error(err) return @@ -787,68 +513,23 @@ func userTotpHandler(ctx *Context) { if u.Secret != "" { secret = u.Secret } - valid := otp.Validate(pin, secret) user := ctx.User() - if user != req.Username && ctx.Admin() { - valid = true - } - if !valid { + if !otp.Validate(pin, secret) { ctx.Error("validation failed") return } - if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil { + + if err := ctx.Srv.DB.UpdateUserSecret(req.Secret); err != nil { ctx.Error(err) return } if req.Secret == "" { - log.Printf("totp: disabled by %s for %s\n", user, req.Username) + log.Printf("totp: disabled for %s\n", user) } else { - log.Printf("totp: enabled by %s for %s\n", user, req.Username) + log.Printf("totp: enabled for %s\n", user) } - ctx.Redirect("/user/edit/"+req.Username, http.StatusFound) - } -} - -func userUnlockHandler(ctx *Context) { - user := ctx.Var("user") - if err := ctx.Srv.DB.UnlockUser(user); err != nil { - log.Println("unlock:", err) - } - log.Printf("unlock: %s unlocked by %s\n", user, ctx.User()) - ctx.Redirect("/users", http.StatusFound) -} - -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.DB.GetUserWithoutPassword(ctx.Var("user")) - if err != nil { - ctx.Error(err) - return - } - ctx.Data.Data = user - ctx.Exec() - case "POST": - if !ctx.CheckXsrf() { - return - } - self := ctx.User() - user := ctx.Var("user") - if err := ctx.Srv.DB.DeleteUser(user); err != nil { - log.Println("delete:", err) - } - log.Printf("delete: user %s deleted by %s\n", user, self) - if user == self { - ctx.Redirect("/logout", http.StatusFound) - return - } - ctx.Redirect("/users", http.StatusFound) + ctx.Redirect("/user/edit", http.StatusFound) } } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 0fa247d..bdab796 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -2,6 +2,8 @@ package server +import "git.giftfish.de/ston1th/docstore/pkg/core" + type route struct { Path string Handler ctxHandler @@ -10,90 +12,106 @@ type route struct { var static = []route{ { - "/favicon.ico", + core.Favicon, staticHandler, []string{"GET"}, }, { - "/bootstrap.css", + core.BootstrapCSS, staticHandler, []string{"GET"}, }, { - "/custom.css", + core.CustomCSS, staticHandler, []string{"GET"}, }, } -var register = append(static, route{ - "/", - registerHandler, +var registerRoutes = append(static, route{ + core.IndexURI, + jwtRegisterHandler( + registerHandler), []string{"GET", "POST"}, }) +var prefixRoutes = []route{ + { + core.RawPrefix, + jwtHandler( + authHandler( + fileHandler)), + []string{"GET"}, + }, + { + core.IndexPrefix, + jwtHandler( + authHandler( + indexHandler)), + []string{"GET", "POST"}, + }, + { + core.MovePrefix, + jwtHandler( + authHandler( + moveHandler)), + []string{"GET", "POST"}, + }, + { + core.DeletePrefix, + jwtHandler( + authHandler( + deleteHandler)), + []string{"GET", "POST"}, + }, + { + core.IndexURI, + jwtHandler( + authHandler( + dirHandler)), + []string{"GET", "POST", "PUT", "DELETE"}, + }, +} + var routes = append(static, []route{ - { - "/", - authHandler( - dirHandler), - []string{"GET"}, - }, - { - "/sections", - sectionsHandler, - []string{"GET"}, - }, { "/search", - authHandler( - searchHandler), + jwtHandler( + authHandler( + searchHandler)), []string{"GET", "POST"}, }, { - "/upload", - authHandler( - searchHandler), + core.LoginURI, + jwtHandler( + loginHandler), []string{"GET", "POST"}, }, { - "/login", - loginHandler, + core.TotpURI, + jwtHandler( + totpAuthHandler( + loginTotpHandler)), []string{"GET", "POST"}, }, { - "/totp", - totpAuthHandler( - loginTotpHandler), + "/user/edit", + jwtHandler( + authHandler( + userEditHandler)), []string{"GET", "POST"}, }, { - "/user/edit/{user:[a-zA-Z0-9]+$}", - authHandler( - userEditHandler), + "/user/totp", + jwtHandler( + authHandler( + userTotpHandler)), []string{"GET", "POST"}, }, { - "/user/totp/{user:[a-zA-Z0-9]+$}", - authHandler( - userTotpHandler), + core.LogoutURI, + jwtHandler( + logoutHandler), []string{"GET", "POST"}, }, - { - "/logout", - logoutHandler, - []string{"GET", "POST"}, - }, - { - "/{dir}", - authHandler( - dirHandler), - []string{"GET"}, - }, - { - "/{dir}/{file}", - authHandler( - fileHandler), - []string{"GET"}, - }, }...) diff --git a/pkg/server/server.go b/pkg/server/server.go index 2fc6067..4a67705 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -8,7 +8,9 @@ import ( "errors" "git.giftfish.de/ston1th/docstore/pkg/core" "git.giftfish.de/ston1th/docstore/pkg/db" + "git.giftfish.de/ston1th/docstore/pkg/fs" "git.giftfish.de/ston1th/docstore/pkg/log" + "git.giftfish.de/ston1th/docstore/pkg/tesseract" "git.giftfish.de/ston1th/godrop/v2" "git.giftfish.de/ston1th/jwt/v3" "github.com/gorilla/mux" @@ -19,7 +21,7 @@ import ( ) const ( - cookieName = "gosession" + cookieName = "docstore_session" ) type HTTPServer struct { @@ -29,8 +31,10 @@ type HTTPServer struct { srv *http.Server handler http.Handler - DB *db.DB - JWT *jwt.JWT + DB *db.DB + JWT *jwt.JWT + Tesseract *tesseract.Tesseract + FS *fs.Filesystem templ map[string]*template.Template res map[string][]byte @@ -40,14 +44,22 @@ func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) { srv = &HTTPServer{ Config: cfg, listener: l, - handler: srv.buildRoutes(), } + srv.handler = srv.buildRoutes() srv.loadTemplates() srv.srv = &http.Server{} return } func (s *HTTPServer) Start() (err error) { + s.FS, err = fs.NewFilesystem(s.Config.DataDir) + if err != nil { + return errors.New("fs: " + err.Error()) + } + s.Tesseract, err = tesseract.NewTesseract(s.Config.DataDir, nil, time.Second*30) + if err != nil { + return errors.New("tesseract: " + err.Error()) + } s.DB, err = db.New(s.Config) if err != nil { return errors.New("db: " + err.Error()) @@ -65,6 +77,7 @@ func (s *HTTPServer) Start() (err error) { return errors.New("server: " + err.Error()) } if !s.DB.UserExists() { + log.Debug("server: starting register handler") s.srv.Handler = s.register() } else { s.srv.Handler = s.handler @@ -84,29 +97,28 @@ func (s *HTTPServer) Stop() error { } func (s *HTTPServer) register() http.Handler { - m := mux.NewRouter() - m.NotFoundHandler = ®isterNotFoundHandler{s} - for _, v := range register { - m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) + r := mux.NewRouter() + r.NotFoundHandler = ®isterNotFoundHandler{s} + for _, v := range registerRoutes { + r.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) } - return m + return r } func (s *HTTPServer) buildRoutes() http.Handler { - m := mux.NewRouter() - m.NotFoundHandler = ¬FoundHandler{s} + r := mux.NewRouter() + r.NotFoundHandler = ¬FoundHandler{s} for _, v := range routes { - m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) + r.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) } - return m + for _, v := range prefixRoutes { + r.PathPrefix(v.Path).HandlerFunc(s.contextWrapper(v.Handler)).Methods(v.Methods...) + } + return r } func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - ctx := newContext(w, r, s) - if ctx == nil { - return - } - h(ctx) + h(newContext(w, r, s)) } } diff --git a/pkg/server/templates.go b/pkg/server/templates.go index 3f152ef..bf64c2f 100644 --- a/pkg/server/templates.go +++ b/pkg/server/templates.go @@ -13,6 +13,94 @@ import ( ) const ( + delete = `{{define "body"}} +
You are about to delete
+ {{if eq .Data 1}}the directory{{end}}
+ {{if eq .Data 2}}the file{{end}}
+ {{.Path}}.
+ {{if eq .Data 1}}
This will also remove all files and directories below.
| + | Path | +Options | +
|---|---|---|
| + | {{$item.Name}}/ | +Move | +
| + | {{$item.Name}} | ++ |
You are about to delete user {{.Data.Username}}.
This will also remove all pages of this user.
| Username | -Flags | -Options | -
|---|---|---|
| {{$item.Username}} | -- {{if $item.Secret}}TOTP{{end}} - {{if eq $item.Locked 3}}Locked{{end}} - {{if $item.Admin}}Admin{{end}} - | -- - | -
You are about to delete
+ {{if eq .Data 1}}the directory{{end}}
+ {{if eq .Data 2}}the file{{end}}
+ {{.Path}}.
+ {{if eq .Data 1}}
This will also remove all files and directories below.
| + | Path | +Options | +
|---|---|---|
| + | {{$item.Name}}/ | +Move | +
| + | {{$item.Name}} | ++ |
{{$item.HTML}}You are about to delete user {{.Data.Username}}.
This will also remove all pages of this user.
| Username | -Flags | -Options | -
|---|---|---|
| {{$item.Username}} | -- {{if $item.Secret}}TOTP{{end}} - {{if eq $item.Locked 3}}Locked{{end}} - {{if $item.Admin}}Admin{{end}} - | -- - | -