diff --git a/blog.go b/blog.go index 0d4bfd2..b6e728c 100644 --- a/blog.go +++ b/blog.go @@ -29,6 +29,7 @@ import ( "github.com/kirsle/blog/src/middleware" "github.com/kirsle/blog/src/middleware/auth" "github.com/kirsle/blog/src/models" + "github.com/kirsle/blog/src/ratelimit" "github.com/kirsle/blog/src/render" "github.com/kirsle/blog/src/responses" "github.com/kirsle/blog/src/sessions" @@ -123,6 +124,7 @@ func (b *Blog) Configure() { b.Cache = cache b.jsonDB.Cache = cache markdown.Cache = cache + ratelimit.Cache = cache } } diff --git a/go.mod b/go.mod index 7978f46..ff331a3 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ module github.com/kirsle/blog -go 1.12 +go 1.22 require ( - github.com/disintegration/imaging v1.6.0 // indirect github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b github.com/garyburd/redigo v1.6.0 github.com/google/uuid v1.1.1 @@ -14,9 +13,24 @@ require ( github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292 github.com/microcosm-cc/bluemonday v1.0.2 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 + github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 + github.com/urfave/negroni v1.0.0 + golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df +) + +require ( + github.com/disintegration/imaging v1.6.0 // indirect + github.com/gorilla/context v1.1.1 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mattn/go-sqlite3 v1.10.0 // indirect + github.com/russross/blackfriday v1.5.2 // indirect github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect github.com/sergi/go-diff v1.0.0 // indirect - github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 + github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a // indirect + github.com/shurcooL/go-goon v1.0.0 // indirect github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 // indirect github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1 // indirect github.com/shurcooL/octicon v0.0.0-20181222203144-9ff1a4cf27f4 // indirect @@ -24,7 +38,8 @@ require ( github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e // indirect - github.com/urfave/negroni v1.0.0 - golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443 - gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df + golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 // indirect + golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect + golang.org/x/sys v0.0.0-20190412213103-97732733099d // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/go.sum b/go.sum index 3c4d406..6ad13c5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -9,7 +10,10 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 h1:tkum0XDgfR0jcVVXuTsYv/erY2NnEDqwRojbxR1rBYA= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/disintegration/imaging v1.6.0 h1:nVPXRUUQ36Z7MNf0O77UzgnOb1mkMMor7lmJMJXc/mA= github.com/disintegration/imaging v1.6.0/go.mod h1:xuIt+sRxDFrHS0drzXUlCJthkJ8k7lkkUojDSR247MQ= @@ -18,12 +22,14 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b h1:6CBzNasH8+bKeFwr5Bt5JtALHLFN4iQp7sf4ShlP/ik= github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b/go.mod h1:KoE3Ti1qbQXCb3s/XGj0yApHnbnNnn1bXTtB5Auq/Vc= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc= github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -57,6 +63,7 @@ github.com/jinzhu/gorm v1.9.9 h1:Gc8bP20O+vroFUzZEXA1r7vNGQZGQ+RKgOnriuNF3ds= github.com/jinzhu/gorm v1.9.9/go.mod h1:Kh6hTsSGffh4ui079FHrR5Gg+5D0hgihqDcsDN2BBJY= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -65,6 +72,11 @@ github.com/kirsle/golog v0.0.0-20180411020913-51290b4f9292/go.mod h1:0KaOvOX8s5Y github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= @@ -79,7 +91,9 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -89,6 +103,8 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= @@ -97,6 +113,10 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470 h1:qb9IthCFBmROJ6YBS31BEMeSYjOscSiG+EO+JVNTz64= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a h1:ZHfoO7ZJhws9NU1kzZhStUnnVQiPtDe1PzpUnc6HirU= +github.com/shurcooL/go v0.0.0-20230706063926-5fe729b41b3a/go.mod h1:DNrlr0AR9NsHD/aoc2pPeu4uSBZ/71yCHkR42yrzW3M= +github.com/shurcooL/go-goon v1.0.0 h1:BCQPvxGkHHJ4WpBO4m/9FXbITVIsvAm/T66cCcCGI7E= +github.com/shurcooL/go-goon v1.0.0/go.mod h1:2wTHMsGo7qnpmqA8ADYZtP4I1DD94JpXGQ3Dxq2YQ5w= github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 h1:KaKXZldeYH73dpQL+Nr38j1r5BgpAYQjYvENOUpIZDQ= github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= github.com/shurcooL/highlight_go v0.0.0-20181215221002-9d8641ddf2e1 h1:a6a6gGfBoO2ty+yyHNd7M6gkp37EwE3GIoycUnLo1Oo= @@ -111,6 +131,7 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e h1:qpG93cPwA5f7s/ZPBJnGOYQNK/vKsaDaseuKT5Asee8= github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e h1:Ee+VZw13r9NTOMnwTPs6O5KZ0MJU54hsxu9FpZ4pQ10= github.com/tomnomnom/xtermcolor v0.0.0-20160428124646-b78803f00a7e/go.mod h1:fSIW/szJHsRts/4U8wlMPhs+YqJC+7NYR+Qqb1uJVpA= @@ -162,6 +183,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -169,6 +191,8 @@ google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRn google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/jsondb/caches/redis/redis.go b/jsondb/caches/redis/redis.go index 59a4757..ac87dd8 100644 --- a/jsondb/caches/redis/redis.go +++ b/jsondb/caches/redis/redis.go @@ -1,6 +1,7 @@ package redis import ( + "encoding/json" "fmt" "time" @@ -44,6 +45,16 @@ func (r *Redis) Get(key string) ([]byte, error) { return n, err } +// GetJSON gets a JSON value from a Redis key. +func (r *Redis) GetJSON(key string, v any) error { + val, err := r.Get(key) + if err != nil { + return err + } + + return json.Unmarshal(val, v) +} + // Set a key in Redis. func (r *Redis) Set(key string, v []byte, expires int) error { conn := r.pool.Get() @@ -55,6 +66,16 @@ func (r *Redis) Set(key string, v []byte, expires int) error { return nil } +// SetJSON sets a JSON encoded value into a Redis key. +func (r *Redis) SetJSON(key string, v any, expires int) error { + bin, err := json.Marshal(v) + if err != nil { + return err + } + + return r.Set(key, bin, expires) +} + // Delete keys from Redis. func (r *Redis) Delete(key ...string) { conn := r.pool.Get() diff --git a/src/controllers/authctl/authctl.go b/src/controllers/authctl/authctl.go index dae0f2e..33a768e 100644 --- a/src/controllers/authctl/authctl.go +++ b/src/controllers/authctl/authctl.go @@ -5,10 +5,11 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/kirsle/blog/models/users" "github.com/kirsle/blog/src/forms" "github.com/kirsle/blog/src/log" "github.com/kirsle/blog/src/middleware/auth" - "github.com/kirsle/blog/models/users" + "github.com/kirsle/blog/src/ratelimit" "github.com/kirsle/blog/src/render" "github.com/kirsle/blog/src/responses" "github.com/kirsle/blog/src/sessions" @@ -44,6 +45,20 @@ func loginHandler(w http.ResponseWriter, r *http.Request) { if err != nil { vars["Error"] = err } else { + // Rate limit by guessed username. + limiter := &ratelimit.Limiter{ + Namespace: "login", + ID: form.Username, + Limit: 10, + Window: 3600, + CooldownAt: 3, + Cooldown: 10, + } + if err := limiter.Ping(); err != nil { + responses.FlashAndReload(w, r, err.Error()) + return + } + // Test the login. user, err := users.CheckAuth(form.Username, form.Password) if err != nil { diff --git a/src/ratelimit/ratelimit.go b/src/ratelimit/ratelimit.go new file mode 100644 index 0000000..57f624e --- /dev/null +++ b/src/ratelimit/ratelimit.go @@ -0,0 +1,108 @@ +package ratelimit + +import ( + "errors" + "fmt" + "time" + + "github.com/kirsle/blog/jsondb/caches/redis" +) + +// Limiter implements a Redis-backed rate limit for logins or otherwise. +type Limiter struct { + Namespace string // kind of rate limiter ("login") + ID interface{} // unique ID of the resource being pinged (str or ints) + Limit int // how many pings within the window period + Window int // the window period/expiration of Redis key + CooldownAt int // how many pings before the cooldown is enforced + Cooldown int // time to wait between fails +} + +// The active Redis cache given by the webapp. +var Cache *redis.Redis + +// Redis object behind the rate limiter. +type Data struct { + Pings int + NotBefore time.Time +} + +// Ping the rate limiter. +func (l *Limiter) Ping() error { + if Cache == nil { + return errors.New("redis not ready") + } + + var ( + key = l.Key() + now = time.Now() + ) + + // Get stored data from Redis if any. + var data Data + Cache.GetJSON(key, &data) + + // Are we cooling down? + if now.Before(data.NotBefore) { + return fmt.Errorf( + "You are doing that too often.", + ) + } + + // Increment the ping count. + data.Pings++ + + // Have we hit the wall? + if data.Pings >= l.Limit { + return fmt.Errorf( + "You have hit the rate limit; come back later.", + ) + } + + // Are we throttled? + if l.CooldownAt > 0 && data.Pings > l.CooldownAt { + data.NotBefore = now.Add(time.Duration(l.Cooldown) * time.Second) + if err := Cache.SetJSON(key, data, l.Window); err != nil { + return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err) + } + return fmt.Errorf( + "Please wait %ds before trying again. You have %d more attempt(s) remaining before you will be locked "+ + "out for %ds.", + l.Cooldown, + l.Limit-data.Pings, + l.Window, + ) + } + + // Save their ping count to Redis. + if err := Cache.SetJSON(key, data, l.Window); err != nil { + return fmt.Errorf("Couldn't set Redis key for rate limiter: %s", err) + } + + return nil +} + +// Clear the rate limiter, cleaning up the Redis key (e.g., after successful login). +func (l *Limiter) Clear() { + Cache.Delete(l.Key()) +} + +// Key formats the Redis key. +func (l *Limiter) Key() string { + var str string + switch t := l.ID.(type) { + case int: + str = fmt.Sprintf("%d", t) + case uint64: + str = fmt.Sprintf("%d", t) + case int64: + str = fmt.Sprintf("%d", t) + case uint32: + str = fmt.Sprintf("%d", t) + case int32: + str = fmt.Sprintf("%d", t) + default: + str = fmt.Sprintf("%s", t) + } + return fmt.Sprintf("rlimit/%s/%s", l.Namespace, str) +}