From c0d1888e256bc60afba337717e128c57bc9e761b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BE=AE=E5=87=89?= <927625802@qq.com> Date: Mon, 15 Nov 2021 18:00:43 +0800 Subject: [PATCH] :sparkles: support 189cloud(login refer to PanIndex) --- drivers/189.go | 465 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 drivers/189.go diff --git a/drivers/189.go b/drivers/189.go new file mode 100644 index 00000000..552c6a0d --- /dev/null +++ b/drivers/189.go @@ -0,0 +1,465 @@ +package drivers + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/model" + "github.com/Xhofe/alist/utils" + "github.com/gin-gonic/gin" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" + "golang.org/x/net/publicsuffix" + mathRand "math/rand" + "net/http/cookiejar" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" +) + +type Cloud189 struct { +} + +var client189Map map[string]*resty.Client + +func (c Cloud189) Items() []Item { + return []Item{ + { + Name: "username", + Label: "username", + Type: "string", + Required: true, + Description: "account username/phone number", + }, + { + Name: "password", + Label: "password", + Type: "string", + Required: true, + Description: "account password", + }, + { + Name: "root_folder", + Label: "root folder file_id", + Type: "string", + Required: true, + }, + { + Name: "order_by", + Label: "order_by", + Type: "select", + Values: "name,size,lastOpTime,createdDate", + Required: true, + }, + { + Name: "order_direction", + Label: "desc", + Type: "select", + Values: "true,false", + Required: true, + }, + } +} + +func (c Cloud189) Save(account *model.Account, old *model.Account) error { + if old != nil { + delete(client189Map, old.Name) + } + if err := c.Login(account); err != nil { + return err + } + return nil +} + +func (c Cloud189) FormatFile(file *Cloud189File) *model.File { + f := &model.File{ + Name: file.Name, + Size: file.Size, + Driver: "189Cloud", + UpdatedAt: nil, + Thumbnail: file.Icon.SmallUrl, + Url: file.Url, + } + if file.Size == -1 { + f.Type = conf.FOLDER + } else { + f.Type = utils.GetFileType(filepath.Ext(file.Name)) + } + return f +} + +func (c Cloud189) Path(path string, account *model.Account) (*model.File, []*model.File, error) { + path = utils.ParsePath(path) + log.Debugf("189 path: %s", path) + cache, err := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path)) + if err == nil { + file, ok := cache.(Cloud189File) + if ok { + return c.FormatFile(&file), nil, nil + } else { + files, _ := cache.([]Cloud189File) + if len(files) != 0 { + res := make([]*model.File, 0) + for _, file = range files { + res = append(res, c.FormatFile(&file)) + } + return nil, res, nil + } + } + } + // no cache or len(files) == 0 + fileId := account.RootFolder + if path != "/" { + dir, name := filepath.Split(path) + dir = utils.ParsePath(dir) + _, _, err = c.Path(dir, account) + if err != nil { + return nil, nil, err + } + parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir)) + parentFiles, _ := parentFiles_.([]Cloud189File) + found := false + for _, file := range parentFiles { + if file.Name == name { + found = true + if file.Size != -1 { + url, err := c.Link(path, account) + if err != nil { + return nil, nil, err + } + file.Url = url + return c.FormatFile(&file), nil, nil + } else { + fileId = strconv.FormatInt(file.Id, 10) + break + } + } + } + if !found { + return nil, nil, fmt.Errorf("path not found") + } + } + files, err := c.GetFiles(fileId, account) + if err != nil { + return nil, nil, err + } + _ = conf.Cache.Set(conf.Ctx, fmt.Sprintf("%s%s", account.Name, path), files, nil) + res := make([]*model.File, 0) + for _, file := range files { + res = append(res, c.FormatFile(&file)) + } + return nil, res, nil +} + +func (c Cloud189) GetFile(path string, account *model.Account) (*Cloud189File, error) { + dir, name := filepath.Split(path) + dir = utils.ParsePath(dir) + _, _, err := c.Path(dir, account) + if err != nil { + return nil, err + } + parentFiles_, _ := conf.Cache.Get(conf.Ctx, fmt.Sprintf("%s%s", account.Name, dir)) + parentFiles, _ := parentFiles_.([]Cloud189File) + for _, file := range parentFiles { + if file.Name == name { + if file.Size != -1 { + return &file, err + } else { + return nil, fmt.Errorf("not file") + } + } + } + return nil, fmt.Errorf("path not found") +} + +type Cloud189Down struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + FileDownloadUrl string `json:"fileDownloadUrl"` +} + +func (c Cloud189) Link(path string, account *model.Account) (string, error) { + file, err := c.GetFile(utils.ParsePath(path), account) + if err != nil { + return "", err + } + client, ok := client189Map[account.Name] + if !ok { + return "", fmt.Errorf("can't find [%s] client", account.Name) + } + var e Cloud189Error + var resp Cloud189Down + _, err = client.R().SetResult(&resp).SetError(&e). + SetHeader("Accept","application/json;charset=UTF-8"). + SetQueryParams(map[string]string{ + "noCache": random(), + "fileId": strconv.FormatInt(file.Id, 10), + }).Get("https://cloud.189.cn/api/open/file/getFileDownloadUrl.action") + if err != nil { + return "", err + } + if e.ErrorCode != "" { + if e.ErrorCode == "InvalidSessionKey" { + err = c.Login(account) + if err != nil { + return "", err + } + return c.Link(path, account) + } + } + if resp.ResCode != 0 { + return "", fmt.Errorf(resp.ResMessage) + } + return resp.FileDownloadUrl, nil +} + +func (c Cloud189) Proxy(ctx *gin.Context) { + ctx.Request.Header.Del("Origin") +} + +func (c Cloud189) Preview(path string, account *model.Account) (interface{}, error) { + return nil, nil +} + +var _ Driver = (*Cloud189)(nil) + +func init() { + RegisterDriver("189Cloud", &Cloud189{}) + client189Map = make(map[string]*resty.Client, 0) +} + +// refer to PanIndex +type LoginResp struct { + Msg string `json:"msg"` + Result int `json:"result"` + ToUrl string `json:"toUrl"` +} + +func (c Cloud189) Login(account *model.Account) error { + client, ok := client189Map[account.Name] + if !ok { + cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) + client = resty.New() + client.SetCookieJar(cookieJar) + client.SetRetryCount(3) + } + url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action" + res, err := client.R().Get(url) + if err != nil { + return err + } + b := res.String() + lt := "" + ltText := regexp.MustCompile(`lt = "(.+?)"`) + ltTextArr := ltText.FindStringSubmatch(b) + if len(ltTextArr) > 0 { + lt = ltTextArr[1] + } else { + return fmt.Errorf("ltTextArr = 0") + } + captchaToken := regexp.MustCompile(`captchaToken' value='(.+?)'`).FindStringSubmatch(b)[1] + returnUrl := regexp.MustCompile(`returnUrl = '(.+?)'`).FindStringSubmatch(b)[1] + paramId := regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(b)[1] + //reqId := regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(b)[1] + jRsakey := regexp.MustCompile(`j_rsaKey" value="(\S+)"`).FindStringSubmatch(b)[1] + vCodeID := regexp.MustCompile(`picCaptcha\.do\?token\=([A-Za-z0-9\&\=]+)`).FindStringSubmatch(b)[1] + vCodeRS := "" + if vCodeID != "" { + // need ValidateCode + } + userRsa := RsaEncode([]byte(account.Username), jRsakey) + passwordRsa := RsaEncode([]byte(account.Password), jRsakey) + url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do" + var loginResp LoginResp + res, err = client.R(). + SetHeaders(map[string]string{ + "lt": lt, + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", + "Referer": "https://open.e.189.cn/", + "accept": "application/json;charset=UTF-8", + }).SetFormData(map[string]string{ + "appKey": "cloud", + "accountType": "01", + "userName": "{RSA}" + userRsa, + "password": "{RSA}" + passwordRsa, + "validateCode": vCodeRS, + "captchaToken": captchaToken, + "returnUrl": returnUrl, + "mailSuffix": "@pan.cn", + "paramId": paramId, + "clientType": "10010", + "dynamicCheck": "FALSE", + "cb_SaveName": "1", + "isOauth2": "false", + }).Post(url) + if err != nil { + return err + } + err = json.Unmarshal(res.Body(), &loginResp) + if err != nil { + log.Error(err.Error()) + return err + } + if loginResp.Result != 0 { + return fmt.Errorf(loginResp.Msg) + } + _, err = client.R().Get(loginResp.ToUrl) + if err != nil { + log.Errorf(err.Error()) + return err + } + client189Map[account.Name] = client + return nil +} + +type Cloud189Error struct { + ErrorCode string `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} + +type Cloud189File struct { + Id int64 `json:"id"` + LastOpTime string `json:"lastOpTime"` + Name string `json:"name"` + Size int64 `json:"size"` + Icon struct { + SmallUrl string `json:"smallUrl"` + //LargeUrl string `json:"largeUrl"` + } `json:"icon"` + Url string `json:"url"` +} + +type Cloud189Folder struct { + Id int64 `json:"id"` + LastOpTime string `json:"lastOpTime"` + Name string `json:"name"` +} + +type Cloud189Files struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + FileListAO struct { + Count int `json:"count"` + FileList []Cloud189File `json:"fileList"` + FolderList []Cloud189Folder `json:"folderList"` + } `json:"fileListAO"` +} + +func (c Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud189File, error) { + client, ok := client189Map[account.Name] + if !ok { + return nil, fmt.Errorf("can't find [%s] client", account.Name) + } + res := make([]Cloud189File, 0) + pageNum := 1 + for { + var e Cloud189Error + var resp Cloud189Files + _, err := client.R().SetResult(&resp).SetError(&e). + SetHeader("Accept","application/json;charset=UTF-8"). + SetQueryParams(map[string]string{ + "noCache": random(), + "pageSize": "60", + "pageNum": strconv.Itoa(pageNum), + "mediaType": "0", + "folderId": fileId, + "iconOption": "5", + "orderBy": account.OrderBy, + "descending": account.OrderDirection, + }).Get("https://cloud.189.cn/api/open/file/listFiles.action") + if err != nil { + return nil, err + } + if e.ErrorCode != "" { + if e.ErrorCode == "InvalidSessionKey" { + err = c.Login(account) + if err != nil { + return nil, err + } + return c.GetFiles(fileId, account) + } + } + if resp.ResCode != 0 { + return nil, fmt.Errorf(resp.ResMessage) + } + if resp.FileListAO.Count == 0 { + break + } + res = append(res, resp.FileListAO.FileList...) + for _, folder := range resp.FileListAO.FolderList { + res = append(res, Cloud189File{ + Id: folder.Id, + LastOpTime: folder.LastOpTime, + Name: folder.Name, + Size: -1, + }) + } + pageNum++ + } + return res, nil +} + +func random() string { + return fmt.Sprintf("0.%17v", mathRand.New(mathRand.NewSource(time.Now().UnixNano())).Int63n(100000000000000000)) +} + +func RsaEncode(origData []byte, j_rsakey string) string { + publicKey := []byte("-----BEGIN PUBLIC KEY-----\n" + j_rsakey + "\n-----END PUBLIC KEY-----") + block, _ := pem.Decode(publicKey) + pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes) + pub := pubInterface.(*rsa.PublicKey) + b, err := rsa.EncryptPKCS1v15(rand.Reader, pub, origData) + if err != nil { + log.Errorf("err: %s", err.Error()) + } + return b64tohex(base64.StdEncoding.EncodeToString(b)) +} + +var b64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +var BI_RM = "0123456789abcdefghijklmnopqrstuvwxyz" + +func int2char(a int) string { + return strings.Split(BI_RM, "")[a] +} + +func b64tohex(a string) string { + d := "" + e := 0 + c := 0 + for i := 0; i < len(a); i++ { + m := strings.Split(a, "")[i] + if m != "=" { + v := strings.Index(b64map, m) + if 0 == e { + e = 1 + d += int2char(v >> 2) + c = 3 & v + } else if 1 == e { + e = 2 + d += int2char(c<<2 | v>>4) + c = 15 & v + } else if 2 == e { + e = 3 + d += int2char(c) + d += int2char(v >> 2) + c = 3 & v + } else { + e = 0 + d += int2char(c<<2 | v>>4) + d += int2char(15 & v) + } + } + } + if e == 1 { + d += int2char(c << 2) + } + return d +}