copyparty 的 on404/on403 Handler 插件机制:自定义 404、随机图片重定向与权限覆盖
copyparty 允许你用一个 Python 脚本"劫持"HTTP 404 与 403 响应,为不存在的文件或无权限的请求提供自定义处理逻辑。本文基于仓库中的 handler 插件说明 与 HTTP 客户端实现 展开,讲清 handler 插件的加载方式、main(cli, vn, rem) API、服务端调用链与返回值语义,并逐个走读 bin/handlers/ 下的官方示例,最后说明该机制的适用边界与安全注意点。
一、handler 插件解决什么问题
默认的 Web 文件服务器在文件缺失时只能返回固定的 404 页面,而 copyparty 通过 handler 插件把这两个"失败响应"变成了可编程的钩子:
on404:文件/目录不存在时执行的插件,可以返回自定义消息、重定向客户端、从同目录随机挑一张图代替,甚至自动"变出"文件让 404 彻底消失;on403:用户无访问权限时执行的插件,最典型的用途是按客户端 IP 等条件放行访问。
这两个配置项在 配置解析表 中被注册为 volume 级(卷级)flag:
"handlers\n(better explained in --help-handlers)": {
"on404=PY": "handle 404s by executing PY file",
"on403=PY": "handle 403s by executing PY file",
}
即它们以 on404=PY文件路径 / on403=PY文件路径 的形式配置,值为任意一个定义了 main() 的 Python 文件。
二、加载方式:全局或单卷
按照 插件说明文档,加载方式有两种:
-
全局加载:启动参数中直接指定,例如让所有卷的 404 都交给
sorry.py处理:copyparty --on404 ~/dev/copyparty/bin/handlers/sorry.py /somedir -
针对特定卷加载:在卷名前加 flag 前缀,语法为
:flag1,flag2=...,例如只对某个卷启用:copyparty :c,on404=~/handlers/sorry.py /somedir这里
c表示该卷可写,on404=~/handlers/sorry.py表示 404 由该插件接管。卷级 flag 会记录在 VFS 节点的flags字典中,服务端正是通过vn.flags["on404"]/vn.flags["on403"]判断是否有插件可用。
三、插件 API:main(cli, vn, rem)
每个 handler 插件必须定义 main(),接收 3 个参数(原文档 api 一节):
| 参数 | 含义 |
|---|---|
cli |
copyparty/httpcli.py 的连接实例,可访问 cli.ip(客户端 IP)、cli.vpath(原始请求路径)、cli.reply()(直接写响应)、cli.redirect()、cli.can_read 等 |
vn |
与请求 URL 重叠的 VFS 节点(volume node),可通过 vn.vpath 得到卷挂载路径、vn.canonical(rem) 把剩余路径解析为真实文件系统路径 |
rem |
位于该 VFS 挂载点之下的 URL 剩余部分 |
三者满足 vn.vpath + rem == cli.vpath == 原始请求路径 的对应关系。注意 redirect.py 中特别强调:vn.vpath 与 cli.vpath 都不带前导 /,拼接或打印时需要自行补上。
四、服务端调用链:on40x 如何执行插件
4.1 统一入口 on40x
httpcli.py 中的 on40x() 是 404 与 403 钩子的共同实现:
def on40x(self, mods: list[str], vn: VFS, rem: str) -> str:
for mpath in mods:
try:
mod = loadpy(mpath, self.args.hot_handlers)
except Exception as ex:
self.log("import failed: {!r}".format(ex))
continue
ret = mod.main(self, vn, rem)
if ret:
return ret.lower()
return "" # unhandled / fallthrough
从源码结构看有几个关键行为:
- 插件按列表顺序依次尝试,第一个返回非空值的插件生效,其余插件被跳过;
- 插件文件导入失败(语法错误、路径不存在等)只会记一条
import failed日志并continue,不会让服务崩溃; - 返回值会被
lower()归一化后交给调用方判断; - 所有插件都没处理时返回空串
"",表示"unhandled / fallthrough",走默认响应路径。
4.2 插件文件的运行时加载 loadpy
插件不是随服务启动静态导入的,而是由 util.py 中的 loadpy() 在请求触发时动态加载:
def loadpy(ap: str, hot: bool) -> Any:
ap = os.path.expanduser(expand_osenv_c(ap))
mdir, mfile = os.path.split(absreal(ap))
mname = mfile.rsplit(".", 1)[0]
sys.path.insert(0, mdir)
...
它支持 ~ 展开与环境变量展开,并把插件所在目录临时插入 sys.path,因此插件可以相对导入同目录的其他模块。第二个参数来自 self.args.hot_handlers——从参数命名可以推断,开启该选项后插件支持热重载,便于调试时改完即生效。
4.3 404 钩子触发点
GET 处理流程中,当对真实路径 bos.stat(abspath) 失败(文件不存在)时:
except:
if "on404" not in vn.flags:
return self.tx_404(not self.can_read)
ret = self.on40x(vn.flags["on404"], vn, rem)
if ret == "true":
return True
elif ret == "false":
return False
elif ret == "retry":
...
也就是说:卷上没配 on404 就原样返回 404;配了就交给插件,插件返回 "true" 表示"请求已处理完毕"。
4.4 403 钩子触发点与 "allow" 权限覆盖
403 分支位于 权限检查处:当用户对该资源 can_read、can_write、can_get 全部为假,且卷配置了 on403 时触发:
if not self.can_read and not self.can_write and not self.can_get:
t = "@%s has no access to %r"
if self.vn.realpath and "on403" in self.vn.flags:
t += " (on403)"
...
ret = self.on40x(self.vn.flags["on403"], self.vn, self.rem)
if ret == "true":
return True
elif ret == "false":
return False
elif ret == "home":
self.uparam["h"] = ""
elif ret == "allow":
self.log("plugin override; access permitted")
self.can_read = self.can_write = self.can_move = True
self.can_delete = self.can_get = self.can_upget = True
self.can_admin = True
else:
return self.tx_404(True)
从这段代码可以确认插件返回值的完整语义(针对 403 场景):
| 返回值 | 效果 |
|---|---|
"true" |
请求视为已处理,停止后续处理 |
"false" |
请求被拒绝,按访问失败处理 |
"home" |
把用户重定向/重置到首页上下文 |
"allow" |
权限覆盖:把 can_read、can_write、can_move、can_delete、can_get、can_upget、can_admin 全部置为 True,即插件放行后用户获得该卷的完整管理权限 |
| 空串/其他 | 按默认 404 处理 |
这意味着 403 插件返回 "allow" 是极强的动作——它不是一般意义的"允许只读",而是授予全部权限,编写插件时需要明确意识到这一点。
五、官方示例插件走读
bin/handlers/ 目录提供了 8 个示例,以下选取 4 个核心示例完整讲解。
5.1 sorry.py —— 自定义 404 消息
sorry.py 全文只有几行,是理解 API 的最小样板:
def main(cli, vn, rem):
msg = f"sorry {cli.ip} but {cli.vpath} doesn't exist"
return str(cli.reply(msg.encode("utf-8"), 404, "text/plain"))
它直接用 cli.reply(body, status, content_type) 写出一个自定义的 404 纯文本响应,并把客户端 IP 写进消息体。返回 str(...) 为真值,服务端将其视为已处理。
5.2 redirect.py —— 301/302 重定向或"页面已迁移"错误页
redirect.py 封装了三种重定向方式:
def send_http_302_temporary_redirect(cli, new_path):
# new_path 可以是:
# - "http://a.com/" 跳到其他网站
# - "/foo/bar" 跳到本服务器(注意必须带前导 '/')
cli.reply(b"redirecting...", 302, headers={"Location": new_path})
def send_http_301_permanent_redirect(cli, new_path):
cli.reply(b"redirecting...", 301, headers={"Location": new_path})
def send_errorpage_with_redirect_link(cli, new_path):
# 返回一个带跳转链接的错误页,new_path 不带前导 '/'
cli.redirect(new_path, click=False, msg="this page has moved")
def main(cli, vn, rem):
print(f"this client just hit a 404: {cli.ip}")
print(f"they were accessing this volume: /{vn.vpath}")
print(f"and the original request-path (straight from the URL) was /{cli.vpath}")
print(f"...which resolves to the following filesystem path: {vn.canonical(rem)}")
new_path = "/foo/bar/"
# 三选一:
send_http_302_temporary_redirect(cli, new_path)
# send_http_301_permanent_redirect(cli, new_path)
# send_errorpage_with_redirect_link(cli, new_path)
return "true"
示例里同时演示了 API 的"调试面":vn.canonical(rem) 可把相对挂载点的剩余路径还原为真实文件系统绝对路径,print 输出会进入 copyparty 的服务日志,便于排查是谁、以什么 URL 打到了 404。
5.3 randpic.py —— 404 时随机返回同目录图片
randpic.py 实现"访问 /foo/bar/randpic.jpg 时,若文件不存在则 302 到该目录下随机一张同名后缀的图片",常用于相册随机图接口:
def main(cli, vn, rem):
req_fn = rem.split("/")[-1]
if not cli.can_read or not req_fn.startswith("randpic"):
return
req_abspath = vn.canonical(rem)
req_ap_dir = os.path.dirname(req_abspath)
files_in_dir = os.listdir(req_ap_dir)
if "." in req_fn:
file_ext = "." + req_fn.split(".")[-1]
files_in_dir = [x for x in files_in_dir if x.lower().endswith(file_ext)]
if not files_in_dir:
return
selected_file = random.choice(files_in_dir)
req_url = "/".join([vn.vpath, rem]).strip("/")
req_dir = req_url.rsplit("/", 1)[0]
new_url = "/".join([req_dir, quote(selected_file)]).strip("/")
cli.reply(b"redirecting...", 302, headers={"Location": "/" + new_url})
return "true"
注意它体现了插件的"条件弃权"写法:请求文件名不以 randpic 开头、目录里没有匹配文件时直接 return(返回 None),on40x 会继续尝试下一个插件或回落到默认 404;文件名含扩展名时用 urllib.parse.quote 对选中的文件做 URL 编码,处理空格等特殊字符。
5.4 ip-ok.py —— 按 IP 放行 403
ip-ok.py 全文:
def main(cli, vn, rem):
if cli.ip == "1.2.3.4":
return "allow"
对应 4.4 节的 "allow" 分支:客户端 IP 匹配时,服务端把该请求的全部权限标志置真。这是一个极简的白名单模板,实际使用时应替换为 CIDR 匹配、内网段判断等逻辑,并牢记放行即全权限覆盖的语义。
5.5 其余示例概览
插件说明文档 还列出了 4 个示例,可按需打开对应文件阅读:
- never404.py:按需自动创建空文件,"保证 404 从此不存在";
- caching-proxy.py:把 copyparty 改造成简易缓存代理(squid/varnish 风格);
- 404-to-fail2ban.py:把 404 请求落成 fail2ban 可解析的日志,用于封禁恶意扫描;
- nooo.py:回复无尽的 "noooooooooooooo",属于演示性质的玩具插件。
六、限制与注意事项
- on403 只覆盖"基础 HTTP 访问"权限检查。原文档 notes 一节明确指出:on403 只在普通 HTTP 请求的 trivial 场景生效(作者自述实现 on404 时顺手加的),对 SFTP/WebDAV/其他协议通道的鉴权不生效。这与源码一致——403 钩子位于 HTTP 请求的权限判断路径上,且要求
vn.realpath为真(真实文件卷)。 - 插件返回
"allow"等于授予全量权限(含can_admin),编写 403 插件时必须让匹配条件足够严格。 - 插件在运行时被导入执行:
loadpy会把插件目录插入sys.path并执行其模块级代码,插件文件相当于不受沙箱保护的服务器端代码,只应指向自己信任的脚本路径。 - 多个插件可叠加:
on40x按配置列表顺序执行,取第一个非空返回;条件不满足时返回None即可把机会让给后续插件或默认行为。 - 路径约定:
vn.vpath/cli.vpath无前导/,Location头与vn.canonical()输出的用法如 5.2 节所示,照抄示例可减少拼接错误。
七、延伸阅读
- handler 插件总览:usage / api / examples 原始说明
- on40x 统一入口、403 钩子、404 钩子:服务端调用链
- loadpy 动态加载:插件导入与热重载参数
- 卷级 flag 注册表:
on404=PY/on403=PY配置项定义 - 同目录 hooks 插件说明:与 handler 互补的事件钩子体系
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00