From 8bec1ad732e880345d5ee9dd564055da3b8647e2 Mon Sep 17 00:00:00 2001
From: outlook84 <96007761+outlook84@users.noreply.github.com>
Date: Sat, 21 Mar 2026 15:52:46 +0800
Subject: [PATCH 1/5] fix: handle PikPak captcha token expiry correctly
- Fix incorrect captcha token lifecycle handling in pikpak and pikpak_share, preventing requests from reusing
expired tokens.
- Track captcha token expiry explicitly and validate token freshness before sending requests.
- Remove legacy multi-platform PikPak branches and keep the active Web profile only.
- Enable PreferProxy for both pikpak and pikpak_share.
---
drivers/pikpak/driver.go | 71 +++-----
drivers/pikpak/meta.go | 13 +-
drivers/pikpak/types.go | 7 +
drivers/pikpak/util.go | 290 ++++++++++++++++++---------------
drivers/pikpak_share/driver.go | 41 ++---
drivers/pikpak_share/meta.go | 11 +-
drivers/pikpak_share/types.go | 7 +
drivers/pikpak_share/util.go | 169 ++++++++-----------
8 files changed, 290 insertions(+), 319 deletions(-)
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index 8b72c3638..8dafab41e 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -41,7 +41,7 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
client: base.NewRestyClient(),
CaptchaToken: "",
UserID: "",
- DeviceID: utils.GetMD5EncodeStr(d.Username + d.Password),
+ DeviceID: genDeviceID(),
UserAgent: "",
RefreshCTokenCk: func(token string) {
d.Common.CaptchaToken = token
@@ -50,66 +50,40 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
}
}
- 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.Platform = ""
+ op.MustSaveDriverStorage(d)
+ } 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 == "" {
+ if d.Addition.CaptchaToken != "" {
d.SetCaptchaToken(d.Addition.CaptchaToken)
}
+ if d.Addition.RefreshToken != "" {
+ d.RefreshToken = d.Addition.RefreshToken
+ }
if d.Addition.DeviceID != "" {
d.SetDeviceID(d.Addition.DeviceID)
} else {
+ if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
+ d.SetDeviceID(genDeviceID())
+ }
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
- }
- }
- // 获取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(); 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 +220,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..22cbb601d 100644
--- a/drivers/pikpak/meta.go
+++ b/drivers/pikpak/meta.go
@@ -9,16 +9,17 @@ 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:""`
+ CaptchaToken string `json:"captcha_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..dc56093bb 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"
@@ -28,17 +26,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 +44,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,23 +51,29 @@ 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 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) login() error {
// 检查用户名和密码是否为空
if d.Addition.Username == "" || d.Addition.Password == "" {
@@ -101,32 +81,47 @@ func (d *PikPak) login() error {
}
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)
+ if !d.HasValidCaptchaToken() {
+ if err := d.RefreshCaptchaTokenInLogin(action, d.Username); err != nil {
return err
}
}
- 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
+ doLogin := func() error {
+ 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())
+ d.Addition.RefreshToken = d.RefreshToken
+ op.MustSaveDriverStorage(d)
+ return nil
}
- if e.ErrorCode != 0 {
- return &e
+
+ err := doLogin()
+ if apiErr, ok := err.(*ErrResp); ok && apiErr.ErrorCode == 9 {
+ d.Common.SetCaptchaExpiry(time.Time{})
+ d.Common.SetCaptchaToken("")
+ if err = d.RefreshCaptchaTokenInLogin(action, d.Username); err != nil {
+ return err
+ }
+ return doLogin()
}
- 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 {
@@ -168,7 +163,32 @@ func (d *PikPak) refreshToken(refreshToken string) error {
return nil
}
+func (d *PikPak) ensureAuthorized() error {
+ if d.AccessToken != "" {
+ return nil
+ }
+ if d.RefreshToken == "" {
+ d.RefreshToken = d.Addition.RefreshToken
+ }
+ if d.RefreshToken != "" {
+ return d.refreshToken(d.RefreshToken)
+ }
+ if err := d.login(); err != nil {
+ return err
+ }
+ return nil
+}
+
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ if err := d.ensureAuthorized(); err != nil {
+ return nil, err
+ }
+ if !d.HasValidCaptchaToken() {
+ if err := d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
+ return nil, err
+ }
+ }
+
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
//"Authorization": "Bearer " + d.AccessToken,
@@ -203,6 +223,8 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
}
return d.request(url, method, callback, resp)
case 9: // 验证码token过期
+ d.Common.SetCaptchaExpiry(time.Time{})
+ d.Common.SetCaptchaToken("")
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
return nil, err
}
@@ -248,9 +270,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,64 +282,11 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
+ captchaMu 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.DeviceID = deviceID
}
@@ -332,10 +302,25 @@ func (c *Common) SetUserAgent(userAgent string) {
func (c *Common) SetCaptchaToken(captchaToken string) {
c.CaptchaToken = captchaToken
}
+
+func (c *Common) SetCaptchaExpiry(expiry time.Time) {
+ c.CaptchaExpiry = expiry
+}
+
func (c *Common) GetCaptchaToken() string {
return c.CaptchaToken
}
+func (c *Common) HasValidCaptchaToken() bool {
+ if c.CaptchaToken == "" {
+ return false
+ }
+ if c.CaptchaExpiry.IsZero() {
+ return true
+ }
+ return time.Now().Before(c.CaptchaExpiry.Add(-10 * time.Second))
+}
+
func (c *Common) GetUserAgent() string {
return c.UserAgent
}
@@ -385,36 +370,81 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
// refreshCaptchaToken 刷新CaptchaToken
func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
- param := CaptchaTokenRequest{
- Action: action,
- CaptchaToken: d.GetCaptchaToken(),
- ClientID: d.ClientID,
- DeviceID: d.GetDeviceID(),
- Meta: metas,
- RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
+ d.Common.captchaMu.Lock()
+ if d.Common.HasValidCaptchaToken() {
+ d.Common.captchaMu.Unlock()
+ return nil
+ }
+
+ param := func(oldToken string) CaptchaTokenRequest {
+ return CaptchaTokenRequest{
+ Action: action,
+ CaptchaToken: oldToken,
+ ClientID: d.ClientID,
+ DeviceID: d.GetDeviceID(),
+ 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)
+ execCaptchaInit := func(oldToken string) error {
+ e = ErrResp{}
+ resp = CaptchaTokenResponse{}
+ req := base.RestyClient.R().
+ SetHeaders(map[string]string{
+ "User-Agent": d.GetUserAgent(),
+ "X-Device-ID": d.GetDeviceID(),
+ }).
+ SetError(&e).
+ SetResult(&resp).
+ SetBody(param(oldToken)).
+ SetQueryParam("client_id", d.ClientID)
+ if d.AccessToken != "" {
+ req.SetHeader("Authorization", "Bearer "+d.AccessToken)
+ }
+ _, err := req.Execute(http.MethodPost, "https://user.mypikpak.net/v1/shield/captcha/init")
+ return err
+ }
+ err := execCaptchaInit(d.GetCaptchaToken())
if err != nil {
+ d.Common.captchaMu.Unlock()
return err
}
+ if e.ErrorCode == 4122 || e.ErrorCode == 4121 || e.ErrorCode == 16 {
+ d.Common.captchaMu.Unlock()
+ if err = d.refreshToken(d.RefreshToken); err != nil {
+ return err
+ }
+ d.Common.captchaMu.Lock()
+ if d.Common.HasValidCaptchaToken() {
+ d.Common.captchaMu.Unlock()
+ return nil
+ }
+ err = execCaptchaInit("")
+ if err != nil {
+ d.Common.captchaMu.Unlock()
+ return err
+ }
+ }
if e.IsError() {
+ d.Common.captchaMu.Unlock()
return errors.New(e.Error())
}
if resp.Url != "" {
+ d.Common.captchaMu.Unlock()
return fmt.Errorf(`need verify: Click Here`, resp.Url)
}
+ d.Common.SetCaptchaToken(resp.CaptchaToken)
+ d.Common.SetCaptchaExpiry(resp.Expiry())
if d.Common.RefreshCTokenCk != nil {
d.Common.RefreshCTokenCk(resp.CaptchaToken)
}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
+ d.Common.captchaMu.Unlock()
return nil
}
diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go
index d6323d18d..9b963dbf9 100644
--- a/drivers/pikpak_share/driver.go
+++ b/drivers/pikpak_share/driver.go
@@ -2,8 +2,8 @@ package pikpak_share
import (
"context"
+ "fmt"
"net/http"
- "time"
"github.com/OpenListTeam/OpenList/v4/internal/op"
@@ -31,7 +31,7 @@ func (d *PikPakShare) GetAddition() driver.Additional {
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()),
+ DeviceID: genDeviceID(),
UserAgent: "",
RefreshCTokenCk: func(token string) {
d.Common.CaptchaToken = token
@@ -39,36 +39,29 @@ func (d *PikPakShare) Init(ctx context.Context) error {
},
}
}
+ if d.Platform == "web" {
+ d.Platform = ""
+ op.MustSaveDriverStorage(d)
+ } 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 {
+ if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
+ d.SetDeviceID(genDeviceID())
+ }
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"
- }
+ 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"
// 获取CaptchaToken
err := d.RefreshCaptchaToken(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/share:batch_file_info"), "")
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..58c7d75e0 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,38 +33,36 @@ 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 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 *PikPakShare) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
+ if !d.HasValidCaptchaToken() {
+ if err := d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
+ return nil, err
+ }
+ }
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"User-Agent": d.GetUserAgent(),
@@ -102,6 +87,8 @@ func (d *PikPakShare) request(url string, method string, callback base.ReqCallba
case 0:
return res.Body(), nil
case 9: // 验证码token过期
+ d.Common.SetCaptchaExpiry(time.Time{})
+ d.Common.SetCaptchaToken("")
if err = d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
return nil, err
}
@@ -177,8 +164,9 @@ func GetAction(method string, url string) string {
}
type Common struct {
- client *resty.Client
- CaptchaToken string
+ client *resty.Client
+ CaptchaToken string
+ CaptchaExpiry time.Time
// 必要值,签名相关
ClientID string
ClientSecret string
@@ -187,6 +175,7 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
+ captchaMu sync.Mutex
// 验证码token刷新成功回调
RefreshCTokenCk func(token string)
}
@@ -199,6 +188,10 @@ func (c *Common) SetCaptchaToken(captchaToken string) {
c.CaptchaToken = captchaToken
}
+func (c *Common) SetCaptchaExpiry(expiry time.Time) {
+ c.CaptchaExpiry = expiry
+}
+
func (c *Common) SetDeviceID(deviceID string) {
c.DeviceID = deviceID
}
@@ -207,6 +200,16 @@ func (c *Common) GetCaptchaToken() string {
return c.CaptchaToken
}
+func (c *Common) HasValidCaptchaToken() bool {
+ if c.CaptchaToken == "" {
+ return false
+ }
+ if c.CaptchaExpiry.IsZero() {
+ return true
+ }
+ return time.Now().Before(c.CaptchaExpiry.Add(-10 * time.Second))
+}
+
func (c *Common) GetClientID() string {
return c.ClientID
}
@@ -219,60 +222,6 @@ 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{
@@ -297,6 +246,12 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
// refreshCaptchaToken 刷新CaptchaToken
func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {
+ d.Common.captchaMu.Lock()
+ defer d.Common.captchaMu.Unlock()
+ if d.Common.HasValidCaptchaToken() {
+ return nil
+ }
+
param := CaptchaTokenRequest{
Action: action,
CaptchaToken: d.GetCaptchaToken(),
@@ -306,9 +261,16 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
}
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": d.GetDeviceID(),
+ }).
+ SetError(&e).
+ SetResult(&resp).
+ SetBody(param)
+ _, err := req.Execute(http.MethodPost, "https://user.mypikpak.net/v1/shield/captcha/init")
if err != nil {
return err
@@ -322,9 +284,10 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
// return fmt.Errorf(`need verify: Click Here`, resp.Url)
//}
+ d.Common.SetCaptchaToken(resp.CaptchaToken)
+ d.Common.SetCaptchaExpiry(resp.Expiry())
if d.Common.RefreshCTokenCk != nil {
d.Common.RefreshCTokenCk(resp.CaptchaToken)
}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
return nil
}
From cc3ddb30d1e12749c8735d1d8b2f37fd3673ff80 Mon Sep 17 00:00:00 2001
From: outlook84 <96007761+outlook84@users.noreply.github.com>
Date: Sat, 21 Mar 2026 17:42:08 +0800
Subject: [PATCH 2/5] fix: rework PikPak state locking and drop captcha token
persistence
---
drivers/pikpak/driver.go | 31 +++--
drivers/pikpak/meta.go | 1 -
drivers/pikpak/util.go | 208 ++++++++++++++++++++++-----------
drivers/pikpak_share/driver.go | 40 +++++--
drivers/pikpak_share/util.go | 73 ++++++++----
5 files changed, 245 insertions(+), 108 deletions(-)
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index 8dafab41e..b07168b9b 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -7,6 +7,7 @@ import (
"net/http"
"strconv"
"strings"
+ "sync"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/driver"
@@ -25,6 +26,8 @@ type PikPak struct {
*Common
RefreshToken string
AccessToken string
+ authMu sync.RWMutex
+ persistMu sync.Mutex
}
func (d *PikPak) Config() driver.Config {
@@ -35,6 +38,15 @@ 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{
@@ -43,10 +55,6 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
UserID: "",
DeviceID: genDeviceID(),
UserAgent: "",
- RefreshCTokenCk: func(token string) {
- d.Common.CaptchaToken = token
- op.MustSaveDriverStorage(d)
- },
}
}
@@ -57,17 +65,15 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
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.Platform = ""
- op.MustSaveDriverStorage(d)
+ 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.SetCaptchaToken(d.Addition.CaptchaToken)
- }
if d.Addition.RefreshToken != "" {
- d.RefreshToken = d.Addition.RefreshToken
+ d.setRefreshTokenState(d.Addition.RefreshToken)
}
if d.Addition.DeviceID != "" {
@@ -76,8 +82,9 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
d.SetDeviceID(genDeviceID())
}
- d.Addition.DeviceID = d.Common.DeviceID
- op.MustSaveDriverStorage(d)
+ d.saveStorage(func() {
+ d.Addition.DeviceID = d.GetDeviceID()
+ })
}
if err = d.ensureAuthorized(); err != nil {
diff --git a/drivers/pikpak/meta.go b/drivers/pikpak/meta.go
index 22cbb601d..21f348dd2 100644
--- a/drivers/pikpak/meta.go
+++ b/drivers/pikpak/meta.go
@@ -11,7 +11,6 @@ type Addition struct {
Password string `json:"password" required:"true"`
Platform string `json:"platform" ignore:"true" default:""`
RefreshToken string `json:"refresh_token" ignore:"true" default:""`
- CaptchaToken string `json:"captcha_token" ignore:"true" default:""`
DeviceID string `json:"device_id" ignore:"true" default:""`
DisableMediaLink bool `json:"disable_media_link" default:"true"`
}
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index dc56093bb..38ccd09b1 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -18,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"
@@ -104,11 +103,13 @@ func (d *PikPak) login() error {
return &e
}
data := res.Body()
- 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.Addition.RefreshToken = refreshToken
+ })
return nil
}
@@ -135,8 +136,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 {
@@ -149,29 +151,29 @@ func (d *PikPak) refreshToken(refreshToken string) error {
return d.login()
}
}
- 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) ensureAuthorized() error {
- if d.AccessToken != "" {
+ if d.getAccessToken() != "" {
return nil
}
- if d.RefreshToken == "" {
- d.RefreshToken = d.Addition.RefreshToken
- }
- if d.RefreshToken != "" {
- return d.refreshToken(d.RefreshToken)
+ if refreshToken := d.getRefreshToken(); refreshToken != "" {
+ return d.refreshToken(refreshToken)
}
if err := d.login(); err != nil {
return err
@@ -196,8 +198,8 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
"X-Device-ID": d.GetDeviceID(),
"X-Captcha-Token": d.GetCaptchaToken(),
})
- if d.AccessToken != "" {
- req.SetHeader("Authorization", "Bearer "+d.AccessToken)
+ if accessToken := d.getAccessToken(); accessToken != "" {
+ req.SetHeader("Authorization", "Bearer "+accessToken)
}
if callback != nil {
@@ -218,7 +220,11 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
return res.Body(), nil
case 4122, 4121, 16:
// access_token 过期
- if err1 := d.refreshToken(d.RefreshToken); err1 != nil {
+ if refreshToken := d.getRefreshToken(); refreshToken != "" {
+ if err1 := d.refreshToken(refreshToken); err1 != nil {
+ return nil, err1
+ }
+ } else if err1 := d.login(); err1 != nil {
return nil, err1
}
return d.request(url, method, callback, resp)
@@ -282,16 +288,21 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
- captchaMu sync.Mutex
+ stateMu sync.RWMutex
+ refreshMu sync.Mutex
// 验证码token刷新成功回调
RefreshCTokenCk func(token 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
}
@@ -300,25 +311,26 @@ func (c *Common) SetUserAgent(userAgent string) {
}
func (c *Common) SetCaptchaToken(captchaToken string) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.CaptchaToken = captchaToken
}
func (c *Common) SetCaptchaExpiry(expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.CaptchaExpiry = expiry
}
func (c *Common) GetCaptchaToken() string {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
return c.CaptchaToken
}
func (c *Common) HasValidCaptchaToken() bool {
- if c.CaptchaToken == "" {
- return false
- }
- if c.CaptchaExpiry.IsZero() {
- return true
- }
- return time.Now().Before(c.CaptchaExpiry.Add(-10 * time.Second))
+ token, expiry, _, _ := c.captchaSnapshot()
+ return hasValidCaptchaToken(token, expiry)
}
func (c *Common) GetUserAgent() string {
@@ -326,13 +338,71 @@ func (c *Common) GetUserAgent() string {
}
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
}
+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) setCaptchaState(token string, expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ c.CaptchaToken = token
+ c.CaptchaExpiry = expiry
+}
+
+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) authSnapshot() (accessToken, refreshToken string) {
+ d.authMu.RLock()
+ defer d.authMu.RUnlock()
+ return d.AccessToken, 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
+}
+
// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
metas := map[string]string{
@@ -360,7 +430,7 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
// 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)
}
@@ -370,81 +440,89 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
// refreshCaptchaToken 刷新CaptchaToken
func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
- d.Common.captchaMu.Lock()
- if d.Common.HasValidCaptchaToken() {
- d.Common.captchaMu.Unlock()
+ d.Common.refreshMu.Lock()
+ locked := true
+ defer func() {
+ if locked {
+ d.Common.refreshMu.Unlock()
+ }
+ }()
+
+ oldToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
+ if hasValidCaptchaToken(oldToken, expiry) {
return nil
}
- param := func(oldToken string) CaptchaTokenRequest {
- return CaptchaTokenRequest{
+ doCaptchaInit := func(oldToken, deviceID, accessToken string) (ErrResp, CaptchaTokenResponse, error) {
+ e := ErrResp{}
+ resp := CaptchaTokenResponse{}
+ param := CaptchaTokenRequest{
Action: action,
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
- execCaptchaInit := func(oldToken string) error {
- e = ErrResp{}
- resp = CaptchaTokenResponse{}
req := base.RestyClient.R().
SetHeaders(map[string]string{
"User-Agent": d.GetUserAgent(),
- "X-Device-ID": d.GetDeviceID(),
+ "X-Device-ID": deviceID,
}).
SetError(&e).
SetResult(&resp).
- SetBody(param(oldToken)).
+ SetBody(param).
SetQueryParam("client_id", d.ClientID)
- if d.AccessToken != "" {
- req.SetHeader("Authorization", "Bearer "+d.AccessToken)
+ if accessToken != "" {
+ req.SetHeader("Authorization", "Bearer "+accessToken)
}
_, err := req.Execute(http.MethodPost, "https://user.mypikpak.net/v1/shield/captcha/init")
- return err
+ return e, resp, err
}
- err := execCaptchaInit(d.GetCaptchaToken())
+ accessToken := d.getAccessToken()
+ e, resp, err := doCaptchaInit(oldToken, deviceID, accessToken)
if err != nil {
- d.Common.captchaMu.Unlock()
return err
}
if e.ErrorCode == 4122 || e.ErrorCode == 4121 || e.ErrorCode == 16 {
- d.Common.captchaMu.Unlock()
- if err = d.refreshToken(d.RefreshToken); err != nil {
+ d.Common.refreshMu.Unlock()
+ locked = false
+ if refreshToken := d.getRefreshToken(); refreshToken != "" {
+ if err = d.refreshToken(refreshToken); err != nil {
+ return err
+ }
+ } else if err = d.login(); err != nil {
return err
}
- d.Common.captchaMu.Lock()
- if d.Common.HasValidCaptchaToken() {
- d.Common.captchaMu.Unlock()
+
+ d.Common.refreshMu.Lock()
+ locked = true
+ oldToken, expiry, deviceID, _ = d.Common.captchaSnapshot()
+ if hasValidCaptchaToken(oldToken, expiry) {
return nil
}
- err = execCaptchaInit("")
+
+ accessToken, _ = d.authSnapshot()
+ e, resp, err = doCaptchaInit("", deviceID, accessToken)
if err != nil {
- d.Common.captchaMu.Unlock()
return err
}
}
if e.IsError() {
- d.Common.captchaMu.Unlock()
return errors.New(e.Error())
}
if resp.Url != "" {
- d.Common.captchaMu.Unlock()
return fmt.Errorf(`need verify: Click Here`, resp.Url)
}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
- d.Common.SetCaptchaExpiry(resp.Expiry())
- if d.Common.RefreshCTokenCk != nil {
- d.Common.RefreshCTokenCk(resp.CaptchaToken)
+ d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
+ refreshCTokenCk := d.Common.RefreshCTokenCk
+ if refreshCTokenCk != nil {
+ refreshCTokenCk(resp.CaptchaToken)
}
- d.Common.captchaMu.Unlock()
return nil
}
diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go
index 9b963dbf9..bf48ab8c0 100644
--- a/drivers/pikpak_share/driver.go
+++ b/drivers/pikpak_share/driver.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net/http"
+ "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,20 +31,38 @@ 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: genDeviceID(),
UserAgent: "",
- RefreshCTokenCk: func(token string) {
- d.Common.CaptchaToken = token
- op.MustSaveDriverStorage(d)
- },
}
}
if d.Platform == "web" {
- d.Platform = ""
- op.MustSaveDriverStorage(d)
+ 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)
}
@@ -52,8 +73,9 @@ func (d *PikPakShare) Init(ctx context.Context) error {
if d.GetDeviceID() == "" || len(d.GetDeviceID()) != 32 {
d.SetDeviceID(genDeviceID())
}
- d.Addition.DeviceID = d.Common.DeviceID
- op.MustSaveDriverStorage(d)
+ d.saveStorage(func() {
+ d.Addition.DeviceID = d.GetDeviceID()
+ })
}
d.ClientID = WebClientID
@@ -95,7 +117,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/util.go b/drivers/pikpak_share/util.go
index 58c7d75e0..d48b4d4c9 100644
--- a/drivers/pikpak_share/util.go
+++ b/drivers/pikpak_share/util.go
@@ -114,7 +114,7 @@ func (d *PikPakShare) getSharePassToken() error {
if err != nil {
return err
}
- d.PassCodeToken = resp.PassCodeToken
+ d.SetPassCodeToken(resp.PassCodeToken)
return nil
}
@@ -133,7 +133,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": d.GetPassCodeToken(),
}
var resp ShareResp
_, err := d.request("https://api-drive.mypikpak.net/drive/v1/share/detail", http.MethodGet, func(req *resty.Request) {
@@ -175,7 +175,8 @@ type Common struct {
Algorithms []string
DeviceID string
UserAgent string
- captchaMu sync.Mutex
+ stateMu sync.RWMutex
+ refreshMu sync.Mutex
// 验证码token刷新成功回调
RefreshCTokenCk func(token string)
}
@@ -185,29 +186,32 @@ func (c *Common) SetUserAgent(userAgent string) {
}
func (c *Common) SetCaptchaToken(captchaToken string) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.CaptchaToken = captchaToken
}
func (c *Common) SetCaptchaExpiry(expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.CaptchaExpiry = expiry
}
func (c *Common) SetDeviceID(deviceID string) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
c.DeviceID = deviceID
}
func (c *Common) GetCaptchaToken() string {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
return c.CaptchaToken
}
func (c *Common) HasValidCaptchaToken() bool {
- if c.CaptchaToken == "" {
- return false
- }
- if c.CaptchaExpiry.IsZero() {
- return true
- }
- return time.Now().Before(c.CaptchaExpiry.Add(-10 * time.Second))
+ token, expiry, _ := c.captchaSnapshot()
+ return hasValidCaptchaToken(token, expiry)
}
func (c *Common) GetClientID() string {
@@ -219,9 +223,34 @@ func (c *Common) GetUserAgent() string {
}
func (c *Common) GetDeviceID() string {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
return c.DeviceID
}
+func (c *Common) captchaSnapshot() (token string, expiry time.Time, deviceID string) {
+ c.stateMu.RLock()
+ defer c.stateMu.RUnlock()
+ return c.CaptchaToken, c.CaptchaExpiry, c.DeviceID
+}
+
+func (c *Common) setCaptchaState(token string, expiry time.Time) {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ c.CaptchaToken = token
+ c.CaptchaExpiry = expiry
+}
+
+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 {
metas := map[string]string{
@@ -236,7 +265,7 @@ func (d *PikPakShare) RefreshCaptchaToken(action, userID string) error {
// 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)
}
@@ -246,17 +275,19 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
// refreshCaptchaToken 刷新CaptchaToken
func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {
- d.Common.captchaMu.Lock()
- defer d.Common.captchaMu.Unlock()
- if d.Common.HasValidCaptchaToken() {
+ d.Common.refreshMu.Lock()
+ defer d.Common.refreshMu.Unlock()
+
+ oldToken, expiry, deviceID := d.Common.captchaSnapshot()
+ if hasValidCaptchaToken(oldToken, expiry) {
return nil
}
param := CaptchaTokenRequest{
Action: action,
- CaptchaToken: d.GetCaptchaToken(),
+ CaptchaToken: oldToken,
ClientID: d.ClientID,
- DeviceID: d.GetDeviceID(),
+ DeviceID: deviceID,
Meta: metas,
}
var e ErrResp
@@ -265,7 +296,7 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
SetHeaders(map[string]string{
"User-Agent": d.GetUserAgent(),
"X-Client-ID": d.GetClientID(),
- "X-Device-ID": d.GetDeviceID(),
+ "X-Device-ID": deviceID,
}).
SetError(&e).
SetResult(&resp).
@@ -284,10 +315,10 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
// return fmt.Errorf(`need verify: Click Here`, resp.Url)
//}
- d.Common.SetCaptchaToken(resp.CaptchaToken)
- d.Common.SetCaptchaExpiry(resp.Expiry())
- if d.Common.RefreshCTokenCk != nil {
- d.Common.RefreshCTokenCk(resp.CaptchaToken)
+ d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
+ refreshCTokenCk := d.Common.RefreshCTokenCk
+ if refreshCTokenCk != nil {
+ refreshCTokenCk(resp.CaptchaToken)
}
return nil
}
From aca3071396c2c87b8e8e5b44c366bc19be106108 Mon Sep 17 00:00:00 2001
From: outlook84 <96007761+outlook84@users.noreply.github.com>
Date: Sun, 22 Mar 2026 05:23:40 +0800
Subject: [PATCH 3/5] fix: serialize pikpak auth with singleflight
---
drivers/pikpak/driver.go | 4 +-
drivers/pikpak/util.go | 149 +++++++++++++++++++++------------------
2 files changed, 85 insertions(+), 68 deletions(-)
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index b07168b9b..3556b2ce2 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -14,6 +14,7 @@ import (
"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"
@@ -27,6 +28,7 @@ type PikPak struct {
RefreshToken string
AccessToken string
authMu sync.RWMutex
+ authG singleflight.Group[struct{}]
persistMu sync.Mutex
}
@@ -87,7 +89,7 @@ func (d *PikPak) Init(ctx context.Context) (err error) {
})
}
- if err = d.ensureAuthorized(); err != nil {
+ if err = d.ensureAuthorized(false, ""); err != nil {
return err
}
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index 38ccd09b1..eb8c04563 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -73,7 +73,7 @@ func genDeviceID() string {
return string(base)
}
-func (d *PikPak) login() error {
+func (d *PikPak) loginRaw() error {
// 检查用户名和密码是否为空
if d.Addition.Username == "" || d.Addition.Password == "" {
return errors.New("username or password is empty")
@@ -125,7 +125,7 @@ func (d *PikPak) login() error {
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).
@@ -148,7 +148,7 @@ 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.saveStorage(func() {
@@ -168,21 +168,39 @@ func (d *PikPak) refreshToken(refreshToken string) error {
return nil
}
-func (d *PikPak) ensureAuthorized() error {
- if d.getAccessToken() != "" {
- return nil
- }
+func (d *PikPak) authorizeRaw() error {
if refreshToken := d.getRefreshToken(); refreshToken != "" {
- return d.refreshToken(refreshToken)
+ return d.refreshTokenRaw(refreshToken)
}
- if err := d.login(); err != nil {
- return err
+ return d.loginRaw()
+}
+
+func (d *PikPak) ensureAuthorized(force bool, staleAccessToken string) error {
+ if !force && d.getAccessToken() != "" {
+ return nil
}
- return nil
+ if force && staleAccessToken != "" {
+ if currentAccessToken := d.getAccessToken(); currentAccessToken != "" && currentAccessToken != staleAccessToken {
+ return nil
+ }
+ }
+
+ _, 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
}
func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- if err := d.ensureAuthorized(); err != nil {
+ if err := d.ensureAuthorized(false, ""); err != nil {
return nil, err
}
if !d.HasValidCaptchaToken() {
@@ -198,7 +216,8 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
"X-Device-ID": d.GetDeviceID(),
"X-Captcha-Token": d.GetCaptchaToken(),
})
- if accessToken := d.getAccessToken(); accessToken != "" {
+ accessToken := d.getAccessToken()
+ if accessToken != "" {
req.SetHeader("Authorization", "Bearer "+accessToken)
}
@@ -220,11 +239,7 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
return res.Body(), nil
case 4122, 4121, 16:
// access_token 过期
- if refreshToken := d.getRefreshToken(); refreshToken != "" {
- if err1 := d.refreshToken(refreshToken); err1 != nil {
- return nil, err1
- }
- } else if err1 := d.login(); err1 != nil {
+ if err1 := d.ensureAuthorized(true, accessToken); err1 != nil {
return nil, err1
}
return d.request(url, method, callback, resp)
@@ -411,7 +426,7 @@ func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
"user_id": userID,
}
metas["timestamp"], metas["captcha_sign"] = d.Common.GetCaptchaSign()
- return d.refreshCaptchaToken(action, metas)
+ return d.refreshCaptchaToken(action, metas, true)
}
// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
@@ -424,7 +439,7 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
} else {
metas["username"] = username
}
- return d.refreshCaptchaToken(action, metas)
+ return d.refreshCaptchaToken(action, metas, false)
}
// GetCaptchaSign 获取验证码签名
@@ -438,73 +453,73 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
return
}
+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: oldToken,
+ ClientID: d.ClientID,
+ DeviceID: deviceID,
+ Meta: metas,
+ RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
+ }
+ 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
+}
+
+func (d *PikPak) repairAuthorizationForCaptcha(accessToken string) error {
+ d.Common.refreshMu.Unlock()
+ defer d.Common.refreshMu.Lock()
+ return d.ensureAuthorized(true, accessToken)
+}
+
// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string) error {
+func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string, allowAuthRepair bool) error {
d.Common.refreshMu.Lock()
- locked := true
- defer func() {
- if locked {
- d.Common.refreshMu.Unlock()
- }
- }()
+ defer d.Common.refreshMu.Unlock()
oldToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
if hasValidCaptchaToken(oldToken, expiry) {
return nil
}
- doCaptchaInit := func(oldToken, deviceID, accessToken string) (ErrResp, CaptchaTokenResponse, error) {
- e := ErrResp{}
- resp := CaptchaTokenResponse{}
- param := CaptchaTokenRequest{
- Action: action,
- CaptchaToken: oldToken,
- ClientID: d.ClientID,
- DeviceID: deviceID,
- Meta: metas,
- RedirectUri: "xlaccsdk01://xbase.cloud/callback?state=harbor",
- }
- 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
+ accessToken := ""
+ if allowAuthRepair {
+ accessToken = d.getAccessToken()
}
-
- accessToken := d.getAccessToken()
- e, resp, err := doCaptchaInit(oldToken, deviceID, accessToken)
+ e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID, accessToken)
if err != nil {
return err
}
- if e.ErrorCode == 4122 || e.ErrorCode == 4121 || e.ErrorCode == 16 {
- d.Common.refreshMu.Unlock()
- locked = false
- if refreshToken := d.getRefreshToken(); refreshToken != "" {
- if err = d.refreshToken(refreshToken); err != nil {
- return err
- }
- } else if err = d.login(); err != nil {
+ if allowAuthRepair && isAuthExpiredErrorCode(e.ErrorCode) {
+ if err = d.repairAuthorizationForCaptcha(accessToken); err != nil {
return err
}
- d.Common.refreshMu.Lock()
- locked = true
oldToken, expiry, deviceID, _ = d.Common.captchaSnapshot()
if hasValidCaptchaToken(oldToken, expiry) {
return nil
}
- accessToken, _ = d.authSnapshot()
- e, resp, err = doCaptchaInit("", deviceID, accessToken)
+ accessToken = d.getAccessToken()
+ e, resp, err = d.initCaptchaToken(action, metas, "", deviceID, accessToken)
if err != nil {
return err
}
From f998cb4a337c582d32539b31cb0d2ec81cc8cb86 Mon Sep 17 00:00:00 2001
From: outlook84 <96007761+outlook84@users.noreply.github.com>
Date: Sun, 22 Mar 2026 05:29:43 +0800
Subject: [PATCH 4/5] refactor: decouple pikpak captcha refresh from auth
repair
---
drivers/pikpak/util.go | 128 ++++++++++++++++++++---------------------
1 file changed, 61 insertions(+), 67 deletions(-)
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index eb8c04563..d8093839c 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -115,8 +115,7 @@ func (d *PikPak) loginRaw() error {
err := doLogin()
if apiErr, ok := err.(*ErrResp); ok && apiErr.ErrorCode == 9 {
- d.Common.SetCaptchaExpiry(time.Time{})
- d.Common.SetCaptchaToken("")
+ d.Common.invalidateCaptchaToken()
if err = d.RefreshCaptchaTokenInLogin(action, d.Username); err != nil {
return err
}
@@ -244,8 +243,7 @@ func (d *PikPak) request(url string, method string, callback base.ReqCallback, r
}
return d.request(url, method, callback, resp)
case 9: // 验证码token过期
- d.Common.SetCaptchaExpiry(time.Time{})
- d.Common.SetCaptchaToken("")
+ d.Common.invalidateCaptchaToken()
if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
return nil, err
}
@@ -325,18 +323,6 @@ func (c *Common) SetUserAgent(userAgent string) {
c.UserAgent = userAgent
}
-func (c *Common) SetCaptchaToken(captchaToken string) {
- c.stateMu.Lock()
- defer c.stateMu.Unlock()
- c.CaptchaToken = captchaToken
-}
-
-func (c *Common) SetCaptchaExpiry(expiry time.Time) {
- c.stateMu.Lock()
- defer c.stateMu.Unlock()
- c.CaptchaExpiry = expiry
-}
-
func (c *Common) GetCaptchaToken() string {
c.stateMu.RLock()
defer c.stateMu.RUnlock()
@@ -377,6 +363,10 @@ func (c *Common) setCaptchaState(token string, expiry time.Time) {
c.CaptchaExpiry = expiry
}
+func (c *Common) invalidateCaptchaToken() {
+ c.setCaptchaState("", time.Time{})
+}
+
func hasValidCaptchaToken(token string, expiry time.Time) bool {
if token == "" {
return false
@@ -399,12 +389,6 @@ func (d *PikPak) getRefreshToken() string {
return d.RefreshToken
}
-func (d *PikPak) authSnapshot() (accessToken, refreshToken string) {
- d.authMu.RLock()
- defer d.authMu.RUnlock()
- return d.AccessToken, d.RefreshToken
-}
-
func (d *PikPak) setRefreshTokenState(refreshToken string) {
d.authMu.Lock()
defer d.authMu.Unlock()
@@ -418,19 +402,17 @@ func (d *PikPak) setAuthTokens(accessToken, refreshToken string) {
d.RefreshToken = refreshToken
}
-// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
-func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
+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, true)
+ 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
@@ -439,7 +421,27 @@ func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
} else {
metas["username"] = username
}
- return d.refreshCaptchaToken(action, metas, false)
+ return metas
+}
+
+// RefreshCaptchaTokenAtLogin 刷新验证码token(登录后)
+func (d *PikPak) RefreshCaptchaTokenAtLogin(action, userID string) error {
+ metas := d.authorizedCaptchaMetas(userID)
+ previousCaptchaToken := d.GetCaptchaToken()
+ accessToken := d.getAccessToken()
+ 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 err
+}
+
+// RefreshCaptchaTokenInLogin 刷新验证码token(登录时)
+func (d *PikPak) RefreshCaptchaTokenInLogin(action, username string) error {
+ return d.refreshCaptchaToken(action, d.loginCaptchaMetas(username), "")
}
// GetCaptchaSign 获取验证码签名
@@ -484,61 +486,53 @@ func (d *PikPak) initCaptchaToken(action string, metas map[string]string, oldTok
return e, resp, err
}
-func (d *PikPak) repairAuthorizationForCaptcha(accessToken string) error {
- d.Common.refreshMu.Unlock()
- defer d.Common.refreshMu.Lock()
- return d.ensureAuthorized(true, accessToken)
+func (d *PikPak) finishCaptchaTokenRefresh(errResp *ErrResp, resp *CaptchaTokenResponse) error {
+ if errResp.IsError() {
+ return errResp
+ }
+ if resp.Url != "" {
+ return fmt.Errorf(`need verify: Click Here`, resp.Url)
+ }
+
+ d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
+ refreshCTokenCk := d.Common.RefreshCTokenCk
+ if refreshCTokenCk != nil {
+ refreshCTokenCk(resp.CaptchaToken)
+ }
+ return nil
}
-// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string, allowAuthRepair bool) error {
+func (d *PikPak) refreshCaptchaTokenAfterReauth(action string, metas map[string]string, previousCaptchaToken string) error {
d.Common.refreshMu.Lock()
defer d.Common.refreshMu.Unlock()
- oldToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
- if hasValidCaptchaToken(oldToken, expiry) {
+ currentToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
+ if currentToken != previousCaptchaToken && hasValidCaptchaToken(currentToken, expiry) {
return nil
}
- accessToken := ""
- if allowAuthRepair {
- accessToken = d.getAccessToken()
- }
- e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID, accessToken)
+ e, resp, err := d.initCaptchaToken(action, metas, "", deviceID, d.getAccessToken())
if err != nil {
return err
}
- if allowAuthRepair && isAuthExpiredErrorCode(e.ErrorCode) {
- if err = d.repairAuthorizationForCaptcha(accessToken); err != nil {
- return err
- }
-
- oldToken, expiry, deviceID, _ = d.Common.captchaSnapshot()
- if hasValidCaptchaToken(oldToken, expiry) {
- return nil
- }
-
- accessToken = d.getAccessToken()
- e, resp, err = d.initCaptchaToken(action, metas, "", deviceID, accessToken)
- if err != nil {
- return err
- }
- }
+ return d.finishCaptchaTokenRefresh(&e, &resp)
+}
- if e.IsError() {
- return errors.New(e.Error())
- }
+// refreshCaptchaToken 刷新CaptchaToken
+func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string, accessToken string) error {
+ d.Common.refreshMu.Lock()
+ defer d.Common.refreshMu.Unlock()
- if resp.Url != "" {
- return fmt.Errorf(`need verify: Click Here`, resp.Url)
+ oldToken, expiry, deviceID, _ := d.Common.captchaSnapshot()
+ if hasValidCaptchaToken(oldToken, expiry) {
+ return nil
}
- d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
- refreshCTokenCk := d.Common.RefreshCTokenCk
- if refreshCTokenCk != nil {
- refreshCTokenCk(resp.CaptchaToken)
+ e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID, accessToken)
+ if err != nil {
+ return err
}
- return nil
+ return d.finishCaptchaTokenRefresh(&e, &resp)
}
func (d *PikPak) UploadByOSS(ctx context.Context, params *S3Params, s model.FileStreamer, up driver.UpdateProgress) error {
From c3ae958f577330ae382ad7e770d1ab40ab616646 Mon Sep 17 00:00:00 2001
From: outlook84 <96007761+outlook84@users.noreply.github.com>
Date: Sun, 22 Mar 2026 05:39:55 +0800
Subject: [PATCH 5/5] refactor: tighten pikpak and pikpak_share request
recovery flows
---
drivers/pikpak/driver.go | 9 +-
drivers/pikpak/util.go | 202 +++++++++++++++-----------
drivers/pikpak_share/driver.go | 11 +-
drivers/pikpak_share/util.go | 249 +++++++++++++++++++++------------
4 files changed, 288 insertions(+), 183 deletions(-)
diff --git a/drivers/pikpak/driver.go b/drivers/pikpak/driver.go
index 3556b2ce2..5ec61b85e 100644
--- a/drivers/pikpak/driver.go
+++ b/drivers/pikpak/driver.go
@@ -52,11 +52,10 @@ func (d *PikPak) saveStorage(update func()) {
func (d *PikPak) Init(ctx context.Context) (err error) {
if d.Common == nil {
d.Common = &Common{
- client: base.NewRestyClient(),
- CaptchaToken: "",
- UserID: "",
- DeviceID: genDeviceID(),
- UserAgent: "",
+ client: base.NewRestyClient(),
+ UserID: "",
+ DeviceID: genDeviceID(),
+ UserAgent: "",
}
}
diff --git a/drivers/pikpak/util.go b/drivers/pikpak/util.go
index d8093839c..bd1d4e41e 100644
--- a/drivers/pikpak/util.go
+++ b/drivers/pikpak/util.go
@@ -81,16 +81,16 @@ func (d *PikPak) loginRaw() error {
url := "https://user.mypikpak.net/v1/auth/signin"
action := GetAction(http.MethodPost, url)
- if !d.HasValidCaptchaToken() {
- if err := d.RefreshCaptchaTokenInLogin(action, d.Username); err != nil {
+ var loginCaptchaToken string
+ doLogin := func() error {
+ var err error
+ loginCaptchaToken, err = d.ensureCaptchaTokenInLogin(action, d.Username)
+ if err != nil {
return err
}
- }
-
- doLogin := func() error {
var e ErrResp
res, err := base.RestyClient.SetRetryCount(1).R().SetError(&e).SetBody(base.Json{
- "captcha_token": d.GetCaptchaToken(),
+ "captcha_token": loginCaptchaToken,
"client_id": d.ClientID,
"client_secret": d.ClientSecret,
"username": d.Username,
@@ -116,7 +116,7 @@ func (d *PikPak) loginRaw() error {
err := doLogin()
if apiErr, ok := err.(*ErrResp); ok && apiErr.ErrorCode == 9 {
d.Common.invalidateCaptchaToken()
- if err = d.RefreshCaptchaTokenInLogin(action, d.Username); err != nil {
+ if _, err = d.ensureCaptchaTokenInLogin(action, d.Username); err != nil {
return err
}
return doLogin()
@@ -198,61 +198,90 @@ func (d *PikPak) ensureAuthorized(force bool, staleAccessToken string) error {
return err
}
-func (d *PikPak) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
- if err := d.ensureAuthorized(false, ""); err != nil {
- return nil, err
+type requestRetryAction uint8
+
+const (
+ requestRetryNone requestRetryAction = iota
+ requestRetryAuth
+ requestRetryCaptcha
+)
+
+func classifyRequestError(errResp *ErrResp) (requestRetryAction, error) {
+ switch errResp.ErrorCode {
+ case 0:
+ return requestRetryNone, nil
+ case 4122, 4121, 16:
+ 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
}
- if !d.HasValidCaptchaToken() {
- if err := d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != 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
+ }
+ 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": d.GetCaptchaToken(),
- })
- accessToken := d.getAccessToken()
- if accessToken != "" {
- req.SetHeader("Authorization", "Bearer "+accessToken)
- }
+ 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
- }
+ 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
+ }
- switch e.ErrorCode {
- case 0:
- return res.Body(), nil
- case 4122, 4121, 16:
- // access_token 过期
- if err1 := d.ensureAuthorized(true, accessToken); err1 != nil {
- return nil, err1
+ retryAction, err := classifyRequestError(&e)
+ if err != nil {
+ return nil, err
}
- return d.request(url, method, callback, resp)
- case 9: // 验证码token过期
- d.Common.invalidateCaptchaToken()
- if err = d.RefreshCaptchaTokenAtLogin(GetAction(method, url), d.GetUserID()); err != nil {
+ 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) {
@@ -323,17 +352,6 @@ func (c *Common) SetUserAgent(userAgent string) {
c.UserAgent = userAgent
}
-func (c *Common) GetCaptchaToken() string {
- c.stateMu.RLock()
- defer c.stateMu.RUnlock()
- return c.CaptchaToken
-}
-
-func (c *Common) HasValidCaptchaToken() bool {
- token, expiry, _, _ := c.captchaSnapshot()
- return hasValidCaptchaToken(token, expiry)
-}
-
func (c *Common) GetUserAgent() string {
return c.UserAgent
}
@@ -356,7 +374,7 @@ func (c *Common) captchaSnapshot() (token string, expiry time.Time, deviceID, us
return c.CaptchaToken, c.CaptchaExpiry, c.DeviceID, c.UserID
}
-func (c *Common) setCaptchaState(token string, expiry time.Time) {
+func (c *Common) storeCaptchaState(token string, expiry time.Time) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
c.CaptchaToken = token
@@ -364,7 +382,21 @@ func (c *Common) setCaptchaState(token string, expiry time.Time) {
}
func (c *Common) invalidateCaptchaToken() {
- c.setCaptchaState("", time.Time{})
+ 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 {
@@ -426,21 +458,31 @@ func (d *PikPak) loginCaptchaMetas(username string) map[string]string {
// 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.GetCaptchaToken()
+ previousCaptchaToken, _, _, _ := d.Common.captchaSnapshot()
accessToken := d.getAccessToken()
- err := d.refreshCaptchaToken(action, metas, accessToken)
+ 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 "", err
}
return d.refreshCaptchaTokenAfterReauth(action, metas, previousCaptchaToken)
}
- return err
+ 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), "")
}
@@ -486,51 +528,51 @@ func (d *PikPak) initCaptchaToken(action string, metas map[string]string, oldTok
return e, resp, err
}
-func (d *PikPak) finishCaptchaTokenRefresh(errResp *ErrResp, resp *CaptchaTokenResponse) error {
+func (d *PikPak) finishCaptchaTokenRefresh(errResp *ErrResp, resp *CaptchaTokenResponse) (string, error) {
if errResp.IsError() {
- return errResp
+ return "", errResp
}
if resp.Url != "" {
- return fmt.Errorf(`need verify: Click Here`, resp.Url)
+ return "", fmt.Errorf(`need verify: Click Here`, resp.Url)
}
- d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
+ d.Common.storeCaptchaState(resp.CaptchaToken, resp.Expiry())
refreshCTokenCk := d.Common.RefreshCTokenCk
if refreshCTokenCk != nil {
refreshCTokenCk(resp.CaptchaToken)
}
- return nil
+ return resp.CaptchaToken, nil
}
-func (d *PikPak) refreshCaptchaTokenAfterReauth(action string, metas map[string]string, previousCaptchaToken string) error {
+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 nil
+ return currentToken, nil
}
e, resp, err := d.initCaptchaToken(action, metas, "", deviceID, d.getAccessToken())
if err != nil {
- return err
+ return "", err
}
return d.finishCaptchaTokenRefresh(&e, &resp)
}
// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPak) refreshCaptchaToken(action string, metas map[string]string, accessToken string) error {
+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 nil
+ return oldToken, nil
}
e, resp, err := d.initCaptchaToken(action, metas, oldToken, deviceID, accessToken)
if err != nil {
- return err
+ return "", err
}
return d.finishCaptchaTokenRefresh(&e, &resp)
}
diff --git a/drivers/pikpak_share/driver.go b/drivers/pikpak_share/driver.go
index bf48ab8c0..fecaeb750 100644
--- a/drivers/pikpak_share/driver.go
+++ b/drivers/pikpak_share/driver.go
@@ -55,8 +55,9 @@ func (d *PikPakShare) GetPassCodeToken() string {
func (d *PikPakShare) Init(ctx context.Context) error {
if d.Common == nil {
d.Common = &Common{
- DeviceID: genDeviceID(),
- UserAgent: "",
+ captchaStates: make(map[string]captchaState),
+ DeviceID: genDeviceID(),
+ UserAgent: "",
}
}
if d.Platform == "web" {
@@ -85,12 +86,6 @@ func (d *PikPakShare) Init(ctx context.Context) error {
d.Algorithms = WebAlgorithms
d.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0"
- // 获取CaptchaToken
- err := d.RefreshCaptchaToken(GetAction(http.MethodGet, "https://api-drive.mypikpak.net/drive/v1/share:batch_file_info"), "")
- if err != nil {
- return err
- }
-
if d.SharePwd != "" {
return d.getSharePassToken()
}
diff --git a/drivers/pikpak_share/util.go b/drivers/pikpak_share/util.go
index d48b4d4c9..92139bb42 100644
--- a/drivers/pikpak_share/util.go
+++ b/drivers/pikpak_share/util.go
@@ -57,47 +57,69 @@ func genDeviceID() string {
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 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) {
- if !d.HasValidCaptchaToken() {
- if err := d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
+ reqAction := GetAction(method, url)
+ for attempts := 0; attempts < 3; attempts++ {
+ captchaToken, err := d.ensureCaptchaToken(reqAction, "")
+ if err != nil {
return nil, err
}
- }
- 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)
- }
- if resp != nil {
- req.SetResult(resp)
- }
- var e ErrResp
- req.SetError(&e)
- res, err := req.Execute(method, url)
- if err != nil {
- return nil, err
- }
- switch e.ErrorCode {
- case 0:
- return res.Body(), nil
- case 9: // 验证码token过期
- d.Common.SetCaptchaExpiry(time.Time{})
- d.Common.SetCaptchaToken("")
- if err = d.RefreshCaptchaToken(GetAction(method, url), ""); err != nil {
+ 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
}
- return d.request(url, method, callback, resp)
- case 10: // 操作频繁
- return nil, errors.New(e.ErrorDescription)
- default:
- return nil, errors.New(e.Error())
+
+ 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 {
@@ -121,10 +143,13 @@ func (d *PikPakShare) getSharePassToken() error {
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,
@@ -133,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.GetPassCodeToken(),
+ "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) {
@@ -143,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
+ }
+ 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)
}
- 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
}
@@ -163,10 +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
- CaptchaExpiry time.Time
+ captchaStates map[string]captchaState
// 必要值,签名相关
ClientID string
ClientSecret string
@@ -185,33 +233,18 @@ func (c *Common) SetUserAgent(userAgent string) {
c.UserAgent = userAgent
}
-func (c *Common) SetCaptchaToken(captchaToken string) {
- c.stateMu.Lock()
- defer c.stateMu.Unlock()
- c.CaptchaToken = captchaToken
-}
-
-func (c *Common) SetCaptchaExpiry(expiry time.Time) {
- c.stateMu.Lock()
- defer c.stateMu.Unlock()
- c.CaptchaExpiry = expiry
-}
-
func (c *Common) SetDeviceID(deviceID string) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
c.DeviceID = deviceID
}
-func (c *Common) GetCaptchaToken() string {
- c.stateMu.RLock()
- defer c.stateMu.RUnlock()
- return c.CaptchaToken
-}
-
-func (c *Common) HasValidCaptchaToken() bool {
- token, expiry, _ := c.captchaSnapshot()
- return hasValidCaptchaToken(token, expiry)
+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 {
@@ -228,17 +261,41 @@ func (c *Common) GetDeviceID() string {
return c.DeviceID
}
-func (c *Common) captchaSnapshot() (token string, expiry time.Time, deviceID string) {
+func (c *Common) captchaSnapshot(action string) (token string, expiry time.Time, deviceID string) {
c.stateMu.RLock()
defer c.stateMu.RUnlock()
- return c.CaptchaToken, c.CaptchaExpiry, c.DeviceID
+ state := c.captchaStates[action]
+ return state.Token, state.Expiry, c.DeviceID
}
-func (c *Common) setCaptchaState(token string, expiry time.Time) {
+func (c *Common) setCaptchaState(action, token string, expiry time.Time) {
c.stateMu.Lock()
defer c.stateMu.Unlock()
- c.CaptchaToken = token
- c.CaptchaExpiry = expiry
+ 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}
+}
+
+func (c *Common) invalidateCaptchaToken() {
+ c.stateMu.Lock()
+ defer c.stateMu.Unlock()
+ clear(c.captchaStates)
+}
+
+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 hasValidCaptchaToken(token string, expiry time.Time) bool {
@@ -251,15 +308,24 @@ func hasValidCaptchaToken(token string, expiry time.Time) bool {
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 获取验证码签名
@@ -273,16 +339,9 @@ func (c *Common) GetCaptchaSign() (timestamp, sign string) {
return
}
-// refreshCaptchaToken 刷新CaptchaToken
-func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string) error {
- d.Common.refreshMu.Lock()
- defer d.Common.refreshMu.Unlock()
-
- oldToken, expiry, deviceID := d.Common.captchaSnapshot()
- if hasValidCaptchaToken(oldToken, expiry) {
- return nil
- }
-
+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: oldToken,
@@ -290,8 +349,6 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
DeviceID: deviceID,
Meta: metas,
}
- var e ErrResp
- var resp CaptchaTokenResponse
req := base.RestyClient.R().
SetHeaders(map[string]string{
"User-Agent": d.GetUserAgent(),
@@ -302,23 +359,35 @@ func (d *PikPakShare) refreshCaptchaToken(action string, metas map[string]string
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)
//}
- d.Common.setCaptchaState(resp.CaptchaToken, resp.Expiry())
+ d.Common.setCaptchaState(action, resp.CaptchaToken, resp.Expiry())
refreshCTokenCk := d.Common.RefreshCTokenCk
if refreshCTokenCk != nil {
refreshCTokenCk(resp.CaptchaToken)
}
- return nil
+ return resp.CaptchaToken, nil
}