added the rate limiter - Whoa There, Tiger!
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user