initial commit
This commit is contained in:
commit
f48fa210bb
49 changed files with 4058 additions and 0 deletions
450
pkg/server/context.go
Normal file
450
pkg/server/context.go
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
// Copyright (C) 2022 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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
|
||||
Data *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,
|
||||
"url", 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,
|
||||
"url", 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) 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())
|
||||
req.AcceptConsentRequest(*client.NewAcceptConsentRequest())
|
||||
req.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())
|
||||
req.RejectRequest(*client.NewRejectRequest())
|
||||
req.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) {
|
||||
req := c.Srv.API.AdminApi.GetConsentRequest(context.Background())
|
||||
req.ConsentChallenge(challenge)
|
||||
_, resp, err := req.Execute()
|
||||
if err != nil {
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue