Skip to content

Commit 991718a

Browse files
UcnacDx2John Doe
authored andcommitted
feat(drivers/139): optimize login flow with cookie reuse and robust fallback
- Cookie Reuse Strategy: Introduced a fast-path login mechanism. If valid MailCookies (containing Os_SSo_Sid) are present, the driver attempts to skip the full password login (Step 1) and directly exchange the SID for a token (Step 2 -> Step 3). This significantly reduces risk control triggers and improves initialization speed. - Authorization Priority: Added a check to skip the entire login process if a valid Authorization string is already present in the configuration. - Robust Fallback: Implemented a fallback mechanism. If the fast-path (cookie reuse) fails (e.g., expired cookie), the driver automatically falls back to the full password login flow (Step 1 -> Step 2 -> Step 3) to ensure service availability. - Credential Validation: Refined validation logic. Now accepts configuration with only Authorization, or only MailCookies (for fast path), while strictly enforcing that if Username or Password is provided, all three credentials (including MailCookies) must be present to support the fallback password login. - Security: Ensured that when falling back to password login, only necessary cookies are sent (via sanitizeLoginCookies) to avoid polluting the request. - Code Cleanup: Removed unused imports and improved code formatting.
1 parent 64d3911 commit 991718a

2 files changed

Lines changed: 224 additions & 49 deletions

File tree

drivers/139/driver.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,11 @@ func (d *Yun139) GetAddition() driver.Additional {
4242

4343
func (d *Yun139) Init(ctx context.Context) error {
4444
if d.ref == nil {
45-
if len(d.Authorization) == 0 {
46-
if d.Username != "" && d.Password != "" {
47-
log.Infof("139yun: authorization is empty, trying to login with password.")
48-
newAuth, err := d.loginWithPassword()
49-
log.Debugf("newAuth: Ok: %s", newAuth)
50-
if err != nil {
51-
return fmt.Errorf("login with password failed: %w", err)
52-
}
53-
} else {
54-
return fmt.Errorf("authorization is empty and username/password is not provided")
55-
}
45+
if err := d.validateAndInitCredentials(); err != nil {
46+
return err
5647
}
48+
49+
// Always refresh token for renewal (uses original fallback behavior)
5750
err := d.refreshToken()
5851
if err != nil {
5952
return err

drivers/139/util.go

Lines changed: 220 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -171,29 +171,23 @@ func (d *Yun139) request(url string, method string, callback base.ReqCallback, r
171171
}
172172
log.Debugf("[139] response body: %s", res.String())
173173
if !e.Success {
174-
// Always try to unmarshal to the specific response type first if 'resp' is provided.
175-
if resp != nil {
176-
err = utils.Json.Unmarshal(res.Body(), resp)
177-
if err != nil {
178-
log.Debugf("[139] failed to unmarshal response to specific type: %v", err)
179-
return nil, err // Return unmarshal error
180-
}
181-
if createBatchOprTaskResp, ok := resp.(*CreateBatchOprTaskResp); ok {
182-
log.Debugf("[139] CreateBatchOprTaskResp.Result.ResultCode: %s", createBatchOprTaskResp.Result.ResultCode)
183-
if createBatchOprTaskResp.Result.ResultCode == "0" {
184-
goto SUCCESS_PROCESS
185-
}
174+
if resp == nil {
175+
return nil, errors.New(e.Message)
176+
}
177+
// Attempt to unmarshal to see if it contains the special success code.
178+
if err := utils.Json.Unmarshal(res.Body(), resp); err == nil {
179+
if taskResp, ok := resp.(*CreateBatchOprTaskResp); ok && taskResp.Result.ResultCode == "0" {
180+
return res.Body(), nil
186181
}
187182
}
188-
return nil, errors.New(e.Message) // Fallback to original error if not handled
183+
return nil, errors.New(e.Message)
189184
}
185+
190186
if resp != nil {
191-
err = utils.Json.Unmarshal(res.Body(), resp)
192-
if err != nil {
187+
if err := utils.Json.Unmarshal(res.Body(), resp); err != nil {
193188
return nil, err
194189
}
195190
}
196-
SUCCESS_PROCESS:
197191
return res.Body(), nil
198192
}
199193

@@ -763,10 +757,84 @@ func getMd5(dataStr string) string {
763757
return fmt.Sprintf("%x", hash)
764758
}
765759

760+
// sanitizeLoginCookies enforces a strict allowlist and order for cookies to prevent login failures.
761+
func sanitizeLoginCookies(existingCookies string, newJSessionID string) string {
762+
orderedCookieNames := []string{
763+
"behaviorid",
764+
"Os_SSo_Sid",
765+
"_139_index_isLoginType",
766+
"_139_login_version",
767+
"Login_UserNumber",
768+
"cookiepartid8011",
769+
"_139_login_agreement",
770+
"UserData",
771+
"rmUin8011",
772+
"cookiepartid",
773+
"UUIDToken",
774+
"SkinPath28011",
775+
"cbauto",
776+
"areaCode8011",
777+
"cookieLen",
778+
"DEVICE_INFO_DIGEST",
779+
"JSESSIONID",
780+
"loginProcessFlag",
781+
"provCode8011",
782+
"S_DEVICE_TOKEN",
783+
"taskIdCloud",
784+
"UserNowState",
785+
"UserNowState8011",
786+
"ut8011",
787+
}
788+
789+
// Store existing cookies in a map for easy lookup
790+
existingCookiesMap := make(map[string]string)
791+
cookies := strings.Split(existingCookies, ";")
792+
for _, cookie := range cookies {
793+
cookie = strings.TrimSpace(cookie)
794+
parts := strings.SplitN(cookie, "=", 2)
795+
if len(parts) == 2 {
796+
existingCookiesMap[parts[0]] = parts[1]
797+
}
798+
}
799+
800+
var finalCookieParts []string
801+
// Iterate through the ordered names and build the final cookie string
802+
for _, name := range orderedCookieNames {
803+
if name == "JSESSIONID" {
804+
if newJSessionID != "" {
805+
finalCookieParts = append(finalCookieParts, name+"="+newJSessionID)
806+
}
807+
continue
808+
}
809+
810+
if value, ok := existingCookiesMap[name]; ok {
811+
finalCookieParts = append(finalCookieParts, name+"="+value)
812+
}
813+
}
814+
815+
return strings.Join(finalCookieParts, "; ")
816+
}
817+
766818
func (d *Yun139) step1_password_login() (string, error) {
767819
log.Debugf("--- 执行步骤 1: 登录 API ---")
768820
loginURL := "https://mail.10086.cn/Login/Login.ashx"
769821

822+
log.Debugf("--- 执行步骤 1.1: 获取 JSESSIONID ---")
823+
getResp, err := base.RestyClient.R().Get(loginURL)
824+
if err != nil {
825+
return "", fmt.Errorf("step1 get jsessionid failed: %w", err)
826+
}
827+
var jsessionid string
828+
for _, cookie := range getResp.Cookies() {
829+
if cookie.Name == "JSESSIONID" {
830+
jsessionid = cookie.Value
831+
break
832+
}
833+
}
834+
if jsessionid == "" {
835+
log.Warnf("139yun: failed to get JSESSIONID from GET request.")
836+
}
837+
770838
// 密码 SHA1 哈希
771839
hashedPassword := sha1Hash(fmt.Sprintf("fetion.com.cn:%s", d.Password))
772840
log.Debugf("DEBUG: 原始密码: %s", d.Password)
@@ -775,6 +843,8 @@ func (d *Yun139) step1_password_login() (string, error) {
775843

776844
cguid := strconv.FormatInt(time.Now().UnixMilli(), 10) // 随机生成 cguid
777845

846+
sanitizedCookie := sanitizeLoginCookies(d.MailCookies, jsessionid)
847+
778848
loginHeaders := map[string]string{
779849
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
780850
"accept-language": "zh-CN,zh;q=0.9,zh-TW;q=0.8,en-US;q=0.7,en;q=0.6,en-GB;q=0.5",
@@ -793,7 +863,7 @@ func (d *Yun139) step1_password_login() (string, error) {
793863
"sec-fetch-user": "?1",
794864
"upgrade-insecure-requests": "1",
795865
"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",
796-
"Cookie": d.MailCookies,
866+
"Cookie": sanitizedCookie,
797867
}
798868

799869
loginData := url.Values{}
@@ -811,37 +881,42 @@ func (d *Yun139) step1_password_login() (string, error) {
811881
log.Debugf("DEBUG: 登录请求 Body: %s", loginData.Encode())
812882

813883
// 设置客户端不跟随重定向
814-
client := base.RestyClient.SetRedirectPolicy(resty.NoRedirectPolicy())
884+
// Create a new client to avoid race conditions on the global client's redirect policy.
885+
client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy())
815886
res, err := client.R().
816887
SetHeaders(loginHeaders).
817888
SetFormDataFromValues(loginData).
818889
Post(loginURL)
819890

820-
if err != nil {
821-
// 如果是重定向错误,则不作为失败处理,因为我们禁止了自动重定向
822-
if res != nil && res.StatusCode() >= 300 && res.StatusCode() < 400 {
823-
log.Debugf("DEBUG: 登录响应 Status Code: %d (Redirect)", res.StatusCode())
824-
} else {
825-
return "", fmt.Errorf("step1 login request failed: %w", err)
826-
}
827-
} else {
828-
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
891+
// When NoRedirectPolicy is used, resty returns an error on redirect, but the response should still be available.
892+
if err != nil && !strings.Contains(err.Error(), "auto redirect is disabled") {
893+
return "", fmt.Errorf("step1 login request failed: %w", err)
829894
}
830-
// 恢复客户端的默认重定向策略,以免影响后续请求
831-
base.RestyClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
895+
if res == nil {
896+
return "", fmt.Errorf("step1 login request failed: response is nil (error: %v)", err)
897+
}
898+
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
832899
log.Debugf("DEBUG: 登录响应 Headers: %+v", res.Header())
833900

834901
var sid, extractedCguid string
835902

836-
// 从 Location 头部提取 sid 和 cguid
903+
// 从 Location 头部提取 sid 和 cguid, 并处理风控
837904
locationHeader := res.Header().Get("Location")
838905
if locationHeader != "" {
906+
if ecMatch := regexp.MustCompile(`ec=([^&]+)`).FindStringSubmatch(locationHeader); len(ecMatch) > 1 {
907+
return "", fmt.Errorf("risk control triggered: %s", ecMatch[0])
908+
}
909+
839910
sidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader)
840911
cguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader)
912+
841913
if len(sidMatch) > 1 {
842914
sid = sidMatch[1]
843915
log.Debugf("DEBUG: 从 Location 提取到 sid: %s", sid)
916+
} else if strings.Contains(locationHeader, "default.html") {
917+
return "", errors.New("authentication failed: sid is missing in default.html redirect")
844918
}
919+
845920
if len(cguidMatch) > 1 {
846921
extractedCguid = cguidMatch[1]
847922
log.Debugf("DEBUG: 从 Location 提取到 cguid: %s", extractedCguid)
@@ -869,16 +944,28 @@ func (d *Yun139) step1_password_login() (string, error) {
869944
return "", errors.New("failed to extract sid or cguid from login response")
870945
}
871946

872-
// 提取并记录 cookies
873-
loginUrlObj, _ := url.Parse(loginURL)
874-
cookies := base.RestyClient.GetClient().Jar.Cookies(loginUrlObj)
875-
var cookieStrings []string
947+
// Update cookies from response, merging new ones with existing ones.
948+
existingCookiesMap := make(map[string]string)
949+
// 1. Populate map with existing cookies from the driver.
950+
cookies := strings.Split(d.MailCookies, ";")
876951
for _, cookie := range cookies {
877-
cookieStrings = append(cookieStrings, cookie.Name+"="+cookie.Value)
952+
cookie = strings.TrimSpace(cookie)
953+
parts := strings.SplitN(cookie, "=", 2)
954+
if len(parts) == 2 {
955+
existingCookiesMap[parts[0]] = parts[1]
956+
}
957+
}
958+
// 2. Update map with new cookies from the Set-Cookie headers in the response.
959+
for _, cookie := range res.Cookies() {
960+
existingCookiesMap[cookie.Name] = cookie.Value
878961
}
879-
cookieStr := strings.Join(cookieStrings, "; ")
880-
log.Debugf("DEBUG: 提取到的 Cookies: %s", cookieStr)
881-
d.MailCookies = cookieStr
962+
// 3. Rebuild the cookie string. The order doesn't matter here, as sanitizeLoginCookies will reorder it later if needed.
963+
var finalCookieParts []string
964+
for name, value := range existingCookiesMap {
965+
finalCookieParts = append(finalCookieParts, name+"="+value)
966+
}
967+
d.MailCookies = strings.Join(finalCookieParts, "; ")
968+
log.Debugf("DEBUG: 更新后的 Cookies: %s", d.MailCookies)
882969

883970
return sid, nil
884971
}
@@ -1231,6 +1318,101 @@ func (d *Yun139) step3_third_party_login(dycpwd string) (string, error) {
12311318
return newAuthorization, nil
12321319
}
12331320

1321+
func (d *Yun139) validateAndInitCredentials() error {
1322+
// More robust validation for MailCookies
1323+
trimmedCookies := strings.TrimSpace(d.MailCookies)
1324+
if trimmedCookies != "" {
1325+
d.MailCookies = trimmedCookies // Update with trimmed value
1326+
if !strings.Contains(d.MailCookies, "=") || len(strings.Split(d.MailCookies, "=")[0]) == 0 {
1327+
return fmt.Errorf("MailCookies format is invalid, please check your configuration")
1328+
}
1329+
}
1330+
1331+
// Priority 1: If Authorization exists, skip login process completely.
1332+
// We assume it's valid for now; validity will be checked by refreshToken() later in Init().
1333+
if d.Authorization != "" {
1334+
log.Debugf("139yun: Authorization exists, skipping initialization login.")
1335+
return nil
1336+
}
1337+
1338+
// Validate all-or-nothing check for username and password
1339+
// "Cookies can exist alone, but if username or password is provided, all three must be provided"
1340+
hasUserOrPass := d.Username != "" || d.Password != ""
1341+
hasAll := d.MailCookies != "" && d.Username != "" && d.Password != ""
1342+
1343+
if hasUserOrPass && !hasAll {
1344+
return fmt.Errorf("if username or password is provided, all three (mail_cookies, username, password) must be provided")
1345+
}
1346+
1347+
// If no Authorization, attempt to generate it.
1348+
// We can try if we have ALL credentials OR if we just have MailCookies (try fast path only)
1349+
if hasAll || d.MailCookies != "" {
1350+
log.Infof("139yun: Authorization missing, attempting login...")
1351+
1352+
success := false
1353+
var sid string
1354+
1355+
// Priority 2: Try fast login using existing cookies (Step 2 -> Step 3)
1356+
// Extract SID from current MailCookies
1357+
cookies := strings.Split(d.MailCookies, ";")
1358+
for _, cookie := range cookies {
1359+
cookie = strings.TrimSpace(cookie)
1360+
// Check for Os_SSo_Sid
1361+
if strings.HasPrefix(cookie, "Os_SSo_Sid=") {
1362+
sid = strings.TrimPrefix(cookie, "Os_SSo_Sid=")
1363+
break
1364+
}
1365+
}
1366+
1367+
// Try Step 2 directly with existing SID and Cookies (using full cookies as implicit context)
1368+
if sid != "" {
1369+
log.Infof("139yun: attempting fast login using existing SID/Cookies (Step 2).")
1370+
token, err := d.step2_get_single_token(sid)
1371+
if err == nil && token != "" {
1372+
log.Infof("139yun: Step 2 success. Proceeding to Step 3.")
1373+
// If Step 2 succeeds, proceed to Step 3
1374+
auth, err := d.step3_third_party_login(token)
1375+
if err == nil {
1376+
d.Authorization = auth
1377+
op.MustSaveDriverStorage(d)
1378+
success = true
1379+
log.Infof("139yun: fast login success (Step 2 -> Step 3).")
1380+
} else {
1381+
log.Warnf("139yun: fast login Step 3 failed: %v", err)
1382+
}
1383+
} else {
1384+
log.Warnf("139yun: fast login Step 2 failed: %v", err)
1385+
}
1386+
} else {
1387+
if d.MailCookies != "" {
1388+
log.Warnf("139yun: Os_SSo_Sid not found in existing cookies. Skipping fast login.")
1389+
}
1390+
}
1391+
1392+
// Priority 3: Fallback to full password login (Step 1 -> Step 2 -> Step 3)
1393+
// Only possible if we have ALL credentials (hasAll == true)
1394+
if !success {
1395+
if hasAll {
1396+
log.Infof("139yun: fast login failed or not possible, performing full password login (Step 1).")
1397+
// loginWithPassword() calls step1_password_login(), which internally strictly uses
1398+
// sanitizeLoginCookies() to ensure only necessary cookies are sent for password login.
1399+
_, err := d.loginWithPassword()
1400+
if err != nil {
1401+
return fmt.Errorf("login with password failed: %w", err)
1402+
}
1403+
} else {
1404+
// If we don't have password, we can't fallback. report error.
1405+
return fmt.Errorf("fast login with cookies failed, and cannot fallback to password login (missing username/password)")
1406+
}
1407+
}
1408+
} else {
1409+
// No Authorization and missing credentials (and even no cookies)
1410+
return fmt.Errorf("authorization is empty and credentials are not provided")
1411+
}
1412+
1413+
return nil
1414+
}
1415+
12341416
func (d *Yun139) loginWithPassword() (string, error) {
12351417
if d.Username == "" || d.Password == "" || d.MailCookies == "" {
12361418
return "", errors.New("username, password or mail_cookies is empty")

0 commit comments

Comments
 (0)