Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: HCaptcha Middleware #1071

Merged
merged 16 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions hcaptcha/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hcaptcha

import (
"bytes"
"encoding/json"
"fmt"
"github.com/gofiber/fiber/v3"
)

const DefaultSiteVerifyURL = "https://api.hcaptcha.com/siteverify"

type Config struct {
// SecretKey is the secret key you get from HCaptcha when you create a new application
SecretKey string
// ResponseKeyFunc should return the generated pass UUID from the ctx, which will be validated
ResponseKeyFunc func(fiber.Ctx) (string, error)
// SiteVerifyURL is the endpoint URL where the program should verify the given token
// default value is: "https://api.hcaptcha.com/siteverify"
SiteVerifyURL string
}

func DefaultResponseKeyFunc(c fiber.Ctx) (string, error) {
data := struct {
HCaptchaToken string `json:"hcaptcha_token"`
}{}

err := json.NewDecoder(bytes.NewReader(c.Body())).Decode(&data)

if err != nil {
return "", fmt.Errorf("failed to decode HCaptcha token: %w", err)
}

return data.HCaptchaToken, nil
}
74 changes: 74 additions & 0 deletions hcaptcha/hcaptcha.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Package hcaptcha is a simple middleware that checks for an HCaptcha UUID
// and then validates it. It returns an error if the UUID is not valid (the request may have been sent by a robot).
package hcaptcha

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/valyala/fasthttp"
"net/url"
)

type HCaptcha struct {
Config
}

func New(config Config) fiber.Handler {
if config.SiteVerifyURL == "" {
config.SiteVerifyURL = DefaultSiteVerifyURL
}

if config.ResponseKeyFunc == nil {
config.ResponseKeyFunc = DefaultResponseKeyFunc
}

h := &HCaptcha{
config,
}
return h.Validate
}

func (h *HCaptcha) Validate(c fiber.Ctx) error {
token, err := h.ResponseKeyFunc(c)
if err != nil {
c.Status(fiber.StatusBadRequest)
return fmt.Errorf("error retrieving HCaptcha token: %w", err)
}

req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
req.SetBody([]byte(url.Values{
"secret": {h.SecretKey},
"response": {token},
}.Encode()))
req.Header.SetMethod("POST")
req.Header.SetContentType("application/x-www-form-urlencoded; charset=UTF-8")
req.Header.Set("Accept", "application/json")
req.SetRequestURI(c.Host())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the request URI to h.SiteVerifyURL instead of c.Host() for the HCaptcha API call.

- req.SetRequestURI(c.Host())
+ req.SetRequestURI(h.SiteVerifyURL)

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
req.SetRequestURI(c.Host())
req.SetRequestURI(h.SiteVerifyURL)

res := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(res)

if err = fasthttp.Do(req, res); err != nil {
c.Status(fiber.StatusBadRequest)
return fmt.Errorf("error sending request to HCaptcha API: %w", err)
}

o := struct {
Success bool `json:"success"`
}{}

if err = json.NewDecoder(bytes.NewReader(res.Body())).Decode(&o); err != nil {
c.Status(fiber.StatusInternalServerError)
return fmt.Errorf("error decoding HCaptcha API response: %w", err)
}

if !o.Success {
c.Status(fiber.StatusForbidden)
return errors.New("unable to check that you are not a robot")
}

return c.Next()
}
49 changes: 49 additions & 0 deletions hcaptcha/hcaptcha_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package hcaptcha

import (
"github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert"
"io"
"net/http/httptest"
"testing"
)

const (
TestSecretKey = "0x0000000000000000000000000000000000000000"
TestResponseToken = "20000000-aaaa-bbbb-cccc-000000000002" // Got by using this site key: 20000000-ffff-ffff-ffff-000000000002
)

func TestHCaptcha(t *testing.T) {
app := fiber.New()

m := New(Config{
SecretKey: TestSecretKey,
ResponseKeyFunc: func(c fiber.Ctx) (string, error) {
return c.Query("token"), nil
},
})

app.Get("/hcaptcha", m, func(c fiber.Ctx) error {
return c.Status(200).SendString("ok")
})

req := httptest.NewRequest("GET", "/hcaptcha?token="+TestResponseToken, nil)
req.Header.Set("Content-Type", "application/json")

res, err := app.Test(req, -1)
defer res.Body.Close()
Comment on lines +34 to +35
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure defer res.Body.Close() is called after checking for an error from app.Test(req, -1) to avoid potential nil pointer dereference.

- defer res.Body.Close()
  res, err := app.Test(req, -1)
+ if err != nil {
+     t.Fatal(err)
+ }
+ defer res.Body.Close()

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
res, err := app.Test(req, -1)
defer res.Body.Close()
res, err := app.Test(req, -1)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()


if err != nil {
t.Fatal(err)
}

assert.Equal(t, res.StatusCode, fiber.StatusOK, "Response status code")

body, err := io.ReadAll(res.Body)

if err != nil {
t.Fatal(err)
}

assert.Equal(t, "ok", string(body))
}