diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index 8b72c3638..5ec61b85e 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -7,12 +7,14 @@ import (
"net/http"
"strconv"
"strings"
+ "sync"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
streamPkg "github.com/OpenListTeam/OpenList/v4/internal/stream"
+ "github.com/OpenListTeam/OpenList/v4/pkg/singleflight"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
hash_extend "github.com/OpenListTeam/OpenList/v4/pkg/utils/hash"
"github.com/go-resty/resty/v2"
@@ -25,6 +27,9 @@ type PikPak struct {
*Common
RefreshToken string
AccessToken string
+ authMu sync.RWMutex
+ authG singleflight.Group[struct{}]
+ persistMu sync.Mutex
}
func (d *PikPak) Config() driver.Config {
@@ -35,81 +40,58 @@ func (d *PikPak) GetAddition() driver.Additional {
return &d.Addition
}
+func (d *PikPak) saveStorage(update func()) {
+ d.persistMu.Lock()
+ defer d.persistMu.Unlock()
+ if update != nil {
+ update()
+ }
+ op.MustSaveDriverStorage(d)
+}
+
func (d *PikPak) Init(ctx context.Context) (err error) {
if d.Common == nil {
d.Common = &Common{
- client: base.NewRestyClient(),
- CaptchaToken: "",
- UserID: "",
- DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password),
- UserAgent: "",
- RefreshCTokenCk: func(token string) {
- d.Common.CaptchaToken = token
- op.MustSaveDriverStorage(d)
- },
+ client: base.NewRestyClient(),
+ UserID: "",
+ DeviceID: genDeviceID(),
+ UserAgent: "",
}
}
- if d.Platform == "android" {
- d.ClientID = AndroidClientID
- d.ClientSecret = AndroidClientSecret
- d.ClientVersion = AndroidClientVersion
- d.PackageName = AndroidPackageName
- d.Algorithms = AndroidAlgorithms
- d.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), 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"
- } else if d.Platform == "pc" {
- d.ClientID = PCClientID
- d.ClientSecret = PCClientSecret
- d.ClientVersion = PCClientVersion
- d.PackageName = PCPackageName
- d.Algorithms = PCAlgorithms
- d.UserAgent = "MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.6.11.4955 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36"
+ 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; rv:129.0) Gecko/20100101 Firefox/129.0"
+ if d.Platform == "web" {
+ d.saveStorage(func() {
+ d.Platform = ""
+ })
+ } else if d.Platform != "" {
+ return fmt.Errorf("legacy pikpak %q profile was removed; recreate this storage with the current PikPak driver settings", d.Platform)
}
- if d.Addition.CaptchaToken != "" && d.Addition.RefreshToken == "" {
- d.SetCaptchaToken(d.Addition.CaptchaToken)
+ if d.Addition.RefreshToken != "" {
+ d.setRefreshTokenState(d.Addition.RefreshToken)
}
if d.Addition.DeviceID != "" {
d.SetDeviceID(d.Addition.DeviceID)
} else {
- d.Addition.DeviceID = d.Common.DeviceID
- op.MustSaveDriverStorage(d)
- }
- // 如果已经有RefreshToken,直接获取AccessToken
- if d.Addition.RefreshToken != "" {
- if err = d.refreshToken(d.Addition.RefreshToken); err != nil {
- return err
- }
- } else {
- // 如果没有填写RefreshToken,尝试登录 获取 refreshToken
- if err = d.login(); err != nil {
- return err
+ if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
+ d.SetDeviceID(genDeviceID())
}
+ d.saveStorage(func() {
+ d.Addition.DeviceID = d.GetDeviceID()
+ })
}
- // 获取CaptchaToken
- err = d.RefreshCaptchaTokenAtLogin(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/files"), d.Common.GetUserID())
- if err != nil {
+ if err = d.ensureAuthorized(false, ""); err != nil {
return err
}
- // 更新UserAgent
- if d.Platform == "android" {
- d.Common.UserAgent = BuildCustomUserAgent(utils.GetMD5EncodeStr(d.Username+d.Password), AndroidClientID, AndroidPackageName, AndroidSdkVersion, AndroidClientVersion, AndroidPackageName, d.Common.UserID)
- }
-
- // 保存 有效的 RefreshToken
- d.Addition.RefreshToken = d.RefreshToken
- op.MustSaveDriverStorage(d)
-
return nil
}
@@ -246,11 +228,6 @@ func (d *PikPak) Put(ctx context.Context, dstDir model.Obj, stream model.FileStr
}
params := resp.Resumable.Params
- // endpoint := strings.Join(strings.Split(params.Endpoint, ".")[1:], ".")
- // web 端上传 返回的endpoint 为 `mypikpak.net` | android 端上传 返回的endpoint 为 `vip-lixian-07.mypikpak.net`·
- if d.Addition.Platform == "android" {
- params.Endpoint = "mypikpak.net"
- }
if stream.GetSize() <= 10*utils.MB { // 文件大小 小于10MB,改用普通模式上传
return d.UploadByOSS(ctx, ¶ms, stream, up)
diff --git a/drivers/pikpak/meta.go b/drivers/pikpak/meta.go
index c602bd2a1..21f348dd2 100644
--- a/drivers/pikpak/meta.go
+++ b/drivers/pikpak/meta.go
@@ -9,16 +9,16 @@ type Addition struct {
driver.RootID
Username string `json:"username" required:"true"`
Password string `json:"password" required:"true"`
- Platform string `json:"platform" required:"true" default:"web" type:"select" options:"android,web,pc"`
- RefreshToken string `json:"refresh_token" required:"true" default:""`
- CaptchaToken string `json:"captcha_token" default:""`
- DeviceID string `json:"device_id" required:"false" default:""`
+ Platform string `json:"platform" ignore:"true" default:""`
+ RefreshToken string `json:"refresh_token" ignore:"true" default:""`
+ DeviceID string `json:"device_id" ignore:"true" default:""`
DisableMediaLink bool `json:"disable_media_link" default:"true"`
}
var config = driver.Config{
- Name: "PikPak",
- LocalSort: true,
+ Name: "PikPak",
+ LocalSort: true,
+ PreferProxy: true,
}
func init() {
diff --git a/drivers/pikpak/types.go b/drivers/pikpak/types.go
index 6ae78a455..851f2b74e 100644
--- a/drivers/pikpak/types.go
+++ b/drivers/pikpak/types.go
@@ -196,6 +196,13 @@ type CaptchaTokenResponse struct {
Url string `json:"url"`
}
+func (c *CaptchaTokenResponse) Expiry() time.Time {
+ if c == nil || c.ExpiresIn <= 0 {
+ return time.Time{}
+ }
+ return time.Now().Add(time.Duration(c.ExpiresIn) * time.Second)
+}
+
type AboutResponse struct {
Quota struct {
Limit string `json:"limit"`
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index 1d091217a..bd1d4e41e 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -3,9 +3,7 @@ package pikpak
import (
"bytes"
"context"
- "crypto/md5"
- "crypto/sha1"
- "encoding/hex"
+ "crypto/rand"
"fmt"
"io"
"net/http"
@@ -20,7 +18,6 @@ import (
"github.com/OpenListTeam/OpenList/v4/internal/driver"
"github.com/OpenListTeam/OpenList/v4/internal/model"
netutil "github.com/OpenListTeam/OpenList/v4/internal/net"
- "github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"github.com/go-resty/resty/v2"
@@ -28,17 +25,6 @@ import (
"github.com/pkg/errors"
)
-var AndroidAlgorithms = []string{
- "SOP04dGzk0TNO7t7t9ekDbAmx+eq0OI1ovEx",
- "nVBjhYiND4hZ2NCGyV5beamIr7k6ifAsAbl",
- "Ddjpt5B/Cit6EDq2a6cXgxY9lkEIOw4yC1GDF28KrA",
- "VVCogcmSNIVvgV6U+AochorydiSymi68YVNGiz",
- "u5ujk5sM62gpJOsB/1Gu/zsfgfZO",
- "dXYIiBOAHZgzSruaQ2Nhrqc2im",
- "z5jUTBSIpBN9g4qSJGlidNAutX6",
- "KJE2oveZ34du/g1tiimm",
-}
-
var WebAlgorithms = []string{
"C9qPpZLN8ucRTaTiUMWYS9cQvWOE",
"+r6CQVxjzJV6LCV",
@@ -57,19 +43,6 @@ var WebAlgorithms = []string{
"NhXXU9rg4XXdzo7u5o",
}
-var PCAlgorithms = []string{
- "KHBJ07an7ROXDoK7Db",
- "G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE",
- "JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb",
- "fQnw/AmSlbbI91Ik15gpddGgyU7U",
- "/Dv9JdPYSj3sHiWjouR95NTQff",
- "yGx2zuTjbWENZqecNI+edrQgqmZKP",
- "ljrbSzdHLwbqcRn",
- "lSHAsqCkGDGxQqqwrVu",
- "TsWXI81fD1",
- "vk7hBjawK/rOSrSWajtbMk95nfgf3",
-}
-
const (
OSSUserAgent = "aliyun-sdk-android/2.9.13(Linux/Android 14/M2004j7ac;UKQ1.231108.001)"
OssSecurityTokenHeaderName = "X-OSS-Security-Token"
@@ -77,59 +50,81 @@ const (
)
const (
- AndroidClientID = "YNxT9w7GMdWvEOKa"
- AndroidClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
- AndroidClientVersion = "1.53.2"
- AndroidPackageName = "com.pikcloud.pikpak"
- AndroidSdkVersion = "2.0.6.206003"
- WebClientID = "YUMx5nI8ZU8Ap8pm"
- WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
- WebClientVersion = "2.0.0"
- WebPackageName = "mypikpak.com"
- WebSdkVersion = "8.0.3"
- PCClientID = "YvtoWO6GNHiuCl7x"
- PCClientSecret = "1NIH5R1IEe2pAxZE3hv3uA"
- PCClientVersion = "undefined" // 2.6.11.4955
- PCPackageName = "mypikpak.com"
- PCSdkVersion = "8.0.3"
+ WebClientID = "YUMx5nI8ZU8Ap8pm"
+ WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
+ WebClientVersion = "2.0.0"
+ WebPackageName = "mypikpak.com"
)
-func (d *PikPak) login() error {
+func genDeviceID() string {
+ base := []byte("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
+ random := make([]byte, len(base))
+ if _, err := rand.Read(random); err != nil {
+ return utils.GetMD5EncodeStr(fmt.Sprintf("%d", time.Now().UnixNano()))
+ }
+ for i, char := range base {
+ switch char {
+ case 'x':
+ base[i] = "0123456789abcdef"[random[i]&0x0f]
+ case 'y':
+ base[i] = "0123456789abcdef"[random[i]&0x03|0x08]
+ }
+ }
+ return string(base)
+}
+
+func (d *PikPak) loginRaw() error {
// 检查用户名和密码是否为空
if d.Addition.Username == "" || d.Addition.Password == "" {
return errors.New("username or password is empty")
}
url := "https://user.mypikpak.net/v1/auth/signin"
- // 使用 用户填写的 CaptchaToken —————— (验证后的captcha_token)
- if d.GetCaptchaToken() == "" {
- if err := d.RefreshCaptchaTokenInLogin(GetAction(http.MethodPost, url), d.Username); err != nil {
+ action := GetAction(http.MethodPost, url)
+ var loginCaptchaToken string
+ doLogin := func() error {
+ var err error
+ loginCaptchaToken, err = d.ensureCaptchaTokenInLogin(action, d.Username)
+ if err != nil {
return err
}
+ var e ErrResp
+ res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).SetBody(base.Json{
+ "captcha_token": loginCaptchaToken,
+ "client_id": d.ClientID,
+ "client_secret": d.ClientSecret,
+ "username": d.Username,
+ "password": d.Password,
+ }).SetQueryParam("client_id", d.ClientID).Post(url)
+ if err != nil {
+ return err
+ }
+ if e.ErrorCode != 0 {
+ return &e
+ }
+ data := res.Body()
+ refreshToken := jsoniter.Get(data, "refresh_token").ToString()
+ accessToken := jsoniter.Get(data, "access_token").ToString()
+ d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
+ d.setAuthTokens(accessToken, refreshToken)
+ d.saveStorage(func() {
+ d.Addition.RefreshToken = refreshToken
+ })
+ return nil
+ }
+
+ err := doLogin()
+ if apiErr, ok := err.(*ErrResp); ok && apiErr.ErrorCode == 9 {
+ d.Common.invalidateCaptchaToken()
+ if _, err = d.ensureCaptchaTokenInLogin(action, d.Username); err != nil {
+ return err
+ }
+ return doLogin()
}
-
- var e ErrResp
- res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).SetBody(base.Json{
- "captcha_token": d.GetCaptchaToken(),
- "client_id": d.ClientID,
- "client_secret": d.ClientSecret,
- "username": d.Username,
- "password": d.Password,
- }).SetQueryParam("client_id", d.ClientID).Post(url)
- if err != nil {
- return err
- }
- if e.ErrorCode != 0 {
- return &e
- }
- data := res.Body()
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
- d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
- return nil
+ return err
}
-func (d *PikPak) refreshToken(refreshToken string) error {
+func (d *PikPak) refreshTokenRaw(refreshToken string) error {
url := "https://user.mypikpak.net/v1/auth/token"
var e ErrResp
res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).
@@ -140,8 +135,9 @@ func (d *PikPak) refreshToken(refreshToken string) error {
"refresh_token": refreshToken,
}).SetQueryParam("client_id", d.ClientID).Post(url)
if err != nil {
- d.Status = err.Error()
- op.MustSaveDriverStorage(d)
+ d.saveStorage(func() {
+ d.Status = err.Error()
+ })
return err
}
if e.ErrorCode != 0 {
@@ -151,67 +147,141 @@ func (d *PikPak) refreshToken(refreshToken string) error {
return errors.New("refresh_token invalid, please re-provide refresh_token")
} else {
// refresh_token invalid, re-login
- return d.login()
+ return d.loginRaw()
}
}
- d.Status = e.Error()
- op.MustSaveDriverStorage(d)
+ d.saveStorage(func() {
+ d.Status = e.Error()
+ })
return errors.New(e.Error())
}
data := res.Body()
- d.Status = "work"
- d.RefreshToken = jsoniter.Get(data, "refresh_token").ToString()
- d.AccessToken = jsoniter.Get(data, "access_token").ToString()
+ refreshToken = jsoniter.Get(data, "refresh_token").ToString()
+ accessToken := jsoniter.Get(data, "access_token").ToString()
d.Common.SetUserID(jsoniter.Get(data, "sub").ToString())
- d.Addition.RefreshToken = d.RefreshToken
- op.MustSaveDriverStorage(d)
+ d.setAuthTokens(accessToken, refreshToken)
+ d.saveStorage(func() {
+ d.Status = "work"
+ d.Addition.RefreshToken = refreshToken
+ })
return nil
}
-func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- req := base.RestyClient.R()
- req.SetHeaders(map[string]string{
- //"Authorization": "Bearer " + d.AccessToken,
- "User-Agent": d.GetUserAgent(),
- "X-Device-ID": d.GetDeviceID(),
- "X-Captcha-Token": d.GetCaptchaToken(),
- })
- if d.AccessToken != "" {
- req.SetHeader("Authorization", "Bearer "+d.AccessToken)
+func (d *PikPak) authorizeRaw() error {
+ if refreshToken := d.getRefreshToken(); refreshToken != "" {
+ return d.refreshTokenRaw(refreshToken)
}
+ return d.loginRaw()
+}
- if callback != nil {
- callback(req)
+func (d *PikPak) ensureAuthorized(force bool, staleAccessToken string) error {
+ if !force && d.getAccessToken() != "" {
+ return nil
}
- if resp != nil {
- req.SetResult(resp)
- }
- var e ErrResp
- req.SetError(&e)
- res, err := req.Execute(method, url)
- if err != nil {
- return nil, err
+ if force && staleAccessToken != "" {
+ if currentAccessToken := d.getAccessToken(); currentAccessToken != "" && currentAccessToken != staleAccessToken {
+ return nil
+ }
}
- switch e.ErrorCode {
+ _, err, _ := d.authG.Do("auth", func() (struct{}, error) {
+ if !force && d.getAccessToken() != "" {
+ return struct{}{}, nil
+ }
+ if force && staleAccessToken != "" {
+ if currentAccessToken := d.getAccessToken(); currentAccessToken != "" && currentAccessToken != staleAccessToken {
+ return struct{}{}, nil
+ }
+ }
+ return struct{}{}, d.authorizeRaw()
+ })
+ return err
+}
+
+type requestRetryAction uint8
+
+const (
+ requestRetryNone requestRetryAction = iota
+ requestRetryAuth
+ requestRetryCaptcha
+)
+
+func classifyRequestError(errResp *ErrResp) (requestRetryAction, error) {
+ switch errResp.ErrorCode {
case 0:
- return res.Body(), nil
+ return requestRetryNone, nil
case 4122, 4121, 16:
- // access_token 过期
- if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
- return nil, err1
+ return requestRetryAuth, nil
+ case 9:
+ return requestRetryCaptcha, nil
+ case 10:
+ return requestRetryNone, errors.New(errResp.ErrorDescription)
+ default:
+ return requestRetryNone, errors.New(errResp.Error())
+ }
+}
+
+func (d *PikPak) recoverRequest(action requestRetryAction, reqAction, staleAccessToken, staleCaptchaToken string) error {
+ switch action {
+ case requestRetryAuth:
+ return d.ensureAuthorized(true, staleAccessToken)
+ case requestRetryCaptcha:
+ d.Common.invalidateCaptchaToken()
+ _, err := d.ensureCaptchaTokenAtLogin(reqAction, d.GetUserID())
+ return err
+ default:
+ return nil
+ }
+}
+
+func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ reqAction := GetAction(method, url)
+ for attempts := 0; attempts < 3; attempts++ {
+ if err := d.ensureAuthorized(false, ""); err != nil {
+ return nil, err
}
- return d.request(url, method, callback, resp)
- case 9: // 验证码token过期
- if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
+ captchaToken, err := d.ensureCaptchaTokenAtLogin(reqAction, d.GetUserID())
+ if err != nil {
+ return nil, err
+ }
+
+ req := base.RestyClient.R()
+ req.SetHeaders(map[string]string{
+ //"Authorization": "Bearer " + d.AccessToken,
+ "User-Agent": d.GetUserAgent(),
+ "X-Device-ID": d.GetDeviceID(),
+ "X-Captcha-Token": captchaToken,
+ })
+ accessToken := d.getAccessToken()
+ if accessToken != "" {
+ req.SetHeader("Authorization", "Bearer "+accessToken)
+ }
+
+ if callback != nil {
+ callback(req)
+ }
+ if resp != nil {
+ req.SetResult(resp)
+ }
+ var e ErrResp
+ req.SetError(&e)
+ res, err := req.Execute(method, url)
+ if err != nil {
+ return nil, err
+ }
+
+ retryAction, err := classifyRequestError(&e)
+ if err != nil {
+ return nil, err
+ }
+ if retryAction == requestRetryNone {
+ return res.Body(), nil
+ }
+ if err := d.recoverRequest(retryAction, reqAction, accessToken, captchaToken); 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 nil, errors.New("request retry limit exceeded")
}
func (d *PikPak) getFiles(id string) ([]File, error) {
@@ -248,9 +318,10 @@ func GetAction(method string, url string) string {
}
type Common struct {
- client *resty.Client
- CaptchaToken string
- UserID string
+ client *resty.Client
+ CaptchaToken string
+ CaptchaExpiry time.Time
+ UserID string
// 必要值,签名相关
ClientID string
ClientSecret string
@@ -259,69 +330,21 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
+ stateMu sync.RWMutex
+ refreshMu sync.Mutex
// 验证码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.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.DeviceID = deviceID
}
func (c *Common) SetUserID(userID string) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.UserID = userID
}
@@ -329,38 +352,99 @@ 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 {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
return c.DeviceID
}
func (c *Common) GetUserID() string {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
return c.UserID
}
-// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
-func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
+func (c *Common) captchaSnapshot() (token string, expiry time.Time, deviceID, userID string) {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
+ return c.CaptchaToken, c.CaptchaExpiry, c.DeviceID, c.UserID
+}
+
+func (c *Common) storeCaptchaState(token string, expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ c.CaptchaToken = token
+ c.CaptchaExpiry = expiry
+}
+
+func (c *Common) invalidateCaptchaToken() {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ c.CaptchaToken = ""
+ c.CaptchaExpiry = time.Time{}
+}
+
+func (c *Common) invalidateCaptchaTokenIfMatch(expectedToken string) bool {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ if c.CaptchaToken != expectedToken {
+ return false
+ }
+ c.CaptchaToken = ""
+ c.CaptchaExpiry = time.Time{}
+ return true
+}
+
+func hasValidCaptchaToken(token string, expiry time.Time) bool {
+ if token == "" {
+ return false
+ }
+ if expiry.IsZero() {
+ return true
+ }
+ return time.Now().Before(expiry.Add(-10 * time.Second))
+}
+
+func (d *PikPak) getAccessToken() string {
+ d.authMu.RLock()
+ defer d.authMu.RUnlock()
+ return d.AccessToken
+}
+
+func (d *PikPak) getRefreshToken() string {
+ d.authMu.RLock()
+ defer d.authMu.RUnlock()
+ return d.RefreshToken
+}
+
+func (d *PikPak) setRefreshTokenState(refreshToken string) {
+ d.authMu.Lock()
+ defer d.authMu.Unlock()
+ d.RefreshToken = refreshToken
+}
+
+func (d *PikPak) setAuthTokens(accessToken, refreshToken string) {
+ d.authMu.Lock()
+ defer d.authMu.Unlock()
+ d.AccessToken = accessToken
+ d.RefreshToken = refreshToken
+}
+
+func (d *PikPak) authorizedCaptchaMetas(userID string) map[string]string {
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)
+ return metas
}
-// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
-func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
+func (d *PikPak) loginCaptchaMetas(username string) map[string]string {
metas := make(map[string]string)
if ok, _ := regexp.MatchString(`\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*`, username); ok {
metas["email"] = username
@@ -369,13 +453,43 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
} else {
metas["username"] = username
}
- return d.refreshCaptchaToken(action, metas)
+ return metas
+}
+
+// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
+func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
+ _, err := d.ensureCaptchaTokenAtLogin(action, userID)
+ return err
+}
+
+func (d *PikPak) ensureCaptchaTokenAtLogin(action, userID string) (string, error) {
+ metas := d.authorizedCaptchaMetas(userID)
+ previousCaptchaToken, _, _, _ := d.Common.captchaSnapshot()
+ accessToken := d.getAccessToken()
+ token, err := d.refreshCaptchaToken(action, metas, accessToken)
+ if apiErr, ok := err.(*ErrResp); ok && isAuthExpiredErrorCode(apiErr.ErrorCode) {
+ if err = d.ensureAuthorized(true, accessToken); err != nil {
+ return "", err
+ }
+ return d.refreshCaptchaTokenAfterReauth(action, metas, previousCaptchaToken)
+ }
+ return token, err
+}
+
+// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
+func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
+ _, err := d.ensureCaptchaTokenInLogin(action, username)
+ return err
+}
+
+func (d *PikPak) ensureCaptchaTokenInLogin(action, username string) (string, error) {
+ return d.refreshCaptchaToken(action, d.loginCaptchaMetas(username), "")
}
// 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)
+ str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.GetDeviceID(), timestamp)
for _, algorithm := range c.Algorithms {
str = utils.GetMD5EncodeStr(str + algorithm)
}
@@ -383,39 +497,84 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
return
}
-// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
+func isAuthExpiredErrorCode(code int64) bool {
+ return code == 4122 || code == 4121 || code == 16
+}
+
+func (d *PikPak) initCaptchaToken(action string, metas map[string]string, oldToken, deviceID, accessToken string) (ErrResp, CaptchaTokenResponse, error) {
+ e := ErrResp{}
+ resp := CaptchaTokenResponse{}
param := CaptchaTokenRequest{
Action: action,
- CaptchaToken: d.GetCaptchaToken(),
+ CaptchaToken: oldToken,
ClientID: d.ClientID,
- DeviceID: d.GetDeviceID(),
+ DeviceID: deviceID,
Meta: metas,
RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
}
- var e ErrResp
- var resp CaptchaTokenResponse
- _, err := d.request("https://user.mypikpak.net/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
- req.SetError(&e).SetBody(param).SetQueryParam("client_id", d.ClientID)
- }, &resp)
+ req := base.RestyClient.R().
+ SetHeaders(map[string]string{
+ "User-Agent": d.GetUserAgent(),
+ "X-Device-ID": deviceID,
+ }).
+ SetError(&e).
+ SetResult(&resp).
+ SetBody(param).
+ SetQueryParam("client_id", d.ClientID)
+ if accessToken != "" {
+ req.SetHeader("Authorization", "Bearer "+accessToken)
+ }
+ _, err := req.Execute(http.MethodPost, "https://user.mypikpak.net/v1/shield/captcha/init")
+ return e, resp, err
+}
- if err != nil {
- return err
+func (d *PikPak) finishCaptchaTokenRefresh(errResp *ErrResp, resp *CaptchaTokenResponse) (string, error) {
+ if errResp.IsError() {
+ return "", errResp
+ }
+ if resp.Url != "" {
+ return "", fmt.Errorf(`need verify: Click Here`, resp.Url)
}
- if e.IsError() {
- return errors.New(e.Error())
+ d.Common.storeCaptchaState(resp.CaptchaToken, resp.Expiry())
+ refreshCTokenCk := d.Common.RefreshCTokenCk
+ if refreshCTokenCk != nil {
+ refreshCTokenCk(resp.CaptchaToken)
}
+ return resp.CaptchaToken, nil
+}
- if resp.Url != "" {
- return fmt.Errorf(`need verify: Click Here`, resp.Url)
+func (d *PikPak) refreshCaptchaTokenAfterReauth(action string, metas map[string]string, previousCaptchaToken string) (string, error) {
+ d.Common.refreshMu.Lock()
+ defer d.Common.refreshMu.Unlock()
+
+ currentToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
+ if currentToken != previousCaptchaToken && hasValidCaptchaToken(currentToken, expiry) {
+ return currentToken, nil
}
- if d.Common.RefreshCTokenCk != nil {
- d.Common.RefreshCTokenCk(resp.CaptchaToken)
+ e, resp, err := d.initCaptchaToken(action, metas, "", deviceID, d.getAccessToken())
+ if err != nil {
+ return "", err
}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
- return nil
+ return d.finishCaptchaTokenRefresh(&e, &resp)
+}
+
+// refreshCaptchaToken 刷新CaptchaToken
+func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string, accessToken string) (string, error) {
+ d.Common.refreshMu.Lock()
+ defer d.Common.refreshMu.Unlock()
+
+ oldToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
+ if hasValidCaptchaToken(oldToken, expiry) {
+ return oldToken, nil
+ }
+
+ e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID, accessToken)
+ if err != nil {
+ return "", err
+ }
+ return d.finishCaptchaTokenRefresh(&e, &resp)
}
func (d *PikPak) UploadByOSS(ctx context.Context, params *S3Params, s model.FileStreamer, up driver.UpdateProgress) error {
diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go
index d6323d18d..fecaeb750 100644
--- a/drivers/pikpak_share/driver.go
+++ b/drivers/pikpak_share/driver.go
@@ -2,8 +2,9 @@ package pikpak_share
import (
"context"
+ "fmt"
"net/http"
- "time"
+ "sync"
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -18,6 +19,8 @@ type PikPakShare struct {
Addition
*Common
PassCodeToken string
+ passCodeMu sync.RWMutex
+ persistMu sync.Mutex
}
func (d *PikPakShare) Config() driver.Config {
@@ -28,53 +31,60 @@ func (d *PikPakShare) GetAddition() driver.Additional {
return &d.Addition
}
+func (d *PikPakShare) saveStorage(update func()) {
+ d.persistMu.Lock()
+ defer d.persistMu.Unlock()
+ if update != nil {
+ update()
+ }
+ op.MustSaveDriverStorage(d)
+}
+
+func (d *PikPakShare) SetPassCodeToken(token string) {
+ d.passCodeMu.Lock()
+ defer d.passCodeMu.Unlock()
+ d.PassCodeToken = token
+}
+
+func (d *PikPakShare) GetPassCodeToken() string {
+ d.passCodeMu.RLock()
+ defer d.passCodeMu.RUnlock()
+ return d.PassCodeToken
+}
+
func (d *PikPakShare) Init(ctx context.Context) error {
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)
- },
+ captchaStates: make(map[string]captchaState),
+ DeviceID: genDeviceID(),
+ UserAgent: "",
}
}
+ if d.Platform == "web" {
+ d.saveStorage(func() {
+ d.Platform = ""
+ })
+ } else if d.Platform != "" {
+ return fmt.Errorf("legacy pikpak_share %q profile was removed; recreate this storage with the current PikPakShare driver settings", d.Platform)
+ }
if d.Addition.DeviceID != "" {
d.SetDeviceID(d.Addition.DeviceID)
} else {
- d.Addition.DeviceID = d.Common.DeviceID
- op.MustSaveDriverStorage(d)
- }
-
- 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"
- } else if d.Platform == "pc" {
- d.ClientID = PCClientID
- d.ClientSecret = PCClientSecret
- d.ClientVersion = PCClientVersion
- d.PackageName = PCPackageName
- d.Algorithms = PCAlgorithms
- d.UserAgent = "MainWindow Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) PikPak/2.6.11.4955 Chrome/100.0.4896.160 Electron/18.3.15 Safari/537.36"
+ if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
+ d.SetDeviceID(genDeviceID())
+ }
+ d.saveStorage(func() {
+ d.Addition.DeviceID = d.GetDeviceID()
+ })
}
- // 获取CaptchaToken
- err := d.RefreshCaptchaToken(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/share:batch_file_info"), "")
- if err != nil {
- return err
- }
+ 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; rv:129.0) Gecko/20100101 Firefox/129.0"
if d.SharePwd != "" {
return d.getSharePassToken()
@@ -102,7 +112,7 @@ func (d *PikPakShare) Link(ctx context.Context, file model.Obj, args model.LinkA
query := map[string]string{
"share_id": d.ShareId,
"file_id": file.GetID(),
- "pass_code_token": d.PassCodeToken,
+ "pass_code_token": d.GetPassCodeToken(),
}
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share/file_info", http.MethodGet, func(req *resty.Request) {
req.SetQueryParams(query)
diff --git a/drivers/pikpak_share/meta.go b/drivers/pikpak_share/meta.go
index 18842de23..49bb7030c 100644
--- a/drivers/pikpak_share/meta.go
+++ b/drivers/pikpak_share/meta.go
@@ -9,15 +9,16 @@ type Addition struct {
driver.RootID
ShareId string `json:"share_id" required:"true"`
SharePwd string `json:"share_pwd"`
- Platform string `json:"platform" default:"web" required:"true" type:"select" options:"android,web,pc"`
- DeviceID string `json:"device_id" required:"false" default:""`
+ Platform string `json:"platform" ignore:"true" default:""`
+ DeviceID string `json:"device_id" ignore:"true" default:""`
UseTransCodingAddress bool `json:"use_transcoding_address" required:"true" default:"false"`
}
var config = driver.Config{
- Name: "PikPakShare",
- LocalSort: true,
- NoUpload: true,
+ Name: "PikPakShare",
+ LocalSort: true,
+ NoUpload: true,
+ PreferProxy: true,
}
func init() {
diff --git a/drivers/pikpak_share/types.go b/drivers/pikpak_share/types.go
index 285d6cfd9..e5d2628ba 100644
--- a/drivers/pikpak_share/types.go
+++ b/drivers/pikpak_share/types.go
@@ -90,6 +90,13 @@ type CaptchaTokenResponse struct {
Url string `json:"url"`
}
+func (c *CaptchaTokenResponse) Expiry() time.Time {
+ if c == nil || c.ExpiresIn <= 0 {
+ return time.Time{}
+ }
+ return time.Now().Add(time.Duration(c.ExpiresIn) * time.Second)
+}
+
type ErrResp struct {
ErrorCode int64 `json:"error_code"`
ErrorMsg string `json:"error"`
diff --git a/drivers/pikpak_share/util.go b/drivers/pikpak_share/util.go
index 4fb880dfe..92139bb42 100644
--- a/drivers/pikpak_share/util.go
+++ b/drivers/pikpak_share/util.go
@@ -1,14 +1,12 @@
package pikpak_share
import (
- "crypto/md5"
- "crypto/sha1"
- "encoding/hex"
+ "crypto/rand"
"errors"
"fmt"
"net/http"
"regexp"
- "strings"
+ "sync"
"time"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
@@ -17,17 +15,6 @@ import (
"github.com/go-resty/resty/v2"
)
-var AndroidAlgorithms = []string{
- "SOP04dGzk0TNO7t7t9ekDbAmx+eq0OI1ovEx",
- "nVBjhYiND4hZ2NCGyV5beamIr7k6ifAsAbl",
- "Ddjpt5B/Cit6EDq2a6cXgxY9lkEIOw4yC1GDF28KrA",
- "VVCogcmSNIVvgV6U+AochorydiSymi68YVNGiz",
- "u5ujk5sM62gpJOsB/1Gu/zsfgfZO",
- "dXYIiBOAHZgzSruaQ2Nhrqc2im",
- "z5jUTBSIpBN9g4qSJGlidNAutX6",
- "KJE2oveZ34du/g1tiimm",
-}
-
var WebAlgorithms = []string{
"C9qPpZLN8ucRTaTiUMWYS9cQvWOE",
"+r6CQVxjzJV6LCV",
@@ -46,71 +33,93 @@ var WebAlgorithms = []string{
"NhXXU9rg4XXdzo7u5o",
}
-var PCAlgorithms = []string{
- "KHBJ07an7ROXDoK7Db",
- "G6n399rSWkl7WcQmw5rpQInurc1DkLmLJqE",
- "JZD1A3M4x+jBFN62hkr7VDhkkZxb9g3rWqRZqFAAb",
- "fQnw/AmSlbbI91Ik15gpddGgyU7U",
- "/Dv9JdPYSj3sHiWjouR95NTQff",
- "yGx2zuTjbWENZqecNI+edrQgqmZKP",
- "ljrbSzdHLwbqcRn",
- "lSHAsqCkGDGxQqqwrVu",
- "TsWXI81fD1",
- "vk7hBjawK/rOSrSWajtbMk95nfgf3",
-}
-
const (
- AndroidClientID = "YNxT9w7GMdWvEOKa"
- AndroidClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
- AndroidClientVersion = "1.53.2"
- AndroidPackageName = "com.pikcloud.pikpak"
- AndroidSdkVersion = "2.0.6.206003"
- WebClientID = "YUMx5nI8ZU8Ap8pm"
- WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
- WebClientVersion = "2.0.0"
- WebPackageName = "mypikpak.com"
- WebSdkVersion = "8.0.3"
- PCClientID = "YvtoWO6GNHiuCl7x"
- PCClientSecret = "1NIH5R1IEe2pAxZE3hv3uA"
- PCClientVersion = "undefined" // 2.6.11.4955
- PCPackageName = "mypikpak.com"
- PCSdkVersion = "8.0.3"
+ WebClientID = "YUMx5nI8ZU8Ap8pm"
+ WebClientSecret = "dbw2OtmVEeuUvIptb1Coyg"
+ WebClientVersion = "2.0.0"
+ WebPackageName = "mypikpak.com"
)
-func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- req := base.RestyClient.R()
- 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)
+func genDeviceID() string {
+ base := []byte("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx")
+ random := make([]byte, len(base))
+ if _, err := rand.Read(random); err != nil {
+ return utils.GetMD5EncodeStr(fmt.Sprintf("%d", time.Now().UnixNano()))
}
- if resp != nil {
- req.SetResult(resp)
- }
- var e ErrResp
- req.SetError(&e)
- res, err := req.Execute(method, url)
- if err != nil {
- return nil, err
+ for i, char := range base {
+ switch char {
+ case 'x':
+ base[i] = "0123456789abcdef"[random[i]&0x0f]
+ case 'y':
+ base[i] = "0123456789abcdef"[random[i]&0x03|0x08]
+ }
}
- switch e.ErrorCode {
+ return string(base)
+}
+
+type requestRetryAction uint8
+
+const (
+ requestRetryNone requestRetryAction = iota
+ requestRetryCaptcha
+ maxSharePassRefreshesPerProgress = 8
+)
+
+func classifyRequestError(errResp *ErrResp) (requestRetryAction, error) {
+ switch errResp.ErrorCode {
case 0:
- return res.Body(), nil
- case 9: // 验证码token过期
- if err = d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
+ return requestRetryNone, nil
+ case 9:
+ return requestRetryCaptcha, nil
+ case 10:
+ return requestRetryNone, errors.New(errResp.ErrorDescription)
+ default:
+ return requestRetryNone, errors.New(errResp.Error())
+ }
+}
+
+func isPassCodeErrorStatus(status string) bool {
+ return status == "PASS_CODE_EMPTY" || status == "PASS_CODE_ERROR"
+}
+
+func (d *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ reqAction := GetAction(method, url)
+ for attempts := 0; attempts < 3; attempts++ {
+ captchaToken, err := d.ensureCaptchaToken(reqAction, "")
+ if 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())
+ req := base.RestyClient.R()
+ req.SetHeaders(map[string]string{
+ "User-Agent": d.GetUserAgent(),
+ "X-Client-ID": d.GetClientID(),
+ "X-Device-ID": d.GetDeviceID(),
+ "X-Captcha-Token": captchaToken,
+ })
+
+ if callback != nil {
+ callback(req)
+ }
+ if resp != nil {
+ req.SetResult(resp)
+ }
+ var e ErrResp
+ req.SetError(&e)
+ res, err := req.Execute(method, url)
+ if err != nil {
+ return nil, err
+ }
+
+ retryAction, err := classifyRequestError(&e)
+ if err != nil {
+ return nil, err
+ }
+ if retryAction == requestRetryNone {
+ return res.Body(), nil
+ }
+ d.Common.invalidateCaptchaTokenIfMatch(captchaToken, reqAction)
}
+ return nil, errors.New("request retry limit exceeded")
}
func (d *PikPakShare) getSharePassToken() error {
@@ -127,17 +136,20 @@ func (d *PikPakShare) getSharePassToken() error {
if err != nil {
return err
}
- d.PassCodeToken = resp.PassCodeToken
+ d.SetPassCodeToken(resp.PassCodeToken)
return nil
}
func (d *PikPakShare) getFiles(id string) ([]File, error) {
res := make([]File, 0)
pageToken := "first"
+ pagesFetched := 0
+ passRefreshesByProgress := make(map[int]int)
for pageToken != "" {
if pageToken == "first" {
pageToken = ""
}
+ currentPassCodeToken := d.GetPassCodeToken()
query := map[string]string{
"parent_id": id,
"share_id": d.ShareId,
@@ -146,7 +158,7 @@ func (d *PikPakShare) getFiles(id string) ([]File, error) {
"limit": "100",
"filters": `{"phase":{"eq":"PHASE_TYPE_COMPLETE"},"trashed":{"eq":false}}`,
"page_token": pageToken,
- "pass_code_token": d.PassCodeToken,
+ "pass_code_token": currentPassCodeToken,
}
var resp ShareResp
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
@@ -156,17 +168,36 @@ func (d *PikPakShare) getFiles(id string) ([]File, error) {
return nil, err
}
if resp.ShareStatus != "OK" {
- if resp.ShareStatus == "PASS_CODE_EMPTY" || resp.ShareStatus == "PASS_CODE_ERROR" {
- err = d.getSharePassToken()
- if err != nil {
- return nil, err
- }
- return d.getFiles(id)
+ if !isPassCodeErrorStatus(resp.ShareStatus) {
+ return nil, errors.New(resp.ShareStatusText)
+ }
+ latestPassCodeToken := d.GetPassCodeToken()
+ if latestPassCodeToken != "" && latestPassCodeToken != currentPassCodeToken {
+ res = make([]File, 0)
+ pageToken = "first"
+ pagesFetched = 0
+ continue
}
- return nil, errors.New(resp.ShareStatusText)
+ passRefreshesByProgress[pagesFetched]++
+ if passRefreshesByProgress[pagesFetched] > maxSharePassRefreshesPerProgress {
+ return nil, fmt.Errorf("share pass code token retry limit exceeded after %d fetched pages", pagesFetched)
+ }
+
+ if err = d.getSharePassToken(); err != nil {
+ return nil, err
+ }
+ newPassCodeToken := d.GetPassCodeToken()
+ if newPassCodeToken == "" {
+ return nil, errors.New(resp.ShareStatusText)
+ }
+ res = make([]File, 0)
+ pageToken = "first"
+ pagesFetched = 0
+ continue
}
pageToken = resp.NextPageToken
res = append(res, resp.Files...)
+ pagesFetched++
}
return res, nil
}
@@ -176,9 +207,14 @@ func GetAction(method string, url string) string {
return method + ":" + urlpath
}
+type captchaState struct {
+ Token string
+ Expiry time.Time
+}
+
type Common struct {
- client *resty.Client
- CaptchaToken string
+ client *resty.Client
+ captchaStates map[string]captchaState
// 必要值,签名相关
ClientID string
ClientSecret string
@@ -187,6 +223,8 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
+ stateMu sync.RWMutex
+ refreshMu sync.Mutex
// 验证码token刷新成功回调
RefreshCTokenCk func(token string)
}
@@ -195,16 +233,18 @@ 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.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.DeviceID = deviceID
}
-func (c *Common) GetCaptchaToken() string {
- return c.CaptchaToken
+func (c *Common) captchaTokenForAction(action string) (string, bool) {
+ token, expiry, _ := c.captchaSnapshot(action)
+ if !hasValidCaptchaToken(token, expiry) {
+ return "", false
+ }
+ return token, true
}
func (c *Common) GetClientID() string {
@@ -216,78 +256,82 @@ func (c *Common) GetUserAgent() string {
}
func (c *Common) GetDeviceID() string {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
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)
+func (c *Common) captchaSnapshot(action string) (token string, expiry time.Time, deviceID string) {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
+ state := c.captchaStates[action]
+ return state.Token, state.Expiry, c.DeviceID
+}
- md5String := hex.EncodeToString(md5Result)
+func (c *Common) setCaptchaState(action, token string, expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ if token == "" {
+ delete(c.captchaStates, action)
+ return
+ }
+ if c.captchaStates == nil {
+ c.captchaStates = make(map[string]captchaState)
+ }
+ c.captchaStates[action] = captchaState{Token: token, Expiry: expiry}
+}
- deviceSign := fmt.Sprintf("div101.%s%s", deviceID, md5String)
+func (c *Common) invalidateCaptchaToken() {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ clear(c.captchaStates)
+}
- return deviceSign
+func (c *Common) invalidateCaptchaTokenIfMatch(expectedToken, expectedAction string) bool {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ state, ok := c.captchaStates[expectedAction]
+ if !ok || state.Token != expectedToken {
+ return false
+ }
+ delete(c.captchaStates, expectedAction)
+ return true
}
-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 hasValidCaptchaToken(token string, expiry time.Time) bool {
+ if token == "" {
+ return false
+ }
+ if expiry.IsZero() {
+ return true
+ }
+ return time.Now().Before(expiry.Add(-10 * time.Second))
}
-// RefreshCaptchaToken 刷新验证码token
-func (d *PikPakShare) RefreshCaptchaToken(action, userID string) error {
+func (d *PikPakShare) captchaMetas(userID string) map[string]string {
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)
+ return metas
+}
+
+// RefreshCaptchaToken 刷新验证码token
+func (d *PikPakShare) RefreshCaptchaToken(action, userID string) error {
+ _, err := d.ensureCaptchaToken(action, userID)
+ return err
+}
+
+func (d *PikPakShare) ensureCaptchaToken(action, userID string) (string, error) {
+ return d.refreshCaptchaToken(action, d.captchaMetas(userID))
}
// 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)
+ str := fmt.Sprint(c.ClientID, c.ClientVersion, c.PackageName, c.GetDeviceID(), timestamp)
for _, algorithm := range c.Algorithms {
str = utils.GetMD5EncodeStr(str + algorithm)
}
@@ -295,36 +339,55 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
return
}
-// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {
+func (d *PikPakShare) initCaptchaToken(action string, metas map[string]string, oldToken, deviceID string) (ErrResp, CaptchaTokenResponse, error) {
+ e := ErrResp{}
+ resp := CaptchaTokenResponse{}
param := CaptchaTokenRequest{
Action: action,
- CaptchaToken: d.GetCaptchaToken(),
+ CaptchaToken: oldToken,
ClientID: d.ClientID,
- DeviceID: d.GetDeviceID(),
+ DeviceID: deviceID,
Meta: metas,
}
- var e ErrResp
- var resp CaptchaTokenResponse
- _, err := d.request("https://user.mypikpak.net/v1/shield/captcha/init", http.MethodPost, func(req *resty.Request) {
- req.SetError(&e).SetBody(param)
- }, &resp)
+ req := base.RestyClient.R().
+ SetHeaders(map[string]string{
+ "User-Agent": d.GetUserAgent(),
+ "X-Client-ID": d.GetClientID(),
+ "X-Device-ID": deviceID,
+ }).
+ SetError(&e).
+ SetResult(&resp).
+ SetBody(param)
+ _, err := req.Execute(http.MethodPost, "https://user.mypikpak.net/v1/shield/captcha/init")
+ return e, resp, err
+}
+
+// refreshCaptchaToken 刷新CaptchaToken
+func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) (string, error) {
+ d.Common.refreshMu.Lock()
+ defer d.Common.refreshMu.Unlock()
+ oldToken, expiry, deviceID := d.Common.captchaSnapshot(action)
+ if hasValidCaptchaToken(oldToken, expiry) {
+ return oldToken, nil
+ }
+ e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID)
if err != nil {
- return err
+ return "", err
}
if e.IsError() {
- return errors.New(e.Error())
+ return "", &e
}
//if resp.Url != "" {
// return fmt.Errorf(`need verify: Click Here`, resp.Url)
//}
- if d.Common.RefreshCTokenCk != nil {
- d.Common.RefreshCTokenCk(resp.CaptchaToken)
+ d.Common.setCaptchaState(action, resp.CaptchaToken, resp.Expiry())
+ refreshCTokenCk := d.Common.RefreshCTokenCk
+ if refreshCTokenCk != nil {
+ refreshCTokenCk(resp.CaptchaToken)
}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
- return nil
+ return resp.CaptchaToken, nil
}