first working version

This commit is contained in:
ston1th 2019-04-28 18:16:40 +02:00
commit de359ab415
47 changed files with 1016 additions and 2012 deletions

View file

@ -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)
}
}