-
Notifications
You must be signed in to change notification settings - Fork 119
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
Changes from 1 commit
1497fa4
3bfb4b7
de91137
555ed8f
17f65b9
22f21fa
522e48a
e220b39
9086f27
439bd1d
af3d830
f5b94a8
2306bfb
d4f8f47
d126e50
21b1464
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} |
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()) | ||
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() | ||
} |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure - defer res.Body.Close()
res, err := app.Test(req, -1)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close() Committable suggestion
Suggested change
|
||||||||||||||||
|
||||||||||||||||
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)) | ||||||||||||||||
} |
There was a problem hiding this comment.
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 ofc.Host()
for the HCaptcha API call.Committable suggestion