diff --git a/drivers/189/189.go b/drivers/189/189.go index f8232792..b2870208 100644 --- a/drivers/189/189.go +++ b/drivers/189/189.go @@ -1,20 +1,28 @@ package _89 import ( + "crypto/aes" + "crypto/hmac" + "crypto/md5" "crypto/rand" "crypto/rsa" + "crypto/sha1" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/json" "encoding/pem" + "errors" "fmt" "github.com/Xhofe/alist/conf" "github.com/Xhofe/alist/drivers/base" "github.com/Xhofe/alist/model" "github.com/Xhofe/alist/utils" "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" log "github.com/sirupsen/logrus" mathRand "math/rand" + "net/url" "path/filepath" "regexp" "strconv" @@ -22,7 +30,6 @@ import ( "time" ) - var client189Map map[string]*resty.Client func (driver Cloud189) FormatFile(file *Cloud189File) *model.File { @@ -90,6 +97,7 @@ func (driver Cloud189) Login(account *model.Account) error { client = resty.New() //client.SetCookieJar(cookieJar) client.SetRetryCount(3) + client.SetHeader("Referer", "https://cloud.189.cn/") } url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action" b := "" @@ -254,6 +262,102 @@ func (driver Cloud189) GetFiles(fileId string, account *model.Account) ([]Cloud1 return res, nil } +func (driver Cloud189) Request(url string, method string, form map[string]string, headers map[string]string, account *model.Account) ([]byte, error) { + client, ok := client189Map[account.Name] + if !ok { + return nil, fmt.Errorf("can't find [%s] client", account.Name) + } + //var resp base.Json + var e Cloud189Error + req := client.R().SetError(&e). + SetHeader("Accept", "application/json;charset=UTF-8"). + SetQueryParams(map[string]string{ + "noCache": random(), + }) + if form != nil { + req = req.SetFormData(form) + } + if headers != nil { + req = req.SetHeaders(headers) + } + var err error + var res *resty.Response + if strings.ToUpper(method) == "GET" { + res, err = req.Get(url) + } else { + res, err = req.Post(url) + } + if err != nil { + return nil, err + } + if e.ErrorCode != "" { + if e.ErrorCode == "InvalidSessionKey" { + err = driver.Login(account) + if err != nil { + return nil, err + } + return driver.Request(url, method, form, nil, account) + } + } + //log.Debug(res, jsoniter.Get(res.Body(),"res_code").ToInt()) + if jsoniter.Get(res.Body(),"res_code").ToInt() != 0 { + err = errors.New(jsoniter.Get(res.Body(),"res_message").ToString()) + } + return res.Body(), err +} + +func (driver Cloud189) GetSessionKey(account *model.Account) (string, error) { + resp, err := driver.Request("https://cloud.189.cn/v2/getUserBriefInfo.action", "GET", nil, nil, account) + if err != nil { + return "", err + } + return jsoniter.Get(resp, "sessionKey").ToString(), nil +} + +func (driver Cloud189) GetResKey(account *model.Account) (string, string, error) { + resp, err := driver.Request("https://cloud.189.cn/api/security/generateRsaKey.action", "GET", nil, nil, account) + if err != nil { + return "", "", err + } + return jsoniter.Get(resp, "pubKey").ToString(), jsoniter.Get(resp, "pkId").ToString(), nil +} + +func (driver Cloud189) UploadRequest(url string, form map[string]string, account *model.Account) ([]byte, error) { + sessionKey, err := driver.GetSessionKey(account) + if err != nil { + return nil, err + } + pubKey, pkId, err := driver.GetResKey(account) + if err != nil { + return nil, err + } + xRId := "e007e99a-370c-4a14-a143-1b1541972fcf" + pkey := strings.ReplaceAll(xRId, "-", "") + params := aesEncrypt(qs(form), pkey[:16]) + date := strconv.FormatInt(time.Now().Unix(), 10) + signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s¶ms=%s", sessionKey, url, date, params), pkey) + encryptionText := RsaEncode([]byte(pkey), pubKey) + res, err := base.RestyClient.R().SetHeaders(map[string]string{ + "signature": signature, + "sessionKey": sessionKey, + "encryptionText": encryptionText, + "pkId": pkId, + "x-request-id": xRId, + "x-request-date": date, + "origin": "https://cloud.189.cn", + "referer": "https://cloud.189.cn/", + }).SetQueryParam("params", params).Get("https://upload.cloud.189.cn" + url) + if err != nil { + return nil, err + } + log.Debug(res.String()) + data := res.Body() + if jsoniter.Get(data, "code").ToString() != "SUCCESS" { + return nil, errors.New(jsoniter.Get(data, "msg").ToString()) + } + return data, nil +} + func random() string { return fmt.Sprintf("0.%17v", mathRand.New(mathRand.NewSource(time.Now().UnixNano())).Int63n(100000000000000000)) } @@ -312,7 +416,76 @@ func b64tohex(a string) string { return d } +func qs(form map[string]string) string { + strList := make([]string, 0) + for k, v := range form { + strList = append(strList, fmt.Sprintf("%s=%s", k, url.QueryEscape(v))) + } + return strings.Join(strList, "&") +} + +func aesEncrypt(data, key string) string { + encrypted := AesEncryptECB([]byte(data), []byte(key)) + //return string(encrypted) + return hex.EncodeToString(encrypted) +} + +func hmacSha1(data string, secret string) string { + h := hmac.New(sha1.New, []byte(secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func AesEncryptECB(origData []byte, key []byte) (encrypted []byte) { + cipher, _ := aes.NewCipher(generateKey(key)) + length := (len(origData) + aes.BlockSize) / aes.BlockSize + plain := make([]byte, length*aes.BlockSize) + copy(plain, origData) + pad := byte(len(plain) - len(origData)) + for i := len(origData); i < len(plain); i++ { + plain[i] = pad + } + encrypted = make([]byte, len(plain)) + // 分组分块加密 + for bs, be := 0, cipher.BlockSize(); bs <= len(origData); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() { + cipher.Encrypt(encrypted[bs:be], plain[bs:be]) + } + + return encrypted +} +func AesDecryptECB(encrypted []byte, key []byte) (decrypted []byte) { + cipher, _ := aes.NewCipher(generateKey(key)) + decrypted = make([]byte, len(encrypted)) + // + for bs, be := 0, cipher.BlockSize(); bs < len(encrypted); bs, be = bs+cipher.BlockSize(), be+cipher.BlockSize() { + cipher.Decrypt(decrypted[bs:be], encrypted[bs:be]) + } + + trim := 0 + if len(decrypted) > 0 { + trim = len(decrypted) - int(decrypted[len(decrypted)-1]) + } + + return decrypted[:trim] +} +func generateKey(key []byte) (genKey []byte) { + genKey = make([]byte, 16) + copy(genKey, key) + for i := 16; i < len(key); { + for j := 0; j < 16 && i < len(key); j, i = j+1, i+1 { + genKey[j] ^= key[i] + } + } + return genKey +} + +func getMd5(data []byte) []byte { + h := md5.New() + h.Write(data) + return h.Sum(nil) +} + func init() { base.RegisterDriver(&Cloud189{}) client189Map = make(map[string]*resty.Client, 0) -} \ No newline at end of file +} diff --git a/drivers/189/driver.go b/drivers/189/driver.go index 6f701939..85d00637 100644 --- a/drivers/189/driver.go +++ b/drivers/189/driver.go @@ -1,21 +1,32 @@ package _89 import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "encoding/json" "fmt" "github.com/Xhofe/alist/conf" "github.com/Xhofe/alist/drivers/base" "github.com/Xhofe/alist/model" "github.com/Xhofe/alist/utils" "github.com/gin-gonic/gin" + jsoniter "github.com/json-iterator/go" log "github.com/sirupsen/logrus" + "io" + "math" + "net/http" "path/filepath" + "strconv" + "strings" ) -type Cloud189 struct {} +type Cloud189 struct{} func (driver Cloud189) Config() base.DriverConfig { return base.DriverConfig{ - Name: "189Cloud", + Name: "189Cloud", OnlyProxy: false, } } @@ -169,7 +180,7 @@ func (driver Cloud189) Link(path string, account *model.Account) (*base.Link, er link := base.Link{} if res.StatusCode() == 302 { link.Url = res.Header().Get("location") - }else { + } else { link.Url = resp.FileDownloadUrl } return &link, nil @@ -205,25 +216,232 @@ func (driver Cloud189) Preview(path string, account *model.Account) (interface{} return nil, base.ErrNotSupport } - func (driver Cloud189) MakeDir(path string, account *model.Account) error { - return base.ErrNotImplement + dir, name := filepath.Split(path) + parent, err := driver.File(dir, account) + if err != nil { + return err + } + if !parent.IsDir() { + return base.ErrNotFolder + } + form := map[string]string{ + "parentFolderId": parent.Id, + "folderName": name, + } + _, err = driver.Request("https://cloud.189.cn/api/open/file/createFolder.action", "POST", form,nil, account) + if err == nil { + _ = base.DeleteCache(dir, account) + } + return err } func (driver Cloud189) Move(src string, dst string, account *model.Account) error { - return base.ErrNotImplement + srcDir, _ := filepath.Split(src) + dstDir, dstName := filepath.Split(dst) + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + // rename + if srcDir == dstDir { + url := "https://cloud.189.cn/api/open/file/renameFile.action" + idKey := "fileId" + nameKey := "destFileName" + if srcFile.IsDir() { + url = "https://cloud.189.cn/api/open/file/renameFolder.action" + idKey = "folderId" + nameKey = "destFolderName" + } + form := map[string]string{ + idKey: srcFile.Id, + nameKey: dstName, + } + _, err = driver.Request(url, "POST", form,nil, account) + } else { + // move + dstDirFile, err := driver.File(dstDir, account) + if err != nil { + return err + } + isFolder := 0 + if srcFile.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": srcFile.Id, + "fileName": dstName, + "isFolder": isFolder, + }, + } + taskInfosBytes, err := json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "MOVE", + "targetFolderId": dstDirFile.Id, + "taskInfos": string(taskInfosBytes), + } + _, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form,nil, account) + } + if err == nil { + _ = base.DeleteCache(srcDir, account) + _ = base.DeleteCache(dstDir, account) + } + return err } func (driver Cloud189) Copy(src string, dst string, account *model.Account) error { - return base.ErrNotImplement + dstDir, dstName := filepath.Split(dst) + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + dstDirFile, err := driver.File(dstDir, account) + if err != nil { + return err + } + isFolder := 0 + if srcFile.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": srcFile.Id, + "fileName": dstName, + "isFolder": isFolder, + }, + } + taskInfosBytes, err := json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "COPY", + "targetFolderId": dstDirFile.Id, + "taskInfos": string(taskInfosBytes), + } + _, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form,nil, account) + if err == nil { + _ = base.DeleteCache(dstDir, account) + } + return err } func (driver Cloud189) Delete(path string, account *model.Account) error { - return base.ErrNotImplement + path = utils.ParsePath(path) + file, err := driver.File(path, account) + if err != nil { + return err + } + isFolder := 0 + if file.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": file.Id, + "fileName": file.Name, + "isFolder": isFolder, + }, + } + taskInfosBytes, err := json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "DELETE", + "targetFolderId": "", + "taskInfos": string(taskInfosBytes), + } + _, err = driver.Request("https://cloud.189.cn/api/open/batch/createBatchTask.action", "POST", form,nil, account) + if err == nil { + _ = base.DeleteCache(utils.Dir(path), account) + } + return err } +// Upload Error: decrypt encryptionText failed func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error { - return base.ErrNotImplement + const DEFAULT uint64 = 10485760 + var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT))) + var finish uint64 = 0 + parentFile, err := driver.File(file.ParentPath, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + res, err := driver.UploadRequest("/person/initMultiUpload", map[string]string{ + "parentFolderId": parentFile.Id, + "fileName": file.Name, + "fileSize": strconv.FormatInt(int64(file.Size),10), + "sliceSize": strconv.FormatInt(int64(DEFAULT),10), + "lazyCheck": "1", + },account) + if err != nil { + return err + } + uploadFileId := jsoniter.Get(res, "data.uploadFileId").ToString() + var i int64 + var byteSize uint64 + md5s := make([]string, 0) + md5Sum := md5.New() + for i = 1; i <= count; i++ { + byteSize = file.GetSize() - finish + if DEFAULT < byteSize { + byteSize = DEFAULT + } + log.Debugf("%d,%d", byteSize, finish) + byteData := make([]byte, byteSize) + n, err := io.ReadFull(file, byteData) + log.Debug(err, n) + if err != nil { + return err + } + finish += uint64(n) + md5Bytes := getMd5(byteData) + md5Str := hex.EncodeToString(md5Bytes) + md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes) + md5s = append(md5s, md5Str) + md5Sum.Write(byteData) + res, err = driver.UploadRequest("/person/getMultiUploadUrls", map[string]string{ + "partInfo": fmt.Sprintf("%s-%s",strconv.FormatInt(i,10),md5Base64), + "uploadFileId": uploadFileId, + },account) + if err != nil { + return err + } + uploadData := jsoniter.Get(res,"uploadUrls.partNumber_"+strconv.FormatInt(i,10)) + headers := strings.Split(uploadData.Get("requestHeader").ToString(),"&") + req, err := http.NewRequest("PUT", uploadData.Get("requestURL").ToString(), bytes.NewBuffer(byteData)) + if err != nil { + return err + } + for _,header := range headers{ + kv := strings.Split(header, "=") + req.Header.Set(kv[0],strings.Join(kv[1:],"=")) + } + res, err := base.HttpClient.Do(req) + if err != nil { + return err + } + log.Debugf("%+v", res) + } + id := md5Sum.Sum(nil) + res,err = driver.UploadRequest("/person/commitMultiUploadFile", map[string]string{ + "uploadFileId": uploadFileId, + "fileMd5": hex.EncodeToString(id), + "sliceMd5": utils.GetMD5Encode(strings.Join(md5s,"\n")), + "lazyCheck":"1", + },account) + if err == nil { + _ = base.DeleteCache(file.ParentPath, account) + } + return err } -var _ base.Driver = (*Cloud189)(nil) \ No newline at end of file +var _ base.Driver = (*Cloud189)(nil)