diff --git a/CONFIG.md b/CONFIG.md index 0ce57ee..2cf0d03 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -53,6 +53,12 @@ Bearer token queues max size. Internally, bearer queues are put in an LRU map, t Requests are never interrupted midway, even when an entry is evicted. A low LRU size may cause increased 429s if a bearer token has too many requests queued and fires another one after eviction. Default: 1024 +##### DISABLE_HTTP_2 +Flag to disable HTTP/2 requests on the client that makes discord requests. Does not impact the http server. +This flag defaults to true due to go http2 support having a few minor issues that can cause requests to fail. + +Default: true + ## Unstable env vars Collection of env vars that may be removed at any time, mainly used for Discord introducing new behaviour on their edge api versions diff --git a/README.md b/README.md index dded248..d9b5ca8 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Configuration options are | CLUSTER_PORT | number | 7946 | | CLUSTER_MEMBERS | string list (comma separated) | "" | | CLUSTER_DNS | string | "" | +| MAX_BEARER_COUNT| number | 1024 | +| DISABLE_HTTP_2 | bool | true | Information on each config var can be found [here](https://github.com/germanoeich/nirn-proxy/blob/main/CONFIG.md) diff --git a/lib/bucketpath.go b/lib/bucketpath.go index f35081d..d5906c7 100644 --- a/lib/bucketpath.go +++ b/lib/bucketpath.go @@ -66,7 +66,11 @@ func GetOptimisticBucketPath(url string, method string) string { l := len(cleanUrl) i := strings.Index(cleanUrl, "/") cleanUrl = cleanUrl[i+1:l] + } else { + // Handle unversioned endpoints + cleanUrl = strings.ReplaceAll(cleanUrl, "/api/", "") } + parts := strings.Split(cleanUrl, "/") numParts := len(parts) diff --git a/lib/bucketpath_test.go b/lib/bucketpath_test.go index 5055e60..a3dfcff 100644 --- a/lib/bucketpath_test.go +++ b/lib/bucketpath_test.go @@ -30,6 +30,12 @@ func TestPaths(t *testing.T) { {"/api/v9/guilds/203039963636301824/channels", "GET", "/guilds/!/channels"}, // Wierd routes {"/api/v9/guilds/templates/203039963636301824", "GET", "/guilds/templates/!"}, + // Unversioned routes + {"/api/webhooks/203039963636301824/VSOzAqY1OZFF5WJVtbIzFtmjGupk-84Hn0A_ZzToF_CHsPIeCk0Q9Uok_mjxR0dNtApI", "POST", "/webhooks/203039963636301824/!"}, + {"/api/interactions/203039963636301824/aW50ZXJhY3Rpb246ODg3NTU5MDA01AY4NTUxNDU0OnZwS3QycDhvREk2aVF3U1BqN2prcXBkRmNqNlp4VEhGRjZvSVlXSGh4WG4yb3l6Z3B6NTBPNVc3OHphV05OULLMOHBMa2RTZmVKd3lzVDA2b2h3OTUxaFJ4QlN0dkxXallPcmhnSHNJb0tSV0M5ZzY1NkN4VGRvemFOSHY4b05c/callback", "GET", "/interactions/203039963636301824/!/callback"}, + {"/api/channels/872712139712913438/messages/872712150509047809/reactions/PandaOhShit:863985751205085195", "GET", "/channels/872712139712913438/messages/!/reactions/!/!"}, + {"/api/invites/dyno", "GET", "/invites/!"}, + } for _, tt := range tests { testname := fmt.Sprintf("%s-%s", tt.method, tt.path) diff --git a/lib/discord.go b/lib/discord.go index 6ebb3c0..75bd927 100644 --- a/lib/discord.go +++ b/lib/discord.go @@ -29,59 +29,61 @@ type BotUserResponse struct { Discrim string `json:"discriminator"` } -func createTransport(ip string) http.RoundTripper { +func createTransport(ip string, disableHttp2 bool) http.RoundTripper { + var transport http.Transport if ip == "" { - // http.DefaultTransport options, with http2 disabled - return &http.Transport{ + // http.DefaultTransport options + transport = http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, - ForceAttemptHTTP2: false, + ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, - TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{}, } - } - addr, err := net.ResolveTCPAddr("tcp", ip+":0") + } else { + addr, err := net.ResolveTCPAddr("tcp", ip+":0") - if err != nil { - panic(err) - } + if err != nil { + panic(err) + } - dialer := &net.Dialer{ - Deadline: time.Time{}, - LocalAddr: addr, - FallbackDelay: 0, - Resolver: nil, - Control: nil, - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - } + dialer := &net.Dialer{ + LocalAddr: addr, + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + } - dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { - conn, err := dialer.Dial(network, addr) - return conn, err + dialContext := func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := dialer.Dial(network, addr) + return conn, err + } + + transport = http.Transport{ + ForceAttemptHTTP2: true, + MaxIdleConns: 1000, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 2 * time.Second, + DialContext: dialContext, + ResponseHeaderTimeout: 0, + } } - transport := http.Transport{ - ForceAttemptHTTP2: false, - MaxIdleConns: 1000, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 2 * time.Second, - DialContext: dialContext, - ResponseHeaderTimeout: 0, - TLSNextProto: map[string]func(string, *tls.Conn) http.RoundTripper{}, + if disableHttp2 { + transport.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{} + transport.ForceAttemptHTTP2 = false } + return &transport } -func ConfigureDiscordHTTPClient(ip string, timeout time.Duration) { - transport := createTransport(ip) +func ConfigureDiscordHTTPClient(ip string, timeout time.Duration, disableHttp2 bool) { + transport := createTransport(ip, disableHttp2) client = &http.Client{ Transport: transport, Timeout: 90 * time.Second, diff --git a/lib/env.go b/lib/env.go index 87939fa..e263cc6 100644 --- a/lib/env.go +++ b/lib/env.go @@ -13,6 +13,19 @@ func EnvGet(name string, defaultVal string) string { return val } +func EnvGetBool(name string, defaultVal bool) bool { + val := os.Getenv(name) + if val == "" { + return defaultVal + } + + if val != "true" && val != "false" { + panic("Invalid env var, expected true or false, got " + val + " for " + name) + } + return val == "true" +} + + func EnvMustGet(name string) string { val := os.Getenv(name) if val == "" { diff --git a/lib/logger.go b/lib/logger.go index 0b4d202..e85d19b 100644 --- a/lib/logger.go +++ b/lib/logger.go @@ -2,10 +2,30 @@ package lib import ( "github.com/sirupsen/logrus" + "regexp" ) +type GlobalHook struct { +} + +var loggerHookRegex = regexp.MustCompile("(\\/[0-9]{17,26}\\/)[a-zA-Z0-9\\-_]{63,}") + +func (h *GlobalHook) Levels() []logrus.Level { + return logrus.AllLevels +} + +func (h *GlobalHook) Fire(e *logrus.Entry) error { + e.Message = loggerHookRegex.ReplaceAllString(e.Message, "$1:token") + if e.Data["path"] != nil { + e.Data["path"] = loggerHookRegex.ReplaceAllString(e.Data["path"].(string), "$1:token") + } + return nil +} + + var logger *logrus.Logger func SetLogger(l *logrus.Logger) { logger = l + logger.AddHook(&GlobalHook{}) } diff --git a/lib/queue_manager.go b/lib/queue_manager.go index ef56f75..9a7703a 100644 --- a/lib/queue_manager.go +++ b/lib/queue_manager.go @@ -1,6 +1,8 @@ package lib import ( + "context" + "errors" lru "github.com/hashicorp/golang-lru" "github.com/hashicorp/memberlist" "github.com/sirupsen/logrus" @@ -171,7 +173,7 @@ func (m *QueueManager) Generate429(resp *http.ResponseWriter) { writer.Header().Set("x-ratelimit-scope", "user") writer.Header().Set("x-ratelimit-limit", "1") writer.Header().Set("x-ratelimit-remaining", "0") - writer.Header().Set("x-ratelimit-reset", string(time.Now().Add(1 * time.Second).Unix())) + writer.Header().Set("x-ratelimit-reset", strconv.FormatInt(time.Now().Add(1*time.Second).Unix(), 10)) writer.Header().Set("x-ratelimit-after", "1") writer.Header().Set("retry-after", "1") writer.Header().Set("content-type", "application/json") @@ -301,7 +303,12 @@ func (m *QueueManager) fulfillRequest(resp *http.ResponseWriter, req *http.Reque } _, _, err = q.Queue(req, resp, path, pathHash) if err != nil { - logger.WithFields(logrus.Fields{"function": "Queue"}).Error(err) + log := logger.WithFields(logrus.Fields{"function": "Queue"}) + if errors.Is(err, context.DeadlineExceeded) { + log.Warn(err) + } else { + log.Error(err) + } } } else { var res *http.Response diff --git a/main.go b/main.go index dfffcec..6a844f6 100644 --- a/main.go +++ b/main.go @@ -69,7 +69,9 @@ func main() { timeout := lib.EnvGetInt("REQUEST_TIMEOUT", 5000) - lib.ConfigureDiscordHTTPClient(outboundIp, time.Duration(timeout) * time.Millisecond) + disableHttp2 := lib.EnvGetBool("DISABLE_HTTP_2", true) + + lib.ConfigureDiscordHTTPClient(outboundIp, time.Duration(timeout) * time.Millisecond, disableHttp2) port := lib.EnvGet("PORT", "8080") bindIp := lib.EnvGet("BIND_IP", "0.0.0.0")