code.gitea.io/gitea@v1.19.3/modules/hcaptcha/hcaptcha.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package hcaptcha 5 6 import ( 7 "context" 8 "io" 9 "net/http" 10 "net/url" 11 "strings" 12 13 "code.gitea.io/gitea/modules/json" 14 "code.gitea.io/gitea/modules/setting" 15 ) 16 17 const verifyURL = "https://hcaptcha.com/siteverify" 18 19 // Client is an hCaptcha client 20 type Client struct { 21 ctx context.Context 22 http *http.Client 23 24 secret string 25 } 26 27 // PostOptions are optional post form values 28 type PostOptions struct { 29 RemoteIP string 30 Sitekey string 31 } 32 33 // ClientOption is a func to modify a new Client 34 type ClientOption func(*Client) 35 36 // WithHTTP sets the http.Client of a Client 37 func WithHTTP(httpClient *http.Client) func(*Client) { 38 return func(hClient *Client) { 39 hClient.http = httpClient 40 } 41 } 42 43 // WithContext sets the context.Context of a Client 44 func WithContext(ctx context.Context) func(*Client) { 45 return func(hClient *Client) { 46 hClient.ctx = ctx 47 } 48 } 49 50 // New returns a new hCaptcha Client 51 func New(secret string, options ...ClientOption) (*Client, error) { 52 if strings.TrimSpace(secret) == "" { 53 return nil, ErrMissingInputSecret 54 } 55 56 client := &Client{ 57 ctx: context.Background(), 58 http: http.DefaultClient, 59 secret: secret, 60 } 61 62 for _, opt := range options { 63 opt(client) 64 } 65 66 return client, nil 67 } 68 69 // Response is an hCaptcha response 70 type Response struct { 71 Success bool `json:"success"` 72 ChallengeTS string `json:"challenge_ts"` 73 Hostname string `json:"hostname"` 74 Credit bool `json:"credit,omitempty"` 75 ErrorCodes []ErrorCode `json:"error-codes"` 76 } 77 78 // Verify checks the response against the hCaptcha API 79 func (c *Client) Verify(token string, opts PostOptions) (*Response, error) { 80 if strings.TrimSpace(token) == "" { 81 return nil, ErrMissingInputResponse 82 } 83 84 post := url.Values{ 85 "secret": []string{c.secret}, 86 "response": []string{token}, 87 } 88 if strings.TrimSpace(opts.RemoteIP) != "" { 89 post.Add("remoteip", opts.RemoteIP) 90 } 91 if strings.TrimSpace(opts.Sitekey) != "" { 92 post.Add("sitekey", opts.Sitekey) 93 } 94 95 // Basically a copy of http.PostForm, but with a context 96 req, err := http.NewRequestWithContext(c.ctx, http.MethodPost, verifyURL, strings.NewReader(post.Encode())) 97 if err != nil { 98 return nil, err 99 } 100 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 101 102 resp, err := c.http.Do(req) 103 if err != nil { 104 return nil, err 105 } 106 107 body, err := io.ReadAll(resp.Body) 108 if err != nil { 109 return nil, err 110 } 111 defer resp.Body.Close() 112 113 var response *Response 114 if err := json.Unmarshal(body, &response); err != nil { 115 return nil, err 116 } 117 118 return response, nil 119 } 120 121 // Verify calls hCaptcha API to verify token 122 func Verify(ctx context.Context, response string) (bool, error) { 123 client, err := New(setting.Service.HcaptchaSecret, WithContext(ctx)) 124 if err != nil { 125 return false, err 126 } 127 128 resp, err := client.Verify(response, PostOptions{ 129 Sitekey: setting.Service.HcaptchaSitekey, 130 }) 131 if err != nil { 132 return false, err 133 } 134 135 var respErr error 136 if len(resp.ErrorCodes) > 0 { 137 respErr = resp.ErrorCodes[0] 138 } 139 return resp.Success, respErr 140 }