diff --git a/Featurecreep.md b/Featurecreep.md
index 2c00257c..6523bef0 100644
--- a/Featurecreep.md
+++ b/Featurecreep.md
@@ -155,6 +155,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [x] /info endpoint, in dem dann zb die limits und version etc steht
* [x] Bindata for templates
* [x] User struct should have a field for the avatar url (-> gravatar md5 calculated by the backend)
+* [x] Middleware to have configurable rate-limiting per user
* [ ] Endpoint to get all possible rights with description and code
* [ ] Reminders via mail
* [ ] Be able to "really" delete the account -> delete all lists and move ownership for others
@@ -164,7 +165,6 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [ ] All `ReadAll` methods should return the number of items per page, the number of items on this page, the total pages and the items
-> Check if there's a way to do that efficently. Maybe only implementing it in the web handler.
* [ ] Move lists between namespaces -> Extra endpoint
-* [ ] Middleware to have configurable rate-limiting per user
### Infra
diff --git a/config.yml.sample b/config.yml.sample
index b768881d..245e45f6 100644
--- a/config.yml.sample
+++ b/config.yml.sample
@@ -91,4 +91,16 @@ log:
# Whether to log http requests or not. Possible values are stdout, stderr, file or off to disable http logging.
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
- echo: "off"
\ No newline at end of file
+ echo: "off"
+
+ratelimit:
+ # whether or not to enable the rate limit
+ enabled: false
+ # The kind on which rates are based. Can be either "user" for a rate limit per user or "ip" for an ip-based rate limit.
+ kind: user
+ # The time period in seconds for the limit
+ period: 60
+ # The max number of requests a user is allowed to do in the configured time period
+ limit: 100
+ # The store where the limit counter for each user is stored. Possible values are "memory" or "redis"
+ store: memory
diff --git a/docs/content/doc/setup/config.md b/docs/content/doc/setup/config.md
index 64eb7442..1644408f 100644
--- a/docs/content/doc/setup/config.md
+++ b/docs/content/doc/setup/config.md
@@ -135,4 +135,16 @@ log:
http: "stdout"
# Echo has its own logging which usually is unnessecary, which is why it is disabled by default. Possible values are stdout, stderr, file or off to disable standard logging.
echo: "off"
+
+ratelimit:
+ # whether or not to enable the rate limit
+ enabled: false
+ # The kind on which rates are based. Can be either "user" for a rate limit per user or "ip" for an ip-based rate limit.
+ kind: user
+ # The time period in seconds for the limit
+ period: 60
+ # The max number of requests a user is allowed to do in the configured time period
+ limit: 100
+ # The store where the limit counter for each user is stored. Possible values are "memory" or "redis"
+ store: memory
{{< /highlight >}}
diff --git a/go.mod b/go.mod
index 08a60931..9d3b12f3 100644
--- a/go.mod
+++ b/go.mod
@@ -64,6 +64,7 @@ require (
github.com/spf13/viper v1.3.2
github.com/stretchr/testify v1.3.0
github.com/swaggo/swag v1.5.0
+ github.com/ulule/limiter/v3 v3.3.0
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4
golang.org/x/lint v0.0.0-20190409202823-959b441ac422
google.golang.org/appengine v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index a6ff15c2..750d81d0 100644
--- a/go.sum
+++ b/go.sum
@@ -20,6 +20,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
+github.com/astaxie/beego v1.10.0/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
@@ -52,6 +53,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
+github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/go-openapi/jsonpointer v0.17.0 h1:nH6xp8XdXHx8dqveo0ZuJBluCO2qGrPbDNZ0dwoRHP0=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
@@ -69,6 +71,7 @@ github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.0 h1:Kg7Wl7LkTPlmc393QZQ/5rQadPhi7pBVEMZxyTi0Ii8=
github.com/go-openapi/swag v0.19.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
+github.com/go-redis/redis v6.14.0+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4=
github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -114,6 +117,7 @@ github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb h1:D5s1HIu80AcM
github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb/go.mod h1:82TxjOpWQiPmywlbIaB2ZkqJoSYJdLGPgAJDvM3PbKc=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible h1:PkEEpmbrFXlMul8cOplR8nkcIM/NDbx+H6fq2+vaKAA=
github.com/kolaente/caldav-go v3.0.1-0.20190524174923-9e5cd1688227+incompatible/go.mod h1:y1UhTNI4g0hVymJrI6yJ5/ohy09hNBeU8iJEZjgdDOw=
@@ -127,6 +131,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
+github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.1.5/go.mod h1:3LbYC6VkwmUnmLPZ8WFdHdQHG77e9GQbjyhWdb1QvC4=
github.com/labstack/echo/v4 v4.1.6 h1:WOvLa4T1KzWCRpANwz0HGgWDelXSSGwIKtKBbFdHTv4=
github.com/labstack/echo/v4 v4.1.6/go.mod h1:kU/7PwzgNxZH4das4XNsSpBSOD09XIF5YEPzjpkGnGE=
@@ -171,12 +177,14 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88=
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -233,10 +241,13 @@ github.com/swaggo/gin-swagger v1.1.0/go.mod h1:FQlm07YuT1glfN3hQiO11UQ2m39vOCZ/a
github.com/swaggo/swag v1.4.0/go.mod h1:hog2WgeMOrQ/LvQ+o1YGTeT+vWVrbi0SiIslBtxKTyM=
github.com/swaggo/swag v1.5.0 h1:haK8VG3hj+v/c8hQ4f3U+oYpkdI/26m9LAUTXHOv+2U=
github.com/swaggo/swag v1.5.0/go.mod h1:+xZrnu5Ut3GcUkKAJm9spnOooIS1WB1cUOkLNPrvrE0=
+github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
+github.com/ulule/limiter/v3 v3.3.0 h1:DuMRthpkl1wW9Em6xOVw5HMHnbDumSIDydiMqP0PTXs=
+github.com/ulule/limiter/v3 v3.3.0/go.mod h1:E6sfg3hfRgW+yFvkE/rZf6YLqXYFMWTmZaZKvdEiQsA=
github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@@ -246,6 +257,7 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
+golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -261,6 +273,7 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 2ae2541b..7ceb82c1 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -78,6 +78,12 @@ const (
LogHTTP Key = `log.echo`
LogEcho Key = `log.echo`
LogPath Key = `log.path`
+
+ RateLimitEnabled Key = `ratelimit.enabled`
+ RateLimitKind Key = `ratelimit.kind`
+ RateLimitPeriod Key = `ratelimit.period`
+ RateLimitLimit Key = `ratelimit.limit`
+ RateLimitStore Key = `ratelimit.store`
)
// GetString returns a string config value
@@ -95,6 +101,11 @@ func (k Key) GetInt() int {
return viper.GetInt(string(k))
}
+// GetInt64 returns an int64 config value
+func (k Key) GetInt64() int64 {
+ return viper.GetInt64(string(k))
+}
+
// GetDuration returns a duration config value
func (k Key) GetDuration() time.Duration {
return viper.GetDuration(string(k))
@@ -174,6 +185,12 @@ func InitConfig() {
LogHTTP.setDefault("stdout")
LogEcho.setDefault("off")
LogPath.setDefault(ServiceRootpath.GetString() + "/logs")
+ // Rate Limit
+ RateLimitEnabled.setDefault(false)
+ RateLimitKind.setDefault("user")
+ RateLimitLimit.setDefault(100)
+ RateLimitPeriod.setDefault(60)
+ RateLimitStore.setDefault("memory")
// Init checking for environment variables
viper.SetEnvPrefix("vikunja")
diff --git a/pkg/log/logging.go b/pkg/log/logging.go
index d957b71c..70e8c9e5 100644
--- a/pkg/log/logging.go
+++ b/pkg/log/logging.go
@@ -116,7 +116,7 @@ func Debug(args ...interface{}) {
// Debugf is for debug messages
func Debugf(format string, args ...interface{}) {
- logInstance.Debugf(format, args)
+ logInstance.Debugf(format, args...)
}
// Info is for info messages
@@ -126,7 +126,7 @@ func Info(args ...interface{}) {
// Infof is for info messages
func Infof(format string, args ...interface{}) {
- logInstance.Infof(format, args)
+ logInstance.Infof(format, args...)
}
// Error is for error messages
@@ -136,7 +136,7 @@ func Error(args ...interface{}) {
// Errorf is for error messages
func Errorf(format string, args ...interface{}) {
- logInstance.Errorf(format, args)
+ logInstance.Errorf(format, args...)
}
// Warning is for warning messages
@@ -146,7 +146,7 @@ func Warning(args ...interface{}) {
// Warningf is for warning messages
func Warningf(format string, args ...interface{}) {
- logInstance.Warningf(format, args)
+ logInstance.Warningf(format, args...)
}
// Critical is for critical messages
@@ -156,7 +156,7 @@ func Critical(args ...interface{}) {
// Criticalf is for critical messages
func Criticalf(format string, args ...interface{}) {
- logInstance.Critical(format, args)
+ logInstance.Criticalf(format, args...)
}
// Fatal is for fatal messages
@@ -166,5 +166,5 @@ func Fatal(args ...interface{}) {
// Fatalf is for fatal messages
func Fatalf(format string, args ...interface{}) {
- logInstance.Fatal(format, args)
+ logInstance.Fatalf(format, args...)
}
diff --git a/pkg/routes/rate_limit.go b/pkg/routes/rate_limit.go
new file mode 100644
index 00000000..e3434d47
--- /dev/null
+++ b/pkg/routes/rate_limit.go
@@ -0,0 +1,103 @@
+// Copyright 2019 Vikunja and contriubtors. All rights reserved.
+//
+// This file is part of Vikunja.
+//
+// Vikunja is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Vikunja is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Vikunja. If not, see .
+
+package routes
+
+import (
+ "code.vikunja.io/api/pkg/config"
+ "code.vikunja.io/api/pkg/log"
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/api/pkg/red"
+ "github.com/labstack/echo/v4"
+ "github.com/ulule/limiter/v3"
+ "github.com/ulule/limiter/v3/drivers/store/memory"
+ "github.com/ulule/limiter/v3/drivers/store/redis"
+ "net/http"
+ "strconv"
+ "time"
+)
+
+// RateLimit is the rate limit middleware
+func RateLimit(rateLimiter *limiter.Limiter) echo.MiddlewareFunc {
+ return func(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) (err error) {
+ var rateLimitKey string
+ switch config.RateLimitKind.GetString() {
+ case "ip":
+ rateLimitKey = c.RealIP()
+ case "user":
+ user, err := models.GetCurrentUser(c)
+ if err != nil {
+ log.Errorf("Error while getting the current user for rate limiting: %s", err)
+ }
+ rateLimitKey = "user_" + strconv.FormatInt(user.ID, 10)
+ default:
+ log.Errorf("Unknown rate limit kind configured: %s", config.RateLimitKind.GetString())
+ }
+ limiterCtx, err := rateLimiter.Get(c.Request().Context(), rateLimitKey)
+ if err != nil {
+ log.Errorf("IPRateLimit - rateLimiter.Get - err: %v, %s on %s", err, rateLimitKey, c.Request().URL)
+ return c.JSON(http.StatusInternalServerError, echo.Map{
+ "message": err,
+ })
+ }
+
+ h := c.Response().Header()
+ h.Set("X-RateLimit-Limit", strconv.FormatInt(limiterCtx.Limit, 10))
+ h.Set("X-RateLimit-Remaining", strconv.FormatInt(limiterCtx.Remaining, 10))
+ h.Set("X-RateLimit-Reset", strconv.FormatInt(limiterCtx.Reset, 10))
+
+ if limiterCtx.Reached {
+ log.Infof("Too Many Requests from %s on %s", rateLimitKey, c.Request().URL)
+ return c.JSON(http.StatusTooManyRequests, echo.Map{
+ "message": "Too Many Requests on " + c.Request().URL.String(),
+ })
+ }
+
+ // log.Printf("%s request continue", c.RealIP())
+ return next(c)
+ }
+ }
+}
+
+func setupRateLimit(a *echo.Group) {
+ if config.RateLimitEnabled.GetBool() {
+ rate := limiter.Rate{
+ Period: config.RateLimitPeriod.GetDuration() * time.Second,
+ Limit: config.RateLimitLimit.GetInt64(),
+ }
+ var store limiter.Store
+ var err error
+ switch config.RateLimitStore.GetString() {
+ case "memory":
+ store = memory.NewStore()
+ case "redis":
+ if !config.RedisEnabled.GetBool() {
+ log.Fatal("Redis is configured for rate limiting, but not enabled!")
+ }
+ store, err = redis.NewStore(red.GetRedis())
+ if err != nil {
+ log.Fatalf("Error while creating rate limit redis store: %s", err)
+ }
+ default:
+ log.Fatalf("Unknown Rate limit store \"%s\"", config.RateLimitStore.GetString())
+ }
+ rateLimiter := limiter.New(store, rate)
+ log.Debugf("Rate limit configured with %s and %v requests per %v", config.RateLimitStore.GetString(), rate.Limit, rate.Period)
+ a.Use(RateLimit(rateLimiter))
+ }
+}
diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go
index 93986b09..0114e242 100644
--- a/pkg/routes/routes.go
+++ b/pkg/routes/routes.go
@@ -218,10 +218,13 @@ func registerAPIRoutes(a *echo.Group) {
// Info endpoint
a.GET("/info", apiv1.Info)
- // ===== Routes with Authetification =====
+ // ===== Routes with Authetication =====
// Authetification
a.Use(middleware.JWT([]byte(config.ServiceJWTSecret.GetString())))
+ // Rate limit
+ setupRateLimit(a)
+
// Middleware to collect metrics
if config.ServiceJWTSecret.GetBool() {
a.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
diff --git a/vendor/github.com/hashicorp/hcl/.gitignore b/vendor/github.com/hashicorp/hcl/.gitignore
index 822fa09f..15586a2b 100644
--- a/vendor/github.com/hashicorp/hcl/.gitignore
+++ b/vendor/github.com/hashicorp/hcl/.gitignore
@@ -1,9 +1,9 @@
-y.output
-
-# ignore intellij files
-.idea
-*.iml
-*.ipr
-*.iws
-
-*.test
+y.output
+
+# ignore intellij files
+.idea
+*.iml
+*.ipr
+*.iws
+
+*.test
diff --git a/vendor/github.com/hashicorp/hcl/Makefile b/vendor/github.com/hashicorp/hcl/Makefile
index 9fafd501..84fd743f 100644
--- a/vendor/github.com/hashicorp/hcl/Makefile
+++ b/vendor/github.com/hashicorp/hcl/Makefile
@@ -1,18 +1,18 @@
-TEST?=./...
-
-default: test
-
-fmt: generate
- go fmt ./...
-
-test: generate
- go get -t ./...
- go test $(TEST) $(TESTARGS)
-
-generate:
- go generate ./...
-
-updatedeps:
- go get -u golang.org/x/tools/cmd/stringer
-
-.PHONY: default generate test updatedeps
+TEST?=./...
+
+default: test
+
+fmt: generate
+ go fmt ./...
+
+test: generate
+ go get -t ./...
+ go test $(TEST) $(TESTARGS)
+
+generate:
+ go generate ./...
+
+updatedeps:
+ go get -u golang.org/x/tools/cmd/stringer
+
+.PHONY: default generate test updatedeps
diff --git a/vendor/github.com/pelletier/go-toml/example-crlf.toml b/vendor/github.com/pelletier/go-toml/example-crlf.toml
index 3d902f28..12950a16 100644
--- a/vendor/github.com/pelletier/go-toml/example-crlf.toml
+++ b/vendor/github.com/pelletier/go-toml/example-crlf.toml
@@ -1,29 +1,29 @@
-# This is a TOML document. Boom.
-
-title = "TOML Example"
-
-[owner]
-name = "Tom Preston-Werner"
-organization = "GitHub"
-bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
-dob = 1979-05-27T07:32:00Z # First class dates? Why not?
-
-[database]
-server = "192.168.1.1"
-ports = [ 8001, 8001, 8002 ]
-connection_max = 5000
-enabled = true
-
-[servers]
-
- # You can indent as you please. Tabs or spaces. TOML don't care.
- [servers.alpha]
- ip = "10.0.0.1"
- dc = "eqdc10"
-
- [servers.beta]
- ip = "10.0.0.2"
- dc = "eqdc10"
-
-[clients]
-data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
+# This is a TOML document. Boom.
+
+title = "TOML Example"
+
+[owner]
+name = "Tom Preston-Werner"
+organization = "GitHub"
+bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
+dob = 1979-05-27T07:32:00Z # First class dates? Why not?
+
+[database]
+server = "192.168.1.1"
+ports = [ 8001, 8001, 8002 ]
+connection_max = 5000
+enabled = true
+
+[servers]
+
+ # You can indent as you please. Tabs or spaces. TOML don't care.
+ [servers.alpha]
+ ip = "10.0.0.1"
+ dc = "eqdc10"
+
+ [servers.beta]
+ ip = "10.0.0.2"
+ dc = "eqdc10"
+
+[clients]
+data = [ ["gamma", "delta"], [1, 2] ] # just an update to make sure parsers support it
diff --git a/vendor/github.com/spf13/afero/.travis.yml b/vendor/github.com/spf13/afero/.travis.yml
index 8fc1261c..0637db72 100644
--- a/vendor/github.com/spf13/afero/.travis.yml
+++ b/vendor/github.com/spf13/afero/.travis.yml
@@ -1,21 +1,21 @@
-sudo: false
-language: go
-
-go:
- - 1.9
- - "1.10"
- - tip
-
-os:
- - linux
- - osx
-
-matrix:
- allow_failures:
- - go: tip
- fast_finish: true
-
-script:
- - go build
- - go test -race -v ./...
-
+sudo: false
+language: go
+
+go:
+ - 1.9
+ - "1.10"
+ - tip
+
+os:
+ - linux
+ - osx
+
+matrix:
+ allow_failures:
+ - go: tip
+ fast_finish: true
+
+script:
+ - go build
+ - go test -race -v ./...
+
diff --git a/vendor/github.com/ulule/limiter/v3/.editorconfig b/vendor/github.com/ulule/limiter/v3/.editorconfig
new file mode 100644
index 00000000..c7d5dfb1
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/.editorconfig
@@ -0,0 +1,25 @@
+root = true
+
+[*]
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+insert_final_newline = true
+charset = utf-8
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[*.go]
+indent_size = 8
+indent_style = tab
+
+[*.json]
+indent_size = 4
+indent_style = space
+
+[Makefile]
+indent_style = tab
+indent_size = 4
diff --git a/vendor/github.com/ulule/limiter/v3/.gitignore b/vendor/github.com/ulule/limiter/v3/.gitignore
new file mode 100644
index 00000000..61ead866
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/.gitignore
@@ -0,0 +1 @@
+/vendor
diff --git a/vendor/github.com/ulule/limiter/v3/.golangci.yml b/vendor/github.com/ulule/limiter/v3/.golangci.yml
new file mode 100644
index 00000000..9ecb3763
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/.golangci.yml
@@ -0,0 +1,81 @@
+run:
+ concurrency: 4
+ deadline: 1m
+ issues-exit-code: 1
+ tests: true
+
+
+output:
+ format: colored-line-number
+ print-issued-lines: true
+ print-linter-name: true
+
+
+linters-settings:
+ errcheck:
+ check-type-assertions: false
+ check-blank: false
+ govet:
+ check-shadowing: false
+ use-installed-packages: false
+ golint:
+ min-confidence: 0.8
+ gofmt:
+ simplify: true
+ gocyclo:
+ min-complexity: 10
+ maligned:
+ suggest-new: true
+ dupl:
+ threshold: 80
+ goconst:
+ min-len: 3
+ min-occurrences: 3
+ misspell:
+ locale: US
+ lll:
+ line-length: 120
+ unused:
+ check-exported: false
+ unparam:
+ algo: cha
+ check-exported: false
+ nakedret:
+ max-func-lines: 30
+
+linters:
+ enable:
+ - megacheck
+ - govet
+ - errcheck
+ - gas
+ - structcheck
+ - varcheck
+ - ineffassign
+ - deadcode
+ - typecheck
+ - golint
+ - interfacer
+ - unconvert
+ - gocyclo
+ - gofmt
+ - misspell
+ - lll
+ - nakedret
+ enable-all: false
+ disable:
+ - depguard
+ - prealloc
+ - dupl
+ - maligned
+ disable-all: false
+
+
+issues:
+ exclude-use-default: false
+ max-per-linter: 1024
+ max-same: 1024
+ exclude:
+ - "G304"
+ - "G101"
+ - "G104"
diff --git a/vendor/github.com/ulule/limiter/v3/AUTHORS b/vendor/github.com/ulule/limiter/v3/AUTHORS
new file mode 100644
index 00000000..c4be8901
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/AUTHORS
@@ -0,0 +1,5 @@
+Primary contributors:
+
+ Gilles FABIO
+ Florent MESSA
+ Thomas LE ROUX
diff --git a/vendor/github.com/ulule/limiter/v3/LICENSE b/vendor/github.com/ulule/limiter/v3/LICENSE
new file mode 100644
index 00000000..cb93018e
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2018 Ulule
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/ulule/limiter/v3/Makefile b/vendor/github.com/ulule/limiter/v3/Makefile
new file mode 100644
index 00000000..90f076e0
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/Makefile
@@ -0,0 +1,7 @@
+.PHONY: test lint
+
+test:
+ @(scripts/test)
+
+lint:
+ @(scripts/lint)
diff --git a/vendor/github.com/ulule/limiter/v3/README.md b/vendor/github.com/ulule/limiter/v3/README.md
new file mode 100644
index 00000000..4f949cd9
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/README.md
@@ -0,0 +1,185 @@
+# Limiter
+
+[![Documentation][godoc-img]][godoc-url]
+![License][license-img]
+[![Build Status][circle-img]][circle-url]
+[![Go Report Card][goreport-img]][goreport-url]
+
+*Dead simple rate limit middleware for Go.*
+
+* Simple API
+* "Store" approach for backend
+* Redis support (but not tied too)
+* Middlewares: HTTP and [Gin][4]
+
+## Installation
+
+Using [Go Modules](https://github.com/golang/go/wiki/Modules)
+
+```bash
+$ go get github.com/ulule/limiter/v3@v3.2.0
+```
+
+**Dep backport:**
+
+Please use [v3-dep](https://github.com/ulule/limiter/tree/v3-dep) branch.
+
+## Usage
+
+In five steps:
+
+* Create a `limiter.Rate` instance _(the number of requests per period)_
+* Create a `limiter.Store` instance _(see [Redis](https://github.com/ulule/limiter/blob/master/drivers/store/redis/store.go) or [In-Memory](https://github.com/ulule/limiter/blob/master/drivers/store/memory/store.go))_
+* Create a `limiter.Limiter` instance that takes store and rate instances as arguments
+* Create a middleware instance using the middleware of your choice
+* Give the limiter instance to your middleware initializer
+
+**Example:**
+
+```go
+// Create a rate with the given limit (number of requests) for the given
+// period (a time.Duration of your choice).
+import "github.com/ulule/limiter/v3"
+
+rate := limiter.Rate{
+ Period: 1 * time.Hour,
+ Limit: 1000,
+}
+
+// You can also use the simplified format "-"", with the given
+// periods:
+//
+// * "S": second
+// * "M": minute
+// * "H": hour
+// * "D": day
+//
+// Examples:
+//
+// * 5 reqs/second: "5-S"
+// * 10 reqs/minute: "10-M"
+// * 1000 reqs/hour: "1000-H"
+// * 2000 reqs/day: "2000-D"
+//
+rate, err := limiter.NewRateFromFormatted("1000-H")
+if err != nil {
+ panic(err)
+}
+
+// Then, create a store. Here, we use the bundled Redis store. Any store
+// compliant to limiter.Store interface will do the job. The defaults are
+// "limiter" as Redis key prefix and a maximum of 3 retries for the key under
+// race condition.
+import "github.com/ulule/limiter/v3/drivers/store/redis"
+
+store, err := redis.NewStore(client)
+if err != nil {
+ panic(err)
+}
+
+// Alternatively, you can pass options to the store with the "WithOptions"
+// function. For example, for Redis store:
+import "github.com/ulule/limiter/v3/drivers/store/redis"
+
+store, err := redis.NewStoreWithOptions(pool, limiter.StoreOptions{
+ Prefix: "your_own_prefix",
+ MaxRetry: 4,
+})
+if err != nil {
+ panic(err)
+}
+
+// Or use a in-memory store with a goroutine which clears expired keys.
+import "github.com/ulule/limiter/v3/drivers/store/memory"
+
+store := memory.NewStore()
+
+// Then, create the limiter instance which takes the store and the rate as arguments.
+// Now, you can give this instance to any supported middleware.
+instance := limiter.New(store, rate)
+```
+
+See middleware examples:
+
+* [HTTP](https://github.com/ulule/limiter/tree/master/examples/http/main.go)
+* [Gin](https://github.com/ulule/limiter/tree/master/examples/gin/main.go)
+* [Beego](https://github.com/ulule/limiter/blob/master/examples/beego/main.go)
+* [Chi](https://github.com/ulule/limiter/tree/master/examples/chi/main.go)
+* [Echo](https://github.com/ulule/limiter/tree/master/examples/echo/main.go)
+
+
+## How it works
+
+The ip address of the request is used as a key in the store.
+
+If the key does not exist in the store we set a default
+value with an expiration period.
+
+You will find two stores:
+
+* Redis: rely on [TTL](http://redis.io/commands/ttl) and incrementing the rate limit on each request.
+* In-Memory: rely on a fork of [go-cache](https://github.com/patrickmn/go-cache) with a goroutine to clear expired keys using a default interval.
+
+When the limit is reached, a `429` HTTP status code is sent.
+
+## Why Yet Another Package
+
+You could ask us: why yet another rate limit package?
+
+Because existing packages did not suit our needs.
+
+We tried a lot of alternatives:
+
+1. [Throttled][1]. This package uses the generic cell-rate algorithm. To cite the
+documentation: *"The algorithm has been slightly modified from its usual form to
+support limiting with an additional quantity parameter, such as for limiting the
+number of bytes uploaded"*. It is brillant in term of algorithm but
+documentation is quite unclear at the moment, we don't need *burst* feature for
+now, impossible to get a correct `After-Retry` (when limit exceeds, we can still
+make a few requests, because of the max burst) and it only supports ``http.Handler``
+middleware (we use [Gin][4]). Currently, we only need to return `429`
+and `X-Ratelimit-*` headers for `n reqs/duration`.
+
+2. [Speedbump][3]. Good package but maybe too lightweight. No `Reset` support,
+only one middleware for [Gin][4] framework and too Redis-coupled. We rather
+prefer to use a "store" approach.
+
+3. [Tollbooth][5]. Good one too but does both too much and too little. It limits by
+remote IP, path, methods, custom headers and basic auth usernames... but does not
+provide any Redis support (only *in-memory*) and a ready-to-go middleware that sets
+`X-Ratelimit-*` headers. `tollbooth.LimitByRequest(limiter, r)` only returns an HTTP
+code.
+
+4. [ratelimit][2]. Probably the closer to our needs but, once again, too
+lightweight, no middleware available and not active (last commit was in August
+2014). Some parts of code (Redis) comes from this project. It should deserve much
+more love.
+
+There are other many packages on GitHub but most are either too lightweight, too
+old (only support old Go versions) or unmaintained. So that's why we decided to
+create yet another one.
+
+## Contributing
+
+* Ping us on twitter:
+ * [@oibafsellig](https://twitter.com/oibafsellig)
+ * [@thoas](https://twitter.com/thoas)
+ * [@novln_](https://twitter.com/novln_)
+* Fork the [project](https://github.com/ulule/limiter)
+* Fix [bugs](https://github.com/ulule/limiter/issues)
+
+Don't hesitate ;)
+
+[1]: https://github.com/throttled/throttled
+[2]: https://github.com/r8k/ratelimit
+[3]: https://github.com/etcinit/speedbump
+[4]: https://github.com/gin-gonic/gin
+[5]: https://github.com/didip/tollbooth
+
+[godoc-url]: https://godoc.org/github.com/ulule/limiter
+[godoc-img]: https://godoc.org/github.com/ulule/limiter?status.svg
+[license-img]: https://img.shields.io/badge/license-MIT-blue.svg
+[goreport-url]: https://goreportcard.com/report/github.com/ulule/limiter
+[goreport-img]: https://goreportcard.com/badge/github.com/ulule/limiter
+[circle-url]: https://circleci.com/gh/ulule/limiter/tree/master
+[circle-img]: https://circleci.com/gh/ulule/limiter.svg?style=shield&circle-token=baf62ec320dd871b3a4a7e67fa99530fbc877c99
diff --git a/vendor/github.com/ulule/limiter/v3/defaults.go b/vendor/github.com/ulule/limiter/v3/defaults.go
new file mode 100644
index 00000000..091edf3d
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/defaults.go
@@ -0,0 +1,15 @@
+package limiter
+
+import "time"
+
+const (
+ // DefaultPrefix is the default prefix to use for the key in the store.
+ DefaultPrefix = "limiter"
+
+ // DefaultMaxRetry is the default maximum number of key retries under
+ // race condition (mainly used with database-based stores).
+ DefaultMaxRetry = 3
+
+ // DefaultCleanUpInterval is the default time duration for cleanup.
+ DefaultCleanUpInterval = 30 * time.Second
+)
diff --git a/vendor/github.com/ulule/limiter/v3/drivers/store/common/context.go b/vendor/github.com/ulule/limiter/v3/drivers/store/common/context.go
new file mode 100644
index 00000000..d181a460
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/drivers/store/common/context.go
@@ -0,0 +1,28 @@
+package common
+
+import (
+ "time"
+
+ "github.com/ulule/limiter/v3"
+)
+
+// GetContextFromState generate a new limiter.Context from given state.
+func GetContextFromState(now time.Time, rate limiter.Rate, expiration time.Time, count int64) limiter.Context {
+ limit := rate.Limit
+ remaining := int64(0)
+ reached := true
+
+ if count <= limit {
+ remaining = limit - count
+ reached = false
+ }
+
+ reset := expiration.Unix()
+
+ return limiter.Context{
+ Limit: limit,
+ Remaining: remaining,
+ Reset: reset,
+ Reached: reached,
+ }
+}
diff --git a/vendor/github.com/ulule/limiter/v3/drivers/store/memory/cache.go b/vendor/github.com/ulule/limiter/v3/drivers/store/memory/cache.go
new file mode 100644
index 00000000..361471f1
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/drivers/store/memory/cache.go
@@ -0,0 +1,159 @@
+package memory
+
+import (
+ "runtime"
+ "sync"
+ "time"
+)
+
+// Forked from https://github.com/patrickmn/go-cache
+
+// CacheWrapper is used to ensure that the underlying cleaner goroutine used to clean expired keys will not prevent
+// Cache from being garbage collected.
+type CacheWrapper struct {
+ *Cache
+}
+
+// A cleaner will periodically delete expired keys from cache.
+type cleaner struct {
+ interval time.Duration
+ stop chan bool
+}
+
+// Run will periodically delete expired keys from given cache until GC notify that it should stop.
+func (cleaner *cleaner) Run(cache *Cache) {
+ ticker := time.NewTicker(cleaner.interval)
+ for {
+ select {
+ case <-ticker.C:
+ cache.Clean()
+ case <-cleaner.stop:
+ ticker.Stop()
+ return
+ }
+ }
+}
+
+// stopCleaner is a callback from GC used to stop cleaner goroutine.
+func stopCleaner(wrapper *CacheWrapper) {
+ wrapper.cleaner.stop <- true
+}
+
+// startCleaner will start a cleaner goroutine for given cache.
+func startCleaner(cache *Cache, interval time.Duration) {
+ cleaner := &cleaner{
+ interval: interval,
+ stop: make(chan bool),
+ }
+
+ cache.cleaner = cleaner
+ go cleaner.Run(cache)
+}
+
+// Counter is a simple counter with an optional expiration.
+type Counter struct {
+ Value int64
+ Expiration int64
+}
+
+// Expired returns true if the counter has expired.
+func (counter Counter) Expired() bool {
+ if counter.Expiration == 0 {
+ return false
+ }
+ return time.Now().UnixNano() > counter.Expiration
+}
+
+// Cache contains a collection of counters.
+type Cache struct {
+ mutex sync.RWMutex
+ counters map[string]Counter
+ cleaner *cleaner
+}
+
+// NewCache returns a new cache.
+func NewCache(cleanInterval time.Duration) *CacheWrapper {
+
+ cache := &Cache{
+ counters: map[string]Counter{},
+ }
+
+ wrapper := &CacheWrapper{Cache: cache}
+
+ if cleanInterval > 0 {
+ startCleaner(cache, cleanInterval)
+ runtime.SetFinalizer(wrapper, stopCleaner)
+ }
+
+ return wrapper
+}
+
+// Increment increments given value on key.
+// If key is undefined or expired, it will create it.
+func (cache *Cache) Increment(key string, value int64, duration time.Duration) (int64, time.Time) {
+ cache.mutex.Lock()
+
+ counter, ok := cache.counters[key]
+ if !ok || counter.Expired() {
+ expiration := time.Now().Add(duration).UnixNano()
+ counter = Counter{
+ Value: value,
+ Expiration: expiration,
+ }
+
+ cache.counters[key] = counter
+ cache.mutex.Unlock()
+
+ return value, time.Unix(0, expiration)
+ }
+
+ value = counter.Value + value
+ counter.Value = value
+ expiration := counter.Expiration
+
+ cache.counters[key] = counter
+ cache.mutex.Unlock()
+
+ return value, time.Unix(0, expiration)
+}
+
+// Get returns key's value and expiration.
+func (cache *Cache) Get(key string, duration time.Duration) (int64, time.Time) {
+ cache.mutex.RLock()
+
+ counter, ok := cache.counters[key]
+ if !ok || counter.Expired() {
+ expiration := time.Now().Add(duration).UnixNano()
+ cache.mutex.RUnlock()
+ return 0, time.Unix(0, expiration)
+ }
+
+ value := counter.Value
+ expiration := counter.Expiration
+ cache.mutex.RUnlock()
+
+ return value, time.Unix(0, expiration)
+}
+
+// Clean will deleted any expired keys.
+func (cache *Cache) Clean() {
+ now := time.Now().UnixNano()
+
+ cache.mutex.Lock()
+ for key, counter := range cache.counters {
+ if now > counter.Expiration {
+ delete(cache.counters, key)
+ }
+ }
+ cache.mutex.Unlock()
+}
+
+// Reset changes the key's value and resets the expiration.
+func (cache *Cache) Reset(key string, duration time.Duration) (int64, time.Time) {
+ cache.mutex.Lock()
+ delete(cache.counters, key)
+ cache.mutex.Unlock()
+
+ expiration := time.Now().Add(duration).UnixNano()
+ return 0, time.Unix(0, expiration)
+}
diff --git a/vendor/github.com/ulule/limiter/v3/drivers/store/memory/store.go b/vendor/github.com/ulule/limiter/v3/drivers/store/memory/store.go
new file mode 100644
index 00000000..db36ce1f
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/drivers/store/memory/store.go
@@ -0,0 +1,67 @@
+package memory
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/ulule/limiter/v3"
+ "github.com/ulule/limiter/v3/drivers/store/common"
+)
+
+// Store is the in-memory store.
+type Store struct {
+ // Prefix used for the key.
+ Prefix string
+ // cache used to store values in-memory.
+ cache *CacheWrapper
+}
+
+// NewStore creates a new instance of memory store with defaults.
+func NewStore() limiter.Store {
+ return NewStoreWithOptions(limiter.StoreOptions{
+ Prefix: limiter.DefaultPrefix,
+ CleanUpInterval: limiter.DefaultCleanUpInterval,
+ })
+}
+
+// NewStoreWithOptions creates a new instance of memory store with options.
+func NewStoreWithOptions(options limiter.StoreOptions) limiter.Store {
+ return &Store{
+ Prefix: options.Prefix,
+ cache: NewCache(options.CleanUpInterval),
+ }
+}
+
+// Get returns the limit for given identifier.
+func (store *Store) Get(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ count, expiration := store.cache.Increment(key, 1, rate.Period)
+
+ lctx := common.GetContextFromState(now, rate, expiration, count)
+ return lctx, nil
+}
+
+// Peek returns the limit for given identifier, without modification on current values.
+func (store *Store) Peek(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ count, expiration := store.cache.Get(key, rate.Period)
+
+ lctx := common.GetContextFromState(now, rate, expiration, count)
+ return lctx, nil
+}
+
+// Reset returns the limit for given identifier.
+func (store *Store) Reset(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ count, expiration := store.cache.Reset(key, rate.Period)
+
+ lctx := common.GetContextFromState(now, rate, expiration, count)
+ return lctx, nil
+}
diff --git a/vendor/github.com/ulule/limiter/v3/drivers/store/redis/store.go b/vendor/github.com/ulule/limiter/v3/drivers/store/redis/store.go
new file mode 100644
index 00000000..31915e04
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/drivers/store/redis/store.go
@@ -0,0 +1,320 @@
+package redis
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ libredis "github.com/go-redis/redis"
+ "github.com/pkg/errors"
+
+ "github.com/ulule/limiter/v3"
+ "github.com/ulule/limiter/v3/drivers/store/common"
+)
+
+// Client is an interface thats allows to use a redis cluster or a redis single client seamlessly.
+type Client interface {
+ Ping() *libredis.StatusCmd
+ Get(key string) *libredis.StringCmd
+ Set(key string, value interface{}, expiration time.Duration) *libredis.StatusCmd
+ Watch(handler func(*libredis.Tx) error, keys ...string) error
+ Del(keys ...string) *libredis.IntCmd
+ SetNX(key string, value interface{}, expiration time.Duration) *libredis.BoolCmd
+ Eval(script string, keys []string, args ...interface{}) *libredis.Cmd
+}
+
+// Store is the redis store.
+type Store struct {
+ // Prefix used for the key.
+ Prefix string
+ // MaxRetry is the maximum number of retry under race conditions.
+ MaxRetry int
+ // client used to communicate with redis server.
+ client Client
+}
+
+// NewStore returns an instance of redis store with defaults.
+func NewStore(client Client) (limiter.Store, error) {
+ return NewStoreWithOptions(client, limiter.StoreOptions{
+ Prefix: limiter.DefaultPrefix,
+ CleanUpInterval: limiter.DefaultCleanUpInterval,
+ MaxRetry: limiter.DefaultMaxRetry,
+ })
+}
+
+// NewStoreWithOptions returns an instance of redis store with options.
+func NewStoreWithOptions(client Client, options limiter.StoreOptions) (limiter.Store, error) {
+ store := &Store{
+ client: client,
+ Prefix: options.Prefix,
+ MaxRetry: options.MaxRetry,
+ }
+
+ if store.MaxRetry <= 0 {
+ store.MaxRetry = 1
+ }
+
+ _, err := store.ping()
+ if err != nil {
+ return nil, err
+ }
+
+ return store, nil
+}
+
+// Get returns the limit for given identifier.
+func (store *Store) Get(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ lctx := limiter.Context{}
+ onWatch := func(rtx *libredis.Tx) error {
+
+ created, err := store.doSetValue(rtx, key, rate.Period)
+ if err != nil {
+ return err
+ }
+
+ if created {
+ expiration := now.Add(rate.Period)
+ lctx = common.GetContextFromState(now, rate, expiration, 1)
+ return nil
+ }
+
+ count, ttl, err := store.doUpdateValue(rtx, key, rate.Period)
+ if err != nil {
+ return err
+ }
+
+ expiration := now.Add(rate.Period)
+ if ttl > 0 {
+ expiration = now.Add(ttl)
+ }
+
+ lctx = common.GetContextFromState(now, rate, expiration, count)
+ return nil
+ }
+
+ err := store.client.Watch(onWatch, key)
+ if err != nil {
+ err = errors.Wrapf(err, "limiter: cannot get value for %s", key)
+ return limiter.Context{}, err
+ }
+
+ return lctx, nil
+}
+
+// Peek returns the limit for given identifier, without modification on current values.
+func (store *Store) Peek(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ lctx := limiter.Context{}
+ onWatch := func(rtx *libredis.Tx) error {
+ count, ttl, err := store.doPeekValue(rtx, key)
+ if err != nil {
+ return err
+ }
+
+ expiration := now.Add(rate.Period)
+ if ttl > 0 {
+ expiration = now.Add(ttl)
+ }
+
+ lctx = common.GetContextFromState(now, rate, expiration, count)
+ return nil
+ }
+
+ err := store.client.Watch(onWatch, key)
+ if err != nil {
+ err = errors.Wrapf(err, "limiter: cannot peek value for %s", key)
+ return limiter.Context{}, err
+ }
+
+ return lctx, nil
+}
+
+// Reset returns the limit for given identifier which is set to zero.
+func (store *Store) Reset(ctx context.Context, key string, rate limiter.Rate) (limiter.Context, error) {
+ key = fmt.Sprintf("%s:%s", store.Prefix, key)
+ now := time.Now()
+
+ lctx := limiter.Context{}
+ onWatch := func(rtx *libredis.Tx) error {
+
+ err := store.doResetValue(rtx, key)
+ if err != nil {
+ return err
+ }
+
+ count := int64(0)
+ expiration := now.Add(rate.Period)
+
+ lctx = common.GetContextFromState(now, rate, expiration, count)
+ return nil
+ }
+
+ err := store.client.Watch(onWatch, key)
+ if err != nil {
+ err = errors.Wrapf(err, "limiter: cannot reset value for %s", key)
+ return limiter.Context{}, err
+ }
+
+ return lctx, nil
+}
+
+// doPeekValue will execute peekValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached.
+func (store *Store) doPeekValue(rtx *libredis.Tx, key string) (int64, time.Duration, error) {
+ for i := 0; i < store.MaxRetry; i++ {
+ count, ttl, err := peekValue(rtx, key)
+ if err == nil {
+ return count, ttl, nil
+ }
+ }
+ return 0, 0, errors.New("retry limit exceeded")
+}
+
+// peekValue will retrieve the counter and its expiration for given key.
+func peekValue(rtx *libredis.Tx, key string) (int64, time.Duration, error) {
+ pipe := rtx.Pipeline()
+ value := pipe.Get(key)
+ expire := pipe.PTTL(key)
+
+ _, err := pipe.Exec()
+ if err != nil && err != libredis.Nil {
+ return 0, 0, err
+ }
+
+ count, err := value.Int64()
+ if err != nil && err != libredis.Nil {
+ return 0, 0, err
+ }
+
+ ttl, err := expire.Result()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return count, ttl, nil
+}
+
+// doSetValue will execute setValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached.
+func (store *Store) doSetValue(rtx *libredis.Tx, key string, expiration time.Duration) (bool, error) {
+ for i := 0; i < store.MaxRetry; i++ {
+ created, err := setValue(rtx, key, expiration)
+ if err == nil {
+ return created, nil
+ }
+ }
+ return false, errors.New("retry limit exceeded")
+}
+
+// setValue will try to initialize a new counter if given key doesn't exists.
+func setValue(rtx *libredis.Tx, key string, expiration time.Duration) (bool, error) {
+ value := rtx.SetNX(key, 1, expiration)
+
+ created, err := value.Result()
+ if err != nil {
+ return false, err
+ }
+
+ return created, nil
+}
+
+// doUpdateValue will execute setValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached.
+func (store *Store) doUpdateValue(rtx *libredis.Tx, key string,
+ expiration time.Duration) (int64, time.Duration, error) {
+ for i := 0; i < store.MaxRetry; i++ {
+ count, ttl, err := updateValue(rtx, key, expiration)
+ if err == nil {
+ return count, ttl, nil
+ }
+
+ // If ttl is negative and there is an error, do not retry an update.
+ if ttl < 0 {
+ return 0, 0, err
+ }
+ }
+ return 0, 0, errors.New("retry limit exceeded")
+}
+
+// updateValue will try to increment the counter identified by given key.
+func updateValue(rtx *libredis.Tx, key string, expiration time.Duration) (int64, time.Duration, error) {
+ pipe := rtx.Pipeline()
+ value := pipe.Incr(key)
+ expire := pipe.PTTL(key)
+
+ _, err := pipe.Exec()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ count, err := value.Result()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ ttl, err := expire.Result()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ // If ttl is -1ms, we have to define key expiration.
+ // PTTL return values changed as of Redis 2.8
+ // Now the command returns -2ms if the key does not exist, and -1ms if the key exists, but there is no expiry set
+ // We shouldn't try to set an expiry on a key that doesn't exist
+ if ttl == (-1 * time.Millisecond) {
+ expire := rtx.Expire(key, expiration)
+
+ ok, err := expire.Result()
+ if err != nil {
+ return count, ttl, err
+ }
+
+ if !ok {
+ return count, ttl, errors.New("cannot configure timeout on key")
+ }
+ }
+
+ return count, ttl, nil
+
+}
+
+// doResetValue will execute resetValue with a retry mecanism (optimistic locking) until store.MaxRetry is reached.
+func (store *Store) doResetValue(rtx *libredis.Tx, key string) error {
+ for i := 0; i < store.MaxRetry; i++ {
+ err := resetValue(rtx, key)
+ if err == nil {
+ return nil
+ }
+ }
+ return errors.New("retry limit exceeded")
+}
+
+// resetValue will try to reset the counter identified by given key.
+func resetValue(rtx *libredis.Tx, key string) error {
+ deletion := rtx.Del(key)
+
+ count, err := deletion.Result()
+ if err != nil {
+ return err
+ }
+ if count != 1 {
+ return errors.New("cannot delete key")
+ }
+
+ return nil
+
+}
+
+// ping checks if redis is alive.
+func (store *Store) ping() (bool, error) {
+ cmd := store.client.Ping()
+
+ pong, err := cmd.Result()
+ if err != nil {
+ return false, errors.Wrap(err, "limiter: cannot ping redis server")
+ }
+
+ return (pong == "PONG"), nil
+}
diff --git a/vendor/github.com/ulule/limiter/v3/go.mod b/vendor/github.com/ulule/limiter/v3/go.mod
new file mode 100644
index 00000000..958ed4f4
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/go.mod
@@ -0,0 +1,24 @@
+module github.com/ulule/limiter/v3
+
+go 1.12
+
+require (
+ github.com/astaxie/beego v1.10.0
+ github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
+ github.com/gin-gonic/gin v1.3.0
+ github.com/go-chi/chi v3.3.3+incompatible
+ github.com/go-redis/redis v6.14.0+incompatible
+ github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b // indirect
+ github.com/labstack/echo v3.3.10+incompatible
+ github.com/labstack/gommon v0.2.9 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
+ github.com/onsi/gomega v1.4.2 // indirect
+ github.com/pkg/errors v0.8.0
+ github.com/stretchr/testify v1.3.0
+ github.com/ugorji/go v1.1.1 // indirect
+ golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac // indirect
+ golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 // indirect
+ gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
+ gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
+)
diff --git a/vendor/github.com/ulule/limiter/v3/go.sum b/vendor/github.com/ulule/limiter/v3/go.sum
new file mode 100644
index 00000000..78c0f529
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/go.sum
@@ -0,0 +1,77 @@
+github.com/astaxie/beego v1.10.0 h1:s0OZ1iUO0rl8+lwWZfPK/0GhQi1tFUcIClTevyz48Pg=
+github.com/astaxie/beego v1.10.0/go.mod h1:0R4++1tUqERR0WYFWdfkcrsyoVBCG4DgpDGokT3yb+U=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
+github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
+github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
+github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
+github.com/go-chi/chi v3.3.3+incompatible h1:KHkmBEMNkwKuK4FdQL7N2wOeB9jnIx7jR5wsuSBEFI8=
+github.com/go-chi/chi v3.3.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-redis/redis v6.14.0+incompatible h1:AMPZkM7PbsJbilelrJUAyC4xQbGROTOLSuDd7fnMXCI=
+github.com/go-redis/redis v6.14.0+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
+github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b h1:X61dhFTE1Au92SvyF8HyAwdjWqiSdfBgFR7wTxC0+uU=
+github.com/json-iterator/go v0.0.0-20180806060727-1624edc4454b/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
+github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
+github.com/labstack/gommon v0.2.9 h1:heVeuAYtevIQVYkGj6A41dtfT91LrvFG220lavpWhrU=
+github.com/labstack/gommon v0.2.9/go.mod h1:E8ZTmW9vw5az5/ZyHWCp0Lw4OH2ecsaBP1C/NKavGG4=
+github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
+github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
+github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
+github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.2 h1:3mYCb7aPxS/RU7TI1y4rkEn1oKmPRjNJLNEXgw7MH2I=
+github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
+github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
+github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
+golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac h1:7d7lG9fHOLdL6jZPtnV4LpI41SbohIJ1Atq7U991dMg=
+golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1 h1:Y/KGZSOdz/2r0WJ9Mkmz6NJBusp0kiNx1Cn82lzJQ6w=
+golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed h1:uPxWBzB3+mlnjy9W58qY1j/cjyFjutgw/Vhan2zLy/A=
+golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
+gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
+gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
+gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/vendor/github.com/ulule/limiter/v3/limiter.go b/vendor/github.com/ulule/limiter/v3/limiter.go
new file mode 100644
index 00000000..753ed872
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/limiter.go
@@ -0,0 +1,60 @@
+package limiter
+
+import (
+ "context"
+)
+
+// -----------------------------------------------------------------
+// Context
+// -----------------------------------------------------------------
+
+// Context is the limit context.
+type Context struct {
+ Limit int64
+ Remaining int64
+ Reset int64
+ Reached bool
+}
+
+// -----------------------------------------------------------------
+// Limiter
+// -----------------------------------------------------------------
+
+// Limiter is the limiter instance.
+type Limiter struct {
+ Store Store
+ Rate Rate
+ Options Options
+}
+
+// New returns an instance of Limiter.
+func New(store Store, rate Rate, options ...Option) *Limiter {
+ opt := Options{
+ IPv4Mask: DefaultIPv4Mask,
+ IPv6Mask: DefaultIPv6Mask,
+ TrustForwardHeader: false,
+ }
+ for _, o := range options {
+ o(&opt)
+ }
+ return &Limiter{
+ Store: store,
+ Rate: rate,
+ Options: opt,
+ }
+}
+
+// Get returns the limit for given identifier.
+func (limiter *Limiter) Get(ctx context.Context, key string) (Context, error) {
+ return limiter.Store.Get(ctx, key, limiter.Rate)
+}
+
+// Peek returns the limit for given identifier, without modification on current values.
+func (limiter *Limiter) Peek(ctx context.Context, key string) (Context, error) {
+ return limiter.Store.Peek(ctx, key, limiter.Rate)
+}
+
+// Reset sets the limit for given identifier to zero.
+func (limiter *Limiter) Reset(ctx context.Context, key string) (Context, error) {
+ return limiter.Store.Reset(ctx, key, limiter.Rate)
+}
diff --git a/vendor/github.com/ulule/limiter/v3/network.go b/vendor/github.com/ulule/limiter/v3/network.go
new file mode 100644
index 00000000..8b39723c
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/network.go
@@ -0,0 +1,72 @@
+package limiter
+
+import (
+ "net"
+ "net/http"
+ "strings"
+)
+
+var (
+ // DefaultIPv4Mask defines the default IPv4 mask used to obtain user IP.
+ DefaultIPv4Mask = net.CIDRMask(32, 32)
+ // DefaultIPv6Mask defines the default IPv6 mask used to obtain user IP.
+ DefaultIPv6Mask = net.CIDRMask(128, 128)
+)
+
+// GetIP returns IP address from request.
+func (limiter *Limiter) GetIP(r *http.Request) net.IP {
+ return GetIP(r, limiter.Options)
+}
+
+// GetIPWithMask returns IP address from request by applying a mask.
+func (limiter *Limiter) GetIPWithMask(r *http.Request) net.IP {
+ return GetIPWithMask(r, limiter.Options)
+}
+
+// GetIPKey extracts IP from request and returns hashed IP to use as store key.
+func (limiter *Limiter) GetIPKey(r *http.Request) string {
+ return limiter.GetIPWithMask(r).String()
+}
+
+// GetIP returns IP address from request.
+// If options is defined and TrustForwardHeader is true, it will lookup IP in
+// X-Forwarded-For and X-Real-IP headers.
+func GetIP(r *http.Request, options ...Options) net.IP {
+ if len(options) >= 1 && options[0].TrustForwardHeader {
+ ip := r.Header.Get("X-Forwarded-For")
+ if ip != "" {
+ parts := strings.SplitN(ip, ",", 2)
+ part := strings.TrimSpace(parts[0])
+ return net.ParseIP(part)
+ }
+
+ ip = strings.TrimSpace(r.Header.Get("X-Real-IP"))
+ if ip != "" {
+ return net.ParseIP(ip)
+ }
+ }
+
+ remoteAddr := strings.TrimSpace(r.RemoteAddr)
+ host, _, err := net.SplitHostPort(remoteAddr)
+ if err != nil {
+ return net.ParseIP(remoteAddr)
+ }
+
+ return net.ParseIP(host)
+}
+
+// GetIPWithMask returns IP address from request by applying a mask.
+func GetIPWithMask(r *http.Request, options ...Options) net.IP {
+ if len(options) == 0 {
+ return GetIP(r)
+ }
+
+ ip := GetIP(r, options[0])
+ if ip.To4() != nil {
+ return ip.Mask(options[0].IPv4Mask)
+ }
+ if ip.To16() != nil {
+ return ip.Mask(options[0].IPv6Mask)
+ }
+ return ip
+}
diff --git a/vendor/github.com/ulule/limiter/v3/options.go b/vendor/github.com/ulule/limiter/v3/options.go
new file mode 100644
index 00000000..e7e850c2
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/options.go
@@ -0,0 +1,39 @@
+package limiter
+
+import (
+ "net"
+)
+
+// Option is a functional option.
+type Option func(*Options)
+
+// Options are limiter options.
+type Options struct {
+ // IPv4Mask defines the mask used to obtain a IPv4 address.
+ IPv4Mask net.IPMask
+ // IPv6Mask defines the mask used to obtain a IPv6 address.
+ IPv6Mask net.IPMask
+ // TrustForwardHeader enable parsing of X-Real-IP and X-Forwarded-For headers to obtain user IP.
+ TrustForwardHeader bool
+}
+
+// WithIPv4Mask will configure the limiter to use given mask for IPv4 address.
+func WithIPv4Mask(mask net.IPMask) Option {
+ return func(o *Options) {
+ o.IPv4Mask = mask
+ }
+}
+
+// WithIPv6Mask will configure the limiter to use given mask for IPv6 address.
+func WithIPv6Mask(mask net.IPMask) Option {
+ return func(o *Options) {
+ o.IPv6Mask = mask
+ }
+}
+
+// WithTrustForwardHeader will configure the limiter to trust X-Real-IP and X-Forwarded-For headers.
+func WithTrustForwardHeader(enable bool) Option {
+ return func(o *Options) {
+ o.TrustForwardHeader = enable
+ }
+}
diff --git a/vendor/github.com/ulule/limiter/v3/rate.go b/vendor/github.com/ulule/limiter/v3/rate.go
new file mode 100644
index 00000000..9e0faf27
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/rate.go
@@ -0,0 +1,54 @@
+package limiter
+
+import (
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/pkg/errors"
+)
+
+// Rate is the rate.
+type Rate struct {
+ Formatted string
+ Period time.Duration
+ Limit int64
+}
+
+// NewRateFromFormatted returns the rate from the formatted version.
+func NewRateFromFormatted(formatted string) (Rate, error) {
+ rate := Rate{}
+
+ values := strings.Split(formatted, "-")
+ if len(values) != 2 {
+ return rate, errors.Errorf("incorrect format '%s'", formatted)
+ }
+
+ periods := map[string]time.Duration{
+ "S": time.Second, // Second
+ "M": time.Minute, // Minute
+ "H": time.Hour, // Hour
+ "D": time.Hour * 24, // Day
+ }
+
+ limit, period := values[0], strings.ToUpper(values[1])
+
+ duration, ok := periods[period]
+ if !ok {
+ return rate, errors.Errorf("incorrect period '%s'", period)
+ }
+
+ p := 1 * duration
+ l, err := strconv.ParseInt(limit, 10, 64)
+ if err != nil {
+ return rate, errors.Errorf("incorrect limit '%s'", limit)
+ }
+
+ rate = Rate{
+ Formatted: formatted,
+ Period: p,
+ Limit: l,
+ }
+
+ return rate, nil
+}
diff --git a/vendor/github.com/ulule/limiter/v3/store.go b/vendor/github.com/ulule/limiter/v3/store.go
new file mode 100644
index 00000000..a9799d76
--- /dev/null
+++ b/vendor/github.com/ulule/limiter/v3/store.go
@@ -0,0 +1,28 @@
+package limiter
+
+import (
+ "context"
+ "time"
+)
+
+// Store is the common interface for limiter stores.
+type Store interface {
+ // Get returns the limit for given identifier.
+ Get(ctx context.Context, key string, rate Rate) (Context, error)
+ // Peek returns the limit for given identifier, without modification on current values.
+ Peek(ctx context.Context, key string, rate Rate) (Context, error)
+ // Reset resets the limit to zero for given identifier.
+ Reset(ctx context.Context, key string, rate Rate) (Context, error)
+}
+
+// StoreOptions are options for store.
+type StoreOptions struct {
+ // Prefix is the prefix to use for the key.
+ Prefix string
+
+ // MaxRetry is the maximum number of retry under race conditions.
+ MaxRetry int
+
+ // CleanUpInterval is the interval for cleanup.
+ CleanUpInterval time.Duration
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 22def5bc..2b52905c 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -168,6 +168,11 @@ github.com/stretchr/testify/assert
github.com/swaggo/swag/cmd/swag
github.com/swaggo/swag
github.com/swaggo/swag/gen
+# github.com/ulule/limiter/v3 v3.3.0
+github.com/ulule/limiter/v3
+github.com/ulule/limiter/v3/drivers/store/memory
+github.com/ulule/limiter/v3/drivers/store/redis
+github.com/ulule/limiter/v3/drivers/store/common
# github.com/urfave/cli v1.20.0
github.com/urfave/cli
# github.com/valyala/bytebufferpool v1.0.0