From a93937f80da8011c2dbd6464967d190b20ed8301 Mon Sep 17 00:00:00 2001 From: YangXu <47767754+Three-taile-dragon@users.noreply.github.com> Date: Sun, 14 Jul 2024 21:07:00 +0800 Subject: [PATCH] fix(pikpak): add captcha_token generation function (#6775) closes #6752 closes #6760 --- drivers/pikpak/driver.go | 42 ++++++- drivers/pikpak/types.go | 65 +++++++++++ drivers/pikpak/util.go | 237 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 342 insertions(+), 2 deletions(-) diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go index 3ecc31d6bff..23198455a74 100644 --- a/drivers/pikpak/driver.go +++ b/drivers/pikpak/driver.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/alist-org/alist/v3/internal/op" "net/http" "strconv" "strings" @@ -25,7 +26,7 @@ import ( type PikPak struct { model.Storage Addition - + *Common oauth2Token oauth2.TokenSource } @@ -43,6 +44,20 @@ func (d *PikPak) Init(ctx context.Context) (err error) { d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg" } + if d.Common == nil { + d.Common = &Common{ + client: base.NewRestyClient(), + CaptchaToken: "", + UserID: "", + DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password), + UserAgent: BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), ClientID, PackageName, SdkVersion, ClientVersion, PackageName, ""), + RefreshCTokenCk: func(token string) { + d.Common.CaptchaToken = token + op.MustSaveDriverStorage(d) + }, + } + } + oauth2Config := &oauth2.Config{ ClientID: d.ClientID, ClientSecret: d.ClientSecret, @@ -60,6 +75,14 @@ func (d *PikPak) Init(ctx context.Context) (err error) { d.Password, ) })) + + // 获取CaptchaToken + _ = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.com/drive/v1/files"), d.Username) + + // 获取用户ID + _ = d.GetUserID(ctx) + // 更新UserAgent + d.Common.UserAgent = BuildCustomUserAgent(d.Common.DeviceID, ClientID, PackageName, SdkVersion, ClientVersion, PackageName, d.Common.UserID) return nil } @@ -79,7 +102,7 @@ func (d *PikPak) List(ctx context.Context, dir model.Obj, args model.ListArgs) ( func (d *PikPak) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { var resp File - _, err := d.request(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()), + _, err := d.requestWithCaptchaToken(fmt.Sprintf("https://api-drive.mypikpak.com/drive/v1/files/%s?_magic=2021&thumbnail_size=SIZE_LARGE", file.GetID()), http.MethodGet, nil, &resp) if err != nil { return nil, err @@ -297,4 +320,19 @@ func (d *PikPak) DeleteOfflineTasks(ctx context.Context, taskIDs []string, delet return nil } +func (d *PikPak) GetUserID(ctx context.Context) error { + url := "https://api-drive.mypikpak.com/vip/v1/vip/info" + var resp VipInfo + _, err := d.requestWithCaptchaToken(url, http.MethodGet, func(req *resty.Request) { + req.SetContext(ctx) + }, &resp) + if err != nil { + return fmt.Errorf("failed to get user id : %w", err) + } + if resp.Data.UserID != "" { + d.Common.SetUserID(resp.Data.UserID) + } + return nil +} + var _ driver.Driver = (*PikPak)(nil) diff --git a/drivers/pikpak/types.go b/drivers/pikpak/types.go index a9928d00ec2..a831642e27f 100644 --- a/drivers/pikpak/types.go +++ b/drivers/pikpak/types.go @@ -1,6 +1,7 @@ package pikpak import ( + "fmt" "strconv" "time" @@ -168,3 +169,67 @@ type ReferenceResource struct { Tags []string `json:"tags"` ThumbnailLink string `json:"thumbnail_link"` } + +type ErrResp struct { + ErrorCode int64 `json:"error_code"` + ErrorMsg string `json:"error"` + ErrorDescription string `json:"error_description"` + // ErrorDetails interface{} `json:"error_details"` +} + +func (e *ErrResp) IsError() bool { + return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != "" +} + +func (e *ErrResp) Error() string { + return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription) +} + +type CaptchaTokenRequest struct { + Action string `json:"action"` + CaptchaToken string `json:"captcha_token"` + ClientID string `json:"client_id"` + DeviceID string `json:"device_id"` + Meta map[string]string `json:"meta"` + RedirectUri string `json:"redirect_uri"` +} + +type CaptchaTokenResponse struct { + CaptchaToken string `json:"captcha_token"` + ExpiresIn int64 `json:"expires_in"` + Url string `json:"url"` +} + +type VipInfo struct { + Data struct { + Expire time.Time `json:"expire"` + ExtUserInfo struct { + UserRegion string `json:"userRegion"` + } `json:"extUserInfo"` + ExtType string `json:"ext_type"` + FeeRecord string `json:"fee_record"` + Restricted struct { + Result bool `json:"result"` + Content struct { + Text string `json:"text"` + Color string `json:"color"` + DeepLink string `json:"deepLink"` + } `json:"content"` + LearnMore struct { + Text string `json:"text"` + Color string `json:"color"` + DeepLink string `json:"deepLink"` + } `json:"learnMore"` + } `json:"restricted"` + Status string `json:"status"` + Type string `json:"type"` + UserID string `json:"user_id"` + VipItem []struct { + Type string `json:"type"` + Description string `json:"description"` + Status string `json:"status"` + Expire time.Time `json:"expire"` + SurplusDay int `json:"surplus_day"` + } `json:"vipItem"` + } `json:"data"` +} diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go index 0edfc384eba..a794001a3b3 100644 --- a/drivers/pikpak/util.go +++ b/drivers/pikpak/util.go @@ -1,8 +1,16 @@ package pikpak import ( + "crypto/md5" + "crypto/sha1" + "encoding/hex" "errors" + "fmt" + "github.com/alist-org/alist/v3/pkg/utils" "net/http" + "regexp" + "strings" + "time" "github.com/alist-org/alist/v3/drivers/base" "github.com/go-resty/resty/v2" @@ -10,6 +18,26 @@ import ( // do others that not defined in Driver interface +var Algorithms = []string{ + "PAe56I7WZ6FCSkFy77A96jHWcQA27ui80Qy4", + "SUbmk67TfdToBAEe2cZyP8vYVeN", + "1y3yFSZVWiGN95fw/2FQlRuH/Oy6WnO", + "8amLtHJpGzHPz4m9hGz7r+i+8dqQiAk", + "tmIEq5yl2g/XWwM3sKZkY4SbL8YUezrvxPksNabUJ", + "4QvudeJwgJuSf/qb9/wjC21L5aib", + "D1RJd+FZ+LBbt+dAmaIyYrT9gxJm0BB", + "1If", + "iGZr/SJPUFRkwvC174eelKy", +} + +const ( + ClientID = "YNxT9w7GMdWvEOKa" + ClientSecret = "dbw2OtmVEeuUvIptb1Coyg" + ClientVersion = "1.46.2" + PackageName = "com.pikcloud.pikpak" + SdkVersion = "2.0.4.204000 " +) + func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { req := base.RestyClient.R() @@ -38,6 +66,44 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r return res.Body(), nil } +func (d *PikPak) requestWithCaptchaToken(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + + data, err := d.request(url, method, func(req *resty.Request) { + req.SetHeaders(map[string]string{ + "User-Agent": d.GetUserAgent(), + "X-Device-ID": d.GetDeviceID(), + "X-Captcha-Token": d.GetCaptchaToken(), + }) + if callback != nil { + callback(req) + } + }, resp) + + errResp, ok := err.(*ErrResp) + if !ok { + return nil, err + } + + switch errResp.ErrorCode { + case 0: + return data, nil + //case 4122, 4121, 10, 16: + // if d.refreshTokenFunc != nil { + // if err = xc.refreshTokenFunc(); err == nil { + // break + // } + // } + // return nil, err + case 9: // 验证码token过期 + if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.Common.UserID); err != nil { + return nil, err + } + default: + return nil, err + } + return d.request(url, method, callback, resp) +} + func (d *PikPak) getFiles(id string) ([]File, error) { res := make([]File, 0) pageToken := "first" @@ -65,3 +131,174 @@ func (d *PikPak) getFiles(id string) ([]File, error) { } return res, nil } + +func GetAction(method string, url string) string { + urlpath := regexp.MustCompile(`://[^/]+((/[^/\s?#]+)*)`).FindStringSubmatch(url)[1] + return method + ":" + urlpath +} + +type Common struct { + client *resty.Client + CaptchaToken string + UserID string + // 必要值,签名相关 + DeviceID string + UserAgent string + // 验证码token刷新成功回调 + RefreshCTokenCk func(token string) +} + +func generateDeviceSign(deviceID, packageName string) string { + + signatureBase := fmt.Sprintf("%s%s%s%s", deviceID, packageName, "1", "appkey") + + sha1Hash := sha1.New() + sha1Hash.Write([]byte(signatureBase)) + sha1Result := sha1Hash.Sum(nil) + + sha1String := hex.EncodeToString(sha1Result) + + md5Hash := md5.New() + md5Hash.Write([]byte(sha1String)) + md5Result := md5Hash.Sum(nil) + + md5String := hex.EncodeToString(md5Result) + + deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String) + + return deviceSign +} + +func BuildCustomUserAgent(deviceID, clientID, appName, sdkVersion, clientVersion, packageName, userID string) string { + deviceSign := generateDeviceSign(deviceID, packageName) + var sb strings.Builder + + sb.WriteString(fmt.Sprintf("ANDROID-%s/%s ", appName, clientVersion)) + sb.WriteString("protocolVersion/200 ") + sb.WriteString("accesstype/ ") + sb.WriteString(fmt.Sprintf("clientid/%s ", clientID)) + sb.WriteString(fmt.Sprintf("clientversion/%s ", clientVersion)) + sb.WriteString("action_type/ ") + sb.WriteString("networktype/WIFI ") + sb.WriteString("sessionid/ ") + sb.WriteString(fmt.Sprintf("deviceid/%s ", deviceID)) + sb.WriteString("providername/NONE ") + sb.WriteString(fmt.Sprintf("devicesign/%s ", deviceSign)) + sb.WriteString("refresh_token/ ") + sb.WriteString(fmt.Sprintf("sdkversion/%s ", sdkVersion)) + sb.WriteString(fmt.Sprintf("datetime/%d ", time.Now().UnixMilli())) + sb.WriteString(fmt.Sprintf("usrno/%s ", userID)) + sb.WriteString(fmt.Sprintf("appname/android-%s ", appName)) + sb.WriteString(fmt.Sprintf("session_origin/ ")) + sb.WriteString(fmt.Sprintf("grant_type/ ")) + sb.WriteString(fmt.Sprintf("appid/ ")) + sb.WriteString(fmt.Sprintf("clientip/ ")) + sb.WriteString(fmt.Sprintf("devicename/Xiaomi_M2004j7ac ")) + sb.WriteString(fmt.Sprintf("osversion/13 ")) + sb.WriteString(fmt.Sprintf("platformversion/10 ")) + sb.WriteString(fmt.Sprintf("accessmode/ ")) + sb.WriteString(fmt.Sprintf("devicemodel/M2004J7AC ")) + + return sb.String() +} + +func (c *Common) SetDeviceID(deviceID string) { + c.DeviceID = deviceID +} + +func (c *Common) SetUserID(userID string) { + c.UserID = userID +} + +func (c *Common) SetUserAgent(userAgent string) { + c.UserAgent = userAgent +} + +func (c *Common) SetCaptchaToken(captchaToken string) { + c.CaptchaToken = captchaToken +} +func (c *Common) GetCaptchaToken() string { + return c.CaptchaToken +} + +func (c *Common) GetUserAgent() string { + return c.UserAgent +} + +func (c *Common) GetDeviceID() string { + return c.DeviceID +} + +// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后) +func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error { + metas := map[string]string{ + "client_version": ClientVersion, + "package_name": PackageName, + "user_id": userID, + } + metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign() + return d.refreshCaptchaToken(action, metas) +} + +// RefreshCaptchaTokenInLogin 刷新验证码token(登录时) +func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error { + metas := make(map[string]string) + if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok { + metas["email"] = username + } else if len(username) >= 11 && len(username) <= 18 { + metas["phone_number"] = username + } else { + metas["username"] = username + } + return d.refreshCaptchaToken(action, metas) +} + +// GetCaptchaSign 获取验证码签名 +func (c *Common) GetCaptchaSign() (timestamp, sign string) { + timestamp = fmt.Sprint(time.Now().UnixMilli()) + str := fmt.Sprint(ClientID, ClientVersion, PackageName, c.DeviceID, timestamp) + for _, algorithm := range Algorithms { + str = utils.GetMD5EncodeStr(str + algorithm) + } + sign = "1." + str + return +} + +// 刷新验证码token +func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error { + param := CaptchaTokenRequest{ + Action: action, + CaptchaToken: d.Common.CaptchaToken, + ClientID: ClientID, + DeviceID: d.Common.DeviceID, + Meta: metas, + RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor", + } + var e ErrResp + var resp CaptchaTokenResponse + _, err := d.request("https://user.mypikpak.com/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) { + req.SetError(&e).SetBody(param) + }, &resp) + + if err != nil { + return err + } + + if e.IsError() { + return &e + } + + if resp.Url != "" { + return fmt.Errorf(`need verify: Click Here`, resp.Url) + } + + if resp.CaptchaToken == "" { + return fmt.Errorf("empty captchaToken") + } + + if d.Common.RefreshCTokenCk != nil { + d.Common.RefreshCTokenCk(resp.CaptchaToken) + } + d.Common.SetCaptchaToken(resp.CaptchaToken) + return nil +}