feat(api): add rate limiting
This commit is contained in:
		
							parent
							
								
									52a03b4aa6
								
							
						
					
					
						commit
						2ee1087eec
					
				
					 4 changed files with 41 additions and 0 deletions
				
			
		|  | @ -43,6 +43,8 @@ type APIError struct { | ||||||
| 	Message string `json:"message,omitempty"` | 	Message string `json:"message,omitempty"` | ||||||
| 	Details string `json:"details,omitempty"` | 	Details string `json:"details,omitempty"` | ||||||
| 
 | 
 | ||||||
|  | 	RatelimitReset *int `json:"ratelimit_reset,omitempty"` | ||||||
|  | 
 | ||||||
| 	// Status is set as the HTTP status code. | 	// Status is set as the HTTP status code. | ||||||
| 	Status int `json:"-"` | 	Status int `json:"-"` | ||||||
| } | } | ||||||
|  | @ -67,6 +69,7 @@ const ( | ||||||
| 	ErrForbidden           = 403 | 	ErrForbidden           = 403 | ||||||
| 	ErrNotFound            = 404 | 	ErrNotFound            = 404 | ||||||
| 	ErrMethodNotAllowed    = 405 | 	ErrMethodNotAllowed    = 405 | ||||||
|  | 	ErrTooManyRequests     = 429 | ||||||
| 	ErrInternalServerError = 500 // catch-all code for unknown errors | 	ErrInternalServerError = 500 // catch-all code for unknown errors | ||||||
| 
 | 
 | ||||||
| 	// Login/authorize error codes | 	// Login/authorize error codes | ||||||
|  | @ -83,6 +86,7 @@ var errCodeMessages = map[int]string{ | ||||||
| 	ErrForbidden:           "Forbidden", | 	ErrForbidden:           "Forbidden", | ||||||
| 	ErrInternalServerError: "Internal server error", | 	ErrInternalServerError: "Internal server error", | ||||||
| 	ErrNotFound:            "Not found", | 	ErrNotFound:            "Not found", | ||||||
|  | 	ErrTooManyRequests:     "Rate limit reached", | ||||||
| 	ErrMethodNotAllowed:    "Method not allowed", | 	ErrMethodNotAllowed:    "Method not allowed", | ||||||
| 
 | 
 | ||||||
| 	ErrInvalidState:     "Invalid OAuth state", | 	ErrInvalidState:     "Invalid OAuth state", | ||||||
|  | @ -97,6 +101,7 @@ var errCodeStatuses = map[int]int{ | ||||||
| 	ErrForbidden:           http.StatusForbidden, | 	ErrForbidden:           http.StatusForbidden, | ||||||
| 	ErrInternalServerError: http.StatusInternalServerError, | 	ErrInternalServerError: http.StatusInternalServerError, | ||||||
| 	ErrNotFound:            http.StatusNotFound, | 	ErrNotFound:            http.StatusNotFound, | ||||||
|  | 	ErrTooManyRequests:     http.StatusTooManyRequests, | ||||||
| 	ErrMethodNotAllowed:    http.StatusMethodNotAllowed, | 	ErrMethodNotAllowed:    http.StatusMethodNotAllowed, | ||||||
| 
 | 
 | ||||||
| 	ErrInvalidState:     http.StatusBadRequest, | 	ErrInvalidState:     http.StatusBadRequest, | ||||||
|  |  | ||||||
|  | @ -3,11 +3,14 @@ package server | ||||||
| import ( | import ( | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"strconv" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/db" | 	"codeberg.org/u1f320/pronouns.cc/backend/db" | ||||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/server/auth" | 	"codeberg.org/u1f320/pronouns.cc/backend/server/auth" | ||||||
| 	"github.com/go-chi/chi/v5" | 	"github.com/go-chi/chi/v5" | ||||||
| 	"github.com/go-chi/chi/v5/middleware" | 	"github.com/go-chi/chi/v5/middleware" | ||||||
|  | 	"github.com/go-chi/httprate" | ||||||
| 	"github.com/go-chi/render" | 	"github.com/go-chi/render" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -41,6 +44,33 @@ func New() (*Server, error) { | ||||||
| 	// enable authentication for all routes (but don't require it) | 	// enable authentication for all routes (but don't require it) | ||||||
| 	s.Router.Use(s.maybeAuth) | 	s.Router.Use(s.maybeAuth) | ||||||
| 
 | 
 | ||||||
|  | 	// rate limit handling | ||||||
|  | 	// - 120 req/minute (2/s) | ||||||
|  | 	// - keyed by Authorization header if valid token is provided, otherwise by IP | ||||||
|  | 	// - returns rate limit reset info in error | ||||||
|  | 	s.Router.Use(httprate.Limit( | ||||||
|  | 		120, time.Minute, | ||||||
|  | 		httprate.WithKeyFuncs(func(r *http.Request) (string, error) { | ||||||
|  | 			_, ok := ClaimsFromContext(r.Context()) | ||||||
|  | 			if token := r.Header.Get("Authorization"); ok && token != "" { | ||||||
|  | 				return token, nil | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			ip, err := httprate.KeyByIP(r) | ||||||
|  | 			return ip, err | ||||||
|  | 		}), | ||||||
|  | 		httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 			reset, _ := strconv.Atoi(w.Header().Get("X-RateLimit-Reset")) | ||||||
|  | 
 | ||||||
|  | 			render.Status(r, http.StatusTooManyRequests) | ||||||
|  | 			render.JSON(w, r, APIError{ | ||||||
|  | 				Code:           ErrTooManyRequests, | ||||||
|  | 				Message:        errCodeMessages[ErrTooManyRequests], | ||||||
|  | 				RatelimitReset: &reset, | ||||||
|  | 			}) | ||||||
|  | 		}), | ||||||
|  | 	)) | ||||||
|  | 
 | ||||||
| 	// return an API error for not found + method not allowed | 	// return an API error for not found + method not allowed | ||||||
| 	s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) { | 	s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		render.Status(r, errCodeStatuses[ErrNotFound]) | 		render.Status(r, errCodeStatuses[ErrNotFound]) | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -8,6 +8,7 @@ require ( | ||||||
| 	github.com/bwmarrin/discordgo v0.25.0 | 	github.com/bwmarrin/discordgo v0.25.0 | ||||||
| 	github.com/georgysavva/scany v0.3.0 | 	github.com/georgysavva/scany v0.3.0 | ||||||
| 	github.com/go-chi/chi/v5 v5.0.7 | 	github.com/go-chi/chi/v5 v5.0.7 | ||||||
|  | 	github.com/go-chi/httprate v0.5.3 | ||||||
| 	github.com/go-chi/render v1.0.1 | 	github.com/go-chi/render v1.0.1 | ||||||
| 	github.com/golang-jwt/jwt/v4 v4.4.1 | 	github.com/golang-jwt/jwt/v4 v4.4.1 | ||||||
| 	github.com/jackc/pgconn v1.12.0 | 	github.com/jackc/pgconn v1.12.0 | ||||||
|  | @ -21,6 +22,7 @@ require ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
|  | 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | ||||||
| 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect | 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect | ||||||
| 	github.com/golang/protobuf v1.5.2 // indirect | 	github.com/golang/protobuf v1.5.2 // indirect | ||||||
| 	github.com/gorilla/websocket v1.4.2 // indirect | 	github.com/gorilla/websocket v1.4.2 // indirect | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -60,6 +60,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO | ||||||
| github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= | github.com/bwmarrin/discordgo v0.25.0 h1:NXhdfHRNxtwso6FPdzW2i3uBvvU7UIQTghmV2T4nqAs= | ||||||
| github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= | github.com/bwmarrin/discordgo v0.25.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= | ||||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||||
|  | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= | ||||||
|  | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= | ||||||
| github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | ||||||
| github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | ||||||
| github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | ||||||
|  | @ -95,6 +97,8 @@ github.com/georgysavva/scany v0.3.0/go.mod h1:q8QyrfXjmBk9iJD00igd4lbkAKEXAH/zIY | ||||||
| github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= | ||||||
| github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= | github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= | ||||||
| github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | ||||||
|  | github.com/go-chi/httprate v0.5.3 h1:5HPWb0N6ymIiuotMtCfOGpQKiKeqXVzMexHh1W1yXPc= | ||||||
|  | github.com/go-chi/httprate v0.5.3/go.mod h1:kYR4lorHX3It9tTh4eTdHhcF2bzrYnCrRNlv5+IBm2M= | ||||||
| github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= | github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= | ||||||
| github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= | github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= | ||||||
| github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue