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.~~
|
* ~~Support all customizations that were done with the EMinds instance of Venice.~~
|
||||||
* ~~Gitea-like status page showing Go-specific internals.~~
|
* ~~Gitea-like status page showing Go-specific internals.~~
|
||||||
* Build static Tailwind CSS file rather than using remote-loaded version. (Gate on debug/prod flag)
|
* Build static Tailwind CSS file rather than using remote-loaded version. (Gate on debug/prod flag)
|
||||||
* Rate limiter.
|
* ~~Rate limiter.~~
|
||||||
* ~~Better logging configuration.~~
|
* ~~Better logging configuration.~~
|
||||||
|
|
||||||
## Immediate Cleanups Required
|
## Immediate Cleanups Required
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -35,6 +36,9 @@ const AMSTERDAM_COPYRIGHT = "2025-2026"
|
|||||||
// CONFIGFILE_NAME is the name of the standard configuration file.
|
// CONFIGFILE_NAME is the name of the standard configuration file.
|
||||||
const CONFIGFILE_NAME = "amsterdam.yaml"
|
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.
|
// AmCLI is the command-line interface arguments structure.
|
||||||
type AmCLI struct {
|
type AmCLI struct {
|
||||||
ConfigFile string `arg:"-C,--config,env:AMSTERDAM_CONFIG" help:"Location of the configuration file."`
|
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"`
|
WelcomeTitle string `yaml:"welcomeTitle"`
|
||||||
WelcomeMessage string `yaml:"welcomeMessage"`
|
WelcomeMessage string `yaml:"welcomeMessage"`
|
||||||
TopPostsTitle string `yaml:"topPostsTitle"`
|
TopPostsTitle string `yaml:"topPostsTitle"`
|
||||||
|
RateLimit struct {
|
||||||
|
Rate float64 `yaml:"rate"`
|
||||||
|
Burst int `yaml:"burst"`
|
||||||
|
ExpireMinutes int `yaml:"expireMinutes"`
|
||||||
|
} `yaml:"rateLimit"`
|
||||||
} `yaml:"site"`
|
} `yaml:"site"`
|
||||||
Database struct {
|
Database struct {
|
||||||
Driver string `yaml:"driver"`
|
Driver string `yaml:"driver"`
|
||||||
@@ -278,6 +287,14 @@ func overlayStructValue(dest, loaded, defaults reflect.Value) {
|
|||||||
} else {
|
} else {
|
||||||
fldDest.Set(fldLoaded)
|
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 {
|
} else {
|
||||||
// if we see this message, this function needs more work
|
// 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())
|
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,
|
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.
|
using one of the links above.
|
||||||
topPostsTitle: "Latest from the Conferences"
|
topPostsTitle: "Latest from the Conferences"
|
||||||
|
rateLimit:
|
||||||
|
rate: 20.0
|
||||||
|
burst: 0
|
||||||
|
expireMinutes: 3
|
||||||
database:
|
database:
|
||||||
driver: "mysql"
|
driver: "mysql"
|
||||||
dsn: "amsdb:x00yes2k@tcp(localhost)/amsterdam?parseTime=true&loc=UTC"
|
dsn: "amsdb:x00yes2k@tcp(localhost)/amsterdam?parseTime=true&loc=UTC"
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.erbosoft.com/amy/amsterdam/config"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/labstack/echo/v4/middleware"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EBUTTON is the standard error for an unknown button.
|
// 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)
|
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
|
github.com/tkuchiki/go-timezone v0.2.3
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.50.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.34.0
|
||||||
|
golang.org/x/time v0.11.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,5 +35,4 @@ require (
|
|||||||
golang.org/x/crypto v0.48.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect
|
||||||
golang.org/x/sys v0.41.0 // 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)
|
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.
|
// 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...)
|
e.RouteNotFound("/*", ui.AmWrap(AmNotFoundHandler), uiset...)
|
||||||
fs, err := config.AmOpenExternalContentPath()
|
fs, err := config.AmOpenExternalContentPath()
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ func AmSendPageData(ctxt echo.Context, amctxt AmContext, command string, data an
|
|||||||
httprc = http.StatusForbidden
|
httprc = http.StatusForbidden
|
||||||
command = "framed"
|
command = "framed"
|
||||||
data = "ipban.jet"
|
data = "ipban.jet"
|
||||||
|
case "ratelimit":
|
||||||
|
amctxt.SetFrameTitle("Rate Limit Exceeded")
|
||||||
|
httprc = http.StatusTooManyRequests
|
||||||
|
command = "framed"
|
||||||
|
data = "ratelimit.jet"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process commands.
|
// 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