added the rate limiter - Whoa There, Tiger!

This commit is contained in:
2026-03-10 22:59:35 -06:00
parent e10cf0b48c
commit a1837b2cea
8 changed files with 95 additions and 3 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ After the point where it reaches feature parity with Venice circa 2006.
* ~~Support all customizations that were done with the EMinds instance of Venice.~~
* ~~Gitea-like status page showing Go-specific internals.~~
* Build static Tailwind CSS file rather than using remote-loaded version. (Gate on debug/prod flag)
* Rate limiter.
* ~~Rate limiter.~~
* ~~Better logging configuration.~~
## Immediate Cleanups Required
+17
View File
@@ -15,6 +15,7 @@ import (
"fmt"
"io"
"io/fs"
"math"
"os"
"path/filepath"
"reflect"
@@ -35,6 +36,9 @@ const AMSTERDAM_COPYRIGHT = "2025-2026"
// CONFIGFILE_NAME is the name of the standard configuration file.
const CONFIGFILE_NAME = "amsterdam.yaml"
// epsilon is used in testing if a float value is 0.
const epsilon = 1e-9
// AmCLI is the command-line interface arguments structure.
type AmCLI struct {
ConfigFile string `arg:"-C,--config,env:AMSTERDAM_CONFIG" help:"Location of the configuration file."`
@@ -90,6 +94,11 @@ type AmConfig struct {
WelcomeTitle string `yaml:"welcomeTitle"`
WelcomeMessage string `yaml:"welcomeMessage"`
TopPostsTitle string `yaml:"topPostsTitle"`
RateLimit struct {
Rate float64 `yaml:"rate"`
Burst int `yaml:"burst"`
ExpireMinutes int `yaml:"expireMinutes"`
} `yaml:"rateLimit"`
} `yaml:"site"`
Database struct {
Driver string `yaml:"driver"`
@@ -278,6 +287,14 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
} else {
fldDest.Set(fldLoaded)
}
} else if fldDest.CanFloat() {
// float field handling
n := fldLoaded.Float()
if math.Abs(n) <= epsilon {
fldDest.Set(fldDefaults)
} else {
fldDest.Set(fldLoaded)
}
} else {
// if we see this message, this function needs more work
log.Errorf("*** unable to deal with field %s of type %s", structField.Name, typ.Name())
+4
View File
@@ -30,6 +30,10 @@ site:
Welcome to the <strong>Amsterdam Web Communities System</strong>. To get the most out of this site, you should log in or create an account,
using one of the links above.
topPostsTitle: "Latest from the Conferences"
rateLimit:
rate: 20.0
burst: 0
expireMinutes: 3
database:
driver: "mysql"
dsn: "amsdb:x00yes2k@tcp(localhost)/amsterdam?parseTime=true&loc=UTC"
+34
View File
@@ -13,10 +13,14 @@ import (
"errors"
"fmt"
"net/http"
"time"
"git.erbosoft.com/amy/amsterdam/config"
"git.erbosoft.com/amy/amsterdam/ui"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
log "github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
// EBUTTON is the standard error for an unknown button.
@@ -80,3 +84,33 @@ func AmErrorHandler(err error, c echo.Context) {
log.Errorf("Error rendering error (%v): %v", err, cerr)
}
}
// rateLimitErrorHandler is called if there's an error getting the identifier for a connection (unlikely).
func rateLimitErrorHandler(c echo.Context, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
return "error", err
})
}
// rateLimitDenyHandler is called if the rate limit is exceeded by a connection.
func rateLimitDenyHandler(c echo.Context, identifier string, err error) error {
return ui.AmWithTempContext(c, func(ctxt ui.AmContext) (string, any) {
ctxt.VarMap().Set("identifier", identifier)
return "ratelimit", err
})
}
// AmSetupRateLimiter sets up the rate-limiting middleware.
func AmSetupRateLimiter() echo.MiddlewareFunc {
rcfg := middleware.RateLimiterMemoryStoreConfig{
Rate: rate.Limit(config.GlobalConfig.Site.RateLimit.Rate),
Burst: config.GlobalConfig.Site.RateLimit.Burst,
ExpiresIn: time.Duration(config.GlobalConfig.Site.RateLimit.ExpireMinutes) * time.Minute,
}
cfg := middleware.RateLimiterConfig{
Store: middleware.NewRateLimiterMemoryStoreWithConfig(rcfg),
ErrorHandler: rateLimitErrorHandler,
DenyHandler: rateLimitDenyHandler,
}
return middleware.RateLimiterWithConfig(cfg)
}
+1 -1
View File
@@ -20,6 +20,7 @@ require (
github.com/tkuchiki/go-timezone v0.2.3
golang.org/x/net v0.50.0
golang.org/x/text v0.34.0
golang.org/x/time v0.11.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -34,5 +35,4 @@ require (
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/time v0.11.0 // indirect
)
+4 -1
View File
@@ -49,8 +49,11 @@ func setupEcho() *echo.Echo {
}
e.Use(LogrusMiddleware)
// set up the rate limiter from the configuration
rateLimiter := AmSetupRateLimiter()
// This is the set of all middleware functions used by the UI, as opposed to other things.
uiset := []echo.MiddlewareFunc{ui.SessionStoreInjector, ui.ContextCreator, ui.IPBanTest, ui.CookieLoginTest}
uiset := []echo.MiddlewareFunc{ui.SessionStoreInjector, ui.ContextCreator, rateLimiter, ui.IPBanTest, ui.CookieLoginTest}
e.RouteNotFound("/*", ui.AmWrap(AmNotFoundHandler), uiset...)
fs, err := config.AmOpenExternalContentPath()
+5
View File
@@ -88,6 +88,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
httprc = http.StatusForbidden
command = "framed"
data = "ipban.jet"
case "ratelimit":
amctxt.SetFrameTitle("Rate Limit Exceeded")
httprc = http.StatusTooManyRequests
command = "framed"
data = "ratelimit.jet"
}
// Process commands.
+29
View File
@@ -0,0 +1,29 @@
{*
* Amsterdam Web Communities System
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*}
<div class="p-4">
<!-- Rate Limit Dialog -->
<div class="max-w-2xl w-full">
<div class="bg-white border-2 border-gray-600 rounded-lg shadow-2xl overflow-hidden">
<!-- Dialog Header -->
<div class="bg-gray-600 px-6 py-4">
<h1 class="text-white text-2xl font-bold text-center flex items-center justify-center gap-3">
<span class="text-3xl">✋</span>Whoa There, Tiger!<span class="text-3xl">✋</span>
</h1>
</div>
<!-- Dialog Body -->
<div class="px-8 py-8">
<div class="text-center mb-8">
<p class="text-black-800 text-lg leading-relaxed"><b>You're hitting this site a little fast, aren't you? Calm down and try again in a bit.</b></p>
<p class="text-black-800 text-lg leading-relaxed">Offending identifier is {{ identifier }}.</p>
</div>
</div>
</div>
</div>
</div>