@@ -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+
766818func (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+
12341416func (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