a jab at rate limiting! 🔥

This commit is contained in:
Wouter Groeneveld 2021-04-11 15:42:44 +02:00
parent 0bb76043fd
commit 9a07341d0e
8 changed files with 104 additions and 25 deletions

View File

@ -4,7 +4,7 @@ Go module `brainbaking.com/go-jamming`:
> A minimalistic Go-powered jamstack-augmented microservice for webmentions etc
✅️ **This is a fork of [https://github.com/wgroeneveld/serve-my-jams](serve-my-jams)**, the Node-powered original microservice, which is no longer being maintained.
✅️ **This is a fork of [serve-my-jams](https://github.com/wgroeneveld/serve-my-jams)**, the Node-powered original microservice, which is no longer being maintained.
**Are you looking for a way to DO something with this?** See https://github.com/wgroeneveld/jam-my-stack !

90
app/limiter.go Normal file
View File

@ -0,0 +1,90 @@
package app
import (
"brainbaking.com/go-jamming/common"
"brainbaking.com/go-jamming/rest"
"github.com/rs/zerolog/log"
"golang.org/x/time/rate"
"net/http"
"sync"
"time"
)
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
type RateLimiter struct {
visitors map[string]*visitor
mu sync.RWMutex
rateLimitPerSec int
rateBurst int
Middleware func(next http.Handler) http.Handler
}
func NewRateLimiter(rateLimitPerSec int, rateBurst int) *RateLimiter {
rl := &RateLimiter{
visitors: make(map[string]*visitor),
mu: sync.RWMutex{},
rateBurst: rateBurst,
rateLimitPerSec: rateLimitPerSec,
}
rl.Middleware = func(next http.Handler) http.Handler {
return rl.limiterMiddleware(next)
}
go rl.cleanupVisitors()
return rl
}
const (
ttl = 5 * time.Minute
cleanupCron = 2 * time.Minute
)
func (rl *RateLimiter) getVisitor(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
v, exists := rl.visitors[ip]
if !exists {
limiter := rate.NewLimiter(rate.Limit(rl.rateLimitPerSec), rl.rateBurst)
rl.visitors[ip] = &visitor{limiter, common.Now()}
return limiter
}
v.lastSeen = common.Now()
return v.limiter
}
func (rl *RateLimiter) cleanupVisitors() {
for {
time.Sleep(cleanupCron)
rl.mu.Lock()
for ip, v := range rl.visitors {
if time.Since(v.lastSeen) > ttl {
log.Debug().Str("ip", ip).Msg("Cleaning up rate limiter visitor")
delete(rl.visitors, ip)
}
}
rl.mu.Unlock()
}
}
// with the help of https://www.alexedwards.net/blog/how-to-rate-limit-http-requests, TY!
func (rl *RateLimiter) limiterMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr // also contains port, but don't care
limiter := rl.getVisitor(ip)
if limiter.Allow() == false {
log.Error().Str("ip", ip).Msg("Someone spamming? Rate limit hit!")
rest.TooManyRequests(w)
return
}
next.ServeHTTP(w, r)
})
}

View File

@ -17,7 +17,7 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.ResponseWriter.WriteHeader(code)
}
func loggingMiddleware(next http.Handler) http.Handler {
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
logWriter := &loggingResponseWriter{w, http.StatusOK}
next.ServeHTTP(logWriter, r)

View File

@ -35,7 +35,8 @@ func Start() {
server.routes()
http.Handle("/", r)
r.Use(loggingMiddleware)
r.Use(LoggingMiddleware)
r.Use(NewRateLimiter(5, 10).Middleware)
log.Info().Int("port", server.conf.Port).Msg("Serving...")
http.ListenAndServe(":"+strconv.Itoa(server.conf.Port), nil)

1
go.mod
View File

@ -8,5 +8,6 @@ require (
github.com/hashicorp/go-retryablehttp v0.6.8
github.com/rs/zerolog v1.21.0
github.com/stretchr/testify v1.7.0
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
willnorris.com/go/microformats v1.1.1
)

2
go.sum
View File

@ -46,6 +46,8 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=

View File

@ -1,24 +1,5 @@
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
)
func mainz() {
fmt.Println("Hello, playground")
resp, err := http.Get("https://brainbaking.com/notes")
if err != nil {
log.Fatalln(err)
}
body, err2 := ioutil.ReadAll(resp.Body)
if err2 != nil {
log.Fatalln(err)
}
fmt.Printf("tis ditte")
fmt.Printf("%s", body)
//time.Tick()
}

View File

@ -8,11 +8,15 @@ import (
// mimicing NotFound: https://golang.org/src/net/http/server.go?s=64787:64830#L2076
func BadRequest(w http.ResponseWriter) {
http.Error(w, "400 bad request", http.StatusBadRequest)
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
}
func TooManyRequests(w http.ResponseWriter) {
http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests)
}
func Unauthorized(w http.ResponseWriter) {
http.Error(w, "401 unauthorized", http.StatusUnauthorized)
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
func Json(w http.ResponseWriter, data interface{}) {