Skip to content

Commit 4ba6f0a

Browse files
authored
Implement rate limiting (#26)
1 parent a740c1e commit 4ba6f0a

11 files changed

+88
-25
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Available flags:
3838
- Type: string, space-separated list of IP addresses or URLs
3939
- Default value: none, requests are made directly
4040
- Also available as `PROXY` environment variable
41+
- `-ratelimit`
42+
- Sets the maximum number of requests per minute per IP address
43+
- Type: unsigned integer
44+
- Default value: 512
4145
- `-verbose`
4246
- Allows to put the app into verbose mode and print out additional logs to stdout
4347
- Default value: none, no additional output is produced

config/config.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
)
1111

1212
type config struct {
13-
mu sync.RWMutex
1413
cacheTTL time.Duration
1514
maintenanceTTL time.Duration
15+
mu sync.RWMutex
1616
port int
1717
proxyList []string
1818
proxySwitcher colly.ProxyFunc
19+
rateLimit int64
1920
verbosity bool
2021
}
2122

@@ -29,6 +30,7 @@ func getInstance() *config {
2930
maintenanceTTL: 5 * time.Minute,
3031
port: 8001,
3132
proxyList: nil,
33+
rateLimit: 512,
3234
verbosity: false,
3335
}
3436
})
@@ -102,11 +104,24 @@ func GetVerbosity() bool {
102104
return getInstance().verbosity
103105
}
104106

107+
func SetRateLimit(rateLimit int64) {
108+
getInstance().mu.Lock()
109+
defer getInstance().mu.Unlock()
110+
getInstance().rateLimit = rateLimit
111+
}
112+
113+
func GetRateLimit() int64 {
114+
getInstance().mu.RLock()
115+
defer getInstance().mu.RUnlock()
116+
return getInstance().rateLimit
117+
}
118+
105119
func PrintConfig() {
106120
fmt.Printf("Configuration:\n" +
107121
fmt.Sprintf("\tPort:\t\t%v\n", GetPort()) +
108122
fmt.Sprintf("\tProxies:\t%v\n", GetProxyList()) +
109123
fmt.Sprintf("\tVerbosity:\t%v\n", GetVerbosity()) +
110-
fmt.Sprintf("\tCache TTL:\t%v\n", GetCacheTTL()),
124+
fmt.Sprintf("\tCache TTL:\t%v\n", GetCacheTTL()) +
125+
fmt.Sprintf("\tRate limit:\t%v/min\n", GetRateLimit()),
111126
)
112127
}

docs/openapi.json

+4
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@
103103
"proxies": {
104104
"type": "number"
105105
},
106+
"rateLimit": {
107+
"type": "number",
108+
"example": 512
109+
},
106110
"uptime": {
107111
"type": "string",
108112
"example": "2m49s"

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/gocolly/colly/v2 v2.1.0
77
github.com/patrickmn/go-cache v2.1.0+incompatible
88
github.com/sa-/slicefunk v0.1.4
9+
github.com/ulule/limiter/v3 v3.11.2
910
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
1011
)
1112

@@ -19,6 +20,7 @@ require (
1920
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
2021
github.com/golang/protobuf v1.5.3 // indirect
2122
github.com/kennygrant/sanitize v1.2.4 // indirect
23+
github.com/pkg/errors v0.9.1 // indirect
2224
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect
2325
github.com/temoto/robotstxt v1.1.2 // indirect
2426
golang.org/x/net v0.19.0 // indirect

go.sum

+8-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8Nz
6363
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
6464
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
6565
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
66+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
67+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
6668
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6769
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
6870
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -73,11 +75,14 @@ github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7
7375
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
7476
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
7577
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
76-
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
7778
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
79+
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
80+
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7881
github.com/temoto/robotstxt v1.1.1/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
7982
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
8083
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
84+
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
85+
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
8186
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
8287
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
8388
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
@@ -179,5 +184,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
179184
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
180185
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
181186
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
187+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
188+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
182189
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
183190
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

handlers/ListenAndServe.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ func ListenAndServe() {
2222
mux.HandleFunc("/", catchall)
2323

2424
middlewareStack := middleware.CreateStack(
25-
middleware.SetHeaders,
25+
middleware.GetSetHeadersMiddleware(),
26+
middleware.GetRateLimitMiddleware(),
2627
)
2728

2829
log.Println("Listening for requests")

handlers/getStatus.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
var initTime = time.Now()
13-
var version = "1.9.3"
13+
var version = "1.9.4"
1414

1515
func getStatus(w http.ResponseWriter, r *http.Request) {
1616
json.NewEncoder(w).Encode(map[string]interface{}{
@@ -27,9 +27,10 @@ func getStatus(w http.ResponseWriter, r *http.Request) {
2727
"maintenanceStatus": config.GetMaintenanceStatusTTL().Round(time.Minute).String(),
2828
},
2929
},
30-
"docs": docsLink,
31-
"proxies": len(config.GetProxyList()),
32-
"uptime": time.Since(initTime).Round(time.Second).String(),
33-
"version": version,
30+
"docs": docsLink,
31+
"proxies": len(config.GetProxyList()),
32+
"rateLimit": config.GetRateLimit(),
33+
"uptime": time.Since(initTime).Round(time.Second).String(),
34+
"version": version,
3435
})
3536
}

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func main() {
1616
flagMaintenanceTTL := flag.Int("maintenancettl", 5, "Allows to limit how frequently scraper can check for maintenance end in minutes")
1717
flagPort := flag.Int("port", 8001, "Port to catch requests on")
1818
flagProxy := flag.String("proxy", "", "Open proxy address to make requests to BDO servers")
19+
flagRateLimit := flag.Int64("ratelimit", 512, "Maximum number of requests per minute per IP")
1920
flagVerbose := flag.Bool("verbose", false, "Print out additional logs into stdout")
2021
flag.Parse()
2122

@@ -42,6 +43,7 @@ func main() {
4243
config.SetCacheTTL(time.Duration(*flagCacheTTL) * time.Minute)
4344
config.SetMaintenanceStatusTTL(time.Duration(*flagMaintenanceTTL) * time.Minute)
4445
config.SetVerbosity(*flagVerbose)
46+
config.SetRateLimit(*flagRateLimit)
4547

4648
config.PrintConfig()
4749
handlers.ListenAndServe()

middleware/GetRateLimitMiddleware.go

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package middleware
2+
3+
import (
4+
"time"
5+
6+
"github.com/ulule/limiter/v3"
7+
"github.com/ulule/limiter/v3/drivers/middleware/stdlib"
8+
"github.com/ulule/limiter/v3/drivers/store/memory"
9+
10+
"bdo-rest-api/config"
11+
)
12+
13+
func GetRateLimitMiddleware() Middleware {
14+
var rate = limiter.Rate{
15+
Limit: config.GetRateLimit(),
16+
Period: time.Minute,
17+
}
18+
var store = memory.NewStore()
19+
var instance = limiter.New(store, rate, limiter.WithClientIPHeader("CF-Connecting-IP"))
20+
21+
var middleware = stdlib.NewMiddleware(instance)
22+
return middleware.Handler
23+
}

middleware/GetSetHeadersMiddleware.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"time"
6+
)
7+
8+
func GetSetHeadersMiddleware() Middleware {
9+
var setHeaders = func(next http.Handler) http.Handler {
10+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
11+
w.Header().Set("Access-Control-Allow-Origin", "*")
12+
w.Header().Set("Content-Type", "application/json")
13+
w.Header().Set("Date", time.Now().Format(time.RFC1123Z))
14+
15+
next.ServeHTTP(w, r)
16+
})
17+
}
18+
19+
return setHeaders
20+
}

middleware/SetHeaders.go

-16
This file was deleted.

0 commit comments

Comments
 (0)