1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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
}
|