Skip to content

Commit b0461d0

Browse files
authored
Merge pull request #42 from wneessen/feature/41-implement-cloudflare-turnstile-as-captcha-method
v0.3.0: Implement Cloudflare Turnstile as supported captcha feature
2 parents 7d8e035 + 132f112 commit b0461d0

File tree

5 files changed

+72
-2
lines changed

5 files changed

+72
-2
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ API that can be accessed via JavaScript `Fetch()` or `XMLHttpRequest`.
1818
* Per-form mail server configuration
1919
* hCaptcha support
2020
* reCaptcha v2 support
21+
* Turnstile support
2122
* Form field type validation (text, email, number, bool)
2223
* Confirmation mail to poster
2324
* Custom Reply-To header based on sending mail address
@@ -114,6 +115,10 @@ the JSON syntax of the form configuration is very simple, yet flexible.
114115
"enabled": true,
115116
"secret_key": "0x01234567890"
116117
},
118+
"turnstile": {
119+
"enabled": true,
120+
"secret_key": "0x01234567890"
121+
},
117122
"honeypot": "street",
118123
"fields": [
119124
{
@@ -165,6 +170,9 @@ the JSON syntax of the form configuration is very simple, yet flexible.
165170
* `recaptcha (type: struct)`: The struct for the forms reCaptcha configuration
166171
* `enabled (type: bool)`: Enable reCaptcha challenge-response validation
167172
* `secret_key (type: string)`: Your reCaptcha secret key
173+
* `turnstile (type: struct)`: The struct for the forms Turnstile configuration
174+
* `enabled (type: bool)`: Enable Turnstile challenge-response validation
175+
* `secret_key (type: string)`: Your Turnstile secret key
168176
* `honeypot (type: string)`: Name of the honeypot field, that is expected to be empty (Anti-SPAM)
169177
* `fields (type: []struct)`: Array of single field validation configurations
170178
* `name (type: string)`: Field validation identifier

api/sendform_mw.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,15 @@ type HcaptchaResponse CaptchaResponse
3838
// ReCaptchaResponse is the CaptchaResponse for Google ReCaptcha
3939
type ReCaptchaResponse CaptchaResponse
4040

41+
// TurnstileResponse is the CaptchaResponse for Cloudflare Turnstile
42+
type TurnstileResponse CaptchaResponse
43+
4144
// List of common errors
4245
const (
4346
ErrNoValidObject = "no valid form object found"
4447
ErrHCaptchaValidateFailed = "hCaptcha validation failed"
4548
ErrReCaptchaVaildateFailed = "reCaptcha validation failed"
49+
ErrTurnstileVaildateFailed = "Turnstile validation failed"
4650
)
4751

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

308+
// SendFormTurnstile is a middleware that checks the form data against Cloudflare Turnstile
309+
func (r *Route) SendFormTurnstile(next echo.HandlerFunc) echo.HandlerFunc {
310+
return func(c echo.Context) error {
311+
sr := c.Get("formobj").(*SendFormRequest)
312+
if sr == nil {
313+
return echo.NewHTTPError(http.StatusInternalServerError, ErrNoValidObject)
314+
}
315+
316+
if sr.FormObj.Validation.Turnstile.Enabled {
317+
turnstileResponse := c.FormValue("cf-turnstile-response")
318+
if turnstileResponse == "" {
319+
return echo.NewHTTPError(http.StatusBadRequest, "missing Turnstile response")
320+
}
321+
322+
// Create a HTTP request
323+
postData := url.Values{
324+
"response": {turnstileResponse},
325+
"secret": {sr.FormObj.Validation.Turnstile.SecretKey},
326+
"remoteip": {c.RealIP()},
327+
}
328+
httpResp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", postData)
329+
if err != nil {
330+
c.Logger().Errorf("failed to post HTTP request to Turnstile: %s", err)
331+
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
332+
}
333+
334+
var respBody bytes.Buffer
335+
_, err = respBody.ReadFrom(httpResp.Body)
336+
if err != nil {
337+
c.Logger().Errorf("reading HTTP response body failed: %s", err)
338+
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
339+
}
340+
if httpResp.StatusCode == http.StatusOK {
341+
var turnstileResp ReCaptchaResponse
342+
if err := json.Unmarshal(respBody.Bytes(), &turnstileResp); err != nil {
343+
c.Logger().Errorf("HTTP response JSON unmarshalling failed: %s", err)
344+
return echo.NewHTTPError(http.StatusInternalServerError, ErrTurnstileVaildateFailed)
345+
}
346+
if !turnstileResp.Success {
347+
return echo.NewHTTPError(http.StatusBadRequest,
348+
"Turnstile challenge-response validation failed")
349+
}
350+
return next(c)
351+
}
352+
353+
return echo.NewHTTPError(http.StatusBadRequest,
354+
"Turnstile challenge-response validation failed")
355+
}
356+
357+
return next(c)
358+
}
359+
}
360+
304361
// SendFormCheckToken is a middleware that checks the form security token
305362
func (r *Route) SendFormCheckToken(next echo.HandlerFunc) echo.HandlerFunc {
306363
return func(c echo.Context) error {

form/form.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ type Form struct {
4848
Enabled bool `fig:"enabled"`
4949
SecretKey string `fig:"secret_key"`
5050
}
51+
Turnstile struct {
52+
Enabled bool `fig:"enabled"`
53+
SecretKey string `fig:"secret_key"`
54+
}
5155
}
5256
}
5357

server/router_api.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ func (s *Srv) RouterAPI() {
1818
ag.Add("POST", "/token", apiRoute.GetToken)
1919
ag.Add("POST", "/send/:fid/:token", apiRoute.SendForm,
2020
apiRoute.SendFormBindForm, apiRoute.SendFormReqFields, apiRoute.SendFormHoneypot,
21-
apiRoute.SendFormHcaptcha, apiRoute.SendFormRecaptcha, apiRoute.SendFormCheckToken)
21+
apiRoute.SendFormHcaptcha, apiRoute.SendFormRecaptcha, apiRoute.SendFormTurnstile,
22+
apiRoute.SendFormCheckToken)
2223
}

server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import (
1919
)
2020

2121
// VERSION is the global version string contstant
22-
const VERSION = "0.2.9"
22+
const VERSION = "0.3.0"
2323

2424
// Srv represents the server object
2525
type Srv struct {

0 commit comments

Comments
 (0)