mirror of https://github.com/cloudreve/Cloudreve
				
				
				
			
		
			
				
	
	
		
			183 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
			
		
		
	
	
			183 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Go
		
	
	
package recaptcha
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/json"
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"time"
 | 
						|
)
 | 
						|
 | 
						|
const reCAPTCHALink = "https://www.recaptcha.net/recaptcha/api/siteverify"
 | 
						|
 | 
						|
// VERSION the recaptcha api version
 | 
						|
type VERSION int8
 | 
						|
 | 
						|
const (
 | 
						|
	// V2 recaptcha api v2
 | 
						|
	V2 VERSION = iota
 | 
						|
	// V3 recaptcha api v3, more details can be found here : https://developers.google.com/recaptcha/docs/v3
 | 
						|
	V3
 | 
						|
	// DefaultTreshold Default minimin score when using V3 api
 | 
						|
	DefaultTreshold float32 = 0.5
 | 
						|
)
 | 
						|
 | 
						|
type reCHAPTCHARequest struct {
 | 
						|
	Secret   string `json:"secret"`
 | 
						|
	Response string `json:"response"`
 | 
						|
	RemoteIP string `json:"remoteip,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
type reCHAPTCHAResponse struct {
 | 
						|
	Success        bool      `json:"success"`
 | 
						|
	ChallengeTS    time.Time `json:"challenge_ts"`
 | 
						|
	Hostname       string    `json:"hostname,omitempty"`
 | 
						|
	ApkPackageName string    `json:"apk_package_name,omitempty"`
 | 
						|
	Action         string    `json:"action,omitempty"`
 | 
						|
	Score          float32   `json:"score,omitempty"`
 | 
						|
	ErrorCodes     []string  `json:"error-codes,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
// custom client so we can mock in tests
 | 
						|
type netClient interface {
 | 
						|
	PostForm(url string, formValues url.Values) (resp *http.Response, err error)
 | 
						|
}
 | 
						|
 | 
						|
// custom clock so we can mock in tests
 | 
						|
type clock interface {
 | 
						|
	Since(t time.Time) time.Duration
 | 
						|
}
 | 
						|
 | 
						|
type realClock struct {
 | 
						|
}
 | 
						|
 | 
						|
func (realClock) Since(t time.Time) time.Duration {
 | 
						|
	return time.Since(t)
 | 
						|
}
 | 
						|
 | 
						|
// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler
 | 
						|
type ReCAPTCHA struct {
 | 
						|
	client        netClient
 | 
						|
	Secret        string
 | 
						|
	ReCAPTCHALink string
 | 
						|
	Version       VERSION
 | 
						|
	Timeout       time.Duration
 | 
						|
	horloge       clock
 | 
						|
}
 | 
						|
 | 
						|
// NewReCAPTCHA new ReCAPTCHA instance if version is set to V2 uses recatpcha v2 API, get your secret from https://www.google.com/recaptcha/admin
 | 
						|
//  if version is set to V2 uses recatpcha v2 API, get your secret from https://g.co/recaptcha/v3
 | 
						|
func NewReCAPTCHA(ReCAPTCHASecret string, version VERSION, timeout time.Duration) (ReCAPTCHA, error) {
 | 
						|
	if ReCAPTCHASecret == "" {
 | 
						|
		return ReCAPTCHA{}, fmt.Errorf("recaptcha secret cannot be blank")
 | 
						|
	}
 | 
						|
	return ReCAPTCHA{
 | 
						|
		client: &http.Client{
 | 
						|
			Timeout: timeout,
 | 
						|
		},
 | 
						|
		horloge:       &realClock{},
 | 
						|
		Secret:        ReCAPTCHASecret,
 | 
						|
		ReCAPTCHALink: reCAPTCHALink,
 | 
						|
		Timeout:       timeout,
 | 
						|
		Version:       version,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Verify returns `nil` if no error and the client solved the challenge correctly
 | 
						|
func (r *ReCAPTCHA) Verify(challengeResponse string) error {
 | 
						|
	body := reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
 | 
						|
	return r.confirm(body, VerifyOption{})
 | 
						|
}
 | 
						|
 | 
						|
// VerifyOption verification options expected for the challenge
 | 
						|
type VerifyOption struct {
 | 
						|
	Threshold      float32 // ignored in v2 recaptcha
 | 
						|
	Action         string  // ignored in v2 recaptcha
 | 
						|
	Hostname       string
 | 
						|
	ApkPackageName string
 | 
						|
	ResponseTime   time.Duration
 | 
						|
	RemoteIP       string
 | 
						|
}
 | 
						|
 | 
						|
// VerifyWithOptions returns `nil` if no error and the client solved the challenge correctly and all options are natching
 | 
						|
// `Threshold` and `Action` are ignored when using V2 version
 | 
						|
func (r *ReCAPTCHA) VerifyWithOptions(challengeResponse string, options VerifyOption) error {
 | 
						|
	var body reCHAPTCHARequest
 | 
						|
	if options.RemoteIP == "" {
 | 
						|
		body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse}
 | 
						|
	} else {
 | 
						|
		body = reCHAPTCHARequest{Secret: r.Secret, Response: challengeResponse, RemoteIP: options.RemoteIP}
 | 
						|
	}
 | 
						|
	return r.confirm(body, options)
 | 
						|
}
 | 
						|
 | 
						|
func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest, options VerifyOption) (Err error) {
 | 
						|
	Err = nil
 | 
						|
	var formValues url.Values
 | 
						|
	if recaptcha.RemoteIP != "" {
 | 
						|
		formValues = url.Values{"secret": {recaptcha.Secret}, "remoteip": {recaptcha.RemoteIP}, "response": {recaptcha.Response}}
 | 
						|
	} else {
 | 
						|
		formValues = url.Values{"secret": {recaptcha.Secret}, "response": {recaptcha.Response}}
 | 
						|
	}
 | 
						|
	response, err := r.client.PostForm(r.ReCAPTCHALink, formValues)
 | 
						|
	if err != nil {
 | 
						|
		Err = fmt.Errorf("error posting to recaptcha endpoint: '%s'", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	defer response.Body.Close()
 | 
						|
	resultBody, err := ioutil.ReadAll(response.Body)
 | 
						|
	if err != nil {
 | 
						|
		Err = fmt.Errorf("couldn't read response body: '%s'", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	var result reCHAPTCHAResponse
 | 
						|
	err = json.Unmarshal(resultBody, &result)
 | 
						|
	if err != nil {
 | 
						|
		Err = fmt.Errorf("invalid response body json: '%s'", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if options.Hostname != "" && options.Hostname != result.Hostname {
 | 
						|
		Err = fmt.Errorf("invalid response hostname '%s', while expecting '%s'", result.Hostname, options.Hostname)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if options.ApkPackageName != "" && options.ApkPackageName != result.ApkPackageName {
 | 
						|
		Err = fmt.Errorf("invalid response ApkPackageName '%s', while expecting '%s'", result.ApkPackageName, options.ApkPackageName)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if options.ResponseTime != 0 {
 | 
						|
		duration := r.horloge.Since(result.ChallengeTS)
 | 
						|
		if options.ResponseTime < duration {
 | 
						|
			Err = fmt.Errorf("time spent in resolving challenge '%fs', while expecting maximum '%fs'", duration.Seconds(), options.ResponseTime.Seconds())
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if r.Version == V3 {
 | 
						|
		if options.Action != "" && options.Action != result.Action {
 | 
						|
			Err = fmt.Errorf("invalid response action '%s', while expecting '%s'", result.Action, options.Action)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if options.Threshold != 0 && options.Threshold >= result.Score {
 | 
						|
			Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, options.Threshold)
 | 
						|
			return
 | 
						|
		}
 | 
						|
		if options.Threshold == 0 && DefaultTreshold >= result.Score {
 | 
						|
			Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, DefaultTreshold)
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if result.ErrorCodes != nil {
 | 
						|
		Err = fmt.Errorf("remote error codes: %v", result.ErrorCodes)
 | 
						|
		return
 | 
						|
	}
 | 
						|
	if !result.Success && recaptcha.RemoteIP != "" {
 | 
						|
		Err = fmt.Errorf("invalid challenge solution or remote IP")
 | 
						|
	} else if !result.Success {
 | 
						|
		Err = fmt.Errorf("invalid challenge solution")
 | 
						|
	}
 | 
						|
	return
 | 
						|
}
 |