From 57ffeb8b41c7fe005c05da054096ab592e85dfc1 Mon Sep 17 00:00:00 2001 From: matheusgomes28 Date: Wed, 29 May 2024 22:11:57 +0100 Subject: [PATCH 1/2] Adding support for recaptcha for contact pages --- app/app.go | 2 +- app/contact.go | 67 ++++++++++++++++++++++++++++++++++++++++-- common/app_settings.go | 2 ++ urchin_config.toml | 5 ++++ views/contact.templ | 47 ++++++++++++++++++++++------- 5 files changed, 110 insertions(+), 13 deletions(-) diff --git a/app/app.go b/app/app.go index 715b8c7..ec5219e 100644 --- a/app/app.go +++ b/app/app.go @@ -36,7 +36,7 @@ func SetupRoutes(app_settings common.AppSettings, database database.Database) *g addCachableHandler(r, "GET", "/page/:num", homeHandler, &cache, app_settings, database) // DO not cache as it needs to handlenew form values - r.POST("/contact-send", makeContactFormHandler()) + r.POST("/contact-send", makeContactFormHandler(app_settings)) // Where all the static files (css, js, etc) are served from r.Static("/static", "./static") diff --git a/app/contact.go b/app/contact.go index 6aa8384..00013ae 100644 --- a/app/contact.go +++ b/app/contact.go @@ -2,8 +2,12 @@ package app import ( "bytes" + "encoding/json" + "fmt" + "io" "net/http" "net/mail" + "net/url" "github.com/gin-gonic/gin" "github.com/matheusgomes28/urchin/common" @@ -12,7 +16,53 @@ import ( "github.com/rs/zerolog/log" ) -func makeContactFormHandler() func(*gin.Context) { +const RECAPTCHA_VERIFY_URL string = "https://www.google.com/recaptcha/api/siteverify" + +type RecaptchaResponse struct { + Success bool `json:"success"` + Score float32 `json:"score"` + Timestamp string `json:"challenge_ts"` + Hostname string `json:"hostname"` +} + +/* + "success": true|false, + "challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) + "hostname": string, // the hostname of the site where the reCAPTCHA was solved + "error-codes": [...] // optional +*/ + +func verifyRecaptcha(recaptcha_secret string, recaptcha_response string) error { + // Validate that the recaptcha response was actually + // not a bot by checking the success rate + recaptcha_response_data, err := http.PostForm(RECAPTCHA_VERIFY_URL, url.Values{ + "secret": {recaptcha_secret}, + "response": {recaptcha_response}, + }) + if err != nil { + err_str := fmt.Sprintf("could not do recaptcha post request: %s", err) + return fmt.Errorf("%s: %s", err_str, err) + } + defer recaptcha_response_data.Body.Close() + + if recaptcha_response_data.StatusCode != http.StatusOK { + return fmt.Errorf("invalid recaptcha response: %s", recaptcha_response_data.Status) + } + var recaptcha_answer RecaptchaResponse + recaptcha_response_data_buffer, _ := io.ReadAll(recaptcha_response_data.Body) + err = json.Unmarshal(recaptcha_response_data_buffer, &recaptcha_answer) + if err != nil { + return fmt.Errorf("could not parse recaptcha response: %s", err) + } + + if !recaptcha_answer.Success || (recaptcha_answer.Score < 0.9) { + return fmt.Errorf("could not validate recaptcha") + } + + return nil +} + +func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) { return func(c *gin.Context) { if err := c.Request.ParseForm(); err != nil { log.Error().Msgf("could not parse form %v", err) @@ -25,6 +75,19 @@ func makeContactFormHandler() func(*gin.Context) { email := c.Request.FormValue("email") name := c.Request.FormValue("name") message := c.Request.FormValue("message") + recaptcha_response := c.Request.FormValue("g-recaptcha-response") + + // Make the request to Google's API + if (len(app_settings.RecaptchaSecret) > 0) && (len(app_settings.RecaptchaSiteKey) > 0) { + err := verifyRecaptcha(app_settings.RecaptchaSecret, recaptcha_response) + if err != nil { + log.Error().Msgf("could not validate recaptcha %v", err) + if err = render(c, http.StatusOK, views.MakeContactFailure("could not validate recaptcha", err.Error())); err != nil { + log.Error().Msgf("could not render %v", err) + } + return + } + } // Parse email _, err := mail.ParseAddress(email) @@ -59,7 +122,7 @@ func makeContactFormHandler() func(*gin.Context) { // TODO : This is a duplicate of the index handler... abstract func contactHandler(c *gin.Context, app_settings common.AppSettings, db database.Database) ([]byte, error) { - index_view := views.MakeContactPage(app_settings.AppNavbar.Links) + index_view := views.MakeContactPage(app_settings.AppNavbar.Links, app_settings.RecaptchaSiteKey) html_buffer := bytes.NewBuffer(nil) if err := index_view.Render(c, html_buffer); err != nil { log.Error().Msgf("could not render: %v", err) diff --git a/common/app_settings.go b/common/app_settings.go index 43dd8e6..f8109e6 100644 --- a/common/app_settings.go +++ b/common/app_settings.go @@ -18,6 +18,8 @@ type AppSettings struct { AdminPort int `toml:"admin_port"` ImageDirectory string `toml:"image_dir"` CacheEnabled bool `toml:"cache_enabled"` + RecaptchaSiteKey string `toml:"recaptcha_sitekey,omitempty"` + RecaptchaSecret string `toml:"recaptcha_secret,omitempty"` AppNavbar Navbar `toml:"navbar"` } diff --git a/urchin_config.toml b/urchin_config.toml index beb76d5..96a1285 100644 --- a/urchin_config.toml +++ b/urchin_config.toml @@ -26,6 +26,11 @@ image_dir = "./images" # Enable/disable endpoint cache cache_enabled = true +# Recaptcha settings +recaptcha_sitekey = "6LecCewpAAAAAK7QS2SwuyCIDzwlyXMs4J1Z5LBq" +recaptcha_secret = "6LecCewpAAAAAPP8Aaxh6jWaDE1xG_MBQa1OOs8f" + + [navbar] links = [ { name = "Home", href = "/", title = "Homepage" }, diff --git a/views/contact.templ b/views/contact.templ index 2ab0d50..8512f79 100644 --- a/views/contact.templ +++ b/views/contact.templ @@ -2,7 +2,7 @@ package views import "github.com/matheusgomes28/urchin/common" -templ MakeContactPage(links []common.Link) { +templ MakeContactPage(links []common.Link, recaptcha_sitekey string) { @@ -13,6 +13,9 @@ templ MakeContactPage(links []common.Link) { + if len(recaptcha_sitekey) > 0 { + + } @@ -20,18 +23,42 @@ templ MakeContactPage(links []common.Link) {

Contact Us

-
- -

+ if len(recaptcha_sitekey) > 0 { + + +

- -

+ +

-
-

+
+

- -
+ + + + } else { +
+ +

+ + +

+ +
+

+ +
+ }
@MakeFooter() From cf5c42ca541706ce8a8b910485161f35f23541c9 Mon Sep 17 00:00:00 2001 From: matheusgomes28 Date: Thu, 30 May 2024 21:23:57 +0100 Subject: [PATCH 2/2] Making the code a little nicer --- app/contact.go | 51 ++++++++++++++++++++----------- views/contact.templ | 74 +++++++++++++++++++++++++-------------------- 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/app/contact.go b/app/contact.go index 00013ae..0a1e13a 100644 --- a/app/contact.go +++ b/app/contact.go @@ -16,6 +16,8 @@ import ( "github.com/rs/zerolog/log" ) +// Change this in case Google decides to deprecate +// the reCAPTCHA validation endpoint const RECAPTCHA_VERIFY_URL string = "https://www.google.com/recaptcha/api/siteverify" type RecaptchaResponse struct { @@ -25,13 +27,6 @@ type RecaptchaResponse struct { Hostname string `json:"hostname"` } -/* - "success": true|false, - "challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) - "hostname": string, // the hostname of the site where the reCAPTCHA was solved - "error-codes": [...] // optional -*/ - func verifyRecaptcha(recaptcha_secret string, recaptcha_response string) error { // Validate that the recaptcha response was actually // not a bot by checking the success rate @@ -62,6 +57,28 @@ func verifyRecaptcha(recaptcha_secret string, recaptcha_response string) error { return nil } +func validateEmail(email string) error { + _, err := mail.ParseAddress(email) + if err != nil { + return fmt.Errorf("could not parse email: %s", email) + } + + return nil +} + +func renderErrorPage(c *gin.Context, email string, err error) error { + if err = render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())); err != nil { + log.Error().Msgf("could not render error page: %v", err) + } + return err +} + +func logError(err error) { + if err != nil { + log.Error().Msgf("%v", err) + } +} + func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) { return func(c *gin.Context) { if err := c.Request.ParseForm(); err != nil { @@ -77,25 +94,21 @@ func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) message := c.Request.FormValue("message") recaptcha_response := c.Request.FormValue("g-recaptcha-response") - // Make the request to Google's API + // Make the request to Google's API only if user + // configured recatpcha settings if (len(app_settings.RecaptchaSecret) > 0) && (len(app_settings.RecaptchaSiteKey) > 0) { err := verifyRecaptcha(app_settings.RecaptchaSecret, recaptcha_response) if err != nil { - log.Error().Msgf("could not validate recaptcha %v", err) - if err = render(c, http.StatusOK, views.MakeContactFailure("could not validate recaptcha", err.Error())); err != nil { - log.Error().Msgf("could not render %v", err) - } + log.Error().Msgf("%v", err) + defer logError(renderErrorPage(c, email, err)) return } } - // Parse email - _, err := mail.ParseAddress(email) + err := validateEmail(email) if err != nil { - log.Error().Msgf("could not parse email: %v", err) - if err = render(c, http.StatusOK, views.MakeContactFailure(email, err.Error())); err != nil { - log.Error().Msgf("could not render: %v", err) - } + log.Error().Msgf("%v", err) + defer logError(renderErrorPage(c, email, err)) return } @@ -103,6 +116,7 @@ func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) if len(name) > 200 { if err = render(c, http.StatusOK, views.MakeContactFailure(email, "name too long (200 chars max)")); err != nil { log.Error().Msgf("could not render: %v", err) + logError(renderErrorPage(c, email, err)) } return } @@ -110,6 +124,7 @@ func makeContactFormHandler(app_settings common.AppSettings) func(*gin.Context) if len(message) > 10000 { if err = render(c, http.StatusOK, views.MakeContactFailure(email, "message too long (1000 chars max)")); err != nil { log.Error().Msgf("could not render: %v", err) + logError(renderErrorPage(c, email, err)) } return } diff --git a/views/contact.templ b/views/contact.templ index 8512f79..17e3a6d 100644 --- a/views/contact.templ +++ b/views/contact.templ @@ -2,6 +2,45 @@ package views import "github.com/matheusgomes28/urchin/common" +templ MakeContactFormWithRecaptcha(recaptcha_sitekey string) { +
+ +

+ + +

+ +
+

+ + + +
+} + +templ MakeContactForm() { +
+ +

+ + +

+ +
+

+ +
+} + templ MakeContactPage(links []common.Link, recaptcha_sitekey string) { @@ -24,40 +63,9 @@ templ MakeContactPage(links []common.Link, recaptcha_sitekey string) {

Contact Us

if len(recaptcha_sitekey) > 0 { -
- -

- - -

- -
-

- - - -
+ @MakeContactFormWithRecaptcha(recaptcha_sitekey) } else { -
- -

- - -

- -
-

- -
+ @MakeContactForm() }