Contact form antispam
This commit is contained in:
parent
8085e092bc
commit
c97cc28b13
|
@ -47,13 +47,21 @@ const (
|
|||
ChangeEmailRedisKey = "change-email/%s"
|
||||
SignupTokenExpires = 24 * time.Hour // used for all tokens so far
|
||||
|
||||
// Rate limit
|
||||
// Rate limits
|
||||
RateLimitRedisKey = "rate-limit/%s/%s" // namespace, id
|
||||
LoginRateLimitWindow = 1 * time.Hour
|
||||
LoginRateLimit = 10 // 10 failed login attempts = locked for full hour
|
||||
LoginRateLimitCooldownAt = 3 // 3 failed attempts = start throttling
|
||||
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.
|
||||
LastLoginAtCooldown = 8 * time.Hour
|
||||
)
|
||||
|
|
|
@ -15,11 +15,12 @@ var Current = DefaultVariable()
|
|||
|
||||
// Variable configuration attributes (loaded from settings.json).
|
||||
type Variable struct {
|
||||
BaseURL string
|
||||
AdminEmail string
|
||||
Mail Mail
|
||||
Redis Redis
|
||||
Database Database
|
||||
BaseURL string
|
||||
AdminEmail string
|
||||
Mail Mail
|
||||
Redis Redis
|
||||
Database Database
|
||||
UseXForwardedFor bool
|
||||
}
|
||||
|
||||
// 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/markdown"
|
||||
"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/templates"
|
||||
)
|
||||
|
@ -26,6 +27,8 @@ func Contact() http.HandlerFunc {
|
|||
title = "Contact Us"
|
||||
message = r.FormValue("message")
|
||||
replyTo = r.FormValue("email")
|
||||
trap1 = r.FormValue("url") != "https://"
|
||||
trap2 = r.FormValue("comment") != ""
|
||||
tableID int
|
||||
tableName string
|
||||
tableLabel string // front-end user feedback about selected report item
|
||||
|
@ -85,6 +88,32 @@ func Contact() http.HandlerFunc {
|
|||
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.
|
||||
fb := &models.Feedback{
|
||||
Intent: intent,
|
||||
|
|
|
@ -56,7 +56,7 @@ func (l *Limiter) Ping() error {
|
|||
}
|
||||
|
||||
// Are we throttled?
|
||||
if data.Pings >= l.CooldownAt {
|
||||
if l.CooldownAt > 0 && data.Pings > l.CooldownAt {
|
||||
data.NotBefore = now.Add(l.Cooldown)
|
||||
if err := redis.Set(key, data, l.Window); err != nil {
|
||||
return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err)
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
|
@ -112,6 +113,19 @@ func Get(r *http.Request) *Session {
|
|||
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.
|
||||
func (s *Session) ReadFlashes(w http.ResponseWriter) (flashes, errors []string) {
|
||||
flashes = s.Flashes
|
||||
|
|
|
@ -68,3 +68,8 @@
|
|||
.card.nonshy-collapsible-mobile {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Hide an element */
|
||||
.nonshy-hidden {
|
||||
display: none;
|
||||
}
|
|
@ -89,6 +89,26 @@
|
|||
</div>
|
||||
{{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">
|
||||
<button type="submit" class="button is-success">
|
||||
Send Message
|
||||
|
|
Loading…
Reference in New Issue
Block a user