diff --git a/drivers/xunlei/driver.go b/drivers/xunlei/driver.go index de192ea3..bd2dddbc 100644 --- a/drivers/xunlei/driver.go +++ b/drivers/xunlei/driver.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "strconv" + "strings" "time" "github.com/Xhofe/alist/conf" @@ -15,6 +16,7 @@ import ( "github.com/Xhofe/alist/utils" "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/go-resty/resty/v2" + "github.com/google/uuid" ) type XunLeiCloud struct{} @@ -46,11 +48,65 @@ func (driver XunLeiCloud) Items() []base.Item { Required: true, Description: "account password", }, + { + Name: "captcha_token", + Label: "verified captcha token", + Type: base.TypeString, + }, { Name: "root_folder", Label: "root folder file_id", Type: base.TypeString, }, + { + Name: "client_version", + Label: "client version", + Default: "7.43.0.7998", + Type: base.TypeString, + Required: true, + }, + { + Name: "client_id", + Label: "client id", + Default: "Xp6vsxz_7IYVw2BB", + Type: base.TypeString, + Required: true, + }, + { + Name: "client_secret", + Label: "client secret", + Default: "Xp6vsy4tN9toTVdMSpomVdXpRmES", + Type: base.TypeString, + Required: true, + }, + { + Name: "algorithms", + Label: "algorithms", + Default: "hrVPGbeqYPs+CIscj05VpAtjalzY5yjpvlMS8bEo,DrI0uTP,HHK0VXyMgY0xk2K0o,BBaXsExvL3GadmIacjWv7ISUJp3ifAwqbJumu,5toJ7ejB+bh1,5LsZTFAFjgvFvIl1URBgOAJ,QcJ5Ry+,hYgZVz8r7REROaCYfd9,zw6gXgkk/8TtGrmx6EGfekPESLnbZfDFwqR,gtSwLnMBa8h12nF3DU6+LwEQPHxd,fMG8TvtAYbCkxuEbIm0Xi/Lb7Z", + Type: base.TypeString, + Required: true, + }, + { + Name: "package_name", + Label: "package name", + Default: "com.xunlei.downloadprovider", + Type: base.TypeString, + Required: true, + }, + { + Name: "user_agent", + Label: "user agent", + Default: "ANDROID-com.xunlei.downloadprovider/7.43.0.7998 netWorkType/WIFI appid/40 deviceName/Samsung_Sm-g9810 deviceModel/SM-G9810 OSVersion/7.1.2 protocolVersion/301 platformVersion/10 sdkVersion/220200 Oauth2Client/0.9 (Linux 4_0_9+) (JAVA 0)", + Type: base.TypeString, + Required: false, + }, + { + Name: "device_id", + Label: "device id", + Default: utils.GetMD5Encode(uuid.NewString()), + Type: base.TypeString, + Required: false, + }, } } @@ -58,10 +114,18 @@ func (driver XunLeiCloud) Save(account *model.Account, old *model.Account) error if account == nil { return nil } + client := GetClient(account) + // 指定验证通过的captchaToken + if client.captchaToken != "" { + client.captchaToken = account.CaptchaToken + account.CaptchaToken = "" + } + if client.token == "" { return client.Login(account) } + account.Status = "work" model.SaveAccount(account) return nil @@ -105,6 +169,7 @@ func (driver XunLeiCloud) Files(path string, account *model.Account) ([]model.Fi return nil, err } + time.Sleep(time.Millisecond * 400) files := make([]model.File, 0) for { var fileList FileList @@ -161,7 +226,9 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li return nil, base.ErrNotFile } var lFile Files - _, err = GetClient(account).Request("GET", FILE_API_URL+"/"+file.Id, func(r *resty.Request) { + clinet := GetClient(account) + _, err = clinet.Request("GET", FILE_API_URL+"/{fileID}", func(r *resty.Request) { + r.SetPathParam("fileID", file.Id) r.SetQueryParam("with_audit", "true") r.SetResult(&lFile) }, account) @@ -170,7 +237,7 @@ func (driver XunLeiCloud) Link(args base.Args, account *model.Account) (*base.Li } return &base.Link{ Headers: []base.Header{ - {Name: "User-Agent", Value: base.UserAgent}, + {Name: "User-Agent", Value: clinet.userAgent}, }, Url: lFile.WebContentLink, }, nil @@ -201,7 +268,8 @@ func (driver XunLeiCloud) Rename(src string, dst string, account *model.Account) if err != nil { return err } - _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/"+srcFile.Id, func(r *resty.Request) { + _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}", func(r *resty.Request) { + r.SetPathParam("fileID", srcFile.Id) r.SetBody(&base.Json{"name": filepath.Base(dst)}) }, account) return err @@ -270,7 +338,8 @@ func (driver XunLeiCloud) Delete(path string, account *model.Account) error { if err != nil { return err } - _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/"+srcFile.Id+"/trash", func(r *resty.Request) { + _, err = GetClient(account).Request("PATCH", FILE_API_URL+"/{fileID}/trash", func(r *resty.Request) { + r.SetPathParam("fileID", srcFile.Id) r.SetBody(&base.Json{}) }, account) return err @@ -318,6 +387,7 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account) param := resp.Resumable.Params if resp.UploadType == UPLOAD_TYPE_RESUMABLE { + param.Endpoint = strings.TrimLeft(param.Endpoint, param.Bucket+".") client, err := oss.New(param.Endpoint, param.AccessKeyID, param.AccessKeySecret, oss.SecurityToken(param.SecurityToken), oss.EnableMD5(true)) if err != nil { return err @@ -331,7 +401,6 @@ func (driver XunLeiCloud) Upload(file *model.FileStream, account *model.Account) return err } } - time.Sleep(time.Millisecond * 200) return nil } diff --git a/drivers/xunlei/types.go b/drivers/xunlei/types.go index cd5008f4..87f9df04 100644 --- a/drivers/xunlei/types.go +++ b/drivers/xunlei/types.go @@ -12,6 +12,10 @@ type Erron struct { // ErrorDetails interface{} `json:"error_details"` } +func (e *Erron) HasError() bool { + return e.ErrorCode != 0 || e.ErrorMsg != "" || e.ErrorDescription != "" +} + func (e *Erron) Error() string { return fmt.Sprintf("ErrorCode: %d ,Error: %s ,ErrorDescription: %s ", e.ErrorCode, e.ErrorMsg, e.ErrorDescription) } @@ -25,7 +29,7 @@ type CaptchaTokenRequest struct { ClientID string `json:"client_id"` DeviceID string `json:"device_id"` Meta map[string]string `json:"meta"` - //RedirectUri string `json:"redirect_uri"` + RedirectUri string `json:"redirect_uri"` } type CaptchaTokenResponse struct { @@ -173,9 +177,3 @@ type UploadTaskResponse struct { File Files `json:"file"` } - -type Tasks struct { - Tasks []interface{} - NextPageToken string `json:"next_page_token"` - //ExpiresIn int64 `json:"expires_in"` -} diff --git a/drivers/xunlei/util.go b/drivers/xunlei/util.go index a2470c3d..040b9bae 100644 --- a/drivers/xunlei/util.go +++ b/drivers/xunlei/util.go @@ -3,41 +3,10 @@ package xunlei import ( "crypto/sha1" "encoding/hex" - "fmt" "io" - "net" "net/url" - - "github.com/Xhofe/alist/utils" ) -const ( - // 小米浏览器 - CLIENT_ID = "X7MtiU0Gb5YqWv-6" - CLIENT_SECRET = "84MYEih3Eeu2HF4RrGce3Q" - CLIENT_VERSION = "5.1.0.51045" - - ALG_VERSION = "1" - PACKAGE_NAME = "com.xunlei.xcloud.lib" -) - -var Algorithms = []string{ - "", - "BXza40wm+P4zw8rEFpHA", - "UfZLfKfYRmKTA0", - "OMBGVt/9Wcaln1XaBz", - "Jn217F4rk5FPPWyhoeV", - "w5OwkGo0pGpb0Xe/XZ5T3", - "5guM3DNiY4F78x49zQ97q75", - "QXwn4D2j884wJgrYXjGClM/IVrJX", - "NXBRosYvbHIm6w8vEB", - "2kZ8Ie1yW2ib4O2iAkNpJobP", - "11CoVJJQEc", - "xf3QWysVwnVsNv5DCxU+cgNT1rK", - "9eEfKkrqkfw", - "T78dnANexYRbiZy", -} - const ( API_URL = "https://api-pan.xunlei.com/drive/v1" FILE_API_URL = API_URL + "/files" @@ -45,9 +14,8 @@ const ( ) const ( - FOLDER = "drive#folder" - FILE = "drive#file" - + FOLDER = "drive#folder" + FILE = "drive#file" RESUMABLE = "drive#resumable" ) @@ -58,18 +26,9 @@ const ( UPLOAD_TYPE_URL = "UPLOAD_TYPE_URL" ) -// 验证码签名 -func captchaSign(driverID string, time int64) string { - str := fmt.Sprint(CLIENT_ID, CLIENT_VERSION, PACKAGE_NAME, driverID, time) - for _, algorithm := range Algorithms { - str = utils.GetMD5Encode(str + algorithm) - } - return ALG_VERSION + "." + str -} - func getAction(method string, u string) string { c, _ := url.Parse(u) - return fmt.Sprint(method, ":", c.Path) + return method + ":" + c.Path } // 计算文件Gcid @@ -102,13 +61,3 @@ func getGcid(r io.Reader, size int64) (string, error) { } return hex.EncodeToString(hash1.Sum(nil)), nil } - -// 获取driverID -func getDriverID(username string) string { - interfaces, _ := net.Interfaces() - str := username - for _, inter := range interfaces { - str += inter.HardwareAddr.String() - } - return utils.GetMD5Encode(str) -} diff --git a/drivers/xunlei/xunlei.go b/drivers/xunlei/xunlei.go index 692843ea..d98bd4c0 100644 --- a/drivers/xunlei/xunlei.go +++ b/drivers/xunlei/xunlei.go @@ -3,6 +3,7 @@ package xunlei import ( "fmt" "net/http" + "strings" "sync" "time" @@ -13,12 +14,7 @@ import ( log "github.com/sirupsen/logrus" ) -var xunleiClient = resty.New(). - SetHeaders(map[string]string{ - "Accept": "application/json;charset=UTF-8", - }). - SetTimeout(base.DefaultTimeout) - +// 缓存登录状态 var userClients sync.Map func GetClient(account *model.Account) *Client { @@ -27,8 +23,15 @@ func GetClient(account *model.Account) *Client { } client := &Client{ - Client: xunleiClient, - driverID: getDriverID(account.Username), + Client: base.RestyClient, + + clientID: account.ClientId, + clientSecret: account.ClientSecret, + clientVersion: account.ClientVersion, + packageName: account.PackageName, + algorithms: strings.Split(account.Algorithms, ","), + userAgent: account.UserAgent, + deviceID: account.DeviceId, } userClients.Store(account.Username, client) return client @@ -38,7 +41,14 @@ type Client struct { *resty.Client sync.Mutex - driverID string + clientID string + clientSecret string + clientVersion string + packageName string + algorithms []string + userAgent string + deviceID string + captchaToken string token string @@ -48,25 +58,26 @@ type Client struct { // 请求验证码token func (c *Client) requestCaptchaToken(action string, meta map[string]string) error { - req := CaptchaTokenRequest{ + param := CaptchaTokenRequest{ Action: action, CaptchaToken: c.captchaToken, - ClientID: CLIENT_ID, - DeviceID: c.driverID, + ClientID: c.clientID, + DeviceID: c.deviceID, Meta: meta, + RedirectUri: "xlaccsdk01://xunlei.com/callback?state=harbor", } var e Erron var resp CaptchaTokenResponse - _, err := xunleiClient.R(). - SetBody(&req). + _, err := c.Client.R(). + SetBody(¶m). SetError(&e). SetResult(&resp). Post(XLUSER_API_URL + "/shield/captcha/init") if err != nil { return err } - if e.ErrorCode != 0 || e.ErrorMsg != "" { + if e.HasError() { return &e } @@ -81,6 +92,15 @@ func (c *Client) requestCaptchaToken(action string, meta map[string]string) erro return nil } +// 验证码签名 +func (c *Client) captchaSign(time string) string { + str := fmt.Sprint(c.clientID, c.clientVersion, c.packageName, c.deviceID, time) + for _, algorithm := range c.algorithms { + str = utils.GetMD5Encode(str + algorithm) + } + return "1." + str +} + // 登录 func (c *Client) Login(account *model.Account) (err error) { c.Lock() @@ -103,13 +123,13 @@ func (c *Client) Login(account *model.Account) (err error) { var e Erron var resp TokenResponse - _, err = xunleiClient.R(). + _, err = c.Client.R(). SetResult(&resp). SetError(&e). SetBody(&SignInRequest{ CaptchaToken: c.captchaToken, - ClientID: CLIENT_ID, - ClientSecret: CLIENT_SECRET, + ClientID: c.clientID, + ClientSecret: c.clientSecret, Username: account.Username, Password: account.Password, }). @@ -118,7 +138,7 @@ func (c *Client) Login(account *model.Account) (err error) { return err } - if e.ErrorCode != 0 || e.ErrorMsg != "" { + if e.HasError() { return &e } @@ -137,14 +157,15 @@ func (c *Client) RefreshCaptchaToken(action string) error { c.Lock() defer c.Unlock() - ctime := time.Now().UnixMilli() - return c.requestCaptchaToken(action, map[string]string{ - "captcha_sign": captchaSign(c.driverID, ctime), - "client_version": CLIENT_VERSION, - "package_name": PACKAGE_NAME, - "timestamp": fmt.Sprint(ctime), + timestamp := fmt.Sprint(time.Now().UnixMilli()) + param := map[string]string{ + "client_version": c.clientVersion, + "package_name": c.packageName, "user_id": c.userID, - }) + "captcha_sign": c.captchaSign(timestamp), + "timestamp": timestamp, + } + return c.requestCaptchaToken(action, param) } // 刷新token @@ -154,22 +175,27 @@ func (c *Client) RefreshToken() error { var e Erron var resp TokenResponse - _, err := xunleiClient.R(). + _, err := c.Client.R(). SetError(&e). SetResult(&resp). SetBody(&base.Json{ "grant_type": "refresh_token", "refresh_token": c.refreshToken, - "client_id": CLIENT_ID, - "client_secret": CLIENT_SECRET, + "client_id": c.clientID, + "client_secret": c.clientSecret, }). Post(XLUSER_API_URL + "/auth/token") if err != nil { return err } - if e.ErrorCode != 0 || e.ErrorMsg != "" { + if e.HasError() { return &e } + + if resp.RefreshToken == "" { + return base.ErrEmptyToken + } + c.token = resp.TokenType + " " + resp.AccessToken c.refreshToken = resp.RefreshToken c.userID = resp.UserID @@ -178,13 +204,14 @@ func (c *Client) RefreshToken() error { func (c *Client) Request(method string, url string, callback func(*resty.Request), account *model.Account) (*resty.Response, error) { c.Lock() - req := xunleiClient.R(). + req := c.Client.R(). SetHeaders(map[string]string{ - "X-Device-Id": c.driverID, + "X-Device-Id": c.deviceID, "Authorization": c.token, "X-Captcha-Token": c.captchaToken, - }). - SetQueryParam("client_id", CLIENT_ID) + "User-Agent": c.userAgent, + "client_id": c.clientID, + }) if callback != nil { callback(req) } @@ -205,7 +232,7 @@ func (c *Client) Request(method string, url string, callback func(*resty.Request switch e.ErrorCode { case 0: return res, nil - case 4122, 4121: // token过期 + case 4122, 4121, 10: // token过期 if err = c.RefreshToken(); err == nil { break } diff --git a/model/account.go b/model/account.go index e14be95a..60033015 100644 --- a/model/account.go +++ b/model/account.go @@ -50,6 +50,13 @@ type Account struct { CustomHost string `json:"custom_host"` ExtractFolder string `json:"extract_folder"` Bool1 bool `json:"bool_1"` + // for xunlei + Algorithms string `json:"algorithms"` + ClientVersion string `json:"client_version"` + PackageName string `json:"package_name"` + UserAgent string `json:"user_agent"` + CaptchaToken string `json:"captcha_token"` + DeviceId string `json:"device_id"` } var accountsMap = make(map[string]Account)