mirror of
https://github.com/OpenListTeam/OpenList.git
synced 2025-09-19 20:26:26 +08:00
feat(189PC,189TV): add refreshToken and qrcode login (#1205)
### Key Changes - **189PC**: Add QR code login and refresh token support - **189TV**: Add session refresh mechanism and fix TempUuid persistence issue - **Both**: Implement session keep-alive with cron jobs (5min interval) ### Features - QR code authentication for 189PC as alternative to password login - Automatic token refresh to avoid frequent re-authentication - Session keep-alive to maintain long-term connections - Retry logic with max attempts to prevent infinite loops ### Fixes - Fixed 189TV TempUuid causing storage corruption on QR code reload - Enhanced error handling for token expiration scenarios
This commit is contained in:
@ -1,7 +1,6 @@
|
|||||||
package _189_tv
|
package _189_tv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"container/ring"
|
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -12,18 +11,20 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cloud189TV struct {
|
type Cloud189TV struct {
|
||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
tokenInfo *AppSessionResp
|
tokenInfo *AppSessionResp
|
||||||
uploadThread int
|
uploadThread int
|
||||||
familyTransferFolder *ring.Ring
|
storageConfig driver.Config
|
||||||
cleanFamilyTransferFile func()
|
|
||||||
storageConfig driver.Config
|
TempUuid string
|
||||||
|
cron *cron.Cron // 新增 cron 字段
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) Config() driver.Config {
|
func (y *Cloud189TV) Config() driver.Config {
|
||||||
@ -79,10 +80,17 @@ func (y *Cloud189TV) Init(ctx context.Context) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.cron = cron.NewCron(time.Minute * 5)
|
||||||
|
y.cron.Do(y.keepAlive)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
func (y *Cloud189TV) Drop(ctx context.Context) error {
|
||||||
|
if y.cron != nil {
|
||||||
|
y.cron.Stop()
|
||||||
|
y.cron = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
type Addition struct {
|
type Addition struct {
|
||||||
driver.RootID
|
driver.RootID
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
TempUuid string
|
|
||||||
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
Type string `json:"type" type:"select" options:"personal,family" default:"personal"`
|
||||||
|
@ -66,6 +66,10 @@ func (y *Cloud189TV) AppKeySignatureHeader(url, method string) map[string]string
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
|
func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, isFamily ...bool) ([]byte, error) {
|
||||||
|
return y.requestWithRetry(url, method, callback, params, resp, 0, isFamily...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) requestWithRetry(url, method string, callback base.ReqCallback, params map[string]string, resp interface{}, retryCount int, isFamily ...bool) ([]byte, error) {
|
||||||
req := y.client.R().SetQueryParams(clientSuffix())
|
req := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
|
||||||
if params != nil {
|
if params != nil {
|
||||||
@ -91,7 +95,22 @@ func (y *Cloud189TV) request(url, method string, callback base.ReqCallback, para
|
|||||||
|
|
||||||
if strings.Contains(res.String(), "userSessionBO is null") ||
|
if strings.Contains(res.String(), "userSessionBO is null") ||
|
||||||
strings.Contains(res.String(), "InvalidSessionKey") {
|
strings.Contains(res.String(), "InvalidSessionKey") {
|
||||||
return nil, errors.New("session expired")
|
// 限制重试次数,避免无限递归
|
||||||
|
if retryCount >= 3 {
|
||||||
|
y.Addition.AccessToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil, errors.New("session expired after retry")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试刷新会话
|
||||||
|
if err := y.refreshSession(); err != nil {
|
||||||
|
// 如果刷新失败,说明AccessToken也已过期,需要重新登录
|
||||||
|
y.Addition.AccessToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil, errors.New("session expired")
|
||||||
|
}
|
||||||
|
// 如果刷新成功,则重试原始请求(增加重试计数)
|
||||||
|
return y.requestWithRetry(url, method, callback, params, resp, retryCount+1, isFamily...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理错误
|
// 处理错误
|
||||||
@ -211,7 +230,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
var erron RespErr
|
var erron RespErr
|
||||||
var tokenInfo AppSessionResp
|
var tokenInfo AppSessionResp
|
||||||
if y.Addition.AccessToken == "" {
|
if y.Addition.AccessToken == "" {
|
||||||
if y.Addition.TempUuid == "" {
|
if y.TempUuid == "" {
|
||||||
// 获取登录参数
|
// 获取登录参数
|
||||||
var uuidInfo UuidInfoResp
|
var uuidInfo UuidInfoResp
|
||||||
req.SetResult(&uuidInfo).SetError(&erron)
|
req.SetResult(&uuidInfo).SetError(&erron)
|
||||||
@ -230,7 +249,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
if uuidInfo.Uuid == "" {
|
if uuidInfo.Uuid == "" {
|
||||||
return errors.New("uuidInfo is empty")
|
return errors.New("uuidInfo is empty")
|
||||||
}
|
}
|
||||||
y.Addition.TempUuid = uuidInfo.Uuid
|
y.TempUuid = uuidInfo.Uuid
|
||||||
op.MustSaveDriverStorage(y)
|
op.MustSaveDriverStorage(y)
|
||||||
|
|
||||||
// 展示二维码
|
// 展示二维码
|
||||||
@ -258,7 +277,7 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
// Signature
|
// Signature
|
||||||
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
|
req.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/qrcodeLoginResult.action",
|
||||||
http.MethodGet))
|
http.MethodGet))
|
||||||
req.SetQueryParam("uuid", y.Addition.TempUuid)
|
req.SetQueryParam("uuid", y.TempUuid)
|
||||||
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
|
_, err = req.Execute(http.MethodGet, ApiUrl+"/family/manage/qrcodeLoginResult.action")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -270,7 +289,6 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
return errors.New("E189AccessToken is empty")
|
return errors.New("E189AccessToken is empty")
|
||||||
}
|
}
|
||||||
y.Addition.AccessToken = accessTokenResp.E189AccessToken
|
y.Addition.AccessToken = accessTokenResp.E189AccessToken
|
||||||
y.Addition.TempUuid = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取SessionKey 和 SessionSecret
|
// 获取SessionKey 和 SessionSecret
|
||||||
@ -294,6 +312,44 @@ func (y *Cloud189TV) login() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// refreshSession 尝试使用现有的 AccessToken 刷新会话
|
||||||
|
func (y *Cloud189TV) refreshSession() (err error) {
|
||||||
|
var erron RespErr
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
reqb := y.client.R().SetQueryParams(clientSuffix())
|
||||||
|
reqb.SetResult(&tokenInfo).SetError(&erron)
|
||||||
|
// Signature
|
||||||
|
reqb.SetHeaders(y.AppKeySignatureHeader(ApiUrl+"/family/manage/loginFamilyMerge.action",
|
||||||
|
http.MethodGet))
|
||||||
|
reqb.SetQueryParam("e189AccessToken", y.Addition.AccessToken)
|
||||||
|
_, err = reqb.Execute(http.MethodGet, ApiUrl+"/family/manage/loginFamilyMerge.action")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if erron.HasError() {
|
||||||
|
return &erron
|
||||||
|
}
|
||||||
|
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189TV) keepAlive() {
|
||||||
|
_, err := y.get(ApiUrl+"/keepUserSession.action", func(r *resty.Request) {
|
||||||
|
r.SetQueryParams(clientSuffix())
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Warnf("189tv: Failed to keep user session alive: %v", err)
|
||||||
|
// 如果keepAlive失败,尝试刷新session
|
||||||
|
if refreshErr := y.refreshSession(); refreshErr != nil {
|
||||||
|
utils.Log.Errorf("189tv: Failed to refresh session after keepAlive error: %v", refreshErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.Log.Debugf("189tv: User session kept alive successfully.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
func (y *Cloud189TV) RapidUpload(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, isFamily bool, overwrite bool) (model.Obj, error) {
|
||||||
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
fileMd5 := stream.GetHash().GetHash(utils.MD5)
|
||||||
if len(fileMd5) < utils.MD5.Width {
|
if len(fileMd5) < utils.MD5.Width {
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
"github.com/OpenListTeam/OpenList/v4/internal/driver"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
"github.com/OpenListTeam/OpenList/v4/internal/errs"
|
||||||
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
"github.com/OpenListTeam/OpenList/v4/internal/model"
|
||||||
|
"github.com/OpenListTeam/OpenList/v4/pkg/cron"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -21,12 +22,12 @@ type Cloud189PC struct {
|
|||||||
model.Storage
|
model.Storage
|
||||||
Addition
|
Addition
|
||||||
|
|
||||||
identity string
|
|
||||||
|
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
|
|
||||||
loginParam *LoginParam
|
loginParam *LoginParam
|
||||||
tokenInfo *AppSessionResp
|
qrcodeParam *QRLoginParam
|
||||||
|
|
||||||
|
tokenInfo *AppSessionResp
|
||||||
|
|
||||||
uploadThread int
|
uploadThread int
|
||||||
|
|
||||||
@ -35,6 +36,7 @@ type Cloud189PC struct {
|
|||||||
|
|
||||||
storageConfig driver.Config
|
storageConfig driver.Config
|
||||||
ref *Cloud189PC
|
ref *Cloud189PC
|
||||||
|
cron *cron.Cron
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) Config() driver.Config {
|
func (y *Cloud189PC) Config() driver.Config {
|
||||||
@ -84,14 +86,22 @@ func (y *Cloud189PC) Init(ctx context.Context) (err error) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免重复登陆
|
// 先尝试用Token刷新,之后尝试登陆
|
||||||
identity := utils.GetMD5EncodeStr(y.Username + y.Password)
|
if y.Addition.RefreshToken != "" {
|
||||||
if !y.isLogin() || y.identity != identity {
|
y.tokenInfo = &AppSessionResp{RefreshToken: y.Addition.RefreshToken}
|
||||||
y.identity = identity
|
if err = y.refreshToken(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if err = y.login(); err != nil {
|
if err = y.login(); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化并启动 cron 任务
|
||||||
|
y.cron = cron.NewCron(time.Duration(time.Minute * 5))
|
||||||
|
// 每5分钟执行一次 keepAlive
|
||||||
|
y.cron.Do(y.keepAlive)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理家庭云ID
|
// 处理家庭云ID
|
||||||
@ -128,6 +138,10 @@ func (d *Cloud189PC) InitReference(storage driver.Driver) error {
|
|||||||
|
|
||||||
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
func (y *Cloud189PC) Drop(ctx context.Context) error {
|
||||||
y.ref = nil
|
y.ref = nil
|
||||||
|
if y.cron != nil {
|
||||||
|
y.cron.Stop()
|
||||||
|
y.cron = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,6 +80,20 @@ func timestamp() int64 {
|
|||||||
return time.Now().UTC().UnixNano() / 1e6
|
return time.Now().UTC().UnixNano() / 1e6
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatDate formats a time.Time object into the "YYYY-MM-DDHH:mm:ssSSS" format.
|
||||||
|
func formatDate(t time.Time) string {
|
||||||
|
// The layout string "2006-01-0215:04:05.000" corresponds to:
|
||||||
|
// 2006 -> Year (YYYY)
|
||||||
|
// 01 -> Month (MM)
|
||||||
|
// 02 -> Day (DD)
|
||||||
|
// 15 -> Hour (HH)
|
||||||
|
// 04 -> Minute (mm)
|
||||||
|
// 05 -> Second (ss)
|
||||||
|
// 000 -> Millisecond (SSS) with leading zeros
|
||||||
|
// Note the lack of a separator between the date and hour, matching the desired output.
|
||||||
|
return t.Format("2006-01-0215:04:05.000")
|
||||||
|
}
|
||||||
|
|
||||||
func MustParseTime(str string) *time.Time {
|
func MustParseTime(str string) *time.Time {
|
||||||
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
|
lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05 -07", str+" +08", time.Local)
|
||||||
return &lastOpTime
|
return &lastOpTime
|
||||||
|
@ -6,9 +6,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Addition struct {
|
type Addition struct {
|
||||||
Username string `json:"username" required:"true"`
|
LoginType string `json:"login_type" type:"select" options:"password,qrcode" default:"password" required:"true"`
|
||||||
Password string `json:"password" required:"true"`
|
Username string `json:"username" required:"true"`
|
||||||
VCode string `json:"validate_code"`
|
Password string `json:"password" required:"true"`
|
||||||
|
VCode string `json:"validate_code"`
|
||||||
|
RefreshToken string `json:"refresh_token" help:"To switch accounts, please clear this field"`
|
||||||
driver.RootID
|
driver.RootID
|
||||||
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
OrderBy string `json:"order_by" type:"select" options:"filename,filesize,lastOpTime" default:"filename"`
|
||||||
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
OrderDirection string `json:"order_direction" type:"select" options:"asc,desc" default:"asc"`
|
||||||
|
@ -68,15 +68,7 @@ func (e *RespErr) Error() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// 登陆需要的参数
|
type BaseLoginParam struct {
|
||||||
type LoginParam struct {
|
|
||||||
// 加密后的用户名和密码
|
|
||||||
RsaUsername string
|
|
||||||
RsaPassword string
|
|
||||||
|
|
||||||
// rsa密钥
|
|
||||||
jRsaKey string
|
|
||||||
|
|
||||||
// 请求头参数
|
// 请求头参数
|
||||||
Lt string
|
Lt string
|
||||||
ReqId string
|
ReqId string
|
||||||
@ -88,6 +80,27 @@ type LoginParam struct {
|
|||||||
CaptchaToken string
|
CaptchaToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QRLoginParam 用于暂存二维码登录过程中的参数
|
||||||
|
type QRLoginParam struct {
|
||||||
|
BaseLoginParam
|
||||||
|
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
EncodeUUID string `json:"encodeuuid"`
|
||||||
|
EncryUUID string `json:"encryuuid"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登陆需要的参数
|
||||||
|
type LoginParam struct {
|
||||||
|
// 加密后的用户名和密码
|
||||||
|
RsaUsername string
|
||||||
|
RsaPassword string
|
||||||
|
|
||||||
|
// rsa密钥
|
||||||
|
jRsaKey string
|
||||||
|
|
||||||
|
BaseLoginParam
|
||||||
|
}
|
||||||
|
|
||||||
// 登陆加密相关
|
// 登陆加密相关
|
||||||
type EncryptConfResp struct {
|
type EncryptConfResp struct {
|
||||||
Result int `json:"result"`
|
Result int `json:"result"`
|
||||||
|
@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
"github.com/OpenListTeam/OpenList/v4/internal/stream"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
"github.com/OpenListTeam/OpenList/v4/pkg/errgroup"
|
||||||
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
|
||||||
"github.com/avast/retry-go"
|
"github.com/avast/retry-go"
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
@ -54,6 +55,9 @@ const (
|
|||||||
MAC = "TELEMAC"
|
MAC = "TELEMAC"
|
||||||
|
|
||||||
CHANNEL_ID = "web_cloud.189.cn"
|
CHANNEL_ID = "web_cloud.189.cn"
|
||||||
|
|
||||||
|
// Error codes
|
||||||
|
UserInvalidOpenTokenError = "UserInvalidOpenToken"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
func (y *Cloud189PC) SignatureHeader(url, method, params string, isFamily bool) map[string]string {
|
||||||
@ -264,7 +268,14 @@ func (y *Cloud189PC) findFileByName(ctx context.Context, searchName string, fold
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (y *Cloud189PC) login() (err error) {
|
func (y *Cloud189PC) login() error {
|
||||||
|
if y.LoginType == "qrcode" {
|
||||||
|
return y.loginByQRCode()
|
||||||
|
}
|
||||||
|
return y.loginByPassword()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) loginByPassword() (err error) {
|
||||||
// 初始化登陆所需参数
|
// 初始化登陆所需参数
|
||||||
if y.loginParam == nil {
|
if y.loginParam == nil {
|
||||||
if err = y.initLoginParam(); err != nil {
|
if err = y.initLoginParam(); err != nil {
|
||||||
@ -278,10 +289,15 @@ func (y *Cloud189PC) login() (err error) {
|
|||||||
// 销毁登陆参数
|
// 销毁登陆参数
|
||||||
y.loginParam = nil
|
y.loginParam = nil
|
||||||
// 遇到错误,重新加载登陆参数(刷新验证码)
|
// 遇到错误,重新加载登陆参数(刷新验证码)
|
||||||
if err != nil && y.NoUseOcr {
|
if err != nil {
|
||||||
if err1 := y.initLoginParam(); err1 != nil {
|
if y.NoUseOcr {
|
||||||
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
|
if err1 := y.initLoginParam(); err1 != nil {
|
||||||
|
err = fmt.Errorf("err1: %s \nerr2: %s", err, err1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.Status = err.Error()
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -336,14 +352,105 @@ func (y *Cloud189PC) login() (err error) {
|
|||||||
err = fmt.Errorf(tokenInfo.ResMessage)
|
err = fmt.Errorf(tokenInfo.ResMessage)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
y.tokenInfo = &tokenInfo
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 初始化登陆需要的参数
|
func (y *Cloud189PC) loginByQRCode() error {
|
||||||
* 如果遇到验证码返回错误
|
if y.qrcodeParam == nil {
|
||||||
*/
|
if err := y.initQRCodeParam(); err != nil {
|
||||||
func (y *Cloud189PC) initLoginParam() error {
|
// 二维码也通过错误返回
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var state struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
RedirectUrl string `json:"redirectUrl"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
_, err := y.client.R().
|
||||||
|
SetHeaders(map[string]string{
|
||||||
|
"Referer": AUTH_URL,
|
||||||
|
"Reqid": y.qrcodeParam.ReqId,
|
||||||
|
"lt": y.qrcodeParam.Lt,
|
||||||
|
}).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"appId": APP_ID,
|
||||||
|
"clientType": CLIENT_TYPE,
|
||||||
|
"returnUrl": RETURN_URL,
|
||||||
|
"paramId": y.qrcodeParam.ParamId,
|
||||||
|
"uuid": y.qrcodeParam.UUID,
|
||||||
|
"encryuuid": y.qrcodeParam.EncryUUID,
|
||||||
|
"date": formatDate(now),
|
||||||
|
"timeStamp": fmt.Sprint(now.UTC().UnixNano() / 1e6),
|
||||||
|
}).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetResult(&state).
|
||||||
|
Post(AUTH_URL + "/api/logbox/oauth2/qrcodeLoginState.do")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check QR code state: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch state.Status {
|
||||||
|
case 0: // 登录成功
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetResult(&tokenInfo).
|
||||||
|
SetQueryParams(clientSuffix()).
|
||||||
|
SetQueryParam("redirectURL", state.RedirectUrl).
|
||||||
|
Post(API_URL + "/getSessionForPC.action")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tokenInfo.ResCode != 0 {
|
||||||
|
return fmt.Errorf(tokenInfo.ResMessage)
|
||||||
|
}
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return nil
|
||||||
|
case -11001: // 二维码过期
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
return errors.New("QR code expired, please try again")
|
||||||
|
case -106: // 等待扫描
|
||||||
|
return y.genQRCode("QR code has not been scanned yet, please scan and save again")
|
||||||
|
case -11002: // 等待确认
|
||||||
|
return y.genQRCode("QR code has been scanned, please confirm the login on your phone and save again")
|
||||||
|
default: // 其他错误
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
return fmt.Errorf("QR code login failed with status %d: %s", state.Status, state.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) genQRCode(text string) error {
|
||||||
|
// 展示二维码
|
||||||
|
qrTemplate := `<body>
|
||||||
|
state: %s
|
||||||
|
<br><img src="data:image/jpeg;base64,%s"/>
|
||||||
|
<br>Or Click here: <a href="%s">Login</a>
|
||||||
|
</body>`
|
||||||
|
|
||||||
|
// Generate QR code
|
||||||
|
qrCode, err := qrcode.Encode(y.qrcodeParam.UUID, qrcode.Medium, 256)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate QR code: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode QR code to base64
|
||||||
|
qrCodeBase64 := base64.StdEncoding.EncodeToString(qrCode)
|
||||||
|
|
||||||
|
// Create the HTML page
|
||||||
|
qrPage := fmt.Sprintf(qrTemplate, text, qrCodeBase64, y.qrcodeParam.UUID)
|
||||||
|
return fmt.Errorf("need verify: \n%s", qrPage)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) initBaseParams() (*BaseLoginParam, error) {
|
||||||
// 清除cookie
|
// 清除cookie
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
y.client.SetCookieJar(jar)
|
y.client.SetCookieJar(jar)
|
||||||
@ -357,17 +464,30 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
}).
|
}).
|
||||||
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
Get(WEB_URL + "/api/portal/unifyLoginForPC.action")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
param := LoginParam{
|
return &BaseLoginParam{
|
||||||
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1],
|
||||||
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1],
|
||||||
// jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1],
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 初始化登陆需要的参数
|
||||||
|
* 如果遇到验证码返回错误
|
||||||
|
*/
|
||||||
|
func (y *Cloud189PC) initLoginParam() error {
|
||||||
|
y.loginParam = nil
|
||||||
|
|
||||||
|
baseParam, err := y.initBaseParams()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
y.loginParam = &LoginParam{BaseLoginParam: *baseParam}
|
||||||
|
|
||||||
// 获取rsa公钥
|
// 获取rsa公钥
|
||||||
var encryptConf EncryptConfResp
|
var encryptConf EncryptConfResp
|
||||||
_, err = y.client.R().
|
_, err = y.client.R().
|
||||||
@ -378,18 +498,17 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
param.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
|
y.loginParam.jRsaKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", encryptConf.Data.PubKey)
|
||||||
param.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Username)
|
y.loginParam.RsaUsername = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Username)
|
||||||
param.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(param.jRsaKey, y.Password)
|
y.loginParam.RsaPassword = encryptConf.Data.Pre + RsaEncrypt(y.loginParam.jRsaKey, y.Password)
|
||||||
y.loginParam = ¶m
|
|
||||||
|
|
||||||
// 判断是否需要验证码
|
// 判断是否需要验证码
|
||||||
resp, err := y.client.R().
|
resp, err := y.client.R().
|
||||||
SetHeader("REQID", param.ReqId).
|
SetHeader("REQID", y.loginParam.ReqId).
|
||||||
SetFormData(map[string]string{
|
SetFormData(map[string]string{
|
||||||
"appKey": APP_ID,
|
"appKey": APP_ID,
|
||||||
"accountType": ACCOUNT_TYPE,
|
"accountType": ACCOUNT_TYPE,
|
||||||
"userName": param.RsaUsername,
|
"userName": y.loginParam.RsaUsername,
|
||||||
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
|
}).Post(AUTH_URL + "/api/logbox/oauth2/needcaptcha.do")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -401,8 +520,8 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
// 拉取验证码
|
// 拉取验证码
|
||||||
imgRes, err := y.client.R().
|
imgRes, err := y.client.R().
|
||||||
SetQueryParams(map[string]string{
|
SetQueryParams(map[string]string{
|
||||||
"token": param.CaptchaToken,
|
"token": y.loginParam.CaptchaToken,
|
||||||
"REQID": param.ReqId,
|
"REQID": y.loginParam.ReqId,
|
||||||
"rnd": fmt.Sprint(timestamp()),
|
"rnd": fmt.Sprint(timestamp()),
|
||||||
}).
|
}).
|
||||||
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
|
Get(AUTH_URL + "/api/logbox/oauth2/picCaptcha.do")
|
||||||
@ -429,10 +548,38 @@ func (y *Cloud189PC) initLoginParam() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getQRCode 获取并返回二维码
|
||||||
|
func (y *Cloud189PC) initQRCodeParam() (err error) {
|
||||||
|
y.qrcodeParam = nil
|
||||||
|
|
||||||
|
baseParam, err := y.initBaseParams()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrcodeParam QRLoginParam
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetFormData(map[string]string{"appId": APP_ID}).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetResult(&qrcodeParam).
|
||||||
|
Post(AUTH_URL + "/api/logbox/oauth2/getUUID.do")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
qrcodeParam.BaseLoginParam = *baseParam
|
||||||
|
y.qrcodeParam = &qrcodeParam
|
||||||
|
|
||||||
|
return y.genQRCode("please scan the QR code with the 189 Cloud app, then save the settings again.")
|
||||||
|
}
|
||||||
|
|
||||||
// 刷新会话
|
// 刷新会话
|
||||||
func (y *Cloud189PC) refreshSession() (err error) {
|
func (y *Cloud189PC) refreshSession() (err error) {
|
||||||
|
return y.refreshSessionWithRetry(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) refreshSessionWithRetry(retryCount int) (err error) {
|
||||||
if y.ref != nil {
|
if y.ref != nil {
|
||||||
return y.ref.refreshSession()
|
return y.ref.refreshSessionWithRetry(retryCount)
|
||||||
}
|
}
|
||||||
var erron RespErr
|
var erron RespErr
|
||||||
var userSessionResp UserSessionResp
|
var userSessionResp UserSessionResp
|
||||||
@ -449,24 +596,87 @@ func (y *Cloud189PC) refreshSession() (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误影响正常访问,下线该储存
|
// token生效刷新token
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
y.GetStorage().SetStatus(fmt.Sprintf("%+v", err.Error()))
|
|
||||||
op.MustSaveDriverStorage(y)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if erron.HasError() {
|
if erron.HasError() {
|
||||||
if erron.ResCode == "UserInvalidOpenToken" {
|
if erron.ResCode == UserInvalidOpenTokenError {
|
||||||
if err = y.login(); err != nil {
|
return y.refreshTokenWithRetry(retryCount)
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return &erron
|
return &erron
|
||||||
}
|
}
|
||||||
y.tokenInfo.UserSessionResp = userSessionResp
|
y.tokenInfo.UserSessionResp = userSessionResp
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshToken 刷新token,失败时返回错误,不再直接调用login
|
||||||
|
func (y *Cloud189PC) refreshToken() (err error) {
|
||||||
|
return y.refreshTokenWithRetry(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) refreshTokenWithRetry(retryCount int) (err error) {
|
||||||
|
if y.ref != nil {
|
||||||
|
return y.ref.refreshTokenWithRetry(retryCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限制重试次数,避免无限递归
|
||||||
|
if retryCount >= 3 {
|
||||||
|
if y.Addition.RefreshToken != "" {
|
||||||
|
y.Addition.RefreshToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
}
|
||||||
|
return errors.New("refresh token failed after maximum retries")
|
||||||
|
}
|
||||||
|
|
||||||
|
var erron RespErr
|
||||||
|
var tokenInfo AppSessionResp
|
||||||
|
_, err = y.client.R().
|
||||||
|
SetResult(&tokenInfo).
|
||||||
|
ForceContentType("application/json;charset=UTF-8").
|
||||||
|
SetError(&erron).
|
||||||
|
SetFormData(map[string]string{
|
||||||
|
"clientId": APP_ID,
|
||||||
|
"refreshToken": y.tokenInfo.RefreshToken,
|
||||||
|
"grantType": "refresh_token",
|
||||||
|
"format": "json",
|
||||||
|
}).
|
||||||
|
Post(AUTH_URL + "/api/oauth2/refreshToken.do")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果刷新失败,返回错误给上层处理
|
||||||
|
if erron.HasError() {
|
||||||
|
if y.Addition.RefreshToken != "" {
|
||||||
|
y.Addition.RefreshToken = ""
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据登录类型决定下一步行为
|
||||||
|
if y.LoginType == "qrcode" {
|
||||||
|
return errors.New("QR code session has expired, please re-scan the code to log in")
|
||||||
|
}
|
||||||
|
// 密码登录模式下,尝试回退到完整登录
|
||||||
|
return y.login()
|
||||||
|
}
|
||||||
|
|
||||||
|
y.Addition.RefreshToken = tokenInfo.RefreshToken
|
||||||
|
y.tokenInfo = &tokenInfo
|
||||||
|
op.MustSaveDriverStorage(y)
|
||||||
|
return y.refreshSessionWithRetry(retryCount + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (y *Cloud189PC) keepAlive() {
|
||||||
|
_, err := y.get(API_URL+"/keepUserSession.action", func(r *resty.Request) {
|
||||||
|
r.SetQueryParams(clientSuffix())
|
||||||
|
}, nil)
|
||||||
|
if err != nil {
|
||||||
|
utils.Log.Warnf("189pc: Failed to keep user session alive: %v", err)
|
||||||
|
// 如果keepAlive失败,尝试刷新session
|
||||||
|
if refreshErr := y.refreshSession(); refreshErr != nil {
|
||||||
|
utils.Log.Errorf("189pc: Failed to refresh session after keepAlive error: %v", refreshErr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.Log.Debugf("189pc: User session kept alive successfully.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 普通上传
|
// 普通上传
|
||||||
|
Reference in New Issue
Block a user