aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-22 23:44:36 -0400
committerpg9182 <96569817+pg9182@users.noreply.github.com>2022-10-22 23:44:36 -0400
commitd2ac8b85ff21b85d24cae3018b7b2baf001cf193 (patch)
tree5a3230a72e3d46e43308ef15ea2bb77b1aa76648
parent021d37d22c6015a85000b3febabcb6f381eb3aa6 (diff)
downloadAtlas-d2ac8b85ff21b85d24cae3018b7b2baf001cf193.tar.gz
Atlas-d2ac8b85ff21b85d24cae3018b7b2baf001cf193.zip
pkg/origin: Switch login from originX to juno (fixes #7)
* Update URLs. * Handle new form elements. * Handle TOS update page. * Handle new final code redirect flow. Will likely need to update the nucleus token stuff once origin is deprecated completely since that still uses the ORIGIN_JS_SDK flow. I'm also planning to refactor this later since it's gotten a bit messy.
-rw-r--r--pkg/origin/login.go335
1 files changed, 273 insertions, 62 deletions
diff --git a/pkg/origin/login.go b/pkg/origin/login.go
index 2eae807..3ba61b0 100644
--- a/pkg/origin/login.go
+++ b/pkg/origin/login.go
@@ -35,7 +35,7 @@ func Login(ctx context.Context, email, password string) (SID, error) {
Jar: jar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
switch host, _, _ := strings.Cut(req.URL.Host, ":"); strings.ToLower(host) {
- case "accounts.ea.com", "signin.ea.com", "www.origin.com":
+ case "accounts.ea.com", "signin.ea.com", "www.ea.com":
default:
return fmt.Errorf("domain %q is not whitelisted", host)
}
@@ -56,10 +56,19 @@ func Login(ctx context.Context, email, password string) (SID, error) {
return "", err
}
- r2, err := login2(ctx, c, r1)
+ r2, again, err := login2(ctx, c, r1)
if err != nil {
return "", err
}
+ if !again {
+ r2, again, err = login2(ctx, c, r2)
+ if err != nil {
+ return "", err
+ }
+ if again {
+ return "", fmt.Errorf("looped at request to %q", r2.URL.String())
+ }
+ }
_, err = login3(ctx, c, r2)
if err != nil {
@@ -83,7 +92,7 @@ func Login(ctx context.Context, email, password string) (SID, error) {
// Returns a HTTP request for opening the login form.
func login0(ctx context.Context, c *http.Client) (*http.Request, error) {
// init locale and cookie settings
- for _, host := range []string{"www.origin.com", "accounts.ea.com", "signin.ea.com"} {
+ for _, host := range []string{"www.ea.com", "accounts.ea.com", "signin.ea.com"} {
c.Jar.SetCookies(&url.URL{
Scheme: "https",
Host: host,
@@ -94,15 +103,23 @@ func login0(ctx context.Context, c *http.Client) (*http.Request, error) {
})
}
- // login page (opened with window.open from the Origin webapp)
- return http.NewRequestWithContext(ctx, http.MethodGet, "https://accounts.ea.com/connect/auth?response_type=code&client_id=ORIGIN_SPA_ID&display=originXWeb/login&locale=en_US&release_type=prod&redirect_uri=https://www.origin.com/views/login.html", nil)
+ // login page (from the www.ea.com homepage)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.ea.com/login", nil)
+ if err != nil {
+ return nil, err
+ }
+
+ req.Header.Set("Referrer", "https://www.ea.com/en-us")
+
+ return req, nil
}
// login1 starts the login flow.
//
-// GET https://accounts.ea.com/connect/auth?response_type=code&client_id=ORIGIN_SPA_ID&display=originXWeb/login&locale=en_US&release_type=prod&redirect_uri=https://www.origin.com/views/login.html
-// 302 https://signin.ea.com/p/originX/login?fid=...
-// 302 https://signin.ea.com/p/originX/login?execution=e678590193s1&initref=...
+// - GET https://www.ea.com/en-us
+// - 303 https://accounts.ea.com/connect/auth?response_type=code&redirect_uri=https://www.ea.com/login_check&state=...&locale=en_US&client_id=EADOTCOM-WEB-SERVER&display=junoWeb/login
+// - 302 https://signin.ea.com/p/juno/login?fid=...
+// - 302 https://signin.ea.com/p/juno/login?execution=...s1&initref=...
//
// Returns a HTTP request for submitting the login form.
func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, password string) (*http.Request, error) {
@@ -131,7 +148,7 @@ func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, passwo
return nil, fmt.Errorf("start login flow: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
}
- if resp.Request.URL.Path != "/p/originX/login" {
+ if resp.Request.URL.Path != "/p/juno/login" {
return nil, fmt.Errorf("start login flow: unexpected login form path %q (the code probably needs to be updated)", resp.Request.URL.Path)
}
@@ -175,18 +192,64 @@ func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, passwo
/*
<form id="login-form" method="post">
-
- <div class="otkform otkform-inputgroup">
-
- <div class="otkinput otkinput-grouped">
- <i class="otkinput-icon otkicon otkicon-profile"></i>
- <input type="text" id="email" name="email" value="" placeholder="Email Address" autocorrect="off" autocapitalize="off" autocomplete="off" />
+ <div class="otkform otkform-inputgroup input-margin-bottom-error-message">
+ <div id="email-phone-login-div">
+ <div id="toggle-form-email-input">
+ <div class="otkform-group">
+ <label class="otklabel label-uppercase" for="email">Phone or Email</label>
+ <div class="otkinput otkinput-grouped otkform-group-field input-margin-bottom-error-message">
+ <input type="text" id="email" name="email" value="" placeholder="Enter your phone or email" autocorrect="off" autocapitalize="none" autocomplete="off">
+ </div>
+ <div id="online-input-error-email" class="otkform-group-help">
+ <p class="otkinput-errormsg otkc"></p>
+ </div>
+ </div>
+ </div>
+ <div id="toggle-form-phone-number-input" style="display: none;">
+ <div class="otkform-group input-margin-bottom-error-message">
+ <label class="otklabel label-uppercase">Phone or Email</label>
+ <span class="otkselect" style="display: none;">
+ <select id="regionCode" name="regionCode">
+ ...
+ </select>
+ <span class="otkselect-label otkselect-placeholder phone-number-placeholder"></span>
+ <span class="otkselect-label otkselect-selected phone-number-pad">(+1)</span>
+ </span>
+ <div style="display: none;" id="hidden-svg-container"></div>
+ <div class="otkinput otkinput-grouped otkform-group-field">
+ <a href="javascript:void(0);" class="region-select-drop-down-btn">
+ <span class="quantum-input-icon">
+ <svg>
+ <use xlink:href="#ca"></use>
+ </svg>
+ </span>
+ <span class="quantum-input-icon-2">+1</span>
+ </a>
+ <div class="region-select-drop-down-panel" style="display: none;">...
+ </div>
+ <input type="text" id="phoneNumber" name="phoneNumber" value="" placeholder="Enter your phone or email" autocorrect="off" autocapitalize="none" autocomplete="tel">
+ </div>
+ </div>
+ </div>
</div>
- <div class="otkinput otkinput-grouped">
- <i class="otkinput-icon otkicon otkicon-lockclosed"></i>
- <input type="password" id="password" name="password" placeholder="Password" autocorrect="off" autocapitalize="off" autocomplete="off" />
+ <label class="otklabel label-uppercase" for="password">Password</label>
+ <div class="otkinput otkinput-grouped input-margin-bottom-error-message">
+ <input type="password" id="password" name="password" placeholder="Enter your password" autocorrect="off" autocapitalize="none" autocomplete="off">
<i class="otkinput-capslock otkicon otkicon-capslock otkicon-capslock-position"></i>
- <span id="passwordShow" class="otkbtn otkbtn-light">SHOW</span>
+ <button role="button" aria-label="Show password" id="passwordShow" class="otkbtn passwordShowBtn">
+ <span id="showIcon" class="quantum-input-icon eye-icon"><svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
+ <g fill="none" fill-rule="evenodd">
+ <path d="m0 0h16v16h-16z"></path>
+ <path d="m8 3.66666667c3.2032732 0 6.6666667 2.54318984 6.6666667 4.33333333s-3.4633935 4.3333333-6.6666667 4.3333333c-3.20327316 0-6.66666667-2.54318981-6.66666667-4.3333333s3.46339351-4.33333333 6.66666667-4.33333333zm0 1.33333333c-1.38181706 0-2.74575629.50269607-3.87107038 1.3290207-.87134632.63983463-1.46226295 1.41228951-1.46226295 1.6709793s.59091663 1.03114467 1.46226295 1.6709793c1.12531409.8263246 2.48925332 1.3290207 3.87107038 1.3290207s2.7457563-.5026961 3.8710704-1.3290207c.8713463-.63983463 1.4622629-1.41228951 1.4622629-1.6709793s-.5909166-1.03114467-1.4622629-1.6709793c-1.1253141-.82632463-2.48925334-1.3290207-3.8710704-1.3290207zm0 1c1.1045695 0 2 .8954305 2 2s-.8954305 2-2 2-2-.8954305-2-2 .8954305-2 2-2zm0 1.33333333c-.36818983 0-.66666667.29847684-.66666667.66666667s.29847684.66666667.66666667.66666667.66666667-.29847684.66666667-.66666667-.29847684-.66666667-.66666667-.66666667z" fill="#fff"></path>
+ </g>
+ </svg></span><span id="hideIcon" class="quantum-input-icon eye-icon hide-icon"><svg viewBox="0 0 24 24" height="16" width="16" xmlns="http://www.w3.org/2000/svg">
+ <g fill-rule="evenodd" fill="none">
+ <path d="M14.318 14.404a3 3 0 0 0-4.223-4.223l1.436 1.436a1 1 0 0 1 1.352 1.352l1.435 1.435zm-5.27-2.442a3 3 0 0 0 3.49 3.49l-3.49-3.49z" fill="#fff"></path>
+ <path d="M17.484 17.57c.546-.294 1.05-.617 1.506-.951.875-.643 1.597-1.348 2.11-2.02a5.54 5.54 0 0 0 .628-1.01c.15-.323.272-.7.272-1.089s-.122-.766-.272-1.088A5.54 5.54 0 0 0 21.1 10.4c-.514-.67-1.235-1.376-2.11-2.019C17.245 7.1 14.793 6 12 6c-1.824 0-3.503.47-4.934 1.152L8.585 8.67A9.309 9.309 0 0 1 12 8c2.27 0 4.318.9 5.807 1.994.742.545 1.321 1.12 1.704 1.621.192.251.323.468.403.64.071.153.083.231.086.244v.001c-.003.014-.015.092-.086.245a3.57 3.57 0 0 1-.403.64c-.383.5-.962 1.076-1.704 1.622a10.73 10.73 0 0 1-1.812 1.073l1.489 1.49zM6.718 9.632a9.89 9.89 0 0 0-.525.362c-.742.545-1.321 1.12-1.704 1.621a3.57 3.57 0 0 0-.403.64c-.071.153-.083.231-.086.244v.001c.003.014.015.092.086.245.08.172.21.389.403.64.383.5.962 1.076 1.704 1.622C7.683 16.1 9.73 17 12 17a8.92 8.92 0 0 0 1.882-.204l1.626 1.627c-1.08.357-2.26.577-3.508.577-2.792 0-5.245-1.1-6.99-2.381-.875-.643-1.597-1.348-2.11-2.02a5.544 5.544 0 0 1-.628-1.01c-.15-.323-.272-.7-.272-1.089s.122-.766.272-1.088A5.61 5.61 0 0 1 2.9 10.4c.513-.67 1.235-1.376 2.11-2.019.087-.064.176-.127.267-.19l1.44 1.441z" fill="#fff"></path>
+ <path d="M3.543 2.793a1 1 0 0 1 1.414 0l17 17a1 1 0 0 1-1.414 1.414l-17-17a1 1 0 0 1 0-1.414z" fill="#fff"></path>
+ </g>
+ </svg></span>
+ </button>
</div>
</div>
@@ -204,28 +267,30 @@ func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, passwo
</div>
<div class="panel-action-area">
- <input type="hidden" name="_eventId" value="submit" id="_eventId" />
+ <input type="hidden" name="_eventId" value="submit" id="_eventId">
<input type="hidden" id="cid" name="cid" value="">
<input type="hidden" id="showAgeUp" name="showAgeUp" value="true">
<input type="hidden" id="thirdPartyCaptchaResponse" name="thirdPartyCaptchaResponse" value="">
- <span class="otkcheckbox checkbox-login-first">
- <input type="hidden" name="_rememberMe" value="on" />
- <input type="checkbox" id="rememberMe" name="rememberMe" checked="checked" />
- <label for=rememberMe>
- <span id="content">Remember me</span>
+ <input type="hidden" id="loginMethod" name="loginMethod" value="">
+ <span class="otkcheckbox checkbox-login-first">
+ <input type="hidden" name="_rememberMe" value="on">
+ <input type="checkbox" id="rememberMe" name="rememberMe" checked="checked">
+ <label for="rememberMe">
+ <span id="content" class="link-in-message">Remember me</span>
</label>
</span>
- <a class='otkbtn otkbtn-primary ' href="#" id="logInBtn">Sign in</a>
- <input type="hidden" id="errorCode" value="" />
- <input type="hidden" id="errorCodeWithDescription" value="" />
- <input type="hidden" id="storeKey" value="" />
- <input type="hidden" id="bannerType" value="" />
- <input type="hidden" id="bannerText" value="" />
+ <div class="button-top-separator"></div>
+ <a role="button" class="otkbtn otkbtn-primary " href="javascript:void(0);" id="logInBtn">Sign in</a>
+ <input type="hidden" id="errorCode" value="">
+ <input type="hidden" id="errorCodeWithDescription" value="">
+ <input type="hidden" id="storeKey" value="">
+ <input type="hidden" id="bannerType" value="">
+ <input type="hidden" id="bannerText" value="">
</div>
</form>
@@ -253,7 +318,11 @@ func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, passwo
}
}
if el.DataAtom != atom.Input {
- return nil, fmt.Errorf("start login flow: parse document: unexpected form %s element %s", el.DataAtom, eName)
+ if el.DataAtom == atom.Select && eName == "regionCode" {
+ eValue = "US"
+ } else {
+ return nil, fmt.Errorf("start login flow: parse document: unexpected form %s element %s", el.DataAtom, eName)
+ }
}
if eType == "submit" || eType == "reset" || eType == "image" || eType == "button" {
continue // ignore buttons
@@ -269,10 +338,11 @@ func login1(ctx context.Context, c *http.Client, r0 *http.Request, email, passwo
}
data.Set(eName, eValue)
}
- if !data.Has("email") || !data.Has("password") {
+ if !data.Has("loginMethod") || !data.Has("email") || !data.Has("password") {
return nil, fmt.Errorf("start login flow: parse document: missing username or password field (data=%s)", data.Encode())
}
+ data.Set("loginMethod", "emailPassword")
data.Set("email", email)
data.Set("password", password)
@@ -288,20 +358,32 @@ var login2re = regexp.MustCompile(`(?m)window.location\s*=\s*["'](https://[^"'\\
// login2 submits the login form.
//
-// POST https://signin.ea.com/p/originX/login?execution=...s1&initref=... (email=...&password=...&_eventId=submit&cid=...&showAgeUp=true&thirdPartyCaptchaResponse=&_rememberMe=on&rememberMe=on)
-// window.location = "https://accounts.ea.com:443/connect/auth?display=originXWeb%2Flogin&response_type=code&release_type=prod&redirect_uri=https%3A%2F%2Fwww.origin.com%2Fviews%2Flogin.html&locale=en_US&client_id=ORIGIN_SPA_ID&fid=...";
+// TOS Update (step 1):
+// - POST https://signin.ea.com/p/juno/login?execution=...s1&initref=https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER
+// (email=...&regionCode=US&phoneNumber=&password=...&_eventId=submit&cid=...&showAgeUp=true&thirdPartyCaptchaResponse=&loginMethod=emailPassword&_rememberMe=on&rememberMe=on)
+// - 302 https://signin.ea.com/p/juno/login?execution=...s2&initref=https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER
+//
+// TOS Update (step 2):
+// - POST https://signin.ea.com/p/juno/login?execution=...s2&initref=https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER
+// (_readAccept=on&readAccept=on&_eventId=accept)
+// window.location = "https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER&fid=...";
//
-// Returns the redirect request.
-func login2(ctx context.Context, c *http.Client, r1 *http.Request) (*http.Request, error) {
+// Normal:
+// - POST https://signin.ea.com/p/juno/login?execution=...s1&initref=https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER
+// (email=...&regionCode=US&phoneNumber=&password=...&_eventId=submit&cid=...&showAgeUp=true&thirdPartyCaptchaResponse=&loginMethod=emailPassword&_rememberMe=on&rememberMe=on)
+// window.location = "https://accounts.ea.com:443/connect/auth?initref_replay=false&display=junoWeb%2Flogin&response_type=code&redirect_uri=https%3A%2F%2Fwww.ea.com%2Flogin_check&locale=en_CA&client_id=EADOTCOM-WEB-SERVER&fid=...";
+//
+// Returns another request if another submission is needed to accept the TOS before proceeding.
+func login2(ctx context.Context, c *http.Client, r1 *http.Request) (*http.Request, bool, error) {
resp, err := c.Do(r1)
if err != nil {
- return nil, fmt.Errorf("submit login form: %w", err)
+ return nil, false, fmt.Errorf("submit login form: %w", err)
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
- return nil, fmt.Errorf("submit login form: %w", err)
+ return nil, false, fmt.Errorf("submit login form: %w", err)
}
if resp.StatusCode != http.StatusOK {
@@ -312,56 +394,182 @@ func login2(ctx context.Context, c *http.Client, r1 *http.Request) (*http.Reques
Code int `json:"code"`
}
if err := json.Unmarshal(buf, &obj); err == nil && obj.Code != 0 {
- return nil, fmt.Errorf("submit login form: %w: error %d: %s (%q)", ErrOrigin, obj.Code, obj.Error, obj.ErrorDescription)
+ return nil, false, fmt.Errorf("submit login form: %w: error %d: %s (%q)", ErrOrigin, obj.Code, obj.Error, obj.ErrorDescription)
}
}
- return nil, fmt.Errorf("submit login form: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
+ return nil, false, fmt.Errorf("submit login form: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
}
m := login2re.FindSubmatch(buf)
if m == nil {
if doc, err := html.Parse(bytes.NewReader(buf)); err == nil {
+ if form := cascadia.Query(doc, cascadia.MustCompile(`form#tosForm`)); form != nil {
+ submitURL := &url.URL{
+ Scheme: "https",
+ Host: resp.Request.URL.Host,
+ Path: resp.Request.URL.Path,
+ RawPath: resp.Request.URL.RawPath,
+ RawQuery: resp.Request.URL.RawQuery,
+ }
+ for _, a := range form.Attr {
+ if a.Namespace == "" {
+ switch strings.ToLower(a.Key) {
+ case "action":
+ if v, err := resp.Request.URL.Parse(a.Val); err == nil {
+ submitURL = v
+ } else {
+ return nil, false, fmt.Errorf("submit login form: parse document: tos form: resolve form submit url: %w", err)
+ }
+ case "method":
+ if a.Val != "" && strings.ToLower(a.Val) != "post" {
+ return nil, false, fmt.Errorf("submit login form: parse document: tos form: unexpected form method %q", a.Val)
+ }
+ case "enctype":
+ if a.Val != "" && strings.ToLower(a.Val) != "application/x-www-form-urlencoded" {
+ return nil, false, fmt.Errorf("submit login form: parse document: tos form: unexpected form method %q", a.Val)
+ }
+ }
+ }
+ }
+
+ /*
+ <form method="post" id="tosForm">
+ <div id="tos-update" class="views">
+ <section id="tosReview">
+ <a href=javascript:void(0); role="button" id=back class="back-btn">
+ <i class="otkicon"></i>
+ <span class="quantum-input-icon back-btn-icon">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" class="injected-svg css-ojkidl" data-src="/static/media/ARROW_LEFT.e40e5952.svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="Icons/SM/Navigation/Arrow-Left" fill="none" fill-rule="evenodd">
+ <path id="Bounding-Box" d="M0 0h16v16H0z"></path>
+ <path id="Primary" fill="#FFF" d="M5.293 4.707a1 1 0 0 1 1.414-1.414l4 4a1 1 0 0 1 0 1.415l-4 4a1 1 0 1 1-1.414-1.415L8.586 8 5.293 4.707z" transform="matrix(-1 0 0 1 16 0)"></path>
+ </g>
+ </svg> </span>
+ Back
+ </a>
+
+ <!-- logo -->
+ <img class="header" aria-hidden="true" src="https://eaassets-a.akamaihd.net/resource_signin_ea_com/551.0.220928.064.d05eced/p/statics/juno/img/EALogo-New.svg" />
+
+ <h1 id="page_header" class="otktitle otktitle-2">Please review our terms</h1>
+
+ <div class="general-error">
+ <div>
+ <div></div>
+ </div>
+ </div>
+
+
+ <p class="otkc link-in-message">Thank you for choosing EA. Please take a few minutes to review our latest <a href="https://tos.ea.com/legalapp/webterms/CA/en/PC2/" target="_blank">User Agreement</a> and <a href="https://tos.ea.com/legalapp/webprivacy/CA/en/PC2/" target="_blank">Privacy and Cookie Policy</a>.</p>
+ <span class="otkcheckbox ">
+ <input type="hidden" name="_readAccept" value="on" />
+ <input type="checkbox" id="readAccept" name="readAccept" />
+ <label for=readAccept>
+ <span id="content" class="link-in-message">I have read and accept the <a href="https://tos.ea.com/legalapp/webterms/CA/en/PC2/" target="_blank">User Agreement</a> and <a href="https://tos.ea.com/legalapp/webprivacy/CA/en/PC2/" target="_blank">EA's Privacy and Cookie Policy</a>.</span>
+
+ </label>
+ </span>
+
+ <a role="button" class='otkbtn otkbtn-primary right' href="javascript:void(0);" id="btnNext">Next</a>
+ <input type="hidden" name="_eventId" value="accept" id="_eventId" />
+ </section>
+ </div>
+ </form>
+ */
+
+ data := url.Values{}
+ for _, el := range cascadia.QueryAll(form, cascadia.MustCompile(`[name]`)) {
+ if el.DataAtom == atom.A {
+ continue
+ }
+ var eName, eValue, eType string
+ var eChecked bool
+ for _, a := range el.Attr {
+ if a.Namespace == "" {
+ switch strings.ToLower(a.Key) {
+ case "name":
+ eName = a.Val
+ case "value":
+ eValue = a.Val
+ case "type":
+ eType = strings.ToLower(a.Val)
+ case "checked":
+ eChecked = true
+ }
+ }
+ }
+ if el.DataAtom != atom.Input {
+ return nil, false, fmt.Errorf("submit login form: parse document: tos form: unexpected form %s element %s", el.DataAtom, eName)
+ }
+ if eType == "submit" || eType == "reset" || eType == "image" || eType == "button" {
+ continue // ignore buttons
+ }
+ if eChecked && eValue == "" {
+ eValue = "on"
+ }
+ if (eType == "checkbox" || eType == "radio") && eValue == "" {
+ continue
+ }
+ data.Set(eName, eValue)
+ }
+ if !data.Has("_readAccept") {
+ return nil, false, fmt.Errorf("submit login form: parse document: tos form: missing readAccept field (data=%s)", data.Encode())
+ }
+
+ data.Set("_readAccept", "on")
+ data.Set("readAccept", "on")
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, submitURL.String(), strings.NewReader(data.Encode()))
+ if err == nil {
+ req.Header.Set("Referrer", resp.Request.URL.String())
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ }
+ return req, false, err
+ }
if n := cascadia.Query(doc, cascadia.MustCompile(`#errorCode[value]`)); n != nil {
for _, a := range n.Attr {
- // based on origin login js
+ // based on juno login js
if a.Namespace == "" && strings.EqualFold(a.Key, "value") {
switch errCode := a.Val; errCode {
case "10001": // try offline auth
- return nil, fmt.Errorf("submit login form: ea auth error %s: why the fuck does origin think we're offline", errCode)
+ return nil, false, fmt.Errorf("submit login form: ea auth error %s: why the fuck does origin think we're offline", errCode)
case "10002": // credentials
- return nil, fmt.Errorf("submit login form: ea auth error %s: %w", errCode, ErrInvalidLogin)
+ return nil, false, fmt.Errorf("submit login form: ea auth error %s: %w", errCode, ErrInvalidLogin)
case "10003": // general error
- return nil, fmt.Errorf("submit login form: ea auth error %s: login error", errCode)
+ return nil, false, fmt.Errorf("submit login form: ea auth error %s: login error", errCode)
case "10004": // wtf
- return nil, fmt.Errorf("submit login form: ea auth error %s: idk wtf this is", errCode)
+ return nil, false, fmt.Errorf("submit login form: ea auth error %s: idk wtf this is", errCode)
case "":
// no error, but this shouldn't happen
default:
- return nil, fmt.Errorf("submit login form: ea auth error %s", errCode)
+ return nil, false, fmt.Errorf("submit login form: ea auth error %s", errCode)
}
}
}
}
}
- return nil, fmt.Errorf("submit login form: could not find JS redirect URL")
+ return nil, false, fmt.Errorf("submit login form: could not find JS redirect URL")
}
u, err := resp.Request.URL.Parse(string(m[1]))
if err != nil {
- return nil, fmt.Errorf("submit login form: could not resolve JS redirect URL %q against %q", string(m[1]), resp.Request.URL.String())
+ return nil, false, fmt.Errorf("submit login form: could not resolve JS redirect URL %q against %q", string(m[1]), resp.Request.URL.String())
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err == nil {
req.Header.Set("Referrer", resp.Request.URL.String())
}
- return req, err
+ return req, true, err
}
// login3 finishes the login flow.
//
-// GET https://accounts.ea.com:443/connect/auth?display=originXWeb%2Flogin&response_type=code&release_type=prod&redirect_uri=https%3A%2F%2Fwww.origin.com%2Fviews%2Flogin.html&locale=en_US&client_id=ORIGIN_SPA_ID&fid=...
-// 302 https://www.origin.com/views/login.html?code=QUOxACG9yPs6t_IHz2K1adbc5yV4UPG-1hb_v2HY
+// - GET https://accounts.ea.com/connect/auth?initref_replay=false&display=junoWeb/login&response_type=code&redirect_uri=https://www.ea.com/login_check&locale=en_US&client_id=EADOTCOM-WEB-SERVER&fid=...
+// - 302 https://www.ea.com/login_check?code=...&state=...
+// - 303 https://www.ea.com/
+// - 302 https://www.ea.com/en-us/
+// - 301 https://www.ea.com/en-us
//
// Returns the token.
func login3(_ context.Context, c *http.Client, r2 *http.Request) (string, error) {
@@ -372,7 +580,7 @@ func login3(_ context.Context, c *http.Client, r2 *http.Request) (string, error)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
- if host, _, _ := strings.Cut(resp.Request.URL.Host, ":"); strings.ToLower(host) == "accounts.ea.com" {
+ if resp.Request.URL.Hostname() == r2.URL.Hostname() {
buf, _ := io.ReadAll(resp.Body)
var obj struct {
ErrorCode string `json:"error_code"`
@@ -380,25 +588,28 @@ func login3(_ context.Context, c *http.Client, r2 *http.Request) (string, error)
ErrorNumber json.Number `json:"error_number"`
}
if obj.ErrorCode == "login_required" {
- return "", fmt.Errorf("get nucleus token: %w: wants us to login, but we just did that", ErrOrigin)
+ return "", fmt.Errorf("finish login flow: %w: wants us to login, but we just did that", ErrOrigin)
}
if err := json.Unmarshal(buf, &obj); err == nil && obj.Error != "" {
- return "", fmt.Errorf("get nucleus token: %w: error %s: %s (%q)", ErrOrigin, obj.ErrorNumber, obj.ErrorCode, obj.Error)
+ return "", fmt.Errorf("finish login flow: %w: error %s: %s (%q)", ErrOrigin, obj.ErrorNumber, obj.ErrorCode, obj.Error)
}
- return "", fmt.Errorf("get nucleus token: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
+ return "", fmt.Errorf("finish login flow: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
}
return "", fmt.Errorf("finish login flow: request %q: response status %d (%s)", resp.Request.URL.String(), resp.StatusCode, resp.Status)
}
- code := resp.Request.URL.Query().Get("code")
- if code == "" {
- return "", fmt.Errorf("finish login flow: failed to extract token from redirect URL %q", resp.Request.URL.String())
- }
-
// don't waste the connection
_, _ = io.Copy(io.Discard, resp.Body)
- return code, nil
+ last := resp.Request
+ for last != nil && last.URL.Hostname() != r2.URL.Hostname() {
+ code := last.URL.Query().Get("code")
+ if code != "" {
+ return code, nil
+ }
+ last = last.Response.Request
+ }
+ return "", fmt.Errorf("finish login flow: failed to extract token from redirect chain ending in %q", resp.Request.URL.String())
}
// GetNucleusToken generates a Nucleus AuthToken from the active session. Note