diff --git a/cloudpan/api_constant.go b/cloudpan/api_constant.go index 67f7e4f..65b2ac4 100644 --- a/cloudpan/api_constant.go +++ b/cloudpan/api_constant.go @@ -17,7 +17,6 @@ package cloudpan const ( WEB_URL string = "https://cloud.189.cn" AUTH_URL string = "https://open.e.189.cn/api/logbox/oauth2" - API_URL string = "https://api.cloud.189.cn" ACCOUNT_TYPE = "02" APP_ID = "8025431004" @@ -32,3 +31,8 @@ const ( CHANNEL_ID = "web_cloud.189.cn" ) + +// API_URL is declared as a var (rather than const) so that tests can inject a +// httptest mock-server URL by reassigning cloudpan.API_URL. Its runtime value is +// unchanged. +var API_URL string = "https://api.cloud.189.cn" diff --git a/cloudpan/app_login.go b/cloudpan/app_login.go index af5c646..a1bf380 100644 --- a/cloudpan/app_login.go +++ b/cloudpan/app_login.go @@ -33,59 +33,59 @@ import ( type ( appLoginParams struct { CaptchaToken string - Lt string - ReturnUrl string - ParamId string - ReqId string - jRsaKey string + Lt string + ReturnUrl string + ParamId string + ReqId string + jRsaKey string } AppLoginToken struct { - SessionKey string `json:"sessionKey"` - SessionSecret string `json:"sessionSecret"` - FamilySessionKey string `json:"familySessionKey"` + SessionKey string `json:"sessionKey"` + SessionSecret string `json:"sessionSecret"` + FamilySessionKey string `json:"familySessionKey"` FamilySessionSecret string `json:"familySessionSecret"` - AccessToken string `json:"accessToken"` - RefreshToken string `json:"refreshToken"` + AccessToken string `json:"accessToken"` + RefreshToken string `json:"refreshToken"` // 有效期的token SskAccessToken string `json:"sskAccessToken"` // token 过期时间点,时间戳ms - SskAccessTokenExpiresIn int64 `json:"sskAccessTokenExpiresIn"` - RsaPublicKey string `json:"rsaPublicKey"` + SskAccessTokenExpiresIn int64 `json:"sskAccessTokenExpiresIn"` + RsaPublicKey string `json:"rsaPublicKey"` } appSessionResp struct { - ResCode int `json:"res_code"` - ResMessage string `json:"res_message"` - AccessToken string `json:"accessToken"` - FamilySessionKey string `json:"familySessionKey"` + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + AccessToken string `json:"accessToken"` + FamilySessionKey string `json:"familySessionKey"` FamilySessionSecret string `json:"familySessionSecret"` - GetFileDiffSpan int `json:"getFileDiffSpan"` - GetUserInfoSpan int `json:"getUserInfoSpan"` - IsSaveName string `json:"isSaveName"` - KeepAlive int `json:"keepAlive"` - LoginName string `json:"loginName"` - RefreshToken string `json:"refreshToken"` - SessionKey string `json:"sessionKey"` - SessionSecret string `json:"sessionSecret"` + GetFileDiffSpan int `json:"getFileDiffSpan"` + GetUserInfoSpan int `json:"getUserInfoSpan"` + IsSaveName string `json:"isSaveName"` + KeepAlive int `json:"keepAlive"` + LoginName string `json:"loginName"` + RefreshToken string `json:"refreshToken"` + SessionKey string `json:"sessionKey"` + SessionSecret string `json:"sessionSecret"` } accessTokenResp struct { // token过期时间,默认30天 - ExpiresIn int64 `json:"expiresIn"` + ExpiresIn int64 `json:"expiresIn"` AccessToken string `json:"accessToken"` } - + appRefreshUserSessionResp struct { - XMLName xml.Name `xml:"userSession"` - LoginName string `xml:"loginName"` - SessionKey string `xml:"sessionKey"` - SessionSecret string `xml:"sessionSecret"` - KeepAlive int `xml:"keepAlive"` - GetFileDiffSpan int `xml:"getFileDiffSpan"` - GetUserInfoSpan int `xml:"getUserInfoSpan"` - FamilySessionKey string `xml:"familySessionKey"` - FamilySessionSecret string `xml:"familySessionSecret"` + XMLName xml.Name `xml:"userSession"` + LoginName string `xml:"loginName"` + SessionKey string `xml:"sessionKey"` + SessionSecret string `xml:"sessionSecret"` + KeepAlive int `xml:"keepAlive"` + GetFileDiffSpan int `xml:"getFileDiffSpan"` + GetUserInfoSpan int `xml:"getUserInfoSpan"` + FamilySessionKey string `xml:"familySessionKey"` + FamilySessionSecret string `xml:"familySessionSecret"` } ) @@ -109,29 +109,29 @@ func AppLogin(username, password string) (result *AppLoginToken, error *apierror rsaPassword, _ := crypto.RsaEncrypt([]byte(rsaKey.String()), []byte(password)) urlStr := "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do" - headers := map[string]string { - "Content-Type": "application/x-www-form-urlencoded", - "Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do", - "Cookie": "LT=" + loginParams.Lt, + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + "Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do", + "Cookie": "LT=" + loginParams.Lt, "X-Requested-With": "XMLHttpRequest", - "REQID": loginParams.ReqId, - "lt": loginParams.Lt, - } - formData := map[string]string { - "appKey": "8025431004", - "accountType": "02", - "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaUserName))), - "password": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaPassword))), + "REQID": loginParams.ReqId, + "lt": loginParams.Lt, + } + formData := map[string]string{ + "appKey": "8025431004", + "accountType": "02", + "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaUserName))), + "password": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaPassword))), "validateCode": "", "captchaToken": loginParams.CaptchaToken, - "returnUrl": loginParams.ReturnUrl, - "mailSuffix": "@189.cn", + "returnUrl": loginParams.ReturnUrl, + "mailSuffix": "@189.cn", "dynamicCheck": "FALSE", - "clientType": "10020", - "cb_SaveName": "1", - "isOauth2": "false", - "state": "", - "paramId": loginParams.ParamId, + "clientType": "10020", + "cb_SaveName": "1", + "isOauth2": "false", + "state": "", + "paramId": loginParams.ParamId, } logger.Verboseln("do request url: " + urlStr) @@ -147,13 +147,43 @@ func AppLogin(username, password string) (result *AppLoginToken, error *apierror return nil, apierror.NewFailedApiError(err.Error()) } if r.Result != 0 || r.ToUrl == "" { - return nil, apierror.NewFailedApiError("登录失败") + errMsg := r.Msg + if errMsg == "" { + errMsg = "登录失败" + } + return nil, apierror.NewFailedApiError(errMsg) + } + + // 访问ToUrl获取COOKIE_LOGIN_USER cookie + toUrlHeaders := map[string]string{ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "zh-CN,zh;q=0.9", + } + logger.Verboseln("访问ToUrl获取cookie: " + r.ToUrl) + _, err1 = appClient.Fetch("GET", r.ToUrl, nil, toUrlHeaders) + if err1 != nil { + logger.Verboseln("访问ToUrl error: ", err1.Error()) + } + + // 从cookie jar中获取COOKIE_LOGIN_USER + cloudpanUrl := &url.URL{ + Scheme: "http", + Host: "cloud.189.cn", + Path: "/", + } + cks := appClient.Jar.Cookies(cloudpanUrl) + for _, cookie := range cks { + if cookie.Name == "COOKIE_LOGIN_USER" { + logger.Verboseln("获取到COOKIE_LOGIN_USER: ", cookie.Value) + // 这里不返回,继续后续流程 + break + } } fullUrl := &strings.Builder{} fmt.Fprintf(fullUrl, "%s/getSessionForPC.action?clientType=%s&version=%s&channelId=%s&redirectURL=%s", API_URL, "TELEMAC", "1.0.0", "web_cloud.189.cn", url.QueryEscape(r.ToUrl)) - headers = map[string]string { + headers = map[string]string{ "Accept": "application/json;charset=UTF-8", } logger.Verboseln("do request url: " + fullUrl.String()) @@ -183,16 +213,16 @@ func AppLogin(username, password string) (result *AppLoginToken, error *apierror fmt.Fprintf(fullUrl, "%s/open/oauth2/getAccessTokenBySsKey.action?sessionKey=%s", API_URL, rs.SessionKey) timestamp := apiutil.Timestamp() - signParams := map[string]string { - "Timestamp": strconv.Itoa(timestamp), + signParams := map[string]string{ + "Timestamp": strconv.Itoa(timestamp), "sessionKey": rs.SessionKey, - "AppKey": "601102120", + "AppKey": "601102120", } - headers = map[string]string { - "AppKey": "601102120", + headers = map[string]string{ + "AppKey": "601102120", "Signature": apiutil.SignatureOfMd5(signParams), "Sign-Type": "1", - "Accept": "application/json", + "Accept": "application/json", "Timestamp": strconv.Itoa(timestamp), } body, err1 = appClient.Fetch("GET", fullUrl.String(), nil, headers) @@ -212,7 +242,7 @@ func AppLogin(username, password string) (result *AppLoginToken, error *apierror } func appGetLoginParams() (params appLoginParams, error *apierror.ApiError) { - header := map[string]string { + header := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", } fullUrl := &strings.Builder{} @@ -227,23 +257,35 @@ func appGetLoginParams() (params appLoginParams, error *apierror.ApiError) { } content := string(data) - re, _ := regexp.Compile("captchaToken' value='(.+?)'") - params.CaptchaToken = re.FindStringSubmatch(content)[1] + match := regexp.MustCompile("name='captchaToken' value='(.+?)'").FindStringSubmatch(content) + if len(match) > 1 { + params.CaptchaToken = match[1] + } - re, _ = regexp.Compile("lt = \"(.+?)\"") - params.Lt = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("var lt = \"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.Lt = match[1] + } - re, _ = regexp.Compile("returnUrl = '(.+?)'") - params.ReturnUrl = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("returnUrl = '(.+?)'").FindStringSubmatch(content) + if len(match) > 1 { + params.ReturnUrl = match[1] + } - re, _ = regexp.Compile("paramId = \"(.+?)\"") - params.ParamId = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("var paramId = \"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.ParamId = match[1] + } - re, _ = regexp.Compile("reqId = \"(.+?)\"") - params.ReqId = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("var reqId = \"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.ReqId = match[1] + } - re, _ = regexp.Compile("j_rsaKey\" value=\"(.+?)\"") - params.jRsaKey = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("id=\"j_rsaKey\" value=\"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.jRsaKey = match[1] + } return } @@ -252,7 +294,7 @@ func getSessionByAccessToken(accessToken string) (*appRefreshUserSessionResp, *a fullUrl := &strings.Builder{} fmt.Fprintf(fullUrl, "%s/getSessionForPC.action?appId=%s&accessToken=%s&clientSn=%s&%s", API_URL, "8025431004", accessToken, apiutil.Uuid(), apiutil.PcClientInfoSuffixParam()) - headers := map[string]string { + headers := map[string]string{ "X-Request-ID": apiutil.XRequestId(), } logger.Verboseln("do request url: " + fullUrl.String()) @@ -268,4 +310,21 @@ func getSessionByAccessToken(accessToken string) (*appRefreshUserSessionResp, *a return nil, apierror.NewApiErrorWithError(err) } return item, nil -} \ No newline at end of file +} + +// RefreshUserSessionByAccessToken 用 accessToken 刷新 PC session(非密码登录)。 +// 接通原死代码 getSessionByAccessToken,返回含新 sessionKey/sessionSecret/family* 的 AppLoginToken; +// accessToken 本身不变(在其有效期内可重复刷新),供 PanUser.RefreshAppSession 主动续期。 +func RefreshUserSessionByAccessToken(accessToken string) (*AppLoginToken, *apierror.ApiError) { + item, err := getSessionByAccessToken(accessToken) + if err != nil { + return nil, err + } + return &AppLoginToken{ + SessionKey: item.SessionKey, + SessionSecret: item.SessionSecret, + FamilySessionKey: item.FamilySessionKey, + FamilySessionSecret: item.FamilySessionSecret, + AccessToken: accessToken, + }, nil +} diff --git a/cloudpan/login.go b/cloudpan/login.go index 529ba9a..08ee797 100644 --- a/cloudpan/login.go +++ b/cloudpan/login.go @@ -42,15 +42,15 @@ const ( type ( loginParams struct { CaptchaToken string - Lt string - ReturnUrl string - ParamId string + Lt string + ReturnUrl string + ParamId string } loginResult struct { - Result int `json:"result"` - Msg string `json:"msg"` - ToUrl string `json:"toUrl"` + Result int `json:"result"` + Msg string `json:"msg"` + ToUrl string `json:"toUrl"` } WebLoginToken struct { @@ -96,12 +96,20 @@ func LoginWithCaptcha(username, password, captchaCode string) (webToken *WebLogi r, err := doLoginAct(username, password, captchaCode, latestLoginParams.CaptchaToken, latestLoginParams.ReturnUrl, latestLoginParams.ParamId, latestLoginParams.Lt) - if err != nil || r.Msg != "登录成功" { + if err != nil { logger.Verboseln("login failed ", err) return webToken, apierror.NewFailedApiError(err.Error()) } + if r == nil || r.Msg != "登录成功" { + msg := "登录失败" + if r != nil { + msg = r.Msg + } + logger.Verboseln("login failed: ", msg) + return webToken, apierror.NewFailedApiError(msg) + } // request toUrl to get COOKIE_LOGIN_USER cookie - header := map[string]string { + header := map[string]string{ "lt": latestLoginParams.Lt, "Content-Type": "application/x-www-form-urlencoded", "Referer": "https://open.e.189.cn/", @@ -111,7 +119,7 @@ func LoginWithCaptcha(username, password, captchaCode string) (webToken *WebLogi cloudpanUrl := &url.URL{ Scheme: "http", Host: "cloud.189.cn", - Path: "/", + Path: "/", } cks := client.Jar.Cookies(cloudpanUrl) for _, cookie := range cks { @@ -136,28 +144,36 @@ func GetCaptchaImage() (savePath string, error *apierror.ApiError) { } func getLoginParams() (params loginParams, error *apierror.ApiError) { - header := map[string]string { + header := map[string]string{ "Content-Type": "application/x-www-form-urlencoded", } - data, err := client.Fetch("GET", WEB_URL+ "/udb/udb_login.jsp?pageId=1&redirectURL=/main.action", - nil, header) + fullUrl := WEB_URL + "/unifyLoginForPC.action?appId=8025431004&clientType=10020&returnURL=https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html&timeStamp=" + fmt.Sprintf("%d", apiutil.Timestamp()) + data, err := client.Fetch("GET", fullUrl, nil, header) if err != nil { logger.Verboseln("login redirectURL occurs error: ", err.Error()) return params, apierror.NewApiErrorWithError(err) } content := string(data) - re, _ := regexp.Compile("captchaToken' value='(.+?)'") - params.CaptchaToken = re.FindStringSubmatch(content)[1] + match := regexp.MustCompile("name='captchaToken' value='(.+?)'").FindStringSubmatch(content) + if len(match) > 1 { + params.CaptchaToken = match[1] + } - re, _ = regexp.Compile("lt = \"(.+?)\"") - params.Lt = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("var lt = \"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.Lt = match[1] + } - re, _ = regexp.Compile("returnUrl = '(.+?)'") - params.ReturnUrl = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("returnUrl = '(.+?)'").FindStringSubmatch(content) + if len(match) > 1 { + params.ReturnUrl = match[1] + } - re, _ = regexp.Compile("paramId = \"(.+?)\"") - params.ParamId = re.FindStringSubmatch(content)[1] + match = regexp.MustCompile("var paramId = \"(.+?)\"").FindStringSubmatch(content) + if len(match) > 1 { + params.ParamId = match[1] + } return } @@ -167,15 +183,15 @@ func checkNeedCaptchaCodeOrNot(username, lt string) (error *apierror.ApiError) { if err != nil { return apierror.NewApiErrorWithError(err) } - postData := map[string]string { + postData := map[string]string{ "accountType": "01", - "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsa))), - "appKey": "cloud", + "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsa))), + "appKey": "cloud", } - header := map[string]string { - "lt": lt, + header := map[string]string{ + "lt": lt, "Content-Type": "application/x-www-form-urlencoded", - "Referer": "https://open.e.189.cn/", + "Referer": "https://open.e.189.cn/", } body, err := client.Fetch("POST", url, postData, header) if err != nil { @@ -218,21 +234,21 @@ func doLoginAct(username, password, validateCode, captchaToken, returnUrl, param url := AUTH_URL + "/loginSubmit.do" rsaUserName, _ := crypto.RsaEncrypt([]byte(apiutil.RsaPublicKey), []byte(username)) rsaPassword, _ := crypto.RsaEncrypt([]byte(apiutil.RsaPublicKey), []byte(password)) - data := map[string]string { - "appKey": "cloud", - "accountType": "01", - "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaUserName))), - "password": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaPassword))), + data := map[string]string{ + "appKey": "cloud", + "accountType": "01", + "userName": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaUserName))), + "password": "{RSA}" + apiutil.B64toHex(string(crypto.Base64Encode(rsaPassword))), "validateCode": validateCode, "captchaToken": captchaToken, - "returnUrl": returnUrl, - "mailSuffix": "@189.cn", - "paramId": paramId, + "returnUrl": returnUrl, + "mailSuffix": "@189.cn", + "paramId": paramId, } - header := map[string]string { - "lt": lt, + header := map[string]string{ + "lt": lt, "Content-Type": "application/x-www-form-urlencoded", - "Referer": "https://open.e.189.cn/", + "Referer": "https://open.e.189.cn/", } body, err := client.Fetch("POST", url, data, header) @@ -255,12 +271,12 @@ func buildCookie(cookieMap map[string]string) []*http.Cookie { } c := make([]*http.Cookie, 0, 0) - for k,v := range cookieMap { + for k, v := range cookieMap { c = append(c, &http.Cookie{ - Name: k, + Name: k, Value: v, - Path: "/", + Path: "/", }) } return c @@ -269,16 +285,10 @@ func buildCookie(cookieMap map[string]string) []*http.Cookie { func RefreshCookieToken(sessionKey string) string { client := requester.NewHTTPClient() - header := map[string]string { + header := map[string]string{ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,ja;q=0.7", } - //cloudpanUrl := &url.URL{ - // Scheme: "http", - // Host: "cloud.189.cn", - // Path: "/", - //} - fullUrl := &strings.Builder{} fmt.Fprintf(fullUrl, "%s/ssoLogin.action?sessionKey=%s&redirectUrl=main.action%%23recycle", WEB_URL, sessionKey) @@ -288,11 +298,17 @@ func RefreshCookieToken(sessionKey string) string { logger.Verboseln("refresh web token cookie error ", err) return "" } - cks := resp.Request.Cookies() + cks := resp.Cookies() + for _, cookie := range cks { + if cookie.Name == "COOKIE_LOGIN_USER" { + return cookie.Value + } + } + cks = resp.Request.Cookies() for _, cookie := range cks { if cookie.Name == "COOKIE_LOGIN_USER" { return cookie.Value } } return "" -} \ No newline at end of file +} diff --git a/cloudpan/share.go b/cloudpan/share.go index 6ac27fc..94a297e 100644 --- a/cloudpan/share.go +++ b/cloudpan/share.go @@ -19,7 +19,6 @@ import ( "fmt" "github.com/tickstep/cloudpan189-api/cloudpan/apierror" "github.com/tickstep/library-go/logger" - "github.com/tickstep/library-go/text" "net/url" "strconv" "strings" @@ -266,7 +265,7 @@ func (p *PanClient) ShareCancel(shareIdList []int64) (bool, *apierror.ApiError) shareIds += strconv.FormatInt(id, 10) + "," } if strings.LastIndex(shareIds, ",") == (len(shareIds) - 1) { - shareIds = text.Substr(shareIds, 0, len(shareIds)-1) + shareIds = strings.TrimSuffix(shareIds, ",") } fmt.Fprintf(fullUrl, "%s/api/portal/cancelShare.action?shareIdList=%s&ancelType=1",