Skip to content

Commit 3e49fa4

Browse files
authored
Merge pull request #9627 from okatu-loli/codex/fix-lanzou-download-links
fix(lanzou): support new password download pages
2 parents 2fa7a0a + c5348e9 commit 3e49fa4

5 files changed

Lines changed: 270 additions & 35 deletions

File tree

drivers/lanzou/driver.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package lanzou
33
import (
44
"context"
55
"net/http"
6+
"sync"
67

78
"github.com/alist-org/alist/v3/drivers/base"
89
"github.com/alist-org/alist/v3/internal/driver"
@@ -19,6 +20,9 @@ type LanZou struct {
1920
vei string
2021

2122
flag int32
23+
24+
clientOnce sync.Once
25+
client *resty.Client
2226
}
2327

2428
func (d *LanZou) Config() driver.Config {

drivers/lanzou/help.go

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,23 +159,24 @@ var findKVReg = regexp.MustCompile(`'(.+?)':('?([^' },]*)'?)`) // 拆分kv
159159

160160
// 根据key查询js变量
161161
func findJSVarFunc(key, data string) string {
162-
var values []string
163-
if key != "sasign" {
164-
values = regexp.MustCompile(`var ` + key + `\s*=\s*['"]?(.+?)['"]?;`).FindStringSubmatch(data)
165-
} else {
166-
matches := regexp.MustCompile(`var `+key+`\s*=\s*['"]?(.+?)['"]?;`).FindAllStringSubmatch(data, -1)
167-
if len(matches) == 3 {
168-
values = matches[1]
169-
} else {
170-
if len(matches) > 0 {
171-
values = matches[0]
162+
matches := regexp.MustCompile(`var\s+`+regexp.QuoteMeta(key)+`\s*=\s*(?:'([^']*)'|"([^"]*)"|([^;\s]+))\s*;`).FindAllStringSubmatch(data, -1)
163+
value := func(match []string) string {
164+
for _, candidate := range match[1:] {
165+
if candidate != "" {
166+
return candidate
172167
}
173168
}
174-
}
175-
if len(values) == 0 {
176169
return ""
177170
}
178-
return values[1]
171+
if key == "sasign" && len(matches) == 3 {
172+
return value(matches[1])
173+
}
174+
for i := len(matches) - 1; i >= 0; i-- {
175+
if v := value(matches[i]); v != "" {
176+
return v
177+
}
178+
}
179+
return ""
179180
}
180181

181182
var findFunction = regexp.MustCompile(`(?ims)^function[^{]+`)

drivers/lanzou/help_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package lanzou
2+
3+
import "testing"
4+
5+
func TestFindJSVarFunc(t *testing.T) {
6+
tests := []struct {
7+
name string
8+
key string
9+
data string
10+
want string
11+
}{
12+
{name: "single quoted", key: "sign", data: `var sign = 'complete-sign';`, want: "complete-sign"},
13+
{name: "double quoted", key: "sign", data: `var sign = "complete-sign";`, want: "complete-sign"},
14+
{name: "unquoted", key: "kd", data: `var kd = 1;`, want: "1"},
15+
{name: "last non-empty declaration", key: "isngis", data: `var isngis = ''; var isngis = 'real-sign';`, want: "real-sign"},
16+
{name: "sasign middle declaration", key: "sasign", data: `var sasign='first'; var sasign='middle'; var sasign='last';`, want: "middle"},
17+
}
18+
for _, tt := range tests {
19+
t.Run(tt.name, func(t *testing.T) {
20+
if got := findJSVarFunc(tt.key, tt.data); got != tt.want {
21+
t.Fatalf("findJSVarFunc() = %q, want %q", got, tt.want)
22+
}
23+
})
24+
}
25+
}
26+
27+
func TestHTMLJSONToMapUsesCompleteLastVariable(t *testing.T) {
28+
data := `var isngis = ''; var isngis = 'complete-sign'; data: { 'action':'downprocess','sign':isngis,'kd':1,'p':pwd }`
29+
params, err := htmlJsonToMap(data)
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
if params["sign"] != "complete-sign" {
34+
t.Fatalf("sign = %q, want %q", params["sign"], "complete-sign")
35+
}
36+
}

drivers/lanzou/util.go

Lines changed: 51 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"errors"
55
"fmt"
66
"net/http"
7+
"net/http/cookiejar"
78
"regexp"
89
"runtime"
910
"strconv"
@@ -90,7 +91,7 @@ func (d *LanZou) _post(url string, callback base.ReqCallback, resp interface{},
9091
if info == "" {
9192
info = utils.Json.Get(data, "info").ToString()
9293
}
93-
return data, fmt.Errorf(info)
94+
return data, errors.New(info)
9495
}
9596
}
9697

@@ -102,7 +103,11 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
102103
})
103104
client = upClient
104105
} else {
105-
client = base.RestyClient
106+
d.clientOnce.Do(func() {
107+
jar, _ := cookiejar.New(nil)
108+
d.client = base.RestyClient.Clone().SetCookieJar(jar)
109+
})
110+
client = d.client
106111
}
107112

108113
// acw_sc__v2 反爬挑战可能出现在任意页面/接口(分享页、iframe 页、ajaxm.php 等)。
@@ -115,10 +120,10 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
115120
"Referer": "https://pc.woozooo.com",
116121
"User-Agent": d.UserAgent,
117122
})
118-
if d.Cookie != "" {
123+
if d.Cookie != "" && strings.HasPrefix(url, strings.TrimRight(d.BaseUrl, "/")+"/") {
119124
req.SetHeader("cookie", d.Cookie)
120125
}
121-
if acwScV2 != "" {
126+
if acwScV2 != "" && client.GetClient().Jar == nil {
122127
req.SetCookie(&http.Cookie{Name: "acw_sc__v2", Value: acwScV2})
123128
}
124129
if callback != nil {
@@ -139,6 +144,13 @@ func (d *LanZou) request(url string, method string, callback base.ReqCallback, u
139144
return body, e
140145
}
141146
acwScV2 = vs
147+
if jar := client.GetClient().Jar; jar != nil {
148+
jar.SetCookies(res.Request.RawRequest.URL, []*http.Cookie{{
149+
Name: "acw_sc__v2",
150+
Value: vs,
151+
Path: "/",
152+
}})
153+
}
142154
continue
143155
}
144156
return body, nil
@@ -308,8 +320,30 @@ var findSubFolderReg = regexp.MustCompile(`(?i)(?:folderlink|mbxfolder).+href="/
308320
// 获取下载页面链接
309321
var findDownPageParamReg = regexp.MustCompile(`<iframe.*?src="(.+?)"`)
310322

311-
// 获取文件ID
312-
var findFileIDReg = regexp.MustCompile(`'/ajaxm\.php\?file=(\d+)'`)
323+
// 获取下载接口及文件 ID。旧页面使用 ajaxm.php,新版密码文件页面使用
324+
// ajaxfile.php;地址也可能使用单/双引号或完整 URL。
325+
var findAjaxPathReg = regexp.MustCompile(`(?i)(/ajax(?:m|file)\.php\?file=(\d+)\b)`)
326+
var findFileIDVarReg = regexp.MustCompile(`(?i)\b(?:f_id|fid)\s*=\s*['"]?(\d+)['"]?\s*;`)
327+
328+
func findFileID(data string) (string, bool) {
329+
if matches := findAjaxPathReg.FindStringSubmatch(data); len(matches) == 3 {
330+
return matches[2], true
331+
}
332+
if matches := findFileIDVarReg.FindStringSubmatch(data); len(matches) == 2 {
333+
return matches[1], true
334+
}
335+
return "", false
336+
}
337+
338+
func getAjaxmPath(data string) string {
339+
if matches := findAjaxPathReg.FindStringSubmatch(data); len(matches) == 3 {
340+
return matches[1]
341+
}
342+
if fileID, ok := findFileID(data); ok {
343+
return "/ajaxm.php?file=" + fileID
344+
}
345+
return "/ajaxm.php"
346+
}
313347

314348
// GET 页面并去除注释(acw_sc__v2 反爬挑战已在 request 层统一处理)
315349
func (d *LanZou) getHtml(url string, callback base.ReqCallback) (string, error) {
@@ -394,15 +428,17 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
394428
}
395429
param["p"] = pwd
396430

397-
fileIDs := findFileIDReg.FindStringSubmatch(sharePageData)
398-
var fileID string
399-
if len(fileIDs) > 1 {
400-
fileID = fileIDs[1]
401-
} else {
402-
return nil, fmt.Errorf("not find file id")
403-
}
404431
var resp FileShareInfoAndUrlResp[string]
405-
_, err = d.post(d.ShareUrl+"/ajaxm.php?file="+fileID, func(req *resty.Request) { req.SetFormData(param) }, &resp)
432+
_, err = d.post(d.ShareUrl+getAjaxmPath(sharePageData), func(req *resty.Request) {
433+
req.SetHeader("Accept", "application/json, text/javascript, */*; q=0.01")
434+
req.SetHeader("Referer", strings.TrimRight(d.ShareUrl, "/")+"/"+shareID)
435+
req.SetHeader("Origin", strings.TrimRight(d.ShareUrl, "/"))
436+
req.SetHeader("X-Requested-With", "XMLHttpRequest")
437+
req.SetHeader("Sec-Fetch-Dest", "empty")
438+
req.SetHeader("Sec-Fetch-Mode", "cors")
439+
req.SetHeader("Sec-Fetch-Site", "same-origin")
440+
req.SetFormData(param)
441+
}, &resp)
406442
if err != nil {
407443
return nil, err
408444
}
@@ -425,15 +461,8 @@ func (d *LanZou) getFilesByShareUrl(shareID, pwd string, sharePageData string) (
425461
return nil, err
426462
}
427463

428-
fileIDs := findFileIDReg.FindStringSubmatch(nextPageData)
429-
var fileID string
430-
if len(fileIDs) > 1 {
431-
fileID = fileIDs[1]
432-
} else {
433-
return nil, fmt.Errorf("not find file id")
434-
}
435464
var resp FileShareInfoAndUrlResp[int]
436-
_, err = d.post(d.ShareUrl+"/ajaxm.php?file="+fileID, func(req *resty.Request) { req.SetFormData(param) }, &resp)
465+
_, err = d.post(d.ShareUrl+getAjaxmPath(nextPageData), func(req *resty.Request) { req.SetFormData(param) }, &resp)
437466
if err != nil {
438467
return nil, err
439468
}

drivers/lanzou/util_test.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package lanzou
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/alist-org/alist/v3/drivers/base"
10+
"github.com/go-resty/resty/v2"
11+
)
12+
13+
func TestFindFileID(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
html string
17+
want string
18+
ok bool
19+
}{
20+
{name: "legacy single quoted path", html: `url: '/ajaxm.php?file=12345'`, want: "12345", ok: true},
21+
{name: "double quoted path", html: `url: "/ajaxm.php?file=23456"`, want: "23456", ok: true},
22+
{name: "absolute URL", html: `url: "https://example.lanzouv.com/ajaxm.php?file=34567"`, want: "34567", ok: true},
23+
{name: "unquoted expression", html: `fetch('/ajaxm.php?file=45678&p=1')`, want: "45678", ok: true},
24+
{name: "password file endpoint", html: `url : '/ajaxfile.php?file=215138709'`, want: "215138709", ok: true},
25+
{name: "f_id variable", html: `var f_id = '56789';`, want: "56789", ok: true},
26+
{name: "fid variable without quotes", html: `let fid=67890;`, want: "67890", ok: true},
27+
{name: "missing ID", html: `url: '/ajaxm.php?action=downprocess'`, ok: false},
28+
}
29+
30+
for _, tt := range tests {
31+
t.Run(tt.name, func(t *testing.T) {
32+
got, ok := findFileID(tt.html)
33+
if got != tt.want || ok != tt.ok {
34+
t.Fatalf("findFileID() = (%q, %v), want (%q, %v)", got, ok, tt.want, tt.ok)
35+
}
36+
})
37+
}
38+
}
39+
40+
func TestGetAjaxmPath(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
html string
44+
want string
45+
}{
46+
{name: "legacy page with file ID", html: `url: '/ajaxm.php?file=12345'`, want: "/ajaxm.php?file=12345"},
47+
{name: "password file endpoint", html: `url : '/ajaxfile.php?file=215138709'`, want: "/ajaxfile.php?file=215138709"},
48+
{name: "new page without file ID", html: `data: {'action':'downprocess','sign':sign,'ves':1}`, want: "/ajaxm.php"},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
if got := getAjaxmPath(tt.html); got != tt.want {
54+
t.Fatalf("getAjaxmPath() = %q, want %q", got, tt.want)
55+
}
56+
})
57+
}
58+
}
59+
60+
func TestGetFilesByShareURLWithoutFileID(t *testing.T) {
61+
originalClient, originalNoRedirectClient := base.RestyClient, base.NoRedirectClient
62+
base.RestyClient = resty.New()
63+
base.NoRedirectClient = resty.New().SetRedirectPolicy(resty.RedirectPolicyFunc(
64+
func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse },
65+
))
66+
t.Cleanup(func() {
67+
base.RestyClient = originalClient
68+
base.NoRedirectClient = originalNoRedirectClient
69+
})
70+
71+
var server *httptest.Server
72+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
73+
switch r.URL.Path {
74+
case "/fn":
75+
fmt.Fprint(w, `<script>var sign = 'test-sign'; data: {'action':'downprocess','sign':sign,'ves':1}</script>`)
76+
case "/ajaxm.php":
77+
if r.URL.RawQuery != "" {
78+
t.Errorf("unexpected ajaxm query: %q", r.URL.RawQuery)
79+
}
80+
w.Header().Set("Content-Type", "application/json")
81+
fmt.Fprintf(w, `{"zt":1,"dom":%q,"url":"download","inf":0}`, server.URL)
82+
case "/file/download":
83+
w.Header().Set("Location", server.URL+"/direct")
84+
w.WriteHeader(http.StatusFound)
85+
default:
86+
http.NotFound(w, r)
87+
}
88+
}))
89+
t.Cleanup(server.Close)
90+
91+
d := &LanZou{Addition: Addition{ShareUrl: server.URL, UserAgent: "test"}}
92+
sharePage := `<title>test.txt - 蓝奏云</title><iframe src="/fn"></iframe><span>大小 1 M</span>`
93+
file, err := d.getFilesByShareUrl("share-id", "", sharePage)
94+
if err != nil {
95+
t.Fatalf("getFilesByShareUrl() error = %v", err)
96+
}
97+
if file.Url != server.URL+"/direct" {
98+
t.Fatalf("file URL = %q, want %q", file.Url, server.URL+"/direct")
99+
}
100+
}
101+
102+
func TestGetPasswordFileThroughAjaxfileEndpoint(t *testing.T) {
103+
originalClient, originalNoRedirectClient := base.RestyClient, base.NoRedirectClient
104+
base.RestyClient = resty.New()
105+
base.NoRedirectClient = resty.New().SetRedirectPolicy(resty.RedirectPolicyFunc(
106+
func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse },
107+
))
108+
t.Cleanup(func() {
109+
base.RestyClient = originalClient
110+
base.NoRedirectClient = originalNoRedirectClient
111+
})
112+
113+
var server *httptest.Server
114+
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115+
switch r.URL.Path {
116+
case "/share-id":
117+
http.SetCookie(w, &http.Cookie{Name: "share_session", Value: "ok", Path: "/"})
118+
fmt.Fprint(w, `<title>protected.apk - 蓝奏云</title><div id="passwddiv"></div><script>
119+
function down_p(){
120+
var sign = 'test-sign';
121+
$.ajax({url:'/ajaxfile.php?file=215138709',data:{'action':'downprocess','sign':sign,'kd':1,'p':pwd}});
122+
}</script><span>大小 1 M</span>`)
123+
case "/ajaxfile.php":
124+
if r.URL.Query().Get("file") != "215138709" {
125+
t.Errorf("unexpected file query: %q", r.URL.RawQuery)
126+
}
127+
cookie, err := r.Cookie("share_session")
128+
if err != nil || cookie.Value != "ok" {
129+
t.Errorf("share cookie missing: %v", err)
130+
}
131+
if got := r.Header.Get("Referer"); got != server.URL+"/share-id" {
132+
t.Errorf("Referer = %q", got)
133+
}
134+
if got := r.Header.Get("Origin"); got != server.URL {
135+
t.Errorf("Origin = %q", got)
136+
}
137+
if got := r.Header.Get("X-Requested-With"); got != "XMLHttpRequest" {
138+
t.Errorf("X-Requested-With = %q", got)
139+
}
140+
if err := r.ParseForm(); err != nil {
141+
t.Fatal(err)
142+
}
143+
if got := r.Form.Get("p"); got != "1234" {
144+
t.Errorf("password = %q", got)
145+
}
146+
w.Header().Set("Content-Type", "application/json")
147+
fmt.Fprintf(w, `{"zt":1,"dom":%q,"url":"download","inf":"protected.apk"}`, server.URL)
148+
case "/file/download":
149+
w.Header().Set("Location", server.URL+"/direct")
150+
w.WriteHeader(http.StatusFound)
151+
default:
152+
http.NotFound(w, r)
153+
}
154+
}))
155+
t.Cleanup(server.Close)
156+
157+
d := &LanZou{Addition: Addition{ShareUrl: server.URL, UserAgent: "test"}}
158+
file, err := d.GetFilesByShareUrl("share-id", "1234")
159+
if err != nil {
160+
t.Fatalf("GetFilesByShareUrl() error = %v", err)
161+
}
162+
if file.Url != server.URL+"/direct" {
163+
t.Fatalf("file URL = %q, want %q", file.Url, server.URL+"/direct")
164+
}
165+
}

0 commit comments

Comments
 (0)