general improvements

This commit is contained in:
ston1th 2018-10-27 21:28:19 +02:00
commit ddb11f0619
25 changed files with 305 additions and 140 deletions

View file

@ -55,7 +55,7 @@ func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Con
ctx.Token = *t
return
}
ctx.Redirect(core.TotpURI, 302)
ctx.Redirect(core.TotpURI, http.StatusFound)
return nil
}
@ -114,7 +114,7 @@ func (c *Context) Exec() {
}
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
c.Data.Version = c.Srv.Version
c.Data.Version = c.Srv.Config.Version
if c.Data.BodyTitle == "" {
c.Data.BodyTitle = c.Data.Title
}
@ -162,6 +162,14 @@ func (c *Context) NotFound() {
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) Template(name string) {
c.T = c.Srv.templ[name]
}
@ -221,7 +229,7 @@ func (c *Context) LoggedOn() (ok bool) {
}
func (c *Context) LogSetCookie(msg string, err error) {
log.Println(msg, err)
log.Printf("ctx: %s %s\n", msg, err)
c.SetCookie(nil)
}
@ -240,7 +248,7 @@ func (c *Context) SetCookieToken(t *jwt.Token) {
Value: c.Token.String(),
Path: "/",
MaxAge: int(jwt.DefaultExpiry.Seconds()),
Secure: c.Srv.Secure,
Secure: c.Srv.Config.SecureCookie,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}

View file

@ -26,7 +26,7 @@ func totpAuthHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
@ -36,7 +36,7 @@ func adminAuthHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
ctx.NotFound()
ctx.Forbidden()
}
}
@ -50,7 +50,7 @@ func userAuthHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
ctx.NotFound()
ctx.Forbidden()
}
}
@ -60,7 +60,7 @@ func authHandler(h ctxHandler) ctxHandler {
h(ctx)
return
}
ctx.NotFound()
ctx.Forbidden()
}
}
@ -80,12 +80,12 @@ func staticHandler(ctx *Context) {
}
func indexHandler(ctx *Context) {
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
}
func loginHandler(ctx *Context) {
if ctx.LoggedOn() {
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
return
}
ctx.Template("loginHandler")
@ -93,17 +93,20 @@ func loginHandler(ctx *Context) {
Title: "Login",
BodyTitle: "Login",
}
var user string
switch ctx.Method() {
case "GET":
ctx.Data.Data = user
ctx.Exec()
case "POST":
if !ctx.CheckXsrf() {
return
}
user := ctx.Form("user")
user = ctx.Form("user")
ctx.Data.Data = user
password := ctx.Form("password")
if user == "" || password == "" {
ctx.Error("wrong inputs")
ctx.Error("empty user or password")
return
}
u, err := ctx.Srv.DB.Login(user, password)
@ -112,21 +115,22 @@ func loginHandler(ctx *Context) {
return
}
if u.Secret == "" {
log.Println("login:", u.Username)
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
return
}
ctx.SetCookieToken(jwt.NewToken(map[string]interface{}{
totpClaim: u.Username,
jwt.ExpClaim: jwt.NewExp(time.Minute),
}, nil))
ctx.Redirect(core.TotpURI, 302)
ctx.Redirect(core.TotpURI, http.StatusFound)
}
}
func loginTotpHandler(ctx *Context) {
if ctx.LoggedOn() {
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
return
}
ctx.Template("loginTotpHandler")
@ -143,7 +147,7 @@ func loginTotpHandler(ctx *Context) {
}
pin := ctx.Form("pin")
if pin == "" {
ctx.Error("wrong inputs")
ctx.Error("empty pin")
return
}
u, err := ctx.Srv.DB.Totp(ctx.Totp(), pin)
@ -151,8 +155,9 @@ func loginTotpHandler(ctx *Context) {
ctx.Error(err)
return
}
log.Println("login:", u.Username)
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
@ -214,6 +219,14 @@ func pageNewHandler(ctx *Context) {
Perm: perm,
}
ctx.Data.Data = p
if title == "" {
ctx.Error("empty title")
return
}
if perm == core.Invalid {
ctx.Error("invalid permission")
return
}
if section != core.WikiSection {
section = ctx.User()
@ -223,7 +236,7 @@ func pageNewHandler(ctx *Context) {
ctx.Error(err)
return
}
ctx.Redirect(page.StoreTitle, 302)
ctx.Redirect(page.StoreTitle, http.StatusFound)
}
}
@ -237,19 +250,19 @@ func pageHandler(ctx *Context) {
if key := ctx.Var("key"); key != "" {
t, err := jwt.DecodeToken(key)
if err != nil {
log.Println("DecodeToken:", err)
log.Println("share: DecodeToken:", err)
ctx.NotFound()
return
}
if err = ctx.Srv.JWT.Verify(t); err != nil {
log.Println("VerifyToken:", err)
log.Println("share: VerifyToken:", err)
ctx.NotFound()
return
}
user = t.Claims.GetString(sharedClaim)
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(createdClaim)) {
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
log.Println("Invalidate:", err)
log.Println("share: Invalidate:", err)
}
ctx.NotFound()
return
@ -310,7 +323,7 @@ func pageShareHandler(ctx *Context) {
return
}
d, err := time.ParseDuration(ctx.Form("duration"))
d, err := duration(ctx.Form("duration"), ctx.Form("mode"))
if err != nil {
ctx.Error(err)
return
@ -324,7 +337,7 @@ func pageShareHandler(ctx *Context) {
}, nil)
if err = ctx.Srv.JWT.Sign(t); err != nil {
log.Println("SignToken:", err)
log.Println("share: SignToken:", err)
ctx.NotFound()
return
}
@ -404,7 +417,7 @@ func pageEditHandler(ctx *Context) {
ctx.Error(err)
return
}
ctx.Redirect(page.StoreTitle, 302)
ctx.Redirect(page.StoreTitle, http.StatusFound)
}
}
@ -426,7 +439,7 @@ func pageDelHandler(ctx *Context) {
ctx.Error(err)
return
}
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
@ -471,15 +484,20 @@ func userNewHandler(ctx *Context) {
adm = true
}
if user == "" || password == "" || password != repeat {
ctx.Error("wrong inputs")
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
}
ctx.Redirect("/user", 302)
log.Printf("create: %s created by %s\n", user, ctx.User())
ctx.Redirect("/user", http.StatusFound)
}
}
@ -511,8 +529,8 @@ func userEditHandler(ctx *Context) {
adm = true
}
if user == "" || password != repeat {
ctx.Error("wrong inputs")
if password != repeat {
ctx.Error("passwords do not match")
return
}
if ctx.Admin() {
@ -520,17 +538,16 @@ func userEditHandler(ctx *Context) {
ctx.Error(err)
return
}
} else {
if user == ctx.User() {
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
ctx.Error(err)
return
}
ctx.Redirect(core.IndexURI, 302)
return
}
log.Printf("update: %s updated by %s\n", user, ctx.User())
ctx.Redirect("/user", http.StatusFound)
return
}
ctx.Redirect("/user", 302)
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)
}
}
@ -558,7 +575,7 @@ func userTotpHandler(ctx *Context) {
req.URL = ctx.Form("url")
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
if err != nil {
ctx.Error("wrong inputs")
ctx.Error(err)
return
}
}
@ -582,7 +599,8 @@ func userTotpHandler(ctx *Context) {
secret = u.Secret
}
valid := otp.Validate(pin, secret)
if ctx.User() != req.Username && ctx.Admin() {
user := ctx.User()
if user != req.Username && ctx.Admin() {
valid = true
}
@ -594,7 +612,12 @@ func userTotpHandler(ctx *Context) {
ctx.Error(err)
return
}
ctx.Redirect("/user/edit/"+req.Username, 302)
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)
}
}
@ -603,7 +626,8 @@ func userUnlockHandler(ctx *Context) {
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
log.Println("unlock:", err)
}
ctx.Redirect("/user", 302)
log.Printf("unlock: %s unlocked by %s\n", user, ctx.User())
ctx.Redirect("/user", http.StatusFound)
}
func userDelHandler(ctx *Context) {
@ -625,15 +649,23 @@ func userDelHandler(ctx *Context) {
if !ctx.CheckXsrf() {
return
}
if err := ctx.Srv.DB.DeleteUser(ctx.Var("user")); err != nil {
self := ctx.User()
user := ctx.Var("user")
if err := ctx.Srv.DB.DeleteUser(user); err != nil {
log.Println("delete:", err)
}
ctx.Redirect("/user", 302)
log.Printf("delete: %s deleted by %s\n", user, self)
if user == self {
ctx.Redirect("/logout", http.StatusFound)
return
}
ctx.Redirect("/user", http.StatusFound)
}
}
func logoutHandler(ctx *Context) {
log.Println("logout:", ctx.User())
ctx.Srv.JWT.Invalidate(&ctx.Token)
ctx.SetCookie(nil)
ctx.Redirect(core.IndexURI, 302)
ctx.Redirect(core.IndexURI, http.StatusFound)
}

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

@ -0,0 +1,47 @@
// Copyright (C) 2018 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) {
var m int
m, _ = strconv.Atoi(mode)
switch m {
case 0:
err = errors.New("invalid mode")
return
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)
}
if d < min {
err = errors.New("minimum duration is 30 seconds")
}
if d > max {
err = errors.New("maximum duration is 90 days")
}
return
}

View file

@ -71,16 +71,10 @@ var routes = []route{
},
{
"/user/del/{user:[a-zA-Z0-9]+$}",
adminAuthHandler(
userAuthHandler(
userDelHandler),
[]string{"GET", "POST"},
},
{
"/user/unlock/{user:[a-zA-Z0-9]+$}",
adminAuthHandler(
userUnlockHandler),
[]string{"POST"},
},
{
"/user/edit/{user:[a-zA-Z0-9]+$}",
userAuthHandler(
@ -93,6 +87,12 @@ var routes = []route{
userTotpHandler),
[]string{"GET", "POST"},
},
{
"/user/unlock/{user:[a-zA-Z0-9]+$}",
adminAuthHandler(
userUnlockHandler),
[]string{"POST"},
},
{
"/logout",
logoutHandler,

View file

@ -23,10 +23,7 @@ const (
)
type HTTPServer struct {
Version string
Secure bool
Secret string
DataDir string
Config core.Config
listener net.Listener
srv *http.Server
@ -38,12 +35,9 @@ type HTTPServer struct {
res map[string][]byte
}
func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) {
func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
srv = &HTTPServer{
Version: conf.Version,
Secure: conf.SecureCookie,
Secret: conf.Secret,
DataDir: conf.DataDir,
Config: cfg,
listener: l,
}
srv.loadTemplates()
@ -54,7 +48,7 @@ func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) {
}
func (s *HTTPServer) Start() (err error) {
s.DB, err = db.New(s.DataDir)
s.DB, err = db.New(s.Config)
if err != nil {
return errors.New("db: " + err.Error())
}
@ -62,8 +56,8 @@ func (s *HTTPServer) Start() (err error) {
if err != nil {
return
}
if s.Secret != "" {
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Secret))
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)
}

View file

@ -42,6 +42,23 @@ const (
</ul>
</div>
{{end}}
{{end}}`
forbidden = `{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>403 - Forbidden</strong>
</div>
<div class="card-body">
<h5>Access to this page is restricted</h5>
<a href="/" class="btn btn-sm btn-primary">Back</a>
</div>
</div>
</div>
</div>
</div>
{{end}}`
index = `<!DOCTYPE html>
<html>
@ -99,7 +116,7 @@ const (
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" autofocus required>
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data}}" autofocus required>
</div>
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
@ -168,10 +185,10 @@ const (
<div class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>404</strong>
<strong>404 - Page Not Found</strong>
</div>
<div class="card-body">
<h4>Page not found</h4>
<h5>The requested page could not be found</h5>
<a href="/" class="btn btn-sm btn-primary">Back</a>
</div>
</div>
@ -252,7 +269,8 @@ const (
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
</form>
{{end}}`
page = `{{define "body"}}
@ -293,7 +311,7 @@ const (
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
{{end}}
{{if ne .Data.StoreTitle "wiki/Index"}}
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-primary">Delete</a>
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
{{end}}
{{end}}
</div>
@ -304,7 +322,7 @@ const (
pageMD = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
</div>
</div>
<div class="row mt-3">
@ -348,11 +366,10 @@ const (
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
<button class="btn btn-sm btn-primary" type="submit">Create</button>
</form>
{{end}}`
pageShare = `{{define "body"}}
{{if not .Msg}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 offset-4">
@ -366,15 +383,27 @@ const (
<label class="col-form-label" for="key">Key</label>
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
</div>
<a href="/view/{{.Key}}">Share Link</a>
<a href="/view/{{.Key}}" class="btn btn-sm btn-danger">Share Link</a>
<a href="/{{.Data}}/share" class="btn btn-sm btn-primary">Back</a>
{{else}}
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="duration">Lifetime</label>
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="1d (day) 10m (minutes)" autofocus required>
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="min: 30s max: 90d" autocomplete="off" autofocus required>
</div>
<div class="form-group">
<div class="custom-control custom-radio">
<input type="radio" id="mode1" name="mode" value="1" class="custom-control-input" checked="">
<label class="custom-control-label" for="mode1">Parse (15m, 10h)</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="mode2" name="mode" value="2" class="custom-control-input">
<label class="custom-control-label" for="mode2">Day</label>
</div>
</div>
<button class="btn btn-sm btn-primary" type="submit">Share</button>
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
</form>
{{end}}
</div>
@ -382,7 +411,6 @@ const (
</div>
</div>
</div>
{{end}}
{{end}}`
pageView = `{{define "body"}}
<div class="page-header">
@ -512,12 +540,13 @@ const (
{{end}}
</div>
{{end}}
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
<button class="btn btn-sm btn-primary" type="submit">Update</button>
{{if .Admin}}
<a href="/user" class="btn btn-sm btn-primary">Back</a>
{{else}}
<a href="/" class="btn btn-sm btn-primary">Back</a>
{{end}}
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
</form>
{{if .Admin}}
{{if eq .Data.Locked 3}}
@ -555,7 +584,7 @@ const (
<thead>
<tr>
<th>Username</th>
<th>Admin</th>
<th>Flags</th>
<th>Options</th>
</tr>
</thead>
@ -564,8 +593,12 @@ const (
{{$admin := .Admin}}
{{range $item := .Data}}
<tr>
<td>{{$item.Username}} {{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}</td>
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
<td>{{$item.Username}}</td>
<td>
{{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}
{{if eq $item.Locked 3}}<span class="badge badge-warning">Locked</span>{{end}}
{{if $item.Admin}}<span class="badge badge-danger">Admin</span>{{end}}
</td>
<td>
<div class="btn-group">
{{if $admin}}
@ -612,7 +645,7 @@ const (
<label class="col-form-label" for="admin">Admin Privileges</label>
<input type="checkbox" id="admin" name="admin" value="0">
</div>
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
<button class="btn btn-sm btn-primary" type="submit">Create</button>
<a href="/user" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
@ -637,7 +670,7 @@ const (
</div>
<div class="form-group">
<label class="col-form-label" for="secret">Secret</label>
<p id="secret">{{.Data.Secret}}</p>
<pre><code id="secret">{{.Data.Secret}}</code></pre>
</div>
{{end}}
<div class="form-group">
@ -836,6 +869,7 @@ func (s *HTTPServer) loadTemplates() {
// pages
s.templ["notFoundHandler"] = parse(index, menu, notFound)
s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
s.templ["loginHandler"] = parse(index, menu, login)
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
s.templ["searchHandler"] = parse(index, menu, search)