Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
608fcdf
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
06cb122
fix(139yun): Address login logic loop defect
UcnacDx2 Jan 14, 2026
460340d
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
dd3c6e6
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
49da04d
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
d8869d6
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
388f064
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
d4bed2b
fix(139yun): Address login logic loop defect
google-labs-jules[bot] Jan 14, 2026
6c1805c
Fix 139 yun login loop
UcnacDx2 Jan 14, 2026
b64aae8
fix(drivers/139): Address multiple issues in 139 driver
google-labs-jules[bot] Jan 14, 2026
4a6d9cb
fix(drivers/139): Use local resty client to fix concurrency issue
google-labs-jules[bot] Jan 14, 2026
b0b94f7
fix(drivers/139): Correctly handle cookies after login
google-labs-jules[bot] Jan 14, 2026
a104620
fix(drivers/139): Address multiple issues in 139 driver
UcnacDx2 Jan 14, 2026
beb2915
Initial plan
Copilot Jan 14, 2026
b870bea
Optimize 139 driver login flow - check step2 params first, simplify code
Copilot Jan 14, 2026
ab40f16
Fix syntax errors in 139 driver optimizations
Copilot Jan 14, 2026
607d4c7
Address code review feedback - remove unused variable and fix log level
Copilot Jan 14, 2026
403ee26
Improve code comments for accuracy and clarity
Copilot Jan 14, 2026
3217aed
Final improvements - trim sid value and clarify comment
Copilot Jan 14, 2026
f5675d9
Add RMKEY check and clarify comment about op.MustSaveDriverStorage
Copilot Jan 14, 2026
ffda3f1
Add early termination for cookie loop performance optimization
Copilot Jan 14, 2026
337b45f
Fix: Always require password login when all three credentials are pre…
Copilot Jan 14, 2026
5ee3721
Add all-or-nothing validation for credentials (MailCookies, Username,…
Copilot Jan 14, 2026
7050f6a
Simplify all-or-nothing credential validation logic
Copilot Jan 14, 2026
da68b63
Optimize 139 driver login flow - eliminate unnecessary HTTP request a…
UcnacDx2 Jan 14, 2026
205e1d6
Merge branch 'main' into main
UcnacDx2 Jan 16, 2026
8985f0b
Merge branch 'main' into main
UcnacDx2 Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions drivers/139/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net/http"
"path"
"strconv"
"strings"
"time"

"github.com/OpenListTeam/OpenList/v4/drivers/base"
Expand Down Expand Up @@ -42,19 +43,39 @@ func (d *Yun139) GetAddition() driver.Additional {

func (d *Yun139) Init(ctx context.Context) error {
if d.ref == nil {
if len(d.Authorization) == 0 {
if d.Username != "" && d.Password != "" {
log.Infof("139yun: authorization is empty, trying to login with password.")
newAuth, err := d.loginWithPassword()
log.Debugf("newAuth: Ok: %s", newAuth)
if err != nil {
return fmt.Errorf("login with password failed: %w", err)
}
} else {
return fmt.Errorf("authorization is empty and username/password is not provided")
// More robust validation for MailCookies
trimmedCookies := strings.TrimSpace(d.MailCookies)
if trimmedCookies != "" {
d.MailCookies = trimmedCookies // Update with trimmed value
if !strings.Contains(d.MailCookies, "=") || len(strings.Split(d.MailCookies, "=")[0]) == 0 {
return fmt.Errorf("MailCookies format is invalid, please check your configuration")
}
}

// Validate all-or-nothing: if any credential is provided, all three must be provided
hasAny := d.MailCookies != "" || d.Username != "" || d.Password != ""
hasAll := d.MailCookies != "" && d.Username != "" && d.Password != ""
if hasAny && !hasAll {
return fmt.Errorf("if any of mail_cookies, username, or password is provided, all three must be provided")
}

// When all three elements (MailCookies, Username, Password) are present,
// always validate credentials with password login to ensure settings are correct.
// This prevents automatic renewal from failing with wrong passwords.
var err error
if hasAll {
log.Infof("139yun: all credentials present, performing password login to validate.")
// Password login validates credentials, updates d.Authorization, and saves via op.MustSaveDriverStorage()
_, err = d.loginWithPassword()
if err != nil {
return fmt.Errorf("login with password failed: %w", err)
}
} else if len(d.Authorization) == 0 {
return fmt.Errorf("authorization is empty and credentials are not provided")
}
err := d.refreshToken()

// Always refresh token for renewal (uses original fallback behavior)
err = d.refreshToken()
if err != nil {
return err
}
Expand Down
212 changes: 176 additions & 36 deletions drivers/139/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,29 +171,24 @@ func (d *Yun139) request(url string, method string, callback base.ReqCallback, r
}
log.Debugf("[139] response body: %s", res.String())
if !e.Success {
// Always try to unmarshal to the specific response type first if 'resp' is provided.
if resp != nil {
err = utils.Json.Unmarshal(res.Body(), resp)
if err != nil {
log.Debugf("[139] failed to unmarshal response to specific type: %v", err)
return nil, err // Return unmarshal error
}
if createBatchOprTaskResp, ok := resp.(*CreateBatchOprTaskResp); ok {
log.Debugf("[139] CreateBatchOprTaskResp.Result.ResultCode: %s", createBatchOprTaskResp.Result.ResultCode)
if createBatchOprTaskResp.Result.ResultCode == "0" {
goto SUCCESS_PROCESS
// Attempt to unmarshal to see if it contains the special success code.
if err := utils.Json.Unmarshal(res.Body(), resp); err == nil {
if taskResp, ok := resp.(*CreateBatchOprTaskResp); ok {
if taskResp.Result.ResultCode == "0" {
return res.Body(), nil
}
}
}
}
return nil, errors.New(e.Message) // Fallback to original error if not handled
return nil, errors.New(e.Message)
}

if resp != nil {
err = utils.Json.Unmarshal(res.Body(), resp)
if err != nil {
if err := utils.Json.Unmarshal(res.Body(), resp); err != nil {
return nil, err
}
}
SUCCESS_PROCESS:
return res.Body(), nil
}

Expand Down Expand Up @@ -761,10 +756,87 @@ func getMd5(dataStr string) string {
return fmt.Sprintf("%x", hash)
}

// sanitizeLoginCookies filters and orders cookies based on a predefined allowlist.
// This is necessary because the login endpoint requires a specific cookie order and
// rejects unknown cookies. The function ensures that only necessary cookies are sent,
// preventing potential login failures due to cookie changes by the service.
func sanitizeLoginCookies(existingCookies string, newJSessionID string) string {
orderedCookieNames := []string{
"behaviorid",
"Os_SSo_Sid",
"_139_index_isLoginType",
"_139_login_version",
"Login_UserNumber",
"cookiepartid8011",
"_139_login_agreement",
"UserData",
"rmUin8011",
"cookiepartid",
"UUIDToken",
"SkinPath28011",
"cbauto",
"areaCode8011",
"cookieLen",
"DEVICE_INFO_DIGEST",
"JSESSIONID",
"loginProcessFlag",
"provCode8011",
"S_DEVICE_TOKEN",
"taskIdCloud",
"UserNowState",
"UserNowState8011",
"ut8011",
}

// Store existing cookies in a map for easy lookup
existingCookiesMap := make(map[string]string)
cookies := strings.Split(existingCookies, ";")
for _, cookie := range cookies {
cookie = strings.TrimSpace(cookie)
parts := strings.SplitN(cookie, "=", 2)
if len(parts) == 2 {
existingCookiesMap[parts[0]] = parts[1]
}
}

var finalCookieParts []string
// Iterate through the ordered names and build the final cookie string
for _, name := range orderedCookieNames {
if name == "JSESSIONID" {
if newJSessionID != "" {
finalCookieParts = append(finalCookieParts, name+"="+newJSessionID)
}
continue
}

if value, ok := existingCookiesMap[name]; ok {
finalCookieParts = append(finalCookieParts, name+"="+value)
}
}

return strings.Join(finalCookieParts, "; ")
}

func (d *Yun139) step1_password_login() (string, error) {
log.Debugf("--- 执行步骤 1: 登录 API ---")
loginURL := "https://mail.10086.cn/Login/Login.ashx"

log.Debugf("--- 执行步骤 1.1: 获取 JSESSIONID ---")
getResp, err := base.RestyClient.R().Get(loginURL)
if err != nil {
return "", fmt.Errorf("step1 get jsessionid failed: %w", err)
}
var jsessionid string
for _, cookie := range getResp.Cookies() {
if cookie.Name == "JSESSIONID" {
jsessionid = cookie.Value
break
}
}
if jsessionid == "" {
log.Warnf("139yun: failed to get JSESSIONID from GET request.")
}

// 密码 SHA1 哈希
hashedPassword := sha1Hash(fmt.Sprintf("fetion.com.cn:%s", d.Password))
log.Debugf("DEBUG: 原始密码: %s", d.Password)
Expand All @@ -773,6 +845,8 @@ func (d *Yun139) step1_password_login() (string, error) {

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

sanitizedCookie := sanitizeLoginCookies(d.MailCookies, jsessionid)

loginHeaders := map[string]string{
"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",
"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",
Expand All @@ -791,7 +865,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": d.MailCookies,
"Cookie": sanitizedCookie,
}

loginData := url.Values{}
Expand All @@ -809,37 +883,42 @@ func (d *Yun139) step1_password_login() (string, error) {
log.Debugf("DEBUG: 登录请求 Body: %s", loginData.Encode())

// 设置客户端不跟随重定向
client := base.RestyClient.SetRedirectPolicy(resty.NoRedirectPolicy())
// Create a new client to avoid race conditions on the global client's redirect policy.
client := resty.New().SetRedirectPolicy(resty.NoRedirectPolicy())
res, err := client.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())
// When NoRedirectPolicy is used, resty returns an error on redirect, but the response should still be available.
if err != nil && !strings.Contains(err.Error(), "auto redirect is disabled") {
return "", fmt.Errorf("step1 login request failed: %w", err)
}
if res == nil {
return "", fmt.Errorf("step1 login request failed: response is nil (error: %v)", err)
}
// 恢复客户端的默认重定向策略,以免影响后续请求
base.RestyClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
log.Debugf("DEBUG: 登录响应 Headers: %+v", res.Header())

var sid, extractedCguid string

// 从 Location 头部提取 sid 和 cguid
// 从 Location 头部提取 sid 和 cguid, 并处理风险控制
locationHeader := res.Header().Get("Location")
if locationHeader != "" {
if ecMatch := regexp.MustCompile(`ec=([^&]+)`).FindStringSubmatch(locationHeader); len(ecMatch) > 1 {
return "", fmt.Errorf("risk control triggered: %s", ecMatch[0])
}

sidMatch := regexp.MustCompile(`sid=([^&]+)`).FindStringSubmatch(locationHeader)
cguidMatch := regexp.MustCompile(`cguid=([^&]+)`).FindStringSubmatch(locationHeader)

if len(sidMatch) > 1 {
sid = sidMatch[1]
log.Debugf("DEBUG: 从 Location 提取到 sid: %s", sid)
} else if strings.Contains(locationHeader, "default.html") {
return "", errors.New("authentication failed: sid is missing in default.html redirect")
}

if len(cguidMatch) > 1 {
extractedCguid = cguidMatch[1]
log.Debugf("DEBUG: 从 Location 提取到 cguid: %s", extractedCguid)
Expand Down Expand Up @@ -867,16 +946,28 @@ func (d *Yun139) step1_password_login() (string, error) {
return "", errors.New("failed to extract sid or cguid from login response")
}

// 提取并记录 cookies
loginUrlObj, _ := url.Parse(loginURL)
cookies := base.RestyClient.GetClient().Jar.Cookies(loginUrlObj)
var cookieStrings []string
// Update cookies from response, merging new ones with existing ones.
existingCookiesMap := make(map[string]string)
// 1. Populate map with existing cookies from the driver.
cookies := strings.Split(d.MailCookies, ";")
for _, cookie := range cookies {
cookieStrings = append(cookieStrings, cookie.Name+"="+cookie.Value)
cookie = strings.TrimSpace(cookie)
parts := strings.SplitN(cookie, "=", 2)
if len(parts) == 2 {
existingCookiesMap[parts[0]] = parts[1]
}
}
// 2. Update map with new cookies from the Set-Cookie headers in the response.
for _, cookie := range res.Cookies() {
existingCookiesMap[cookie.Name] = cookie.Value
}
cookieStr := strings.Join(cookieStrings, "; ")
log.Debugf("DEBUG: 提取到的 Cookies: %s", cookieStr)
d.MailCookies = cookieStr
// 3. Rebuild the cookie string. The order doesn't matter here, as sanitizeLoginCookies will reorder it later if needed.
var finalCookieParts []string
for name, value := range existingCookiesMap {
finalCookieParts = append(finalCookieParts, name+"="+value)
}
d.MailCookies = strings.Join(finalCookieParts, "; ")
log.Debugf("DEBUG: 更新后的 Cookies: %s", d.MailCookies)

return sid, nil
}
Expand Down Expand Up @@ -1230,6 +1321,55 @@ func (d *Yun139) step3_third_party_login(dycpwd string) (string, error) {
return newAuthorization, nil
}

// preAuthLogin attempts to login using existing cookies without making a request to appmail.mail.10086.cn
// It checks if step2 required parameters (Os_SSo_Sid and RMKEY) exist and tries step2 first
// Returns true if pre-auth succeeds, false if it fails (caller should proceed with full password login)
func (d *Yun139) preAuthLogin() (bool, error) {
// Extract sid and check for RMKEY from cookies - both are required for step2
var sid string
hasRMKEY := false
cookies := strings.Split(d.MailCookies, ";")
for _, cookie := range cookies {
cookie = strings.TrimSpace(cookie)
if strings.HasPrefix(cookie, "Os_SSo_Sid=") {
sid = strings.TrimSpace(strings.TrimPrefix(cookie, "Os_SSo_Sid="))
}
if strings.HasPrefix(cookie, "RMKEY=") {
hasRMKEY = true
}
// Early termination once both required cookies are found
if sid != "" && hasRMKEY {
break
}
}

if sid == "" || !hasRMKEY {
log.Warnf("139yun: Os_SSo_Sid or RMKEY not found in cookies, cannot use pre-auth.")
return false, nil
}

log.Infof("139yun: found Os_SSo_Sid in cookies, attempting step2 directly.")
// Try step2 with existing sid
token, err := d.step2_get_single_token(sid)
if err != nil {
log.Warnf("139yun: step2_get_single_token failed with existing sid: %v. sid may be expired.", err)
// sid is expired or invalid, return false so caller can perform full password login
return false, nil
}

// Try step3
newAuth, err := d.step3_third_party_login(token)
if err != nil {
log.Warnf("139yun: step3_third_party_login failed: %v. proceeding with full login.", err)
return false, nil
}

d.Authorization = newAuth
op.MustSaveDriverStorage(d)
log.Infof("139yun: pre-auth login successful using existing sid.")
return true, nil
}

func (d *Yun139) loginWithPassword() (string, error) {
if d.Username == "" || d.Password == "" || d.MailCookies == "" {
return "", errors.New("username, password or mail_cookies is empty")
Expand Down