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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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] basic renderers (HTML template, JSON, XML)
|
||||
* [x] rate limiters (one by IP and one that is indiscriminate)
|
||||
|
||||
### 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] `Provision`: use BASIC authentication to provision an access token
|
||||
- [x] `Validate`: valiate the bearer token against the `UserStore`
|
||||
- [x] `Throttle`: rate limit using a `func(*http.Request)bool`
|
||||
|
||||
## license
|
||||
|
||||
|
|
|
@ -167,3 +167,15 @@ func Defend(next http.Handler, userStore auth.UserStore, denied string) http.Han
|
|||
|
||||
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"
|
||||
"hacklab.nilfm.cc/quartzgun/indentalUserDB"
|
||||
"hacklab.nilfm.cc/quartzgun/middleware"
|
||||
"hacklab.nilfm.cc/quartzgun/rateLimiter"
|
||||
"hacklab.nilfm.cc/quartzgun/renderer"
|
||||
"hacklab.nilfm.cc/quartzgun/router"
|
||||
"html/template"
|
||||
|
@ -36,6 +37,12 @@ func TestMain(m *testing.M) {
|
|||
udb.AddUser("nilix", "questing")
|
||||
sesh, _ := udb.InitiateSession("nilix", "questing", 60)
|
||||
|
||||
bouncer := rateLimiter.IpRateLimiter{
|
||||
map[string]*rateLimiter.RateLimitData{},
|
||||
5,
|
||||
2,
|
||||
}
|
||||
|
||||
fmt.Printf("%s // %s\n", sesh, sesh)
|
||||
rtr := &router.Router{
|
||||
StaticPaths: map[string]string{
|
||||
|
@ -56,7 +63,7 @@ func TestMain(m *testing.M) {
|
|||
renderer.Template(
|
||||
"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"))
|
||||
|
||||
|
|
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