From 500bfdc41735bc8d617cbfd4f1ab6b5980c8f1e5 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Thu, 2 Nov 2023 19:06:05 +0100 Subject: dashboard: throttle incoming requests To ensure service stability, let's rate limit incoming requests to our web endpoints. --- dashboard/app/handler.go | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) (limited to 'dashboard/app/handler.go') diff --git a/dashboard/app/handler.go b/dashboard/app/handler.go index 98827f8ca..7162bd85c 100644 --- a/dashboard/app/handler.go +++ b/dashboard/app/handler.go @@ -33,6 +33,9 @@ func handleContext(fn contextHandler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) c = context.WithValue(c, ¤tURLKey, r.URL.RequestURI()) + if !throttleRequest(c, w, r) { + return + } if err := fn(c, w, r); err != nil { hdr := commonHeaderRaw(c, r) data := &struct { @@ -75,6 +78,42 @@ func handleContext(fn contextHandler) http.Handler { }) } +func throttleRequest(c context.Context, w http.ResponseWriter, r *http.Request) bool { + // AppEngine removes all App Engine-specific headers, which include + // X-Appengine-User-IP and X-Forwarded-For. + // https://cloud.google.com/appengine/docs/standard/reference/request-headers?tab=python#removed_headers + ip := r.Header.Get("X-Appengine-User-IP") + if ip == "" { + ip = r.Header.Get("X-Forwarded-For") + ip, _, _ = strings.Cut(ip, ",") // X-Forwarded-For is a comma-delimited list. + ip = strings.TrimSpace(ip) + } + cron := r.Header.Get("X-Appengine-Cron") != "" + if ip == "" || cron { + log.Infof(c, "cannot throttle request from %q, cron %t", ip, cron) + return true + } + accept, err := ThrottleRequest(c, ip) + if err != nil { + log.Errorf(c, "failed to throttle: %v", err) + } + log.Infof(c, "throttling for %q: %t", ip, accept) + if !accept { + http.Error(w, throttlingErrorMessage(c), http.StatusTooManyRequests) + return false + } + return true +} + +func throttlingErrorMessage(c context.Context) string { + ret := "429 Too Many Requests" + email := getConfig(c).ContactEmail + if email == "" { + return ret + } + return fmt.Sprintf("%s\nPlease contact us at %s if you need access to our data.", ret, email) +} + var currentURLKey = "the URL of the HTTP request in context" func getCurrentURL(c context.Context) string { -- cgit mrf-deployment