Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 39 additions & 65 deletions drivers/123_share/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package _123Share

import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"net/http"
"net/url"
"sync"
"time"

Expand Down Expand Up @@ -71,7 +69,21 @@ func (d *Pan123Share) List(ctx context.Context, dir model.Obj, args model.ListAr
})
}

// resolveAnonLink 匿名换链入口,声明为 var 以便单测替换(规避真实网络/op 依赖)。
var resolveAnonLink = func(d *Pan123Share, f File, ip string) (*model.Link, error) {
return d.anonDownloadLink(f, ip)
}

func (d *Pan123Share) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) {
// 匿名优先:公开分享可免登录换直链,无需 123Pan 账号。
if f, ok := file.(File); ok {
if link, err := resolveAnonLink(d, f, args.IP); err == nil {
return link, nil
} else if errors.Is(err, err123TrafficLimit) {
// 分享方流量耗尽,账号重试也无意义,直接返回真实原因。
return nil, err
}
}
count := op.GetDriverCount("123Pan")
var lastErr error
for i := 0; i < count; i++ {
Expand All @@ -92,71 +104,33 @@ func (d *Pan123Share) link(ctx context.Context, file model.Obj, args model.LinkA
}
pan123 := storage.(*_123.Pan123)
log.Infof("[%v] 获取123文件直链 %v %v %v", pan123.ID, file.GetName(), file.GetID(), file.GetSize())
// TODO return link of file, required
if f, ok := file.(File); ok {
//var resp DownResp
var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
//"X-Real-IP": "1.1.1.1",
"X-Forwarded-For": args.IP,
}
}
data := base.Json{
"driveId": "0",
"shareKey": d.ShareKey,
"SharePwd": d.SharePwd,
"etag": f.Etag,
"fileId": f.FileId,
"s3keyFlag": f.S3KeyFlag,
"FileName": f.FileName,
"size": f.Size,
}
resp, err := pan123.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetHeaders(headers)
}, nil)
if err != nil {
return nil, err
}
downloadUrl := utils.Json.Get(resp, "data", "DownloadURL").ToString()
ou, err := url.Parse(downloadUrl)
if err != nil {
return nil, err
}
u_ := ou.String()
nu := ou.Query().Get("params")
if nu != "" {
du, _ := base64.StdEncoding.DecodeString(nu)
u, err := url.Parse(string(du))
if err != nil {
return nil, err
}
u_ = u.String()
}

log.Debug("download url: ", u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", "https://yun.123pan.com/").Get(u_)
if err != nil {
return nil, err
}
log.Debug(res.String())
exp := 15 * time.Minute
link := model.Link{
Expiration: &exp,
URL: u_,
}
log.Debugln("res code: ", res.StatusCode())
if res.StatusCode() == 302 {
link.URL = res.Header().Get("location")
} else if res.StatusCode() < 300 {
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
}
link.Header = http.Header{
"Referer": []string{fmt.Sprintf("%s://%s/", ou.Scheme, ou.Host)},
f, ok := file.(File)
if !ok {
return nil, fmt.Errorf("can't convert obj")
}
var headers map[string]string
if !utils.IsLocalIPAddr(args.IP) {
headers = map[string]string{
"X-Forwarded-For": args.IP,
}
return &link, nil
}
return nil, fmt.Errorf("can't convert obj")
data := base.Json{
"driveId": "0",
"shareKey": d.ShareKey,
"SharePwd": d.SharePwd,
"etag": f.Etag,
"fileId": f.FileId,
"s3keyFlag": f.S3KeyFlag,
"FileName": f.FileName,
"size": f.Size,
}
resp, err := pan123.Request(DownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(data).SetHeaders(headers)
}, nil)
if err != nil {
return nil, err
}
return unwrap123DownloadLink(utils.Json.Get(resp, "data", "DownloadURL").ToString())
}

func (d *Pan123Share) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error {
Expand Down
53 changes: 53 additions & 0 deletions drivers/123_share/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package _123Share

import (
"context"
"errors"
"testing"

"github.com/OpenListTeam/OpenList/v4/internal/model"
)

// File 实现了 model.Obj,可直接作为 Link 入参。

func TestPan123ShareLink_AnonymousFirstReturnsAnonLink(t *testing.T) {
// 无需 123Pan 账号:匿名成功即返回,不走账号路径。
origAnon := resolveAnonLink
anonCalls := 0
resolveAnonLink = func(d *Pan123Share, f File, ip string) (*model.Link, error) {
anonCalls++
return &model.Link{URL: "https://example.com/anon-direct"}, nil
}
t.Cleanup(func() { resolveAnonLink = origAnon })

d := &Pan123Share{}
file := File{FileName: "video.mp4"}

link, err := d.Link(context.Background(), file, model.LinkArgs{})
if err != nil {
t.Fatalf("link: %v", err)
}
if link == nil || link.URL != "https://example.com/anon-direct" {
t.Fatalf("expected anon direct link, got %+v", link)
}
if anonCalls != 1 {
t.Fatalf("expected anon resolver once, got %d", anonCalls)
}
}

func TestPan123ShareLink_TrafficLimitShortCircuits(t *testing.T) {
// 5112 流量包不足:不回退账号,直接透传真实错误。
origAnon := resolveAnonLink
resolveAnonLink = func(d *Pan123Share, f File, ip string) (*model.Link, error) {
return nil, err123TrafficLimit
}
t.Cleanup(func() { resolveAnonLink = origAnon })

d := &Pan123Share{}
file := File{FileName: "video.mp4"}

_, err := d.Link(context.Background(), file, model.LinkArgs{})
if !errors.Is(err, err123TrafficLimit) {
t.Fatalf("expected err123TrafficLimit, got %v", err)
}
}
118 changes: 116 additions & 2 deletions drivers/123_share/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ package _123Share

import (
"context"
"encoding/base64"
"errors"
"fmt"
_123 "github.com/OpenListTeam/OpenList/v4/drivers/123"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"hash/crc32"
"math"
"math/rand"
Expand All @@ -15,10 +14,14 @@ import (
"strings"
"time"

_123 "github.com/OpenListTeam/OpenList/v4/drivers/123"
"github.com/OpenListTeam/OpenList/v4/drivers/base"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/internal/op"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/go-resty/resty/v2"
jsoniter "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
)

const (
Expand All @@ -29,8 +32,17 @@ const (
FileList = MainApi + "/share/get"
DownloadInfo = MainApi + "/share/download/info"
//AuthKeySalt = "8-8D$sL8gPjom7bk#cY"

// 匿名(免登录)通道:www.123pan.cn 的分享接口对公开分享可直接换直链,无需 Bearer/auth-key。
AnonOrigin = "https://www.123pan.cn"
AnonDownloadInfo = AnonOrigin + "/b/api/share/download/info"
AnonUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"
)

// err123TrafficLimit 分享方提取流量耗尽(API code 5112)。账号重试或旧驱动都无法解决,
// 调用方应直接返回该错误而非静默回退。
var err123TrafficLimit = errors.New("123 分享流量包不足(5112)")

var idx = 0

func signPath(path string, os string, version string) (k string, v string) {
Expand Down Expand Up @@ -95,6 +107,108 @@ func (d *Pan123Share) request(url string, method string, callback base.ReqCallba
return body, nil
}

// requestAnon 匿名请求:无 Authorization、无 auth-key 签名,仅带浏览器 UA + Referer。
// 用于公开分享免登录换直链。返回原始响应体,由调用方检查 code。
func (d *Pan123Share) requestAnon(targetUrl, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
req := base.RestyClient.R()
req.SetHeaders(map[string]string{
"user-agent": AnonUA,
"referer": AnonOrigin + "/",
"origin": AnonOrigin,
"accept": "application/json,text/plain,*/*",
"platform": "android",
"app-version": "43",
"content-type": "application/json;charset=UTF-8",
})
if callback != nil {
callback(req)
}
if resp != nil {
req.SetResult(resp)
}
res, err := req.Execute(method, targetUrl)
if err != nil {
return nil, err
}
return res.Body(), nil
}

// unwrap123DownloadLink 把 download/info 返回的 DownloadURL 解包成可播放直链:
// 处理 base64 params 重定向、302 location / data.redirect_url,并设 Referer;15 分钟有效。
func unwrap123DownloadLink(downloadUrl string) (*model.Link, error) {
if downloadUrl == "" {
return nil, errors.New("empty 123 download url")
}
ou, err := url.Parse(downloadUrl)
if err != nil {
return nil, err
}
u_ := ou.String()
if nu := ou.Query().Get("params"); nu != "" {
du, e := base64.StdEncoding.DecodeString(nu)
if e != nil {
return nil, e
}
u, e := url.Parse(string(du))
if e != nil {
return nil, e
}
u_ = u.String()
}
log.Debug("123 download url: ", u_)
res, err := base.NoRedirectClient.R().SetHeader("Referer", AnonOrigin+"/").Get(u_)
if err != nil {
return nil, err
}
log.Debug(res.String())
exp := 15 * time.Minute
link := &model.Link{Expiration: &exp, URL: u_}
log.Debugln("123 res code: ", res.StatusCode())
if res.StatusCode() == 302 {
link.URL = res.Header().Get("location")
} else if res.StatusCode() < 300 {
link.URL = utils.Json.Get(res.Body(), "data", "redirect_url").ToString()
}
link.Header = http.Header{
"Referer": []string{fmt.Sprintf("%s://%s/", ou.Scheme, ou.Host)},
}
return link, nil
}

// anonDownloadLink 匿名换取 123 分享直链(无需 123Pan 账号)。
// 返回 err123TrafficLimit 表示分享方流量耗尽,调用方不应再回退账号重试。
func (d *Pan123Share) anonDownloadLink(f File, ip string) (*model.Link, error) {
headers := map[string]string{}
if !utils.IsLocalIPAddr(ip) {
headers["X-Forwarded-For"] = ip
}
body := base.Json{
"driveId": "0",
"shareKey": d.ShareKey,
"SharePwd": d.SharePwd,
"etag": f.Etag,
"fileId": f.FileId,
"s3keyFlag": f.S3KeyFlag,
"FileName": f.FileName,
"size": f.Size,
}
respBody, err := d.requestAnon(AnonDownloadInfo, http.MethodPost, func(req *resty.Request) {
req.SetBody(body).SetHeaders(headers)
}, nil)
if err != nil {
return nil, err
}
code := utils.Json.Get(respBody, "code").ToInt()
message := jsoniter.Get(respBody, "message").ToString()
if code == 5112 || strings.Contains(message, "流量包不足") || strings.Contains(message, "提取流量不足") {
return nil, err123TrafficLimit
}
if code != 0 {
return nil, errors.New(message)
}
return unwrap123DownloadLink(utils.Json.Get(respBody, "data", "DownloadURL").ToString())
}

func (d *Pan123Share) getFiles(ctx context.Context, parentId string) ([]File, error) {
page := 1
res := make([]File, 0)
Expand Down
20 changes: 19 additions & 1 deletion drivers/quark_uc_share/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,25 @@ func (d *QuarkUCShare) Link(ctx context.Context, file model.Obj, args model.Link
return link, nil
}

link, err := resolveQuarkUCShareLink(ctx, d, file, args)
// 按主账号类型选主路径,两条路互为兜底:
// 有 SVIP 主账号 → 转存(save+download 原画,全速,超大文件/ISO 可靠)为主,免转存兜底
// 否则(非 SVIP / 无账号) → 免转存(share-direct,原画、省空间省等待)为主,转存兜底
// 超大文件(如 ISO)免转存直链不稳,SVIP 走转存更可靠。
var link *model.Link
var err error
if accountIsSVIP(d) {
link, err = resolveQuarkUCShareLink(ctx, d, file, args)
if err != nil || link == nil {
log.Warnf("SVIP 转存失败,回退免转存: %v", err)
link, err = resolveShareDirectLink(d, file)
}
} else {
link, err = resolveShareDirectLink(d, file)
if err != nil || link == nil {
log.Warnf("免转存失败,回退转存: %v", err)
link, err = resolveQuarkUCShareLink(ctx, d, file, args)
}
}
if err == nil && link != nil {
quarkUCShareLinkCache.Set(key, link)
}
Expand Down
Loading
Loading