Advertisement
Guest User

Untitled

a guest
Aug 20th, 2018
159
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Go 6.20 KB | None | 0 0
  1. package httd
  2.  
  3. import (
  4.     "net/http"
  5.     "strconv"
  6.     "strings"
  7.     "sync"
  8.     "time"
  9.  
  10.     "encoding/json"
  11.     "github.com/andersfylling/snowflake"
  12. )
  13.  
  14. const (
  15.     XRateLimitLimit      = "X-RateLimit-Limit"
  16.     XRateLimitRemaining  = "X-RateLimit-Remaining"
  17.     XRateLimitReset      = "X-RateLimit-Reset"
  18.     XRateLimitGlobal     = "X-RateLimit-Global"
  19.     RateLimitRetryAfter  = "Retry-After"
  20.     GlobalRateLimiterKey = ""
  21. )
  22.  
  23. // const
  24. var majorEndpointPrefixes = []string{
  25.     "/channels/",
  26.     "/guilds/",
  27.     "/webhooks/",
  28. }
  29.  
  30. // TODO: fix ratelimiting logic
  31. func RatelimitChannel(id snowflake.ID) string {
  32.     return "c:" + id.String()
  33. }
  34.  
  35. func RatelimitGuild(id snowflake.ID) string {
  36.     return "g:" + id.String()
  37. }
  38.  
  39. func RatelimitWebhook() string {
  40.     return "wh"
  41. }
  42.  
  43. func RatelimitUsers() string {
  44.     return "u"
  45. }
  46.  
  47. type RateLimiter interface {
  48.     Bucket(key string) *Bucket
  49.     RateLimitTimeout(key string) int64
  50.     RateLimited(key string) bool
  51.     UpdateRegisters(key string, res *http.Response, responseBody []byte)
  52.     WaitTime(req *Request) time.Duration
  53. }
  54.  
  55. type ratelimitBody struct {
  56.     Message    string `json:"message"`
  57.     RetryAfter int64  `json:"retry_after"`
  58.     Global     bool   `json:"global"`
  59.     Empty      bool   `json:"-"`
  60. }
  61.  
  62. type RateLimitInfo struct {
  63.     Message    string `json:"message"`
  64.     RetryAfter int64  `json:"retry_after"`
  65.     Global     bool   `json:"global"`
  66.     Limit      int    `json:"-"`
  67.     Remaining  int    `json:"-"`
  68.     Reset      int64  `json:"-"`
  69.     Empty      bool   `json:"-"`
  70. }
  71.  
  72. func RateLimited(resp *http.Response) bool {
  73.     return resp.StatusCode == http.StatusTooManyRequests
  74. }
  75.  
  76. // GlobalRateLimit assumes that there will always be a header entry when a global rate limit kicks in
  77. func GlobalRateLimit(resp *http.Response) bool {
  78.     return resp.Header.Get(XRateLimitGlobal) == "true"
  79. }
  80.  
  81. func GlobalRateLimitSafe(resp *http.Response, body *ratelimitBody) bool {
  82.     return GlobalRateLimit(resp) && !body.Empty && body.Global
  83. }
  84.  
  85. func ExtractRateLimitInfo(resp *http.Response, body []byte) (info *RateLimitInfo, err error) {
  86.     // extract header information
  87.     limitStr := resp.Header.Get(XRateLimitLimit)
  88.     remainingStr := resp.Header.Get(XRateLimitRemaining)
  89.     resetStr := resp.Header.Get(XRateLimitReset)
  90.     retryAfterStr := resp.Header.Get(RateLimitRetryAfter)
  91.  
  92.     // convert types
  93.     if limitStr != "" {
  94.         info.Limit, err = strconv.Atoi(limitStr)
  95.         if err != nil {
  96.             return
  97.         }
  98.     }
  99.     if remainingStr != "" {
  100.         info.Remaining, err = strconv.Atoi(remainingStr)
  101.         if err != nil {
  102.             return
  103.         }
  104.     }
  105.     if resetStr != "" {
  106.         info.Reset, err = strconv.ParseInt(resetStr, 10, 64)
  107.         if err != nil {
  108.             return
  109.         }
  110.         info.Reset *= 1000 // second => milliseconds
  111.     }
  112.     if retryAfterStr != "" {
  113.         info.RetryAfter, err = strconv.ParseInt(retryAfterStr, 10, 64)
  114.         if err != nil {
  115.             return
  116.         }
  117.     }
  118.     info.Global = GlobalRateLimit(resp) // useless? assuming that global info exists when exceeded an rate limit
  119.  
  120.     // the body only contains information when a rate limit is exceeded
  121.     if RateLimited(resp) {
  122.         err = json.Unmarshal(body, &info)
  123.     }
  124.     return
  125. }
  126.  
  127. func NewRateLimit() *RateLimit {
  128.     return &RateLimit{
  129.         buckets: make(map[string]*Bucket),
  130.         global:  &Bucket{},
  131.     }
  132. }
  133.  
  134. // RateLimit
  135. // TODO: a bucket is created for every request. Might want to delete them after a while. seriously.
  136. // `/users/1` has the same ratelimiter as `/users/2`
  137. // but any major endpoint prefix does not: `/channels/1` != `/channels/2`
  138. type RateLimit struct {
  139.     buckets map[string]*Bucket
  140.     global  *Bucket
  141.     mu      sync.RWMutex
  142. }
  143.  
  144. func (r *RateLimit) Bucket(key string) *Bucket {
  145.     var bucket *Bucket
  146.     var exists bool
  147.  
  148.     // check for major endpoints
  149.     // TODO: this feels frail
  150.     var endpoint string
  151.     for _, major := range majorEndpointPrefixes {
  152.         if !strings.HasPrefix(key, major) {
  153.             continue
  154.         }
  155.         pathAfterMajor := strings.TrimPrefix(key, major)
  156.         endpoint = major
  157.         for _, r := range pathAfterMajor {
  158.             if r == '/' {
  159.                 break
  160.             }
  161.             endpoint += string(r)
  162.         }
  163.     }
  164.     if endpoint == "" {
  165.         endpoint = key
  166.     }
  167.  
  168.     r.mu.Lock()
  169.     if bucket, exists = r.buckets[key]; !exists {
  170.         r.buckets[key] = &Bucket{
  171.             endpoint: key,
  172.             reset:    time.Now().UnixNano() / 1000,
  173.         }
  174.         bucket = r.buckets[key]
  175.     }
  176.     r.mu.Unlock()
  177.  
  178.     return bucket
  179. }
  180.  
  181. func (r *RateLimit) RateLimitTimeout(key string) int64 {
  182.     bucket := r.Bucket(key)
  183.     return bucket.timeout()
  184. }
  185.  
  186. func (r *RateLimit) RateLimited(key string) bool {
  187.     bucket := r.Bucket(key)
  188.     return bucket.limited()
  189. }
  190.  
  191. // WaitTime check both the global and route specific rate limiter bucket before sending any REST requests
  192. func (r *RateLimit) WaitTime(req *Request) time.Duration {
  193.     timeout := int64(0)
  194.     if r.global.remaining == 0 {
  195.         timeout = r.global.timeout()
  196.     } else if req.Ratelimiter != "" {
  197.         timeout = r.RateLimitTimeout(req.Ratelimiter)
  198.     }
  199.  
  200.     // discord specifies this in seconds, however it is converted to milliseconds before stored in memory.
  201.     return time.Millisecond * time.Duration(timeout)
  202. }
  203.  
  204. // TODO: rewrite
  205. func (r *RateLimit) UpdateRegisters(key string, resp *http.Response, content []byte) {
  206.  
  207.     info, err := ExtractRateLimitInfo(resp, content)
  208.     if err != nil {
  209.         return // TODO: logging
  210.     }
  211.  
  212.     // TODO: what if "key" is an endpoint with a global rate limiter only?
  213.     if info.Global {
  214.         r.global.mu.Lock()
  215.         r.global.update(info)
  216.         r.global.mu.Unlock()
  217.     } else {
  218.         bucket := r.Bucket(key)
  219.         bucket.mu.Lock()
  220.         bucket.update(info)
  221.         bucket.mu.Unlock()
  222.     }
  223. }
  224.  
  225. // ---------------------
  226.  
  227. type Bucket struct {
  228.     endpoint  string // endpoint where rate limit is applied. endpoint = key
  229.     limit     uint64 // total allowed requests before rate limit
  230.     remaining uint64 // remaining requests
  231.     reset     int64  // unix milliseconds, even tho discord prefers seconds. global uses milliseconds however.
  232.  
  233.     mu sync.RWMutex
  234. }
  235.  
  236. func (b *Bucket) update(info *RateLimitInfo) {
  237.     b.limit = uint64(info.Limit)
  238.     b.remaining = uint64(info.Remaining)
  239.     b.reset = info.Reset + info.RetryAfter
  240. }
  241.  
  242. func (b *Bucket) limited() bool {
  243.     b.mu.RLock()
  244.     defer b.mu.RUnlock()
  245.  
  246.     return b.reset > (time.Now().UnixNano() / 1000)
  247. }
  248.  
  249. func (b *Bucket) timeout() int64 {
  250.     b.mu.RLock()
  251.     defer b.mu.RUnlock()
  252.  
  253.     now := time.Now().UnixNano() / 1000
  254.     if b.reset > now {
  255.         return b.reset - now
  256.     }
  257.     return 0
  258. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement