// Copyright (C) 2018 Marius Schellenberger package server import ( "errors" "git.giftfish.de/ston1th/gowiki/pkg/log" "git.giftfish.de/ston1th/jwt/v3" "github.com/gorilla/mux" "html/template" "net/http" "time" ) const ( userClaim = "user" adminClaim = "admin" createdClaim = "created" sharedClaim = "shared" sectionClaim = "section" titleClaim = "title" ) 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';frame-ancestors 'none'") ctx = &Context{ Request: r, Response: w, Srv: s, Time: time.Now(), } if c, err := r.Cookie(cookieName); err == nil { t, err := jwt.DecodeToken(c.Value) if err != nil { log.Println("DecodeToken:", err) } else { if err = s.JWT.Verify(t); err != nil { log.Println("VerifyToken:", err) } else { if !s.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(createdClaim)) { ctx.Token = *t return } if err = s.JWT.Invalidate(t); err != nil { log.Println("Invalidate:", err) } } } } ctx.SetCookie(nil) 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 Admin bool Login bool Key string User string Token string Msg string Search string Data interface{} } 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.Version if c.Data.BodyTitle == "" { c.Data.BodyTitle = c.Data.Title } c.Data.Admin = c.Admin() c.Data.User = c.User() c.Data.Login = c.LoggedOn() c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e5 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) 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) 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) SetCookie(claims map[string]interface{}) { t := jwt.NewToken(claims, nil) 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(jwt.DefaultExpiry.Seconds()), Secure: c.Srv.Secure, HttpOnly: true, SameSite: http.SameSiteStrictMode, } if cookie.MaxAge > 0 { d := time.Duration(cookie.MaxAge) * time.Second 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) Admin() bool { return c.Token.Claims.GetBool(adminClaim) } type ctxHandler func(*Context)