// 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" ) //TODO add missing CSRF checks 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.Forbidden() 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 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) { path := strings.TrimPrefix(ctx.Path(), core.IndexPrefix) switch ctx.Method() { case "GET": earlyErr := make(chan error) go func() { file, txt, err := ctx.Srv.Scanner.Scan(path) if err != nil { select { case earlyErr <- 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) p = filepath.Dir(p) select { case err := <-earlyErr: ctx.Template("iseHandler") ctx.Data = webData{ Data: template.HTML(strings.ReplaceAll(err.Error(), "\n", "
")), Path: p, } ctx.Status(http.StatusInternalServerError) ctx.Exec() return case <-time.After(time.Second): } ctx.Redirect(p, 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": 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": 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": 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) 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.Template("deleteHandler") switch ctx.Method() { case "GET": 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) } if id == "" { continue } err = ctx.Srv.DB.Index.Delete(id) if err != nil { log.Printf("index: delete: %s %s", f, err) } } p, _ := filepath.Split(path) p = filepath.Dir(p) ctx.Redirect(p, http.StatusFound) } } func dirHandler(ctx *Context) { ctx.Template("dirHandler") 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: ctx.Data = webData{ Title: "Directory Viewer", } dir := ctx.Srv.FS.ReadDir(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", } ctx.Template("fileHandler") ctx.Data.Data = core.Path{ Name: path, Abs: core.RawPrefix + path, Flag: ctx.Srv.DB.IsIndexed(path), } ctx.Data.Paths = fs.Paths(path) ctx.Exec() } } } 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", } switch ctx.Method() { case "GET": ctx.Exec() case "POST": if !ctx.CheckXsrf() { return } 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 } err = ctx.Srv.DB.GetPaths(res) if err != nil { ctx.Error(err) return } 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.Var("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 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.IndexURI, http.StatusFound) }