code.gitea.io/gitea@v1.19.3/modules/turnstile/turnstile.go (about)

     1  // Copyright 2023 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package turnstile
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"io"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/modules/json"
    15  	"code.gitea.io/gitea/modules/setting"
    16  )
    17  
    18  // Response is the structure of JSON returned from API
    19  type Response struct {
    20  	Success     bool        `json:"success"`
    21  	ChallengeTS string      `json:"challenge_ts"`
    22  	Hostname    string      `json:"hostname"`
    23  	ErrorCodes  []ErrorCode `json:"error-codes"`
    24  	Action      string      `json:"login"`
    25  	Cdata       string      `json:"cdata"`
    26  }
    27  
    28  // Verify calls Cloudflare Turnstile API to verify token
    29  func Verify(ctx context.Context, response string) (bool, error) {
    30  	// Cloudflare turnstile official access instruction address: https://developers.cloudflare.com/turnstile/get-started/server-side-validation/
    31  	post := url.Values{
    32  		"secret":   {setting.Service.CfTurnstileSecret},
    33  		"response": {response},
    34  	}
    35  	// Basically a copy of http.PostForm, but with a context
    36  	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
    37  		"https://challenges.cloudflare.com/turnstile/v0/siteverify", strings.NewReader(post.Encode()))
    38  	if err != nil {
    39  		return false, fmt.Errorf("Failed to create CAPTCHA request: %w", err)
    40  	}
    41  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    42  
    43  	resp, err := http.DefaultClient.Do(req)
    44  	if err != nil {
    45  		return false, fmt.Errorf("Failed to send CAPTCHA response: %w", err)
    46  	}
    47  	defer resp.Body.Close()
    48  	body, err := io.ReadAll(resp.Body)
    49  	if err != nil {
    50  		return false, fmt.Errorf("Failed to read CAPTCHA response: %w", err)
    51  	}
    52  
    53  	var jsonResponse Response
    54  	if err := json.Unmarshal(body, &jsonResponse); err != nil {
    55  		return false, fmt.Errorf("Failed to parse CAPTCHA response: %w", err)
    56  	}
    57  
    58  	var respErr error
    59  	if len(jsonResponse.ErrorCodes) > 0 {
    60  		respErr = jsonResponse.ErrorCodes[0]
    61  	}
    62  	return jsonResponse.Success, respErr
    63  }
    64  
    65  // ErrorCode is a reCaptcha error
    66  type ErrorCode string
    67  
    68  // String fulfills the Stringer interface
    69  func (e ErrorCode) String() string {
    70  	switch e {
    71  	case "missing-input-secret":
    72  		return "The secret parameter was not passed."
    73  	case "invalid-input-secret":
    74  		return "The secret parameter was invalid or did not exist."
    75  	case "missing-input-response":
    76  		return "The response parameter was not passed."
    77  	case "invalid-input-response":
    78  		return "The response parameter is invalid or has expired."
    79  	case "bad-request":
    80  		return "The request was rejected because it was malformed."
    81  	case "timeout-or-duplicate":
    82  		return "The response parameter has already been validated before."
    83  	case "internal-error":
    84  		return "An internal error happened while validating the response. The request can be retried."
    85  	}
    86  	return string(e)
    87  }
    88  
    89  // Error fulfills the error interface
    90  func (e ErrorCode) Error() string {
    91  	return e.String()
    92  }