news 2026/5/5 0:21:28

Rocket 75 行写一个 Pastebin(顺便把类型安全与安全边界一次讲透)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Rocket 75 行写一个 Pastebin(顺便把类型安全与安全边界一次讲透)

1、目标与路由设计

最终我们有 3 个路由:

  • GET /:返回使用说明
  • POST /:接收原始 body,保存成文件,返回可访问的 URL
  • GET /<id>:根据 id 取回内容(不存在就 404)

存储策略:把每次上传保存到项目根目录的upload/目录里;文件名就是 paste 的id(一串可读的随机字符)。

目录结构大致是:

. ├── Cargo.toml ├── src │ ├── main.rs │ └── paste_id.rs └── upload

2、Cargo.toml:最小依赖

[dependencies] rocket = "0.5.1" rand = "0.8"

3、PasteId:把“合法 ID 的规则”收敛成一个类型

我们不想在每个路由里手写一堆校验逻辑,所以用一个PasteId类型集中定义策略:

  • 生成:从 base62(0-9A-Za-z)里挑字符
  • 落盘:只允许在upload/目录里构造路径
  • 校验:只接受 ASCII 字母数字(你也可以加长度限制等)

src/paste_id.rs

usestd::borrow::Cow;usestd::path::{Path,PathBuf};userand::{self,Rng};userocket::request::FromParam;#[derive(rocket::http::uri::UriDisplayPath)]pubstructPasteId<'a>(Cow<'a,str>);implPasteId<'_>{pubfnnew(size:usize)->PasteId<'static>{constBASE62:&[u8]=b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";letmutid=String::with_capacity(size);letmutrng=rand::thread_rng();for_in0..size{id.push(BASE62[rng.gen::<usize>()%62]aschar);}PasteId(Cow::Owned(id))}pubfnfile_path(&self)->PathBuf{letroot=concat!(env!("CARGO_MANIFEST_DIR"),"/","upload");Path::new(root).join(self.0.as_ref())}}/// 把不可信的 path segment 变成可信的 PasteId:/// 只允许字母数字(可按需加长度上限/下限)impl<'a>FromParam<'a>forPasteId<'a>{typeError=&'astr;fnfrom_param(param:&'astr)->Result<Self,Self::Error>{letok_chars=param.chars().all(|c|c.is_ascii_alphanumeric());(ok_chars).then(||PasteId(param.into())).ok_or(param)}}

为什么必须做 FromParam?
如果你在retrieve(id: &str)里直接拿用户输入拼路径,用户完全可以请求/_credentials.txt之类的敏感文件名(或更复杂的变种),导致你把不该暴露的文件读出来。这类问题常被归为路径相关的文件泄露/穿越风险。
PasteId+FromParam后,Rocket 会先校验<id>,不合法就根本不会进入你的 handler,从入口把攻击面切断,而且策略集中维护。

4、main.rs:三条路由,流式上传与类型安全 URI

src/main.rs

#[macro_use]externcraterocket;modpaste_id;usepaste_id::PasteId;userocket::data::{Data,ToByteUnit};userocket::http::uri::Absolute;userocket::tokio::fs::{self,File};constID_LENGTH:usize=3;// 实际生产建议从配置读取;这里只是演示constHOST:Absolute<'static>=uri!("http://localhost:8000");#[get("/")]fnindex()->&'staticstr{r#" USAGE POST / accepts raw data in the body of the request and responds with a URL of a page containing the body's content GET /<id> retrieves the content for the paste with id `<id>` "#}#[get("/<id>")]asyncfnretrieve(id:PasteId<'_>)->Option<File>{File::open(id.file_path()).await.ok()}#[post("/", data ="<paste>")]asyncfnupload(paste:Data<'_>)->std::io::Result<String>{// 确保 upload/ 存在(避免首次运行忘建目录)letupload_dir=concat!(env!("CARGO_MANIFEST_DIR"),"/","upload");fs::create_dir_all(upload_dir).await?;letid=PasteId::new(ID_LENGTH);// 128KiB 只是示例:限制请求体大小,防止被大包打爆磁盘/内存/IOpaste.open(128.kibibytes()).into_file(id.file_path()).await?;// 生成绝对 URL:类型安全、路由变更能编译期兜底Ok(uri!(HOST,retrieve(id)).to_string())}#[launch]fnrocket()->_{rocket::build().mount("/",routes![index,retrieve,upload])}

这里顺手把几个关键点都用上了:

  • Data<'_>:代表“未打开的请求体流”,适合大文件/流式写入
  • paste.open(128.kibibytes()):给上传设上限(默认你不设就可能被打穿)
  • into_file(path):把请求体流直接落盘,不用你手写循环读写
  • PasteId: FromParam:动态路径参数的类型化校验
  • PasteId: UriDisplayPath+uri!:构造 URL 时类型安全、自动编码、路由签名变更可编译期报错

5、跑起来:curl 上传与取回

项目根目录先确保有upload/(代码里也会自动建):

cargo run

另开一个终端上传:

echo"Hello, Rocket!"|curl--data-binary @- http://localhost:8000

会返回类似:

http://localhost:8000/eGs

再 GET 一下:

curlhttp://localhost:8000/eGs

你也可以直接看磁盘:

lsuploadcatupload/*

6、这套写法为什么“工程上更靠谱”

1)安全策略集中化
ID 的合法性只在PasteId::from_param定义一次,任何用到PasteId的路由都自动继承这套策略,后续加DELETE /<id>PUT /<id>也不容易漏。

2)类型安全的 URL 生成
uri!(HOST, retrieve(id))会检查路由参数匹配与类型转换;你改了路由签名,编译器会提醒所有构造 URL 的地方一起改。

3)流式落盘 + 明确限制
Data的模式天然适合大 body,配合上限避免资源型攻击或误操作(比如误传超大文件)。

7、可以继续增强的方向(很适合当练手清单)

  • 更严格的PasteId校验:长度范围、黑名单文件名、甚至检查文件是否存在
  • 返回不同状态码:比如上传达到限制时返回 206 Partial Content,否则 201 Created
  • retrieve/upload返回text/plain(用content::RawText或自定义 Responder)
  • 删除与权限:上传返回一个 key,DELETE /<id>必须带正确 key
  • 支持PUT /<id>覆盖内容(同样要 key)
  • 新增GET /<id>/<lang>:做语法高亮(lang也用FromParam校验)
  • 用 Rocket 的 local client 写单元/集成测试
  • 增加定时清理:启动前/启动后起一个任务,清理过期 paste(注意配合优雅停机)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/28 9:24:35

对比全职与兼职成本,灵活性,给出纯兼职,更省钱安案。

1️⃣ 实际应用场景描述 & 痛点引入在智能制造与数字化工厂领域&#xff0c;企业常面临用人模式的选择&#xff1a;- 全职员工&#xff1a;稳定性高&#xff0c;响应快&#xff0c;但人力成本高&#xff08;社保、福利、办公空间等&#xff09;。- 兼职/自由职业者&#xf…

作者头像 李华
网站建设 2026/5/2 9:51:38

MySQL崩溃恢复神器:innodb_force_recovery 参数详解,DBA 必备!

在 MySQL 的日常运维中&#xff0c;最让人“心跳加速”的场景之一&#xff0c;莫过于数据库突然无法启动&#xff0c;错误日志里赫然写着&#xff1a; InnoDB: Database was not shut down normally!InnoDB: Starting crash recovery....InnoDB: Assertion failure in thread .…

作者头像 李华
网站建设 2026/5/3 10:28:45

自动搬运机械手设计

自动搬运机械手设计与实现 第一章 设计背景与核心目标 传统工业物料搬运多依赖人工或简易机械&#xff0c;存在作业精度低&#xff08;定位误差≥5mm&#xff09;、搬运效率低、劳动强度大、易发生物料磕碰与人员安全事故等问题&#xff0c;难以满足现代化生产线“高精度、高效…

作者头像 李华
网站建设 2026/5/4 23:19:19

JAVA网页分片上传大文件有哪些步骤?

《一个Java老码农的20G文件夹上传历险记》 大家好&#xff0c;我是老王&#xff0c;一个在西安写了15年Java的老程序员。最近接了个外包项目&#xff0c;需求简单概括就是&#xff1a;“用IE9上传20G文件夹&#xff0c;预算100块还要724小时支持”——这感觉就像是让我用自行车…

作者头像 李华
网站建设 2026/5/1 20:10:28

分块上传大文件在JAVA网页中如何实现?

汽车设计图纸大文件上传&#xff08;WEB 集成&#xff09;方案 一、项目背景 在汽车制造行业&#xff0c;汽车设计图纸是核心资产&#xff0c;其文件体积通常较大&#xff0c;动辄达到 100G 左右。客户提出需求&#xff0c;希望能在我们开发的 WEB 系统中实现这类大文件的安全…

作者头像 李华