2

I am trying to implement http request limiter to allow 10 request per second per user by their usernames. At the max 10 request can be hit to the server including requests which are under processing. Below is what I have implemented with reference of rate-limit.

func init() {
    go cleanupVisitors()
}

func getVisitor(username string) *rate.Limiter {
    mu.Lock()
    defer mu.Unlock()
    v, exists := visitors[username]
    if !exists {
        limiter := rate.NewLimiter(10, 3)
        visitors[username] = &visitor{limiter, time.Now()}
        return limiter
    }
    v.lastSeen = time.Now()
    return v.limiter
}
func cleanupVisitors() {
    for {
        time.Sleep(time.Minute)
        mu.Lock()
        for username, v := range visitors {
            if time.Since(v.lastSeen) > 1*time.Minute {
                delete(visitors, username)
            }
        }
        mu.Unlock()
    }
}

func limit(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        mappedArray := hotelapi.SearchResponse{}
        mappedArray.StartTime = time.Now().Format("2006-02-01 15:04:05.000000")
        mappedArray.EndTime = time.Now().Format("2006-02-01 15:04:05.000000")
        userName := r.FormValue("username")
        limiter := getVisitor(userName)
        if !limiter.Allow() {
            w.Header().Set("Content-Type", "application/json")
            w.WriteHeader(http.StatusTooManyRequests)
            mappedArray.MessageInfo = http.StatusText(http.StatusTooManyRequests)
            mappedArray.ErrorCode = strconv.Itoa(http.StatusTooManyRequests)
            json.NewEncoder(w).Encode(mappedArray)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func route() {
    r := mux.NewRouter()
    r.PathPrefix("/hello").HandlerFunc(api.ProcessHello).Methods("GET")
    ws := r.PathPrefix("/index.php").HandlerFunc(api.ProcessWs).Methods("GET", "POST").Subrouter()
    r.Use(panicRecovery)
    ws.Use(limit)
    http.HandleFunc("/favicon.ico", faviconHandler)
    if config.HTTPSEnabled {
        err := http.ListenAndServeTLS(":"+config.Port, config.HTTPSCertificateFilePath, config.HTTPSKeyFilePath, handlers.CompressHandlerLevel(r, gzip.BestSpeed))
        if err != nil {
            fmt.Println(err)
            log.Println(err)
        }
    } else {
        err := http.ListenAndServe(":"+config.Port, handlers.CompressHandler(r))
        if err != nil {
            fmt.Println(err)
            log.Println(err)
        }
    }
}

I have couple of concerns here.

I want limiter only for /index.php and not for /hello. I did implement with Sub route. Is it correct way?

The limit middle ware is not limiting as I assumed. It allows 1 successful request all other requests are returned with too many requests error.

What am I missing here. ?

ganesh.go
  • 39
  • 8
  • 1
    Are you sure `userName := r.FormValue("username")` is getting a valid non-blank value for each request? If not, then all requests will use just a single rate limiter. – colm.anseo Sep 30 '21 at 12:17
  • @colm.anseo yes. I am getting non-blank username. – ganesh.go Sep 30 '21 at 12:23

1 Answers1

1

the subrouter pattern is a solution gorilla proposes , small organizational suggestion though:

    r := mux.NewRouter()
    r.HandlerFunc("/hello", api.ProcessHello).Methods("GET")
    r.HandleFunc("/favicon.ico", faviconHandler)
    r.Use(panicRecovery)

    ws := r.PathPrefix("/index.php").Subrouter()
    ws.Use(limit)
    ws.HandlerFunc(api.ProcessWs).Methods("GET", "POST")

you seem to be calling your middleware not only via the Use() method but also calling it over the handler on ListenAndServe, I also see from gorilla same example that a more clear way to approach this is:

  server := &http.Server{
        Addr:         "0.0.0.0:8080",
        // Good practice to set timeouts to avoid Slowloris attacks.
        WriteTimeout: time.Second * 15,
        ReadTimeout:  time.Second * 15,
        IdleTimeout:  time.Second * 60,
        Handler: router, // Pass our instance of gorilla/mux in.
    }

  fmt.Println("starting server")
  if err := server.ListenAndServe(); err != nil {
    fmt.Println(err)
  }

Also, from your source, the pattern of rate limiting you are implementing is to rate limit per user, but you use usernames instead of their IPs to limit their requests, and your question begins without clarifying if you wish to ratelimit per user or rate limit how many requests can be done to the endpoint overall - so maybe you might be getting unexpected behavior due to that too.

bjornaer
  • 336
  • 3
  • 9