Skip to content

Commit

Permalink
Merge pull request #42 from wneessen/feature/41-implement-cloudflare-…
Browse files Browse the repository at this point in the history
…turnstile-as-captcha-method

v0.3.0: Implement Cloudflare Turnstile as supported captcha feature
  • Loading branch information
wneessen authored Dec 23, 2022
2 parents 7d8e035 + 132f112 commit b0461d0
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 2 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.
* Per-form mail server configuration
* hCaptcha support
* reCaptcha v2 support
* Turnstile support
* Form field type validation (text, email, number, bool)
* Confirmation mail to poster
* Custom Reply-To header based on sending mail address
Expand Down Expand Up @@ -114,6 +115,10 @@ the JSON syntax of the form configuration is very simple, yet flexible.
"enabled": true,
"secret_key": "0x01234567890"
},
"turnstile": {
"enabled": true,
"secret_key": "0x01234567890"
},
"honeypot": "street",
"fields": [
{
Expand Down Expand Up @@ -165,6 +170,9 @@ the JSON syntax of the form configuration is very simple, yet flexible.
* `recaptcha (type: struct)`: The struct for the forms reCaptcha configuration
* `enabled (type: bool)`: Enable reCaptcha challenge-response validation
* `secret_key (type: string)`: Your reCaptcha secret key
* `turnstile (type: struct)`: The struct for the forms Turnstile configuration
* `enabled (type: bool)`: Enable Turnstile challenge-response validation
* `secret_key (type: string)`: Your Turnstile secret key
* `honeypot (type: string)`: Name of the honeypot field, that is expected to be empty (Anti-SPAM)
* `fields (type: []struct)`: Array of single field validation configurations
* `name (type: string)`: Field validation identifier
Expand Down
57 changes: 57 additions & 0 deletions api/sendform_mw.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ type HcaptchaResponse CaptchaResponse
// ReCaptchaResponse is the CaptchaResponse for Google ReCaptcha
type ReCaptchaResponse CaptchaResponse

// TurnstileResponse is the CaptchaResponse for Cloudflare Turnstile
type TurnstileResponse CaptchaResponse

// List of common errors
const (
ErrNoValidObject = "no valid form object found"
ErrHCaptchaValidateFailed = "hCaptcha validation failed"
ErrReCaptchaVaildateFailed = "reCaptcha validation failed"
ErrTurnstileVaildateFailed = "Turnstile validation failed"
)

// SendFormBindForm is a middleware that validates the provided form data and binds
Expand Down Expand Up @@ -301,6 +305,59 @@ func (r *Route) SendFormRecaptcha(next echo.HandlerFunc) echo.HandlerFunc {
}
}

// SendFormTurnstile is a middleware that checks the form data against Cloudflare Turnstile
func (r *Route) SendFormTurnstile(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sr := c.Get("formobj").(*SendFormRequest)
if sr == nil {
return echo.NewHTTPError(http.StatusInternalServerError, ErrNoValidObject)
}

if sr.FormObj.Validation.Turnstile.Enabled {
turnstileResponse := c.FormValue("cf-turnstile-response")
if turnstileResponse == "" {
return echo.NewHTTPError(http.StatusBadRequest, "missing Turnstile response")
}

// Create a HTTP request
postData := url.Values{
"response": {turnstileResponse},
"secret": {sr.FormObj.Validation.Turnstile.SecretKey},
"remoteip": {c.RealIP()},
}
httpResp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", postData)
if err != nil {
c.Logger().Errorf("failed to post HTTP request to Turnstile: %s", err)
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
}

var respBody bytes.Buffer
_, err = respBody.ReadFrom(httpResp.Body)
if err != nil {
c.Logger().Errorf("reading HTTP response body failed: %s", err)
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
}
if httpResp.StatusCode == http.StatusOK {
var turnstileResp ReCaptchaResponse
if err := json.Unmarshal(respBody.Bytes(), &turnstileResp); err != nil {
c.Logger().Errorf("HTTP response JSON unmarshalling failed: %s", err)
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
}
if !turnstileResp.Success {
return echo.NewHTTPError(http.StatusBadRequest,
"Turnstile challenge-response validation failed")
}
return next(c)
}

return echo.NewHTTPError(http.StatusBadRequest,
"Turnstile challenge-response validation failed")
}

return next(c)
}
}

// SendFormCheckToken is a middleware that checks the form security token
func (r *Route) SendFormCheckToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
Expand Down
4 changes: 4 additions & 0 deletions form/form.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ type Form struct {
Enabled bool `fig:"enabled"`
SecretKey string `fig:"secret_key"`
}
Turnstile struct {
Enabled bool `fig:"enabled"`
SecretKey string `fig:"secret_key"`
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion server/router_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@ func (s *Srv) RouterAPI() {
ag.Add("POST", "/token", apiRoute.GetToken)
ag.Add("POST", "/send/:fid/:token", apiRoute.SendForm,
apiRoute.SendFormBindForm, apiRoute.SendFormReqFields, apiRoute.SendFormHoneypot,
apiRoute.SendFormHcaptcha, apiRoute.SendFormRecaptcha, apiRoute.SendFormCheckToken)
apiRoute.SendFormHcaptcha, apiRoute.SendFormRecaptcha, apiRoute.SendFormTurnstile,
apiRoute.SendFormCheckToken)
}
2 changes: 1 addition & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
)

// VERSION is the global version string contstant
const VERSION = "0.2.9"
const VERSION = "0.3.0"

// Srv represents the server object
type Srv struct {
Expand Down

0 comments on commit b0461d0

Please sign in to comment.