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  }