add rateLimiters and Throttle middleware

This commit is contained in:
Iris Lightshard 2024-11-28 09:54:35 -07:00
parent 31f42d90ff
commit d8e9afd8da
Signed by: Iris Lightshard
GPG key ID: 688407174966CAF3
4 changed files with 100 additions and 2 deletions

View file

@ -18,7 +18,7 @@ Thinking about URL routes reminded me of the tree of light the fictional [Quartz
## usage ## usage
A more complete usage guide will be forthcoming, but for now you can check out the [quartzgun_test.go](https://nilfm.cc/git/quartzgun/tree/quartzgun_test.go) file for an overview of how to use it. You can check out the [quartzgun_test.go](./quartzgun_test.go) file for an overview of how to use it, or see projects like [nirvash](https://forge.lightcrystal.system/nilix/nirvash) and [felt](https://forge.lightcrystal.systems/nilix/felt) which use quartzgun extensively.
## roadmap/features ## roadmap/features
@ -28,6 +28,7 @@ Features may be added here at any time as things are in early stages right now:
* [x] router (static service trees, paramaterized routes, and per-method handlers on routes) * [x] router (static service trees, paramaterized routes, and per-method handlers on routes)
* [x] basic renderers (HTML template, JSON, XML) * [x] basic renderers (HTML template, JSON, XML)
* [x] rate limiters (one by IP and one that is indiscriminate)
### auth ### auth
@ -45,6 +46,7 @@ Features may be added here at any time as things are in early stages right now:
- [x] `Defend`: enact CSRF protection (use on the endpoint) - [x] `Defend`: enact CSRF protection (use on the endpoint)
- [x] `Provision`: use BASIC authentication to provision an access token - [x] `Provision`: use BASIC authentication to provision an access token
- [x] `Validate`: valiate the bearer token against the `UserStore` - [x] `Validate`: valiate the bearer token against the `UserStore`
- [x] `Throttle`: rate limit using a `func(*http.Request)bool`
## license ## license

View file

@ -167,3 +167,15 @@ func Defend(next http.Handler, userStore auth.UserStore, denied string) http.Han
return http.HandlerFunc(handlerFunc) return http.HandlerFunc(handlerFunc)
} }
func Throttle(next http.Handler, bouncer func(*http.Request) bool) http.Handler {
handlerFunc := func(w http.ResponseWriter, req *http.Request) {
allowed := bouncer(req)
if allowed {
next.ServeHTTP(w, req)
return
}
w.WriteHeader(http.StatusTooManyRequests)
}
return http.HandlerFunc(handlerFunc)
}

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"hacklab.nilfm.cc/quartzgun/indentalUserDB" "hacklab.nilfm.cc/quartzgun/indentalUserDB"
"hacklab.nilfm.cc/quartzgun/middleware" "hacklab.nilfm.cc/quartzgun/middleware"
"hacklab.nilfm.cc/quartzgun/rateLimiter"
"hacklab.nilfm.cc/quartzgun/renderer" "hacklab.nilfm.cc/quartzgun/renderer"
"hacklab.nilfm.cc/quartzgun/router" "hacklab.nilfm.cc/quartzgun/router"
"html/template" "html/template"
@ -36,6 +37,12 @@ func TestMain(m *testing.M) {
udb.AddUser("nilix", "questing") udb.AddUser("nilix", "questing")
sesh, _ := udb.InitiateSession("nilix", "questing", 60) sesh, _ := udb.InitiateSession("nilix", "questing", 60)
bouncer := rateLimiter.IpRateLimiter{
map[string]*rateLimiter.RateLimitData{},
5,
2,
}
fmt.Printf("%s // %s\n", sesh, sesh) fmt.Printf("%s // %s\n", sesh, sesh)
rtr := &router.Router{ rtr := &router.Router{
StaticPaths: map[string]string{ StaticPaths: map[string]string{
@ -56,7 +63,7 @@ func TestMain(m *testing.M) {
renderer.Template( renderer.Template(
"testData/templates/test.html"), http.MethodGet, udb, "/login")) "testData/templates/test.html"), http.MethodGet, udb, "/login"))
rtr.Get("/json", ApiSomething(renderer.JSON("apiData"))) rtr.Get("/json", middleware.Throttle(ApiSomething(renderer.JSON("apiData")), bouncer.RateLimit))
rtr.Get(`/thing/(?P<Thing>\w+)`, renderer.Template("testData/templates/paramTest.html")) rtr.Get(`/thing/(?P<Thing>\w+)`, renderer.Template("testData/templates/paramTest.html"))

View file

@ -0,0 +1,77 @@
package rateLimiter
import (
"net/http"
"strings"
"time"
)
type RateLimitData struct {
Attempts int
LastAccess time.Time
}
// limit by IP
type IpRateLimiter struct {
Data map[string]*RateLimitData
Seconds int
AttemptsAllowed int
}
func (self *IpRateLimiter) RateLimit(req *http.Request) bool {
curtime := time.Now()
addrWithPort := req.RemoteAddr
addrParts := strings.Split(addrWithPort, ":")
addr := strings.Join(addrParts[:len(addrParts)-1], ":")
data, exists := self.Data[addr]
if !exists {
self.Data[addr] = &RateLimitData{1, curtime}
return true
}
if curtime.Sub(data.LastAccess).Seconds() > float64(self.Seconds) {
data.Attempts = 1
data.LastAccess = curtime
return true
}
if self.AttemptsAllowed > data.Attempts {
data.Attempts++
data.LastAccess = curtime
return true
}
data.Attempts++
return false
}
// limit regardless of IP
type IndiscriminateRateLimiter struct {
Attempts int
AttemptsAllowed int
Seconds int
LastAccess time.Time
}
func (self *IndiscriminateRateLimiter) RateLimit(req *http.Request) bool {
curtime := time.Now()
if curtime.Sub(self.LastAccess).Seconds() > float64(self.Seconds) {
self.Attempts = 1
self.LastAccess = curtime
return true
}
if self.AttemptsAllowed > self.Attempts {
self.Attempts++
self.LastAccess = curtime
return true
}
self.Attempts++
return false
}