// Copyright (C) 2022 Marius Schellenberger package server import ( "context" "encoding/json" "errors" "fmt" "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 *consentInfo } 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.Status = http.StatusNotFound c.Response.WriteHeader(http.StatusNotFound) 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.ts.Get(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) { 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) } type respBody struct { URL string `json:"redirect_to,omitempty"` Error string `json:"error,omitempty"` } func (c *Context) ApproveLogin(user, challenge string) (rdr string, err error) { req := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background()) req.AcceptLoginRequest(*client.NewAcceptLoginRequest(user)) req.LoginChallenge(challenge) _, resp, err := req.Execute() if err != nil { return } defer resp.Body.Close() rb := new(respBody) err = json.NewDecoder(resp.Body).Decode(rb) if err != nil { return } if rb.Error != "" { err = errors.New(rb.Error) return } rdr = rb.URL return } func (c *Context) ApproveConsent(challenge string) (rdr string, err error) { req := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()). AcceptConsentRequest(*client.NewAcceptConsentRequest()). ConsentChallenge(challenge) _, resp, err := req.Execute() if err != nil { return } defer resp.Body.Close() rb := new(respBody) err = json.NewDecoder(resp.Body).Decode(rb) if err != nil { return } if rb.Error != "" { err = errors.New(rb.Error) return } rdr = rb.URL return } func (c *Context) RejectConsent(challenge string) (rdr string, err error) { req := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()). RejectRequest(*client.NewRejectRequest()). ConsentChallenge(challenge) _, resp, err := req.Execute() if err != nil { return } defer resp.Body.Close() rb := new(respBody) err = json.NewDecoder(resp.Body).Decode(rb) if err != nil { return } if rb.Error != "" { err = errors.New(rb.Error) return } rdr = rb.URL return } func (c *Context) Consent(challenge string) (ci *consentInfo, err error) { fmt.Println(challenge) req := c.Srv.API.AdminApi.GetConsentRequest(context.Background()). ConsentChallenge(challenge) _, resp, err := req.Execute() if err != nil { fmt.Println(err) return } defer resp.Body.Close() ci = new(consentInfo) err = json.NewDecoder(resp.Body).Decode(ci) if err != nil { return } if ci.Error != "" { err = errors.New(ci.Error) } return } type consentInfo struct { Acr string `json:"acr"` Amr []string `json:"amr"` Challenge string `json:"challenge"` Error string `json:"error,omitempty"` Client struct { AllowedCorsOrigins []string `json:"allowed_cors_origins"` Audience []string `json:"audience"` BackchannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"` BackchannelLogoutURI string `json:"backchannel_logout_uri"` ClientID string `json:"client_id"` ClientName string `json:"client_name"` ClientSecret string `json:"client_secret"` ClientSecretExpiresAt int `json:"client_secret_expires_at"` ClientURI string `json:"client_uri"` Contacts []string `json:"contacts"` CreatedAt time.Time `json:"created_at"` FrontchannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"` FrontchannelLogoutURI string `json:"frontchannel_logout_uri"` GrantTypes []string `json:"grant_types"` Jwks struct { } `json:"jwks"` JwksURI string `json:"jwks_uri"` LogoURI string `json:"logo_uri"` Metadata struct { } `json:"metadata"` Owner string `json:"owner"` PolicyURI string `json:"policy_uri"` PostLogoutRedirectUris []string `json:"post_logout_redirect_uris"` RedirectUris []string `json:"redirect_uris"` RegistrationAccessToken string `json:"registration_access_token"` RegistrationClientURI string `json:"registration_client_uri"` RequestObjectSigningAlg string `json:"request_object_signing_alg"` RequestUris []string `json:"request_uris"` ResponseTypes []string `json:"response_types"` Scope string `json:"scope"` SectorIdentifierURI string `json:"sector_identifier_uri"` SubjectType string `json:"subject_type"` TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"` TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"` TosURI string `json:"tos_uri"` UpdatedAt time.Time `json:"updated_at"` UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg"` } `json:"client"` LoginChallenge string `json:"login_challenge"` LoginSessionID string `json:"login_session_id"` OidcContext struct { AcrValues []string `json:"acr_values"` Display string `json:"display"` IDTokenHintClaims struct { } `json:"id_token_hint_claims"` LoginHint string `json:"login_hint"` UILocales []string `json:"ui_locales"` } `json:"oidc_context"` RequestURL string `json:"request_url"` RequestedAccessTokenAudience []string `json:"requested_access_token_audience"` RequestedScope []string `json:"requested_scope"` Skip bool `json:"skip"` Subject string `json:"subject"` } type ctxHandler func(*Context)