// Copyright (C) 2022 Marius Schellenberger package server import ( "context" "errors" "html/template" "net/http" "strconv" "time" "git.giftfish.de/ston1th/goacc/pkg/core" "git.giftfish.de/ston1th/jwt/v3" "github.com/go-logr/logr" "github.com/gorilla/mux" client "github.com/ory/hydra-client-go" ) const ( userClaim = "user" adminClaim = "admin" totpClaim = "totp" createdClaim = "created" rememberClaim = "remember" empty = "(empty)" ) func newContext(w http.ResponseWriter, r *http.Request, s *Server) *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") return &Context{ log: s.log, Request: r, Response: w, Srv: s, Time: time.Now(), } } type Context struct { log logr.Logger Request *http.Request Response http.ResponseWriter Srv *Server T *template.Template Status int Err error Time time.Time Token jwt.Token Data webData } type webData struct { Version string Time string Title string BodyTitle string Admin bool Login bool Key string User string Token string Msg string Data interface{} } type loginData struct { User string LoginChallenge string Referer string } type consentData struct { ConsentChallenge string Info *client.ConsentRequest } func (c *Context) Exec() { defer c.accessLog() if c.T == nil { c.Status = http.StatusInternalServerError c.Err = errors.New("template is nil") return } sig := c.Token.RawSig() if len(sig) >= keySize { c.Data.Token = newXsrf(sig[:keySize]) } c.Data.Version = c.Srv.Config.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() 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 } if c.Status == 0 { c.Status = http.StatusOK } } func (c *Context) accessLog() { log := c.log.WithValues("addr", c.Request.RemoteAddr, "method", c.Request.Method, "uri", c.Request.RequestURI, "status", c.Status, ) if c.Err != nil { log.Error(c.Err, "access") return } log.Info("access") } 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.setStatus(http.StatusNotFound) c.Exec() } func (c *Context) Forbidden() { c.Template("forbiddenHandler") c.Data = webData{Title: "403"} c.setStatus(http.StatusForbidden) c.Exec() } func (c *Context) setStatus(status int) { c.Status = status c.Response.WriteHeader(status) } func (c *Context) plainResponse(msg string, status int) { defer c.accessLog() c.Status = status http.Error(c.Response, msg, status) } func (c *Context) PlainUnauthorized() { c.SetHeader("WWW-Authenticate", `Basic realm="auth/basic", charset="UTF-8"`) c.plainResponse("unauthorized", http.StatusUnauthorized) } func (c *Context) PlainForbidden() { c.SetHeader("WWW-Authenticate", `Basic realm="auth/basic", charset="UTF-8"`) c.plainResponse("forbidden", http.StatusForbidden) } func (c *Context) PlainOK() { c.plainResponse("ok", http.StatusOK) } func (c *Context) Template(name string) { c.T = c.Srv.ts.Get(name) } func (c *Context) SetHeader(name, value string) { c.Response.Header().Set(name, value) } func (c *Context) Redirect(uri string, code int) { c.log.Info("access", "addr", c.Request.RemoteAddr, "method", c.Request.Method, "uri", c.Request.RequestURI, "status", code, "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) 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) Query(name string) string { // does nothing if called twice c.Request.ParseForm() return c.Request.Form.Get(name) } func (c *Context) Var(name string) (ret string) { ret = mux.Vars(c.Request)[name] return } func (c *Context) Forwarded() (ret string) { ret = c.Request.Header.Get("X-Forwarded-For") if ret == "" { ret = empty } 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(err error, msg string) { c.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, createdClaim: u.Created, adminClaim: u.Admin}, 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.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) Admin() bool { return c.Token.Claims.GetBool(adminClaim) } func (c *Context) ApproveLogin(u *core.User, challenge string) (rdr string, err error) { accept := client.NewAcceptLoginRequest(u.Username) accept.Acr = new(string) if u.Secret == "" { *accept.Acr = "pwd" } else { *accept.Acr = "mfa" } cr, resp, err := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background()). AcceptLoginRequest(*accept). LoginChallenge(challenge). Execute() if err != nil { return } defer resp.Body.Close() rdr = cr.RedirectTo return } func (c *Context) ApproveConsent(u *core.User, challenge string) (rdr string, err error) { ci, err := c.Consent(challenge) if err != nil { return } accept := client.NewAcceptConsentRequest() accept.GrantScope = ci.RequestedScope accept.GrantAccessTokenAudience = ci.RequestedAccessTokenAudience accept.Remember = new(bool) *accept.Remember = true accept.Session = &client.ConsentRequestSession{ IdToken: u.Claims, } /* map[string]string{ "apps": "git", "git_group": "admin", }, */ cr, resp, err := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()). AcceptConsentRequest(*accept). ConsentChallenge(challenge). Execute() if err != nil { return } defer resp.Body.Close() rdr = cr.RedirectTo return } func (c *Context) RejectConsent(challenge string) (rdr string, err error) { cr, resp, err := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()). RejectRequest(*client.NewRejectRequest()). ConsentChallenge(challenge). Execute() if err != nil { return } defer resp.Body.Close() rdr = cr.RedirectTo return } func (c *Context) Consent(challenge string) (cr *client.ConsentRequest, err error) { cr, resp, err := c.Srv.API.AdminApi.GetConsentRequest(context.Background()). ConsentChallenge(challenge). Execute() if err != nil { return } defer resp.Body.Close() return } type sess struct { ID string Time string } func (c *Context) Sessions(user string) (m map[string]sess, err error) { ss, resp, err := c.Srv.API.AdminApi.ListSubjectConsentSessions(context.Background()). Subject(user). Execute() if err != nil { return } defer resp.Body.Close() m = make(map[string]sess) for _, s := range ss { name := s.ConsentRequest.Client.ClientName if name == nil { continue } n := *name cid := s.ConsentRequest.Client.ClientId if cid == nil { continue } id := *cid if _, ok := m[n]; !ok { m[n] = sess{ id, s.HandledAt.Format(timeFmt), } } } return } func (c *Context) RevokeSession(user, client string, all bool) (err error) { if client == "" && !all { return } resp, err := c.Srv.API.AdminApi.RevokeConsentSessions(context.Background()). Subject(user). Client(client). All(all). Execute() if err != nil { return } defer resp.Body.Close() return } type ctxHandler func(*Context)