diff --git a/README.md b/README.md index 23639f5..91d874c 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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": [ { @@ -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 diff --git a/api/sendform_mw.go b/api/sendform_mw.go index 32aca01..4fcdc1d 100644 --- a/api/sendform_mw.go +++ b/api/sendform_mw.go @@ -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 @@ -301,6 +305,58 @@ 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}, + } + 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 { diff --git a/form/form.go b/form/form.go index 81e45be..4a11f4d 100644 --- a/form/form.go +++ b/form/form.go @@ -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"` + } } } diff --git a/server/router_api.go b/server/router_api.go index 0c92e10..6b0274b 100644 --- a/server/router_api.go +++ b/server/router_api.go @@ -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) } diff --git a/server/server.go b/server/server.go index c95a9a8..cac16b6 100644 --- a/server/server.go +++ b/server/server.go @@ -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 {