diff options
-rw-r--r-- | pkg/api/api0/api.go | 7 | ||||
-rw-r--r-- | pkg/api/api0/client.go | 129 | ||||
-rw-r--r-- | pkg/api/api0/metrics.go | 16 | ||||
-rw-r--r-- | pkg/atlas/server.go | 52 | ||||
-rw-r--r-- | pkg/eax/eax.go | 145 | ||||
-rw-r--r-- | pkg/eax/updatemgr.go | 180 |
6 files changed, 7 insertions, 522 deletions
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go index 355deef..2fc1347 100644 --- a/pkg/api/api0/api.go +++ b/pkg/api/api0/api.go @@ -27,7 +27,6 @@ import ( "github.com/klauspost/compress/gzip" "github.com/pg9182/ip2x" - "github.com/r2northstar/atlas/pkg/eax" "github.com/r2northstar/atlas/pkg/metricsx" "github.com/r2northstar/atlas/pkg/nspkt" "github.com/rs/zerolog/hlog" @@ -48,12 +47,6 @@ type Handler struct { // NSPkt handles connectionless packets. It must be non-nil. NSPkt *nspkt.Listener - // UsernameSource configures the source to use for usernames. - UsernameSource UsernameSource - - // EAXClient makes requests to the EAX API. - EAXClient *eax.Client - // CleanBadWords is used to filter bad words from server names and // descriptions. If not provided, words will not be filtered. CleanBadWords func(s string) string diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go index b718e17..1cb5cf3 100644 --- a/pkg/api/api0/client.go +++ b/pkg/api/api0/client.go @@ -12,34 +12,11 @@ import ( "time" "github.com/r2northstar/atlas/pkg/api/api0/api0gameserver" - "github.com/r2northstar/atlas/pkg/eax" "github.com/r2northstar/atlas/pkg/pdata" "github.com/r2northstar/atlas/pkg/stryder" "github.com/rs/zerolog/hlog" ) -// UsernameSource determines where to get player in-game usernames from. -type UsernameSource string - -const ( - // Don't get usernames. - UsernameSourceNone UsernameSource = "" - - // Get the username from EAX. - UsernameSourceEAX UsernameSource = "eax" - - // Get the username from Stryder (available since October 2, 2023). Note - // that this source only returns usernames for valid tokens. - UsernameSourceStryder UsernameSource = "stryder" - - // Get the username from Stryder, but fall back to EAX on missing/failure. - UsernameSourceStryderEAX UsernameSource = "stryder-eax" - - // Get the username from Stryder, but also check EAX and warn if it's - // different. - UsernameSourceStryderEAXDebug UsernameSource = "stryder-eax-debug" -) - type MainMenuPromos struct { NewInfo MainMenuPromosNew `json:"newInfo"` LargeButton MainMenuPromosButtonLarge `json:"largeButton"` @@ -214,14 +191,7 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) } } } - - select { - case <-r.Context().Done(): // check if the request was canceled to avoid making unnecessary requests - return - default: - } - - username := h.lookupUsername(r, uid, stryderRes) + username, _ := h.lookupUsername(r, uid, stryderRes) select { case <-r.Context().Done(): // check if the request was canceled to avoid making unnecessary requests @@ -298,99 +268,10 @@ func (h *Handler) handleClientOriginAuth(w http.ResponseWriter, r *http.Request) }) } -// lookupUsername gets the username for uid according to the configured -// UsernameSource, returning an empty string if not found or on error. -func (h *Handler) lookupUsername(r *http.Request, uid uint64, stryderRes []byte) (username string) { - switch h.UsernameSource { - case UsernameSourceNone: - break - case UsernameSourceEAX: - username, _ = h.lookupUsernameEAX(r, uid) - case UsernameSourceStryder: - username, _ = h.lookupUsernameStryder(r, uid, stryderRes) - case UsernameSourceStryderEAX: - username, _ = h.lookupUsernameStryder(r, uid, stryderRes) - if username == "" { - if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok { - username = eaxUsername - hlog.FromRequest(r).Warn(). - Uint64("uid", uid). - Str("eax_username", eaxUsername). - Msgf("failed to get username from stryder, but got it from eax") - } - } - case UsernameSourceStryderEAXDebug: - username, _ = h.lookupUsernameStryder(r, uid, stryderRes) - if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok { - if eaxUsername != username { - hlog.FromRequest(r).Warn(). - Uint64("uid", uid). - Str("stryder_username", username). - Str("eax_username", eaxUsername). - Msgf("got username from stryder and eax, but they don't match; using the stryder one") - } - } else { - hlog.FromRequest(r).Warn(). - Uint64("uid", uid). - Str("stryder_username", username). - Msgf("got username from stryder, but failed to get username from eax") - } - default: - hlog.FromRequest(r).Error(). - Msgf("unknown username source %q", h.UsernameSource) - } - return -} - -// lookupUsernameEAX gets the username for uid from the EAX API, returning an -// empty string if a username does not exist for the uid, and false on error. -func (h *Handler) lookupUsernameEAX(r *http.Request, uid uint64) (username string, ok bool) { - select { - case <-r.Context().Done(): // check if the request was canceled to avoid polluting the metrics - return - default: - } - if h.EAXClient == nil { - hlog.FromRequest(r).Error(). - Str("username_source", "eax"). - Msgf("no eax client available for username lookup") - return - } - eaxStart := time.Now() - if p, err := h.EAXClient.PlayerIDByPD(r.Context(), uid); err == nil { - if p != nil { - username = p.DisplayName - h.m().client_originauth_eax_username_lookup_calls_total.success.Inc() - } else { - hlog.FromRequest(r).Warn(). - Err(err). - Uint64("uid", uid). - Str("username_source", "eax"). - Msgf("no eax username found for uid") - h.m().client_originauth_eax_username_lookup_calls_total.notfound.Inc() - } - ok = true - } else if errors.Is(err, eax.ErrVersionRequired) || errors.Is(err, eax.ErrAutoUpdateBackoff) { - hlog.FromRequest(r).Error(). - Err(err). - Str("username_source", "eax"). - Msgf("eax update check failure") - h.m().client_originauth_eax_username_lookup_calls_total.fail_update_check.Inc() - } else if !errors.Is(err, context.Canceled) { - hlog.FromRequest(r).Error(). - Err(err). - Str("username_source", "eax"). - Msgf("failed to get eax player info") - h.m().client_originauth_eax_username_lookup_calls_total.fail_other_error.Inc() - } - h.m().client_originauth_eax_username_lookup_duration_seconds.UpdateDuration(eaxStart) - return -} - -// lookupUsernameStryder gets the username for uid from the Stryder response, -// returning an empty string if the username is empty, or false if the -// username is not present in the response or the response is invalid. -func (h *Handler) lookupUsernameStryder(r *http.Request, uid uint64, res []byte) (username string, ok bool) { +// lookupUsername gets the username for uid from the Stryder response, returning +// an empty string if the username is empty, or false if the username is not +// present in the response or the response is invalid. +func (h *Handler) lookupUsername(r *http.Request, uid uint64, res []byte) (username string, ok bool) { select { case <-r.Context().Done(): // check if the request was canceled to avoid polluting the metrics return diff --git a/pkg/api/api0/metrics.go b/pkg/api/api0/metrics.go index 5c8c462..161c1da 100644 --- a/pkg/api/api0/metrics.go +++ b/pkg/api/api0/metrics.go @@ -70,15 +70,8 @@ type apiMetrics struct { fail_other_error *metrics.Counter http_method_not_allowed *metrics.Counter } - client_originauth_requests_map *metricsx.GeoCounter2 - client_originauth_stryder_auth_duration_seconds *metrics.Histogram - client_originauth_eax_username_lookup_duration_seconds *metrics.Histogram - client_originauth_eax_username_lookup_calls_total struct { - success *metrics.Counter - notfound *metrics.Counter - fail_update_check *metrics.Counter - fail_other_error *metrics.Counter - } + client_originauth_requests_map *metricsx.GeoCounter2 + client_originauth_stryder_auth_duration_seconds *metrics.Histogram client_originauth_stryder_username_lookup_calls_total struct { success *metrics.Counter notfound *metrics.Counter @@ -263,11 +256,6 @@ func (h *Handler) m() *apiMetrics { mo.client_originauth_requests_total.http_method_not_allowed = mo.set.NewCounter(`atlas_api0_client_originauth_requests_total{result="http_method_not_allowed"}`) mo.client_originauth_requests_map = metricsx.NewGeoCounter2(`atlas_api0_client_originauth_requests_map`) mo.client_originauth_stryder_auth_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_stryder_auth_duration_seconds`) - mo.client_originauth_eax_username_lookup_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_eax_username_lookup_duration_seconds`) - mo.client_originauth_eax_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="success"}`) - mo.client_originauth_eax_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="notfound"}`) - mo.client_originauth_eax_username_lookup_calls_total.fail_update_check = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="fail_update_check"}`) - mo.client_originauth_eax_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_eax_username_lookup_calls_total{result="fail_other_error"}`) mo.client_originauth_stryder_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="success"}`) mo.client_originauth_stryder_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="notfound"}`) mo.client_originauth_stryder_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_stryder_username_lookup_calls_total{result="fail_other_error"}`) diff --git a/pkg/atlas/server.go b/pkg/atlas/server.go index 7ad8fce..7399e8e 100644 --- a/pkg/atlas/server.go +++ b/pkg/atlas/server.go @@ -25,7 +25,6 @@ import ( "github.com/r2northstar/atlas/db/pdatadb" "github.com/r2northstar/atlas/pkg/api/api0" "github.com/r2northstar/atlas/pkg/cloudflare" - "github.com/r2northstar/atlas/pkg/eax" "github.com/r2northstar/atlas/pkg/memstore" "github.com/r2northstar/atlas/pkg/nspkt" "github.com/r2northstar/atlas/pkg/regionmap" @@ -298,16 +297,6 @@ func NewServer(c *Config) (*Server, error) { Add(hlog.RequestIDHandler("rid", "")). Then(http.HandlerFunc(s.serveRest)) - if exc, err := configureEAX(c, s.Logger.With().Str("component", "eax").Logger()); err == nil { - s.API0.EAXClient = exc - } else { - return nil, fmt.Errorf("initialize eax: %w", err) - } - if x, err := configureUsernameSource(c); err == nil { - s.API0.UsernameSource = x - } else { - return nil, fmt.Errorf("initialize username lookup: %w", err) - } if astore, err := configureAccountStorage(c); err == nil { s.API0.AccountStorage = astore } else { @@ -491,28 +480,6 @@ func configureLogging(c *Config) (l zerolog.Logger, reopen func(), err error) { return } -func configureEAX(c *Config, l zerolog.Logger) (*eax.Client, error) { - mgr := &eax.UpdateMgr{ - AutoUpdateBackoff: expbackoff, - AutoUpdateHook: func(ver string, err error) { - if err != nil { - l.Err(err).Str("eax_client_version", ver).Msg("eax update error, using old version") - } else { - l.Info().Str("eax_client_version", ver).Msg("updated eax client version") - } - }, - } - if v := c.EAXUpdateVersion; v != "" { - mgr.SetVersion(v) - } else { - mgr.AutoUpdateInterval = c.EAXUpdateInterval - mgr.AutoUpdateBucket = c.EAXUpdateBucket - } - return &eax.Client{ - UpdateMgr: mgr, - }, nil -} - func expbackoff(_ error, last time.Time, count int) bool { var hmax, hmaxat, hrate float64 = 24, 8, 2.3 // ~5m, ~10m, ~23m, ~52m, ~2h, ~4.6h, ~10.5h, 24h @@ -526,25 +493,6 @@ func expbackoff(_ error, last time.Time, count int) bool { return time.Since(last).Hours() >= next } -func configureUsernameSource(c *Config) (api0.UsernameSource, error) { - switch typ := c.UsernameSource; typ { - case "none": - return api0.UsernameSourceNone, nil - case "eax": - return api0.UsernameSourceEAX, nil - case "stryder": - return api0.UsernameSourceStryder, nil - case "stryder-eax": - return api0.UsernameSourceStryderEAX, nil - case "stryder-eax-debug": - return api0.UsernameSourceStryderEAXDebug, nil - case "": - return api0.UsernameSourceNone, nil - default: - return "", fmt.Errorf("unknown source %q", typ) - } -} - func configureAccountStorage(c *Config) (api0.AccountStorage, error) { switch typ, arg, _ := strings.Cut(c.API0_Storage_Accounts, ":"); typ { case "memory": diff --git a/pkg/eax/eax.go b/pkg/eax/eax.go deleted file mode 100644 index 34f5702..0000000 --- a/pkg/eax/eax.go +++ /dev/null @@ -1,145 +0,0 @@ -// Package eax queries the EA App API. -package eax - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime" - "net/http" - "net/url" - "strconv" -) - -type Client struct { - // The [net/http.Client] to use for requests. If not provided, - // [net/http.DefaultClient] is used. - Client *http.Client - - // The UpdateMgr for requests which require version information. - UpdateMgr *UpdateMgr -} - -var ErrVersionRequired = errors.New("client version is required for this endpoint") - -// PlayerID contains basic identifiers and names for a player. -type PlayerID struct { - PD uint64 // origin ID - PSD uint64 // ? - DisplayName string // in-game name - Nickname string // social name? -} - -// PlayerByPd gets basic information about an Origin ID. If the player does not -// exist, nil will be returned. -func (c *Client) PlayerIDByPD(ctx context.Context, pd uint64) (*PlayerID, error) { - var obj struct { - PlayerByPD *struct { - PD string `json:"pd"` - PSD string `json:"psd"` - DisplayName string `json:"displayName"` - Nickname string `json:"nickname"` - } `json:"playerByPd"` - } - if err := c.gql1(ctx, true, `query { playerByPd (pd: `+strconv.FormatUint(pd, 10)+`) { pd psd displayName nickname } }`, &obj); err != nil { - return nil, err - } - if obj.PlayerByPD == nil { - return nil, nil - } - res := &PlayerID{ - DisplayName: obj.PlayerByPD.DisplayName, - Nickname: obj.PlayerByPD.Nickname, - } - if s := obj.PlayerByPD.PD; s != "" { - if v, err := strconv.ParseUint(s, 10, 64); err == nil { - res.PD = v - } else { - return res, fmt.Errorf("parse pd %q: %w", s, err) - } - } - if s := obj.PlayerByPD.PSD; s != "" { - if v, err := strconv.ParseUint(s, 10, 64); err == nil { - res.PD = v - } else { - return res, fmt.Errorf("parse psd %q: %w", s, err) - } - } - return res, nil -} - -// gql1 performs a basic GraphQL query. -func (c *Client) gql1(ctx context.Context, ver bool, query string, out any) error { - req, err := c.req(ctx, ver, http.MethodGet, "https://service-aggregation-layer.juno.ea.com/graphql?query="+url.QueryEscape(query), nil) - if err != nil { - return err - } - - resp, err := c.do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - if mt, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); mt != "application/json" { - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s) with content-type %q", resp.StatusCode, resp.Status, mt) - } - return fmt.Errorf("unexpected content-type %q", mt) - } - - buf, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - var obj struct { - Data json.RawMessage `json:"data"` - Errors []struct { - Message string `json:"message"` - } `json:"errors"` - } - if err := json.Unmarshal(buf, &obj); err != nil { - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s) with invalid json", resp.StatusCode, resp.Status) - } - return fmt.Errorf("invalid json resp: %w", err) - } - if len(obj.Errors) != 0 { - return fmt.Errorf("got %d errors, including %q", len(obj.Errors), obj.Errors[0].Message) - } - if err := json.Unmarshal([]byte(obj.Data), out); err != nil { - return fmt.Errorf("invalid json data: %w", err) - } - return nil -} - -func (c *Client) do(r *http.Request) (*http.Response, error) { - if c.Client == nil { - return http.DefaultClient.Do(r) - } - return c.Client.Do(r) -} - -func (c *Client) req(ctx context.Context, ver bool, method, url string, body io.Reader) (*http.Request, error) { - req, err := http.NewRequestWithContext(ctx, method, url, body) - if err == nil { - if ver { - if c.UpdateMgr == nil { - return nil, ErrVersionRequired - } - ver, _, err := c.UpdateMgr.Update(false) - if err != nil { - return nil, fmt.Errorf("%w: failed to update version: %v", ErrVersionRequired, err) - } - if ver == "" { - return nil, ErrVersionRequired - } - req.Header.Set("User-Agent", "EADesktop/"+ver) - } - req.Header.Set("x-client-id", "EAX-JUNO-CLIENT") - } - return req, err -} diff --git a/pkg/eax/updatemgr.go b/pkg/eax/updatemgr.go deleted file mode 100644 index d1a6a59..0000000 --- a/pkg/eax/updatemgr.go +++ /dev/null @@ -1,180 +0,0 @@ -package eax - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" - "sync" - "time" -) - -// UpdateMgr manages EAX client version information. -type UpdateMgr struct { - // HTTP client to use. If not provided, [net/http.DefaultClient] will be - // used. - Client *http.Client - - // Timeout is the timeout for refreshing tokens. If zero, a reasonable - // default is used. If negative, there is no timeout. - Timeout time.Duration - - // Interval to update at. If zero, will not auto-update. - AutoUpdateInterval time.Duration - - // Auto-update staged roll-out bucket. - AutoUpdateBucket int - - // Auto-update backoff, if provided, checks if another auto-update is - // allowed after a failure. If it returns false, ErrAutoUpdateBackoff will be - // returned from the function triggering the auto-update. - AutoUpdateBackoff func(err error, time time.Time, count int) bool - - // AutoUpdateHook is called for every auto-update attempt with the new (or - // current if error) version, and any error which occurred. - AutoUpdateHook func(v string, err error) - - verInit sync.Once - verPf bool // ensures only one auto-update runs at a time - verCv *sync.Cond // allows other goroutines to wait for that update to complete - verErr error // last auto-update error - verErrTime time.Time // last auto-update error time - verErrCount int // consecutive auto-update errors - ver string // current version - verTime time.Time // last update check time -} - -var ErrAutoUpdateBackoff = errors.New("not updating eax client version due to backoff") - -func (u *UpdateMgr) init() { - u.verInit.Do(func() { - u.verCv = sync.NewCond(new(sync.Mutex)) - }) -} - -// SetVersion sets the current version. -func (u *UpdateMgr) SetVersion(v string) { - u.init() - u.verCv.L.Lock() - for u.verPf { - u.verCv.Wait() - } - u.ver = v - u.verErr = nil - u.verTime = time.Now() - u.verCv.L.Unlock() -} - -// Update gets the latest version, following u.AutoUpdateInterval if provided, -// unless the version is not set or force is true. If another update is in -// progress, it waits for the result of it. True is returned (on success or -// failure) if this call performed a update. This function may block for up to -// Timeout. -func (u *UpdateMgr) Update(force bool) (string, bool, error) { - u.init() - if u.verCv.L.Lock(); u.verPf { - for u.verPf { - u.verCv.Wait() - } - defer u.verCv.L.Unlock() - return u.ver, false, u.verErr - } else { - if force || u.ver == "" || (u.AutoUpdateInterval != 0 && time.Since(u.verTime) > u.AutoUpdateInterval) { - u.verPf = true - u.verCv.L.Unlock() - defer func() { - u.verCv.L.Lock() - u.verCv.Broadcast() - u.verPf = false - u.verCv.L.Unlock() - }() - } else { - defer u.verCv.L.Unlock() - return u.ver, false, u.verErr - } - } - if u.verErr != nil && u.AutoUpdateBackoff != nil { - if !u.AutoUpdateBackoff(u.verErr, u.verErrTime, u.verErrCount) { - return u.ver, true, fmt.Errorf("%w (%d attempts, last error: %v)", ErrAutoUpdateBackoff, u.verErrCount, u.verErr) - } - } - u.verErr = func() error { - var ctx context.Context - var cancel context.CancelFunc - if u.Timeout > 0 { - ctx, cancel = context.WithTimeout(context.Background(), u.Timeout) - } else if u.Timeout == 0 { - ctx, cancel = context.WithTimeout(context.Background(), time.Second*15) - } else { - ctx, cancel = context.WithCancel(context.Background()) - } - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://autopatch.juno.ea.com/autopatch/upgrade/buckets/"+strconv.Itoa(u.AutoUpdateBucket), nil) - if err != nil { - return err - } - if u.ver != "" { - req.Header.Set("User-Agent", "EADesktop/"+u.ver) - } else { - req.Header.Set("User-Agent", "") - } - - cl := u.Client - if cl == nil { - cl = http.DefaultClient - } - - resp, err := cl.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - var obj struct { - Minimum struct { - Version string `json:"version"` - ActivationDate string `json:"activationDate"` - } `json:"minimum"` - Recommended struct { - Version string `json:"version"` - } `json:"recommended"` - } - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("response status %d (%s)", resp.StatusCode, resp.Status) - } - if err := json.NewDecoder(resp.Body).Decode(&obj); err != nil { - return fmt.Errorf("parse autopatch response: %w", err) - } - - var version string - if v := obj.Minimum.Version; v != "" { - version = v - } - if v := obj.Recommended.Version; v != "" { - version = v - } - if version == "" { - return fmt.Errorf("parse autopatch response: missing minimum or recommended version") - } - u.ver = version - u.verTime = time.Now() - return nil - }() - if u.verErrCount != 0 { - u.verErr = fmt.Errorf("%w (attempt %d)", u.verErr, u.verErrCount) - } - if u.verErr != nil { - u.verErrCount++ - u.verErrTime = time.Now() - } else { - u.verErrCount = 0 - u.verErrTime = time.Time{} - } - if u.AutoUpdateHook != nil { - go u.AutoUpdateHook(u.ver, u.verErr) - } - return u.ver, true, u.verErr -} |