From 2e91f5ffa5bca026041632c89dfa990359783d7e Mon Sep 17 00:00:00 2001 From: foxxorcat <95907542+foxxorcat@users.noreply.github.com> Date: Wed, 9 Mar 2022 20:30:56 +0800 Subject: [PATCH] feat: support 189 family cloud (close #612) --- drivers/189pc/189.go | 334 +++++++++++++++++ drivers/189pc/driver.go | 801 ++++++++++++++++++++++++++++++++++++++++ drivers/189pc/type.go | 167 +++++++++ drivers/189pc/util.go | 145 ++++++++ drivers/all.go | 1 + 5 files changed, 1448 insertions(+) create mode 100644 drivers/189pc/189.go create mode 100644 drivers/189pc/driver.go create mode 100644 drivers/189pc/type.go create mode 100644 drivers/189pc/util.go diff --git a/drivers/189pc/189.go b/drivers/189pc/189.go new file mode 100644 index 00000000..2d816dc9 --- /dev/null +++ b/drivers/189pc/189.go @@ -0,0 +1,334 @@ +package _189 + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/cookiejar" + "net/url" + "regexp" + "sync" + + "github.com/Xhofe/alist/conf" + "github.com/Xhofe/alist/drivers/base" + "github.com/Xhofe/alist/model" + "github.com/go-resty/resty/v2" + "github.com/google/uuid" + jsoniter "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" +) + +var userStateCache = struct { + sync.Mutex + States map[string]*State +}{States: make(map[string]*State)} + +func GetState(account *model.Account) *State { + userStateCache.Lock() + defer userStateCache.Unlock() + if v, ok := userStateCache.States[account.Username]; ok && v != nil { + return v + } + state := &State{client: resty.New(). + SetProxy("http://192.168.0.30:8888").SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}). + SetHeaders(map[string]string{ + "Accept": "application/json;charset=UTF-8", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0", + }), + } + userStateCache.States[account.Username] = state + return state +} + +type State struct { + sync.Mutex + client *resty.Client + + RsaPublicKey string + + SessionKey string + SessionSecret string + FamilySessionKey string + FamilySessionSecret string + + AccessToken string + + //怎么刷新的??? + RefreshToken string +} + +func (s *State) login(account *model.Account) error { + // 清除cookie + jar, _ := cookiejar.New(nil) + s.client.SetCookieJar(jar) + + var err error + var res *resty.Response + defer func() { + account.Status = "work" + if err != nil { + account.Status = err.Error() + } + model.SaveAccount(account) + if res != nil { + log.Debug(res.String()) + } + }() + + var param *LoginParam + param, err = s.getLoginParam() + if err != nil { + return err + } + + // 提交登录 + s.RsaPublicKey = fmt.Sprintf("-----BEGIN PUBLIC KEY-----\n%s\n-----END PUBLIC KEY-----", param.jRsaKey) + res, err = s.client.R(). + SetHeaders(map[string]string{ + "Referer": AUTH_URL, + "REQID": param.ReqId, + "lt": param.Lt, + }). + SetFormData(map[string]string{ + "appKey": APP_ID, + "accountType": "02", + "userName": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Username), + "password": "{RSA}" + rsaEncrypt(s.RsaPublicKey, account.Password), + "validateCode": param.vCodeRS, + "captchaToken": param.CaptchaToken, + "returnUrl": RETURN_URL, + "mailSuffix": "@189.cn", + "dynamicCheck": "FALSE", + "clientType": CLIENT_TYPE, + "cb_SaveName": "1", + "isOauth2": "false", + "state": "", + "paramId": param.ParamId, + }). + Post(AUTH_URL + "/api/logbox/oauth2/loginSubmit.do") + if err != nil { + return err + } + toUrl := jsoniter.Get(res.Body(), "toUrl").ToString() + if toUrl == "" { + log.Error(res.String()) + return fmt.Errorf(res.String()) + } + + // 获取Session + var erron Erron + var sessionResp appSessionResp + res, err = s.client.R(). + SetResult(&sessionResp).SetError(&erron). + SetQueryParams(clientSuffix()). + SetQueryParam("redirectURL", url.QueryEscape(toUrl)). + Post(API_URL + "/getSessionForPC.action") + if err != nil { + return err + } + + if erron.ResCode != "" { + err = fmt.Errorf(erron.ResMessage) + return err + } + if sessionResp.ResCode != 0 { + err = fmt.Errorf(sessionResp.ResMessage) + return err + } + s.SessionKey = sessionResp.SessionKey + s.SessionSecret = sessionResp.SessionSecret + s.FamilySessionKey = sessionResp.FamilySessionKey + s.FamilySessionSecret = sessionResp.FamilySessionSecret + s.AccessToken = sessionResp.AccessToken + s.RefreshToken = sessionResp.RefreshToken + return err +} + +func (s *State) getLoginParam() (*LoginParam, error) { + res, err := s.client.R(). + SetQueryParams(map[string]string{ + "appId": APP_ID, + "clientType": CLIENT_TYPE, + "returnURL": RETURN_URL, + "timeStamp": fmt.Sprint(timestamp()), + }). + Get(WEB_URL + "/api/portal/unifyLoginForPC.action") + log.Debug(res.String()) + if err != nil { + return nil, err + } + + param := &LoginParam{ + CaptchaToken: regexp.MustCompile(`'captchaToken' value='(.+?)'`).FindStringSubmatch(res.String())[1], + Lt: regexp.MustCompile(`lt = "(.+?)"`).FindStringSubmatch(res.String())[1], + ParamId: regexp.MustCompile(`paramId = "(.+?)"`).FindStringSubmatch(res.String())[1], + ReqId: regexp.MustCompile(`reqId = "(.+?)"`).FindStringSubmatch(res.String())[1], + jRsaKey: regexp.MustCompile(`"j_rsaKey" value="(.+?)"`).FindStringSubmatch(res.String())[1], + + vCodeID: regexp.MustCompile(`token=([A-Za-z0-9&=]+)`).FindStringSubmatch(res.String())[1], + } + + imgRes, err := s.client.R().Get(fmt.Sprint(AUTH_URL, "/api/logbox/oauth2/picCaptcha.do?token=", param.vCodeID, timestamp())) + if err != nil { + return nil, err + } + if len(imgRes.Body()) > 0 { + vRes, err := resty.New().R(). + SetMultipartField("image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())). + Post(conf.GetStr("ocr api")) + if err != nil { + return nil, err + } + if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 { + return nil, errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString()) + } + param.vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString() + log.Debugln("code: ", param.vCodeRS) + } + return param, nil +} + +func (s *State) refreshSession(account *model.Account) error { + var erron Erron + var userSessionResp UserSessionResp + res, err := s.client.R(). + SetResult(&userSessionResp).SetError(&erron). + SetQueryParams(clientSuffix()). + SetQueryParams(map[string]string{ + "appId": APP_ID, + "accessToken": s.AccessToken, + }). + SetHeader("X-Request-ID", uuid.NewString()). + Get("https://api.cloud.189.cn/getSessionForPC.action") + log.Debug(res.String()) + if err != nil { + return err + } + if erron.ResCode != "" { + return fmt.Errorf(erron.ResMessage) + } + + switch userSessionResp.ResCode { + case 0: + s.SessionKey = userSessionResp.SessionKey + s.SessionSecret = userSessionResp.SessionSecret + s.FamilySessionKey = userSessionResp.FamilySessionKey + s.FamilySessionSecret = userSessionResp.FamilySessionSecret + case 11, 18: + return s.login(account) + default: + account.Status = userSessionResp.ResMessage + model.SaveAccount(account) + return fmt.Errorf(userSessionResp.ResMessage) + } + return nil +} + +func (s *State) IsLogin() bool { + _, err := s.Request("GET", API_URL+"/getUserInfo.action", nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + }, nil) + return err == nil +} + +func (s *State) Login(account *model.Account) error { + s.Lock() + defer s.Unlock() + return s.login(account) +} + +func (s *State) RefreshSession(account *model.Account) error { + s.Lock() + defer s.Unlock() + return s.refreshSession(account) +} + +func (s *State) Request(method string, fullUrl string, params url.Values, callback func(*resty.Request), account *model.Account) (*resty.Response, error) { + s.Lock() + dateOfGmt := getHttpDateStr() + sessionKey := s.SessionKey + sessionSecret := s.SessionSecret + if account != nil && isFamily(account) { + sessionKey = s.FamilySessionKey + sessionSecret = s.FamilySessionSecret + } + + req := s.client.R() + req.SetHeaders(map[string]string{ + "Date": dateOfGmt, + "SessionKey": sessionKey, + "X-Request-ID": uuid.NewString(), + }) + + // 设置params + var paramsData string + if params != nil { + paramsData = AesECBEncrypt(params.Encode(), s.SessionSecret[:16]) + req.SetQueryParam("params", paramsData) + } + req.SetHeader("Signature", signatureOfHmac(sessionSecret, sessionKey, method, fullUrl, dateOfGmt, paramsData)) + + callback(req) + s.Unlock() + + var err error + var res *resty.Response + switch method { + case "GET": + res, err = req.Get(fullUrl) + case "POST": + res, err = req.Post(fullUrl) + case "DELETE": + res, err = req.Delete(fullUrl) + case "PATCH": + res, err = req.Patch(fullUrl) + case "PUT": + res, err = req.Put(fullUrl) + default: + return nil, base.ErrNotSupport + } + if err != nil { + return nil, err + } + log.Debug(res.String()) + + var erron Erron + json.Unmarshal(res.Body(), &erron) + if erron.ResCode != "" { + return nil, fmt.Errorf(erron.ResMessage) + } + if erron.Code != "" && erron.Code != "SUCCESS" { + if erron.Msg == "" { + return nil, fmt.Errorf(erron.Message) + } + return nil, fmt.Errorf(erron.Msg) + } + if erron.ErrorCode != "" { + return nil, fmt.Errorf(erron.ErrorMsg) + } + + if account != nil { + switch jsoniter.Get(res.Body(), "res_code").ToInt64() { + case 11, 18: + if err := s.refreshSession(account); err != nil { + return nil, err + } + return s.Request(method, fullUrl, params, callback, account) + case 0: + if res.StatusCode() == http.StatusOK { + return res, nil + } + fallthrough + default: + return nil, fmt.Errorf(res.String()) + } + } + + if jsoniter.Get(res.Body(), "res_code").ToInt64() != 0 { + return res, fmt.Errorf(jsoniter.Get(res.Body(), "res_message").ToString()) + } + return res, nil +} diff --git a/drivers/189pc/driver.go b/drivers/189pc/driver.go new file mode 100644 index 00000000..58e61f00 --- /dev/null +++ b/drivers/189pc/driver.go @@ -0,0 +1,801 @@ +package _189 + +import ( + "crypto/md5" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "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" + log "github.com/sirupsen/logrus" +) + +func init() { + base.RegisterDriver(new(Cloud189)) +} + +type Cloud189 struct { +} + +func (driver Cloud189) Config() base.DriverConfig { + return base.DriverConfig{ + Name: "189CloudPC", + } +} + +func (driver Cloud189) Items() []base.Item { + return []base.Item{ + { + Name: "username", + Label: "username", + Type: base.TypeString, + Required: true, + Description: "account username/phone number", + }, + { + Name: "password", + Label: "password", + Type: base.TypeString, + Required: true, + Description: "account password", + }, + { + Name: "root_folder", + Label: "root folder file_id", + Type: base.TypeString, + Required: true, + }, + { + Name: "internal_type", + Label: "189cloud type", + Type: base.TypeSelect, + Required: true, + Values: "Personal,Family", + }, + { + Name: "site_id", + Label: "family id", + Type: base.TypeString, + }, + { + Name: "order_by", + Label: "order_by", + Type: base.TypeSelect, + Values: "filename,filesize,lastOpTime", + Required: true, + }, + { + Name: "order_direction", + Label: "desc", + Type: base.TypeSelect, + Values: "true,false", + Required: true, + }, + } +} + +func (driver Cloud189) Save(account *model.Account, old *model.Account) error { + if account == nil { + return nil + } + + if !isFamily(account) && account.RootFolder == "" { + account.RootFolder = "-11" + } + + state := GetState(account) + if !state.IsLogin() { + return state.Login(account) + } + account.Status = "work" + model.SaveAccount(account) + return nil +} + +func (driver Cloud189) File(path string, account *model.Account) (*model.File, error) { + path = utils.ParsePath(path) + if path == "/" { + return &model.File{ + Id: account.RootFolder, + Name: account.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: account.UpdatedAt, + }, nil + } + dir, name := filepath.Split(path) + files, err := driver.Files(dir, account) + if err != nil { + return nil, err + } + for _, file := range files { + if file.Name == name { + return &file, nil + } + } + return nil, base.ErrPathNotFound +} + +func (driver Cloud189) Files(path string, account *model.Account) ([]model.File, error) { + path = utils.ParsePath(path) + cache, err := base.GetCache(path, account) + if err == nil { + files, _ := cache.([]model.File) + return files, nil + } + + file, err := driver.File(path, account) + if err != nil { + return nil, err + } + + fullUrl := API_URL + if isFamily(account) { + fullUrl += "/family/file" + } + fullUrl += "/listFiles.action" + + files := make([]model.File, 0) + client := GetState(account) + for pageNum := 1; ; pageNum++ { + var resp Cloud189FilesResp + queryparam := map[string]string{ + "folderId": file.Id, + "fileType": "0", + "mediaAttr": "0", + "iconOption": "5", + "pageNum": fmt.Sprint(pageNum), + "pageSize": "130", + } + _, err = client.Request("GET", fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()).SetQueryParams(queryparam) + if isFamily(account) { + r.SetQueryParams(map[string]string{ + "familyId": account.SiteId, + "orderBy": toFamilyOrderBy(account.OrderBy), + "descending": account.OrderDirection, + }) + } else { + r.SetQueryParams(map[string]string{ + "recursive": "0", + "orderBy": account.OrderBy, + "descending": account.OrderDirection, + }) + } + r.SetResult(&resp) + }, account) + if err != nil { + return nil, err + } + // 获取完毕跳出 + if resp.FileListAO.Count == 0 { + break + } + + mustTime := func(str string) *time.Time { + time, _ := http.ParseTime(str) + return &time + } + for _, folder := range resp.FileListAO.FolderList { + files = append(files, model.File{ + Id: fmt.Sprint(folder.ID), + Name: folder.Name, + Size: 0, + Type: conf.FOLDER, + Driver: driver.Config().Name, + UpdatedAt: mustTime(folder.CreateDate), + }) + } + for _, file := range resp.FileListAO.FileList { + files = append(files, model.File{ + Id: fmt.Sprint(file.ID), + Name: file.Name, + Size: file.Size, + Type: utils.GetFileType(filepath.Ext(file.Name)), + Driver: driver.Config().Name, + UpdatedAt: mustTime(file.CreateDate), + Thumbnail: file.Icon.SmallUrl, + }) + } + } + return files, nil +} + +func (driver Cloud189) Path(path string, account *model.Account) (*model.File, []model.File, error) { + path = utils.ParsePath(path) + log.Debugf("189PC path: %s", path) + file, err := driver.File(path, account) + if err != nil { + return nil, nil, err + } + if !file.IsDir() { + return file, nil, nil + } + files, err := driver.Files(path, account) + if err != nil { + return nil, nil, err + } + return nil, files, nil +} + +func (driver Cloud189) Link(args base.Args, account *model.Account) (*base.Link, error) { + file, err := driver.File(utils.ParsePath(args.Path), account) + if err != nil { + return nil, err + } + if file.Type == conf.FOLDER { + return nil, base.ErrNotFile + } + + fullUrl := API_URL + if isFamily(account) { + fullUrl += "/family/file" + } + fullUrl += "/getFileDownloadUrl.action" + + var downloadUrl struct { + URL string `json:"fileDownloadUrl"` + } + _, err = GetState(account).Request("GET", fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()).SetQueryParam("fileId", file.Id) + if isFamily(account) { + r.SetQueryParams(map[string]string{ + "familyId": account.SiteId, + }) + } else { + r.SetQueryParams(map[string]string{ + "dt": "3", + "flag": "1", + }) + } + r.SetResult(&downloadUrl) + }, account) + if err != nil { + return nil, err + } + return &base.Link{Url: strings.ReplaceAll(downloadUrl.URL, "&", "&")}, nil +} + +func (driver Cloud189) Preview(path string, account *model.Account) (interface{}, error) { + return nil, base.ErrNotSupport +} + +func (driver Cloud189) MakeDir(path string, account *model.Account) error { + dir, name := filepath.Split(path) + parentFile, err := driver.File(dir, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + + fullUrl := API_URL + if isFamily(account) { + fullUrl += "/family/file" + } + fullUrl += "/createFolder.action" + + _, err = GetState(account).Request("POST", fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()).SetQueryParams(map[string]string{ + "folderName": name, + "relativePath": "", + }) + if isFamily(account) { + r.SetQueryParams(map[string]string{ + "familyId": account.SiteId, + "parentId": parentFile.Id, + }) + } else { + r.SetQueryParams(map[string]string{ + "parentFolderId": parentFile.Id, + }) + } + }, account) + return err +} + +func (driver Cloud189) Move(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + + dstDirFile, err := driver.File(filepath.Dir(dst), account) + if err != nil { + return err + } + + _, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) { + r.SetFormData(clientSuffix()).SetFormData(map[string]string{ + "type": "MOVE", + "taskInfos": string(MustToBytes(json.Marshal( + []*BatchTaskInfo{ + { + FileId: srcFile.Id, + FileName: srcFile.Name, + IsFolder: BoolToNumber(srcFile.IsDir()), + }, + }))), + "targetFolderId": dstDirFile.Id, + }) + if isFamily(account) { + r.SetFormData(map[string]string{ + "familyId": account.SiteId, + }) + } + }, account) + return err +} + +/* +func (driver Cloud189) Move(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + + dstDirFile, err := driver.File(filepath.Dir(dst), account) + if err != nil { + return err + } + + var queryParam map[string]string + fullUrl := API_URL + method := "POST" + if isFamily(account) { + fullUrl += "/family/file" + method = "GET" + } + if srcFile.IsDir() { + fullUrl += "/moveFolder.action" + queryParam = map[string]string{ + "folderId": srcFile.Id, + "destFolderName": srcFile.Name, + } + } else { + fullUrl += "/moveFile.action" + queryParam = map[string]string{ + "fileId": srcFile.Id, + "destFileName": srcFile.Name, + } + } + + _, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(queryParam).SetQueryParams(clientSuffix()) + if isFamily(account) { + r.SetQueryParams(map[string]string{ + "familyId": account.SiteId, + "destParentId": dstDirFile.Id, + }) + } else { + r.SetQueryParam("destParentFolderId", dstDirFile.Id) + } + }, account) + return err +}*/ + +func (driver Cloud189) Rename(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + + var queryParam map[string]string + fullUrl := API_URL + method := "POST" + if isFamily(account) { + fullUrl += "/family/file" + method = "GET" + } + if srcFile.IsDir() { + fullUrl += "/renameFolder.action" + queryParam = map[string]string{ + "folderId": srcFile.Id, + "destFolderName": filepath.Base(dst), + } + } else { + fullUrl += "/renameFile.action" + queryParam = map[string]string{ + "fileId": srcFile.Id, + "destFileName": filepath.Base(dst), + } + } + + _, err = GetState(account).Request(method, fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(queryParam).SetQueryParams(clientSuffix()) + if isFamily(account) { + r.SetQueryParam("familyId", account.SiteId) + } + }, account) + return err +} + +func (driver Cloud189) Copy(src string, dst string, account *model.Account) error { + srcFile, err := driver.File(src, account) + if err != nil { + return err + } + + dstDirFile, err := driver.File(filepath.Dir(dst), account) + if err != nil { + return err + } + + isFolder := 0 + if srcFile.IsDir() { + isFolder = 1 + } + + _, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) { + r.SetFormData(clientSuffix()).SetFormData(map[string]string{ + "type": "COPY", + "taskInfos": string(MustToBytes(json.Marshal( + []*BatchTaskInfo{ + { + FileId: srcFile.Id, + FileName: srcFile.Name, + IsFolder: isFolder, + }, + }))), + "targetFolderId": dstDirFile.Id, + "targetFileName": filepath.Base(dst), + }) + if isFamily(account) { + r.SetFormData(map[string]string{ + "familyId": account.SiteId, + }) + } + }, account) + return err +} + +func (driver Cloud189) Delete(path string, account *model.Account) error { + path = utils.ParsePath(path) + srcFile, err := driver.File(path, account) + if err != nil { + return err + } + + _, err = GetState(account).Request("POST", API_URL+"/batch/createBatchTask.action", nil, func(r *resty.Request) { + r.SetFormData(clientSuffix()).SetFormData(map[string]string{ + "type": "DELETE", + "taskInfos": string(MustToBytes(json.Marshal( + []*BatchTaskInfo{ + { + FileId: srcFile.Id, + FileName: srcFile.Name, + IsFolder: BoolToNumber(srcFile.IsDir()), + }, + }))), + }) + + if isFamily(account) { + r.SetFormData(map[string]string{ + "familyId": account.SiteId, + }) + } + }, account) + return err +} + +func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + + parentFile, err := driver.File(file.ParentPath, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + + if isFamily(account) { + return driver.uploadFamily(file, parentFile, account) + } + return driver.uploadPerson(file, parentFile, account) +} + +func (driver Cloud189) uploadFamily(file *model.FileStream, parentFile *model.File, account *model.Account) error { + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") + if err != nil { + return err + } + + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + fileMd5 := md5.New() + if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil { + return err + } + + client := GetState(account) + var createUpload CreateUploadFileResult + _, err = client.Request("GET", API_URL+"/family/file/createFamilyFile.action", nil, func(r *resty.Request) { + r.SetQueryParams(map[string]string{ + "fileMd5": hex.EncodeToString(fileMd5.Sum(nil)), + "fileName": file.Name, + "familyId": fmt.Sprint(account.SiteId), + "parentId": parentFile.Id, + "resumePolicy": "1", + "fileSize": fmt.Sprint(file.Size), + }) + r.SetQueryParams(clientSuffix()) + r.SetResult(&createUpload) + }, account) + if err != nil { + return err + } + + if createUpload.FileDataExists != 1 { + if err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil { + return err + } + } + + _, err = client.Request("GET", createUpload.FileCommitUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetHeaders(map[string]string{ + "FamilyId": fmt.Sprint(account.SiteId), + "uploadFileId": fmt.Sprint(createUpload.UploadFileId), + "ResumePolicy": "1", + }) + }, account) + return err +} + +func (driver Cloud189) uploadPerson(file *model.FileStream, parentFile *model.File, account *model.Account) error { + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") + if err != nil { + return err + } + + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + fileMd5 := md5.New() + if _, err = io.Copy(io.MultiWriter(fileMd5, tempFile), file); err != nil { + return err + } + + client := GetState(account) + var createUpload CreateUploadFileResult + _, err = client.Request("POST", API_URL+"/createUploadFile.action", nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetFormData(clientSuffix()).SetFormData(map[string]string{ + "parentFolderId": parentFile.Id, + "baseFileId": "", + "fileName": file.Name, + "size": fmt.Sprint(file.Size), + "md5": hex.EncodeToString(fileMd5.Sum(nil)), + // "lastWrite": param.LastWrite, + // "localPath": strings.ReplaceAll(file.ParentPath, "\\", "/"), + "opertype": "1", + "flag": "1", + "resumePolicy": "1", + "isLog": "0", + "fileExt": "", + }) + r.SetResult(&createUpload) + }, account) + if err != nil { + return err + } + + if createUpload.FileDataExists != 1 { + if err = driver.uploadFileData(file, tempFile, createUpload, account); err != nil { + return err + } + } + + _, err = client.Request("POST", createUpload.FileCommitUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetFormData(map[string]string{ + "uploadFileId": fmt.Sprint(createUpload.UploadFileId), + "opertype": "1", //5 覆盖 + "ResumePolicy": "1", + "isLog": "0", + }) + }, account) + return err +} + +func (driver Cloud189) uploadFileData(file *model.FileStream, tempFile *os.File, createUpload CreateUploadFileResult, account *model.Account) error { + var uploadFileState *UploadFileStatusResult + var err error + for i := 0; i < 10; i++ { + if uploadFileState, err = driver.getUploadFileState(createUpload.UploadFileId, account); err != nil { + return err + } + + if uploadFileState.FileDataExists == 1 || uploadFileState.DataSize == int64(file.Size) { + return nil + } + + if _, err = tempFile.Seek(uploadFileState.DataSize, io.SeekStart); err != nil { + return err + } + + _, err = GetState(account).Request("PUT", uploadFileState.FileUploadUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetHeaders(map[string]string{ + "ResumePolicy": "1", + "Edrive-UploadFileId": fmt.Sprint(createUpload.UploadFileId), + "Edrive-UploadFileRange": fmt.Sprintf("bytes=%d-%d", uploadFileState.DataSize, file.Size), + "Expect": "100-continue", + }) + if isFamily(account) { + r.SetHeader("FamilyId", fmt.Sprint(account.SiteId)) + } + r.SetBody(tempFile) + }, account) + if err == nil { + break + } + } + return err +} + +func (driver Cloud189) getUploadFileState(uploadFileId int64, account *model.Account) (*UploadFileStatusResult, error) { + fullUrl := API_URL + if isFamily(account) { + fullUrl += "/family/file/getFamilyFileStatus.action" + } else { + fullUrl += "/getUploadFileStatus.action" + } + var uploadFileState UploadFileStatusResult + _, err := GetState(account).Request("GET", fullUrl, nil, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetQueryParams(map[string]string{ + "uploadFileId": fmt.Sprint(uploadFileId), + "resumePolicy": "1", + }) + if isFamily(account) { + r.SetQueryParam("familyId", fmt.Sprint(account.SiteId)) + } + r.SetResult(&uploadFileState) + }, account) + if err != nil { + return nil, err + } + return &uploadFileState, nil +} + +/* +暂时未解决 +func (driver Cloud189) Upload(file *model.FileStream, account *model.Account) error { + if file == nil { + return base.ErrEmptyFile + } + + parentFile, err := driver.File(file.ParentPath, account) + if err != nil { + return err + } + if !parentFile.IsDir() { + return base.ErrNotFolder + } + + fullUrl := UPLOAD_URL + if isFamily(account) { + fullUrl += "/family" + } else { + fullUrl += "/person" + } + + tempFile, err := ioutil.TempFile(conf.Conf.TempDir, "file-*") + if err != nil { + return err + } + + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + // 初始化上传 + const DEFAULT int64 = 10485760 + count := int64(math.Ceil(float64(file.Size) / float64(DEFAULT))) + fileMd5 := md5.New() + silceMd5 := md5.New() + silceMd5Hexs := make([]string, 0, count) + silceMd5Base64s := make([]string, 0, count) + for i := int64(1); i <= count; i++ { + if _, err := io.CopyN(io.MultiWriter(fileMd5, silceMd5, tempFile), file, DEFAULT); err != io.EOF { + return err + } + md5Byte := silceMd5.Sum(nil) + silceMd5Hexs = append(silceMd5Hexs, strings.ToUpper(hex.EncodeToString(md5Byte))) + silceMd5Base64s = append(silceMd5Base64s, fmt.Sprint(i, "-", base64.StdEncoding.EncodeToString(md5Byte))) + } + fileMd5Hex := strings.ToUpper(hex.EncodeToString(fileMd5.Sum(nil))) + sliceMd5Hex := fileMd5Hex + if int64(file.Size) > DEFAULT { + sliceMd5Hex = strings.ToUpper(utils.GetMD5Encode(strings.Join(silceMd5Hexs, "\n"))) + } + + qID := uuid.NewString() + client := GetState(account) + param := MapToUrlValues(map[string]interface{}{ + "parentFolderId": parentFile.Id, + "fileName": url.QueryEscape(file.Name), + "fileMd5": fileMd5Hex, + "fileSize": fmt.Sprint(file.Size), + "sliceMd5": sliceMd5Hex, + "sliceSize": fmt.Sprint(DEFAULT), + }) + if isFamily(account) { + param.Set("familyId", account.SiteId) + } + + var uploadInfo InitMultiUploadResp + _, err = client.Request("GET", fullUrl+"/initMultiUpload", param, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetHeader("X-Request-ID", qID) + r.SetResult(&uploadInfo) + }, account) + if err != nil { + return err + } + + if uploadInfo.Data.FileDataExists != 1 { + param = MapToUrlValues(map[string]interface{}{ + "uploadFileId": uploadInfo.Data.UploadFileID, + "partInfo": strings.Join(silceMd5Base64s, ","), + }) + if isFamily(account) { + param.Set("familyId", account.SiteId) + } + var uploadUrls UploadUrlsResp + _, err := client.Request("GET", fullUrl+"/getMultiUploadUrls", param, func(r *resty.Request) { + r.SetQueryParams(clientSuffix()) + r.SetHeader("X-Request-ID", qID).SetHeader("content-type", "application/x-www-form-urlencoded") + r.SetResult(&uploadUrls) + + }, account) + if err != nil { + return err + } + var i int64 + for _, uploadurl := range uploadUrls.UploadUrls { + req := resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).SetProxy("http://192.168.0.30:8888").R() + for _, header := range strings.Split(decodeURIComponent(uploadurl.RequestHeader), "&") { + i := strings.Index(header, "=") + req.SetHeader(header[0:i], header[i+1:]) + } + _, err := req.SetBody(io.NewSectionReader(tempFile, i*DEFAULT, DEFAULT)).Put(uploadurl.RequestURL) + if err != nil { + return err + } + } + } + + param = MapToUrlValues(map[string]interface{}{ + "uploadFileId": uploadInfo.Data.UploadFileID, + "isLog": "0", + "opertype": "1", + }) + if isFamily(account) { + param.Set("familyId", account.SiteId) + } + _, err = client.Request("GET", fullUrl+"/commitMultiUploadFile", param, func(r *resty.Request) { + r.SetHeader("X-Request-ID", qID) + r.SetQueryParams(clientSuffix()) + }, account) + return err +} +*/ +var _ base.Driver = (*Cloud189)(nil) diff --git a/drivers/189pc/type.go b/drivers/189pc/type.go new file mode 100644 index 00000000..d1557444 --- /dev/null +++ b/drivers/189pc/type.go @@ -0,0 +1,167 @@ +package _189 + +import "encoding/xml" + +type LoginParam struct { + CaptchaToken string + Lt string + ParamId string + ReqId string + jRsaKey string + + vCodeID string + vCodeRS string +} + +// 居然有四种返回方式 +type Erron struct { + ResCode string `json:"res_code"` + ResMessage string `json:"res_message"` + + XMLName xml.Name `xml:"error"` + Code string `json:"code" xml:"code"` + Message string `json:"message" xml:"message"` + + // Code string `json:"code"` + Msg string `json:"msg"` + + ErrorCode string `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} + +// 刷新session返回 +type UserSessionResp struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + + LoginName string `json:"loginName"` + + KeepAlive int `json:"keepAlive"` + GetFileDiffSpan int `json:"getFileDiffSpan"` + GetUserInfoSpan int `json:"getUserInfoSpan"` + + // 个人云 + SessionKey string `json:"sessionKey"` + SessionSecret string `json:"sessionSecret"` + // 家庭云 + FamilySessionKey string `json:"familySessionKey"` + FamilySessionSecret string `json:"familySessionSecret"` +} + +//登录返回 +type appSessionResp struct { + UserSessionResp + + IsSaveName string `json:"isSaveName"` + + // 会话刷新Token + AccessToken string `json:"accessToken"` + //Token刷新 + RefreshToken string `json:"refreshToken"` +} + +/*文件部分*/ +// 文件 +type Cloud189File struct { + CreateDate string `json:"createDate"` + FileCata int64 `json:"fileCata"` + Icon struct { + //iconOption 5 + SmallUrl string `json:"smallUrl"` + LargeUrl string `json:"largeUrl"` + + // iconOption 10 + Max600 string `json:"max600"` + MediumURL string `json:"mediumUrl"` + } `json:"icon"` + ID int64 `json:"id"` + LastOpTime string `json:"lastOpTime"` + Md5 string `json:"md5"` + MediaType int `json:"mediaType"` + Name string `json:"name"` + Orientation int64 `json:"orientation"` + Rev string `json:"rev"` + Size int64 `json:"size"` + StarLabel int64 `json:"starLabel"` +} + +// 文件夹 +type Cloud189Folder struct { + ID int64 `json:"id"` + ParentID int64 `json:"parentId"` + Name string `json:"name"` + + FileCata int64 `json:"fileCata"` + FileCount int64 `json:"fileCount"` + + LastOpTime string `json:"lastOpTime"` + CreateDate string `json:"createDate"` + + FileListSize int64 `json:"fileListSize"` + Rev string `json:"rev"` + StarLabel int64 `json:"starLabel"` +} + +type Cloud189FilesResp 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"` +} + +// TaskInfo 任务信息 +type BatchTaskInfo struct { + // FileId 文件ID + FileId string `json:"fileId"` + // FileName 文件名 + FileName string `json:"fileName"` + // IsFolder 是否是文件夹,0-否,1-是 + IsFolder int `json:"isFolder"` + // SrcParentId 文件所在父目录ID + //SrcParentId string `json:"srcParentId"` +} + +type CreateUploadFileResult struct { + // UploadFileId 上传文件请求ID + UploadFileId int64 `json:"uploadFileId"` + // FileUploadUrl 上传文件数据的URL路径 + FileUploadUrl string `json:"fileUploadUrl"` + // FileCommitUrl 上传文件完成后确认路径 + FileCommitUrl string `json:"fileCommitUrl"` + // FileDataExists 文件是否已存在云盘中,0-未存在,1-已存在 + FileDataExists int `json:"fileDataExists"` +} + +type UploadFileStatusResult struct { + // 上传文件的ID + UploadFileId int64 `json:"uploadFileId"` + // 已上传的大小 + DataSize int64 `json:"dataSize"` + FileUploadUrl string `json:"fileUploadUrl"` + FileCommitUrl string `json:"fileCommitUrl"` + FileDataExists int `json:"fileDataExists"` +} + +/* +type InitMultiUploadResp struct { + //Code string `json:"code"` + Data struct { + UploadType int `json:"uploadType"` + UploadHost string `json:"uploadHost"` + UploadFileID string `json:"uploadFileId"` + FileDataExists int `json:"fileDataExists"` + } `json:"data"` +} +type UploadUrlsResp struct { + Code string `json:"code"` + UploadUrls map[string]Part `json:"uploadUrls"` +} + +type Part struct { + RequestURL string `json:"requestURL"` + RequestHeader string `json:"requestHeader"` +} +*/ diff --git a/drivers/189pc/util.go b/drivers/189pc/util.go new file mode 100644 index 00000000..f8153127 --- /dev/null +++ b/drivers/189pc/util.go @@ -0,0 +1,145 @@ +package _189 + +import ( + "bytes" + "crypto/aes" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + rand2 "math/rand" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Xhofe/alist/model" +) + +const ( + APP_ID = "8025431004" + CLIENT_TYPE = "10020" + VERSION = "6.2" + + WEB_URL = "https://cloud.189.cn" + AUTH_URL = "https://open.e.189.cn" + API_URL = "https://api.cloud.189.cn" + UPLOAD_URL = "https://upload.cloud.189.cn" + + RETURN_URL = "https://m.cloud.189.cn/zhuanti/2020/loginErrorPc/index.html" + + PC = "TELEPC" + MAC = "TELEMAC" + + CHANNEL_ID = "web_cloud.189.cn" +) + +func clientSuffix() map[string]string { + return map[string]string{ + "clientType": PC, + "version": VERSION, + "channelId": CHANNEL_ID, + "rand": fmt.Sprintf("%d_%d", rand2.Int63n(1e5), rand2.Int63n(1e10)), + } +} + +// 带params的SignatureOfHmac HMAC签名 +func signatureOfHmac(sessionSecret, sessionKey, operate, fullUrl, dateOfGmt, param string) string { + u, _ := url.Parse(fullUrl) + mac := hmac.New(sha1.New, []byte(sessionSecret)) + data := fmt.Sprintf("SessionKey=%s&Operate=%s&RequestURI=%s&Date=%s", sessionKey, operate, u.Path, dateOfGmt) + if param != "" { + data += fmt.Sprintf("¶ms=%s", param) + } + mac.Write([]byte(data)) + return strings.ToUpper(hex.EncodeToString(mac.Sum(nil))) +} + +// 获取http规范的时间 +func getHttpDateStr() string { + return time.Now().UTC().Format(http.TimeFormat) +} + +// RAS 加密用户名密码 +func rsaEncrypt(publicKey, origData string) string { + block, _ := pem.Decode([]byte(publicKey)) + pubInterface, _ := x509.ParsePKIXPublicKey(block.Bytes) + data, _ := rsa.EncryptPKCS1v15(rand.Reader, pubInterface.(*rsa.PublicKey), []byte(origData)) + return base64ToHex(base64.StdEncoding.EncodeToString(data)) +} + +// aes 加密params +func AesECBEncrypt(data, key string) string { + block, _ := aes.NewCipher([]byte(key)) + paddingData := PKCS7Padding([]byte(data), block.BlockSize()) + decrypted := make([]byte, len(paddingData)) + size := block.BlockSize() + for src, dst := paddingData, decrypted; len(src) > 0; src, dst = src[size:], dst[size:] { + block.Encrypt(dst[:size], src[:size]) + } + return strings.ToUpper(hex.EncodeToString(decrypted)) +} + +func PKCS7Padding(ciphertext []byte, blockSize int) []byte { + padding := blockSize - len(ciphertext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + return append(ciphertext, padtext...) +} + +// 时间戳 +func timestamp() int64 { + return time.Now().UTC().UnixNano() / 1e6 +} + +func base64ToHex(a string) string { + v, _ := base64.StdEncoding.DecodeString(a) + return strings.ToUpper(hex.EncodeToString(v)) +} + +func isFamily(account *model.Account) bool { + return account.InternalType == "Family" +} + +func toFamilyOrderBy(o string) string { + switch o { + case "filename": + return "1" + case "filesize": + return "2" + case "lastOpTime": + return "3" + default: + return "1" + } +} + +func MapToUrlValues(m map[string]interface{}) url.Values { + url := make(url.Values, len(m)) + for k, v := range m { + url.Add(k, fmt.Sprint(v)) + } + return url +} + +func decodeURIComponent(str string) string { + r, _ := url.QueryUnescape(str) + //r, _ := url.PathUnescape(str) + //r = strings.ReplaceAll(r, " ", "+") + return r +} + +func MustToBytes(b []byte, err error) []byte { + return b +} + +func BoolToNumber(b bool) int { + if b { + return 1 + } + return 0 +} diff --git a/drivers/all.go b/drivers/all.go index cce2b741..a50357fd 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -4,6 +4,7 @@ import ( _ "github.com/Xhofe/alist/drivers/123" _ "github.com/Xhofe/alist/drivers/139" _ "github.com/Xhofe/alist/drivers/189" + _ "github.com/Xhofe/alist/drivers/189pc" _ "github.com/Xhofe/alist/drivers/alidrive" _ "github.com/Xhofe/alist/drivers/alist" _ "github.com/Xhofe/alist/drivers/baidu"