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"` | ||||
| 	Details string `json:"details,omitempty"` | ||||
| 
 | ||||
| 	RatelimitReset *int `json:"ratelimit_reset,omitempty"` | ||||
| 
 | ||||
| 	// Status is set as the HTTP status code. | ||||
| 	Status int `json:"-"` | ||||
| } | ||||
|  | @ -67,6 +69,7 @@ const ( | |||
| 	ErrForbidden           = 403 | ||||
| 	ErrNotFound            = 404 | ||||
| 	ErrMethodNotAllowed    = 405 | ||||
| 	ErrTooManyRequests     = 429 | ||||
| 	ErrInternalServerError = 500 // catch-all code for unknown errors | ||||
| 
 | ||||
| 	// Login/authorize error codes | ||||
|  | @ -83,6 +86,7 @@ var errCodeMessages = map[int]string{ | |||
| 	ErrForbidden:           "Forbidden", | ||||
| 	ErrInternalServerError: "Internal server error", | ||||
| 	ErrNotFound:            "Not found", | ||||
| 	ErrTooManyRequests:     "Rate limit reached", | ||||
| 	ErrMethodNotAllowed:    "Method not allowed", | ||||
| 
 | ||||
| 	ErrInvalidState:     "Invalid OAuth state", | ||||
|  | @ -97,6 +101,7 @@ var errCodeStatuses = map[int]int{ | |||
| 	ErrForbidden:           http.StatusForbidden, | ||||
| 	ErrInternalServerError: http.StatusInternalServerError, | ||||
| 	ErrNotFound:            http.StatusNotFound, | ||||
| 	ErrTooManyRequests:     http.StatusTooManyRequests, | ||||
| 	ErrMethodNotAllowed:    http.StatusMethodNotAllowed, | ||||
| 
 | ||||
| 	ErrInvalidState:     http.StatusBadRequest, | ||||
|  |  | |||
|  | @ -3,11 +3,14 @@ package server | |||
| import ( | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/db" | ||||
| 	"codeberg.org/u1f320/pronouns.cc/backend/server/auth" | ||||
| 	"github.com/go-chi/chi/v5" | ||||
| 	"github.com/go-chi/chi/v5/middleware" | ||||
| 	"github.com/go-chi/httprate" | ||||
| 	"github.com/go-chi/render" | ||||
| ) | ||||
| 
 | ||||
|  | @ -41,6 +44,33 @@ func New() (*Server, error) { | |||
| 	// enable authentication for all routes (but don't require it) | ||||
| 	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 | ||||
| 	s.Router.NotFound(func(w http.ResponseWriter, r *http.Request) { | ||||
| 		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/georgysavva/scany v0.3.0 | ||||
| 	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/golang-jwt/jwt/v4 v4.4.1 | ||||
| 	github.com/jackc/pgconn v1.12.0 | ||||
|  | @ -21,6 +22,7 @@ require ( | |||
| ) | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/cespare/xxhash/v2 v2.1.2 // indirect | ||||
| 	github.com/go-gorp/gorp/v3 v3.0.2 // indirect | ||||
| 	github.com/golang/protobuf v1.5.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/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= | ||||
| 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/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | ||||
| 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/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/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/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= | ||||
| 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