mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 04:53:09 +08:00
fix(drivers/139): improve mail login credential renewal (#3029)
* fix(drivers/139): improve mail login credential renewal * fix(drivers/139): guard mail login client initialization Fall back to base.NewRestyClient() when base.RestyClient has not been initialized, while preserving cloned global-client behavior and the login/SMS retry and redirect policies.
This commit is contained in:
+4
-4
@@ -7,11 +7,11 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
//Account string `json:"account" required:"true"`
|
||||
Authorization string `json:"authorization" type:"text" help:"Authorization can be used alone. If empty, use mail_cookies alone for fast login, or mail_cookies + username + password for full login fallback."`
|
||||
Username string `json:"username" help:"Required only when using password login fallback with mail_cookies."`
|
||||
Password string `json:"password" secret:"true" help:"Required only when using password login fallback with mail_cookies."`
|
||||
Authorization string `json:"authorization" type:"text" help:"Authorization can be used alone. If empty, use username + password; mail_cookies is optional and will be established/updated automatically. Existing mail_cookies can also be used alone for fast login."`
|
||||
Username string `json:"username" help:"Use together with password when Authorization is empty. mail_cookies may be left empty on the first login."`
|
||||
Password string `json:"password" secret:"true" help:"Use together with username when Authorization is empty. mail_cookies may be left empty on the first login."`
|
||||
SmsCode string `json:"sms_code" secret:"true" help:"Fill this only after OpenList reports that a 139 Mail SMS verification code was sent, then save the storage again."`
|
||||
MailCookies string `json:"mail_cookies" type:"text" help:"Cookies from mail.10086.cn. Used for fast login only when Authorization is empty; otherwise retained as device context for password login fallback."`
|
||||
MailCookies string `json:"mail_cookies" type:"text" help:"Optional cookies from mail.10086.cn. Leave empty for a first username/password login; cookies created or updated by password/SMS login are persisted and reused as device context. Existing cookies may also be used alone for fast login."`
|
||||
driver.RootID
|
||||
Type string `json:"type" type:"select" options:"personal_new,family,group,personal,share" default:"personal_new"`
|
||||
LinkID string `json:"link_id" type:"text" help:"Multiple shares are separated by commas or new lines. Use link_id#password for password-protected shares."`
|
||||
|
||||
+30
-142
@@ -46,31 +46,10 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
mailRootURL = "https://mail.10086.cn/"
|
||||
mailPasswordURL = "https://mail.10086.cn/Login/Login.ashx"
|
||||
mailSMSURL = "https://mail.10086.cn/s"
|
||||
)
|
||||
|
||||
var mailLoginCookieExclusions = map[string]struct{}{
|
||||
"hecaiyun_stay_time": {},
|
||||
"isShowAgreeIconNew": {},
|
||||
"hecaiyundata2021jssdkcross": {},
|
||||
"random": {},
|
||||
"a_l2": {},
|
||||
"hecaiyun_stay_url": {},
|
||||
"a_l": {},
|
||||
"_139mail_login_type": {},
|
||||
"_139mail_login_shortAddr": {},
|
||||
"sajssdk_2015_cross_new_user": {},
|
||||
"fromhtml5": {},
|
||||
"html5SkinPath8011": {},
|
||||
"Os_SSo_Sid": {},
|
||||
"sid": {},
|
||||
"Login_UserNumber": {},
|
||||
"RMKEY": {},
|
||||
"rtexpired": {},
|
||||
}
|
||||
|
||||
type credentialState int
|
||||
|
||||
const (
|
||||
@@ -1195,28 +1174,6 @@ func mergeMailCookieHeader(existing string, responseCookies []*http.Cookie) stri
|
||||
return cookiepkg.ToString(cookies)
|
||||
}
|
||||
|
||||
func sanitizeMailLoginCookies(existing, newJSessionID string) string {
|
||||
cookies := cookiepkg.Parse(existing)
|
||||
filtered := cookies[:0]
|
||||
for _, cookie := range cookies {
|
||||
if _, excluded := mailLoginCookieExclusions[cookie.Name]; excluded {
|
||||
continue
|
||||
}
|
||||
if cookie.Name == "JSESSIONID" {
|
||||
if newJSessionID == "" {
|
||||
continue
|
||||
}
|
||||
cookie.Value = newJSessionID
|
||||
newJSessionID = ""
|
||||
}
|
||||
filtered = append(filtered, cookie)
|
||||
}
|
||||
if newJSessionID != "" {
|
||||
filtered = append(filtered, &http.Cookie{Name: "JSESSIONID", Value: newJSessionID})
|
||||
}
|
||||
return cookiepkg.ToString(filtered)
|
||||
}
|
||||
|
||||
func mailRiskCode(location string) string {
|
||||
parsed, err := url.Parse(location)
|
||||
if err != nil {
|
||||
@@ -1271,7 +1228,7 @@ func new139RestyClient() *resty.Client {
|
||||
if base.RestyClient != nil {
|
||||
return base.RestyClient.Clone()
|
||||
}
|
||||
return resty.New()
|
||||
return base.NewRestyClient()
|
||||
}
|
||||
|
||||
func (d *Yun139) sendSMSVerificationCode(riskCode string) error {
|
||||
@@ -1296,7 +1253,7 @@ func (d *Yun139) sendSMSVerificationCode(riskCode string) error {
|
||||
mailXMLField("scene", strconv.Itoa(scene)),
|
||||
"</object>",
|
||||
}, "")
|
||||
res, err := new139RestyClient().R().
|
||||
res, err := new139RestyClient().SetRetryCount(0).R().
|
||||
SetHeaders(mailXMLHeaders(d.MailCookies)).
|
||||
SetBody(body).
|
||||
Post(mailSMSURL + "?func=" + url.QueryEscape("login:sendSmsCodeByScene") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
@@ -1349,7 +1306,7 @@ func (d *Yun139) verifySMSCode(riskCode string) (string, error) {
|
||||
pwdType,
|
||||
"</object>",
|
||||
}, "")
|
||||
res, err := new139RestyClient().R().
|
||||
res, err := new139RestyClient().SetRetryCount(0).R().
|
||||
SetHeaders(mailXMLHeaders(d.MailCookies)).
|
||||
SetBody(body).
|
||||
Post(mailSMSURL + "?func=" + url.QueryEscape("/login/inlogin.action") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
@@ -1386,25 +1343,6 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
log.Debugf("--- 执行步骤 1: 登录 API ---")
|
||||
loginURL := mailPasswordURL
|
||||
|
||||
preLogin, err := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy()).R().Get(mailRootURL)
|
||||
if preLogin == nil {
|
||||
return "", fmt.Errorf("step1 pre-login request failed: %v", err)
|
||||
}
|
||||
if err != nil && (preLogin.StatusCode() < 300 || preLogin.StatusCode() >= 400) {
|
||||
return "", fmt.Errorf("step1 pre-login request failed: %w", err)
|
||||
}
|
||||
jsessionid := ""
|
||||
for _, cookie := range preLogin.Cookies() {
|
||||
if cookie.Name == "JSESSIONID" {
|
||||
jsessionid = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
if jsessionid == "" {
|
||||
return "", errors.New("step1 pre-login response did not set JSESSIONID")
|
||||
}
|
||||
loginCookies := sanitizeMailLoginCookies(d.MailCookies, jsessionid)
|
||||
|
||||
// 密码 SHA1 哈希
|
||||
hashedPassword := sha1Hash(fmt.Sprintf("fetion.com.cn:%s", d.Password))
|
||||
|
||||
@@ -1428,7 +1366,7 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
"sec-fetch-user": "?1",
|
||||
"upgrade-insecure-requests": "1",
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
|
||||
"Cookie": loginCookies,
|
||||
"Cookie": d.MailCookies,
|
||||
}
|
||||
|
||||
loginData := url.Values{}
|
||||
@@ -1444,31 +1382,25 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
log.Debugf("DEBUG: 登录请求 URL: %s", loginURL)
|
||||
log.Debugf("DEBUG: 登录请求已准备")
|
||||
|
||||
// 设置客户端不跟随重定向
|
||||
client := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
res, err := client.R().
|
||||
res, err := new139RestyClient().
|
||||
SetRetryCount(0).
|
||||
SetRedirectPolicy(resty.RedirectPolicyFunc(func(_ *http.Request, _ []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
})).R().
|
||||
SetHeaders(loginHeaders).
|
||||
SetFormDataFromValues(loginData).
|
||||
Post(loginURL)
|
||||
|
||||
if err != nil {
|
||||
// 如果是重定向错误,则不作为失败处理,因为我们禁止了自动重定向
|
||||
if res != nil && res.StatusCode() >= 300 && res.StatusCode() < 400 {
|
||||
log.Debugf("DEBUG: 登录响应 Status Code: %d (Redirect)", res.StatusCode())
|
||||
} else {
|
||||
return "", fmt.Errorf("step1 login request failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
|
||||
return "", fmt.Errorf("step1 login request failed: %w", err)
|
||||
}
|
||||
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
|
||||
log.Debugf("DEBUG: 登录响应 Location present: %t", res.Header().Get("Location") != "")
|
||||
|
||||
var sid, extractedCguid string
|
||||
d.MailCookies = mergeMailCookieHeader(d.MailCookies, res.Cookies())
|
||||
|
||||
// 从 Location 头部提取 sid 和 cguid
|
||||
sid := ""
|
||||
locationHeader := res.Header().Get("Location")
|
||||
if locationHeader != "" {
|
||||
d.MailCookies = mergeMailCookieHeader(loginCookies, res.Cookies())
|
||||
if riskCode := mailRiskCode(locationHeader); riskCode != "" {
|
||||
if _, ok := smsSceneForRisk(riskCode); !ok {
|
||||
return "", fmt.Errorf("139 Mail risk control triggered: %s", riskCode)
|
||||
@@ -1482,41 +1414,22 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
}
|
||||
return d.verifySMSCode(riskCode)
|
||||
}
|
||||
sidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader)
|
||||
cguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader)
|
||||
if len(sidMatch) > 1 {
|
||||
sid = sidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Location 提取到 sid.")
|
||||
}
|
||||
if len(cguidMatch) > 1 {
|
||||
extractedCguid = cguidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Location 提取到 cguid.")
|
||||
if redirectURL, parseErr := url.Parse(locationHeader); parseErr == nil {
|
||||
sid = redirectURL.Query().Get("sid")
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 Location 中没有,尝试从 Set-Cookie 中提取
|
||||
if sid == "" || extractedCguid == "" {
|
||||
setCookieHeaders := res.Header().Values("Set-Cookie")
|
||||
for _, cookieStr := range setCookieHeaders {
|
||||
ssoSidMatch := regexp.MustCompile(`Os_SSo_Sid=([^;]+)`).FindStringSubmatch(cookieStr)
|
||||
cookieCguidMatch := regexp.MustCompile(`cguid=([^;]+)`).FindStringSubmatch(cookieStr)
|
||||
if len(ssoSidMatch) > 1 && sid == "" {
|
||||
sid = ssoSidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 sid.")
|
||||
}
|
||||
if len(cookieCguidMatch) > 1 && extractedCguid == "" {
|
||||
extractedCguid = cookieCguidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 cguid.")
|
||||
if sid == "" {
|
||||
for _, cookie := range res.Cookies() {
|
||||
if cookie.Name == "Os_SSo_Sid" || cookie.Name == "sid" {
|
||||
sid = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sid == "" || extractedCguid == "" {
|
||||
return "", errors.New("failed to extract sid or cguid from login response")
|
||||
if sid == "" {
|
||||
return "", errors.New("failed to extract sid from login response")
|
||||
}
|
||||
|
||||
d.MailCookies = mergeMailCookieHeader(loginCookies, res.Cookies())
|
||||
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
@@ -1891,10 +1804,6 @@ func extractFastLoginCookies(mailCookies string) (sid string, rmkey string) {
|
||||
return sid, rmkey
|
||||
}
|
||||
|
||||
func isRedirectStatus(statusCode int) bool {
|
||||
return statusCode >= 300 && statusCode <= 399
|
||||
}
|
||||
|
||||
func hasCookiePair(raw string) bool {
|
||||
for _, part := range strings.Split(raw, ";") {
|
||||
name, value, ok := strings.Cut(strings.TrimSpace(part), "=")
|
||||
@@ -1905,26 +1814,6 @@ func hasCookiePair(raw string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func fetchMailJSessionID(endpoint string) (string, error) {
|
||||
client := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
res, err := client.R().Get(endpoint)
|
||||
if res == nil {
|
||||
return "", fmt.Errorf("pre-login request returned no response: %v", err)
|
||||
}
|
||||
if err != nil && !isRedirectStatus(res.StatusCode()) {
|
||||
return "", fmt.Errorf("pre-login request failed with status %d: %w", res.StatusCode(), err)
|
||||
}
|
||||
if res.StatusCode() >= http.StatusBadRequest {
|
||||
return "", fmt.Errorf("pre-login request failed with status %d", res.StatusCode())
|
||||
}
|
||||
for _, cookie := range res.Cookies() {
|
||||
if cookie.Name == "JSESSIONID" && cookie.Value != "" {
|
||||
return cookie.Value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("pre-login response did not set JSESSIONID")
|
||||
}
|
||||
|
||||
func (d *Yun139) tryFastLoginWithCookies() bool {
|
||||
sid, rmkey := extractFastLoginCookies(d.MailCookies)
|
||||
if sid == "" || rmkey == "" {
|
||||
@@ -1965,7 +1854,7 @@ func (d *Yun139) validateAndInitCredentials() error {
|
||||
return nil
|
||||
case credentialStateFullLogin, credentialStateCookiesOnly:
|
||||
log.Infof("139yun: Authorization missing, attempting login...")
|
||||
if d.tryFastLoginWithCookies() {
|
||||
if d.MailCookies != "" && d.tryFastLoginWithCookies() {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2002,16 +1891,15 @@ func (d *Yun139) credentialState() (credentialState, error) {
|
||||
|
||||
hasUsername := d.Username != ""
|
||||
hasPassword := strings.TrimSpace(d.Password) != ""
|
||||
hasCookies := d.MailCookies != ""
|
||||
|
||||
if hasUsername || hasPassword {
|
||||
if !hasUsername || !hasPassword || !hasCookies {
|
||||
return 0, fmt.Errorf("if username or password is provided, all three (mail_cookies, username, password) must be provided")
|
||||
}
|
||||
if hasUsername != hasPassword {
|
||||
return 0, fmt.Errorf("username and password must be provided together")
|
||||
}
|
||||
if hasUsername {
|
||||
return credentialStateFullLogin, nil
|
||||
}
|
||||
|
||||
if hasCookies {
|
||||
if d.MailCookies != "" {
|
||||
return credentialStateCookiesOnly, nil
|
||||
}
|
||||
|
||||
@@ -2019,8 +1907,8 @@ func (d *Yun139) credentialState() (credentialState, error) {
|
||||
}
|
||||
|
||||
func (d *Yun139) loginWithPassword() (string, error) {
|
||||
if d.Username == "" || d.Password == "" || d.MailCookies == "" {
|
||||
return "", errors.New("username, password or mail_cookies is empty")
|
||||
if d.Username == "" || d.Password == "" {
|
||||
return "", errors.New("username or password is empty")
|
||||
}
|
||||
|
||||
passId, err := d.step1_password_login()
|
||||
|
||||
+124
-58
@@ -1,6 +1,7 @@
|
||||
package _139
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -13,12 +14,13 @@ import (
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func TestSanitizeLoginCookiesDropsStaleJSessionIDWhenFreshOneMissing(t *testing.T) {
|
||||
got := sanitizeMailLoginCookies("JSESSIONID=old; behaviorid=b", "")
|
||||
want := "behaviorid=b"
|
||||
if got != want {
|
||||
t.Fatalf("sanitizeMailLoginCookies() = %q, want %q", got, want)
|
||||
}
|
||||
func useRetryingTestClient(t *testing.T) {
|
||||
t.Helper()
|
||||
oldClient := base.RestyClient
|
||||
base.RestyClient = resty.New().SetRetryCount(3)
|
||||
t.Cleanup(func() {
|
||||
base.RestyClient = oldClient
|
||||
})
|
||||
}
|
||||
|
||||
func TestMergeMailCookiesPreservesExistingOrderAndAppendsNewNames(t *testing.T) {
|
||||
@@ -61,6 +63,14 @@ func TestCredentialState(t *testing.T) {
|
||||
}},
|
||||
want: credentialStateFullLogin,
|
||||
},
|
||||
{
|
||||
name: "full login without initial cookies",
|
||||
d: Yun139{Addition: Addition{
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
}},
|
||||
want: credentialStateFullLogin,
|
||||
},
|
||||
{
|
||||
name: "cookies only",
|
||||
d: Yun139{Addition: Addition{MailCookies: "RMKEY=rm; Os_SSo_Sid=sid"}},
|
||||
@@ -142,32 +152,31 @@ func TestIntegrationLoginObtainsAuthorization(t *testing.T) {
|
||||
t.Fatal("OPENLIST_139_MAIL_COOKIES is required")
|
||||
}
|
||||
|
||||
runFastLogin := func(t *testing.T, mailCookies string) {
|
||||
t.Helper()
|
||||
runFastLogin := func(mailCookies string) error {
|
||||
d := Yun139{Addition: Addition{MailCookies: mailCookies}}
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
return fmt.Errorf("credentialState() error: %w", err)
|
||||
}
|
||||
if state != credentialStateCookiesOnly {
|
||||
t.Fatalf("credentialState() = %v, want cookies only", state)
|
||||
return fmt.Errorf("credentialState() = %v, want cookies only", state)
|
||||
}
|
||||
sid, rmkey := extractFastLoginCookies(d.MailCookies)
|
||||
if sid == "" || rmkey == "" {
|
||||
t.Fatal("mail cookies are missing Os_SSo_Sid or RMKEY")
|
||||
return fmt.Errorf("mail cookies are missing Os_SSo_Sid or RMKEY")
|
||||
}
|
||||
token, err := d.step2_get_single_token(sid)
|
||||
if err != nil {
|
||||
t.Fatalf("step2_get_single_token() error: %v", err)
|
||||
return fmt.Errorf("step2_get_single_token() error: %w", err)
|
||||
}
|
||||
auth, err := d.step3_third_party_login(token)
|
||||
if err != nil {
|
||||
t.Fatalf("step3_third_party_login() error: %v", err)
|
||||
return fmt.Errorf("step3_third_party_login() error: %w", err)
|
||||
}
|
||||
d.Authorization = auth
|
||||
if d.Authorization == "" {
|
||||
t.Fatal("authorization is empty after fast login")
|
||||
if auth == "" {
|
||||
return fmt.Errorf("authorization is empty after fast login")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
@@ -231,46 +240,21 @@ func TestIntegrationLoginObtainsAuthorization(t *testing.T) {
|
||||
if sid == "" || rmkey == "" {
|
||||
t.Skip("input mail cookies are missing Os_SSo_Sid or RMKEY")
|
||||
}
|
||||
runFastLogin(t, mailCookies)
|
||||
if err := runFastLogin(mailCookies); err != nil {
|
||||
t.Skipf("input mail cookies are stale or unusable: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mail cookies fast login after password login", func(t *testing.T) {
|
||||
if refreshedMailCookies == "" {
|
||||
t.Fatal("password login did not refresh mail cookies")
|
||||
}
|
||||
runFastLogin(t, refreshedMailCookies)
|
||||
if err := runFastLogin(refreshedMailCookies); err != nil {
|
||||
t.Fatalf("refreshed mail cookies fast login failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIsRedirectStatus(t *testing.T) {
|
||||
for _, status := range []int{300, 301, 302, 307, 399} {
|
||||
if !isRedirectStatus(status) {
|
||||
t.Fatalf("isRedirectStatus(%d) = false, want true", status)
|
||||
}
|
||||
}
|
||||
for _, status := range []int{200, 299, 400, 500} {
|
||||
if isRedirectStatus(status) {
|
||||
t.Fatalf("isRedirectStatus(%d) = true, want false", status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchMailJSessionIDAcceptsRedirectResponse(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.SetCookie(w, &http.Cookie{Name: "JSESSIONID", Value: "fresh"})
|
||||
http.Redirect(w, r, "/next", http.StatusFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
got, err := fetchMailJSessionID(server.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("fetchMailJSessionID() error: %v", err)
|
||||
}
|
||||
if got != "fresh" {
|
||||
t.Fatalf("fetchMailJSessionID() = %q, want fresh", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidAuthorizationDoesNotUseCookieFastLogin(t *testing.T) {
|
||||
d := Yun139{Addition: Addition{
|
||||
Authorization: "not-base64",
|
||||
@@ -304,18 +288,9 @@ func TestSMSSceneForRisk(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeMailLoginCookiesKeepsDeviceContext(t *testing.T) {
|
||||
got := sanitizeMailLoginCookies(
|
||||
"behaviorid=device; Os_SSo_Sid=old-sid; RMKEY=old-rmkey; JSESSIONID=old-session; S_DEVICE_TOKEN=fingerprint",
|
||||
"new-session",
|
||||
)
|
||||
want := "behaviorid=device;JSESSIONID=new-session;S_DEVICE_TOKEN=fingerprint"
|
||||
if got != want {
|
||||
t.Fatalf("sanitizeMailLoginCookies() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSMSVerificationCode(t *testing.T) {
|
||||
useRetryingTestClient(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -349,6 +324,8 @@ func TestSendSMSVerificationCode(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSendSMSVerificationCodeStopsAtPictureChallenge(t *testing.T) {
|
||||
useRetryingTestClient(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"code":"PML401010021"}`)
|
||||
}))
|
||||
@@ -366,6 +343,8 @@ func TestSendSMSVerificationCodeStopsAtPictureChallenge(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestVerifySMSCode(t *testing.T) {
|
||||
useRetryingTestClient(t)
|
||||
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
@@ -406,3 +385,90 @@ func TestVerifySMSCode(t *testing.T) {
|
||||
t.Fatalf("MailCookies = %q", d.MailCookies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordLoginWithoutInitialCookiesStopsAtRedirectAndPersistsCookies(t *testing.T) {
|
||||
useRetryingTestClient(t)
|
||||
|
||||
var loginRequests, redirectRequests int
|
||||
var server *httptest.Server
|
||||
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/Login/Login.ashx":
|
||||
loginRequests++
|
||||
http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "new-rm"})
|
||||
http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "new-sid"})
|
||||
w.Header().Set("Location", server.URL+"/appmail?sid=single-sid")
|
||||
w.WriteHeader(http.StatusFound)
|
||||
case "/appmail":
|
||||
redirectRequests++
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := mailPasswordURL
|
||||
mailPasswordURL = server.URL + "/Login/Login.ashx"
|
||||
defer func() { mailPasswordURL = oldURL }()
|
||||
|
||||
d := Yun139{Addition: Addition{Username: "18800000000", Password: "password"}}
|
||||
sid, err := d.step1_password_login()
|
||||
if err != nil || sid != "single-sid" {
|
||||
t.Fatalf("sid=%q err=%v", sid, err)
|
||||
}
|
||||
if loginRequests != 1 || redirectRequests != 0 {
|
||||
t.Fatalf("login/redirect requests=%d/%d, want 1/0", loginRequests, redirectRequests)
|
||||
}
|
||||
if !strings.Contains(d.MailCookies, "RMKEY=new-rm") || !strings.Contains(d.MailCookies, "Os_SSo_Sid=new-sid") {
|
||||
t.Fatalf("response cookies were not persisted: %q", d.MailCookies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordLoginReusesRawRefreshedCookies(t *testing.T) {
|
||||
useRetryingTestClient(t)
|
||||
|
||||
var n int
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n++
|
||||
cookie := r.Header.Get("Cookie")
|
||||
if !strings.Contains(cookie, "JSESSIONID=stale") || !strings.Contains(cookie, "behaviorid=device") {
|
||||
t.Errorf("login %d lost raw cookie context: %q", n, cookie)
|
||||
}
|
||||
if n == 1 {
|
||||
http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "rm-1"})
|
||||
http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "sid-1"})
|
||||
w.Header().Set("Location", "https://mail.10086.cn/?sid=first")
|
||||
} else {
|
||||
if !strings.Contains(cookie, "RMKEY=rm-1") || !strings.Contains(cookie, "Os_SSo_Sid=sid-1") {
|
||||
t.Errorf("second login did not reuse first refreshed cookies: %q", cookie)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "rm-2"})
|
||||
http.SetCookie(w, &http.Cookie{Name: "Os_SSo_Sid", Value: "sid-2"})
|
||||
w.Header().Set("Location", "https://mail.10086.cn/?sid=second")
|
||||
}
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := mailPasswordURL
|
||||
mailPasswordURL = server.URL
|
||||
defer func() { mailPasswordURL = oldURL }()
|
||||
|
||||
d := Yun139{Addition: Addition{Username: "18800000000", Password: "password", MailCookies: "Os_SSo_Sid=old; RMKEY=old; JSESSIONID=stale; behaviorid=device"}}
|
||||
if sid, err := d.step1_password_login(); err != nil || sid != "first" {
|
||||
t.Fatalf("first login sid=%q err=%v", sid, err)
|
||||
}
|
||||
refreshed := d.MailCookies
|
||||
if !strings.Contains(refreshed, "RMKEY=rm-1") || !strings.Contains(refreshed, "Os_SSo_Sid=sid-1") {
|
||||
t.Fatalf("first login did not persist refreshed cookies: %q", refreshed)
|
||||
}
|
||||
d.Authorization = ""
|
||||
d.MailCookies = refreshed
|
||||
if sid, err := d.step1_password_login(); err != nil || sid != "second" {
|
||||
t.Fatalf("second login sid=%q err=%v", sid, err)
|
||||
}
|
||||
if n != 2 {
|
||||
t.Fatalf("login count=%d, want 2", n)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user