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..0a1e13a 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,70 @@ import ( "github.com/rs/zerolog/log" ) -func makeContactFormHandler() func(*gin.Context) { +// 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 { + Success bool `json:"success"` + Score float32 `json:"score"` + Timestamp string `json:"challenge_ts"` + Hostname string `json:"hostname"` +} + +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 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 { log.Error().Msgf("could not parse form %v", err) @@ -25,14 +92,23 @@ 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") - // Parse email - _, err := mail.ParseAddress(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) + // 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("%v", err) + defer logError(renderErrorPage(c, email, err)) + return } + } + + err := validateEmail(email) + if err != nil { + log.Error().Msgf("%v", err) + defer logError(renderErrorPage(c, email, err)) return } @@ -40,6 +116,7 @@ func makeContactFormHandler() 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 } @@ -47,6 +124,7 @@ func makeContactFormHandler() 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 } @@ -59,7 +137,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..17e3a6d 100644 --- a/views/contact.templ +++ b/views/contact.templ @@ -2,7 +2,46 @@ package views import "github.com/matheusgomes28/urchin/common" -templ MakeContactPage(links []common.Link) { +templ MakeContactFormWithRecaptcha(recaptcha_sitekey string) { +
+} + +templ MakeContactForm() { + +} + +templ MakeContactPage(links []common.Link, recaptcha_sitekey string) { @@ -13,6 +52,9 @@ templ MakeContactPage(links []common.Link) { + if len(recaptcha_sitekey) > 0 { + + } @@ -20,18 +62,11 @@ templ MakeContactPage(links []common.Link) {