mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2026-10-10 04:53:09 +08:00
fix(drivers/139): optimize login flow with cookie reuse and robust fallback (#2067)
* fix(drivers/139): optimize login flow with robust tests * fix(drivers/139): preserve configured HTTP client settings
This commit is contained in:
+5
-10
@@ -45,16 +45,11 @@ func (d *Yun139) GetAddition() driver.Additional {
|
||||
|
||||
func (d *Yun139) Init(ctx context.Context) error {
|
||||
if d.ref == nil {
|
||||
if len(d.Authorization) == 0 && !d.isShare() {
|
||||
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")
|
||||
// Share links may be browsed without account credentials. All other
|
||||
// storage types need an authorization or a supported login fallback.
|
||||
if !d.isShare() {
|
||||
if err := d.validateAndInitCredentials(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if d.Authorization != "" {
|
||||
|
||||
+5
-4
@@ -7,10 +7,11 @@ import (
|
||||
|
||||
type Addition struct {
|
||||
//Account string `json:"account" required:"true"`
|
||||
Authorization string `json:"authorization" type:"text" required:"true"`
|
||||
Username string `json:"username" required:"true"`
|
||||
Password string `json:"password" required:"true" secret:"true"`
|
||||
MailCookies string `json:"mail_cookies" required:"true" type:"text" help:"Cookies from mail.139.com used for login authentication."`
|
||||
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."`
|
||||
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."`
|
||||
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."`
|
||||
|
||||
+476
-40
@@ -6,9 +6,12 @@ import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
crypto_rand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -27,6 +30,7 @@ import (
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||
"github.com/OpenListTeam/OpenList/v4/internal/op"
|
||||
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||
cookiepkg "github.com/OpenListTeam/OpenList/v4/pkg/cookie"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
|
||||
"github.com/avast/retry-go"
|
||||
@@ -36,8 +40,43 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
KEY_HEX_1 = "73634235495062495331515373756c734e7253306c673d3d" // 第一层 AES 解密密钥
|
||||
KEY_HEX_2 = "7150714477323633586746674c337538" // 第二层 AES 解密密钥
|
||||
KEY_HEX_1 = "73634235495062495331515373756c734e7253306c673d3d" // 第一层 AES 解密密钥
|
||||
KEY_HEX_2 = "7150714477323633586746674c337538" // 第二层 AES 解密密钥
|
||||
mailPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnsOHTFtwW5rq/8gGhPlM5Z3RPdeN/d+FYIHb5JmcfBOCozXuT8c+0anvxtkjzghixwNlnmBuhN8OYfS789YuH/qReQbHC7OdlisLildNWPHRNUYcPa0W3lXSG3+81CXK7FDXPvXo5ubw2GqVbIsccMarI1dyfXdi4ITiCXvmM9wYBdUs9yXtoorhlpyYUI2GV8HNsQjWK9P5QZHT3ox5Qy+mjRmvv6RUFJLPOMkOS/pGZ0DwC1ypFZBxstW0/ftVupdOmGWvW7J2/e3dq3A/UvIkC4YUY/diL1wighJx1G9MiRROISjNMvNyUDSTqPJy516+l3sgHEbc067QIJx2NQIDAQAB"
|
||||
)
|
||||
|
||||
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 (
|
||||
credentialStateAuthorization credentialState = iota
|
||||
credentialStateFullLogin
|
||||
credentialStateCookiesOnly
|
||||
)
|
||||
|
||||
// do others that not defined in Driver interface
|
||||
@@ -86,29 +125,28 @@ func (d *Yun139) refreshToken() error {
|
||||
}
|
||||
decode, err := base64.StdEncoding.DecodeString(d.Authorization)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authorization decode failed: %s", err)
|
||||
return d.loginAfterAuthorizationFailure(fmt.Errorf("authorization decode failed: %w", err))
|
||||
}
|
||||
decodeStr := string(decode)
|
||||
splits := strings.Split(decodeStr, ":")
|
||||
if len(splits) < 3 {
|
||||
return fmt.Errorf("authorization is invalid, splits < 3")
|
||||
return d.loginAfterAuthorizationFailure(errors.New("authorization is invalid, splits < 3"))
|
||||
}
|
||||
d.Account = splits[1]
|
||||
strs := strings.Split(splits[2], "|")
|
||||
if len(strs) < 4 {
|
||||
return fmt.Errorf("authorization is invalid, strs < 4")
|
||||
return d.loginAfterAuthorizationFailure(errors.New("authorization is invalid, strs < 4"))
|
||||
}
|
||||
expiration, err := strconv.ParseInt(strs[3], 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("authorization is invalid")
|
||||
return d.loginAfterAuthorizationFailure(errors.New("authorization expiration is invalid"))
|
||||
}
|
||||
expiration -= time.Now().UnixMilli()
|
||||
if expiration > 1000*60*60*24*15 {
|
||||
// Authorization有效期大于15天无需刷新
|
||||
return nil
|
||||
}
|
||||
if expiration < 0 {
|
||||
return fmt.Errorf("authorization has expired")
|
||||
return d.loginAfterAuthorizationFailure(errors.New("authorization has expired"))
|
||||
}
|
||||
|
||||
url := "https://aas.caiyun.feixin.10086.cn:443/tellin/authTokenRefresh.do"
|
||||
@@ -120,13 +158,7 @@ func (d *Yun139) refreshToken() error {
|
||||
SetResult(&resp).
|
||||
Post(url)
|
||||
if err != nil || resp.Return != "0" {
|
||||
log.Warnf("139yun: failed to refresh token with old token: %v, desc: %s. trying to login with password.", err, resp.Desc)
|
||||
newAuth, loginErr := d.loginWithPassword()
|
||||
log.Debugf("newAuth: Ok: %s", newAuth)
|
||||
if loginErr != nil {
|
||||
return fmt.Errorf("failed to login with password after refresh failed: %w", loginErr)
|
||||
}
|
||||
return nil
|
||||
return d.loginAfterAuthorizationFailure(fmt.Errorf("token refresh failed: %v, desc: %s", err, resp.Desc))
|
||||
}
|
||||
|
||||
d.Authorization = base64.StdEncoding.EncodeToString([]byte(splits[0] + ":" + splits[1] + ":" + resp.Token))
|
||||
@@ -134,6 +166,18 @@ func (d *Yun139) refreshToken() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// loginAfterAuthorizationFailure deliberately skips cookie fast login. Mail
|
||||
// cookies are only reused as device context for the password login request.
|
||||
func (d *Yun139) loginAfterAuthorizationFailure(cause error) error {
|
||||
log.Warnf("139yun: %v; trying password login.", cause)
|
||||
newAuth, err := d.loginWithPassword()
|
||||
log.Debugf("139yun: password fallback generated authorization: %t", newAuth != "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v; password login failed: %w", cause, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Yun139) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
|
||||
req := base.RestyClient.R()
|
||||
randStr := random.String(16)
|
||||
@@ -1116,15 +1160,253 @@ func (d *Yun139) getDiskQuotaDetail(ctx context.Context) (*DiskQuotaDetail, erro
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
type mailLoginResponse struct {
|
||||
Code string `json:"code"`
|
||||
Summary string `json:"summary"`
|
||||
Var struct {
|
||||
LoginSuccessURL string `json:"loginSuccessUrl"`
|
||||
} `json:"var"`
|
||||
}
|
||||
|
||||
func parseMailLoginResponse(body []byte) mailLoginResponse {
|
||||
var response mailLoginResponse
|
||||
if utils.Json.Unmarshal(body, &response) == nil && (response.Code != "" || response.Summary != "" || response.Var.LoginSuccessURL != "") {
|
||||
return response
|
||||
}
|
||||
if match := regexp.MustCompile(`['"]?code['"]?\s*:\s*['"]([^'"]+)`).FindSubmatch(body); len(match) == 2 {
|
||||
response.Code = string(match[1])
|
||||
}
|
||||
if match := regexp.MustCompile(`['"]?summary['"]?\s*:\s*['"]([^'"]+)`).FindSubmatch(body); len(match) == 2 {
|
||||
response.Summary = string(match[1])
|
||||
}
|
||||
if match := regexp.MustCompile(`['"]?loginSuccessUrl['"]?\s*:\s*['"]([^'"]+)`).FindSubmatch(body); len(match) == 2 {
|
||||
response.Var.LoginSuccessURL = string(match[1])
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func mergeMailCookieHeader(existing string, responseCookies []*http.Cookie) string {
|
||||
cookies := cookiepkg.Parse(existing)
|
||||
for _, responseCookie := range responseCookies {
|
||||
if responseCookie != nil && responseCookie.Name != "" {
|
||||
cookies = cookiepkg.SetCookie(cookies, responseCookie.Name, responseCookie.Value)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return ""
|
||||
}
|
||||
return parsed.Query().Get("ec")
|
||||
}
|
||||
|
||||
func smsSceneForRisk(riskCode string) (int, bool) {
|
||||
switch riskCode {
|
||||
case "PML401010062":
|
||||
return 2, true
|
||||
case "MW0016":
|
||||
return 4, true
|
||||
case "S025", "S035":
|
||||
return 1, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func encryptMailLoginName(account string) (string, error) {
|
||||
der, err := base64.StdEncoding.DecodeString(mailPublicKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("decode mail public key: %w", err)
|
||||
}
|
||||
parsed, err := x509.ParsePKIXPublicKey(der)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse mail public key: %w", err)
|
||||
}
|
||||
publicKey, ok := parsed.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return "", errors.New("mail public key is not RSA")
|
||||
}
|
||||
encrypted, err := rsa.EncryptPKCS1v15(crypto_rand.Reader, publicKey, []byte(account))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encrypt mail login name: %w", err)
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
func mailXMLHeaders(cookie string) map[string]string {
|
||||
return map[string]string{
|
||||
"Cookie": cookie,
|
||||
"Content-Type": "application/xml; charset=utf-8",
|
||||
"Accept-Encoding": "gzip",
|
||||
"User-Agent": "okhttp/4.12.0",
|
||||
}
|
||||
}
|
||||
|
||||
func new139RestyClient() *resty.Client {
|
||||
if base.RestyClient != nil {
|
||||
return base.RestyClient.Clone()
|
||||
}
|
||||
return resty.New()
|
||||
}
|
||||
|
||||
func (d *Yun139) sendSMSVerificationCode(riskCode string) error {
|
||||
scene, ok := smsSceneForRisk(riskCode)
|
||||
if !ok {
|
||||
return fmt.Errorf("139 Mail risk control does not support SMS verification: %s", riskCode)
|
||||
}
|
||||
loginName, err := encryptMailLoginName(d.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := strings.Join([]string{
|
||||
"<object>",
|
||||
mailXMLField("loginName", loginName),
|
||||
mailXMLField("fv", "4"),
|
||||
mailXMLField("clientId", "1003"),
|
||||
mailXMLField("eMode", "1"),
|
||||
mailXMLField("loginFailureUrl", ""),
|
||||
mailXMLField("loginSuccessUrl", ""),
|
||||
mailXMLField("verifyCode", ""),
|
||||
mailXMLField("version", "1.0"),
|
||||
mailXMLField("scene", strconv.Itoa(scene)),
|
||||
"</object>",
|
||||
}, "")
|
||||
res, err := new139RestyClient().R().
|
||||
SetHeaders(mailXMLHeaders(d.MailCookies)).
|
||||
SetBody(body).
|
||||
Post(mailSMSURL + "?func=" + url.QueryEscape("login:sendSmsCodeByScene") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
if err != nil {
|
||||
return fmt.Errorf("send 139 Mail SMS verification code: %w", err)
|
||||
}
|
||||
d.MailCookies = mergeMailCookieHeader(d.MailCookies, res.Cookies())
|
||||
response := parseMailLoginResponse(res.Body())
|
||||
if response.Code == "S_OK" {
|
||||
return nil
|
||||
}
|
||||
switch response.Code {
|
||||
case "PML401010021", "PML401010022":
|
||||
return fmt.Errorf("139 Mail requires picture verification before SMS can be sent: %s", response.Code)
|
||||
case "PML404010001":
|
||||
return errors.New("139 Mail SMS verification code was requested too frequently")
|
||||
case "PML401010002":
|
||||
return errors.New("139 Mail rejected the SMS verification parameters")
|
||||
default:
|
||||
return fmt.Errorf("send 139 Mail SMS verification code failed: code=%s summary=%s", response.Code, response.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Yun139) verifySMSCode(riskCode string) (string, error) {
|
||||
if _, ok := smsSceneForRisk(riskCode); !ok {
|
||||
return "", fmt.Errorf("139 Mail risk control does not support SMS verification: %s", riskCode)
|
||||
}
|
||||
loginName, err := encryptMailLoginName(d.Username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
pwdType := ""
|
||||
if riskCode == "MW0016" {
|
||||
pwdType = mailXMLField("pwdType", "1")
|
||||
}
|
||||
body := strings.Join([]string{
|
||||
"<object>",
|
||||
mailXMLField("clientId", "1003"),
|
||||
mailXMLField("version", "4"),
|
||||
mailXMLField("loginType", "0"),
|
||||
mailXMLField("authType", "2"),
|
||||
mailXMLField("loginName", loginName),
|
||||
mailXMLField("eMode", "1"),
|
||||
mailXMLField("loginPassword", sha1Hash("fetion.com.cn:"+d.SmsCode)),
|
||||
mailXMLField("createAutoLoginSecretKey", "1"),
|
||||
mailXMLField("verifyCode", ""),
|
||||
mailXMLField("verifyAgentId", ""),
|
||||
mailXMLField("reqFrom", "3"),
|
||||
mailXMLField("needWCookie", "1"),
|
||||
pwdType,
|
||||
"</object>",
|
||||
}, "")
|
||||
res, err := new139RestyClient().R().
|
||||
SetHeaders(mailXMLHeaders(d.MailCookies)).
|
||||
SetBody(body).
|
||||
Post(mailSMSURL + "?func=" + url.QueryEscape("/login/inlogin.action") + "&cguid=" + strconv.FormatInt(time.Now().UnixMilli(), 10))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("verify 139 Mail SMS code: %w", err)
|
||||
}
|
||||
d.MailCookies = mergeMailCookieHeader(d.MailCookies, res.Cookies())
|
||||
response := parseMailLoginResponse(res.Body())
|
||||
if response.Code != "S_OK" {
|
||||
return "", fmt.Errorf("verify 139 Mail SMS code failed: code=%s summary=%s", response.Code, response.Summary)
|
||||
}
|
||||
sid := ""
|
||||
if response.Var.LoginSuccessURL != "" {
|
||||
if successURL, parseErr := url.Parse(response.Var.LoginSuccessURL); parseErr == nil {
|
||||
sid = successURL.Query().Get("sid")
|
||||
}
|
||||
}
|
||||
if sid == "" {
|
||||
for _, cookie := range cookiepkg.Parse(d.MailCookies) {
|
||||
if cookie.Name == "Os_SSo_Sid" || cookie.Name == "sid" {
|
||||
sid = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if sid == "" {
|
||||
return "", errors.New("139 Mail SMS verification succeeded but did not return sid")
|
||||
}
|
||||
d.SmsCode = ""
|
||||
return sid, nil
|
||||
}
|
||||
|
||||
func (d *Yun139) step1_password_login() (string, error) {
|
||||
log.Debugf("--- 执行步骤 1: 登录 API ---")
|
||||
loginURL := "https://mail.10086.cn/Login/Login.ashx"
|
||||
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))
|
||||
log.Debugf("DEBUG: 原始密码: %s", d.Password)
|
||||
log.Debugf("DEBUG: SHA1 输入: fetion.com.cn:%s", d.Password)
|
||||
log.Debugf("DEBUG: 生成的 Password 哈希: %s", hashedPassword)
|
||||
|
||||
cguid := strconv.FormatInt(time.Now().UnixMilli(), 10) // 随机生成 cguid
|
||||
|
||||
@@ -1146,7 +1428,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": loginCookies,
|
||||
}
|
||||
|
||||
loginData := url.Values{}
|
||||
@@ -1160,11 +1442,10 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
loginData.Set("authType", "2")
|
||||
|
||||
log.Debugf("DEBUG: 登录请求 URL: %s", loginURL)
|
||||
log.Debugf("DEBUG: 登录请求 Headers: %+v", loginHeaders)
|
||||
log.Debugf("DEBUG: 登录请求 Body: %s", loginData.Encode())
|
||||
log.Debugf("DEBUG: 登录请求已准备")
|
||||
|
||||
// 设置客户端不跟随重定向
|
||||
client := base.RestyClient.SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
client := new139RestyClient().SetRedirectPolicy(resty.NoRedirectPolicy())
|
||||
res, err := client.R().
|
||||
SetHeaders(loginHeaders).
|
||||
SetFormDataFromValues(loginData).
|
||||
@@ -1180,24 +1461,36 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
} else {
|
||||
log.Debugf("DEBUG: 登录响应 Status Code: %d", res.StatusCode())
|
||||
}
|
||||
// 恢复客户端的默认重定向策略,以免影响后续请求
|
||||
base.RestyClient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(10))
|
||||
log.Debugf("DEBUG: 登录响应 Headers: %+v", res.Header())
|
||||
log.Debugf("DEBUG: 登录响应 Location present: %t", res.Header().Get("Location") != "")
|
||||
|
||||
var sid, extractedCguid string
|
||||
|
||||
// 从 Location 头部提取 sid 和 cguid
|
||||
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)
|
||||
}
|
||||
if strings.TrimSpace(d.SmsCode) == "" {
|
||||
if sendErr := d.sendSMSVerificationCode(riskCode); sendErr != nil {
|
||||
return "", sendErr
|
||||
}
|
||||
op.MustSaveDriverStorage(d)
|
||||
return "", errors.New("139 Mail SMS verification code sent; fill sms_code and save the storage again")
|
||||
}
|
||||
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: %s", sid)
|
||||
log.Debugf("DEBUG: 从 Location 提取到 sid.")
|
||||
}
|
||||
if len(cguidMatch) > 1 {
|
||||
extractedCguid = cguidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Location 提取到 cguid: %s", extractedCguid)
|
||||
log.Debugf("DEBUG: 从 Location 提取到 cguid.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1209,11 +1502,11 @@ func (d *Yun139) step1_password_login() (string, error) {
|
||||
cookieCguidMatch := regexp.MustCompile(`cguid=([^;]+)`).FindStringSubmatch(cookieStr)
|
||||
if len(ssoSidMatch) > 1 && sid == "" {
|
||||
sid = ssoSidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 sid: %s", sid)
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 sid.")
|
||||
}
|
||||
if len(cookieCguidMatch) > 1 && extractedCguid == "" {
|
||||
extractedCguid = cookieCguidMatch[1]
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 cguid: %s", extractedCguid)
|
||||
log.Debugf("DEBUG: 从 Set-Cookie 提取到 cguid.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1222,16 +1515,7 @@ 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
|
||||
for _, cookie := range cookies {
|
||||
cookieStrings = append(cookieStrings, cookie.Name+"="+cookie.Value)
|
||||
}
|
||||
cookieStr := strings.Join(cookieStrings, "; ")
|
||||
log.Debugf("DEBUG: 提取到的 Cookies: %s", cookieStr)
|
||||
d.MailCookies = cookieStr
|
||||
d.MailCookies = mergeMailCookieHeader(loginCookies, res.Cookies())
|
||||
|
||||
return sid, nil
|
||||
}
|
||||
@@ -1287,6 +1571,16 @@ func (d *Yun139) step2_get_single_token(sid string) (string, error) {
|
||||
return dycpwd, nil
|
||||
}
|
||||
|
||||
func mailXMLField(name, value string) string {
|
||||
return `<string name="` + escapeXML(name) + `">` + escapeXML(value) + `</string>`
|
||||
}
|
||||
|
||||
func escapeXML(value string) string {
|
||||
var escaped bytes.Buffer
|
||||
_ = xml.EscapeText(&escaped, []byte(value))
|
||||
return escaped.String()
|
||||
}
|
||||
|
||||
// --- 辅助函数:加密/解密 ---
|
||||
|
||||
// sha1Hash 计算 SHA1 哈希值,返回十六进制字符串。
|
||||
@@ -1582,6 +1876,148 @@ func (d *Yun139) step3_third_party_login(dycpwd string) (string, error) {
|
||||
return newAuthorization, nil
|
||||
}
|
||||
|
||||
func extractFastLoginCookies(mailCookies string) (sid string, rmkey string) {
|
||||
for _, c := range cookiepkg.Parse(mailCookies) {
|
||||
switch c.Name {
|
||||
case "Os_SSo_Sid":
|
||||
sid = c.Value
|
||||
case "RMKEY":
|
||||
rmkey = c.Value
|
||||
}
|
||||
if sid != "" && rmkey != "" {
|
||||
return sid, rmkey
|
||||
}
|
||||
}
|
||||
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), "=")
|
||||
if ok && strings.TrimSpace(name) != "" && value != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
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 == "" {
|
||||
log.Warnf("139yun: fast login skipped, required cookies missing: Os_SSo_Sid=%t RMKEY=%t", sid != "", rmkey != "")
|
||||
return false
|
||||
}
|
||||
|
||||
log.Infof("139yun: attempting fast login using existing SID/Cookies (Step 2).")
|
||||
token, err := d.step2_get_single_token(sid)
|
||||
if err != nil || token == "" {
|
||||
log.Warnf("139yun: fast login Step 2 failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
log.Infof("139yun: Step 2 success. Proceeding to Step 3.")
|
||||
auth, err := d.step3_third_party_login(token)
|
||||
if err != nil {
|
||||
log.Warnf("139yun: fast login Step 3 failed: %v", err)
|
||||
return false
|
||||
}
|
||||
|
||||
d.Authorization = auth
|
||||
op.MustSaveDriverStorage(d)
|
||||
log.Infof("139yun: fast login success (Step 2 -> Step 3).")
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *Yun139) validateAndInitCredentials() error {
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch state {
|
||||
case credentialStateAuthorization:
|
||||
// Authorization is refreshed by Init immediately after this helper returns.
|
||||
log.Debugf("139yun: Authorization exists, skipping initialization login.")
|
||||
return nil
|
||||
case credentialStateFullLogin, credentialStateCookiesOnly:
|
||||
log.Infof("139yun: Authorization missing, attempting login...")
|
||||
if d.tryFastLoginWithCookies() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if state == credentialStateCookiesOnly {
|
||||
return fmt.Errorf("fast login with cookies failed, and cannot fallback to password login (missing username/password)")
|
||||
}
|
||||
|
||||
log.Infof("139yun: fast login failed or not possible, performing full password login (Step 1).")
|
||||
_, err := d.loginWithPassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("login with password failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported credential state: %d", state)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Yun139) credentialState() (credentialState, error) {
|
||||
d.Authorization = strings.TrimSpace(d.Authorization)
|
||||
d.Username = strings.TrimSpace(d.Username)
|
||||
d.MailCookies = strings.TrimSpace(d.MailCookies)
|
||||
|
||||
if d.Authorization != "" {
|
||||
if strings.HasPrefix(strings.ToLower(d.Authorization), "basic ") {
|
||||
return 0, fmt.Errorf("authorization should not include Basic prefix")
|
||||
}
|
||||
return credentialStateAuthorization, nil
|
||||
}
|
||||
|
||||
if d.MailCookies != "" && !hasCookiePair(d.MailCookies) {
|
||||
return 0, fmt.Errorf("MailCookies format is invalid, please check your configuration")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
return credentialStateFullLogin, nil
|
||||
}
|
||||
|
||||
if hasCookies {
|
||||
return credentialStateCookiesOnly, nil
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("authorization is empty and credentials are not provided")
|
||||
}
|
||||
|
||||
func (d *Yun139) loginWithPassword() (string, error) {
|
||||
if d.Username == "" || d.Password == "" || d.MailCookies == "" {
|
||||
return "", errors.New("username, password or mail_cookies is empty")
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
package _139
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/OpenListTeam/OpenList/v4/drivers/base"
|
||||
"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 TestMergeMailCookiesPreservesExistingOrderAndAppendsNewNames(t *testing.T) {
|
||||
got := mergeMailCookieHeader("z=zv; behaviorid=b; Os_SSo_Sid=old", []*http.Cookie{
|
||||
{Name: "RMKEY", Value: "rm"},
|
||||
{Name: "Os_SSo_Sid", Value: "sid"},
|
||||
{Name: "a", Value: "av"},
|
||||
})
|
||||
want := "z=zv;behaviorid=b;Os_SSo_Sid=sid;RMKEY=rm;a=av"
|
||||
if got != want {
|
||||
t.Fatalf("mergeMailCookieHeader() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractFastLoginCookies(t *testing.T) {
|
||||
sid, rmkey := extractFastLoginCookies("RMKEY=rm; Os_SSo_Sid=sid")
|
||||
if sid != "sid" || rmkey != "rm" {
|
||||
t.Fatalf("extractFastLoginCookies() = %q, %q; want sid, rm", sid, rmkey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCredentialState(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
d Yun139
|
||||
want credentialState
|
||||
err bool
|
||||
}{
|
||||
{
|
||||
name: "authorization",
|
||||
d: Yun139{Addition: Addition{Authorization: " auth "}},
|
||||
want: credentialStateAuthorization,
|
||||
},
|
||||
{
|
||||
name: "full login",
|
||||
d: Yun139{Addition: Addition{
|
||||
MailCookies: "RMKEY=rm; Os_SSo_Sid=sid",
|
||||
Username: "user",
|
||||
Password: "password",
|
||||
}},
|
||||
want: credentialStateFullLogin,
|
||||
},
|
||||
{
|
||||
name: "cookies only",
|
||||
d: Yun139{Addition: Addition{MailCookies: "RMKEY=rm; Os_SSo_Sid=sid"}},
|
||||
want: credentialStateCookiesOnly,
|
||||
},
|
||||
{
|
||||
name: "partial password login",
|
||||
d: Yun139{Addition: Addition{Username: "user"}},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "missing credentials",
|
||||
d: Yun139{},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid cookie",
|
||||
d: Yun139{Addition: Addition{MailCookies: "invalid-cookie"}},
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "authorization with basic prefix",
|
||||
d: Yun139{Addition: Addition{Authorization: "Basic abc"}},
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.d.credentialState()
|
||||
if tt.err {
|
||||
if err == nil {
|
||||
t.Fatal("credentialState() expected error")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("credentialState() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationLoginObtainsAuthorization(t *testing.T) {
|
||||
if os.Getenv("OPENLIST_139_INTEGRATION") != "1" {
|
||||
t.Skip("set OPENLIST_139_INTEGRATION=1 to run live 139Yun login checks")
|
||||
}
|
||||
base.RestyClient = resty.New().
|
||||
SetHeader("user-agent", base.UserAgent).
|
||||
SetRetryCount(3).
|
||||
SetRetryResetReaders(true).
|
||||
SetTimeout(30 * time.Second)
|
||||
|
||||
username := os.Getenv("OPENLIST_139_USERNAME")
|
||||
password := os.Getenv("OPENLIST_139_PASSWORD")
|
||||
mailCookies := os.Getenv("OPENLIST_139_MAIL_COOKIES")
|
||||
authorization := strings.TrimSpace(os.Getenv("OPENLIST_139_AUTHORIZATION"))
|
||||
|
||||
if authorization != "" {
|
||||
t.Run("authorization", func(t *testing.T) {
|
||||
d := Yun139{Addition: Addition{Authorization: authorization}}
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
}
|
||||
if state != credentialStateAuthorization {
|
||||
t.Fatalf("credentialState() = %v, want authorization", state)
|
||||
}
|
||||
if d.Authorization == "" || strings.HasPrefix(strings.ToLower(d.Authorization), "basic ") {
|
||||
t.Fatal("authorization should be present without Basic prefix")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if mailCookies == "" {
|
||||
t.Fatal("OPENLIST_139_MAIL_COOKIES is required")
|
||||
}
|
||||
|
||||
runFastLogin := func(t *testing.T, mailCookies string) {
|
||||
t.Helper()
|
||||
d := Yun139{Addition: Addition{MailCookies: mailCookies}}
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
}
|
||||
if state != credentialStateCookiesOnly {
|
||||
t.Fatalf("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")
|
||||
}
|
||||
token, err := d.step2_get_single_token(sid)
|
||||
if err != nil {
|
||||
t.Fatalf("step2_get_single_token() error: %v", err)
|
||||
}
|
||||
auth, err := d.step3_third_party_login(token)
|
||||
if err != nil {
|
||||
t.Fatalf("step3_third_party_login() error: %v", err)
|
||||
}
|
||||
d.Authorization = auth
|
||||
if d.Authorization == "" {
|
||||
t.Fatal("authorization is empty after fast login")
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" || password == "" {
|
||||
t.Fatal("OPENLIST_139_USERNAME and OPENLIST_139_PASSWORD are required for password fallback")
|
||||
}
|
||||
|
||||
var refreshedMailCookies string
|
||||
var generatedAuthorization string
|
||||
t.Run("password login fallback", func(t *testing.T) {
|
||||
d := Yun139{Addition: Addition{
|
||||
MailCookies: mailCookies,
|
||||
Username: username,
|
||||
Password: password,
|
||||
}}
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
}
|
||||
if state != credentialStateFullLogin {
|
||||
t.Fatalf("credentialState() = %v, want full login", state)
|
||||
}
|
||||
passId, err := d.step1_password_login()
|
||||
if err != nil {
|
||||
t.Fatalf("step1_password_login() error: %v", err)
|
||||
}
|
||||
token, err := d.step2_get_single_token(passId)
|
||||
if err != nil {
|
||||
t.Fatalf("step2_get_single_token() error: %v", err)
|
||||
}
|
||||
auth, err := d.step3_third_party_login(token)
|
||||
if err != nil {
|
||||
t.Fatalf("step3_third_party_login() error: %v", err)
|
||||
}
|
||||
d.Authorization = auth
|
||||
if auth == "" || d.Authorization == "" {
|
||||
t.Fatal("authorization is empty after password login")
|
||||
}
|
||||
generatedAuthorization = auth
|
||||
refreshedMailCookies = d.MailCookies
|
||||
})
|
||||
|
||||
t.Run("authorization generated by password login", func(t *testing.T) {
|
||||
if generatedAuthorization == "" {
|
||||
t.Fatal("password login did not generate authorization")
|
||||
}
|
||||
d := Yun139{Addition: Addition{Authorization: generatedAuthorization}}
|
||||
state, err := d.credentialState()
|
||||
if err != nil {
|
||||
t.Fatalf("credentialState() unexpected error: %v", err)
|
||||
}
|
||||
if state != credentialStateAuthorization {
|
||||
t.Fatalf("credentialState() = %v, want authorization", state)
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(d.Authorization), "basic ") {
|
||||
t.Fatal("authorization should not include Basic prefix")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("mail cookies fast login from input", func(t *testing.T) {
|
||||
sid, rmkey := extractFastLoginCookies(mailCookies)
|
||||
if sid == "" || rmkey == "" {
|
||||
t.Skip("input mail cookies are missing Os_SSo_Sid or RMKEY")
|
||||
}
|
||||
runFastLogin(t, mailCookies)
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
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",
|
||||
MailCookies: "Os_SSo_Sid=sid; RMKEY=rmkey",
|
||||
}}
|
||||
|
||||
err := d.refreshToken()
|
||||
if err == nil || !strings.Contains(err.Error(), "password login failed") {
|
||||
t.Fatalf("refreshToken() error = %v, want password login fallback error", err)
|
||||
}
|
||||
if d.Authorization != "not-base64" {
|
||||
t.Fatalf("Authorization = %q, want original invalid value retained", d.Authorization)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMSSceneForRisk(t *testing.T) {
|
||||
tests := map[string]int{
|
||||
"S025": 1,
|
||||
"S035": 1,
|
||||
"PML401010062": 2,
|
||||
"MW0016": 4,
|
||||
}
|
||||
for riskCode, want := range tests {
|
||||
got, ok := smsSceneForRisk(riskCode)
|
||||
if !ok || got != want {
|
||||
t.Fatalf("smsSceneForRisk(%q) = %d, %t; want %d, true", riskCode, got, ok, want)
|
||||
}
|
||||
}
|
||||
if _, ok := smsSceneForRisk("PICTURE_ONLY"); ok {
|
||||
t.Fatal("smsSceneForRisk() accepted unsupported risk code")
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read request body: %v", err)
|
||||
}
|
||||
if r.URL.Query().Get("func") != "login:sendSmsCodeByScene" {
|
||||
t.Errorf("func = %q", r.URL.Query().Get("func"))
|
||||
}
|
||||
if !strings.Contains(string(body), `<string name="scene">1</string>`) {
|
||||
t.Errorf("request body = %s", body)
|
||||
}
|
||||
if !strings.Contains(r.Header.Get("Cookie"), "device=fingerprint") {
|
||||
t.Errorf("Cookie = %q", r.Header.Get("Cookie"))
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "challenge", Value: "sms"})
|
||||
_, _ = io.WriteString(w, `{"code":"S_OK"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := mailSMSURL
|
||||
mailSMSURL = server.URL
|
||||
defer func() { mailSMSURL = oldURL }()
|
||||
|
||||
d := Yun139{Addition: Addition{Username: "18800000000", MailCookies: "device=fingerprint"}}
|
||||
if err := d.sendSMSVerificationCode("S025"); err != nil {
|
||||
t.Fatalf("sendSMSVerificationCode() error = %v", err)
|
||||
}
|
||||
if !strings.Contains(d.MailCookies, "challenge=sms") {
|
||||
t.Fatalf("MailCookies = %q", d.MailCookies)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendSMSVerificationCodeStopsAtPictureChallenge(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = io.WriteString(w, `{"code":"PML401010021"}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := mailSMSURL
|
||||
mailSMSURL = server.URL
|
||||
defer func() { mailSMSURL = oldURL }()
|
||||
|
||||
d := Yun139{Addition: Addition{Username: "18800000000"}}
|
||||
err := d.sendSMSVerificationCode("S025")
|
||||
if err == nil || !strings.Contains(err.Error(), "requires picture verification") {
|
||||
t.Fatalf("sendSMSVerificationCode() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySMSCode(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read request body: %v", err)
|
||||
}
|
||||
if r.URL.Query().Get("func") != "/login/inlogin.action" {
|
||||
t.Errorf("func = %q", r.URL.Query().Get("func"))
|
||||
}
|
||||
wantHash := sha1Hash("fetion.com.cn:123456")
|
||||
if !strings.Contains(string(body), `<string name="loginPassword">`+wantHash+`</string>`) {
|
||||
t.Errorf("request body does not contain SMS code hash: %s", body)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{Name: "RMKEY", Value: "new-rmkey"})
|
||||
_, _ = io.WriteString(w, `{"code":"S_OK","var":{"loginSuccessUrl":"https://mail.10086.cn/?sid=sms-sid"}}`)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldURL := mailSMSURL
|
||||
mailSMSURL = server.URL
|
||||
defer func() { mailSMSURL = oldURL }()
|
||||
|
||||
d := Yun139{Addition: Addition{
|
||||
Username: "18800000000",
|
||||
SmsCode: "123456",
|
||||
MailCookies: "challenge=sms",
|
||||
}}
|
||||
sid, err := d.verifySMSCode("S025")
|
||||
if err != nil {
|
||||
t.Fatalf("verifySMSCode() error = %v", err)
|
||||
}
|
||||
if sid != "sms-sid" {
|
||||
t.Fatalf("sid = %q", sid)
|
||||
}
|
||||
if d.SmsCode != "" {
|
||||
t.Fatalf("SmsCode = %q, want cleared", d.SmsCode)
|
||||
}
|
||||
if !strings.Contains(d.MailCookies, "RMKEY=new-rmkey") {
|
||||
t.Fatalf("MailCookies = %q", d.MailCookies)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user