Skip to content
Closed
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
6 changes: 4 additions & 2 deletions src/backend/drivers/115/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ export class Driver115 implements StorageDriver {
const item = fileToItem(file)
if (!item.is_dir && file.Pc) {
try {
const url = await this.client.getDownloadUrl(file.Pc)
item.raw_url = url
const link = await this.client.getDownloadUrl(file.Pc)
item.raw_url = link.url
// 115 的下载链接按 UA 签发,必须随链接一起交给客户端
item.raw_url_headers = link.headers
} catch (e: any) {
item.raw_url_error = e.message
}
Expand Down
16 changes: 14 additions & 2 deletions src/backend/drivers/115/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,24 @@ export class Client115 {
await this.request("/open/ufile/delete", { fid: fids, ignore_warn: 1 })
}

async getDownloadUrl(pickCode: string): Promise<string> {
/**
* 取下载链接。
*
* 随链接一并返回 User-Agent:115 的 downurl 是按请求方 UA 签发的,同族
* 115open 使用同一约定(其注释:「115 防盗链校验通过率高」,并把 UA 绑进
* 取链请求),115_share 的 link() 同样带 UA + Referer。
*
* 说明:本次是从 YunX 移植行为并与同族驱动对齐,**并未实机验证**原先的裸
* URL 是否真的会失败;改动的目的是让三个 115 驱动行为一致。
*/
async getDownloadUrl(
pickCode: string,
): Promise<{ url: string; headers: Record<string, string> }> {
const resp = await this.request<Cloud115DownResp>("/open/ufile/downurl", {
pickcode: pickCode,
})
const url = resp?.url || resp?.data?.url || ""
if (!url) throw new Error("[115] empty download url")
return url
return { url, headers: { "User-Agent": UA } }
}
}
5 changes: 4 additions & 1 deletion src/backend/drivers/123_open/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,10 @@ export class Driver123Open implements StorageDriver {
const item = this.fileToItem(file)
if (!item.is_dir) {
try {
item.raw_url = await this.client.getDownloadUrl(file.fileId)
const link = await this.client.getDownloadUrl(file.fileId)
item.raw_url = link.url
// CDN 直链需带 Referer + UA
item.raw_url_headers = link.headers
} catch (e: any) {
item.raw_url_error = e.message
}
Expand Down
33 changes: 31 additions & 2 deletions src/backend/drivers/123_open/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ import {

const API = "https://open-api.123pan.com"

/**
* 123 网盘下载请求头。
*
* 依据(参考项目 YunX,https://github.com/CYQawa/YunX):
* - Pan123Api 对解码后的 CDN 直链明确注明「下载需带 Referer: https://yun.123pan.cn/」;
* - Pan123Constants.WEB_UA 给出配套浏览器 UA。
*
* 本仓库此前只把 Referer 用在了**内部**跟随 302 那一步,拿到最终直链后直接
* 交给客户端。
*
* 说明:本次行为移植自 YunX,未实机验证缺少该头是否真的导致失败。
*/

export const PAN123_DOWNLOAD_REFERER = "https://yun.123pan.cn/"

export const PAN123_DOWNLOAD_UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"

/** 取链时可复用的下载头 */
export function pan123DownloadHeaders(): Record<string, string> {
return {
Referer: PAN123_DOWNLOAD_REFERER,
"User-Agent": PAN123_DOWNLOAD_UA,
}
}

export class Client123Open {
private addition: Driver123OpenAddition
private accessToken = ""
Expand Down Expand Up @@ -111,13 +137,16 @@ export class Client123Open {
return all
}

async getDownloadUrl(fileId: number): Promise<string> {
/** 取下载链接;CDN 直链需带 Referer + UA(见文件顶部说明) */
async getDownloadUrl(
fileId: number,
): Promise<{ url: string; headers: Record<string, string> }> {
const qs = new URLSearchParams({ fileId: String(fileId) })
const resp = await this.request<DownloadInfoData123>(`${API}/api/v1/file/download_info?${qs.toString()}`, "GET")
if (resp.code !== 0) throw new Error(`[123Open] ${resp.message}`)
const url = resp.data?.download_url || ""
if (!url) throw new Error("[123Open] empty download url")
return url
return { url, headers: pan123DownloadHeaders() }
}

async mkdir(parentId: string, name: string): Promise<void> {
Expand Down
5 changes: 4 additions & 1 deletion src/backend/drivers/123pan/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,10 @@ export class Pan123Driver implements StorageDriver {
const item = pan123FileToFileItem(file)
if (file.Type !== 1) {
try {
item.raw_url = await this.client.getDownloadLink(file)
const link = await this.client.getDownloadLink(file)
item.raw_url = link.url
// 123 的 CDN 直链要求 Referer + UA,必须随链接交给客户端
item.raw_url_headers = link.headers
if (!item.raw_url) {
item.raw_url_error =
"123 网盘未返回下载链接(DownloadUrl 为空)。常见原因:access_token/cookie 失效,或该文件已删除/被限制下载。请到管理后台更新 access_token 后重试。"
Expand Down
40 changes: 34 additions & 6 deletions src/backend/drivers/123pan/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,32 @@ function getApi(rawUrl: string): string {

// --- Client ---

/**
* 123 网盘下载请求头。
*
* 依据(参考项目 YunX,https://github.com/CYQawa/YunX):
* - Pan123Api 对解码后的 CDN 直链明确注明「下载需带 Referer: https://yun.123pan.cn/」;
* - Pan123Constants.WEB_UA 给出配套浏览器 UA。
*
* 本仓库此前只把 Referer 用在了**内部**跟随 302 那一步,拿到最终直链后直接
* 交给客户端。
*
* 说明:本次行为移植自 YunX,未实机验证缺少该头是否真的导致失败。
*/

export const PAN123_DOWNLOAD_REFERER = "https://yun.123pan.cn/"

export const PAN123_DOWNLOAD_UA =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"

/** 取链时可复用的下载头 */
export function pan123DownloadHeaders(): Record<string, string> {
return {
Referer: PAN123_DOWNLOAD_REFERER,
"User-Agent": PAN123_DOWNLOAD_UA,
}
}

export class Pan123Client {
private addition: Pan123Addition
private accessToken = ""
Expand Down Expand Up @@ -421,7 +447,9 @@ export class Pan123Client {

// ---- Download ----

public async getDownloadLink(file: Pan123File): Promise<string> {
public async getDownloadLink(
file: Pan123File,
): Promise<{ url: string; headers: Record<string, string> }> {
const body = {
driveId: 0,
etag: file.Etag,
Expand Down Expand Up @@ -459,14 +487,14 @@ export class Pan123Client {
redirect: "manual",
headers: { Referer: "https://yun.123pan.com/" },
})
let finalUrl = downloadUrl
if (res.status === 302) {
return res.headers.get("location") || downloadUrl
}
if (res.status < 300) {
finalUrl = res.headers.get("location") || downloadUrl
} else if (res.status < 300) {
const body = await res.json().catch(() => ({}))
return body.data?.redirect_url || downloadUrl
finalUrl = body.data?.redirect_url || downloadUrl
}
return downloadUrl
return { url: finalUrl, headers: pan123DownloadHeaders() }
}

// ---- File operations ----
Expand Down
5 changes: 4 additions & 1 deletion src/backend/drivers/quark_uc_tv/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ export class DriverQuarkUcTv implements StorageDriver {
const item = this.fileToItem(file)
if (!item.is_dir) {
try {
item.raw_url = await this.client.getDownloadUrl(file.fid)
const link = await this.client.getDownloadUrl(file.fid)
item.raw_url = link.url
// 夸克系校验来源,下载头必须随链接一起交给客户端
item.raw_url_headers = link.headers
} catch (e: any) {
item.raw_url_error = e.message
}
Expand Down
21 changes: 18 additions & 3 deletions src/backend/drivers/quark_uc_tv/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,17 @@ export class ClientQuarkUcTv {
return all
}

async getDownloadUrl(fid: string): Promise<string> {
/**
* 取下载/播放链接。
*
* 同族的 quark 驱动(quark/util.ts 的 getDownloadUrl)已确立此形状:链接
* 连同 Referer 与 User-Agent 一起返回。本次据此把 quark_uc_tv 对齐到同一约定。
*
* 说明:属行为移植与统一,未实机验证此前是否真的失败。
*/
async getDownloadUrl(
fid: string,
): Promise<{ url: string; headers: Record<string, string> }> {
if (this.addition.link_method === "streaming") {
const resp = await this.request<QuarkTvCommonResp>("/file", "GET", {
method: "streaming",
Expand All @@ -206,7 +216,7 @@ export class ClientQuarkUcTv {
const data = resp.data as QuarkTvStreamingData | undefined
const url = data?.video_info?.find((v) => v.url)?.url || ""
if (!url) throw new Error("[QuarkTV] no streaming link found")
return url
return { url, headers: quarkTvDownloadHeaders() }
}
const resp = await this.request<QuarkTvCommonResp>("/file", "GET", {
method: "download",
Expand All @@ -217,6 +227,11 @@ export class ClientQuarkUcTv {
})
const data = resp.data as QuarkTvDownloadData | undefined
if (!data?.download_url) throw new Error("[QuarkTV] empty download url")
return data.download_url
return { url: data.download_url, headers: quarkTvDownloadHeaders() }
}
}

/** 夸克系下载请求头:与 quark 驱动保持一致 */
function quarkTvDownloadHeaders(): Record<string, string> {
return { Referer: "https://pan.quark.cn/", "User-Agent": UA }
}
19 changes: 12 additions & 7 deletions src/backend/internal/driver/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,20 @@ test("MustProxy:只包含 Go 标了 OnlyProxy / NoLinkURL 的驱动", () => {
})

test("PreferProxy:与 Go 的 DefaultProxy() 一致", () => {
for (const driver of [
"WebDav",
"BaiduNetdisk",
"123Pan",
"123PanShare",
"123Open",
]) {
for (const driver of ["WebDav", "BaiduNetdisk", "123PanShare"]) {
assert.equal(driverPreferProxy(driver), true, `${driver} 应为 PreferProxy`)
}

// 123pan / 123open 已移出 PreferProxy:直链只需 Referer + UA
// (proxy_request.ts 判定这两头「浏览器/下载器可自供」),
// 留在表内会让 Worker 替客户端搬字节、且客户端无法多线程分片。
for (const driver of ["123Pan", "123Open"]) {
assert.equal(
driverPreferProxy(driver),
false,
`${driver} 应默认为直链(交给客户端多线程),不再走服务端代理`,
)
}
for (const driver of ["Onedrive", "GoogleDrive", "S3", "Terabox"]) {
assert.equal(
driverPreferProxy(driver),
Expand Down
18 changes: 15 additions & 3 deletions src/backend/internal/driver/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,24 @@ const DRIVER_FORCE_PROXY = new Set<string>([
"local",
])

/** 默认代理(Go: Config.DefaultProxy() = PreferProxy) */
/**
* 默认代理(Go: Config.DefaultProxy() = PreferProxy)。
*
* 表单默认值含义:这里是 true 时,新建存储的 `web_proxy` 默认勾选,
* 即由 Worker 转发字节流;客户端因此只能单线程下,Worker 也会吃满
* CPU / 子请求预算,且受平台响应体上限约束(见 proxy_request.ts)。
*
* `123pan` / `123open` 已移出本表:它们的直链只要求 Referer + User-Agent,
* 而 proxy_request.ts 的 PRIVATE_HEADER_NAMES 明确判定这两个头
* 「浏览器/下载器可以自己提供」,不构成必须服务端代理的理由。
* 保留在表内只会让 Cloudflare Workers 替客户端搬字节,白白放弃
* 客户端多线程分片的能力(参考 YunX:同一批直链可开 32~512 线程)。
*
* 需要管理员仍可用 `web_proxy` 显式打开代理,行为不受影响。
*/
const DRIVER_PREFER_PROXY = new Set<string>([
"webdav",
"baidunetdisk",
"123pan",
"123open",
"123panshare",
])

Expand Down
48 changes: 46 additions & 2 deletions src/backend/server/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1295,12 +1295,56 @@ fsRouter.post("/link", async (c) => {
}
const driver = await getDriver(resolved.storage.driver, resolved.storage)
try {
// 优先用驱动自己的 link():它返回 { url, headers },即「直链 + 精确请求头」。
// 这是多线程下载器唯一能拿到正确 UA / Referer / Cookie 的途径 ——
// 302 只能交给浏览器自带的那一套头,拿不到驱动指定的值。
//
// 注意:link() 返回的头可能含 Cookie / Authorization 等私有鉴权信息。
// 本端点已是 admin-only(见上方 isAdmin 判定),受众就是「正在配置下载
// 管理器的管理员」,因此这里原样返回是恰当的。
let linkHeaders: Record<string, string> | undefined
let linkUrl: string | undefined
let linkOk = false
const linkFn = (driver as any).link
if (typeof linkFn === "function") {
try {
const linked = await linkFn.call(
driver,
reqPath,
resolved.physical ?? "/",
)
if (linked?.url) {
linkUrl = linked.url
linkHeaders = linked.headers
linkOk = true
}
} catch {
// link() 失败不阻断:回退到 driver.get() 的 raw_url
}
}

if (linkOk) {
return c.json({
code: 200,
message: "success",
data:
linkHeaders && Object.keys(linkHeaders).length
? { url: linkUrl, headers: linkHeaders }
: { url: linkUrl },
})
}

// link() 不可用或未返回 url 时才走 driver.get(),省一次上游往返
const item = await driver.get(reqPath, resolved.physical ?? "/")
if (item && item.raw_url) {
if (item?.raw_url) {
const headers =
item.raw_url_headers && Object.keys(item.raw_url_headers).length
? item.raw_url_headers
: undefined
return c.json({
code: 200,
message: "success",
data: { url: item.raw_url },
data: headers ? { url: item.raw_url, headers } : { url: item.raw_url },
})
}
} finally {
Expand Down
Loading