diff --git a/README.md b/README.md index 33cf76c..81afb1a 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ GSS (Go serve SPA) is a containerized web server for single-page applications wr - Optimized for single-page apps. - Automatically serves pre-compressed brotli and gzip files if available. - Sensible default cache configuration. +- Configurable rate limiter. - Optional out-of-the-box metrics. - Docker-based. - Configurable via YAML. @@ -14,7 +15,7 @@ GSS (Go serve SPA) is a containerized web server for single-page applications wr ## Usage -GSS works as a Docker image. By default it serves a directory in the container named `dist` at port `8080`, and exposes a metrics endpoint at `:9090/metrics` if enabled. +GSS works as a Docker image. By default it serves a directory in the container named `dist` at port `8080`. ### Running container directly @@ -106,6 +107,20 @@ Enables metrics collection and exposes an endpoint at `:9090/metrics`. Collected > metrics: true > ``` +### Rate limit per minute: `rateLimit` + +##### string: integer + +Enables rate limiting per minute per IP using a memory store. 15 by default. + +> Example: +> +> ```yaml +> # gss.yaml +> +> rateLimit: 10 +> ``` + ## Contributing This project started as a way to learn and to solve a need I had. If you think it can be improved in any way, you are very welcome to contribute! diff --git a/go.mod b/go.mod index c7d8421..485b535 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/felixge/httpsnoop v1.0.3 github.com/prometheus/client_golang v1.15.1 github.com/rs/zerolog v1.29.1 + github.com/sethvargo/go-limiter v0.7.2 github.com/stretchr/testify v1.8.2 gopkg.in/yaml.v2 v2.4.0 ) @@ -15,6 +16,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -22,6 +24,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect golang.org/x/sys v0.7.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index c89f21a..f35eee0 100644 --- a/go.sum +++ b/go.sum @@ -1,72 +1,25 @@ -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/felixge/httpsnoop v1.0.2 h1:+nS9g82KMXccJ/wp0zyRW9ZBHFETmMGtkk+2CTTrW4o= -github.com/felixge/httpsnoop v1.0.2/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -74,147 +27,49 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -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/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II= -github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8= +github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gss.go b/gss.go index be930f3..7c8c97f 100644 --- a/gss.go +++ b/gss.go @@ -14,6 +14,8 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "github.com/sethvargo/go-limiter/httplimit" + "github.com/sethvargo/go-limiter/memorystore" "gopkg.in/yaml.v2" ) @@ -22,7 +24,7 @@ func main() { var metrics *metrics cfg := newConfig().withYAML() - if cfg.Metrics { + if cfg.MetricsEnabled { metrics = registerMetrics() internalServer := newInternalServer(metrics) go func() { @@ -48,12 +50,20 @@ func setUpLogger() { } type config struct { - Headers map[string]string `yaml:"headers,omitempty"` - Metrics bool `yaml:"metrics,omitempty"` + ResponseHeaders map[string]string `yaml:"headers,omitempty"` + MetricsEnabled bool `yaml:"metrics,omitempty"` + RateLimitPerMinute int `yaml:"rateLimit,omitempty"` } func newConfig() *config { - return &config{} + return &config{ + // Default values + ResponseHeaders: map[string]string{ + "Server": "GSS", + }, + MetricsEnabled: false, + RateLimitPerMinute: 15, + } } func (c *config) withYAML() *config { @@ -95,10 +105,23 @@ func newFileServer(cfg *config, metrics *metrics) *fileServer { } func (f *fileServer) init() *fileServer { - if f.Config.Metrics { - f.Server.Handler = metricsMiddleware(f.Metrics)(f.setHeaders((f.serveSPA()))) + store, err := memorystore.New(&memorystore.Config{ + Tokens: uint64(f.Config.RateLimitPerMinute), + Interval: time.Minute, + }) + if err != nil { + log.Fatal().Msgf("Error creating rate limit store: %v", err) + } + + rateLimit, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) + if err != nil { + log.Fatal().Msgf("Error creating rate limit middleware: %v", err) + } + + if f.Config.MetricsEnabled { + f.Server.Handler = metricsMiddleware(f.Metrics)(rateLimit.Handle(f.setHeaders((f.serveSPA())))) } else { - f.Server.Handler = f.setHeaders((f.serveSPA())) + f.Server.Handler = rateLimit.Handle(f.setHeaders((f.serveSPA()))) } return f @@ -112,7 +135,7 @@ func (f *fileServer) setHeaders(h http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Vary", "Accept-Encoding") - for k, v := range f.Config.Headers { + for k, v := range f.Config.ResponseHeaders { w.Header().Set(k, v) } diff --git a/gss_test.go b/gss_test.go index 6352bcd..7edef8a 100644 --- a/gss_test.go +++ b/gss_test.go @@ -15,7 +15,7 @@ func TestGSS(t *testing.T) { t.Parallel() cfg := &config{ - Headers: map[string]string{ + ResponseHeaders: map[string]string{ "X-Test": "test", }, } @@ -28,6 +28,21 @@ func TestGSS(t *testing.T) { assert.Equal(t, "test", w.Header().Get("X-Test")) }) + t.Run("rate limits requests", func(t *testing.T) { + t.Parallel() + + cfg := &config{ + RateLimitPerMinute: 10, + } + fileServer := newFileServer(cfg, metrics).init() + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodGet, "/", nil) + + fileServer.Server.Handler.ServeHTTP(w, r) + + assert.Equal(t, "10", w.Header().Get("X-Ratelimit-Limit")) + }) + t.Run("redirects index correctly", func(t *testing.T) { t.Parallel() diff --git a/test/gss.yaml b/test/gss.yaml index ff978f4..cf1e630 100644 --- a/test/gss.yaml +++ b/test/gss.yaml @@ -1,5 +1,5 @@ headers: Referrer-Policy: "strict-origin-when-cross-origin" - Server: "GSS" Strict-Transport-Security: "max-age=63072000; includeSubDomains; preload" metrics: true +rateLimit: 10 diff --git a/vendor/github.com/sethvargo/go-limiter/LICENSE b/vendor/github.com/sethvargo/go-limiter/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/sethvargo/go-limiter/Makefile b/vendor/github.com/sethvargo/go-limiter/Makefile new file mode 100644 index 0000000..f8073e2 --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/Makefile @@ -0,0 +1,49 @@ +VETTERS = "asmdecl,assign,atomic,bools,buildtag,cgocall,composites,copylocks,errorsas,httpresponse,loopclosure,lostcancel,nilfunc,printf,shift,stdmethods,structtag,tests,unmarshal,unreachable,unsafeptr,unusedresult" +GOFMT_FILES = $(shell go list -f '{{.Dir}}' ./...) + +benchmarks: + @(cd benchmarks/ && go test -bench=. -benchmem -benchtime=1s ./...) +.PHONY: benchmarks + +fmtcheck: + @command -v goimports > /dev/null 2>&1 || (cd tools/ && go get golang.org/x/tools/cmd/goimports) + @CHANGES="$$(goimports -d $(GOFMT_FILES))"; \ + if [ -n "$${CHANGES}" ]; then \ + echo "Unformatted (run goimports -w .):\n\n$${CHANGES}\n\n"; \ + exit 1; \ + fi + @# Annoyingly, goimports does not support the simplify flag. + @CHANGES="$$(gofmt -s -d $(GOFMT_FILES))"; \ + if [ -n "$${CHANGES}" ]; then \ + echo "Unformatted (run gofmt -s -w .):\n\n$${CHANGES}\n\n"; \ + exit 1; \ + fi +.PHONY: fmtcheck + +spellcheck: + @command -v misspell > /dev/null 2>&1 || (cd tools/ && go get github.com/client9/misspell/cmd/misspell) + @misspell -locale="US" -error -source="text" **/* +.PHONY: spellcheck + +staticcheck: + @command -v staticcheck > /dev/null 2>&1 || (cd tools/ && go get honnef.co/go/tools/cmd/staticcheck) + @staticcheck -checks="all" -tests $(GOFMT_FILES) +.PHONY: staticcheck + +test: + @go test \ + -count=1 \ + -short \ + -timeout=5m \ + -vet="${VETTERS}" \ + ./... +.PHONY: test + +test-acc: + @go test \ + -count=1 \ + -race \ + -timeout=10m \ + -vet="${VETTERS}" \ + ./... +.PHONY: test-acc diff --git a/vendor/github.com/sethvargo/go-limiter/README.md b/vendor/github.com/sethvargo/go-limiter/README.md new file mode 100644 index 0000000..e7d4055 --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/README.md @@ -0,0 +1,152 @@ +# Go Rate Limiter + +[![GoDoc](https://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/mod/github.com/sethvargo/go-limiter) +[![GitHub Actions](https://img.shields.io/github/workflow/status/sethvargo/go-limiter/Test?style=flat-square)](https://github.com/sethvargo/go-limiter/actions?query=workflow%3ATest) + + +This package provides a rate limiter in Go (Golang), suitable for use in HTTP +servers and distributed workloads. It's specifically designed for +configurability and flexibility without compromising throughput. + + +## Usage + +1. Create a store. This example uses an in-memory store: + + ```golang + store, err := memorystore.New(&memorystore.Config{ + // Number of tokens allowed per interval. + Tokens: 15, + + // Interval until tokens reset. + Interval: time.Minute, + }) + if err != nil { + log.Fatal(err) + } + ``` + +1. Determine the limit by calling `Take()` on the store: + + ```golang + ctx := context.Background() + + // key is the unique value upon which you want to rate limit, like an IP or + // MAC address. + key := "127.0.0.1" + tokens, remaining, reset, ok, err := store.Take(ctx, key) + + // tokens is the configured tokens (15 in this example). + _ = tokens + + // remaining is the number of tokens remaining (14 now). + _ = remaining + + // reset is the unix nanoseconds at which the tokens will replenish. + _ = reset + + // ok indicates whether the take was successful. If the key is over the + // configured limit, ok will be false. + _ = ok + + // Here's a more realistic example: + if !ok { + return fmt.Errorf("rate limited: retry at %v", reset) + } + ``` + +There's also HTTP middleware via the `httplimit` package. After creating a +store, wrap Go's standard HTTP handler: + +```golang +middleware, err := httplimit.NewMiddleware(store, httplimit.IPKeyFunc()) +if err != nil { + log.Fatal(err) +} + +mux1 := http.NewServeMux() +mux1.Handle("/", middleware.Handle(doWork)) // doWork is your original handler +``` + +The middleware automatically set the following headers, conforming to the latest +RFCs: + +- `X-RateLimit-Limit` - configured rate limit (constant). +- `X-RateLimit-Remaining` - number of remaining tokens in current interval. +- `X-RateLimit-Reset` - UTC time when the limit resets. +- `Retry-After` - Time at which to retry + + +## Why _another_ Go rate limiter? + +I really wanted to learn more about the topic and possibly implementations. The +existing packages in the Go ecosystem either lacked flexibility or traded +flexibility for performance. I wanted to write a package that was highly +extensible while still offering the highest levels of performance. + + +### Speed and performance + +How fast is it? You can run the benchmarks yourself, but here's a few sample +benchmarks with 100,000 unique keys. I added commas to the output for clarity, +but you can run the benchmarks via `make benchmarks`: + +```text +$ make benchmarks +BenchmarkSethVargoMemory/memory/serial-7 13,706,899 81.7 ns/op 16 B/op 1 allocs/op +BenchmarkSethVargoMemory/memory/parallel-7 7,900,639 151 ns/op 61 B/op 3 allocs/op +BenchmarkSethVargoMemory/sweep/serial-7 19,601,592 58.3 ns/op 0 B/op 0 allocs/op +BenchmarkSethVargoMemory/sweep/parallel-7 21,042,513 55.2 ns/op 0 B/op 0 allocs/op +BenchmarkThrottled/memory/serial-7 6,503,260 176 ns/op 0 B/op 0 allocs/op +BenchmarkThrottled/memory/parallel-7 3,936,655 297 ns/op 0 B/op 0 allocs/op +BenchmarkThrottled/sweep/serial-7 6,901,432 171 ns/op 0 B/op 0 allocs/op +BenchmarkThrottled/sweep/parallel-7 5,948,437 202 ns/op 0 B/op 0 allocs/op +BenchmarkTollbooth/memory/serial-7 3,064,309 368 ns/op 0 B/op 0 allocs/op +BenchmarkTollbooth/memory/parallel-7 2,658,014 448 ns/op 0 B/op 0 allocs/op +BenchmarkTollbooth/sweep/serial-7 2,769,937 430 ns/op 192 B/op 3 allocs/op +BenchmarkTollbooth/sweep/parallel-7 2,216,211 546 ns/op 192 B/op 3 allocs/op +BenchmarkUber/memory/serial-7 13,795,612 94.2 ns/op 0 B/op 0 allocs/op +BenchmarkUber/memory/parallel-7 7,503,214 159 ns/op 0 B/op 0 allocs/op +BenchmarkUlule/memory/serial-7 2,964,438 405 ns/op 24 B/op 2 allocs/op +BenchmarkUlule/memory/parallel-7 2,441,778 469 ns/op 24 B/op 2 allocs/op +``` + +There's likely still optimizations to be had, pull requests are welcome! + + +### Ecosystem + +Many of the existing packages in the ecosystem take dependencies on other +packages. I'm an advocate of very thin libraries, and I don't think a rate +limiter should be pulling external packages. That's why **go-limit uses only the +Go standard library**. + + +### Flexible and extensible + +Most of the existing rate limiting libraries make a strong assumption that rate +limiting is only for HTTP services. Baked in that assumption are more +assumptions like rate limiting by "IP address" or are limited to a resolution of +"per second". While go-limit supports rate limiting at the HTTP layer, it can +also be used to rate limit literally anything. It rate limits on a user-defined +arbitrary string key. + + +### Stores + +#### Memory + +Memory is the fastest store, but only works on a single container/virtual +machine since there's no way to share the state. +[Learn more](https://pkg.go.dev/github.com/sethvargo/go-limiter/memorystore). + +#### Redis + +Redis uses Redis + Lua as a shared pool, but comes at a performance cost. +[Learn more](https://pkg.go.dev/github.com/sethvargo/go-redisstore). + +#### Noop + +Noop does no rate limiting, but still implements the interface - useful for +testing and local development. +[Learn more](https://pkg.go.dev/github.com/sethvargo/go-limiter/noopstore). diff --git a/vendor/github.com/sethvargo/go-limiter/httplimit/middleware.go b/vendor/github.com/sethvargo/go-limiter/httplimit/middleware.go new file mode 100644 index 0000000..5c79995 --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/httplimit/middleware.go @@ -0,0 +1,127 @@ +// Package httplimit provides middleware for rate limiting HTTP handlers. +// +// The implementation is designed to work with Go's built-in http.Handler and +// http.HandlerFunc interfaces, so it will also work with any popular web +// frameworks that support middleware with these properties. +package httplimit + +import ( + "fmt" + "net" + "net/http" + "strconv" + "time" + + "github.com/sethvargo/go-limiter" +) + +const ( + // HeaderRateLimitLimit, HeaderRateLimitRemaining, and HeaderRateLimitReset + // are the recommended return header values from IETF on rate limiting. Reset + // is in UTC time. + HeaderRateLimitLimit = "X-RateLimit-Limit" + HeaderRateLimitRemaining = "X-RateLimit-Remaining" + HeaderRateLimitReset = "X-RateLimit-Reset" + + // HeaderRetryAfter is the header used to indicate when a client should retry + // requests (when the rate limit expires), in UTC time. + HeaderRetryAfter = "Retry-After" +) + +// KeyFunc is a function that accepts an http request and returns a string key +// that uniquely identifies this request for the purpose of rate limiting. +// +// KeyFuncs are called on each request, so be mindful of performance and +// implement caching where possible. If a KeyFunc returns an error, the HTTP +// handler will return Internal Server Error and will NOT take from the limiter +// store. +type KeyFunc func(r *http.Request) (string, error) + +// IPKeyFunc returns a function that keys data based on the incoming requests IP +// address. By default this uses the RemoteAddr, but you can also specify a list +// of headers which will be checked for an IP address first (e.g. +// "X-Forwarded-For"). Headers are retrieved using Header.Get(), which means +// they are case insensitive. +func IPKeyFunc(headers ...string) KeyFunc { + return func(r *http.Request) (string, error) { + for _, h := range headers { + if v := r.Header.Get(h); v != "" { + return v, nil + } + } + + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "", err + } + return ip, nil + } +} + +// Middleware is a handler/mux that can wrap other middlware to implement HTTP +// rate limiting. It can rate limit based on an arbitrary KeyFunc, and supports +// anything that implements limiter.StoreWithContext. +type Middleware struct { + store limiter.Store + keyFunc KeyFunc +} + +// NewMiddleware creates a new middleware suitable for use as an HTTP handler. +// This function returns an error if either the Store or KeyFunc are nil. +func NewMiddleware(s limiter.Store, f KeyFunc) (*Middleware, error) { + if s == nil { + return nil, fmt.Errorf("store cannot be nil") + } + + if f == nil { + return nil, fmt.Errorf("key function cannot be nil") + } + + return &Middleware{ + store: s, + keyFunc: f, + }, nil +} + +// Handle returns the HTTP handler as a middleware. This handler calls Take() on +// the store and sets the common rate limiting headers. If the take is +// successful, the remaining middleware is called. If take is unsuccessful, the +// middleware chain is halted and the function renders a 429 to the caller with +// metadata about when it's safe to retry. +func (m *Middleware) Handle(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + // Call the key function - if this fails, it's an internal server error. + key, err := m.keyFunc(r) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + // Take from the store. + limit, remaining, reset, ok, err := m.store.Take(ctx, key) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + resetTime := time.Unix(0, int64(reset)).UTC().Format(time.RFC1123) + + // Set headers (we do this regardless of whether the request is permitted). + w.Header().Set(HeaderRateLimitLimit, strconv.FormatUint(limit, 10)) + w.Header().Set(HeaderRateLimitRemaining, strconv.FormatUint(remaining, 10)) + w.Header().Set(HeaderRateLimitReset, resetTime) + + // Fail if there were no tokens remaining. + if !ok { + w.Header().Set(HeaderRetryAfter, resetTime) + http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) + return + } + + // If we got this far, we're allowed to continue, so call the next middleware + // in the stack to continue processing. + next.ServeHTTP(w, r) + }) +} diff --git a/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime.go b/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime.go new file mode 100644 index 0000000..39cea2d --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime.go @@ -0,0 +1,20 @@ +//go:build !windows +// +build !windows + +// Package fasttime gets wallclock time, but super fast. +package fasttime + +import ( + _ "unsafe" +) + +//go:noescape +//go:linkname now time.now +func now() (sec int64, nsec int32, mono int64) + +// Now returns a monotonic clock value. The actual value will differ across +// systems, but that's okay because we generally only care about the deltas. +func Now() uint64 { + sec, nsec, _ := now() + return uint64(sec)*1e9 + uint64(nsec) +} diff --git a/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime_windows.go b/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime_windows.go new file mode 100644 index 0000000..311f80d --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/internal/fasttime/fasttime_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package fasttime + +import "time" + +// Now returns a monotonic clock value. On Windows, no such clock exists, so we +// fallback to time.Now(). +func Now() uint64 { + return uint64(time.Now().UnixNano()) +} diff --git a/vendor/github.com/sethvargo/go-limiter/limiter.go b/vendor/github.com/sethvargo/go-limiter/limiter.go new file mode 100644 index 0000000..1cff278 --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/limiter.go @@ -0,0 +1,2 @@ +// Package limiter defines rate limiting systems. +package limiter diff --git a/vendor/github.com/sethvargo/go-limiter/memorystore/store.go b/vendor/github.com/sethvargo/go-limiter/memorystore/store.go new file mode 100644 index 0000000..ac0a4ea --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/memorystore/store.go @@ -0,0 +1,328 @@ +// Package memorystore defines an in-memory storage system for limiting. +package memorystore + +import ( + "context" + "sync" + "sync/atomic" + "time" + + "github.com/sethvargo/go-limiter" + "github.com/sethvargo/go-limiter/internal/fasttime" +) + +var _ limiter.Store = (*store)(nil) + +type store struct { + tokens uint64 + interval time.Duration + + sweepInterval time.Duration + sweepMinTTL uint64 + + data map[string]*bucket + dataLock sync.RWMutex + + stopped uint32 + stopCh chan struct{} +} + +// Config is used as input to New. It defines the behavior of the storage +// system. +type Config struct { + // Tokens is the number of tokens to allow per interval. The default value is + // 1. + Tokens uint64 + + // Interval is the time interval upon which to enforce rate limiting. The + // default value is 1 second. + Interval time.Duration + + // SweepInterval is the rate at which to run the garabage collection on stale + // entries. Setting this to a low value will optimize memory consumption, but + // will likely reduce performance and increase lock contention. Setting this + // to a high value will maximum throughput, but will increase the memory + // footprint. This can be tuned in combination with SweepMinTTL to control how + // long stale entires are kept. The default value is 6 hours. + SweepInterval time.Duration + + // SweepMinTTL is the minimum amount of time a session must be inactive before + // clearing it from the entries. There's no validation, but this should be at + // least as high as your rate limit, or else the data store will purge records + // before they limit is applied. The default value is 12 hours. + SweepMinTTL time.Duration + + // InitialAlloc is the size to use for the in-memory map. Go will + // automatically expand the buffer, but choosing higher number can trade + // memory consumption for performance as it limits the number of times the map + // needs to expand. The default value is 4096. + InitialAlloc int +} + +// New creates an in-memory rate limiter that uses a bucketing model to limit +// the number of permitted events over an interval. It's optimized for runtime +// and memory efficiency. +func New(c *Config) (limiter.Store, error) { + if c == nil { + c = new(Config) + } + + tokens := uint64(1) + if c.Tokens > 0 { + tokens = c.Tokens + } + + interval := 1 * time.Second + if c.Interval > 0 { + interval = c.Interval + } + + sweepInterval := 6 * time.Hour + if c.SweepInterval > 0 { + sweepInterval = c.SweepInterval + } + + sweepMinTTL := 12 * time.Hour + if c.SweepMinTTL > 0 { + sweepMinTTL = c.SweepMinTTL + } + + initialAlloc := 4096 + if c.InitialAlloc > 0 { + initialAlloc = c.InitialAlloc + } + + s := &store{ + tokens: tokens, + interval: interval, + + sweepInterval: sweepInterval, + sweepMinTTL: uint64(sweepMinTTL), + + data: make(map[string]*bucket, initialAlloc), + stopCh: make(chan struct{}), + } + go s.purge() + return s, nil +} + +// Take attempts to remove a token from the named key. If the take is +// successful, it returns true, otherwise false. It also returns the configured +// limit, remaining tokens, and reset time. +func (s *store) Take(ctx context.Context, key string) (uint64, uint64, uint64, bool, error) { + // If the store is stopped, all requests are rejected. + if atomic.LoadUint32(&s.stopped) == 1 { + return 0, 0, 0, false, limiter.ErrStopped + } + + // Acquire a read lock first - this allows other to concurrently check limits + // without taking a full lock. + s.dataLock.RLock() + if b, ok := s.data[key]; ok { + s.dataLock.RUnlock() + return b.take() + } + s.dataLock.RUnlock() + + // Unfortunately we did not find the key in the map. Take out a full lock. We + // have to check if the key exists again, because it's possible another + // goroutine created it between our shared lock and exclusive lock. + s.dataLock.Lock() + if b, ok := s.data[key]; ok { + s.dataLock.Unlock() + return b.take() + } + + // This is the first time we've seen this entry (or it's been garbage + // collected), so create the bucket and take an initial request. + b := newBucket(s.tokens, s.interval) + + // Add it to the map and take. + s.data[key] = b + s.dataLock.Unlock() + return b.take() +} + +// Get retrieves the information about the key, if any exists. +func (s *store) Get(ctx context.Context, key string) (uint64, uint64, error) { + // If the store is stopped, all requests are rejected. + if atomic.LoadUint32(&s.stopped) == 1 { + return 0, 0, limiter.ErrStopped + } + + // Acquire a read lock first - this allows other to concurrently check limits + // without taking a full lock. + s.dataLock.RLock() + if b, ok := s.data[key]; ok { + s.dataLock.RUnlock() + return b.get() + } + s.dataLock.RUnlock() + + return 0, 0, nil +} + +// Set configures the bucket-specific tokens and interval. +func (s *store) Set(ctx context.Context, key string, tokens uint64, interval time.Duration) error { + s.dataLock.Lock() + b := newBucket(tokens, interval) + s.data[key] = b + s.dataLock.Unlock() + return nil +} + +// Burst adds the provided value to the bucket's currently available tokens. +func (s *store) Burst(ctx context.Context, key string, tokens uint64) error { + s.dataLock.Lock() + if b, ok := s.data[key]; ok { + b.lock.Lock() + s.dataLock.Unlock() + b.availableTokens = b.availableTokens + tokens + b.lock.Unlock() + return nil + } + + // If we got this far, there's no current record for the key. + b := newBucket(s.tokens+tokens, s.interval) + s.data[key] = b + s.dataLock.Unlock() + return nil +} + +// Close stops the memory limiter and cleans up any outstanding +// sessions. You should always call Close() as it releases the memory consumed +// by the map AND releases the tickers. +func (s *store) Close(ctx context.Context) error { + if !atomic.CompareAndSwapUint32(&s.stopped, 0, 1) { + return nil + } + + // Close the channel to prevent future purging. + close(s.stopCh) + + // Delete all the things. + s.dataLock.Lock() + for k := range s.data { + delete(s.data, k) + } + s.dataLock.Unlock() + return nil +} + +// purge continually iterates over the map and purges old values on the provided +// sweep interval. Earlier designs used a go-function-per-item expiration, but +// it actually generated *more* lock contention under normal use. The most +// performant option with real-world data was a global garbage collection on a +// fixed interval. +func (s *store) purge() { + ticker := time.NewTicker(s.sweepInterval) + defer ticker.Stop() + + for { + select { + case <-s.stopCh: + return + case <-ticker.C: + } + + s.dataLock.Lock() + now := fasttime.Now() + for k, b := range s.data { + b.lock.Lock() + lastTime := b.startTime + (b.lastTick * uint64(b.interval)) + b.lock.Unlock() + + if now-lastTime > s.sweepMinTTL { + delete(s.data, k) + } + } + s.dataLock.Unlock() + } +} + +// bucket is an internal wrapper around a taker. +type bucket struct { + // startTime is the number of nanoseconds from unix epoch when this bucket was + // initially created. + startTime uint64 + + // maxTokens is the maximum number of tokens permitted on the bucket at any + // time. The number of available tokens will never exceed this value. + maxTokens uint64 + + // interval is the time at which ticking should occur. + interval time.Duration + + // availableTokens is the current point-in-time number of tokens remaining. + availableTokens uint64 + + // lastTick is the last clock tick, used to re-calculate the number of tokens + // on the bucket. + lastTick uint64 + + // lock guards the mutable fields. + lock sync.Mutex +} + +// newBucket creates a new bucket from the given tokens and interval. +func newBucket(tokens uint64, interval time.Duration) *bucket { + b := &bucket{ + startTime: fasttime.Now(), + maxTokens: tokens, + availableTokens: tokens, + interval: interval, + } + return b +} + +// get returns information about the bucket. +func (b *bucket) get() (tokens uint64, remaining uint64, retErr error) { + b.lock.Lock() + defer b.lock.Unlock() + + tokens = b.maxTokens + remaining = b.availableTokens + return +} + +// take attempts to remove a token from the bucket. If there are no tokens +// available and the clock has ticked forward, it recalculates the number of +// tokens and retries. It returns the limit, remaining tokens, time until +// refresh, and whether the take was successful. +func (b *bucket) take() (tokens uint64, remaining uint64, reset uint64, ok bool, retErr error) { + // Capture the current request time, current tick, and amount of time until + // the bucket resets. + now := fasttime.Now() + currTick := tick(b.startTime, now, b.interval) + + tokens = b.maxTokens + reset = b.startTime + ((currTick + 1) * uint64(b.interval)) + + b.lock.Lock() + defer b.lock.Unlock() + + // If we're on a new tick since last assessment, perform + // a full reset up to maxTokens. + if b.lastTick < currTick { + b.availableTokens = b.maxTokens + b.lastTick = currTick + } + + if b.availableTokens > 0 { + b.availableTokens-- + ok = true + remaining = b.availableTokens + } + + return +} + +// tick is the total number of times the current interval has occurred between +// when the time started (start) and the current time (curr). For example, if +// the start time was 12:30pm and it's currently 1:00pm, and the interval was 5 +// minutes, tick would return 6 because 1:00pm is the 6th 5-minute tick. Note +// that tick would return 5 at 12:59pm, because it hasn't reached the 6th tick +// yet. +func tick(start, curr uint64, interval time.Duration) uint64 { + return (curr - start) / uint64(interval.Nanoseconds()) +} diff --git a/vendor/github.com/sethvargo/go-limiter/store.go b/vendor/github.com/sethvargo/go-limiter/store.go new file mode 100644 index 0000000..ee9979a --- /dev/null +++ b/vendor/github.com/sethvargo/go-limiter/store.go @@ -0,0 +1,58 @@ +package limiter + +import ( + "context" + "fmt" + "time" +) + +// ErrStopped is the error returned when the store is stopped. All implementers +// should return this error for stoppable stores. +var ErrStopped = fmt.Errorf("store is stopped") + +// Store is an interface for limiter storage backends. +// +// Keys should be hash, sanitized, or otherwise scrubbed of identifiable +// information they will be given to the store in plaintext. If you're rate +// limiting by IP address, for example, the IP address would be stored in the +// storage system in plaintext. This may be undesirable in certain situations, +// like when the store is a public database. In those cases, you should hash or +// HMAC the key before passing giving it to the store. If you want to encrypt +// the value, you must use homomorphic encryption to ensure the value always +// encrypts to the same ciphertext. +type Store interface { + // Take takes a token from the given key if available, returning: + // + // - the configured limit size + // - the number of remaining tokens in the interval + // - the server time when new tokens will be available + // - whether the take was successful + // - any errors that occurred while performing the take - these should be + // backend errors (e.g. connection failures); Take() should never return an + // error for an bucket. + // + // If "ok" is false, the take was unsuccessful and the caller should NOT + // service the request. + // + // See the note about keys on the interface documentation. + Take(ctx context.Context, key string) (tokens, remaining, reset uint64, ok bool, err error) + + // Get gets the current limit and remaining tokens for the provided key. It + // does not change any of the values. + Get(ctx context.Context, key string) (tokens, remaining uint64, err error) + + // Set configures the limit at the provided key. If a limit already exists, it + // is overwritten. This also sets the number of tokens in the bucket to the + // limit. + Set(ctx context.Context, key string, tokens uint64, interval time.Duration) error + + // Burst adds more tokens to the key's current bucket until the next interval + // tick. This will allow the current bucket tick to exceed the maximum number + // maximum ticks until the next interval. + Burst(ctx context.Context, key string, tokens uint64) error + + // Close terminates the store and cleans up any data structures or connections + // that may remain open. After a store is stopped, Take() should always return + // zero values. + Close(ctx context.Context) error +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 853e4e7..8dfc0ac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -14,6 +14,8 @@ github.com/felixge/httpsnoop ## explicit; go 1.9 github.com/golang/protobuf/proto github.com/golang/protobuf/ptypes/timestamp +# github.com/kr/text v0.2.0 +## explicit # github.com/mattn/go-colorable v0.1.13 ## explicit; go 1.15 github.com/mattn/go-colorable @@ -45,12 +47,20 @@ github.com/prometheus/common/model github.com/prometheus/procfs github.com/prometheus/procfs/internal/fs github.com/prometheus/procfs/internal/util +# github.com/rogpeppe/go-internal v1.10.0 +## explicit; go 1.19 # github.com/rs/zerolog v1.29.1 ## explicit; go 1.15 github.com/rs/zerolog github.com/rs/zerolog/internal/cbor github.com/rs/zerolog/internal/json github.com/rs/zerolog/log +# github.com/sethvargo/go-limiter v0.7.2 +## explicit; go 1.14 +github.com/sethvargo/go-limiter +github.com/sethvargo/go-limiter/httplimit +github.com/sethvargo/go-limiter/internal/fasttime +github.com/sethvargo/go-limiter/memorystore # github.com/stretchr/testify v1.8.2 ## explicit; go 1.13 github.com/stretchr/testify/assert