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