From a24dfddc2aa3499148f0840afd92f8b20905a59b Mon Sep 17 00:00:00 2001 From: Noah Hsu Date: Tue, 6 Sep 2022 14:39:21 +0800 Subject: [PATCH] feat: add 189cloud driver --- cmd/lang.go | 1 + drivers/189/driver.go | 215 ++++++++++++++++ drivers/189/help.go | 186 ++++++++++++++ drivers/189/meta.go | 24 ++ drivers/189/types.go | 68 +++++ drivers/189/util.go | 394 +++++++++++++++++++++++++++++ drivers/all.go | 1 + internal/aria2/aria2.go | 4 +- internal/bootstrap/data/setting.go | 1 + internal/conf/const.go | 1 + internal/setting/setting.go | 10 +- internal/sign/sign.go | 4 +- server/common/base.go | 2 +- server/handles/auth.go | 4 +- server/handles/helper.go | 7 +- server/middlewares/auth.go | 2 +- server/router.go | 2 +- server/static/static.go | 12 +- server/webdav.go | 3 +- 19 files changed, 916 insertions(+), 25 deletions(-) create mode 100644 drivers/189/driver.go create mode 100644 drivers/189/help.go create mode 100644 drivers/189/meta.go create mode 100644 drivers/189/types.go create mode 100644 drivers/189/util.go diff --git a/cmd/lang.go b/cmd/lang.go index 41b39e15..ace45cd8 100644 --- a/cmd/lang.go +++ b/cmd/lang.go @@ -43,6 +43,7 @@ func writeFile(name string, data interface{}) { log.Errorf("failed to open %s.json: %+v", name, err) return } + defer f.Close() content, err := io.ReadAll(f) if err != nil { log.Errorf("failed to read %s.json: %+v", name, err) diff --git a/drivers/189/driver.go b/drivers/189/driver.go new file mode 100644 index 00000000..a2731257 --- /dev/null +++ b/drivers/189/driver.go @@ -0,0 +1,215 @@ +package _189 + +import ( + "context" + "net/http" + "strings" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/errs" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/pkg/utils" + "github.com/go-resty/resty/v2" + log "github.com/sirupsen/logrus" +) + +type Cloud189 struct { + model.Storage + Addition + client *resty.Client + rsa Rsa + sessionKey string +} + +func (d *Cloud189) Config() driver.Config { + return config +} + +func (d *Cloud189) GetAddition() driver.Additional { + return d.Addition +} + +func (d *Cloud189) Init(ctx context.Context, storage model.Storage) error { + d.Storage = storage + err := utils.Json.UnmarshalFromString(d.Storage.Addition, &d.Addition) + if err != nil { + return err + } + d.client = resty.New(). + SetTimeout(base.DefaultTimeout). + SetRetryCount(3). + SetHeader("Referer", "https://cloud.189.cn/"). + SetHeader("User-Agent", base.UserAgent) + return d.login() +} + +func (d *Cloud189) Drop(ctx context.Context) error { + return nil +} + +func (d *Cloud189) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { + return d.getFiles(dir.GetID()) +} + +//func (d *Cloud189) Get(ctx context.Context, path string) (model.Obj, error) { +// // this is optional +// return nil, errs.NotImplement +//} + +func (d *Cloud189) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { + var resp DownResp + u := "https://cloud.189.cn/api/portal/getFileInfo.action" + _, err := d.request(u, http.MethodGet, func(req *resty.Request) { + req.SetQueryParam("fileId", file.GetID()) + }, &resp) + if err != nil { + return nil, err + } + client := resty.NewWithClient(d.client.GetClient()).SetRedirectPolicy( + resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + })) + res, err := client.R().SetHeader("User-Agent", base.UserAgent).Get("https:" + resp.FileDownloadUrl) + if err != nil { + return nil, err + } + log.Debugln(res.Status()) + log.Debugln(res.String()) + link := model.Link{} + log.Debugln("first url:", resp.FileDownloadUrl) + if res.StatusCode() == 302 { + link.URL = res.Header().Get("location") + log.Debugln("second url:", link.URL) + _, _ = client.R().Get(link.URL) + if res.StatusCode() == 302 { + link.URL = res.Header().Get("location") + } + log.Debugln("third url:", link.URL) + } else { + link.URL = resp.FileDownloadUrl + } + link.URL = strings.Replace(link.URL, "http://", "https://", 1) + return &link, nil +} + +func (d *Cloud189) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + form := map[string]string{ + "parentFolderId": parentDir.GetID(), + "folderName": dirName, + } + _, err := d.request("https://cloud.189.cn/api/open/file/createFolder.action", http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} + +func (d *Cloud189) Move(ctx context.Context, srcObj, dstDir model.Obj) error { + isFolder := 0 + if srcObj.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": srcObj.GetID(), + "fileName": srcObj.GetName(), + "isFolder": isFolder, + }, + } + taskInfosBytes, err := utils.Json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "MOVE", + "targetFolderId": dstDir.GetID(), + "taskInfos": string(taskInfosBytes), + } + _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} + +func (d *Cloud189) Rename(ctx context.Context, srcObj model.Obj, newName string) error { + url := "https://cloud.189.cn/api/open/file/renameFile.action" + idKey := "fileId" + nameKey := "destFileName" + if srcObj.IsDir() { + url = "https://cloud.189.cn/api/open/file/renameFolder.action" + idKey = "folderId" + nameKey = "destFolderName" + } + form := map[string]string{ + idKey: srcObj.GetID(), + nameKey: newName, + } + _, err := d.request(url, http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} + +func (d *Cloud189) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { + isFolder := 0 + if srcObj.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": srcObj.GetID(), + "fileName": srcObj.GetName(), + "isFolder": isFolder, + }, + } + taskInfosBytes, err := utils.Json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "COPY", + "targetFolderId": dstDir.GetID(), + "taskInfos": string(taskInfosBytes), + } + _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} + +func (d *Cloud189) Remove(ctx context.Context, obj model.Obj) error { + isFolder := 0 + if obj.IsDir() { + isFolder = 1 + } + taskInfos := []base.Json{ + { + "fileId": obj.GetID(), + "fileName": obj.GetName(), + "isFolder": isFolder, + }, + } + taskInfosBytes, err := utils.Json.Marshal(taskInfos) + if err != nil { + return err + } + form := map[string]string{ + "type": "DELETE", + "targetFolderId": "", + "taskInfos": string(taskInfosBytes), + } + _, err = d.request("https://cloud.189.cn/api/open/batch/createBatchTask.action", http.MethodPost, func(req *resty.Request) { + req.SetFormData(form) + }, nil) + return err +} + +func (d *Cloud189) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { + return d.newUpload(dstDir, stream, up) +} + +func (d *Cloud189) Other(ctx context.Context, args model.OtherArgs) (interface{}, error) { + return nil, errs.NotSupport +} + +var _ driver.Driver = (*Cloud189)(nil) diff --git a/drivers/189/help.go b/drivers/189/help.go new file mode 100644 index 00000000..a86108e5 --- /dev/null +++ b/drivers/189/help.go @@ -0,0 +1,186 @@ +package _189 + +import ( + "bytes" + "crypto/aes" + "crypto/hmac" + "crypto/md5" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + "net/url" + "regexp" + "strconv" + "strings" + + myrand "github.com/alist-org/alist/v3/pkg/utils/random" + log "github.com/sirupsen/logrus" +) + +func random() string { + return fmt.Sprintf("0.%17v", myrand.Rand.Int63n(100000000000000000)) +} + +func RsaEncode(origData []byte, j_rsakey string, hex bool) 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()) + } + res := base64.StdEncoding.EncodeToString(b) + if hex { + return b64tohex(res) + } + return res +} + +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 +} + +func qs(form map[string]string) string { + f := make(url.Values) + for k, v := range form { + f.Set(k, v) + } + return EncodeParam(f) + //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 EncodeParam(v url.Values) string { + if v == nil { + return "" + } + var buf strings.Builder + keys := make([]string, 0, len(v)) + for k := range v { + keys = append(keys, k) + } + for _, k := range keys { + vs := v[k] + for _, v := range vs { + if buf.Len() > 0 { + buf.WriteByte('&') + } + buf.WriteString(k) + buf.WriteByte('=') + //if k == "fileName" { + // buf.WriteString(encode(v)) + //} else { + buf.WriteString(v) + //} + } + } + return buf.String() +} + +func encode(str string) string { + //str = strings.ReplaceAll(str, "%", "%25") + //str = strings.ReplaceAll(str, "&", "%26") + //str = strings.ReplaceAll(str, "+", "%2B") + //return str + return url.QueryEscape(str) +} + +func AesEncrypt(data, key []byte) []byte { + block, _ := aes.NewCipher(key) + if block == nil { + return []byte{} + } + data = PKCS7Padding(data, block.BlockSize()) + decrypted := make([]byte, len(data)) + size := block.BlockSize() + for bs, be := 0, size; bs < len(data); bs, be = bs+size, be+size { + block.Encrypt(decrypted[bs:be], data[bs:be]) + } + return 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 hmacSha1(data string, secret string) string { + h := hmac.New(sha1.New, []byte(secret)) + h.Write([]byte(data)) + return hex.EncodeToString(h.Sum(nil)) +} + +func getMd5(data []byte) []byte { + h := md5.New() + h.Write(data) + return h.Sum(nil) +} + +func decodeURIComponent(str string) string { + r, _ := url.PathUnescape(str) + //r = strings.ReplaceAll(r, " ", "+") + return r +} + +func Random(v string) string { + reg := regexp.MustCompilePOSIX("[xy]") + data := reg.ReplaceAllFunc([]byte(v), func(msg []byte) []byte { + var i int64 + t := int64(16 * myrand.Rand.Float32()) + if msg[0] == 120 { + i = t + } else { + i = 3&t | 8 + } + return []byte(strconv.FormatInt(i, 16)) + }) + return string(data) +} diff --git a/drivers/189/meta.go b/drivers/189/meta.go new file mode 100644 index 00000000..0ac607d0 --- /dev/null +++ b/drivers/189/meta.go @@ -0,0 +1,24 @@ +package _189 + +import ( + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/op" +) + +type Addition struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` + driver.RootID +} + +var config = driver.Config{ + Name: "189Cloud", + LocalSort: true, + DefaultRoot: "-11", +} + +func init() { + op.RegisterDriver(config, func() driver.Driver { + return &Cloud189{} + }) +} diff --git a/drivers/189/types.go b/drivers/189/types.go new file mode 100644 index 00000000..5354db95 --- /dev/null +++ b/drivers/189/types.go @@ -0,0 +1,68 @@ +package _189 + +type LoginResp struct { + Msg string `json:"msg"` + Result int `json:"result"` + ToUrl string `json:"toUrl"` +} + +type Error struct { + ErrorCode string `json:"errorCode"` + ErrorMsg string `json:"errorMsg"` +} + +type File 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 Folder struct { + Id int64 `json:"id"` + LastOpTime string `json:"lastOpTime"` + Name string `json:"name"` +} + +type Files struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + FileListAO struct { + Count int `json:"count"` + FileList []File `json:"fileList"` + FolderList []Folder `json:"folderList"` + } `json:"fileListAO"` +} + +type UploadUrlsResp struct { + Code string `json:"code"` + UploadUrls map[string]Part `json:"uploadUrls"` +} + +type Part struct { + RequestURL string `json:"requestURL"` + RequestHeader string `json:"requestHeader"` +} + +type Rsa struct { + Expire int64 `json:"expire"` + PkId string `json:"pkId"` + PubKey string `json:"pubKey"` +} + +type Down struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + FileDownloadUrl string `json:"fileDownloadUrl"` +} + +type DownResp struct { + ResCode int `json:"res_code"` + ResMessage string `json:"res_message"` + FileDownloadUrl string `json:"downloadUrl"` +} diff --git a/drivers/189/util.go b/drivers/189/util.go new file mode 100644 index 00000000..77cf3ff6 --- /dev/null +++ b/drivers/189/util.go @@ -0,0 +1,394 @@ +package _189 + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io" + "math" + "net/http" + "regexp" + "strconv" + "strings" + "time" + + "github.com/alist-org/alist/v3/drivers/base" + "github.com/alist-org/alist/v3/internal/conf" + "github.com/alist-org/alist/v3/internal/driver" + "github.com/alist-org/alist/v3/internal/model" + "github.com/alist-org/alist/v3/internal/setting" + "github.com/alist-org/alist/v3/pkg/utils" + myrand "github.com/alist-org/alist/v3/pkg/utils/random" + "github.com/go-resty/resty/v2" + jsoniter "github.com/json-iterator/go" + log "github.com/sirupsen/logrus" +) + +// do others that not defined in Driver interface + +func (d *Cloud189) login() error { + url := "https://cloud.189.cn/api/portal/loginUrl.action?redirectURL=https%3A%2F%2Fcloud.189.cn%2Fmain.action" + b := "" + lt := "" + ltText := regexp.MustCompile(`lt = "(.+?)"`) + var res *resty.Response + var err error + for i := 0; i < 3; i++ { + res, err = d.client.R().Get(url) + if err != nil { + return err + } + // 已经登陆 + if res.RawResponse.Request.URL.String() == "https://cloud.189.cn/web/main" { + return nil + } + b = res.String() + ltTextArr := ltText.FindStringSubmatch(b) + if len(ltTextArr) > 0 { + lt = ltTextArr[1] + break + } else { + <-time.After(time.Second) + } + } + if lt == "" { + return fmt.Errorf("get page: %s \nstatus: %d \nrequest url: %s\nredirect url: %s", + b, res.StatusCode(), res.RawResponse.Request.URL.String(), res.Header().Get("location")) + } + 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 + log.Debugf("try to identify verification codes") + timeStamp := strconv.FormatInt(time.Now().UnixNano()/1e6, 10) + u := "https://open.e.189.cn/api/logbox/oauth2/picCaptcha.do?token=" + vCodeID + timeStamp + imgRes, err := d.client.R().SetHeaders(map[string]string{ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/76.0", + "Referer": "https://open.e.189.cn/api/logbox/oauth2/unifyAccountLogin.do", + "Sec-Fetch-Dest": "image", + "Sec-Fetch-Mode": "no-cors", + "Sec-Fetch-Site": "same-origin", + }).Get(u) + if err != nil { + return err + } + // Enter the verification code manually + //err = message.GetMessenger().WaitSend(message.Message{ + // Type: "image", + // Content: "data:image/png;base64," + base64.StdEncoding.EncodeToString(imgRes.Body()), + //}, 10) + //if err != nil { + // return err + //} + //vCodeRS, err = message.GetMessenger().WaitReceive(30) + // use ocr api + vRes, err := base.RestyClient.R().SetMultipartField( + "image", "validateCode.png", "image/png", bytes.NewReader(imgRes.Body())). + Post(setting.GetStr(conf.OcrApi)) + if err != nil { + return err + } + if jsoniter.Get(vRes.Body(), "status").ToInt() != 200 { + return errors.New("ocr error:" + jsoniter.Get(vRes.Body(), "msg").ToString()) + } + vCodeRS = jsoniter.Get(vRes.Body(), "result").ToString() + log.Debugln("code: ", vCodeRS) + } + userRsa := RsaEncode([]byte(d.Username), jRsakey, true) + passwordRsa := RsaEncode([]byte(d.Password), jRsakey, true) + url = "https://open.e.189.cn/api/logbox/oauth2/loginSubmit.do" + var loginResp LoginResp + res, err = d.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 = utils.Json.Unmarshal(res.Body(), &loginResp) + if err != nil { + log.Error(err.Error()) + return err + } + if loginResp.Result != 0 { + return fmt.Errorf(loginResp.Msg) + } + _, err = d.client.R().Get(loginResp.ToUrl) + return err +} + +func (d *Cloud189) request(url string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) { + var e Error + req := d.client.R().SetError(&e). + SetHeader("Accept", "application/json;charset=UTF-8"). + SetQueryParams(map[string]string{ + "noCache": random(), + }) + if callback != nil { + callback(req) + } + if resp != nil { + req.SetResult(resp) + } + res, err := req.Execute(method, url) + if err != nil { + return nil, err + } + //log.Debug(res.String()) + if e.ErrorCode != "" { + if e.ErrorCode == "InvalidSessionKey" { + err = d.login() + if err != nil { + return nil, err + } + return d.request(url, method, callback, resp) + } + } + if jsoniter.Get(res.Body(), "res_code").ToInt() != 0 { + err = errors.New(jsoniter.Get(res.Body(), "res_message").ToString()) + } + return res.Body(), err +} + +func (d *Cloud189) getFiles(fileId string) ([]model.Obj, error) { + res := make([]model.Obj, 0) + pageNum := 1 + loc, _ := time.LoadLocation("Local") + for { + var resp Files + _, err := d.request("https://cloud.189.cn/api/open/file/listFiles.action", http.MethodGet, func(req *resty.Request) { + req.SetQueryParams(map[string]string{ + //"noCache": random(), + "pageSize": "60", + "pageNum": strconv.Itoa(pageNum), + "mediaType": "0", + "folderId": fileId, + "iconOption": "5", + "orderBy": "lastOpTime", //account.OrderBy + "descending": "true", //account.OrderDirection + }) + }, &resp) + if err != nil { + return nil, err + } + if resp.FileListAO.Count == 0 { + break + } + for _, folder := range resp.FileListAO.FolderList { + lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", folder.LastOpTime, loc) + res = append(res, &model.Object{ + ID: strconv.FormatInt(folder.Id, 10), + Name: folder.Name, + Modified: lastOpTime, + IsFolder: true, + }) + } + for _, file := range resp.FileListAO.FileList { + lastOpTime, _ := time.ParseInLocation("2006-01-02 15:04:05", file.LastOpTime, loc) + res = append(res, &model.ObjThumb{ + Object: model.Object{ + ID: strconv.FormatInt(file.Id, 10), + Name: file.Name, + Modified: lastOpTime, + }, + Thumbnail: model.Thumbnail{Thumbnail: file.Icon.SmallUrl}, + }) + } + pageNum++ + } + return res, nil +} + +func (d *Cloud189) oldUpload(dstDir model.Obj, file model.FileStreamer) error { + res, err := d.client.R().SetMultipartFormData(map[string]string{ + "parentId": dstDir.GetID(), + "sessionKey": "??", + "opertype": "1", + "fname": file.GetName(), + }).SetMultipartField("Filedata", file.GetName(), file.GetMimetype(), file).Post("https://hb02.upload.cloud.189.cn/v1/DCIWebUploadAction") + if err != nil { + return err + } + if utils.Json.Get(res.Body(), "MD5").ToString() != "" { + return nil + } + log.Debugf(res.String()) + return errors.New(res.String()) +} + +func (d *Cloud189) getSessionKey() (string, error) { + resp, err := d.request("https://cloud.189.cn/v2/getUserBriefInfo.action", http.MethodGet, nil, nil) + if err != nil { + return "", err + } + sessionKey := utils.Json.Get(resp, "sessionKey").ToString() + return sessionKey, nil +} + +func (d *Cloud189) getResKey() (string, string, error) { + now := time.Now().UnixMilli() + if d.rsa.Expire > now { + return d.rsa.PubKey, d.rsa.PkId, nil + } + resp, err := d.request("https://cloud.189.cn/api/security/generateRsaKey.action", http.MethodGet, nil, nil) + if err != nil { + return "", "", err + } + pubKey, pkId := utils.Json.Get(resp, "pubKey").ToString(), utils.Json.Get(resp, "pkId").ToString() + d.rsa.PubKey, d.rsa.PkId = pubKey, pkId + d.rsa.Expire = utils.Json.Get(resp, "expire").ToInt64() + return pubKey, pkId, nil +} + +func (d *Cloud189) uploadRequest(uri string, form map[string]string, resp interface{}) ([]byte, error) { + c := strconv.FormatInt(time.Now().UnixMilli(), 10) + r := Random("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx") + l := Random("xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx") + l = l[0 : 16+int(16*myrand.Rand.Float32())] + + e := qs(form) + data := AesEncrypt([]byte(e), []byte(l[0:16])) + h := hex.EncodeToString(data) + + sessionKey := d.sessionKey + signature := hmacSha1(fmt.Sprintf("SessionKey=%s&Operate=GET&RequestURI=%s&Date=%s¶ms=%s", sessionKey, uri, c, h), l) + + pubKey, pkId, err := d.getResKey() + if err != nil { + return nil, err + } + b := RsaEncode([]byte(l), pubKey, false) + req := d.client.R().SetHeaders(map[string]string{ + "accept": "application/json;charset=UTF-8", + "SessionKey": sessionKey, + "Signature": signature, + "X-Request-Date": c, + "X-Request-ID": r, + "EncryptionText": b, + "PkId": pkId, + }) + if resp != nil { + req.SetResult(resp) + } + res, err := req.Get("https://upload.cloud.189.cn" + uri + "?params=" + h) + if err != nil { + return nil, err + } + data = res.Body() + if utils.Json.Get(data, "code").ToString() != "SUCCESS" { + return nil, errors.New(uri + "---" + jsoniter.Get(data, "msg").ToString()) + } + return data, nil +} + +func (d *Cloud189) newUpload(dstDir model.Obj, file model.FileStreamer, up driver.UpdateProgress) error { + sessionKey, err := d.getSessionKey() + if err != nil { + return err + } + d.sessionKey = sessionKey + const DEFAULT int64 = 10485760 + var count = int64(math.Ceil(float64(file.GetSize()) / float64(DEFAULT))) + + res, err := d.uploadRequest("/person/initMultiUpload", map[string]string{ + "parentFolderId": dstDir.GetID(), + "fileName": encode(file.GetName()), + "fileSize": strconv.FormatInt(file.GetSize(), 10), + "sliceSize": strconv.FormatInt(DEFAULT, 10), + "lazyCheck": "1", + }, nil) + if err != nil { + return err + } + uploadFileId := jsoniter.Get(res, "data", "uploadFileId").ToString() + //_, err = d.uploadRequest("/person/getUploadedPartsInfo", map[string]string{ + // "uploadFileId": uploadFileId, + //}, nil) + var finish int64 = 0 + var i int64 + var byteSize int64 + 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 += int64(n) + md5Bytes := getMd5(byteData) + md5Hex := hex.EncodeToString(md5Bytes) + md5Base64 := base64.StdEncoding.EncodeToString(md5Bytes) + md5s = append(md5s, strings.ToUpper(md5Hex)) + md5Sum.Write(byteData) + var resp UploadUrlsResp + res, err = d.uploadRequest("/person/getMultiUploadUrls", map[string]string{ + "partInfo": fmt.Sprintf("%s-%s", strconv.FormatInt(i, 10), md5Base64), + "uploadFileId": uploadFileId, + }, &resp) + if err != nil { + return err + } + uploadData := resp.UploadUrls["partNumber_"+strconv.FormatInt(i, 10)] + log.Debugf("uploadData: %+v", uploadData) + requestURL := uploadData.RequestURL + uploadHeaders := strings.Split(decodeURIComponent(uploadData.RequestHeader), "&") + req, _ := http.NewRequest(http.MethodPut, requestURL, bytes.NewReader(byteData)) + for _, v := range uploadHeaders { + i := strings.Index(v, "=") + req.Header.Set(v[0:i], v[i+1:]) + } + + r, err := base.HttpClient.Do(req) + log.Debugf("%+v %+v", r, r.Request.Header) + r.Body.Close() + if err != nil { + return err + } + up(int(i * 100 / count)) + } + fileMd5 := hex.EncodeToString(md5Sum.Sum(nil)) + sliceMd5 := fileMd5 + if file.GetSize() > DEFAULT { + sliceMd5 = utils.GetMD5Encode(strings.Join(md5s, "\n")) + } + res, err = d.uploadRequest("/person/commitMultiUploadFile", map[string]string{ + "uploadFileId": uploadFileId, + "fileMd5": fileMd5, + "sliceMd5": sliceMd5, + "lazyCheck": "1", + "opertype": "3", + }, nil) + return err +} diff --git a/drivers/all.go b/drivers/all.go index 6a800d60..36a23e5e 100644 --- a/drivers/all.go +++ b/drivers/all.go @@ -3,6 +3,7 @@ package drivers import ( _ "github.com/alist-org/alist/v3/drivers/123" _ "github.com/alist-org/alist/v3/drivers/139" + _ "github.com/alist-org/alist/v3/drivers/189" _ "github.com/alist-org/alist/v3/drivers/aliyundrive" _ "github.com/alist-org/alist/v3/drivers/baidu_netdisk" _ "github.com/alist-org/alist/v3/drivers/ftp" diff --git a/internal/aria2/aria2.go b/internal/aria2/aria2.go index df3f9fc5..7250afab 100644 --- a/internal/aria2/aria2.go +++ b/internal/aria2/aria2.go @@ -18,8 +18,8 @@ var client rpc.Client func InitClient(timeout int) (string, error) { client = nil - uri := setting.GetByKey(conf.Aria2Uri) - secret := setting.GetByKey(conf.Aria2Secret) + uri := setting.GetStr(conf.Aria2Uri) + secret := setting.GetStr(conf.Aria2Secret) return InitAria2Client(uri, secret, timeout) } diff --git a/internal/bootstrap/data/setting.go b/internal/bootstrap/data/setting.go index aec5050a..689304cd 100644 --- a/internal/bootstrap/data/setting.go +++ b/internal/bootstrap/data/setting.go @@ -116,6 +116,7 @@ func InitialSettings() []model.SettingItem { ([[:xdigit:]]{1,4}(?::[[:xdigit:]]{1,4}){7}|::|:(?::[[:xdigit:]]{1,4}){1,6}|[[:xdigit:]]{1,4}:(?::[[:xdigit:]]{1,4}){1,5}|(?:[[:xdigit:]]{1,4}:){2}(?::[[:xdigit:]]{1,4}){1,4}|(?:[[:xdigit:]]{1,4}:){3}(?::[[:xdigit:]]{1,4}){1,3}|(?:[[:xdigit:]]{1,4}:){4}(?::[[:xdigit:]]{1,4}){1,2}|(?:[[:xdigit:]]{1,4}:){5}:[[:xdigit:]]{1,4}|(?:[[:xdigit:]]{1,4}:){1,6}:) (?U)access_token=(.*)&`, Type: conf.TypeText, Group: model.GLOBAL, Flag: model.PRIVATE}, + {Key: conf.OcrApi, Value: "https://api.nn.ci/ocr/file/json", Type: conf.TypeString, Group: model.GLOBAL}, // aria2 settings {Key: conf.Aria2Uri, Value: "http://localhost:6800/jsonrpc", Type: conf.TypeString, Group: model.ARIA2, Flag: model.PRIVATE}, {Key: conf.Aria2Secret, Value: "", Type: conf.TypeString, Group: model.ARIA2, Flag: model.PRIVATE}, diff --git a/internal/conf/const.go b/internal/conf/const.go index fe66388c..08ac2df1 100644 --- a/internal/conf/const.go +++ b/internal/conf/const.go @@ -39,6 +39,7 @@ const ( CustomizeBody = "customize_body" LinkExpiration = "link_expiration" PrivacyRegs = "privacy_regs" + OcrApi = "ocr_api" // aria2 Aria2Uri = "aria2_uri" diff --git a/internal/setting/setting.go b/internal/setting/setting.go index 58338e5e..5cfd3e2d 100644 --- a/internal/setting/setting.go +++ b/internal/setting/setting.go @@ -6,7 +6,7 @@ import ( "github.com/alist-org/alist/v3/internal/db" ) -func GetByKey(key string, defaultValue ...string) string { +func GetStr(key string, defaultValue ...string) string { val, ok := db.GetSettingsMap()[key] if !ok { if len(defaultValue) > 0 { @@ -17,14 +17,14 @@ func GetByKey(key string, defaultValue ...string) string { return val } -func GetIntSetting(key string, defaultVal int) int { - i, err := strconv.Atoi(GetByKey(key)) +func GetInt(key string, defaultVal int) int { + i, err := strconv.Atoi(GetStr(key)) if err != nil { return defaultVal } return i } -func IsTrue(key string) bool { - return GetByKey(key) == "true" || GetByKey(key) == "1" +func GetBool(key string) bool { + return GetStr(key) == "true" || GetStr(key) == "1" } diff --git a/internal/sign/sign.go b/internal/sign/sign.go index ef733854..978ae7cc 100644 --- a/internal/sign/sign.go +++ b/internal/sign/sign.go @@ -13,7 +13,7 @@ var once sync.Once var instance sign.Sign func Sign(data string) string { - expire := setting.GetIntSetting(conf.LinkExpiration, 0) + expire := setting.GetInt(conf.LinkExpiration, 0) if expire == 0 { return NotExpired(data) } else { @@ -37,5 +37,5 @@ func Verify(data string, sign string) error { } func Instance() { - instance = sign.NewHMACSign([]byte(setting.GetByKey(conf.Token))) + instance = sign.NewHMACSign([]byte(setting.GetStr(conf.Token))) } diff --git a/server/common/base.go b/server/common/base.go index 46df102c..406176e0 100644 --- a/server/common/base.go +++ b/server/common/base.go @@ -10,7 +10,7 @@ import ( ) func GetApiUrl(r *http.Request) string { - api := setting.GetByKey(conf.ApiUrl) + api := setting.GetStr(conf.ApiUrl) protocol := "http" if r != nil { if r.TLS != nil { diff --git a/server/handles/auth.go b/server/handles/auth.go index 3b295aae..0ecd52d8 100644 --- a/server/handles/auth.go +++ b/server/handles/auth.go @@ -130,9 +130,9 @@ func Generate2FA(c *gin.Context) { // to base64 var buf bytes.Buffer png.Encode(&buf, img) - base64 := base64.StdEncoding.EncodeToString(buf.Bytes()) + b64 := base64.StdEncoding.EncodeToString(buf.Bytes()) common.SuccessResp(c, gin.H{ - "qr": "data:image/png;base64," + base64, + "qr": "data:image/png;base64," + b64, "secret": key.Secret(), }) } diff --git a/server/handles/helper.go b/server/handles/helper.go index 09e450ab..8c4c9b61 100644 --- a/server/handles/helper.go +++ b/server/handles/helper.go @@ -2,17 +2,18 @@ package handles import ( "fmt" + "net/url" + "strings" + "github.com/alist-org/alist/v3/internal/conf" "github.com/alist-org/alist/v3/internal/setting" "github.com/alist-org/alist/v3/server/common" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" - "net/url" - "strings" ) func Favicon(c *gin.Context) { - c.Redirect(302, setting.GetByKey(conf.Favicon)) + c.Redirect(302, setting.GetStr(conf.Favicon)) } func Plist(c *gin.Context) { diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index 51a54876..c22f2825 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -14,7 +14,7 @@ import ( // if token is empty, set user to guest func Auth(c *gin.Context) { token := c.GetHeader("Authorization") - if token == setting.GetByKey(conf.Token) { + if token == setting.GetStr(conf.Token) { admin, err := db.GetAdmin() if err != nil { common.ErrorResp(c, err, 500) diff --git a/server/router.go b/server/router.go index b90cb4fc..23f8f892 100644 --- a/server/router.go +++ b/server/router.go @@ -16,7 +16,7 @@ func Init(r *gin.Engine) { common.SecretKey = []byte(conf.Conf.JwtSecret) Cors(r) r.Use(middlewares.StoragesLoaded) - WebDav(r) + WebDav(r.Group("/dav")) r.GET("/favicon.ico", handles.Favicon) r.GET("/i/:link/:name", handles.Plist) diff --git a/server/static/static.go b/server/static/static.go index b186d9fc..cbe3b12e 100644 --- a/server/static/static.go +++ b/server/static/static.go @@ -26,12 +26,12 @@ func InitIndex() { func UpdateIndex() { cdn := strings.TrimSuffix(conf.Conf.Cdn, "/") - basePath := setting.GetByKey(conf.BasePath) - apiUrl := setting.GetByKey(conf.ApiUrl) - favicon := setting.GetByKey(conf.Favicon) - title := setting.GetByKey(conf.SiteTitle) - customizeHead := setting.GetByKey(conf.CustomizeHead) - customizeBody := setting.GetByKey(conf.CustomizeBody) + basePath := setting.GetStr(conf.BasePath) + apiUrl := setting.GetStr(conf.ApiUrl) + favicon := setting.GetStr(conf.Favicon) + title := setting.GetStr(conf.SiteTitle) + customizeHead := setting.GetStr(conf.CustomizeHead) + customizeBody := setting.GetStr(conf.CustomizeBody) conf.ManageHtml = conf.RawIndexHtml replaceMap1 := map[string]string{ "https://jsd.nn.ci/gh/alist-org/logo@main/logo.svg": favicon, diff --git a/server/webdav.go b/server/webdav.go index 9a5b2b37..ddd95028 100644 --- a/server/webdav.go +++ b/server/webdav.go @@ -24,8 +24,7 @@ func init() { } } -func WebDav(r *gin.Engine) { - dav := r.Group("/dav") +func WebDav(dav *gin.RouterGroup) { dav.Use(WebDAVAuth) dav.Any("/*path", ServeWebDAV) dav.Any("", ServeWebDAV)