add rateLimiters and Throttle middleware
This commit is contained in:
parent
31f42d90ff
commit
d8e9afd8da
4 changed files with 100 additions and 2 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
||||||
|
|
77
rateLimiter/rateLimiter.go
Normal file
77
rateLimiter/rateLimiter.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue