From 489b28bdf79e21c5f8ac866328810e16dfcc7d23 Mon Sep 17 00:00:00 2001 From: YangXu <47767754+Three-taile-dragon@users.noreply.github.com> Date: Thu, 22 Aug 2024 00:35:14 +0800 Subject: [PATCH] fix(pikpak_share): add captcha_token generation function (#7045) --- drivers/pikpak_share/driver.go | 68 +++++---- drivers/pikpak_share/meta.go | 11 +- drivers/pikpak_share/types.go | 39 +++++- drivers/pikpak_share/util.go | 244 +++++++++++++++++++++++++++++++-- 4 files changed, 316 insertions(+), 46 deletions(-) diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go index 1862db06..448ad2dd 100644 --- a/drivers/pikpak_share/driver.go +++ b/drivers/pikpak_share/driver.go @@ -2,20 +2,20 @@ package pikpak_share import ( "context" + "github.com/alist-org/alist/v3/internal/op" "net/http" + "time" - "github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils" "github.com/go-resty/resty/v2" - "golang.org/x/oauth2" ) type PikPakShare struct { model.Storage Addition - oauth2Token oauth2.TokenSource + *Common PassCodeToken string } @@ -28,28 +28,45 @@ func (d *PikPakShare) GetAddition() driver.Additional { } func (d *PikPakShare) Init(ctx context.Context) error { - if d.ClientID == "" || d.ClientSecret == "" { - d.ClientID = "YNxT9w7GMdWvEOKa" - d.ClientSecret = "dbw2OtmVEeuUvIptb1Coyg" + if d.Common == nil { + d.Common = &Common{ + DeviceID: utils.GetMD5EncodeStr(d.Addition.ShareId + d.Addition.SharePwd + time.Now().String()), + UserAgent: "", + RefreshCTokenCk: func(token string) { + d.Common.CaptchaToken = token + op.MustSaveDriverStorage(d) + }, + } } - oauth2Config := &oauth2.Config{ - ClientID: d.ClientID, - ClientSecret: d.ClientSecret, - Endpoint: oauth2.Endpoint{ - AuthURL: "https://user.mypikpak.com/v1/auth/signin", - TokenURL: "https://user.mypikpak.com/v1/auth/token", - AuthStyle: oauth2.AuthStyleInParams, - }, + if d.Addition.DeviceID != "" { + d.SetDeviceID(d.Addition.DeviceID) + } else { + d.Addition.DeviceID = d.Common.DeviceID + op.MustSaveDriverStorage(d) } - d.oauth2Token = oauth2.ReuseTokenSource(nil, utils.TokenSource(func() (*oauth2.Token, error) { - return oauth2Config.PasswordCredentialsToken( - context.WithValue(context.Background(), oauth2.HTTPClient, base.HttpClient), - d.Username, - d.Password, - ) - })) + if d.Platform == "android" { + d.ClientID = AndroidClientID + d.ClientSecret = AndroidClientSecret + d.ClientVersion = AndroidClientVersion + d.PackageName = AndroidPackageName + d.Algorithms = AndroidAlgorithms + d.UserAgent = BuildCustomUserAgent(d.GetDeviceID(), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, "") + } else if d.Platform == "web" { + d.ClientID = WebClientID + d.ClientSecret = WebClientSecret + d.ClientVersion = WebClientVersion + d.PackageName = WebPackageName + d.Algorithms = WebAlgorithms + d.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" + } + + // 获取CaptchaToken + err := d.RefreshCaptchaToken(GetAction(http.MethodGet, "https://api-drive.mypikpak.com/drive/v1/share:batch_file_info"), "") + if err != nil { + return err + } if d.SharePwd != "" { return d.getSharePassToken() @@ -87,9 +104,14 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA downloadUrl := resp.FileInfo.WebContentLink if downloadUrl == "" && len(resp.FileInfo.Medias) > 0 { - downloadUrl = resp.FileInfo.Medias[0].Link.Url - } + // 使用转码后的链接 + if d.Addition.UseTransCodingAddress && len(resp.FileInfo.Medias) > 1 { + downloadUrl = resp.FileInfo.Medias[1].Link.Url + } else { + downloadUrl = resp.FileInfo.Medias[0].Link.Url + } + } link := model.Link{ URL: downloadUrl, } diff --git a/drivers/pikpak_share/meta.go b/drivers/pikpak_share/meta.go index 5d05badb..e6f00cda 100644 --- a/drivers/pikpak_share/meta.go +++ b/drivers/pikpak_share/meta.go @@ -7,12 +7,11 @@ import ( type Addition struct { driver.RootID - Username string `json:"username" required:"true"` - Password string `json:"password" required:"true"` - ShareId string `json:"share_id" required:"true"` - SharePwd string `json:"share_pwd"` - ClientID string `json:"client_id" required:"true" default:"YNxT9w7GMdWvEOKa"` - ClientSecret string `json:"client_secret" required:"true" default:"dbw2OtmVEeuUvIptb1Coyg"` + ShareId string `json:"share_id" required:"true"` + SharePwd string `json:"share_pwd"` + Platform string `json:"platform" required:"true" type:"select" options:"android,web"` + DeviceID string `json:"device_id" required:"false" default:""` + UseTransCodingAddress bool `json:"use_transcoding_address" required:"true" default:"false"` } var config = driver.Config{ diff --git a/drivers/pikpak_share/types.go b/drivers/pikpak_share/types.go index 144a05a8..78ea2ff8 100644 --- a/drivers/pikpak_share/types.go +++ b/drivers/pikpak_share/types.go @@ -1,20 +1,16 @@ package pikpak_share import ( + "fmt" "strconv" "time" "github.com/alist-org/alist/v3/internal/model" ) -type RespErr struct { - ErrorCode int `json:"error_code"` - Error string `json:"error"` -} - type ShareResp struct { - ShareStatus string `json:"share_status"` - ShareStatusText string `json:"share_status_text"` + ShareStatus string `json:"share_status"` + ShareStatusText string `json:"share_status_text"` FileInfo File `json:"file_info"` Files []File `json:"files"` NextPageToken string `json:"next_page_token"` @@ -78,3 +74,32 @@ type Media struct { IsVisible bool `json:"is_visible"` Category string `json:"category"` } + +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 ErrResp struct { + ErrorCode int64 `json:"error_code"` + ErrorMsg string `json:"error"` + ErrorDescription string `json:"error_description"` +} + +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) +} diff --git a/drivers/pikpak_share/util.go b/drivers/pikpak_share/util.go index 41bb30d4..a9c8fffe 100644 --- a/drivers/pikpak_share/util.go +++ b/drivers/pikpak_share/util.go @@ -1,21 +1,78 @@ package pikpak_share 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" ) +var AndroidAlgorithms = []string{ + "Gez0T9ijiI9WCeTsKSg3SMlx", + "zQdbalsolyb1R/", + "ftOjr52zt51JD68C3s", + "yeOBMH0JkbQdEFNNwQ0RI9T3wU/v", + "BRJrQZiTQ65WtMvwO", + "je8fqxKPdQVJiy1DM6Bc9Nb1", + "niV", + "9hFCW2R1", + "sHKHpe2i96", + "p7c5E6AcXQ/IJUuAEC9W6", + "", + "aRv9hjc9P+Pbn+u3krN6", + "BzStcgE8qVdqjEH16l4", + "SqgeZvL5j9zoHP95xWHt", + "zVof5yaJkPe3VFpadPof", +} + +var WebAlgorithms = []string{ + "C9qPpZLN8ucRTaTiUMWYS9cQvWOE", + "+r6CQVxjzJV6LCV", + "F", + "pFJRC", + "9WXYIDGrwTCz2OiVlgZa90qpECPD6olt", + "/750aCr4lm/Sly/c", + "RB+DT/gZCrbV", + "", + "CyLsf7hdkIRxRm215hl", + "7xHvLi2tOYP0Y92b", + "ZGTXXxu8E/MIWaEDB+Sm/", + "1UI3", + "E7fP5Pfijd+7K+t6Tg/NhuLq0eEUVChpJSkrKxpO", + "ihtqpG6FMt65+Xk+tWUH2", + "NhXXU9rg4XXdzo7u5o", +} + +const ( + AndroidClientID = "YNxT9w7GMdWvEOKa" + AndroidClientSecret = "dbw2OtmVEeuUvIptb1Coyg" + AndroidClientVersion = "1.47.1" + AndroidPackageName = "com.pikcloud.pikpak" + AndroidSdkVersion = "2.0.4.204000" + WebClientID = "YUMx5nI8ZU8Ap8pm" + WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg" + WebClientVersion = "2.0.0" + WebPackageName = "mypikpak.com" + WebSdkVersion = "8.0.3" +) + func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { req := base.RestyClient.R() - - token, err := d.oauth2Token.Token() - if err != nil { - return nil, err - } - req.SetAuthScheme(token.TokenType).SetAuthToken(token.AccessToken) + req.SetHeaders(map[string]string{ + "User-Agent": d.GetUserAgent(), + "X-Client-ID": d.GetClientID(), + "X-Device-ID": d.GetDeviceID(), + "X-Captcha-Token": d.GetCaptchaToken(), + }) if callback != nil { callback(req) @@ -23,16 +80,25 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba if resp != nil { req.SetResult(resp) } - var e RespErr + var e ErrResp req.SetError(&e) res, err := req.Execute(method, url) if err != nil { return nil, err } - if e.ErrorCode != 0 { - return nil, errors.New(e.Error) + switch e.ErrorCode { + case 0: + return res.Body(), nil + case 9: // 验证码token过期 + if err = d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil { + return nil, err + } + return d.request(url, method, callback, resp) + case 10: // 操作频繁 + return nil, errors.New(e.ErrorDescription) + default: + return nil, errors.New(e.Error()) } - return res.Body(), nil } func (d *PikPakShare) getSharePassToken() error { @@ -92,3 +158,161 @@ func (d *PikPakShare) 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 + // 必要值,签名相关 + ClientID string + ClientSecret string + ClientVersion string + PackageName string + Algorithms []string + DeviceID string + UserAgent string + // 验证码token刷新成功回调 + RefreshCTokenCk func(token string) +} + +func (c *Common) SetUserAgent(userAgent string) { + c.UserAgent = userAgent +} + +func (c *Common) SetCaptchaToken(captchaToken string) { + c.CaptchaToken = captchaToken +} + +func (c *Common) SetDeviceID(deviceID string) { + c.DeviceID = deviceID +} + +func (c *Common) GetCaptchaToken() string { + return c.CaptchaToken +} + +func (c *Common) GetClientID() string { + return c.ClientID +} + +func (c *Common) GetUserAgent() string { + return c.UserAgent +} + +func (c *Common) GetDeviceID() string { + return c.DeviceID +} + +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() +} + +// RefreshCaptchaToken 刷新验证码token +func (d *PikPakShare) RefreshCaptchaToken(action, userID string) error { + metas := map[string]string{ + "client_version": d.ClientVersion, + "package_name": d.PackageName, + "user_id": userID, + } + metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign() + return d.refreshCaptchaToken(action, metas) +} + +// GetCaptchaSign 获取验证码签名 +func (c *Common) GetCaptchaSign() (timestamp, sign string) { + timestamp = fmt.Sprint(time.Now().UnixMilli()) + str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.DeviceID, timestamp) + for _, algorithm := range c.Algorithms { + str = utils.GetMD5EncodeStr(str + algorithm) + } + sign = "1." + str + return +} + +// refreshCaptchaToken 刷新CaptchaToken +func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error { + param := CaptchaTokenRequest{ + Action: action, + CaptchaToken: d.GetCaptchaToken(), + ClientID: d.ClientID, + DeviceID: d.GetDeviceID(), + Meta: metas, + } + 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 errors.New(e.Error()) + } + + //if resp.Url != "" { + // return fmt.Errorf(`need verify: Click Here`, resp.Url) + //} + + if d.Common.RefreshCTokenCk != nil { + d.Common.RefreshCTokenCk(resp.CaptchaToken) + } + d.Common.SetCaptchaToken(resp.CaptchaToken) + return nil +}