initial commit

This commit is contained in:
ston1th 2019-04-19 15:45:28 +02:00
commit 18995db757
871 changed files with 492725 additions and 0 deletions

312
pkg/server/context.go Normal file
View file

@ -0,0 +1,312 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"errors"
"git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/jwt/v3"
"github.com/gorilla/mux"
"html/template"
"net/http"
"net/url"
"time"
)
const (
userClaim = "user"
totpClaim = "totp"
rememberClaim = "remember"
refererClaim = "referer"
)
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) {
h := w.Header()
h.Set("X-Frame-Options", "DENY")
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("Referrer-Policy", "same-origin")
ctx = &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
Token jwt.Token
Data webData
}
type webData struct {
Version string
Time int64
Title string
BodyTitle string
Login bool
Key string
User string
Token string
Msg string
Search string
Data interface{}
}
type loginData struct {
User string
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()
if c.T == nil {
c.Status = http.StatusInternalServerError
c.Err = errors.New("template is nil")
return
}
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
c.Data.Version = c.Srv.Config.Version
if c.Data.BodyTitle == "" {
c.Data.BodyTitle = c.Data.Title
}
c.Data.User = c.User()
c.Data.Login = c.LoggedOn()
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
if c.Data.Msg != "" {
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
}
}
func (c *Context) log() {
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)
return
}
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
}
func (c *Context) Error(i interface{}) {
switch v := i.(type) {
case string:
c.Data.Msg = v
case error:
c.Data.Msg = v.Error()
}
c.Exec()
}
func (c *Context) NotFound() {
c.Template("notFoundHandler")
c.Data = webData{Title: "404"}
c.Status = http.StatusNotFound
c.Response.WriteHeader(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.Exec()
}
func (c *Context) SwitchHandler() {
c.Srv.srv.Handler = c.Srv.handler
c.Redirect(core.IndexURI, http.StatusFound)
}
func (c *Context) Template(name string) {
c.T = c.Srv.templ[name]
}
func (c *Context) Write(buf []byte) (err error) {
_, err = c.Response.Write(buf)
return
}
func (c *Context) SetHeader(name, value string) {
c.Response.Header().Set(name, value)
}
func (c *Context) Redirect(uri string, code int) {
if len(uri) > 0 && uri[0] != '/' {
uri = "/" + uri
}
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
http.Redirect(c.Response, c.Request, uri, code)
}
func (c *Context) Method() string {
return c.Request.Method
}
func (c *Context) Path() string {
return c.Request.URL.Path
}
func (c *Context) RefererURI() string {
u, err := url.Parse(c.Request.Header.Get("Referer"))
if err != nil {
return ""
}
return u.RequestURI()
}
func (c *Context) FormSlice(name string) []string {
// does nothing if called twice
c.Request.ParseForm()
return c.Request.PostForm[name]
}
func (c *Context) Form(name string) string {
return c.Request.PostFormValue(name)
}
func (c *Context) Var(name string) (ret string) {
ret, _ = mux.Vars(c.Request)[name]
return
}
// CheckXsrf validates the xsrf token
func (c *Context) CheckXsrf() (ok bool) {
ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
if !ok {
c.Error("wrong csrf token")
}
return
}
func (c *Context) LoggedOn() (ok bool) {
_, ok = c.Token.Claims.Get(userClaim)
return
}
func (c *Context) LogSetCookie(msg string, err error) {
log.Printf("ctx: %s %s\n", msg, err)
c.SetCookie(nil, 0)
}
func (c *Context) Login(u core.User, remember string) {
var d time.Duration
if remember != "" {
d = day * 7
}
c.SetCookie(jwt.Claims{userClaim: u.Username}, d)
}
func (c *Context) SetCookie(claims jwt.Claims, d time.Duration) {
c.setCookie(jwt.NewToken(claims, nil), d)
}
func (c *Context) setCookie(t *jwt.Token, d time.Duration) {
if d == 0 {
d = jwt.DefaultExpiry
}
t.Claims[jwt.ExpClaim] = jwt.NewExp(d)
if err := c.Srv.JWT.Sign(t); err != nil {
log.Println(err)
return
}
c.Token = *t
cookie := &http.Cookie{
Name: cookieName,
Value: c.Token.String(),
Path: "/",
MaxAge: int(d.Seconds()),
Secure: c.Srv.Config.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
if cookie.MaxAge > 0 {
cookie.Expires = time.Now().Add(d)
} else if cookie.MaxAge < 0 {
cookie.Expires = time.Unix(1, 0)
}
http.SetCookie(c.Response, cookie)
}
func (c *Context) User() string {
return c.Token.Claims.GetString(userClaim)
}
func (c *Context) Totp() string {
return c.Token.Claims.GetString(totpClaim)
}
func (c *Context) Remember() string {
return c.Token.Claims.GetString(rememberClaim)
}
func (c *Context) Referer() string {
return c.Token.Claims.GetString(refererClaim)
}
type ctxHandler func(*Context)

864
pkg/server/handler.go Normal file
View file

@ -0,0 +1,864 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/otp"
"git.giftfish.de/ston1th/jwt/v3"
"net/http"
"time"
)
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 authHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.LoggedOn() {
h(ctx)
return
}
ctx.Forbidden()
}
}
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 "/bootstrap.css":
err = ctx.Write(ctx.Srv.res["bootstrap.css"])
case "/custom.css":
err = ctx.Write(ctx.Srv.res["custom.css"])
case "/favicon.ico":
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":
if !ctx.CheckXsrf() {
return
}
user := ctx.Form("user")
password := ctx.Form("password")
repeat := ctx.Form("repeat")
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 indexHandler(ctx *Context) {
ctx.Redirect(core.IndexURI, http.StatusFound)
}
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
}
search := ctx.Form("search")
res, err := ctx.Srv.DB.Index.Search(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)
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.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)
}
}
func userEditHandler(ctx *Context) {
ctx.Template("userEditHandler")
ctx.Data = webData{
Title: "Edit User",
BodyTitle: "Edit User",
}
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Error(err)
return
}
ctx.Data.Data = u
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
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 {
ctx.Error(err)
return
}
log.Printf("update: %s updated by %s\n", user, 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(req.Username)
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
}
valid := otp.Validate(pin, secret)
user := ctx.User()
if user != req.Username && ctx.Admin() {
valid = true
}
if !valid {
ctx.Error("validation failed")
return
}
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
ctx.Error(err)
return
}
if req.Secret == "" {
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
} else {
log.Printf("totp: enabled by %s for %s\n", user, req.Username)
}
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)
}
}
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)
}

45
pkg/server/helper.go Normal file
View file

@ -0,0 +1,45 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"errors"
"strconv"
"time"
)
const (
day = time.Hour * 24
min = time.Second * 30
max = day * 90
)
func duration(duration, mode string) (d time.Duration, err error) {
switch mode {
case "1":
d, err = time.ParseDuration(duration)
if err != nil {
err = errors.New("could not parse duration")
return
}
case "2":
var t int
t, err = strconv.Atoi(duration)
if err != nil {
err = errors.New("could not parse duration")
return
}
d = time.Duration(int(day) * t)
default:
err = errors.New("invalid mode")
return
}
if d < min {
err = errors.New("minimum duration is 30 seconds")
}
if d > max {
err = errors.New("maximum duration is 90 days")
}
return
}

99
pkg/server/routes.go Normal file
View file

@ -0,0 +1,99 @@
// Copyright (C) 2019 Marius Schellenberger
package server
type route struct {
Path string
Handler ctxHandler
Methods []string
}
var static = []route{
{
"/favicon.ico",
staticHandler,
[]string{"GET"},
},
{
"/bootstrap.css",
staticHandler,
[]string{"GET"},
},
{
"/custom.css",
staticHandler,
[]string{"GET"},
},
}
var register = append(static, route{
"/",
registerHandler,
[]string{"GET", "POST"},
})
var routes = append(static, []route{
{
"/",
authHandler(
dirHandler),
[]string{"GET"},
},
{
"/sections",
sectionsHandler,
[]string{"GET"},
},
{
"/search",
authHandler(
searchHandler),
[]string{"GET", "POST"},
},
{
"/upload",
authHandler(
searchHandler),
[]string{"GET", "POST"},
},
{
"/login",
loginHandler,
[]string{"GET", "POST"},
},
{
"/totp",
totpAuthHandler(
loginTotpHandler),
[]string{"GET", "POST"},
},
{
"/user/edit/{user:[a-zA-Z0-9]+$}",
authHandler(
userEditHandler),
[]string{"GET", "POST"},
},
{
"/user/totp/{user:[a-zA-Z0-9]+$}",
authHandler(
userTotpHandler),
[]string{"GET", "POST"},
},
{
"/logout",
logoutHandler,
[]string{"GET", "POST"},
},
{
"/{dir}",
authHandler(
dirHandler),
[]string{"GET"},
},
{
"/{dir}/{file}",
authHandler(
fileHandler),
[]string{"GET"},
},
}...)

112
pkg/server/server.go Normal file
View file

@ -0,0 +1,112 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"bytes"
"context"
"errors"
"git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/db"
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/godrop/v2"
"git.giftfish.de/ston1th/jwt/v3"
"github.com/gorilla/mux"
"html/template"
"net"
"net/http"
"time"
)
const (
cookieName = "gosession"
)
type HTTPServer struct {
Config core.Config
listener net.Listener
srv *http.Server
handler http.Handler
DB *db.DB
JWT *jwt.JWT
templ map[string]*template.Template
res map[string][]byte
}
func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
srv = &HTTPServer{
Config: cfg,
listener: l,
handler: srv.buildRoutes(),
}
srv.loadTemplates()
srv.srv = &http.Server{}
return
}
func (s *HTTPServer) Start() (err error) {
s.DB, err = db.New(s.Config)
if err != nil {
return errors.New("db: " + err.Error())
}
err = godrop.UnveilBlock()
if err != nil {
return
}
if s.Config.Secret != "" {
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Config.Secret))
} else {
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
}
if err != nil {
return errors.New("server: " + err.Error())
}
if !s.DB.UserExists() {
s.srv.Handler = s.register()
} else {
s.srv.Handler = s.handler
}
go func() {
log.Println(s.srv.Serve(s.listener))
}()
return
}
func (s *HTTPServer) Stop() error {
s.DB.Close()
s.JWT.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return s.srv.Shutdown(ctx)
}
func (s *HTTPServer) register() http.Handler {
m := mux.NewRouter()
m.NotFoundHandler = &registerNotFoundHandler{s}
for _, v := range register {
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
}
return m
}
func (s *HTTPServer) buildRoutes() http.Handler {
m := mux.NewRouter()
m.NotFoundHandler = &notFoundHandler{s}
for _, v := range routes {
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
}
return m
}
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)
}
}

1039
pkg/server/templates.go Normal file

File diff suppressed because one or more lines are too long

49
pkg/server/xsrf.go Normal file
View file

@ -0,0 +1,49 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/jwt/v3"
)
const keySize = jwt.KeySize / 2
func genKey(length int) (bytes []byte) {
bytes = make([]byte, length)
if _, err := rand.Reader.Read(bytes); err != nil {
log.Println("genKey:", err)
}
return
}
func newXsrf(secret []byte) string {
rnd := genKey(keySize)
return base64.RawURLEncoding.EncodeToString(append(rnd, xor(rnd, secret)...))
}
func checkXsrf(xsrf string, secret []byte) bool {
t, err := base64.RawURLEncoding.DecodeString(xsrf)
if err != nil {
return false
}
if len(t) != jwt.KeySize {
return false
}
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
}
func xor(a, b []byte) (c []byte) {
n := len(a)
if len(b) < n {
return nil
}
c = make([]byte, n)
for i := 0; i < n; i++ {
c[i] = a[i] ^ b[i]
}
return
}

31
pkg/server/xsrf_test.go Normal file
View file

@ -0,0 +1,31 @@
// Copyright (C) 2019 Marius Schellenberger
package server
import (
"testing"
)
func TestXsrf(t *testing.T) {
sec := genKey(keySize)
if len(sec) != keySize {
t.Fatal("len(sec) != keySize")
}
xsrf := newXsrf(sec)
if xsrf == "" {
t.Fatal("xsrf is empty")
}
if !checkXsrf(xsrf, sec) {
t.Fatal("valid xsrf check failed")
}
if checkXsrf("test", sec) {
t.Fatal("invalid xsrf check succeeded")
}
rnd := genKey(keySize)
if checkXsrf(xsrf, rnd) {
t.Fatal("invalid xsrf check succeeded")
}
}