summaryrefslogtreecommitdiff
path: root/vendor/github.com/sjtug/cerberus/directives/endpoint.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/sjtug/cerberus/directives/endpoint.go')
-rw-r--r--vendor/github.com/sjtug/cerberus/directives/endpoint.go238
1 files changed, 0 insertions, 238 deletions
diff --git a/vendor/github.com/sjtug/cerberus/directives/endpoint.go b/vendor/github.com/sjtug/cerberus/directives/endpoint.go
deleted file mode 100644
index 0fec02f..0000000
--- a/vendor/github.com/sjtug/cerberus/directives/endpoint.go
+++ /dev/null
@@ -1,238 +0,0 @@
-package directives
-
-import (
- "crypto/subtle"
- "errors"
- "fmt"
- "net/http"
- "strconv"
- "strings"
- "time"
-
- "github.com/caddyserver/caddy/v2"
- "github.com/caddyserver/caddy/v2/modules/caddyhttp"
- "github.com/golang-jwt/jwt/v5"
- "github.com/sjtug/cerberus/core"
- "github.com/sjtug/cerberus/internal/ipblock"
- "github.com/sjtug/cerberus/web"
- "go.uber.org/zap"
-)
-
-// Endpoint is the handler that will be used to serve challenge endpoints and static files.
-type Endpoint struct {
- instance *core.Instance
- logger *zap.Logger
-}
-
-func checkAnswer(s string, difficulty int) bool {
- nibbles := difficulty / 2
- remaining := difficulty % 2
-
- if !strings.HasPrefix(s, strings.Repeat("0", nibbles)) {
- return false
- }
-
- if remaining == 0 {
- return true
- }
-
- return s[nibbles] < '8'
-}
-
-func (e *Endpoint) answerHandle(w http.ResponseWriter, r *http.Request) error {
- c := e.instance
-
- // Just to make sure the response is not cached, although this should be the default behavior for POST requests.
- w.Header().Set("Cache-Control", "no-cache")
-
- nonceStr := r.FormValue("nonce")
- if nonceStr == "" {
- e.logger.Info("nonce is empty")
- return respondFailure(w, r, &c.Config, "nonce is empty", false, http.StatusBadRequest, ".")
- }
- nonce64, err := strconv.ParseUint(nonceStr, 10, 32)
- if err != nil {
- e.logger.Debug("nonce is not an integer", zap.Error(err))
- return respondFailure(w, r, &c.Config, "nonce is not an integer", false, http.StatusBadRequest, ".")
- }
- nonce := uint32(nonce64)
- if !c.InsertUsedNonce(nonce) {
- e.logger.Info("nonce already used")
- return respondFailure(w, r, &c.Config, "nonce already used", false, http.StatusBadRequest, ".")
- }
-
- tsStr := r.FormValue("ts")
- if tsStr == "" {
- e.logger.Info("ts is empty")
- return respondFailure(w, r, &c.Config, "ts is empty", false, http.StatusBadRequest, ".")
- }
- ts, err := strconv.ParseInt(tsStr, 10, 64)
- if err != nil {
- e.logger.Debug("ts is not a integer", zap.Error(err))
- return respondFailure(w, r, &c.Config, "ts is not a integer", false, http.StatusBadRequest, ".")
- }
- now := time.Now().Unix()
- if ts < now-int64(core.NonceTTL) || ts > now {
- e.logger.Info("invalid ts", zap.Int64("ts", ts), zap.Int64("now", now))
- return respondFailure(w, r, &c.Config, "invalid ts", false, http.StatusBadRequest, ".")
- }
-
- signature := r.FormValue("signature")
- if signature == "" {
- e.logger.Info("signature is empty")
- return respondFailure(w, r, &c.Config, "signature is empty", false, http.StatusBadRequest, ".")
- }
-
- solutionStr := r.FormValue("solution")
- if solutionStr == "" {
- e.logger.Info("solution is empty")
- return respondFailure(w, r, &c.Config, "solution is empty", false, http.StatusBadRequest, ".")
- }
- solution, err := strconv.Atoi(solutionStr)
- if err != nil {
- e.logger.Debug("solution is not a integer", zap.Error(err))
- return respondFailure(w, r, &c.Config, "solution is not a integer", false, http.StatusBadRequest, ".")
- }
-
- response := r.FormValue("response")
- redir := r.FormValue("redir")
-
- challenge, err := challengeFor(r, c)
- if err != nil {
- e.logger.Error("failed to calculate challenge", zap.Error(err))
- return err
- }
-
- expectedSignature := calcSignature(challenge, nonce, ts, c)
- if signature != expectedSignature {
- e.logger.Debug("signature mismatch", zap.String("expected", expectedSignature), zap.String("actual", signature))
- return respondFailure(w, r, &c.Config, "signature mismatch", false, http.StatusForbidden, ".")
- }
-
- answer, err := blake3sum(fmt.Sprintf("%s|%d|%d|%s|%d", challenge, nonce, ts, signature, solution))
- if err != nil {
- e.logger.Error("failed to calculate answer", zap.Error(err))
- return err
- }
-
- if !checkAnswer(response, c.Difficulty) {
- clearCookie(w, c.CookieName)
- e.logger.Error("wrong response", zap.String("response", response), zap.Int("difficulty", c.Difficulty))
- return respondFailure(w, r, &c.Config, "wrong response", false, http.StatusForbidden, ".")
- }
-
- if subtle.ConstantTimeCompare([]byte(answer), []byte(response)) != 1 {
- clearCookie(w, c.CookieName)
- e.logger.Error("response mismatch", zap.String("expected", answer), zap.String("actual", response))
- return respondFailure(w, r, &c.Config, "response mismatch", false, http.StatusForbidden, ".")
- }
-
- // Now we know the user passed the challenge, we issue an approval and sign the result.
- approvalID := c.IssueApproval(c.AccessPerApproval)
- token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, jwt.MapClaims{
- "challenge": challenge,
- "response": response,
- "approval_id": approvalID,
- "iat": time.Now().Unix(),
- "nbf": time.Now().Add(-time.Minute).Unix(),
- "exp": time.Now().Add(c.ApprovalTTL).Unix(),
- })
- tokenStr, err := token.SignedString(c.GetPrivateKey())
- if err != nil {
- e.logger.Error("failed to sign token", zap.Error(err))
- return err
- }
-
- http.SetCookie(w, &http.Cookie{
- Name: c.CookieName,
- Value: tokenStr,
- Expires: time.Now().Add(c.ApprovalTTL),
- SameSite: http.SameSiteLaxMode,
- Path: "/",
- })
-
- e.logger.Debug("user passed the challenge")
-
- ipBlockRaw := caddyhttp.GetVar(r.Context(), core.VarIPBlock)
- if ipBlockRaw != nil {
- ipBlock := ipBlockRaw.(ipblock.IPBlock)
- c.DecPending(ipBlock)
- }
-
- w.Header().Set(c.HeaderName, "PASS")
- http.Redirect(w, r, redir, http.StatusSeeOther)
- return nil
-}
-
-// tryServeFile serves static files from the dist directory.
-func tryServeFile(w http.ResponseWriter, r *http.Request) bool {
- if !strings.HasPrefix(r.URL.Path, "/static/") {
- return false
- }
-
- // Remove the /static/ prefix to get the actual file path
- filePath := strings.TrimSuffix(caddyhttp.SanitizedPathJoin("/dist/", strings.TrimPrefix(r.URL.Path, "/static/")), "/")
-
- // Add cache control headers for static assets
- w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") // Cache for 1 year
- w.Header().Set("Vary", "Accept-Encoding")
-
- // Create a new request with the modified path
- req := *r
- req.URL.Path = filePath
-
- // Serve the file using http.FileServer
- http.FileServer(http.FS(web.Content)).ServeHTTP(w, &req)
- return true
-}
-
-func (e *Endpoint) ServeHTTP(w http.ResponseWriter, r *http.Request, _ caddyhttp.Handler) error {
- r = setupRequestID(r)
- r, err := setupLocale(r)
- if err != nil {
- return err
- }
-
- if tryServeFile(w, r) {
- return nil
- }
-
- c := e.instance
-
- path := strings.TrimSuffix(r.URL.Path, "/")
- if path == "/answer" && r.Method == http.MethodPost {
- return e.answerHandle(w, r)
- }
-
- return respondFailure(w, r, &c.Config, "Not found", false, http.StatusNotFound, ".")
-}
-
-func (e *Endpoint) Provision(ctx caddy.Context) error {
- e.logger = ctx.Logger()
-
- appRaw, err := ctx.App("cerberus")
- if err != nil {
- return err
- }
- app := appRaw.(*App)
-
- instance := app.GetInstance()
- if instance == nil {
- return errors.New("no global cerberus app found")
- }
- e.instance = instance
-
- return nil
-}
-
-func (Endpoint) CaddyModule() caddy.ModuleInfo {
- return caddy.ModuleInfo{
- ID: "http.handlers.cerberus_endpoint",
- New: func() caddy.Module { return new(Endpoint) },
- }
-}
-
-var (
- _ caddy.Provisioner = (*Endpoint)(nil)
- _ caddyhttp.MiddlewareHandler = (*Endpoint)(nil)
-)