libwebsockets Deaddrop 插件详解:基于 lws 构建安全的多用户文件上传与共享服务
libwebsockets Deaddrop 插件详解:基于 lws 构建安全的多用户文件上传与共享服务
Deaddrop 是 libwebsockets(lws)官方自带的一个协议插件(plugin),它把"文件上传 + 文件列表实时同步 + 按用户授权删除"的完整能力封装为一个可复用的 WebSocket 协议 lws-deaddrop,配合 lws 自身的 HTTP 挂载点(mount)体系,即可在几乎不写业务代码的前提下,搭建一个受 Basic Auth 保护、支持 TLS 的安全 HTML5 文件上传与共享站点。本文将围绕本仓库中 Deaddrop 插件的官方说明,结合插件源码与配套示例,完整讲解它的构建方式、pvo 配置项、挂载点规划、C 语言集成方法与 lwsws JSON 配置方案,并深入到上传落盘、目录扫描、WebSocket 推送与删除授权等底层实现细节,帮助读者在 TEN-framework 所依赖的这一 lws 第三方源码树中快速定位、理解并落地使用该插件。
插件定位与工作原理
Deaddrop 插件的完整实现在 protocol_lws_deaddrop.c,它对外暴露一个名为 lws-deaddrop 的协议,并被打包成标准的 lws 协议插件(通过 lws_plugin_protocol_t 注册,插件名 deaddrop)。其核心工作方式可以概括为三条链路:
- HTTP POST 上传链路:插件将上传 URL 注册为一个回调型挂载点(
LWSMPRO_CALLBACK),通过 lws 的lws_spa(Secure Parser for Arguments,即 POST 参数解析器)解析multipart/form-data请求,把文件内容流式写入上传目录; - WebSocket 文件列表链路:浏览器打开页面后建立
lws-deaddropWebSocket 连接,服务端将上传目录扫描结果以 JSON 文本帧推送给所有在线客户端,实现列表的实时、多端同步刷新; - 删除链路:客户端通过 WebSocket 发送
{"del":"<用户>/<文件名>"}删除指令,服务端校验发起者身份与文件归属后执行unlink,随后重新扫描目录并广播更新。
从源码结构看,插件使用 struct vhd_deaddrop 保存每个 vhost 的共享状态(上传目录、最大文件大小、按 mtime 排序的文件链表、当前文件列表版本号),用 struct pss_deaddrop 保存每个连接(wsi)的会话状态(SPA 解析器、正在写入的文件描述符、累计字节数、响应码等),二者通过 lws_protocol_vh_priv_zalloc 与 per-session user data 机制关联,这也是理解后续所有配置与行为的基础。
构建插件
Deaddrop 随 lws 一起构建。在 lws 的 CMake 构建体系中,插件开关是 LWS_WITH_PLUGINS(其定义位于 CMakeLists.txt,注释明确说明该选项"支持协议与扩展的插件,并隐含启用 LWS_WITH_PLUGINS_API")。按官方说明,配置与构建方式为:
cmake .. -DLWS_WITH_PLUGINS=1
make
构建完成后,lws-deaddrop 协议即可作为插件被加载;在 lwsws(lws 的 WebSocket 服务器守护进程)场景下,插件还会被自动扫描进插件目录。若希望插件协议直接内联进 lws 库本身,还可以使用 LWS_WITH_PLUGINS_BUILTIN 选项(见 CMakeLists-implied-options.txt 中的说明,该模式下插件以自包含方式编译)。
可配置项(pvo)
插件通过 lws 的"per-vhost options"(pvo)机制接收配置。官方文档给出了三个配置项,源码中的解析逻辑(LWS_CALLBACK_PROTOCOL_INIT 分支)则进一步揭示了它们的默认值与必需性:
| pvo 名称 | 含义 | 源码中的处理细节 |
|---|---|---|
upload-dir |
上传文件落盘的可写目录 | 唯一必需项。源码中若 lws_pvo_get_str 取不到该值,会打印 requires 'upload-dir' pvo 警告并放弃初始化(protocol_lws_deaddrop.c);建议使用绝对路径 |
max-size |
单个文件的最大字节数 | 可选项,源码默认值为 20 * 1024 * 1024(20 MiB,见 protocol_lws_deaddrop.c),配置后通过 atoll 解析 |
basic-auth |
Basic Auth 凭据文件路径,用于同时保护 wss 连接 | 官方示例中该文件内容为每行一条 user:password 形式的凭据(如示例里的 user1:password、user2:password) |
配置生效的路径是:lws_pvo_get_str(in, ...) 从 pvo 链表读取配置写入 vhd_deaddrop,随后 scan_upload_dir() 立即对上传目录做首次扫描。值得注意的一个行为是:即便未配置 basic-auth,插件仍能运行——Basic Auth 的强制要求主要靠"挂载点级"的 basic-auth 配置来落实,因此官方明确建议把插件的 wss 协议和所有相关挂载点都纳入同一套 Basic Auth 保护之下。
必需的挂载点(mount)规划
要让 Deaddrop 真正可用,仅配置协议还不够——官方文档强调,所有挂载点与 ws 协议都应被 Basic Auth 保护,而 Basic Auth 在网络上传输凭据又需要 TLS 防止嗅探,三者缺一不可。官方给出的挂载点规划共四步,这里完整保留并逐条展开:
- 为
lws-deaddrop协议设置basic-authpvo,使 WebSocket 连接本身也必须通过凭据校验(协议层的 pvo 配置见上文表格); - 用同一份 Basic Auth 凭据文件保护你的静态文件挂载点,该挂载点用于对外提供
index.html、CSS、JS 等前端资源; - 添加一个指向
lws-deaddrop协议的回调挂载点,URL 为upload:若你的 Deaddrop 页面 URL 是/tools/share,则上传入口位于/tools/share/upload。该挂载点同样必须由 Basic Auth 保护; - 添加一个静态文件服务挂载点,URL 为
get:接上例即/tools/share/get,其origin(磁盘路径)必须与你在 pvo 中选定的upload-dir保持一致——这是上传文件对外共享下载的出口。由于该目录中可能出现任意类型的上传文件,此挂载点需要补充额外的 mimetype 映射,否则 lws 出于安全考虑(不识别 mimetype 就不予服务)将拒绝下发文件。
用一句话概括:页面(静态挂载点)负责展示,upload 回调挂载点负责收文件,get 静态挂载点负责发文件,三者同属一个 URL 树并由同一份 Basic Auth 文件守护。
使用 C 直接集成插件
官方文档指向的配套示例位于本仓库的 minimal-http-server-deaddrop 示例目录,它演示了不经 lwsws、直接在 C 代码中"静态内联"使用该插件的方法。示例的集成要点如下:
1. 内联插件源码并注册协议
示例在编译期直接 #include 插件源文件并定义 LWS_PLUGIN_STATIC,随后把 LWS_PLUGIN_PROTOCOL_DEADDROP 宏展开进协议表:
#define LWS_PLUGIN_STATIC
#include "../plugins/deaddrop/protocol_lws_deaddrop.c"
static struct lws_protocols protocols[] = {
LWS_PLUGIN_PROTOCOL_DEADDROP,
LWS_PROTOCOL_LIST_TERM
};
2. 通过 pvo 链表传入配置
lws 的 pvo 是一个链式结构,每个节点携带 name/value,通过 child 与 next 指针串成树。示例中为 lws-deaddrop 协议依次挂载了三个配置项(见 minimal-http-server-deaddrop.c):
static struct lws_protocol_vhost_options pvo3 = {
NULL, NULL, "basic-auth", "./ba-passwords"
}, pvo2 = {
&pvo3, NULL, "max-size", "10000000"
}, pvo1 = {
&pvo2, NULL, "upload-dir", "./uploads"
}, pvo = {
NULL, &pvo1, "lws-deaddrop", ""
};
即:上传目录 ./uploads、单文件上限 10,000,000 字节、Basic Auth 凭据文件 ./ba-passwords。
3. 用挂载点串起 URL 树
三个挂载点通过 mount_next 指针构成链表:/ 从 ./mount-origin 目录提供页面(默认文档 index.html),/get 把 ./uploads 目录作为动态文件源(LWSMPRO_FILE),/upload 则把请求转交给 lws-deaddrop 回调协议(LWSMPRO_CALLBACK)。三者都设置了 .basic_auth_login_file = "./ba-passwords"。/get 挂载点还额外附加了 extra_mimetypes 链表,示例代码中补充了 .tar.gz、.pdf、.zip 三种映射。
4. 运行与验证
示例监听 7681 端口并强制使用 TLS(自签证书 localhost-100y.cert / localhost-100y.key)。运行输出中可以直接看到插件初始化日志:
USER: LWS minimal http server deaddrop | visit https://localhost:7681
NOTICE: Creating Vhost 'default' port 7681, 1 protocols, IPv6 off
NOTICE: Using SSL mode
NOTICE: deaddrop: vh default, upload dir ./uploads, max size 10000000
浏览器访问 https://localhost:7681 并按提示输入凭据(示例默认 user1 / password,另有 user2 / password 可用于观察不同用户视角的差异)。上传文件会出现在共享列表中,并被所有打开该页面的客户端实时同步;且只有上传者本人能看到并操作自己文件的删除按钮。
使用 lwsws / lejp-conf 的 JSON 配置方式
作为插件,Deaddrop 的优势在于可以在 lwsws(使用 lejp-conf JSON 配置体系)中按 vhost 用纯 JSON 完成挂载点与 pvo 的全部配置,无需编译任何 C 代码。
挂载点 JSON 配置
官方文档给出的挂载点片段如下(假设服务部署在 /tools/share 路径下,凭据文件为 /var/www/ba):
{
"mountpoint": "/tools/share",
"origin": "file:///var/www/deaddrop",
"default": "index.html",
"basic-auth": "/var/www/ba"
}, {
"mountpoint": "/tools/share/upload",
"origin": "callback://lws-deaddrop",
"basic-auth": "/var/www/ba"
}, {
"mountpoint": "/tools/share/get",
"origin": "file:///var/cache/deaddrop-uploads",
"basic-auth": "/var/www/ba",
"extra-mimetypes": {
".bin": "application/octet-stream",
".ttf": "application/x-font-truetype",
".otf": "application/font-sfnt",
".zip": "application/zip",
".webm": "video/webm",
".romfs": "application/octet-stream",
".pdf": "application/pdf",
".odt": "application/vnd.oasis.opendocument.text",
".tgz": "application/x-gzip",
".tar.gz": "application/x-gzip"
}
}
逐项解读:
/tools/share:静态页面挂载点,从file:///var/www/deaddrop目录提供index.html、CSS、JS;/tools/share/upload:回调挂载点,origin为callback://lws-deaddrop,把上传 POST 请求转给插件协议处理;/tools/share/get:文件下载挂载点,origin指向file:///var/cache/deaddrop-uploads——注意该路径必须与后面 ws-protocols 中的upload-dir一致;extra-mimetypes对象为下载目录补充常见二进制/文档类型的 MIME 映射,否则 lws 会拒绝下发未知类型的文件;- 三个挂载点全部通过
basic-auth字段指向/var/www/ba凭据文件。
ws-protocols 的 pvo JSON 配置
同一 vhost 的 ws-protocols 中完成协议启用与 pvo 配置,使 wss 连接同样依赖有效凭据:
"ws-protocols": [{
"lws-deaddrop": {
"status": "ok",
"upload-dir": "/var/cache/deaddrop-uploads",
"max-size": "52428800",
"basic-auth": "/var/www/ba"
}
}],
即:启用 lws-deaddrop 协议(status: "ok"),上传目录为 /var/cache/deaddrop-uploads,单文件上限 52,428,800 字节(50 MiB),WebSocket 握手也必须通过 /var/www/ba 的 Basic Auth 校验。
前端交互与 WebSocket 数据协议
插件的浏览器端实现位于 assets/deaddrop.js(配套页面与样式见 assets/index.html 和 assets/deaddrop.css)。前端能力包括:
- 拖拽/点选上传:监听
dragenter/dragover/drop事件,把拖入的文件逐个通过fetch("upload/" + encodeURIComponent(name), { method: "POST", body: FormData })提交,也支持<input type="file" multiple>多选上传;多文件上传可并行进行; - 客户端预检:WebSocket 握手成功后服务端先推送
max_size,前端据此在本地就拦截超限文件(显示 "Too Large"),避免无效上传流量; - 实时列表渲染:
ws.onmessage把服务端推送的 JSON 渲染成表格,每行展示文件大小(humanize()将字节数转换为 B/KiB/MiB/GiB)、mtime 时间与下载链接(指向get/<文件名>,带download属性);yours === 1的文件才会渲染删除按钮; - 删除操作:点击删除按钮时通过 WebSocket 发送
{"del":"<文件名>"}指令; - 自动降级:页面以
https://打开时 WebSocket 自动使用wss://,否则使用ws://;连接断开时页面置灰并提示未连接。
服务端推送的 JSON 文本帧格式(来自 protocol_lws_deaddrop.c 的写可写回调)为:
{"max_size":<上限字节数>, "files": [
{"name":"<相对路径>", "size":<字节数>, "mtime":<Unix时间戳>, "yours":<0或1>}
]}
其中 yours 表示该文件是否属于当前认证用户(用于控制删除权限的展示)。文件列表按 mtime 降序排列,若推送期间目录发生变化(filelist_version 不一致),服务端会自动重新开始推送最新列表。
底层实现细节:上传、目录扫描与删除授权
从 protocol_lws_deaddrop.c 的源码中可以确认以下关键行为:
- 上传落盘流程:
LWS_CALLBACK_HTTP_BODY阶段创建lws_spa(参数名为text、send、file、upload),由file_upload_cb回调驱动文件写入。文件先以~结尾的临时文件名写入(权限0600),全部内容接收完成后rename去掉~转正;若上传过程中累计字节数超过max_size,立即关闭文件、unlink临时文件并返回HTTP 413(HTTP_STATUS_REQ_ENTITY_TOO_LARGE); - 按用户分子目录:如果连接带有 Basic Auth 身份(
WSI_TOKEN_HTTP_AUTHORIZATION),上传文件会落到<upload-dir>/<用户名>/子目录(目录权限0700),从而在共享空间中隔离不同用户的文件; - 目录扫描:
scan_upload_dir()递归扫描上传目录(最多嵌套两层子目录),跳过~结尾的临时文件,将条目存入 lwsac 内存池并按 mtime 排序;扫描结果用filelist_version版本号标记,所有在线连接都会被唤醒并重新推送列表; - 文件名净化:上传与删除路径都经过
lws_urldecode与lws_filename_purify_inplace处理,避免路径穿越与注入类文件名; - 删除授权:服务端收到
{"del":"<用户>/<文件名>"}后,先比对指令中携带的用户名与当前连接实际认证的用户名是否一致(不一致直接忽略),再对<upload-dir>/<文件名>执行unlink,随后重新扫描目录并广播。这与前端"仅yours文件显示删除按钮"的策略共同构成"只能删自己文件"的约束。
安全实践小结
结合官方建议与源码行为,部署 Deaddrop 时应遵循以下安全基线:
- 三层防线缺一不可:页面挂载点、
upload回调挂载点、get下载挂载点、lws-deaddropwss 协议,全部配置同一个basic-auth凭据文件; - 务必启用 TLS:Basic Auth 的凭据是明文传输的,只有 wss + https 才能防止嗅探;官方 C 示例与 lwsws 配置均以 TLS 为前提;
- 为下载挂载点补齐 mimetype:lws 对无法识别的文件类型默认拒绝服务,
get挂载点的extra-mimetypes需要覆盖实际会共享的文件类型; - 设置合理的
max-size:upload-dir目录必须可写,max-size按业务需要设定(未配置时默认 20 MiB),超限文件会被服务端直接丢弃。
仓库内的相关资源索引
如需进一步深入,可在本仓库中查阅:
- 插件官方文档:third_party/libwebsockets/plugins/deaddrop/README.md
- 插件核心实现:third_party/libwebsockets/plugins/deaddrop/protocol_lws_deaddrop.c
- 前端页面与脚本:assets/index.html、assets/deaddrop.js、assets/deaddrop.css
- C 语言集成示例(含完整挂载点、pvo 与 TLS 配置):minimal-http-server-deaddrop.c 及其 README
- 插件体系构建开关与插件加载机制:CMakeLists.txt、plugins/CMakeLists.txt、lws-protocols-plugins.h