Skip to content

Commit fdcd8fd

Browse files
author
alist-dev
committed
fix(quark): refresh __puus cookie periodically to keep download signature valid
The __puus session cookie expires after about 3 hours (see #830). Quark only re-issues it when a request does not carry the __puus field, but the driver always sends the stored cookie, so once it expires the driver can never refresh it in-process: file listing keeps working while downloads fail with 403 until restart. - add a background loop that sends a /config request without __puus and persists the reissued cookie, with success/failure logs; refresh every 100 minutes with a random jitter of +/-5 minutes (second precision) to avoid multiple instances refreshing at the same moment - use the request-time cookie snapshot for the download header so it always matches the signature generated by /file/download - protect d.Cookie read-modify-write with a mutex and send the refresh request with the stripped cookie as a request-local value, avoiding races between the refresh loop and concurrent requests - add cookie.DelStr helper and unit tests (httptest mocked endpoints, including concurrent scenarios, -race clean) Closes #830
1 parent aead76e commit fdcd8fd

5 files changed

Lines changed: 476 additions & 7 deletions

File tree

drivers/quark_uc/driver.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"net/http"
1010
"strings"
11+
"sync"
1112
"time"
1213

1314
"github.com/alist-org/alist/v3/drivers/base"
@@ -26,6 +27,15 @@ type QuarkOrUC struct {
2627
Addition
2728
config driver.Config
2829
conf Conf
30+
31+
// client 用于测试时注入自定义 client,nil 时使用全局 base.RestyClient
32+
client *resty.Client
33+
34+
// cookieMu 保护 d.Cookie 的读-改-写,避免多 goroutine(定时刷新 + 并发业务请求)竞态
35+
cookieMu sync.Mutex
36+
37+
refreshMu sync.Mutex
38+
cancel context.CancelFunc
2939
}
3040

3141
func (d *QuarkOrUC) Config() driver.Config {
@@ -46,10 +56,18 @@ func (d *QuarkOrUC) Init(ctx context.Context) error {
4656
}
4757
op.MustSaveDriverStorage(d)
4858
}
59+
// 定时刷新 __puus,避免会话 cookie 过期后下载 403(见 AlistGo/alist#830)
60+
d.startRefreshLoop()
4961
return err
5062
}
5163

5264
func (d *QuarkOrUC) Drop(ctx context.Context) error {
65+
d.refreshMu.Lock()
66+
defer d.refreshMu.Unlock()
67+
if d.cancel != nil {
68+
d.cancel()
69+
d.cancel = nil
70+
}
5371
return nil
5472
}
5573

drivers/quark_uc/util.go

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"html"
1010
"io"
11+
"math/rand"
1112
"net/http"
1213
"strconv"
1314
"strings"
@@ -24,11 +25,37 @@ import (
2425

2526
// do others that not defined in Driver interface
2627

28+
// puusRefreshInterval __puus 有效期约 3 小时,提前定时刷新,见 AlistGo/alist#830。
29+
// 100 分钟刷新一次,叠加 ±5 分钟(精确到秒)的随机抖动,
30+
// 避免多个实例/账号在同一时刻集中刷新
31+
const (
32+
puusRefreshInterval = 100 * time.Minute
33+
puusRefreshJitter = 5 * time.Minute
34+
)
35+
36+
// refreshJitter 返回 [-5min, +5min] 的随机抖动,精确到秒
37+
func refreshJitter() time.Duration {
38+
seconds := rand.Int63n(2*int64(puusRefreshJitter/time.Second)+1) - int64(puusRefreshJitter/time.Second)
39+
return time.Duration(seconds) * time.Second
40+
}
41+
2742
func (d *QuarkOrUC) request(pathname string, method string, callback base.ReqCallback, resp interface{}) ([]byte, error) {
43+
d.cookieMu.Lock()
44+
cookieStr := d.Cookie
45+
d.cookieMu.Unlock()
46+
return d.requestWithCookie(pathname, method, callback, resp, cookieStr)
47+
}
48+
49+
// requestWithCookie 使用指定的 cookie 发起请求,响应中的 __puus/__pus 会合并回 d.Cookie
50+
func (d *QuarkOrUC) requestWithCookie(pathname string, method string, callback base.ReqCallback, resp interface{}, cookieStr string) ([]byte, error) {
2851
u := d.conf.api + pathname
29-
req := base.RestyClient.R()
52+
client := base.RestyClient
53+
if d.client != nil {
54+
client = d.client
55+
}
56+
req := client.R()
3057
req.SetHeaders(map[string]string{
31-
"Cookie": d.Cookie,
58+
"Cookie": cookieStr,
3259
"Accept": "application/json, text/plain, */*",
3360
"Referer": d.conf.referer,
3461
})
@@ -46,18 +73,24 @@ func (d *QuarkOrUC) request(pathname string, method string, callback base.ReqCal
4673
if err != nil {
4774
return nil, err
4875
}
76+
var updated bool
77+
d.cookieMu.Lock()
4978
__puus := cookie.GetCookie(res.Cookies(), "__puus")
5079
if __puus != nil {
5180
d.Cookie = cookie.SetStr(d.Cookie, "__puus", __puus.Value)
52-
op.MustSaveDriverStorage(d)
81+
updated = true
5382
}
5483
if d.UseTransCodingAddress && d.config.Name == "Quark" {
5584
__pus := cookie.GetCookie(res.Cookies(), "__pus")
5685
if __pus != nil {
5786
d.Cookie = cookie.SetStr(d.Cookie, "__pus", __pus.Value)
58-
op.MustSaveDriverStorage(d)
87+
updated = true
5988
}
6089
}
90+
d.cookieMu.Unlock()
91+
if updated {
92+
op.MustSaveDriverStorage(d)
93+
}
6194
if e.Status >= 400 || e.Code != 0 {
6295
return nil, errors.New(e.Message)
6396
}
@@ -111,18 +144,24 @@ func (d *QuarkOrUC) getDownloadLink(file model.Obj) (*model.Link, error) {
111144
}
112145
var resp DownResp
113146
ua := d.conf.ua
114-
_, err := d.request("/file/download", http.MethodPost, func(req *resty.Request) {
147+
// 快照请求前的 cookie:下载 URL 的签名基于请求 /file/download 时携带的 cookie 生成,
148+
// 下载请求头必须与之一致,否则会被上游判定签名无效返回 403。
149+
// 请求和下载头使用同一个快照,避免定时刷新并发修改 d.Cookie 导致两者不一致。
150+
d.cookieMu.Lock()
151+
reqCookie := d.Cookie
152+
d.cookieMu.Unlock()
153+
_, err := d.requestWithCookie("/file/download", http.MethodPost, func(req *resty.Request) {
115154
req.SetHeader("User-Agent", ua).
116155
SetBody(data)
117-
}, &resp)
156+
}, &resp, reqCookie)
118157
if err != nil {
119158
return nil, err
120159
}
121160

122161
return &model.Link{
123162
URL: resp.Data[0].DownloadUrl,
124163
Header: http.Header{
125-
"Cookie": []string{d.Cookie},
164+
"Cookie": []string{reqCookie},
126165
"Referer": []string{d.conf.referer},
127166
"User-Agent": []string{ua},
128167
},
@@ -131,6 +170,66 @@ func (d *QuarkOrUC) getDownloadLink(file model.Obj) (*model.Link, error) {
131170
}, nil
132171
}
133172

173+
// startRefreshLoop 启动 __puus 定时刷新,保证会话 cookie 不过期
174+
func (d *QuarkOrUC) startRefreshLoop() {
175+
d.refreshMu.Lock()
176+
defer d.refreshMu.Unlock()
177+
if d.cancel != nil {
178+
return
179+
}
180+
ctx, cancel := context.WithCancel(context.Background())
181+
d.cancel = cancel
182+
go d.refreshLoop(ctx)
183+
}
184+
185+
func (d *QuarkOrUC) refreshLoop(ctx context.Context) {
186+
for {
187+
select {
188+
case <-ctx.Done():
189+
return
190+
case <-time.After(puusRefreshInterval + refreshJitter()):
191+
_ = d.refreshPuus()
192+
}
193+
}
194+
}
195+
196+
// maskSecret 打码敏感值,仅用于日志展示
197+
func maskSecret(s string) string {
198+
if len(s) <= 8 {
199+
return "***"
200+
}
201+
return s[:8] + "***"
202+
}
203+
204+
// refreshPuus 发起一次不带 __puus 的请求,让服务端重新下发会话 cookie。
205+
// 服务端只在请求缺失 __puus 字段时才更新该 cookie(见 AlistGo/alist#830)。
206+
func (d *QuarkOrUC) refreshPuus() error {
207+
d.cookieMu.Lock()
208+
old := d.Cookie
209+
stripped := cookie.DelStr(old, "__puus")
210+
d.cookieMu.Unlock()
211+
_, err := d.requestWithCookie("/config", http.MethodGet, nil, nil, stripped)
212+
d.cookieMu.Lock()
213+
defer d.cookieMu.Unlock()
214+
if err != nil {
215+
// 刷新失败:仅当没有其他请求带来更新的 __puus 时才恢复旧值,
216+
// 避免覆盖并发请求刚合并进来的新 cookie
217+
if cookie.GetStr(d.Cookie, "__puus") == "" {
218+
d.Cookie = old
219+
}
220+
log.Warnf("quark: refresh __puus failed: %v", err)
221+
return err
222+
}
223+
if cookie.GetStr(d.Cookie, "__puus") == "" {
224+
// 服务端未重新下发:同样只在没有并发新值时恢复旧值
225+
d.Cookie = old
226+
log.Infof("quark: __puus not refreshed, server did not reissue a new value, keeping existing cookie")
227+
return nil
228+
}
229+
log.Infof("quark: __puus refreshed successfully: %s", maskSecret(cookie.GetStr(d.Cookie, "__puus")))
230+
return nil
231+
}
232+
134233
func (d *QuarkOrUC) getTranscodingLink(file model.Obj) (*model.Link, error) {
135234
data := base.Json{
136235
"fid": file.GetID(),

0 commit comments

Comments
 (0)