// Copyright (C) 2019 Marius Schellenberger 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" "html/template" "net/http" "path/filepath" "strings" "time" ) const noSuchFile = "no such file or directory" type notFoundHandler struct { s *HTTPServer } func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { newContext(w, r, nf.s).NotFound() } func totpAuthHandler(h ctxHandler) ctxHandler { return func(ctx *Context) { if ctx.Totp() != "" { h(ctx) return } ctx.Redirect(core.IndexURI, http.StatusFound) } } 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.Redirect(core.LoginURI, http.StatusFound) } } func staticHandler(ctx *Context) { var err error ctx.SetHeader("Content-Type", "text/css; charset=utf-8") ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat)) switch ctx.Path() { case core.BootstrapCSS: err = ctx.Write(ctx.Srv.res["bootstrap.css"]) case core.CustomCSS: err = ctx.Write(ctx.Srv.res["custom.css"]) case core.Favicon: ctx.SetHeader("Content-Type", "image/x-icon") err = ctx.Write(ctx.Srv.res["favicon.ico"]) } if err != nil { log.Println("static:", err) } } type registerNotFoundHandler struct { s *HTTPServer } func (nf *registerNotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { newContext(w, r, nf.s).Redirect(core.IndexURI, http.StatusFound) } func registerHandler(ctx *Context) { ctx.Template("registerHandler") ctx.Data = webData{ Title: "Register User", BodyTitle: "Register User", } switch ctx.Method() { case "GET": ctx.Exec() case "POST": 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") return } if password != repeat { ctx.Error("passwords do not match") return } if err := ctx.Srv.DB.CreateUser(user, password); err != nil { ctx.Error(err) return } ctx.SwitchHandler() } } func rawHandler(ctx *Context) { defer ctx.Log() path := strings.TrimPrefix(ctx.Path(), core.RawPrefix) f, fi, err := ctx.Srv.FS.GetFile(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 scanFile(srv *HTTPServer, path string) (err error) { err = srv.FS.AddScan(path) if err != nil { return } go func() { defer srv.FS.RemoveScan(path) l := srv.Log file, txt, err := srv.Scanner.Scan(path) if err != nil { l.Printf("scanner: %s: %s", path, err) return } tags, err := srv.DB.GetAllRTags() if err != nil { l.Printf("getTags: %s: %s", path, err) } found := tags.Match(txt) id, err := srv.DB.Index.Add(txt, found) if err != nil { l.Printf("index: %s: %s", path, err) return } err = srv.DB.NewFile(id, file, found) if err != nil { l.Printf("newFile: %s: %s", path, err) } }() return } func indexHandler(ctx *Context) { switch ctx.Method() { case "GET": var ( err error fpath string ) path := ctx.Path() if strings.HasPrefix(path, core.IndexPrefix) { fpath = strings.TrimPrefix(path, core.IndexPrefix) err = scanFile(ctx.Srv, fpath) } if strings.HasPrefix(path, core.RetagPrefix) { fpath = strings.TrimPrefix(path, core.RetagPrefix) if fpath == "" { go func() { err := ctx.Srv.DB.RetagAll() if err != nil { ctx.Srv.Log.Printf("retagAll: %s", err) } }() } else { err = ctx.Srv.DB.Retag(fpath, nil) } } p, _ := filepath.Split(fpath) p = filepath.Dir(p) if err != nil { ctx.Template("iseHandler") ctx.Data = webData{ Data: template.HTML(strings.ReplaceAll(err.Error(), "\n", "
")), Path: p, } ctx.Status(http.StatusInternalServerError) ctx.Exec() return } ctx.Redirect(p, http.StatusFound) case "POST": if ctx.Path() != core.IndexPrefix+"/all" || !ctx.CheckXsrf() { ctx.Redirect(core.IndexURI, http.StatusFound) return } go func() { for _, f := range ctx.Srv.FS.RecursiveFiles("/") { if !ctx.Srv.DB.IsIndexed(f) { scanFile(ctx.Srv, f) } } }() ctx.Redirect(core.IndexURI, http.StatusFound) } } func uploadHandler(ctx *Context) { path := strings.TrimPrefix(ctx.Path(), core.UploadPrefix) ctx.Data = webData{ Title: "Upload", BodyTitle: "Upload", } ctx.Data.Path = path ctx.Template("uploadHandler") switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } f, orig, err := ctx.File("file") if err != nil { ctx.Error(err) return } newfile, err := ctx.Srv.FS.NewFile(f, path, orig, ctx.Form("name")) if err != nil { ctx.Error(err) return } ctx.Redirect(newfile, http.StatusFound) } } func newDirHandler(ctx *Context) { path := strings.TrimPrefix(ctx.Path(), core.NewPrefix) ctx.Data = webData{ Title: "Create Directory", BodyTitle: "Create Directory", } ctx.Data.Path = path ctx.Template("newDirHandler") switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } err := ctx.Srv.FS.Mkdir(path, ctx.Form("name")) if err != nil { ctx.Error(err) return } ctx.Redirect(path, http.StatusFound) } } func moveHandler(ctx *Context) { path := strings.TrimPrefix(ctx.Path(), core.MovePrefix) mode, _, err := ctx.Srv.FS.Mode(path) if err != nil { ctx.Error(err) return } switch mode { case fs.IsFile: ctx.Data = webData{ Title: "Move File", BodyTitle: "Move File", } case fs.IsDir: ctx.Data = webData{ Title: "Move Directory", BodyTitle: "Move Directory", } default: ctx.Data = webData{ Title: "Move", BodyTitle: "Move", } ctx.Error(noSuchFile) return } ctx.Data.Path = path ctx.Data.Paths = ctx.Srv.FS.Recursive(path) _, ctx.Data.Data = filepath.Split(path) ctx.Template("moveHandler") switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } name := ctx.Form("name") dest := ctx.Form("destination") moved, newfile, err := ctx.Srv.FS.Move(path, dest, name) if err != nil { ctx.Error(err) return } for _, m := range moved { err = ctx.Srv.DB.MoveFile(m.Old, m.New) if err != nil { log.Printf("move: %s: %s", m, err) continue } log.Debugf("move: %s to %s", m.Old, m.New) } ctx.Redirect(newfile, http.StatusFound) } } func deleteHandler(ctx *Context) { path := strings.TrimPrefix(ctx.Path(), core.DeletePrefix) mode, enoent, err := ctx.Srv.FS.Mode(path) if !enoent && 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: if ctx.Method() == "GET" { ctx.Data = webData{ Title: "Delete", BodyTitle: "Delete", } ctx.Error(noSuchFile) return } } ctx.Data.Path = path ctx.Data.Data = mode ctx.Template("deleteHandler") switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } p, _ := filepath.Split(path) p = filepath.Dir(p) clean := ctx.Form("clean") if clean == "true" { f := fs.Clean(path) id, _, err := ctx.Srv.DB.DeleteFile(f) if err != nil { log.Printf("db: delete: %s %s", f, err) } if id == "" { ctx.Redirect(p, http.StatusFound) return } log.Debugf("delete %s id: %s from index", f, id) err = ctx.Srv.DB.Index.Delete(id) if err != nil { log.Printf("index: delete: %s %s", f, err) } ctx.Redirect(p, http.StatusFound) return } 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) } if id == "" { continue } log.Debugf("delete %s id: %s from index", f, id) err = ctx.Srv.DB.Index.Delete(id) if err != nil { log.Printf("index: delete: %s %s", f, err) } } ctx.Redirect(p, http.StatusFound) } } func dirHandler(ctx *Context) { ctx.Template("dirHandler") path := ctx.Path() switch ctx.Method() { case "GET": mode, enoent, err := ctx.Srv.FS.Mode(path) if !enoent && err != nil { ctx.Error(err) return } switch mode { case fs.IsDir: dir := ctx.Srv.FS.ReadDir(path) ctx.Data = webData{ Title: "Directory Viewer | " + path, } ctx.Srv.DB.GetIndexed(dir.Files) ctx.Data.Path = path ctx.Data.Data = dir ctx.Data.Paths = fs.Paths(path) ctx.Exec() case fs.IsFile: ctx.Data = webData{ Title: "Document Viewer | " + path, } ctx.Template("fileHandler") f := ctx.Srv.DB.IsIndexed(path) ctx.Data.Data = core.Path{ Name: path, Abs: core.RawPrefix + path, Flag: f, Scan: ctx.Srv.FS.CheckScan(path), } ctx.Data.Paths = fs.Paths(path) if f { ctx.Data.Tags, err = ctx.Srv.DB.GetTag(path) if err != nil { ctx.Error(err) return } } ctx.Exec() default: p := fs.Clean(path) if enoent && ctx.Srv.DB.IsIndexed(p) { ctx.Template("enoentHandler") ctx.Data = webData{ Title: "Stalled File", } ctx.Data.Path = p ctx.Exec() return } ctx.Error(err) } } } func loginHandler(ctx *Context) { if ctx.LoggedOn() { ctx.Redirect(core.IndexURI, http.StatusFound) return } ctx.Template("loginHandler") ctx.Data = webData{ Title: "Login", BodyTitle: "Login", } var data loginData switch ctx.Method() { case "GET": data.Referer = ctx.RefererURI() ctx.Data.Data = data ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } data.User = ctx.Form("user") data.Referer = ctx.Form("referer") ctx.Data.Data = data if data.Referer == "" { data.Referer = core.IndexURI } password := ctx.Form("password") if data.User == "" || password == "" { ctx.Error("empty username or password") return } u, err := ctx.Srv.DB.Login(data.User, password) if err != nil { log.Println("login:", data.User, err) ctx.Error("bad username or password") return } if u.Secret == "" { log.Println("login:", u.Username) ctx.Login(u, ctx.Form("remember")) ctx.Redirect(data.Referer, http.StatusFound) return } ctx.SetCookie(jwt.Claims{ totpClaim: u.Username, rememberClaim: ctx.Form("remember"), refererClaim: data.Referer, }, time.Minute) ctx.Redirect(core.TotpURI, http.StatusFound) } } func loginTotpHandler(ctx *Context) { if ctx.LoggedOn() { ctx.Redirect(core.IndexURI, http.StatusFound) return } ctx.Template("loginTotpHandler") ctx.Data = webData{ Title: "TOTP Verification", BodyTitle: "TOTP Veriftcation", } switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } pin := ctx.Form("pin") if pin == "" { ctx.Error("empty pin") return } u, err := ctx.Srv.DB.Totp(pin) if err != nil { ctx.Error(err) return } ref := ctx.Referer() log.Println("login:", u.Username) ctx.Login(u, ctx.Remember()) ctx.Redirect(ref, http.StatusFound) } } func searchHandler(ctx *Context) { ctx.Template("searchHandler") ctx.Data = webData{ Title: "Search", BodyTitle: "Search", } m := ctx.Method() switch m { case "GET", "POST": if m == "POST" && !ctx.CheckXsrf() { return } q := ctx.GetForm("query") ctx.Data.Query = q if q == "" { q = `""` } res, err := ctx.Srv.DB.Index.Search(q) if err != nil { ctx.Error(err) return } ctx.Srv.DB.GetPaths(res) ctx.Data.Data = res ctx.Exec() } } func userEditHandler(ctx *Context) { ctx.Template("userEditHandler") ctx.Data = webData{ Title: "Edit User", BodyTitle: "Edit User", } u, err := ctx.Srv.DB.GetUserWithoutPassword() if err != nil { ctx.Error(err) return } ctx.Data.Data = u switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } password := ctx.Form("password") repeat := ctx.Form("repeat") if password != repeat { ctx.Error("passwords do not match") return } if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil { ctx.Error(err) return } log.Printf("update: %s updated\n", ctx.User()) ctx.Redirect(core.IndexURI, http.StatusFound) } } func userTotpHandler(ctx *Context) { ctx.Template("userTotpHandler") ctx.Data = webData{ Title: "TOTP", BodyTitle: "TOTP", } req := otp.Request{Username: ctx.User()} u, err := ctx.Srv.DB.GetUser() if err != nil { ctx.Error(err) return } if u.Secret == "" { switch ctx.Method() { case "GET": req, err = otp.New(req.Username) if err != nil { ctx.Error(err) return } case "POST": req.URL = ctx.Form("url") req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL) if err != nil { ctx.Error(err) return } } } switch ctx.Method() { case "GET": ctx.Data.Data = req ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } pin := ctx.Form("pin") if pin == "" { ctx.Error("empty pin") return } ctx.Data.Data = req secret := req.Secret if u.Secret != "" { secret = u.Secret } user := ctx.User() if !otp.Validate(pin, secret) { ctx.Error("validation failed") return } if err := ctx.Srv.DB.UpdateUserSecret(req.Secret); err != nil { ctx.Error(err) return } if req.Secret == "" { log.Printf("totp: disabled for %s\n", user) } else { log.Printf("totp: enabled for %s\n", user) } ctx.Redirect("/user/edit", http.StatusFound) } } func logsHandler(ctx *Context) { ctx.Template("logsHandler") ctx.Data = webData{ Title: "Scan Logs", BodyTitle: "Scan Logs", } switch ctx.Method() { case "GET": ctx.Data.Data = ctx.Srv.Log.Logs() ctx.Exec() } } func tagsHandler(ctx *Context) { ctx.Template("tagsHandler") ctx.Data = webData{ Title: "Tags", BodyTitle: "Tags", } switch ctx.Method() { case "GET": t, err := ctx.Srv.DB.GetAllTags() if err != nil { ctx.Error(err) return } ctx.Data.Data = t ctx.Exec() } } func tagsNewHandler(ctx *Context) { ctx.Template("tagsNewHandler") ctx.Data = webData{ Title: "New Tag", BodyTitle: "New Tag", } switch ctx.Method() { case "GET": ctx.Data.Data = core.Tag{} ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } var t core.Tag t.Name = ctx.Form("name") t.Regex = ctx.Form("regex") ctx.Data.Data = t if t.Name == "" { ctx.Error("name cannot be empty") return } if t.Regex == "" { ctx.Error("regex cannot be empty") return } if err := ctx.Srv.DB.NewTag(t.Name, t.Regex); err != nil { ctx.Error(err) return } log.Printf("tag %s created by: %s\n", t.Name, ctx.User()) ctx.Redirect(core.TagsURI, http.StatusFound) } } func tagsEditHandler(ctx *Context) { ctx.Template("tagsEditHandler") ctx.Data = webData{ Title: "Edit Tag", BodyTitle: "Edit Tag", } name := ctx.Var("tag") switch ctx.Method() { case "GET": t, err := ctx.Srv.DB.GetTags(name) if err != nil { ctx.Error(err) return } ctx.Data.Data = t ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } var t core.Tag t.Name = name t.Regex = ctx.Form("regex") ctx.Data.Data = t if t.Regex == "" { ctx.Error("regex cannot be empty") return } if err := ctx.Srv.DB.NewTag(t.Name, t.Regex); err != nil { ctx.Error(err) return } log.Printf("tag %s updated by: %s\n", t.Name, ctx.User()) ctx.Redirect(core.TagsURI, http.StatusFound) } } func tagsDelHandler(ctx *Context) { ctx.Template("tagsDelHandler") ctx.Data = webData{ Title: "Delete Tag", BodyTitle: "Delete Tag", } name := ctx.Var("tag") ctx.Data.Data = name switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } if err := ctx.Srv.DB.DeleteTag(name); err != nil { ctx.Error(err) return } log.Printf("tag %s deleted by: %s\n", name, ctx.User()) ctx.Redirect(core.TagsURI, http.StatusFound) } } func logoutHandler(ctx *Context) { user := ctx.User() if user == "" { user = ctx.Totp() + " (totp)" } log.Println("logout:", user) ctx.Srv.JWT.Invalidate(&ctx.Token) ctx.SetCookie(nil, 0) ctx.Redirect(core.LoginURI, http.StatusFound) }