summaryrefslogtreecommitdiffstats
path: root/modules/hcaptcha
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
commitdd136858f1ea40ad3c94191d647487fa4f31926c (patch)
tree58fec94a7b2a12510c9664b21793f1ed560c6518 /modules/hcaptcha
parentInitial commit. (diff)
downloadforgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.tar.xz
forgejo-dd136858f1ea40ad3c94191d647487fa4f31926c.zip
Adding upstream version 9.0.0.upstream/9.0.0upstreamdebian
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to 'modules/hcaptcha')
-rw-r--r--modules/hcaptcha/error.go47
-rw-r--r--modules/hcaptcha/hcaptcha.go140
-rw-r--r--modules/hcaptcha/hcaptcha_test.go106
3 files changed, 293 insertions, 0 deletions
diff --git a/modules/hcaptcha/error.go b/modules/hcaptcha/error.go
new file mode 100644
index 0000000..7b68bf8
--- /dev/null
+++ b/modules/hcaptcha/error.go
@@ -0,0 +1,47 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hcaptcha
+
+const (
+ ErrMissingInputSecret ErrorCode = "missing-input-secret"
+ ErrInvalidInputSecret ErrorCode = "invalid-input-secret"
+ ErrMissingInputResponse ErrorCode = "missing-input-response"
+ ErrInvalidInputResponse ErrorCode = "invalid-input-response"
+ ErrBadRequest ErrorCode = "bad-request"
+ ErrInvalidOrAlreadySeenResponse ErrorCode = "invalid-or-already-seen-response"
+ ErrNotUsingDummyPasscode ErrorCode = "not-using-dummy-passcode"
+ ErrSitekeySecretMismatch ErrorCode = "sitekey-secret-mismatch"
+)
+
+// ErrorCode is any possible error from hCaptcha
+type ErrorCode string
+
+// String fulfills the Stringer interface
+func (err ErrorCode) String() string {
+ switch err {
+ case ErrMissingInputSecret:
+ return "Your secret key is missing."
+ case ErrInvalidInputSecret:
+ return "Your secret key is invalid or malformed."
+ case ErrMissingInputResponse:
+ return "The response parameter (verification token) is missing."
+ case ErrInvalidInputResponse:
+ return "The response parameter (verification token) is invalid or malformed."
+ case ErrBadRequest:
+ return "The request is invalid or malformed."
+ case ErrInvalidOrAlreadySeenResponse:
+ return "The response parameter has already been checked, or has another issue."
+ case ErrNotUsingDummyPasscode:
+ return "You have used a testing sitekey but have not used its matching secret."
+ case ErrSitekeySecretMismatch:
+ return "The sitekey is not registered with the provided secret."
+ default:
+ return ""
+ }
+}
+
+// Error fulfills the error interface
+func (err ErrorCode) Error() string {
+ return err.String()
+}
diff --git a/modules/hcaptcha/hcaptcha.go b/modules/hcaptcha/hcaptcha.go
new file mode 100644
index 0000000..b970d49
--- /dev/null
+++ b/modules/hcaptcha/hcaptcha.go
@@ -0,0 +1,140 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hcaptcha
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+const verifyURL = "https://hcaptcha.com/siteverify"
+
+// Client is an hCaptcha client
+type Client struct {
+ ctx context.Context
+ http *http.Client
+
+ secret string
+}
+
+// PostOptions are optional post form values
+type PostOptions struct {
+ RemoteIP string
+ Sitekey string
+}
+
+// ClientOption is a func to modify a new Client
+type ClientOption func(*Client)
+
+// WithHTTP sets the http.Client of a Client
+func WithHTTP(httpClient *http.Client) func(*Client) {
+ return func(hClient *Client) {
+ hClient.http = httpClient
+ }
+}
+
+// WithContext sets the context.Context of a Client
+func WithContext(ctx context.Context) func(*Client) {
+ return func(hClient *Client) {
+ hClient.ctx = ctx
+ }
+}
+
+// New returns a new hCaptcha Client
+func New(secret string, options ...ClientOption) (*Client, error) {
+ if strings.TrimSpace(secret) == "" {
+ return nil, ErrMissingInputSecret
+ }
+
+ client := &Client{
+ ctx: context.Background(),
+ http: http.DefaultClient,
+ secret: secret,
+ }
+
+ for _, opt := range options {
+ opt(client)
+ }
+
+ return client, nil
+}
+
+// Response is an hCaptcha response
+type Response struct {
+ Success bool `json:"success"`
+ ChallengeTS string `json:"challenge_ts"`
+ Hostname string `json:"hostname"`
+ Credit bool `json:"credit,omitempty"`
+ ErrorCodes []ErrorCode `json:"error-codes"`
+}
+
+// Verify checks the response against the hCaptcha API
+func (c *Client) Verify(token string, opts PostOptions) (*Response, error) {
+ if strings.TrimSpace(token) == "" {
+ return nil, ErrMissingInputResponse
+ }
+
+ post := url.Values{
+ "secret": []string{c.secret},
+ "response": []string{token},
+ }
+ if strings.TrimSpace(opts.RemoteIP) != "" {
+ post.Add("remoteip", opts.RemoteIP)
+ }
+ if strings.TrimSpace(opts.Sitekey) != "" {
+ post.Add("sitekey", opts.Sitekey)
+ }
+
+ // Basically a copy of http.PostForm, but with a context
+ req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, verifyURL, strings.NewReader(post.Encode()))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+ resp, err := c.http.Do(req)
+ if err != nil {
+ return nil, err
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var response *Response
+ if err := json.Unmarshal(body, &response); err != nil {
+ return nil, err
+ }
+
+ return response, nil
+}
+
+// Verify calls hCaptcha API to verify token
+func Verify(ctx context.Context, response string) (bool, error) {
+ client, err := New(setting.Service.HcaptchaSecret, WithContext(ctx))
+ if err != nil {
+ return false, err
+ }
+
+ resp, err := client.Verify(response, PostOptions{
+ Sitekey: setting.Service.HcaptchaSitekey,
+ })
+ if err != nil {
+ return false, err
+ }
+
+ var respErr error
+ if len(resp.ErrorCodes) > 0 {
+ respErr = resp.ErrorCodes[0]
+ }
+ return resp.Success, respErr
+}
diff --git a/modules/hcaptcha/hcaptcha_test.go b/modules/hcaptcha/hcaptcha_test.go
new file mode 100644
index 0000000..55e01ec
--- /dev/null
+++ b/modules/hcaptcha/hcaptcha_test.go
@@ -0,0 +1,106 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package hcaptcha
+
+import (
+ "net/http"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+const (
+ dummySiteKey = "10000000-ffff-ffff-ffff-000000000001"
+ dummySecret = "0x0000000000000000000000000000000000000000"
+ dummyToken = "10000000-aaaa-bbbb-cccc-000000000001"
+)
+
+func TestMain(m *testing.M) {
+ os.Exit(m.Run())
+}
+
+func TestCaptcha(t *testing.T) {
+ tt := []struct {
+ Name string
+ Secret string
+ Token string
+ Error ErrorCode
+ }{
+ {
+ Name: "Success",
+ Secret: dummySecret,
+ Token: dummyToken,
+ },
+ {
+ Name: "Missing Secret",
+ Token: dummyToken,
+ Error: ErrMissingInputSecret,
+ },
+ {
+ Name: "Missing Token",
+ Secret: dummySecret,
+ Error: ErrMissingInputResponse,
+ },
+ {
+ Name: "Invalid Token",
+ Secret: dummySecret,
+ Token: "test",
+ Error: ErrInvalidInputResponse,
+ },
+ }
+
+ for _, tc := range tt {
+ t.Run(tc.Name, func(t *testing.T) {
+ client, err := New(tc.Secret, WithHTTP(&http.Client{
+ Timeout: time.Second * 5,
+ }))
+ if err != nil {
+ // The only error that can be returned from creating a client
+ if tc.Error == ErrMissingInputSecret && err == ErrMissingInputSecret {
+ return
+ }
+ t.Log(err)
+ t.FailNow()
+ }
+
+ resp, err := client.Verify(tc.Token, PostOptions{
+ Sitekey: dummySiteKey,
+ })
+ if err != nil {
+ // The only error that can be returned prior to the request
+ if tc.Error == ErrMissingInputResponse && err == ErrMissingInputResponse {
+ return
+ }
+ t.Log(err)
+ t.FailNow()
+ }
+
+ if tc.Error.String() != "" {
+ if resp.Success {
+ t.Log("Verification should fail.")
+ t.Fail()
+ }
+ if len(resp.ErrorCodes) == 0 {
+ t.Log("hCaptcha should have returned an error.")
+ t.Fail()
+ }
+ var hasErr bool
+ for _, err := range resp.ErrorCodes {
+ if strings.EqualFold(err.String(), tc.Error.String()) {
+ hasErr = true
+ break
+ }
+ }
+ if !hasErr {
+ t.Log("hCaptcha did not return the error being tested")
+ t.Fail()
+ }
+ } else if !resp.Success {
+ t.Log("Verification should succeed.")
+ t.Fail()
+ }
+ })
+ }
+}