剪藏#Deno#Koofr#WebDAV#代理#路径重写#轻量级中继

剪藏:用 Deno 搭建 Koofr WebDAV 中继加速服务

2026 年 4 月 22 日1 分钟
分享Twitter / XTelegram微博
剪藏:用 Deno 搭建 Koofr WebDAV 中继加速服务 封面

背景

Koofr 自带 WebDAV 能力,适合用来挂载网盘、同步文件或接入支持 WebDAV 的客户端。不过在部分网络环境下,客户端直连 https://app.koofr.net/dav/Koofr 时可能会遇到连接慢、目录加载卡顿、文件同步不稳定等问题。

一种简单的解决思路是:在客户端与 Koofr 之间增加一个轻量级中继层。客户端先访问中继服务,再由中继服务转发请求到 Koofr,从而利用中继节点的网络线路改善访问体验。

本文介绍如何使用 Deno Deploy / Deno Playground 搭建一个 Koofr WebDAV 中继服务,也会补充在 VPS 上自建部署的方式。


方案原理

整体链路如下:

TEXT

WebDAV 客户端



Deno 中继服务



Koofr WebDAV 服务

客户端不再直接访问:

TEXT

https://app.koofr.net/dav/Koofr

而是访问:

TEXT

https://your-proxy.deno.dev/koofr/dav/Koofr

中继服务会把 /koofr 前缀后的请求转发给 https://app.koofr.net

为什么需要特殊处理 PROPFIND

WebDAV 客户端在列出目录时,通常会发送 PROPFIND 请求。Koofr 返回的 XML 响应中会包含文件或目录的 href 路径,例如:

XML

<d:href>/dav/Koofr/example.txt</d:href>

但客户端现在访问的是代理地址:

TEXT

/koofr/dav/Koofr/example.txt

如果不重写这些路径,客户端后续访问文件时仍可能请求原始路径,导致路径错误。因此,中继服务需要在 PROPFIND 响应中把 Koofr 返回的路径改写为带 /koofr 前缀的代理路径。


完整代码

下面代码可以直接部署到 Deno Deploy,也可以在本地或 VPS 上运行。

TS

import { serve } from "https://deno.land/std@0.224.0/http/server.ts";



const pathMappings: Record<string, string> = {

  "/koofr": "https://app.koofr.net",



  // 可选:如果你还想复用这个中继代理其他 API,可以继续保留这些映射。

  "/anthropic": "https://api.anthropic.com",

  "/gemini": "https://generativelanguage.googleapis.com",

  "/openai": "https://api.openai.com",

  "/openrouter": "https://openrouter.ai/api",

  "/xai": "https://api.x.ai",

  "/telegram": "https://api.telegram.org",

  "/discord": "https://discord.com/api",

  "/groq": "https://api.groq.com/openai",

  "/cohere": "https://api.cohere.ai",

  "/huggingface": "https://api-inference.huggingface.co",

  "/together": "https://api.together.xyz",

  "/novita": "https://api.novita.ai",

  "/portkey": "https://api.portkey.ai",

  "/fireworks": "https://api.fireworks.ai/inference",

};



const port = Number(Deno.env.get("PORT") || "8000");



function secureHeaders(contentType?: string): Headers {

  const headers = new Headers();



  if (contentType) {

    headers.set("Content-Type", contentType);

  }



  headers.set("X-Content-Type-Options", "nosniff");

  headers.set("X-Frame-Options", "DENY");

  headers.set("Referrer-Policy", "no-referrer");



  return headers;

}



function removeHopByHopHeaders(headers: Headers): void {

  [

    "Connection",

    "Keep-Alive",

    "Proxy-Authenticate",

    "Proxy-Authorization",

    "TE",

    "Trailer",

    "Transfer-Encoding",

    "Upgrade",

    "Forwarded",

    "Via",

    "X-Forwarded-For",

    "X-Real-IP",

  ].forEach((name) => headers.delete(name));

}



function findMapping(pathname: string): {

  prefix: string;

  targetBaseUrl: string;

} | null {

  for (const [prefix, targetBaseUrl] of Object.entries(pathMappings)) {

    if (pathname === prefix || pathname.startsWith(`${prefix}/`)) {

      return { prefix, targetBaseUrl };

    }

  }



  return null;

}



function buildTargetUrl(

  incomingUrl: URL,

  matchedPrefix: string,

  targetBaseUrlString: string,

): {

  targetUrl: URL;

  suffixPath: string;

} {

  const targetBaseUrl = new URL(targetBaseUrlString);

  const suffixPath = incomingUrl.pathname.slice(matchedPrefix.length) || "/";

  const basePath = targetBaseUrl.pathname.replace(/\/$/, "");



  const targetUrl = new URL(targetBaseUrl.origin);

  targetUrl.pathname = `${basePath}${suffixPath}`;

  targetUrl.search = incomingUrl.search;



  return { targetUrl, suffixPath };

}



function addSecurityHeaders(headers: Headers): Headers {

  headers.set("X-Content-Type-Options", "nosniff");

  headers.set("X-Frame-Options", "DENY");

  headers.set("Referrer-Policy", "no-referrer");

  return headers;

}



serve(async (req: Request) => {

  const incomingUrl = new URL(req.url);

  const incomingPathname = incomingUrl.pathname;



  if (incomingPathname === "/" || incomingPathname === "/index.html") {

    return new Response("Not Found", {

      status: 404,

      headers: secureHeaders("text/plain; charset=utf-8"),

    });

  }



  if (incomingPathname === "/robots.txt") {

    return new Response("User-agent: *
Disallow: /", {

      status: 200,

      headers: secureHeaders("text/plain; charset=utf-8"),

    });

  }



  const mapping = findMapping(incomingPathname);



  if (!mapping) {

    console.warn(`[${new Date().toISOString()}] 未找到路径映射: ${incomingPathname}`);



    return new Response("未找到:此路径没有代理映射。", {

      status: 404,

      headers: secureHeaders("text/plain; charset=utf-8"),

    });

  }



  const { prefix: matchedPrefix, targetBaseUrl } = mapping;

  const { targetUrl, suffixPath } = buildTargetUrl(

    incomingUrl,

    matchedPrefix,

    targetBaseUrl,

  );



  const requestHeaders = new Headers(req.headers);

  removeHopByHopHeaders(requestHeaders);



  const proxyReq = new Request(targetUrl.toString(), {

    method: req.method,

    headers: requestHeaders,

    body: ["GET", "HEAD"].includes(req.method) ? undefined : req.body,

    redirect: "manual",

  });



  try {

    const proxyRes = await fetch(proxyReq);

    const responseHeaders = new Headers(proxyRes.headers);

    removeHopByHopHeaders(responseHeaders);

    addSecurityHeaders(responseHeaders);



    // WebDAV 目录列表需要改写 XML 响应中的 href 路径。

    if (matchedPrefix === "/koofr" && req.method === "PROPFIND") {

      const contentType = responseHeaders.get("Content-Type") || "";



      if (

        proxyRes.ok &&

        (contentType.includes("application/xml") || contentType.includes("text/xml"))

      ) {

        const originalBody = await proxyRes.text();



        let rewrittenBody = originalBody;



        if (suffixPath && suffixPath !== "/") {

          rewrittenBody = rewrittenBody.replaceAll(

            suffixPath,

            incomingPathname,

          );

        }



        responseHeaders.delete("Content-Length");



        return new Response(rewrittenBody, {

          status: proxyRes.status,

          statusText: proxyRes.statusText,

          headers: responseHeaders,

        });

      }

    }



    return new Response(proxyRes.body, {

      status: proxyRes.status,

      statusText: proxyRes.statusText,

      headers: responseHeaders,

    });

  } catch (error) {

    console.error(

      `[${new Date().toISOString()}] 请求上游失败:${targetUrl.toString()}`,

      error,

    );



    return new Response("网关错误:连接上游服务器失败。", {

      status: 502,

      headers: secureHeaders("text/plain; charset=utf-8"),

    });

  }

}, { port });

通过 Deno Deploy 部署

1. 创建 Playground

访问 Deno Deploy 控制台:

TEXT

https://dash.deno.com

使用 GitHub 账号登录后,点击 New Playground

2. 粘贴代码并部署

将上面的完整代码粘贴到 Playground 中,然后点击 Save & Deploy

部署完成后,Deno 会分配一个公开访问地址,格式类似:

TEXT

https://your-project-name.deno.dev

3. 配置 WebDAV 客户端

假设你的 Deno Deploy 地址是:

TEXT

https://my-proxy.deno.dev

原始 Koofr WebDAV 地址为:

TEXT

https://app.koofr.net/dav/Koofr

代理后的 WebDAV 地址就是:

TEXT

https://my-proxy.deno.dev/koofr/dav/Koofr

在 WebDAV 客户端中,将服务器地址替换为代理后的地址即可。用户名和密码仍然使用 Koofr 的 WebDAV 凭据。


在 VPS 上部署

如果你希望完全控制运行环境,也可以把同一份代码部署到自己的 VPS 上。相比 Deno Deploy,VPS 方式维护成本更高,但可控性更强,也更适合长期固定使用。

1. 安装 Deno

使用官方安装脚本:

BASH

curl -fsSL https://deno.land/install.sh | sh

将 Deno 添加到环境变量:

BASH

echo 'export DENO_INSTALL="$HOME/.deno"' >> ~/.bashrc

echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bashrc

source ~/.bashrc

验证安装:

BASH

deno --version

2. 创建代理脚本

创建 proxy.ts

BASH

nano ~/proxy.ts

将完整代码粘贴进去并保存。

3. 测试运行

BASH

deno run --allow-net --allow-env ~/proxy.ts

默认监听端口是 8000。如果要指定端口,可以这样运行:

BASH

PORT=8000 deno run --allow-net --allow-env ~/proxy.ts

4. 配置 systemd 服务

创建服务文件:

BASH

sudo nano /etc/systemd/system/deno-koofr-proxy.service

写入以下内容,并将 your-username 替换为你的实际用户名:

INI

[Unit]

Description=Deno Koofr WebDAV Proxy

After=network.target



[Service]

Type=simple

User=your-username

WorkingDirectory=/home/your-username

Environment="PORT=8000"

ExecStart=/home/your-username/.deno/bin/deno run --allow-net --allow-env /home/your-username/proxy.ts

Restart=always

RestartSec=10



[Install]

WantedBy=multi-user.target

启动服务:

BASH

sudo systemctl daemon-reload

sudo systemctl enable deno-koofr-proxy

sudo systemctl start deno-koofr-proxy

sudo systemctl status deno-koofr-proxy

使用 Nginx 反向代理

建议使用 Nginx 对外提供 HTTPS,并将请求反向代理到本地的 Deno 服务。

1. 安装 Nginx

BASH

sudo apt update

sudo apt install nginx

2. 创建站点配置

BASH

sudo nano /etc/nginx/sites-available/deno-koofr-proxy

写入以下配置,并将 your-domain.com 替换为你的域名:

NGINX

server {

    listen 80;

    server_name your-domain.com;



    location / {

        proxy_pass http://127.0.0.1:8000;



        proxy_http_version 1.1;

        proxy_set_header Host $host;

        proxy_set_header X-Real-IP $remote_addr;

        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        proxy_set_header X-Forwarded-Proto $scheme;



        # WebDAV 大文件上传和目录操作建议关闭缓冲。

        proxy_buffering off;

        request_buffering off;

        client_max_body_size 0;

    }

}

启用配置:

BASH

sudo ln -s /etc/nginx/sites-available/deno-koofr-proxy /etc/nginx/sites-enabled/

sudo nginx -t

sudo systemctl reload nginx

3. 配置 HTTPS

使用 Let's Encrypt 申请免费证书:

BASH

sudo apt install certbot python3-certbot-nginx

sudo certbot --nginx -d your-domain.com

完成后,你的 WebDAV 地址就是:

TEXT

https://your-domain.com/koofr/dav/Koofr

注意事项

  1. 凭据安全

    WebDAV 用户名和密码会经过你的中继服务转发。请只部署在自己信任的平台或自己的服务器上,不要使用不可信的第三方中继。

  2. 不要公开成通用代理

    如果只用于 Koofr,建议删除其他 API 映射,只保留 /koofr,避免服务被滥用。

  3. 流量与限制

    中继服务会消耗 Deno Deploy 或 VPS 的出入站流量。同步大文件前,建议确认平台或服务器的流量限制。

  4. 网络效果取决于中继节点线路

    中继并不一定在所有地区都能加速。实际效果取决于客户端到中继、中继到 Koofr 两段链路的质量。

  5. 客户端兼容性

    不同 WebDAV 客户端对重定向、路径、PROPFIND 响应的处理方式可能不同。如果某个客户端无法正常列目录,可以优先检查 XML 中的 href 是否已经被正确改写。


总结

通过 Deno 搭建 Koofr WebDAV 中继,可以用较低成本改善 Koofr 在部分网络环境下的访问体验。Deno Deploy 的优点是部署简单、维护成本低;VPS 的优点是可控性更强,适合对稳定性和隐私边界要求更高的场景。

如果只是个人使用,建议优先尝试 Deno Deploy;如果需要长期稳定运行,或者希望完全掌控代理环境,可以选择 VPS 部署。

相关文章