Contact form antispam
This commit is contained in:
parent
8085e092bc
commit
c97cc28b13
|
@ -47,13 +47,21 @@ const (
|
||||||
ChangeEmailRedisKey = "change-email/%s"
|
ChangeEmailRedisKey = "change-email/%s"
|
||||||
SignupTokenExpires = 24 * time.Hour // used for all tokens so far
|
SignupTokenExpires = 24 * time.Hour // used for all tokens so far
|
||||||
|
|
||||||
// Rate limit
|
// Rate limits
|
||||||
RateLimitRedisKey = "rate-limit/%s/%s" // namespace, id
|
RateLimitRedisKey = "rate-limit/%s/%s" // namespace, id
|
||||||
LoginRateLimitWindow = 1 * time.Hour
|
LoginRateLimitWindow = 1 * time.Hour
|
||||||
LoginRateLimit = 10 // 10 failed login attempts = locked for full hour
|
LoginRateLimit = 10 // 10 failed login attempts = locked for full hour
|
||||||
LoginRateLimitCooldownAt = 3 // 3 failed attempts = start throttling
|
LoginRateLimitCooldownAt = 3 // 3 failed attempts = start throttling
|
||||||
LoginRateLimitCooldown = 30 * time.Second
|
LoginRateLimitCooldown = 30 * time.Second
|
||||||
|
|
||||||
|
// Contact form rate limits for logged-out users to curb spam robots:
|
||||||
|
// - One message can be submitted every 2 minutes
|
||||||
|
// - If they post 10 minutes in an hour they are paused for one hour.
|
||||||
|
ContactRateLimitWindow = 1 * time.Hour
|
||||||
|
ContactRateLimit = 10
|
||||||
|
ContactRateLimitCooldownAt = 1
|
||||||
|
ContactRateLimitCooldown = 2 * time.Minute
|
||||||
|
|
||||||
// How frequently to refresh LastLoginAt since sessions are long-lived.
|
// How frequently to refresh LastLoginAt since sessions are long-lived.
|
||||||
LastLoginAtCooldown = 8 * time.Hour
|
LastLoginAtCooldown = 8 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Variable struct {
|
||||||
Mail Mail
|
Mail Mail
|
||||||
Redis Redis
|
Redis Redis
|
||||||
Database Database
|
Database Database
|
||||||
|
UseXForwardedFor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultVariable returns the default settings.json data.
|
// DefaultVariable returns the default settings.json data.
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"code.nonshy.com/nonshy/website/pkg/mail"
|
"code.nonshy.com/nonshy/website/pkg/mail"
|
||||||
"code.nonshy.com/nonshy/website/pkg/markdown"
|
"code.nonshy.com/nonshy/website/pkg/markdown"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/ratelimit"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||||
)
|
)
|
||||||
|
@ -26,6 +27,8 @@ func Contact() http.HandlerFunc {
|
||||||
title = "Contact Us"
|
title = "Contact Us"
|
||||||
message = r.FormValue("message")
|
message = r.FormValue("message")
|
||||||
replyTo = r.FormValue("email")
|
replyTo = r.FormValue("email")
|
||||||
|
trap1 = r.FormValue("url") != "https://"
|
||||||
|
trap2 = r.FormValue("comment") != ""
|
||||||
tableID int
|
tableID int
|
||||||
tableName string
|
tableName string
|
||||||
tableLabel string // front-end user feedback about selected report item
|
tableLabel string // front-end user feedback about selected report item
|
||||||
|
@ -85,6 +88,32 @@ func Contact() http.HandlerFunc {
|
||||||
replyTo = currentUser.Email
|
replyTo = currentUser.Email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rate limit submissions, especially for logged-out users.
|
||||||
|
if currentUser == nil {
|
||||||
|
limiter := &ratelimit.Limiter{
|
||||||
|
Namespace: "contact",
|
||||||
|
ID: session.RemoteAddr(r),
|
||||||
|
Limit: config.ContactRateLimit,
|
||||||
|
Window: config.ContactRateLimitWindow,
|
||||||
|
CooldownAt: config.ContactRateLimitCooldownAt,
|
||||||
|
Cooldown: config.ContactRateLimitCooldown,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := limiter.Ping(); err != nil {
|
||||||
|
session.FlashError(w, r, err.Error())
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If they have tripped the spam bot trap fields, don't save their message.
|
||||||
|
if trap1 || trap2 {
|
||||||
|
log.Error("Contact form: bot has tripped the trap fields, do not save message")
|
||||||
|
session.Flash(w, r, success)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Store feedback in the database.
|
// Store feedback in the database.
|
||||||
fb := &models.Feedback{
|
fb := &models.Feedback{
|
||||||
Intent: intent,
|
Intent: intent,
|
||||||
|
|
|
@ -56,7 +56,7 @@ func (l *Limiter) Ping() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we throttled?
|
// Are we throttled?
|
||||||
if data.Pings >= l.CooldownAt {
|
if l.CooldownAt > 0 && data.Pings > l.CooldownAt {
|
||||||
data.NotBefore = now.Add(l.Cooldown)
|
data.NotBefore = now.Add(l.Cooldown)
|
||||||
if err := redis.Set(key, data, l.Window); err != nil {
|
if err := redis.Set(key, data, l.Window); err != nil {
|
||||||
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)
|
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.nonshy.com/nonshy/website/pkg/config"
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
@ -112,6 +113,19 @@ func Get(r *http.Request) *Session {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoteAddr returns the user's remote IP address. If UseXForwardedFor is enabled in settings.json,
|
||||||
|
// the HTTP header X-Forwarded-For may be returned here or otherwise the request RemoteAddr is returned.
|
||||||
|
func RemoteAddr(r *http.Request) string {
|
||||||
|
if config.Current.UseXForwardedFor {
|
||||||
|
xff := r.Header.Get("X-Forwarded-For")
|
||||||
|
if len(xff) > 0 {
|
||||||
|
return strings.SplitN(xff, ",", 1)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.SplitN(r.RemoteAddr, ":", 1)[0]
|
||||||
|
}
|
||||||
|
|
||||||
// ReadFlashes returns and clears the Flashes and Errors for this session.
|
// ReadFlashes returns and clears the Flashes and Errors for this session.
|
||||||
func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) {
|
func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) {
|
||||||
flashes = s.Flashes
|
flashes = s.Flashes
|
||||||
|
|
|
@ -68,3 +68,8 @@
|
||||||
.card.nonshy-collapsible-mobile {
|
.card.nonshy-collapsible-mobile {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide an element */
|
||||||
|
.nonshy-hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -89,6 +89,26 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
<!-- "Trap" fields for dumb automated spammer bots -->
|
||||||
|
<div class="field block nonshy-hidden">
|
||||||
|
<label class="label" for="url">Website URL</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="url" id="url"
|
||||||
|
value="https://">
|
||||||
|
<p class="help">
|
||||||
|
Do not touch this field.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="field block nonshy-hidden">
|
||||||
|
<label class="label" for="url">Comment</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="comment" id="comment"
|
||||||
|
value="">
|
||||||
|
<p class="help">
|
||||||
|
Do not touch this field.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="field has-text-centered">
|
<div class="field has-text-centered">
|
||||||
<button type="submit" class="button is-success">
|
<button type="submit" class="button is-success">
|
||||||
Send Message
|
Send Message
|
||||||
|
|
Loading…
Reference in New Issue
Block a user