docstore/pkg/server/context.go
2021-12-04 18:02:27 +01:00

306 lines
6.3 KiB
Go

// Copyright (C) 2021 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"
"io"
"net/http"
"net/url"
"reflect"
"strconv"
"time"
)
const (
day = time.Hour * 24
max = day * 90
)
const (
userClaim = "user"
totpClaim = "totp"
rememberClaim = "remember"
refererClaim = "referer"
)
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
h := w.Header()
h.Set("X-Frame-Options", "sameorigin")
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-XSS-Protection", "1; mode=block")
h.Set("Content-Security-Policy", "default-src 'none';object-src 'self';frame-src 'self';style-src 'self';img-src 'self' data:;media-src 'self';connect-src 'self';frame-ancestors 'self'")
h.Set("Referrer-Policy", "same-origin")
return &Context{
Request: r,
Response: w,
Srv: s,
Time: time.Now(),
}
}
type Context struct {
Request *http.Request
Response http.ResponseWriter
Srv *HTTPServer
T *template.Template
HTTPStatus int
Err error
Time time.Time
Token jwt.Token
Data webData
}
type webData struct {
Version string
Time string
Title string
BodyTitle string
Login bool
NotFound string
User string
Token string
Msg string
Query string
Path string
Paths core.Paths
Tags []string
Created string
Data interface{}
}
type loginData struct {
User string
Referer string
}
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()
t := strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e6)) + "ms"
if t == "0ms" {
t = strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e5)) + "µs"
}
c.Data.Time = t
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)
}
}
func (c *Context) Log() {
if c.HTTPStatus == 0 {
c.HTTPStatus = int(reflect.Indirect(reflect.ValueOf(c.Response)).FieldByName("status").Int())
}
if c.Err != nil {
log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus, c.Err)
return
}
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus)
}
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.Exec()
}
func (c *Context) Forbidden() {
c.Template("forbiddenHandler")
c.Data = webData{Title: "403"}
c.Status(http.StatusForbidden)
c.Exec()
}
func (c *Context) SwitchHandler() {
log.Debug("server: switching to normal handler")
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) Status(status int) {
c.HTTPStatus = status
c.Response.WriteHeader(status)
}
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) GetForm(name string) string {
return c.Request.FormValue(name)
}
func (c *Context) Var(name string) (ret string) {
ret = mux.Vars(c.Request)[name]
return
}
func (c *Context) File(name string) (io.Reader, string, error) {
var filename string
f, fh, err := c.Request.FormFile(name)
if fh != nil {
filename = fh.Filename
}
return f, filename, err
}
// 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)