docstore/pkg/server/handler.go

642 lines
13 KiB
Go

// 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 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 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)
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", "<br>")),
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)
}
log.Debugf("delete %s id: %s from index", f, id)
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)
}