aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/PRODUCTION.md13
-rw-r--r--pkg/api/api0/api.go5
-rw-r--r--pkg/api/api0/client.go121
-rw-r--r--pkg/api/api0/metrics.go16
-rw-r--r--pkg/atlas/config.go35
-rw-r--r--pkg/atlas/server.go138
-rw-r--r--pkg/juno/login.go716
-rw-r--r--pkg/origin/authmgr.go193
-rw-r--r--pkg/origin/login.go90
-rw-r--r--pkg/origin/origin.go131
-rw-r--r--pkg/origin/origin_test.go99
11 files changed, 7 insertions, 1550 deletions
diff --git a/docs/PRODUCTION.md b/docs/PRODUCTION.md
index 81e1b06..f10e6ac 100644
--- a/docs/PRODUCTION.md
+++ b/docs/PRODUCTION.md
@@ -161,16 +161,7 @@ This document describes the recommended setup for an non-containerized Atlas ser
ATLAS_API0_MAINMENUPROMOS_UPDATENEEDED=file:/etc/atlas/mainmenupromos_updateneeded.json
# username lookup
- ATLAS_USERNAMESOURCE=eax-origin
-
- # origin (the account MUST have app-based two-factor authentication set up)
- ATLAS_ORIGIN_EMAIL=email@example.com
- ATLAS_ORIGIN_PASSWORD=@origin_password
- ATLAS_ORIGIN_TOTP=@origin_totp
- ATLAS_ORIGIN_HAR_GZIP=true
- ATLAS_ORIGIN_HAR_SUCCESS=/var/log/atlas/har
- ATLAS_ORIGIN_HAR_ERROR=/var/log/atlas/har
- ATLAS_ORIGIN_PERSIST=/var/lib/atlas/origin.json
+ ATLAS_USERNAMESOURCE=stryder
# eax
EAX_UPDATE_INTERVAL=24h
@@ -291,7 +282,7 @@ This document describes the recommended setup for an non-containerized Atlas ser
echo "[Service]" | tee /etc/systemd/system/atlas.service.d/credentials.conf
```
- For each credential (in the example config: `northstartf.key`, `northstartf.crt`, `api0_server_id_secret`, `metrics_secret`, `origin_password`, `origin_totp`),
+ For each credential (in the example config: `northstartf.key`, `northstartf.crt`, `api0_server_id_secret`, `metrics_secret`),
```bash
sudo systemd-creds --pretty encrypt --name credential_name - - | tee -a /etc/systemd/system/atlas.service.d/credentials.conf
diff --git a/pkg/api/api0/api.go b/pkg/api/api0/api.go
index 2e3593c..355deef 100644
--- a/pkg/api/api0/api.go
+++ b/pkg/api/api0/api.go
@@ -30,7 +30,6 @@ import (
"github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/metricsx"
"github.com/r2northstar/atlas/pkg/nspkt"
- "github.com/r2northstar/atlas/pkg/origin"
"github.com/rs/zerolog/hlog"
"golang.org/x/mod/semver"
)
@@ -52,10 +51,6 @@ type Handler struct {
// UsernameSource configures the source to use for usernames.
UsernameSource UsernameSource
- // OriginAuthMgr, if provided, manages Origin nucleus tokens for checking
- // usernames.
- OriginAuthMgr *origin.AuthMgr
-
// EAXClient makes requests to the EAX API.
EAXClient *eax.Client
diff --git a/pkg/api/api0/client.go b/pkg/api/api0/client.go
index b3fd686..b718e17 100644
--- a/pkg/api/api0/client.go
+++ b/pkg/api/api0/client.go
@@ -13,7 +13,6 @@ import (
"github.com/r2northstar/atlas/pkg/api/api0/api0gameserver"
"github.com/r2northstar/atlas/pkg/eax"
- "github.com/r2northstar/atlas/pkg/origin"
"github.com/r2northstar/atlas/pkg/pdata"
"github.com/r2northstar/atlas/pkg/stryder"
"github.com/rs/zerolog/hlog"
@@ -26,22 +25,9 @@ const (
// Don't get usernames.
UsernameSourceNone UsernameSource = ""
- // Get the username from the Origin API.
- UsernameSourceOrigin UsernameSource = "origin"
-
- // Get the username from the Origin API, but fall back to EAX on failure.
- UsernameSourceOriginEAX UsernameSource = "origin-eax"
-
// Get the username from EAX.
UsernameSourceEAX UsernameSource = "eax"
- // Get the username from the Origin API, but also check EAX and warn if it's
- // different.
- UsernameSourceOriginEAXDebug UsernameSource = "origin-eax-debug"
-
- // Get the username from EAX, but fall back to the Origin API on failure.
- UsernameSourceEAXOrigin UsernameSource = "eax-origin"
-
// Get the username from Stryder (available since October 2, 2023). Note
// that this source only returns usernames for valid tokens.
UsernameSourceStryder UsernameSource = "stryder"
@@ -318,48 +304,8 @@ func (h *Handler) lookupUsername(r *http.Request, uid uint64, stryderRes []byte)
switch h.UsernameSource {
case UsernameSourceNone:
break
- case UsernameSourceOrigin:
- username, _ = h.lookupUsernameOrigin(r, uid)
- case UsernameSourceOriginEAX:
- username, _ = h.lookupUsernameOrigin(r, uid)
- if username == "" {
- if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok {
- username = eaxUsername
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("origin_username", eaxUsername).
- Msgf("failed to get username from origin, but got it from eax")
- }
- }
- case UsernameSourceOriginEAXDebug:
- username, _ = h.lookupUsernameOrigin(r, uid)
- if eaxUsername, ok := h.lookupUsernameEAX(r, uid); ok {
- if eaxUsername != username {
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("origin_username", username).
- Str("eax_username", eaxUsername).
- Msgf("got username from origin and eax, but they don't match; using the origin one")
- }
- } else {
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("origin_username", username).
- Msgf("got username from origin, but failed to get username from eax")
- }
case UsernameSourceEAX:
username, _ = h.lookupUsernameEAX(r, uid)
- case UsernameSourceEAXOrigin:
- username, _ = h.lookupUsernameEAX(r, uid)
- if username == "" {
- if originUsername, ok := h.lookupUsernameOrigin(r, uid); ok {
- username = originUsername
- hlog.FromRequest(r).Warn().
- Uint64("uid", uid).
- Str("origin_username", originUsername).
- Msgf("failed to get username from eax, but got it from origin")
- }
- }
case UsernameSourceStryder:
username, _ = h.lookupUsernameStryder(r, uid, stryderRes)
case UsernameSourceStryderEAX:
@@ -396,73 +342,6 @@ func (h *Handler) lookupUsername(r *http.Request, uid uint64, stryderRes []byte)
return
}
-// lookupUsernameOrigin gets the username for uid from the Origin API, returning
-// an empty string if a username does not exist for the uid, and false on error.
-func (h *Handler) lookupUsernameOrigin(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.OriginAuthMgr == nil {
- hlog.FromRequest(r).Error().
- Str("username_source", "origin").
- Msgf("no origin auth available for username lookup")
- return
- }
- originStart := time.Now()
- if tok, ours, err := h.OriginAuthMgr.OriginAuth(false); err == nil {
- if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil {
- if len(ui) == 1 {
- username = ui[0].EAID
- h.m().client_originauth_origin_username_lookup_calls_total.success.Inc()
- } else {
- h.m().client_originauth_origin_username_lookup_calls_total.notfound.Inc()
- }
- ok = true
- } else if errors.Is(err, origin.ErrAuthRequired) {
- if tok, ours, err := h.OriginAuthMgr.OriginAuth(true); err == nil {
- if ui, err := origin.GetUserInfo(r.Context(), tok, uid); err == nil {
- if len(ui) == 1 {
- username = ui[0].EAID
- h.m().client_originauth_origin_username_lookup_calls_total.success.Inc()
- } else {
- h.m().client_originauth_origin_username_lookup_calls_total.notfound.Inc()
- }
- ok = true
- }
- } else if ours {
- hlog.FromRequest(r).Error().
- Err(err).
- Str("username_source", "origin").
- Msgf("origin auth token refresh failure")
- h.m().client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh.Inc()
- }
- } else if !errors.Is(err, context.Canceled) {
- hlog.FromRequest(r).Error().
- Err(err).
- Str("username_source", "origin").
- Msgf("failed to get origin user info")
- h.m().client_originauth_origin_username_lookup_calls_total.fail_other_error.Inc()
- }
- if username == "" && ok {
- hlog.FromRequest(r).Warn().
- Err(err).
- Uint64("uid", uid).
- Str("username_source", "origin").
- Msgf("no origin username found for uid")
- }
- } else if ours {
- hlog.FromRequest(r).Error().
- Err(err).
- Str("username_source", "origin").
- Msgf("origin auth token refresh failure")
- h.m().client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh.Inc()
- }
- h.m().client_originauth_origin_username_lookup_duration_seconds.UpdateDuration(originStart)
- 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) {
diff --git a/pkg/api/api0/metrics.go b/pkg/api/api0/metrics.go
index 0c18d0a..5c8c462 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_origin_username_lookup_duration_seconds *metrics.Histogram
- client_originauth_origin_username_lookup_calls_total struct {
- success *metrics.Counter
- notfound *metrics.Counter
- fail_authtok_refresh *metrics.Counter
- fail_other_error *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
@@ -270,11 +263,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_origin_username_lookup_duration_seconds = mo.set.NewHistogram(`atlas_api0_client_originauth_origin_username_lookup_duration_seconds`)
- mo.client_originauth_origin_username_lookup_calls_total.success = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="success"}`)
- mo.client_originauth_origin_username_lookup_calls_total.notfound = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="notfound"}`)
- mo.client_originauth_origin_username_lookup_calls_total.fail_authtok_refresh = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="fail_authtok_refresh"}`)
- mo.client_originauth_origin_username_lookup_calls_total.fail_other_error = mo.set.NewCounter(`atlas_api0_client_originauth_origin_username_lookup_calls_total{result="fail_other_error"}`)
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"}`)
diff --git a/pkg/atlas/config.go b/pkg/atlas/config.go
index ed108c1..07c88ac 100644
--- a/pkg/atlas/config.go
+++ b/pkg/atlas/config.go
@@ -168,41 +168,12 @@ type Config struct {
// Sets the source used for resolving usernames. If not specified, "origin"
// is used if OriginEmail is provided, otherwise, "none" is used.
// - none (don't get usernames)
- // - origin (get the username from the Origin API)
- // - origin-eax (get the username from the Origin API, but fall back to EAX on failure)
- // - origin-eax-debug (get the username from the Origin API, but also check EAX and warn if it's different)
// - eax (get the username from EAX)
- // - eax-origin (get the username from EAX, but fall back to the Origin API on failure)
+ // - stryder (get the username from Stryder)
+ // - stryder-eax (get the username from Stryder, but fall back to EAX on failure)
+ // - stryder-eax-debug (get the username from Stryder, but also check EAX and warn if it's different)
UsernameSource string `env:"ATLAS_USERNAMESOURCE"`
- // The email address to use for Origin login. If not provided, the Origin
- // API will not be used. If it begins with @, it is treated as the name of a
- // systemd credential to load.
- OriginEmail string `env:"ATLAS_ORIGIN_EMAIL" sdcreds:"load,trimspace"`
-
- // The password for Origin login. If it begins with @, it is treated as the
- // name of a systemd credential to load.
- OriginPassword string `env:"ATLAS_ORIGIN_PASSWORD" sdcreds:"load,trimspace"`
-
- // The base32 TOTP secret for Origin login. If it begins with @, it is
- // treated as the name of a systemd credential to load.
- OriginTOTP string `env:"ATLAS_ORIGIN_TOTP" sdcreds:"load,trimspace"`
-
- // OriginHARGzip controls whether to compress saved HAR archives.
- OriginHARGzip bool `env:"ATLAS_ORIGIN_HAR_GZIP"`
-
- // OriginHARSuccess is the path to a directory to save HAR archives of
- // successful Origin auth attempts.
- OriginHARSuccess string `env:"ATLAS_ORIGIN_HAR_SUCCESS"`
-
- // OriginHARError is the path to a directory to save HAR archives of
- // successful Origin auth attempts.
- OriginHARError string `env:"ATLAS_ORIGIN_HAR_ERROR"`
-
- // The JSON file to save Origin login info to so tokens are preserved across
- // restarts. Highly recommended.
- OriginPersist string `env:"ATLAS_ORIGIN_PERSIST"`
-
// Override the EAX EA App version. If specified, updates will not be
// checked automatically.
EAXUpdateVersion string `env:"EAX_UPDATE_VERSION"`
diff --git a/pkg/atlas/server.go b/pkg/atlas/server.go
index 4b450c2..7ad8fce 100644
--- a/pkg/atlas/server.go
+++ b/pkg/atlas/server.go
@@ -20,7 +20,6 @@ import (
"time"
"github.com/VictoriaMetrics/metrics"
- "github.com/klauspost/compress/gzip"
"github.com/pg9182/ip2x"
"github.com/r2northstar/atlas/db/atlasdb"
"github.com/r2northstar/atlas/db/pdatadb"
@@ -29,7 +28,6 @@ import (
"github.com/r2northstar/atlas/pkg/eax"
"github.com/r2northstar/atlas/pkg/memstore"
"github.com/r2northstar/atlas/pkg/nspkt"
- "github.com/r2northstar/atlas/pkg/origin"
"github.com/r2northstar/atlas/pkg/regionmap"
"github.com/rs/zerolog"
"github.com/rs/zerolog/hlog"
@@ -300,11 +298,6 @@ func NewServer(c *Config) (*Server, error) {
Add(hlog.RequestIDHandler("rid", "")).
Then(http.HandlerFunc(s.serveRest))
- if org, err := configureOrigin(c, s.Logger.With().Str("component", "origin").Logger()); err == nil {
- s.API0.OriginAuthMgr = org
- } else {
- return nil, fmt.Errorf("initialize origin auth: %w", err)
- }
if exc, err := configureEAX(c, s.Logger.With().Str("component", "eax").Logger()); err == nil {
s.API0.EAXClient = exc
} else {
@@ -498,125 +491,6 @@ func configureLogging(c *Config) (l zerolog.Logger, reopen func(), err error) {
return
}
-func configureOrigin(c *Config, l zerolog.Logger) (*origin.AuthMgr, error) {
- if c.OriginEmail == "" {
- return nil, nil
- }
- var mu sync.Mutex
- mgr := &origin.AuthMgr{
- Credentials: func() (email, password, otpsecret string, err error) {
- return c.OriginEmail, c.OriginPassword, c.OriginTOTP, nil
- },
- Backoff: expbackoff,
- Updated: func(as origin.AuthState, err error) {
- mu.Lock()
- defer mu.Unlock()
-
- if fn := c.OriginPersist; fn != "" {
- if buf, err := json.Marshal(as); err != nil {
- l.Err(err).Msg("failed to save origin auth json")
- return
- } else if err = os.WriteFile(fn, buf, 0666); err != nil {
- l.Err(err).Msg("failed to save origin auth json")
- return
- }
- }
- if err != nil {
- l.Err(err).Msg("origin auth error; using old token")
- } else {
- l.Info().Msg("got new origin token")
- }
- },
- }
- if fn := c.OriginPersist; fn != "" {
- var as origin.AuthState
- if buf, err := os.ReadFile(fn); err != nil {
- if !os.IsNotExist(err) {
- l.Err(err).Msg("failed to load origin auth json")
- }
- } else if err := json.Unmarshal(buf, &as); err != nil {
- l.Err(err).Msg("failed to load origin auth json")
- } else {
- mgr.SetAuth(as)
- }
- }
- if c.OriginHARError != "" || c.OriginHARSuccess != "" {
- var errPath, successPath string
- if v := c.OriginHARError; v != "" {
- if p, err := filepath.Abs(v); err != nil {
- return nil, fmt.Errorf("resolve error har path: %w", err)
- } else if err := os.MkdirAll(v, 0777); err != nil {
- return nil, fmt.Errorf("mkdir error har path: %w", err)
- } else {
- errPath = p
- }
- }
- if v := c.OriginHARSuccess; v != "" {
- if p, err := filepath.Abs(v); err != nil {
- return nil, fmt.Errorf("resolve success har path: %w", err)
- } else if err := os.MkdirAll(v, 0777); err != nil {
- return nil, fmt.Errorf("mkdir success har path: %w", err)
- } else {
- successPath = p
- }
- }
- var harMu sync.Mutex
- harZ := gzip.NewWriter(io.Discard)
- mgr.SaveHAR = func(write func(w io.Writer) error, err error) {
- harMu.Lock()
- defer harMu.Unlock()
-
- var p string
- if err != nil {
- if errPath != "" {
- p = filepath.Join(errPath, "origin-auth-error-")
- }
- } else {
- if successPath != "" {
- p = filepath.Join(successPath, "origin-auth-success-")
- }
- }
- if p != "" {
- p = p + strconv.FormatInt(time.Now().Unix(), 10) + ".har"
-
- if c.OriginHARGzip {
- p += ".gz"
- }
-
- f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0600)
- if err != nil {
- l.Err(err).Msg("failed to save origin auth har")
- return
- }
- defer f.Close()
-
- if c.OriginHARGzip {
- harZ.Reset(f)
- if err := write(harZ); err != nil {
- l.Err(err).Msg("failed to save origin auth har")
- return
- }
- if err := harZ.Close(); err != nil {
- l.Err(err).Msg("failed to save origin auth har")
- return
- }
- } else {
- if err := write(f); err != nil {
- l.Err(err).Msg("failed to save origin auth har")
- return
- }
- }
-
- if err := f.Close(); err != nil {
- l.Err(err).Msg("failed to save origin auth har")
- return
- }
- }
- }
- }
- return mgr, nil
-}
-
func configureEAX(c *Config, l zerolog.Logger) (*eax.Client, error) {
mgr := &eax.UpdateMgr{
AutoUpdateBackoff: expbackoff,
@@ -656,16 +530,8 @@ func configureUsernameSource(c *Config) (api0.UsernameSource, error) {
switch typ := c.UsernameSource; typ {
case "none":
return api0.UsernameSourceNone, nil
- case "origin":
- return api0.UsernameSourceOrigin, nil
- case "origin-eax":
- return api0.UsernameSourceOriginEAX, nil
- case "origin-eax-debug":
- return api0.UsernameSourceOriginEAXDebug, nil
case "eax":
return api0.UsernameSourceEAX, nil
- case "eax-origin":
- return api0.UsernameSourceEAXOrigin, nil
case "stryder":
return api0.UsernameSourceStryder, nil
case "stryder-eax":
@@ -673,10 +539,6 @@ func configureUsernameSource(c *Config) (api0.UsernameSource, error) {
case "stryder-eax-debug":
return api0.UsernameSourceStryderEAXDebug, nil
case "":
- // backwards compat
- if c.OriginEmail != "" {
- return api0.UsernameSourceOrigin, nil
- }
return api0.UsernameSourceNone, nil
default:
return "", fmt.Errorf("unknown source %q", typ)
diff --git a/pkg/juno/login.go b/pkg/juno/login.go
deleted file mode 100644
index 63784d3..0000000
--- a/pkg/juno/login.go
+++ /dev/null
@@ -1,716 +0,0 @@
-// Package juno implements a client for the EA juno login flow.
-package juno
-
-import (
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha1"
- "encoding/base32"
- "encoding/binary"
- "errors"
- "fmt"
- "hash"
- "io"
- "math"
- "math/rand"
- "net/http"
- "net/http/cookiejar"
- "net/url"
- "regexp"
- "strings"
- "time"
-
- "github.com/andybalholm/cascadia"
- "golang.org/x/net/html"
- "golang.org/x/net/html/atom"
-)
-
-var (
- ErrCaptchaRequired = errors.New("captcha required")
- ErrInvalidTwoFactor = errors.New("invalid two factor code")
-
- ErrJuno = junoLoginError{}
- ErrOnlineLoginNotAvailable = junoLoginError{Code: "10001"}
- ErrInvalidCredentials = junoLoginError{Code: "10002"} // note: triggering this too many times will result in a captcha
- ErrJunoInternalError = junoLoginError{Code: "10003"}
-)
-
-// AuthResult contains authentication tokens.
-type AuthResult struct {
- Code string
- SID SID
-}
-
-// SID is a persistent EA login session ID.
-type SID string
-
-// AddTo adds the SID cookie to j.
-func (s SID) AddTo(j http.CookieJar) {
- j.SetCookies(&url.URL{
- Scheme: "https",
- Host: "accounts.ea.com",
- Path: "/connect",
- }, []*http.Cookie{{
- Name: "sid",
- Value: string(s),
- Secure: true,
- }})
-}
-
-// Login gets a SID using the provided credentials.
-func Login(ctx context.Context, rt http.RoundTripper, email, password, otpsecret string) (AuthResult, error) {
- if rt == nil {
- rt = http.DefaultClient.Transport
- }
-
- s := &junoLoginState{
- Email: email,
- Password: password,
- }
-
- if otpsecret != "" {
- b, err := base32.StdEncoding.DecodeString(strings.ToUpper(strings.ReplaceAll(otpsecret, " ", "")))
- if err != nil {
- return AuthResult{}, fmt.Errorf("parse totp secret: %w", err)
- }
- s.TOTP = func(t time.Time) string {
- return hotp(totp(t, 0), b, 0, nil)
- }
- }
-
- j, _ := cookiejar.New(nil)
- c := &http.Client{
- Transport: rt,
- Jar: j,
- }
-
- for _, host := range []string{"www.ea.com", "accounts.ea.com", "signin.ea.com"} {
- c.Jar.SetCookies(&url.URL{
- Scheme: "https",
- Host: host,
- }, []*http.Cookie{
- {Name: "ealocale", Value: "en-us"},
- {Name: "notice_behavior", Value: "implied,us"},
- {Name: "notice_location", Value: "us"},
- })
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.ea.com/login", nil)
- if err != nil {
- return AuthResult{}, err
- }
- req.Header.Set("Referrer", "https://www.ea.com/en-us/")
-
- var reqs []string
- for {
- req.Header.Set("Accept-Language", "en-US;q=0.7,en;q=0.3")
- req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36")
-
- if reqs = append(reqs, req.Method+" "+req.URL.String()); len(reqs) > 10 {
- return AuthResult{}, fmt.Errorf("too many requests (%q)", reqs)
- }
-
- resp, err := c.Do(req)
- if err != nil {
- return AuthResult{}, fmt.Errorf("do %s %q: %w", req.Method, req.URL.String(), err)
- }
- defer resp.Body.Close()
-
- if resp.StatusCode != http.StatusOK {
- return AuthResult{}, fmt.Errorf("do %s %q: response status %d (%s)", req.Method, req.URL.String(), resp.StatusCode, resp.Status)
- }
-
- buf, err := io.ReadAll(resp.Body)
- if err != nil {
- return AuthResult{}, fmt.Errorf("do %s %q: read response: %w", req.Method, req.URL.String(), err)
- }
-
- var via []string
- for last := resp.Request; last != nil; {
- if via = append(via, last.URL.String()); last.Response != nil {
- last = last.Response.Request
- } else {
- last = nil
- }
- }
-
- switch {
- case asciiEqualFold(resp.Request.URL.Hostname(), "www.ea.com"):
- for last := resp.Request; last != nil; last = last.Response.Request {
- if code := last.URL.Query().Get("code"); code != "" {
- for _, ck := range c.Jar.Cookies(&url.URL{
- Scheme: "https",
- Host: "accounts.ea.com",
- Path: "/connect",
- }) {
- if ck.Name == "sid" {
- return AuthResult{
- Code: code,
- SID: SID(ck.Value),
- }, nil
- }
- }
- return AuthResult{}, fmt.Errorf("missing sid cookie")
- }
- }
- return AuthResult{}, fmt.Errorf("do %s %q: unhandled response url (%q): back to homepage, but could not find auth code", req.Method, req.URL.String(), via)
-
- case asciiEqualFold(resp.Request.URL.Hostname(), "signin.ea.com"):
- if !strings.HasPrefix(resp.Request.URL.Path, "/p/juno/") {
- return AuthResult{}, fmt.Errorf("do %s %q: unhandled response url (%q): not juno", req.Method, req.URL.String(), via)
- }
-
- default:
- return AuthResult{}, fmt.Errorf("do %s %q: unhandled response url (%q)", req.Method, req.URL.String(), via)
- }
-
- doc, err := html.ParseWithOptions(bytes.NewReader(buf), html.ParseOptionEnableScripting(true))
- if err != nil {
- return AuthResult{}, fmt.Errorf("do %s %q: parse document: %w", req.Method, req.URL.String(), err)
- }
- resp.Body.Close()
-
- req, err = s.junoLoginStep(resp.Request.URL, doc)
- if err != nil {
- return AuthResult{}, err
- }
- req = req.WithContext(ctx)
- }
-}
-
-type junoLoginState struct {
- Email string
- Password string
- TOTP func(time.Time) string
-
- seenLogin bool
- seenTOS bool
- seenTwoFactor bool
- seenTwoFactorCode bool
- seenEnd bool
-}
-
-type junoLoginError struct {
- Code string
- Desc string
-}
-
-func (err junoLoginError) Error() string {
- var codeDesc string
- switch err.Code {
- case "10001":
- codeDesc = ": online login not available"
- case "10002":
- codeDesc = ": invalid credentials"
- case "10003":
- codeDesc = ": internal error"
- case "10004":
- codeDesc = ": wtf" // idk what this is
- case "":
- return fmt.Sprintf("juno error (%q)", err.Desc)
- }
- if err.Desc == "" {
- return fmt.Sprintf("juno error %s%s (%q)", err.Code, codeDesc, err.Desc)
- }
- return fmt.Sprintf("juno error %s%s (%q)", err.Code, codeDesc, err.Desc)
-}
-
-func (err junoLoginError) Is(other error) bool {
- if v, ok := other.(junoLoginError); ok {
- return err.Code == "" || v.Code == err.Code
- }
- return false
-}
-
-func (s *junoLoginState) junoLoginStep(u *url.URL, doc *html.Node) (*http.Request, error) {
- if n := qs(doc, "form#login-form"); n != nil {
- r, err := s.junoLoginStepLogin(u, doc, n)
- if err != nil {
- err = fmt.Errorf("handle login: %w", err)
- }
- return r, err
- }
- if n := qs(doc, "form#loginForm:has(#tfa-login)"); n != nil {
- r, err := s.junoStepTwoFactor(u, doc, n)
- if err != nil {
- err = fmt.Errorf("handle two factor auth: %w", err)
- }
- return r, err
- }
- if n := qs(doc, "form#tosForm"); n != nil {
- r, err := s.junoStepTOSUpdate(u, doc, n)
- if err != nil {
- err = fmt.Errorf("handle tos update: %w", err)
- }
- return r, err
- }
- if n := qs(doc, "#login-container-end"); n != nil {
- r, err := s.junoStepEnd(u, doc)
- if err != nil {
- err = fmt.Errorf("handle login end: %w", err)
- }
- return r, err
- }
- var fs []string
- for _, f := range qsa(doc, "form") {
- var (
- id, _ = htmlAttr(f, "id", "")
- name, _ = htmlAttr(f, "name", "")
- )
- fs = append(fs, fmt.Sprintf("form[id=%s][name=%s]", id, name))
- }
- return nil, fmt.Errorf("handle login step (url: %s): unhandled step (forms: %s)", u.String(), strings.Join(fs, ", "))
-}
-
-func (s *junoLoginState) junoLoginStepLogin(u *url.URL, doc, form *html.Node) (*http.Request, error) {
- var (
- errorCode, _ = htmlAttr(qs(form, "#errorCode[value]"), "value", "")
- errorDesc = htmlText(qs(form, "#online-general-error > p"))
- )
- if errorCode != "" || errorDesc != "" {
- return nil, junoLoginError{Code: errorCode, Desc: errorDesc}
- }
- if qs(doc, "#g-recaptcha-response") != nil {
- return nil, fmt.Errorf("%w (recapcha)", ErrCaptchaRequired)
- }
- if qs(doc, "#funcaptcha-solved") != nil {
- return nil, fmt.Errorf("%w (funcaptcha)", ErrCaptchaRequired)
- }
- if s.seenLogin {
- return nil, fmt.Errorf("already seen (and could not find an error)")
- } else {
- s.seenLogin = true
- }
- return junoFillForm(u, form, junoFormData{
- Fill: func(name, defvalue string) (string, error) {
- switch name {
- case "loginMethod":
- return "emailPassword", nil
- case "cid":
- const charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz"
- b := make([]byte, 32)
- for i := range b {
- b[i] = charset[rand.Intn(len(charset))]
- }
- return string(b), nil
- case "email":
- if s.Email == "" {
- return "", fmt.Errorf("%w: no email provided", ErrInvalidCredentials)
- }
- return s.Email, nil
- case "password":
- if s.Password == "" {
- return "", fmt.Errorf("%w: no email provided", ErrInvalidCredentials)
- }
- return s.Password, nil
- default:
- return defvalue, nil
- }
- },
- Expect: map[string]bool{
- "email": true,
- "password": true,
- },
- })
-}
-
-func (s *junoLoginState) junoStepTOSUpdate(u *url.URL, doc, form *html.Node) (*http.Request, error) {
- if s.seenTOS {
- return nil, fmt.Errorf("already seen")
- } else {
- s.seenTOS = true
- }
- return junoFillForm(u, form, junoFormData{
- Fill: func(name, defvalue string) (string, error) {
- switch name {
- case "readAccept", "_readAccept":
- return "on", nil
- default:
- return defvalue, nil
- }
- },
- Expect: map[string]bool{
- "readAccept": true,
- "_readAccept": true,
- },
- })
-}
-
-func (s *junoLoginState) junoStepTwoFactor(u *url.URL, doc, form *html.Node) (*http.Request, error) {
- if qs(form, "#btnSendCode") != nil {
- if s.seenTwoFactor {
- return nil, fmt.Errorf("already seen send code page")
- } else {
- s.seenTwoFactor = true
- }
- return junoFillForm(u, form, junoFormData{
- Fill: func(name, defvalue string) (string, error) {
- switch name {
- case "codeType":
- return "APP", nil
- default:
- return defvalue, nil
- }
- },
- Expect: map[string]bool{
- "codeType": true,
- "oneTimeCode": false,
- },
- })
- }
- req, err := junoFillForm(u, form, junoFormData{
- Fill: func(name, defvalue string) (string, error) {
- switch name {
- case "oneTimeCode":
- if defvalue != "" {
- return "", fmt.Errorf("%w %q", ErrInvalidTwoFactor, defvalue)
- }
- if s.TOTP == nil {
- return "", fmt.Errorf("%w: no totp secret provided", ErrInvalidTwoFactor)
- }
- return s.TOTP(time.Now()), nil
- default:
- return defvalue, nil
- }
- },
- Expect: map[string]bool{
- "oneTimeCode": true,
- },
- })
- if err == nil {
- if s.seenTwoFactorCode {
- return nil, fmt.Errorf("already seen")
- } else {
- s.seenTwoFactorCode = true
- }
- }
- return req, err
-}
-
-func (s *junoLoginState) junoStepEnd(u *url.URL, doc *html.Node) (*http.Request, error) {
- if s.seenEnd {
- return nil, fmt.Errorf("already seen")
- } else {
- s.seenEnd = true
- }
- for _, n := range qsa(doc, "script") {
- var d strings.Builder
- htmlWalkDFS(n, func(n *html.Node, _ int) error {
- if n.Type == html.CommentNode || n.Type == html.TextNode {
- d.WriteString(n.Data)
- }
- return nil
- })
- if m := regexp.MustCompile(`(?m)window.location\s*=\s*["'](https://[^"'\\]+/connect/auth[^"'\\]+)["']`).FindStringSubmatch(d.String()); m != nil {
- r, err := u.Parse(string(m[1]))
- if err != nil {
- return nil, fmt.Errorf("resolve js redirect %q against %q: %w", string(m[1]), u.String(), err)
- }
-
- req, err := http.NewRequest(http.MethodGet, r.String(), nil)
- if err == nil {
- req.Header.Set("Referrer", u.String())
- }
- return req, nil
- }
- }
- return nil, fmt.Errorf("could not find js redirect")
-}
-
-type junoFormData struct {
- Fill func(name, defvalue string) (string, error)
- Expect map[string]bool
-}
-
-// junoFillForm fills the provided HTML form, using values from data.Fill
-// (returning an error if the returned value is invalid for a select/radio/etc),
-// and ensuring that the fields in data.Expect are present (or not) as expected.
-func junoFillForm(u *url.URL, form *html.Node, data junoFormData) (*http.Request, error) {
- if form.DataAtom != atom.Form {
- return nil, fmt.Errorf("element is not a form")
- }
- submitURL := &url.URL{
- Scheme: "https",
- Host: u.Host,
- Path: u.Path,
- RawPath: u.RawPath,
- RawQuery: u.RawQuery,
- }
- for _, a := range form.Attr {
- if a.Namespace == "" {
- switch strings.ToLower(a.Key) {
- case "action":
- if v, err := u.Parse(a.Val); err == nil {
- submitURL = v
- } else {
- return nil, fmt.Errorf("resolve form submit url: %w", err)
- }
- case "method":
- if a.Val != "" && strings.ToLower(a.Val) != "post" {
- return nil, fmt.Errorf("unexpected form method %q", a.Val)
- }
- case "enctype":
- if a.Val != "" && strings.ToLower(a.Val) != "application/x-www-form-urlencoded" {
- return nil, fmt.Errorf("unexpected form method %q", a.Val)
- }
- }
- }
- }
-
- var (
- formData = url.Values{}
- formOptions = map[string][]string{}
- formCheckbox = map[string]string{}
- )
- for _, n := range qsa(form, `[name]`) {
- var (
- eName, _ = htmlAttr(n, "name", "")
- eValue, _ = htmlAttr(n, "value", "")
- eType, _ = htmlAttr(n, "type", "")
- _, eChecked = htmlAttr(n, "checked", "")
- )
- if eName == "" {
- continue
- }
- switch n.DataAtom {
- case atom.A:
- // ignore
- case atom.Input:
- switch {
- case asciiEqualFold(eType, "submit"), asciiEqualFold(eType, "reset"), asciiEqualFold(eType, "image"), asciiEqualFold(eType, "button"):
- continue // ignore buttons
- case asciiEqualFold(eType, "checkbox"):
- if eValue != "" {
- formCheckbox[eName] = eValue
- } else {
- formCheckbox[eName] = "on"
- }
- if eChecked {
- formData[eName] = []string{formCheckbox[eName]}
- } else {
- formData[eName] = nil
- }
- case asciiEqualFold(eType, "radio"):
- if eValue != "" {
- formOptions[eName] = append(formOptions[eName], eValue)
- } else {
- formOptions[eName] = append(formOptions[eName], "on")
- }
- if eChecked {
- if eValue != "" {
- formData[eName] = []string{eValue}
- } else {
- formData[eName] = []string{"on"}
- }
- } else {
- formData[eName] = nil
- }
- default:
- formData[eName] = []string{eValue}
- }
- case atom.Select:
- if _, x := htmlAttr(n, "multiple", ""); x {
- return nil, fmt.Errorf("unhandled form element %s[multiple]", n.DataAtom)
- }
- for i, m := range qsa(n, `option`) {
- if v, ok := htmlAttr(m, "value", ""); ok {
- if _, selected := htmlAttr(m, "selected", ""); selected || i == 0 {
- formData[eName] = []string{v}
- }
- formOptions[eName] = append(formOptions[eName], v)
- }
- }
- default:
- return nil, fmt.Errorf("unhandled form element %s[name=%s]", n.DataAtom, eName)
- }
- }
-
- for k, v := range formData {
- if data.Expect != nil {
- if expected, ok := data.Expect[k]; ok {
- if expected {
- delete(data.Expect, k)
- } else {
- return nil, fmt.Errorf("have unexpected field %q", k)
- }
- }
- }
- var defvalue string
- if len(v) != 0 {
- defvalue = v[0]
- }
- if value, err := data.Fill(k, defvalue); err != nil {
- return nil, fmt.Errorf("fill field %q: %w", k, err)
- } else if value != defvalue {
- if opts, isSelect := formOptions[k]; isSelect {
- if value == "" {
- formData[k] = nil
- } else {
- var found bool
- for _, opt := range opts {
- if value == opt {
- found = true
- break
- }
- }
- if !found {
- return nil, fmt.Errorf("fill field %q: new value %q not in options %q", k, value, opts)
- }
- formData[k] = []string{value}
- }
- } else {
- formData[k] = []string{value}
- }
- }
- }
- if data.Expect != nil {
- for k, expected := range data.Expect {
- if expected {
- return nil, fmt.Errorf("missing expected field %q", k)
- }
- }
- }
-
- req, err := http.NewRequest(http.MethodPost, submitURL.String(), strings.NewReader(formData.Encode()))
- if err == nil {
- req.Header.Set("Referrer", u.String())
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- }
- return req, err
-}
-
-var (
- htmlWalkBreak = errors.New("break") //lint:ignore ST1012 special error
- htmlWalkSkip = errors.New("skip") //lint:ignore ST1012 special error
-)
-
-// htmlWalk does a depth-first walk of the provided node.
-func htmlWalkDFS(n *html.Node, fn func(n *html.Node, depth int) error) error {
- if n != nil {
- var depth int
- var stk []*html.Node
- for stk = append(stk, n); len(stk) != 0; {
- var cur *html.Node
- if cur, stk = stk[len(stk)-1], stk[:len(stk)-1]; cur != nil {
- var skip bool
- if err := fn(cur, depth); err != nil {
- if err == htmlWalkBreak {
- return nil
- }
- if err != htmlWalkSkip {
- return err
- }
- skip = true
- }
- if !skip && cur.LastChild != nil {
- stk = append(stk, nil)
- for n := cur.LastChild; n != nil; n = n.PrevSibling {
- stk = append(stk, n)
- }
- depth++
- }
- } else {
- depth--
- }
- }
- }
- return nil
-}
-
-// htmlText gets the normalized inner text of n.
-func htmlText(n *html.Node) string {
- var tok []string
- htmlWalkDFS(n, func(n *html.Node, _ int) error {
- if n.Type == html.TextNode {
- tok = append(tok, strings.Fields(n.Data)...)
- }
- return nil
- })
- return strings.Join(tok, " ")
-}
-
-// htmlAttr gets the value of a non-namespaced attribute.
-func htmlAttr(n *html.Node, key, defvalue string) (string, bool) {
- if n != nil {
- for _, a := range n.Attr {
- if a.Namespace == "" && asciiEqualFold(a.Key, key) {
- return a.Val, true
- }
- }
- }
- return defvalue, false
-}
-
-// qs executes a CSS selector against n, returning a single match.
-func qs(n *html.Node, q string) *html.Node {
- if n == nil {
- return nil
- }
- return cascadia.Query(n, cascadia.MustCompile(q))
-}
-
-// qsa executes a CSS selector against n, returning all matches.
-func qsa(n *html.Node, q string) []*html.Node {
- if n == nil {
- return nil
- }
- return cascadia.QueryAll(n, cascadia.MustCompile(q))
-}
-
-// asciiEqualFold is like strings.EqualFold, but ASCII-only.
-func asciiEqualFold(s, t string) bool {
- if len(s) != len(t) {
- return false
- }
- for i := 0; i < len(s); i++ {
- if asciiLower(s[i]) != asciiLower(t[i]) {
- return false
- }
- }
- return true
-}
-
-// asciiLower gets the ASCII lowercase version of b.
-func asciiLower(b byte) byte {
- if 'A' <= b && b <= 'Z' {
- return b + ('a' - 'A')
- }
- return b
-}
-
-// totp returns the RFC6238 time-based counter for hotp.
-func totp(t time.Time, s time.Duration) uint64 {
- if t.IsZero() {
- t = time.Now()
- }
- if s == 0 {
- s = time.Second * 30
- }
- return uint64(math.Floor(float64(t.Unix()) / s.Seconds()))
-}
-
-// hotp computes a RFC4226 otp.
-func hotp(c uint64, k []byte, n int, h func() hash.Hash) string {
- if n == 0 {
- n = 6
- }
- if h == nil {
- h = sha1.New
- }
- if n <= 0 || n > 8 {
- panic("otp: must be 0 < n <= 8")
- }
- if len(k) == 0 {
- panic("otp: key must not be empty")
- }
- hsh := hmac.New(h, k)
- binary.Write(hsh, binary.BigEndian, c)
- dst := hsh.Sum(nil)
- off := dst[len(dst)-1] & 0xf
- val := int64(((int(dst[off]))&0x7f)<<24 |
- ((int(dst[off+1] & 0xff)) << 16) |
- ((int(dst[off+2] & 0xff)) << 8) |
- ((int(dst[off+3]) & 0xff) << 0))
- return fmt.Sprintf("%0*d", n, val%int64(math.Pow10(n)))
-}
diff --git a/pkg/origin/authmgr.go b/pkg/origin/authmgr.go
deleted file mode 100644
index 2fb1d55..0000000
--- a/pkg/origin/authmgr.go
+++ /dev/null
@@ -1,193 +0,0 @@
-package origin
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "sync"
- "time"
-
- "github.com/cardigann/harhar"
- "github.com/r2northstar/atlas/pkg/juno"
-)
-
-var ErrAuthMgrBackoff = errors.New("not refreshing token due to backoff")
-
-// AuthMgr manages Origin NucleusTokens. It is efficient and safe for concurrent
-// use.
-//
-// For persistence, load the credentials on startup using SetAuth, and store the
-// credentials using Updated.
-type AuthMgr struct {
- // Timeout is the timeout for refreshing tokens. If zero, a reasonable
- // default is used. If negative, there is no timeout.
- Timeout time.Duration
-
- // Updated, if provided, is called in a new goroutine when tokens have
- // changed. AuthState is always set and should be saved, even if an error
- // occured.
- Updated func(AuthState, error)
-
- // Credentials, if provided, is called to get credentials when updating the
- // SID.
- Credentials func() (email, password, otpsecret string, err error)
-
- // Backoff, if provided, checks if another refresh is allowed after a
- // failure. If it returns false, ErrAuthMgrBackoff will be returned
- // immediately from OriginAuth.
- Backoff func(err error, time time.Time, count int) bool
-
- // SaveHAR, if provided, is called after every attempt to authenticate.
- SaveHAR func(func(w io.Writer) error, error)
-
- authInit sync.Once
- authPf bool // ensures only one update runs at a time
- authCv *sync.Cond // allows other goroutines to wait for that update to complete
- authErr error // last auth error
- authErrTime time.Time // last auth error time
- authErrCount int // consecutive auth errors
- auth AuthState // current auth tokens
-}
-
-// AuthState contains the current authentication tokens.
-type AuthState struct {
- SID juno.SID `json:"sid,omitempty"`
- NucleusToken NucleusToken `json:"nucleus_token,omitempty"`
- NucleusTokenExpiry time.Time `json:"nucleus_token_expiry,omitempty"`
-}
-
-func (a *AuthMgr) init() {
- a.authInit.Do(func() {
- a.authCv = sync.NewCond(new(sync.Mutex))
- })
-}
-
-// SetAuth sets the current Origin credentials. If authentication is in
-// progress, it will block.
-func (a *AuthMgr) SetAuth(auth AuthState) {
- a.init()
- a.authCv.L.Lock()
- for a.authPf {
- a.authCv.Wait()
- }
- a.auth = auth
- a.authErr = nil
- a.authCv.L.Unlock()
-}
-
-// OriginAuth gets the current NucleusToken. If refresh is true or the nucleus
-// token is missing/expired, it generates a new NucleusToken, getting a new SID
-// if required. If another refresh is in progress, it waits for the result of
-// it. True is returned (on success or failure) if this call performed a
-// refresh. This function may block for up to Timeout.
-//
-// In general, OriginAuth(false) should be used first, then if an API call error
-// is ErrAuthRequired, try it again with the token from OriginAuth(true).
-func (a *AuthMgr) OriginAuth(refresh bool) (NucleusToken, bool, error) {
- a.init()
- if a.authCv.L.Lock(); a.authPf {
- for a.authPf {
- a.authCv.Wait()
- }
- defer a.authCv.L.Unlock()
- return a.auth.NucleusToken, false, a.authErr
- } else {
- if refresh || a.auth.NucleusToken == "" || !time.Now().Before(a.auth.NucleusTokenExpiry) {
- a.authPf = true
- a.authCv.L.Unlock()
- defer func() {
- a.authCv.L.Lock()
- a.authCv.Broadcast()
- a.authPf = false
- a.authCv.L.Unlock()
- }()
- } else {
- defer a.authCv.L.Unlock()
- return a.auth.NucleusToken, false, a.authErr
- }
- }
- if a.authErr != nil && a.Backoff != nil {
- if !a.Backoff(a.authErr, a.authErrTime, a.authErrCount) {
- return a.auth.NucleusToken, true, fmt.Errorf("%w (%d attempts, last error: %v)", ErrAuthMgrBackoff, a.authErrCount, a.authErrCount)
- }
- }
- a.authErr = func() (err error) {
- t := http.DefaultClient.Transport
- if t == nil {
- t = http.DefaultTransport
- }
- if a.SaveHAR != nil {
- rec := harhar.NewRecorder()
- rec.RoundTripper, t = t, rec
- defer func() {
- go a.SaveHAR(func(w io.Writer) error {
- return json.NewEncoder(w).Encode(rec.HAR)
- }, err)
- }()
- }
-
- defer func() {
- if p := recover(); p != nil {
- err = fmt.Errorf("panic: %v", p)
- }
- }()
-
- var ctx context.Context
- var cancel context.CancelFunc
- if a.Timeout > 0 {
- ctx, cancel = context.WithTimeout(context.Background(), a.Timeout)
- } else if a.Timeout == 0 {
- ctx, cancel = context.WithTimeout(context.Background(), time.Second*15)
- } else {
- ctx, cancel = context.WithCancel(context.Background())
- }
- defer cancel()
-
- if a.auth.SID != "" {
- if tok, exp, aerr := GetNucleusToken(ctx, t, a.auth.SID); aerr == nil {
- a.auth.NucleusToken = tok
- a.auth.NucleusTokenExpiry = exp
- return
- } else if !errors.Is(aerr, ErrAuthRequired) {
- err = fmt.Errorf("refresh nucleus token: %w", aerr)
- return
- }
- }
- if a.Credentials == nil {
- err = fmt.Errorf("no origin credentials to refresh sid with")
- return
- } else if email, password, otpsecret, aerr := a.Credentials(); aerr != nil {
- err = fmt.Errorf("get origin credentials: %w", aerr)
- return
- } else if res, aerr := juno.Login(ctx, t, email, password, otpsecret); aerr != nil {
- err = fmt.Errorf("refresh sid: %w", aerr)
- return
- } else {
- a.auth.SID = res.SID
- }
- if tok, exp, aerr := GetNucleusToken(ctx, t, a.auth.SID); aerr != nil {
- err = fmt.Errorf("refresh nucleus token with new sid: %w", aerr)
- } else {
- a.auth.NucleusToken = tok
- a.auth.NucleusTokenExpiry = exp
- }
- return
- }()
- if a.authErrCount != 0 {
- a.authErr = fmt.Errorf("%w (attempt %d)", a.authErr, a.authErrCount)
- }
- if a.authErr != nil {
- a.authErrCount++
- a.authErrTime = time.Now()
- } else {
- a.authErrCount = 0
- a.authErrTime = time.Time{}
- }
- if a.Updated != nil {
- go a.Updated(a.auth, a.authErr)
- }
- return a.auth.NucleusToken, true, a.authErr
-}
diff --git a/pkg/origin/login.go b/pkg/origin/login.go
deleted file mode 100644
index f8bb255..0000000
--- a/pkg/origin/login.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package origin
-
-import (
- "context"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "net/http/cookiejar"
- "time"
-
- "github.com/r2northstar/atlas/pkg/juno"
-)
-
-type NucleusToken string
-
-// GetNucleusToken generates a Nucleus AuthToken from the active session. Note
-// that this token generally lasts ~4h.
-//
-// If errors.Is(err, ErrAuthRequired), you need a new SID.
-func GetNucleusToken(ctx context.Context, t http.RoundTripper, sid juno.SID) (NucleusToken, time.Time, error) {
- if t == nil {
- t = http.DefaultClient.Transport
- }
-
- jar, _ := cookiejar.New(nil)
- c := &http.Client{
- Transport: t,
- Jar: jar,
- }
- sid.AddTo(c.Jar)
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://accounts.ea.com/connect/auth?client_id=ORIGIN_JS_SDK&response_type=token&redirect_uri=nucleus:rest&prompt=none&release_type=prod", nil)
- if err != nil {
- return "", time.Time{}, err
- }
-
- req.Header.Set("Referrer", "https://www.origin.com/")
- req.Header.Set("Origin", "https://www.origin.com/")
-
- req.Header.Set("Accept-Language", "en-US;q=0.7,en;q=0.3")
- req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36")
-
- resp, err := c.Do(req)
- if err != nil {
- return "", time.Time{}, fmt.Errorf("get nucleus token: %w", err)
- }
- defer resp.Body.Close()
-
- buf, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", time.Time{}, fmt.Errorf("get nucleus token: %w", err)
- }
-
- var eobj struct {
- ErrorCode string `json:"error_code"`
- Error string `json:"error"`
- ErrorNumber json.Number `json:"error_number"`
- }
- if err := json.Unmarshal(buf, &eobj); err == nil && eobj.Error != "" {
- if eobj.ErrorCode == "login_required" {
- return "", time.Time{}, fmt.Errorf("get nucleus token: %w: login required", ErrAuthRequired)
- }
- return "", time.Time{}, fmt.Errorf("get nucleus token: %w: error %s: %s (%q)", ErrOrigin, eobj.ErrorNumber, eobj.ErrorCode, eobj.Error)
- }
-
- if resp.StatusCode != http.StatusOK {
- return "", time.Time{}, fmt.Errorf("get nucleus token: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
- }
-
- var obj struct {
- AccessToken string `json:"access_token"`
- TokenType string `json:"token_type"`
- ExpiresIn json.Number `json:"expires_in"`
- }
- if err := json.Unmarshal(buf, &obj); err != nil {
- return "", time.Time{}, fmt.Errorf("get nucleus token: %w", err)
- }
- if obj.AccessToken == "" || obj.ExpiresIn == "" {
- return "", time.Time{}, fmt.Errorf("get nucleus token: invalid response %q", string(buf))
- }
-
- var expiry time.Time
- if v, err := obj.ExpiresIn.Int64(); err == nil {
- expiry = time.Now().Add(time.Duration(v) * time.Second)
- } else {
- return "", time.Time{}, fmt.Errorf("get nucleus token: invalid response %q: invalid expiry: %w", string(buf), err)
- }
- return NucleusToken(obj.AccessToken), expiry, nil
-}
diff --git a/pkg/origin/origin.go b/pkg/origin/origin.go
deleted file mode 100644
index d7f4426..0000000
--- a/pkg/origin/origin.go
+++ /dev/null
@@ -1,131 +0,0 @@
-// Package origin is a client for parts of the Origin API used by Northstar for
-// authentication.
-package origin
-
-import (
- "context"
- "encoding/xml"
- "errors"
- "fmt"
- "io"
- "mime"
- "net/http"
- "strconv"
- "strings"
-)
-
-var (
- ErrInvalidResponse = errors.New("invalid origin api response")
- ErrOrigin = errors.New("origin api error")
- ErrAuthRequired = errors.New("origin authentication required")
-)
-
-// Base is the base path for the Origin API.
-var Base = "https://api1.origin.com"
-
-// UserInfo contains information about an Origin account.
-type UserInfo struct {
- UserID uint64
- PersonaID string
- EAID string
-}
-
-// GetUserInfo gets information about Origin accounts by their Origin UserID.
-//
-// If errors.Is(err, ErrAuthRequired), you need a new NucleusToken.
-func GetUserInfo(ctx context.Context, token NucleusToken, uid ...uint64) ([]UserInfo, error) {
- uids := make([]string, len(uid))
- for _, x := range uid {
- uids = append(uids, strconv.FormatUint(x, 10))
- }
-
- req, err := http.NewRequestWithContext(ctx, http.MethodGet, Base+"/atom/users?userIds="+strings.Join(uids, ","), nil)
- if err != nil {
- return nil, err
- }
-
- req.Header.Set("AuthToken", string(token))
- req.Header.Set("X-Origin-Platform", "UnknownOS")
- req.Header.Set("Referrer", "https://www.origin.com/")
-
- resp, err := http.DefaultClient.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
-
- buf, root, err := checkResponseXML(resp)
- if err != nil {
- return nil, err
- }
- return parseUserInfo(buf, root)
-}
-
-func checkResponseXML(resp *http.Response) ([]byte, xml.Name, error) {
- var root xml.Name
- buf, err := io.ReadAll(resp.Body)
- if err != nil {
- return buf, root, err
- }
- if mt, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")); mt != "application/xml" && mt != "text/xml" {
- if resp.StatusCode != http.StatusOK {
- return buf, root, fmt.Errorf("%w: response status %d (%s)", ErrOrigin, resp.StatusCode, resp.Status)
- }
- return buf, root, fmt.Errorf("%w: expected xml, got %q", ErrOrigin, mt)
- }
- if err := xml.Unmarshal(buf, &root); err != nil {
- return buf, root, fmt.Errorf("%w: invalid xml: %v", ErrInvalidResponse, err)
- }
- if root.Local == "error" {
- var obj struct {
- Code int `xml:"code,attr"`
- Failure []struct {
- Field string `xml:"field,attr"`
- Cause string `xml:"cause,attr"`
- Value string `xml:"value,attr"`
- } `xml:"failure"`
- }
- if err := xml.Unmarshal(buf, &obj); err != nil {
- return buf, root, fmt.Errorf("%w: response %#q (unmarshal: %v)", ErrOrigin, string(buf), err)
- }
- for _, f := range obj.Failure {
- if f.Cause == "invalid_token" {
- return buf, root, fmt.Errorf("%w: invalid token", ErrAuthRequired)
- }
- }
- if len(obj.Failure) == 1 {
- return buf, root, fmt.Errorf("%w: error %d: %s (%s) %q", ErrOrigin, obj.Code, obj.Failure[0].Cause, obj.Failure[0].Field, obj.Failure[0].Value)
- }
- return buf, root, fmt.Errorf("%w: error %d: response %#q", ErrOrigin, obj.Code, string(buf))
- }
- return buf, root, nil
-}
-
-func parseUserInfo(buf []byte, root xml.Name) ([]UserInfo, error) {
- var obj struct {
- User []struct {
- UserID string `xml:"userId"`
- PersonaID string `xml:"personaId"`
- EAID string `xml:"EAID"`
- } `xml:"user"`
- }
- if root.Local != "users" {
- return nil, fmt.Errorf("%w: unexpected %s response", ErrInvalidResponse, root.Local)
- }
- if err := xml.Unmarshal(buf, &obj); err != nil {
- return nil, fmt.Errorf("%w: invalid xml: %v", ErrInvalidResponse, err)
- }
- res := make([]UserInfo, len(obj.User))
- for i, x := range obj.User {
- var v UserInfo
- if uid, err := strconv.ParseUint(x.UserID, 10, 64); err == nil {
- v.UserID = uid
- } else {
- return nil, fmt.Errorf("parse userId %q: %w", x.UserID, err)
- }
- v.PersonaID = x.PersonaID
- v.EAID = x.EAID
- res[i] = v
- }
- return res, nil
-}
diff --git a/pkg/origin/origin_test.go b/pkg/origin/origin_test.go
deleted file mode 100644
index 7735355..0000000
--- a/pkg/origin/origin_test.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package origin
-
-import (
- "errors"
- "io"
- "net/http"
- "reflect"
- "strconv"
- "strings"
- "testing"
-)
-
-func TestUserInfoResponse(t *testing.T) {
- testUserInfoResponse(t,
- "SuccessNew",
- 200, "text/xml", `<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><users><user><userId>1001111111111</userId><personaId>1001111111111</personaId><EAID>test</EAID></user><user><userId>1001111111112</userId><personaId>1001111111112</personaId><EAID>test1</EAID></user></users>`,
- []UserInfo{
- {UserID: 1001111111111, PersonaID: "1001111111111", EAID: "test"},
- {UserID: 1001111111112, PersonaID: "1001111111112", EAID: "test1"},
- }, nil,
- )
- testUserInfoResponse(t,
- "SuccessOld",
- 200, "text/xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><users><user><userId>2291234567</userId><personaId>328123456</personaId><EAID>blahblah</EAID></user></users>`,
- []UserInfo{
- {UserID: 2291234567, PersonaID: "328123456", EAID: "blahblah"},
- }, nil,
- )
- testUserInfoResponse(t,
- "EmptyToken",
- 200, "text/xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><error code="10044"><failure value="" field="authToken" cause="MISSING_AUTHTOKEN"/></error>`,
- nil, ErrOrigin,
- )
- testUserInfoResponse(t,
- "InvalidExpiredToken",
- 200, "text/xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><error code="10044"><failure value="" field="authToken" cause="invalid_token"/></error>`,
- nil, ErrAuthRequired,
- )
- testUserInfoResponse(t,
- "FakeWrongRootElement",
- 200, "text/xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><fake/></error>`,
- nil, ErrInvalidResponse,
- )
- testUserInfoResponse(t,
- "FakeError",
- 200, "text/xml", `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><error code="12345"><failure value="" field="dummy" cause="fake"/></error>`,
- nil, ErrOrigin,
- )
- testUserInfoResponse(t,
- "FakeBadResponse",
- 500, "text/plain", `Fake Internal Server Error`,
- nil, ErrOrigin,
- )
- testUserInfoResponse(t,
- "FakeInvalidXML",
- 200, "text/xml", `fake`,
- nil, ErrInvalidResponse,
- )
-}
-
-func testUserInfoResponse(t *testing.T, name string, status int, mime, xml string, v []UserInfo, err error) {
- t.Run(name, func(t *testing.T) {
- buf, root, err1 := checkResponseXML(&http.Response{
- Status: strconv.Itoa(status) + " " + http.StatusText(status),
- StatusCode: status,
- Body: io.NopCloser(strings.NewReader(xml)),
- Header: http.Header{
- "Content-Type": {mime},
- },
- })
- if err1 != nil {
- if err == nil {
- t.Fatalf("expected no error, got %q", err1)
- }
- if !errors.Is(err1, err) {
- t.Fatalf("expected error %q, got %q", err, err1)
- }
- return
- }
-
- ui, err1 := parseUserInfo(buf, root)
- if err1 != nil {
- if err == nil {
- t.Fatalf("expected no error, got %q", err1)
- }
- if !errors.Is(err1, err) {
- t.Fatalf("expected error %q, got %q", err, err1)
- }
- return
- }
- if err != nil {
- t.Fatalf("expected error %q, got nothing", err)
- }
-
- if !reflect.DeepEqual(ui, v) {
- t.Errorf("unexpected result %#v", ui)
- }
- })
-}