proofofwork.go 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. package proofofwork
  2. import (
  3. "crypto/subtle"
  4. "fmt"
  5. "log/slog"
  6. "net/http"
  7. "strconv"
  8. "strings"
  9. "github.com/TecharoHQ/anubis/internal"
  10. chall "github.com/TecharoHQ/anubis/lib/challenge"
  11. "github.com/TecharoHQ/anubis/lib/policy"
  12. "github.com/TecharoHQ/anubis/web"
  13. "github.com/a-h/templ"
  14. )
  15. func init() {
  16. chall.Register("fast", &Impl{Algorithm: "fast"})
  17. chall.Register("slow", &Impl{Algorithm: "slow"})
  18. }
  19. type Impl struct {
  20. Algorithm string
  21. }
  22. func (i *Impl) Fail(w http.ResponseWriter, r *http.Request) error {
  23. return nil
  24. }
  25. func (i *Impl) Issue(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string, ogTags map[string]string) (templ.Component, error) {
  26. component, err := web.BaseWithChallengeAndOGTags("Making sure you're not a bot!", web.Index(), challenge, rule.Challenge, ogTags)
  27. if err != nil {
  28. return nil, fmt.Errorf("can't render page: %w", err)
  29. }
  30. return component, nil
  31. }
  32. func (i *Impl) Validate(r *http.Request, lg *slog.Logger, rule *policy.Bot, challenge string) error {
  33. nonceStr := r.FormValue("nonce")
  34. if nonceStr == "" {
  35. return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
  36. }
  37. nonce, err := strconv.Atoi(nonceStr)
  38. if err != nil {
  39. return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
  40. }
  41. elapsedTimeStr := r.FormValue("elapsedTime")
  42. if elapsedTimeStr == "" {
  43. return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
  44. }
  45. elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
  46. if err != nil {
  47. return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
  48. }
  49. response := r.FormValue("response")
  50. if response == "" {
  51. return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
  52. }
  53. calcString := fmt.Sprintf("%s%d", challenge, nonce)
  54. calculated := internal.SHA256sum(calcString)
  55. if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
  56. return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
  57. }
  58. // compare the leading zeroes
  59. if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
  60. return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
  61. }
  62. lg.Debug("challenge took", "elapsedTime", elapsedTime)
  63. chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
  64. return nil
  65. }