From 30047cb146aa1c0b7378de78da344e72fa679a31 Mon Sep 17 00:00:00 2001 From: Mohamed Ez-zarghili <8616968+ezzarghili@users.noreply.github.com> Date: Wed, 1 Jul 2020 23:09:32 +0100 Subject: [PATCH] Pass error codes to user. (#30) --- README.md | 10 ++++++---- recaptcha.go | 34 +++++++++++++++++++++------------- recaptcha_test.go | 2 ++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 9a26a24..980c1a9 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ Google reCAPTCHA v2 & v3 form submission verification in golang. The API has changed form last version hence the new major version change. Old API is still available using the package `gopkg.in/ezzarghili/recaptcha-go.v2` although it does not provide all options available in this version. -As always install the package in your environment by using a stable API version, see latest version in release page. +As always install the package in your environment by using a stable API version, see latest version in [releases page](https://github.com/ezzarghili/recaptcha-go/releases). ```bash -go get -u gopkg.in/ezzarghili/recaptcha-go.v4 +go get -u gopkg.in/ezzarghili/recaptcha-go.v4 ``` ### recaptcha v2 API @@ -19,7 +19,7 @@ go get -u gopkg.in/ezzarghili/recaptcha-go.v4 ```go import "gopkg.in/ezzarghili/recaptcha-go.v4" func main(){ - captcha, _ := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10*time.Second) // for v2 API get your secret from https://www.google.com/recaptcha/admin + captcha, _ := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V2, 10 * time.Second) // for v2 API get your secret from https://www.google.com/recaptcha/admin } ``` @@ -29,6 +29,7 @@ Now everytime you need to verify a V2 API client with no special options request err := captcha.Verify(recaptchaResponse) if err != nil { // do something with err (log?) + // Example check error codes array if they exist: (err.(*recaptcha.Error)).ErrorCodes } // proceed ``` @@ -49,6 +50,7 @@ Other v3 options are ignored and method will return `nil` when succeeded. err := captcha.VerifyWithOptions(recaptchaResponse, VerifyOption{RemoteIP: "123.123.123.123"}) if err != nil { // do something with err (log?) + // Example check error codes array if they exist: (err.(*recaptcha.Error)).ErrorCodes } // proceed ``` @@ -58,7 +60,7 @@ if err != nil { ```go import "gopkg.in/ezzarghili/recaptcha-go.v4" func main(){ - captcha, _ := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V3, 10*time.Second) // for v3 API use https://g.co/recaptcha/v3 (apperently the same admin UI at the time of writing) + captcha, _ := recaptcha.NewReCAPTCHA(recaptchaSecret, recaptcha.V3, 10 * time.Second) // for v3 API use https://g.co/recaptcha/v3 (apperently the same admin UI at the time of writing) } ``` diff --git a/recaptcha.go b/recaptcha.go index bad57b5..a068b13 100644 --- a/recaptcha.go +++ b/recaptcha.go @@ -56,7 +56,7 @@ func (realClock) Since(t time.Time) time.Duration { return time.Since(t) } -// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler +// ReCAPTCHA recpatcha holder struct, make adding mocking code simpler. type ReCAPTCHA struct { client netClient Secret string @@ -66,6 +66,14 @@ type ReCAPTCHA struct { horloge clock } +// Error custom error to pass ErrorCodes to user. +type Error struct { + msg string + ErrorCodes []string +} + +func (e *Error) Error() string { return e.msg } + // 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) { @@ -122,61 +130,61 @@ func (r *ReCAPTCHA) confirm(recaptcha reCHAPTCHARequest, options VerifyOption) ( } response, err := r.client.PostForm(r.ReCAPTCHALink, formValues) if err != nil { - Err = fmt.Errorf("error posting to recaptcha endpoint: '%s'", err) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("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()) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("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) + Err = &Error{msg: fmt.Sprintf("received score '%f', while expecting minimum '%f'", result.Score, options.Threshold)} return } if options.Threshold == 0 && DefaultThreshold > result.Score { - Err = fmt.Errorf("received score '%f', while expecting minimum '%f'", result.Score, DefaultThreshold) + Err = &Error{msg: fmt.Sprintf("received score '%f', while expecting minimum '%f'", result.Score, DefaultThreshold)} return } } if result.ErrorCodes != nil { - Err = fmt.Errorf("remote error codes: %v", result.ErrorCodes) + Err = &Error{msg: fmt.Sprintf("remote error codes: %v", result.ErrorCodes), ErrorCodes: result.ErrorCodes} return } if !result.Success && recaptcha.RemoteIP != "" { - Err = fmt.Errorf("invalid challenge solution or remote IP") + Err = &Error{msg: fmt.Sprintf("invalid challenge solution or remote IP")} } else if !result.Success { - Err = fmt.Errorf("invalid challenge solution") + Err = &Error{msg: fmt.Sprintf("invalid challenge solution")} } return } diff --git a/recaptcha_test.go b/recaptcha_test.go index 142a663..9da9495 100644 --- a/recaptcha_test.go +++ b/recaptcha_test.go @@ -94,6 +94,7 @@ func (s *ReCaptchaSuite) TestVerifyInvalidSolutionNoRemoteIp(c *C) { err := captcha.Verify("mycode") c.Assert(err, NotNil) c.Check(err, ErrorMatches, "invalid challenge solution") + c.Check((err.(*Error)).ErrorCodes, IsNil) } type mockSuccessClientNoOptions struct{} @@ -140,6 +141,7 @@ func (s *ReCaptchaSuite) TestVerifyWithoutOptions(c *C) { err = captcha.Verify("mycode") c.Assert(err, NotNil) c.Check(err, ErrorMatches, "remote error codes:.*") + c.Check((err.(*Error)).ErrorCodes, DeepEquals, []string{"invalid-input-response", "bad-request"}) }