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