// Copyright (C) 2021 Marius Schellenberger package server import ( "errors" "html/template" "io" "net/http" "net/url" "reflect" "strconv" "time" "git.giftfish.de/ston1th/docstore/pkg/core" "git.giftfish.de/ston1th/jwt/v3" "github.com/gorilla/mux" ) 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()) 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 { c.Srv.Log.Error(c.Err, "access", "client", c.Request.RemoteAddr, "method", c.Request.Method, "status", c.HTTPStatus, "uri", c.Request.URL.Path, ) return } c.Srv.Log.Info("access", "client", c.Request.RemoteAddr, "method", c.Request.Method, "status", c.HTTPStatus, "uri", c.Request.URL.Path, ) } 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() { c.Srv.Log.V(2).Info("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 } c.Srv.Log.Info("access", "client", c.Request.RemoteAddr, "method", c.Request.Method, "status", code, "uri", c.Request.URL.Path, "redirect", 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()) 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) { c.Srv.Log.Error(err, msg) 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 { c.Srv.Log.Error(err, "jwt sign") 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)