Roket
Hello,world
Rocket确保Rust用的是最新版
rustup default stable
开发者一般使用nightly
rustup default nightly
创建二进制的cargo项目
cargo new hello-rocket --bin
需要添加依赖到cargo.toml
[dependencies]
rocket = "=0.5.0-rc.3"
警告:开发版本必须是git依赖的。
带有-dev标签的开发版本不会被发布。要依赖Rocket的开发版本,你需要将Cargo.toml指向Rocket的git仓库。例如,用git commit hash代替######:[dependencies] rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "######" }
src/main.rs
#[macro_use] extern crate rocket;
#[get("/")]
fn index() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
}
运行后输出
> cargo run
🔧 Configured for debug.
>> address: 127.0.0.1
>> port: 8000
>> workers: [..]
>> keep-alive: 5s
>> limits: [..]
>> tls: disabled
>> temp dir: /tmp
>> log level: normal
>> cli colors: true
🛰 Routes:
>> (index) GET /
🚀 Rocket has launched from http://127.0.0.1:8000
访问http://localhost:8000
生命周期
Rocket的主要任务是监听传入的网络请求,将请求分派给应用程序代码,并向客户端返回响应。我们把这个从请求到响应的过程称为 “生命周期”。我们把生命周期总结为以下的步骤序列:
路由
Rocket将传入的HTTP请求解析为本地结构,你的代码间接地对其进行操作。Rocket通过与你的应用程序中声明的路由属性相匹配来决定调用哪个请求处理器。
验证
Rocket根据匹配的路由中存在的类型和防护措施来验证传入的请求。如果验证失败,Rocket将请求转发到下一个匹配的路由或调用一个错误处理程序。
处理
与路由相关的请求处理程序被调用,其参数经过验证。这是一个应用程序的主要业务逻辑。处理过程通过返回一个响应来完成。
响应
返回的Response被处理。Rocket生成适当的HTTP响应并将其发送给客户端。这就完成了整个生命周期。Rocket继续监听请求,为每个传入的请求重新启动生命周期。
Routing路由
Rocket应用以路由和处理程序为中心。路由是一个组合:
一组参数,用于匹配一个传入的请求。
一个处理程序,用于处理请求并返回一个响应。
处理程序是一个简单的函数,它接受任意数量的参数并返回任意类型。
匹配的参数包括静态路径、动态路径、路径段、表单、查询字符串、请求格式指定器和正文数据。Rocket使用属性,它看起来像其他语言中的函数装饰器,使声明路由变得简单。路由的声明是通过注释一个函数,即处理程序,以及要匹配的参数集。一个完整的路由声明看起来像这样:
#[get("/world")] // <- route attribute
fn world() -> &'static str { // <- request handler
"hello, world!"
}
这宣布了世界路由将与传入的GET请求的静态路径"/world "相匹配。我们可以使用#[get],而不是#[post]或#[put]来处理其他HTTP方法,或者使用#[catch]来处理自定义错误页面。此外,在构建更有趣的应用程序时,其他路由参数可能是必要的。在本章之后的Requests一章,有关于路由和错误处理的进一步细节。
Mounting挂载安装
在Rocket可以向一个路由调度请求之前,路由需要被安装:
通过routes! 列出的路线:这里是routes![world],有多条路线:routes![a, b, c]。
这将通过build函数创建一个新的Rocket实例,并将world路由挂载到/hello基本路径上,使Rocket知道这个路由。对/hello/world的GET请求将被引导到world函数。
挂载方法,就像Rocket上的所有其他构建方法一样,可以被连锁任何次数,并且路由可以被挂载点重复使用:
rocket::build()
.mount("/hello", routes![world])
.mount("/hi", routes![world]);
通过将world挂载到/hello和/hi,对"/hello/world “和”/hi/world "的请求将被引导到world函数。
Launching启动
Rocket在启动后开始为请求提供服务,它启动了一个多线程的异步服务器,并在请求到达时将其分配给匹配的路由。
有两种机制可以启动Rocket。第一种也是最好的方法是通过#[launch]路由属性,它可以生成一个主函数,设置一个异步运行时并启动服务器。通过#[launch],我们完整的Hello, world!应用程序看起来像:
第一种:
#[macro_use] extern crate rocket;
#[get("/world")]
fn world() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/hello", routes![world])
}
运行后输出:
> cargo run
🔧 Configured for debug.
>> address: 127.0.0.1
>> port: 8000
>> workers: [..]
>> keep-alive: 5s
>> limits: [..]
>> tls: disabled
>> temp dir: /tmp
>> log level: normal
>> cli colors: true
🛰 Routes:
>> (world) GET /hello/world
🚀 Rocket has launched from http://127.0.0.1:8000
#[launch]能够自动判断输出类型
与Rocket的#[launch]属性特别的是,当返回类型被设置为_时,用#[launch]装饰的函数的返回类型会被自动推断出来。如果你愿意,你也可以明确地将返回类型设置为Rocket。更多例子在这https://github.com/SergioBenitez/Rocket/tree/v0.5-rc/examples/
第二种:
使用#[rocket::main]
#[rocket::main]
async fn main() -> Result<(), rocket::Error> {
let _rocket = rocket::build()
.mount("/hello", routes![world])
.launch()
.await?;
Ok(())
}
#[rocket::main]在需要一个由launch()返回的Future的句柄,或者需要检查launch()的返回值时,是非常有用的。例如,错误处理的例子中就检查了返回值。
Futures and Async
Rocket使用Rust Futures来实现并发性。使用Futures和async/await的异步编程允许路由处理程序执行等待量大的I/O,如文件系统和网络访问,同时仍然允许其他请求取得进展。
一般来说,你应该倾向于在Rocket应用程序中使用异步的库,而不是同步。
async出现在Rocket的几个地方:
1.路由和错误捕捉器可以是async fns。在一个async fn中,你可以从Rocket或其他库中.await Futures。
2.Rocket的一些特性,如FromData和FromRequest,有一些方法可以返回Futures。
3.Data和DataStream,传入的请求数据,以及Response和Body,传出的响应数据,都是基于tokio::io::AsyncRead而不是std::io::Read。
你可以在crates.io上找到带有async标签的async-ready库。
Rocket v0.5使用tokio运行时。如果你使用#[launch]或#[rocket::main],运行时就会为你启动,但你仍然可以通过不使用这两个属性在自定义的运行时上启动()一个Rocket实例。
Async Routes异步路由
Rocket使async/await更容易使用在路由上
use rocket::tokio::time::{sleep, Duration};
#[get("/delay/<seconds>")]
async fn delay(seconds: u64) -> String {
sleep(Duration::from_secs(seconds)).await;
format!("Waited for {} seconds", seconds)
}
首先,注意到路由函数是一个async fn。这使得在处理程序中可以使用await。 sleep是一个异步函数,所以我们必须等待它。
Multitasking多任务处理
Rust的Futures是一种合作多任务的形式。一般来说,Futures和async fn应该只对操作进行.await,而不应该阻塞。一些常见的阻塞例子包括锁定非同步的互斥,加入线程,或使用非同步库函数(包括std中的函数)来执行I/O。
如果一个Future或async fn阻塞了线程,就会出现资源使用效率低下、停顿,有时甚至是死锁。
有时对于一个库或操作来说,没有好的异步选择。如果有必要,你可以用tokio::task::spwn_blocking将同步操作转换为异步操作。
use std::io;
use rocket::tokio::task::spawn_blocking;
#[get("/blocking_task")]
async fn blocking_task() -> io::Result<Vec<u8>> {
// In a real app, use rocket::fs::NamedFile or tokio::fs::File.
let vec = spawn_blocking(|| std::fs::read("data.txt")).await
.map_err(|e| io::Error::new(io::ErrorKind::Interrupted, e))??;
Ok(vec)
}
Requests
请求守卫
在 Rocket 中,请求守卫是一种机制,用于在请求处理函数执行之前对请求进行拦截和处理。请求守卫通常用于验证和授权,以确保请求符合安全和业务规则。如果请求守卫失败,则会返回一个错误响应,而不是执行请求处理函数。
Rocket 的请求守卫可以是全局的(应用程序范围内的所有路由都将受到影响),也可以是特定路由的(仅特定路由将受到影响)。请求守卫可以使用 Rust 的 trait 来定义和实现。它们可以是同步或异步的,并可以在请求级别或会话级别上工作。
Rocket 支持多种类型的请求守卫,例如:
- Guards:验证请求是否符合安全或业务规则,如果验证失败则返回错误响应。
- Catchers:捕获未处理的异常,并返回一个适当的错误响应。
- Fairings:在请求处理函数执行之前或之后运行,用于进行一些与请求相关的操作,例如记录、修改响应头、启用跨域资源共享等。
可以使用 #[guard]
、#[catch]
、#[fairing]
属性来定义请求守卫。例如:
rustCopy code#[get("/hello/<name>")]
#[guard(MyGuard)]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
#[catch(404)]
fn not_found(req: &Request) -> String {
format!("Sorry, {} does not exist.", req.uri())
}
#[derive(Debug)]
struct MyGuard;
impl<'a, 'r> rocket::request::FromRequest<'a, 'r> for MyGuard {
type Error = ();
fn from_request(request: &'a rocket::Request<'r>) -> rocket::request::Outcome<Self, Self::Error> {
if request.headers().contains("Authorization") {
rocket::Outcome::Success(MyGuard)
} else {
rocket::Outcome::Forward(())
}
}
}
在上面的示例中,MyGuard
是一个请求守卫,它将验证请求头是否包含“Authorization”字段。如果验证通过,
它将返回一个 rocket::Outcome::Success
对象,否则将返回一个 rocket::Outcome::Forward
对象。请求处理函数 hello
使用了 #[guard(MyGuard)]
属性来指定 MyGuard
作为守卫,而 not_found
使用了 #[catch(404)]
属性来指定 404
错误时的处理函数。
除了Fairing
以外,Rocket框架还提供了以下可实现的trait:
FromForm
:用于从表单数据解析到Rust结构体。FromParam
:用于从URL参数中解析到Rust数据类型。Responder
:用于将Rust类型转换为HTTP响应。Serialize
:用于将Rust结构体/枚举/元组转换为JSON或其他格式的序列化输出。Template
:用于将Rust结构体/枚举/元组渲染为HTML或其他模板。Stream
:用于生成HTTP响应的数据流。WebSocket
:用于创建WebSocket连接的trait。Responder<'r, 'o>
:用于将Rust类型转换为HTTP响应,并可指定响应体的生命周期。FromData
:用于从请求的正文中解析到Rust结构体/枚举/元组。
其中,FromForm
、FromParam
、Serialize
和Template
等都是用于处理请求的输入数据,而Responder
、Stream
、WebSocket
和Responder<'r, 'o>
等则是用于构造响应的输出数据。FromData
是用于解析请求体的任意数据的通用trait。
路由属性和函数签名一起指定了一个请求必须是真实的,以使路由的处理程序被调用。你已经看到了一个实际的例子:
#[get("/world")]
fn handler() { /* .. */ }
这个路由表明,它只匹配对/world路由的GET请求。Rocket在处理程序被调用之前确保了这一点。当然,你可以做的比指定请求的方法和路径多得多。在其他方面,你可以要求Rocket自动进行验证:
一个动态路径段的类型。
几个动态路径段的类型。
传入的主体数据的类型。
查询字符串、表单和表单值的类型。
一个请求的预期传入或传出格式。
任何任意的、用户定义的安全或验证策略。
路由属性和函数签名协同工作来描述这些验证。Rocket的代码生成负责实际验证这些属性。本节描述了如何要求Rocket对所有这些属性进行验证,以及更多。
Methods
一个路由属性可以是
get,put,post,delete,head,patch,options
每个属性都对应于要匹配的HTTP方法
#[post("/")]
HEAD Requests
当存在一个GET路由时,Rocket会自动处理HEAD请求,否则会匹配。它通过从响应中剥离正文(如果有的话)来做到这一点。你也可以通过声明一个路由来专门处理HEAD请求;Rocket不会干涉你的应用程序明确处理的HEAD请求。
Reinterpreting
因为网络浏览器只支持以GET或POST请求的方式提交HTML表单,Rocket在某些条件下会重新解释请求方法。如果一个POST请求包含一个Content-Type: application/x-www-form-urlencoded的主体,并且表单的第一个字段的名称是_method,并且其值是一个有效的HTTP方法名称(如 “PUT”),那么该字段的值将被用作传入请求的方法。这使得Rocket应用程序可以提交非POST表单。todo例子利用这个功能从一个web表单提交PUT和DELETE请求。
Dynamic Paths
你可以通过在路由的路径中的变量名周围使用角括号来声明路径段为动态
#[get("/hello/<name>")]
fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
如果我们把路径挂在根部(.mount(“/”, routes![hello])),那么任何对有两个非空段的路径的请求,其中第一个段是hello,将被派发到hello路由。例如,如果我们要访问/hello/John,应用程序将响应Hello, John!。
任何数量的动态路径段都是允许的。一个路径段可以是任何类型,包括你自己的,只要该类型实现了FromParam的特性。我们称这些类型为参数保护器。Rocket为许多标准库中的类型,以及一些特殊的Rocket类型实现了FromParam。对于所提供的实现的完整列表,请参阅FromParam API文档。这里有一个更完整的路线来说明不同的用法:
#[get("/hello/<name>/<age>/<cool>")]
fn hello(name: &str, age: u8, cool: bool) -> String {
if cool {
format!("You're a cool {} year old, {}!", age, name)
} else {
format!("{}, we need to talk about your coolness.", name)
}
}
Multiple Segments多段式
你也可以通过在路由路径中使用<param…>来匹配多个段。这些参数的类型,被称为片段护卫,必须实现FromSegments。段落守护必须是路径的最后一个组成部分:在段落守护之后的任何文本将导致编译时错误。
use std::path::PathBuf;
#[get("/page/<path..>")]
fn get_page(path: PathBuf) { /* ... */ }
在/page/之后的路径将在path参数中提供,对于简单的/page、/page/、/page/等路径,path参数可能为空。PathBuf的FromSegments实现确保路径不能导致路径遍历攻击。有了这个,一个安全可靠的静态文件服务器只需要4行就可以实现了
use std::path::{Path, PathBuf};
use rocket::fs::NamedFile;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}
Ignored Segments
一个路由的一个组件可以通过使用<>完全忽略,多个组件可以通过使用<…>忽略。换句话说,通配符名称_是一个动态参数名称,可以忽略该动态参数。一个被忽略的参数不能出现在函数参数列表中。声明为<>的段可以匹配单个段中的任何内容,而声明为<…>的段可以无条件地匹配任意数量的段。
例如,下面的foo_bar路由匹配任何以/foo/开始、以/bar结束的三段式URI的GET请求。下面的everything路由匹配每个GET请求。
#[get("/foo/<_>/bar")]
fn foo_bar() -> &'static str {
"Foo _____ bar!"
}
#[get("/<_..>")]
fn everything() -> &'static str {
"Hey, you're here."
}
Forwarding
请求转发可以有多个方法,用于处理同一个请求不同的参数类型。
rank:用于表示转发处理的优先级,默认-12到-1
#[get("/user/<id>")]
fn user(id: usize) { /* ... */ }
#[get("/user/<id>", rank = 2)]
fn user_int(id: isize) { /* ... */ }
#[get("/user/<id>", rank = 3)]
fn user_str(id: &str) { /* ... */ }
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![user, user_int, user_str])
}
Default Ranking
如果没有明确指定等级,Rocket会分配一个默认等级。在路径和查询中,默认等级优先于静态网段:路由的路径和查询越是静态,其优先级就越高。
路径和查询有三种 colors:
static,意味着所有组件都是静态的
partial,意味着至少有一个组件是动态的
wild,意思是所有组件都是动态的
路径类型 | 查询类型 | 默认优先级 |
---|---|---|
静态 | 静态 | -12 |
静态 | 部分动态 | -11 |
静态 | 全部动态 | -10 |
静态 | 无 | -9 |
部分动态 | 静态 | -8 |
部分动态 | 部分动态 | -7 |
部分动态 | 全部动态 | -6 |
部分动态 | 无 | -5 |
全部动态 | 静态 | -4 |
全部动态 | 部分动态 | -3 |
全部动态 | 全部动态 | -2 |
全部动态 | 无 | -1 |
例子:
#[get("/foo/<_>/bar")]
fn foo_bar() { }
#[get("/<_..>")]
fn everything() { }
上面的是部分动态,下面的是全部动态
Request Guards
请求护卫
请求保护器是Rocket最强大的工具之一。正如它的名字所暗示的,一个请求保护器可以保护处理程序不被基于传入请求中的信息而错误地调用。更具体地说,一个请求保护器是一个代表任意验证策略的类型。验证策略是通过 FromRequest 特性实现的。每个实现FromRequest的类型都是一个请求保护器。
请求保护器作为处理程序的输入出现。任意数量的请求保护器可以作为参数出现在一个路由处理程序中。在调用处理程序之前,Rocket会自动调用FromRequest的实现来处理请求防护。只有当处理程序的所有守卫都通过时,Rocket才会将请求分配给处理程序。
例如,下面的假处理程序使用了三个请求保护器,A、B和C。如果一个输入在路由属性中没有被命名,它就可以被识别为一个请求保护器。
#[get("/<param>")]
fn index(param: isize, a: A, b: B, c: C) { /* ... */ }
请求警卫总是按照从左到右的声明顺序发射。在上面的例子中,顺序将是A跟B跟C。失败是短路;如果一个守护失败,其余的就不会被尝试。
Custom Guards
你可以为你自己的类型实现FromRequest。例如,为了保护一个敏感路由的运行,除非请求头中有ApiKey,你可以创建一个实现FromRequest的ApiKey类型,然后把它作为一个请求防护:
#[get("/sensitive")]
fn sensitive(key: ApiKey) { /* .. */ }
你也可以为AdminUser类型实现FromRequest,使用传入的cookie来验证管理员。然后,任何在其参数列表中带有AdminUser或ApiKey类型的处理程序都会被保证只在满足适当条件的情况下被调用。请求保护器集中了政策,导致了更简单、更安全、更有保障的应用。
Guard Transparency
当一个请求保护类型只能通过它的FromRequest实现来创建,并且该类型不是Copy,请求保护值的存在提供了一个类型级别的证明,即当前请求已经被验证为符合任意策略。这提供了强大的手段,通过要求数据访问方法通过请求保护见证授权证明,来保护你的应用程序免受访问控制的侵犯。我们把使用请求保护作为见证保护的概念称为透明度。
作为一个具体的例子,下面的应用程序有一个函数health_records,用来返回数据库中所有的健康记录。因为健康记录是敏感信息,它们应该只被超级用户访问。超级用户请求保护程序对超级用户进行认证和授权,它的FromRequest实现是构建超级用户的唯一途径。通过对health_records函数进行如下声明,可以保证在编译时防止对健康记录的访问控制违规:
fn health_records(user: &SuperUser) -> Records { /* ... */ }
推理如下:
health_records函数需要一个&SuperUser类型。
SuperUser类型的唯一构造函数是FromRequest。
只有Rocket可以提供一个活跃的&Request来通过FromRequest构造。
因此,必须有一个Request授权给超级用户来调用health_records。
在守护类型中牺牲了一个寿命参数,通过将传递给FromRequest的Request的寿命与请求守护联系起来,确保守护值总是对应于一个活动的请求,可以使保障更加强大。
我们建议在所有的数据访问中利用请求保护的透明度。
Forwarding Guards
请求守护和转发是执行政策的一个强大组合。为了说明这一点,我们考虑如何使用这些机制来实现一个简单的授权系统。
我们从两个请求护卫开始:
用户:一个普通的、经过认证的用户。
User的FromRequest实现会检查一个cookie是否识别了一个用户,如果是,则返回一个User值。如果没有用户可以被认证,守护就会继续前进。
AdminUser:一个被认证为管理员的用户。
对AdminUser的FromRequest实现检查一个cookie是否识别了一个管理用户,如果是,则返回一个AdminUser值。如果没有用户可以被认证,则守护者会转发。
我们现在使用这两个防护与转发相结合,实现以下三个路由,每个路由都通向/admin的管理控制面板:
use rocket::response::Redirect;
#[get("/login")]
fn login() -> Template { /* .. */ }
#[get("/admin")]
fn admin_panel(admin: AdminUser) -> &'static str {
"Hello, administrator. This is the admin panel!"
}
#[get("/admin", rank = 2)]
fn admin_panel_user(user: User) -> &'static str {
"Sorry, you must be an administrator to access this page."
}
#[get("/admin", rank = 3)]
fn admin_panel_redirect() -> Redirect {
Redirect::to(uri!(login))
}
上面的三条路由对认证和授权进行了编码。admin_panel路由只有在管理员登录后才会成功。只有这样才会显示管理面板。如果用户不是管理员,AdminUser防护将转发。由于admin_panel_user路由的排名次之,所以接下来会尝试。如果有任何用户登录,这个路由就会成功,并显示一个授权失败信息。最后,如果有用户没有登录,就会尝试使用admin_panel_redirect路由。因为这个路由没有守卫,它总是成功的。用户会被重定向到一个登录页面。
Cookies
对CookieJar的引用是一个重要的、内置的请求保护程序:它允许你获取、设置和删除cookies。因为&CookieJar是一个请求保护程序,它的一个参数可以简单地被添加到一个处理程序中:
use rocket::http::CookieJar;
#[get("/")]
fn index(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get("message").map(|crumb| format!("Message: {}", crumb.value()))
}
这导致传入的请求的cookie可以从处理程序中访问。上面的例子检索了一个名为message的cookie。Cookie也可以使用CookieJar防护来设置和删除。GitHub上的cookie例子说明了进一步使用CookieJar类型来获取和设置cookie,而CookieJar文档包含完整的使用信息。
Private Cookies
通过CookieJar::add()方法添加的Cookie是在明处设置的。换句话说,设置的值对客户端是可见的。对于敏感数据,Rocket提供了私人cookie。私人cookie与普通cookie类似,只是它们使用验证式加密,这是一种同时提供保密性、完整性和真实性的加密形式。因此,私人cookies不能被检查、篡改或由客户制造。如果你愿意,你可以把私人cookies看成是经过签名和加密的。
对私人cookies的支持必须通过secrets crate功能手动启用。
## in Cargo.toml
rocket = { version = "=0.5.0-rc.3", features = ["secrets"] }
检索、添加和删除私人cookies的API是相同的,只是大多数方法的后缀是_private。这些方法是:get_private、[get_pending]、add_private和remove_private。下面是它们的一个使用例子:
use rocket::http::{Cookie, CookieJar};
use rocket::response::{Flash, Redirect};
/// Retrieve the user's ID, if any.
#[get("/user_id")]
fn user_id(cookies: &CookieJar<'_>) -> Option<String> {
cookies.get_private("user_id")
.map(|crumb| format!("User ID: {}", crumb.value()))
}
/// Remove the `user_id` cookie.
#[post("/logout")]
fn logout(cookies: &CookieJar<'_>) -> Flash<Redirect> {
cookies.remove_private(Cookie::named("user_id"));
Flash::success(Redirect::to("/"), "Successfully logged out.")
}
Secret Key
为了加密私人cookies,Rocket使用secret_key配置参数中指定的256位密钥。当在调试模式下编译时,会自动生成一个新的密钥。在发布模式下,如果秘密功能被启用,Rocket要求你设置一个秘密密钥。如果不这样做,在启动时就会出现一个硬错误。该参数的值可以是一个256位base64或十六进制的字符串,也可以是一个32字节的片断。
生成一个适合作为secret_key配置值的字符串通常是通过openssl等工具完成的。使用openssl,可以用命令openssl rand -base64 32生成一个256位base64密钥。
https://rocket.rs/v0.5-rc/guide/configuration
Format
一个路由可以通过使用格式路由参数来指定它愿意接受或响应的数据格式。该参数的值是一个标识HTTP媒体类型或速记变体的字符串。例如,对于JSON数据,可以使用字符串application/json或简称json。
当一个路由指示一个支持有效载荷的方法(PUT、POST、DELETE和PATCH)时,格式路由参数指示Rocket检查传入请求的Content-Type头。只有内容类型头与格式参数匹配的请求才会与路由匹配。
#[post("/user", format = "application/json", data = "<user>")]
fn new_user(user: User) { /* ... */ }
post属性中的格式参数声明只有带有Content-Type: application/json的传入请求才会匹配new_user。(数据参数将在下一节描述。)最常见的格式参数也支持速记。你可以不使用完整的Content-Type,format = “application/json”,而是使用速记法,如format = “json”。关于可用速记的完整列表,请参见 ContentType::parse_flexible() 文档。
https://api.rocket.rs/v0.5-rc/rocket/http/struct.ContentType.html#method.parse_flexible
当路由指示一个不支持payload的方法(GET、HEAD、OPTIONS)时,format路由参数指示Rocket检查传入请求的接受头。只有Accept头中的首选媒体类型与format参数匹配的请求才会与路由匹配。
#[get("/user/<id>", format = "json")]
fn user(id: usize) -> User { /* .. */ }
get属性中的格式参数声明,只有在Accept头中把application/json作为首选媒体类型的传入请求才会与用户匹配。如果路由被声明为post,Rocket将与传入的响应的Content-Type头的格式相匹配。
Body Data
身体数据处理,就像Rocket的大部分内容一样,是以类型为导向的。为了表明一个处理程序期待body data,用data = ""来注释它,其中param是处理程序中的一个参数。参数的类型必须实现 FromData 特质。它看起来像这样,其中T被假定为实现了FromData:
#[post("/", data = "<input>")]
fn new(input: T) { /* .. */ }
任何实现FromData的类型也被称为数据保护。
JSON
Json防护措施将主体数据反序列化为JSON。唯一的条件是,通用类型T实现了Serde的Deserialize特性。
use rocket::serde::{Deserialize, json::Json};
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Task<'r> {
description: &'r str,
complete: bool
}
#[post("/todo", data = "<task>")]
fn new(task: Json<Task<'_>>) { /* .. */ }
警告:使用Rocket的serde派生再输出需要多花点功夫。
为了方便起见,Rocket从rocket::serde中重新导出了serde的Serialize和Deserialize特性和derive宏。然而,由于Rust对派生宏的再出口支持有限,使用再出口的派生宏需要用#[serde(crate = “rocket::serde”)]来注释结构。如果你想避免这个额外的注释,你必须通过你的crate的Cargo.toml直接依赖serde:serde = { version = “1.0”, features = [“derive”] }
我们在指南中总是使用额外的注解,但你可能更喜欢另一种方式。
注意:支持JSON需要启用Rocket的json功能标志。
Rocket有意将JSON支持,以及对其他数据格式和功能的支持放在功能标志后面。参见api文档中的可用功能列表。json功能可以在Cargo.toml中启用:
rocket = { version = “=0.5.0-rc.3”, features = [“json”] }
Temporary Files
TempFile数据保护器将数据直接流向一个临时文件,然后可以被持久化。它使接受文件的上传变得微不足道:
use rocket::fs::TempFile;
#[post("/upload", format = "plain", data = "<file>")]
async fn upload(mut file: TempFile<'_>) -> std::io::Result<()> {
file.persist_to(permanent_location).await
}
Streaming
有时你只是想直接处理传入的数据。例如,你可能想把传入的数据流到某个汇中。Rocket通过数据类型使之尽可能的简单:
use rocket::tokio;
use rocket::data::{Data, ToByteUnit};
#[post("/debug", data = "<data>")]
async fn debug(data: Data<'_>) -> std::io::Result<()> {
// Stream at most 512KiB all of the body data to stdout.
data.open(512.kibibytes())
.stream_to(tokio::io::stdout())
.await?;
Ok(())
}
上面的路由接受任何到/debug路径的POST请求。传入的最多512KiB的内容会被流传到stdout。如果上传失败,会返回一个错误响应。上面的处理程序就完成了。它真的就这么简单!
注意:Rocket在读取传入数据时需要设置限制。
为了帮助防止DoS攻击,Rocket要求你在打开数据流时,以ByteUnit的形式指定你愿意从客户端接受的数据量。ToByteUnit特性使得指定这样一个值就像128.kibibytes()一样习惯。
Forms
表单是Web应用程序中最常见的数据类型之一,Rocket使处理它们变得简单。Rocket支持多部分和x-www-form-urlencoded的表单,由Form数据保护和可派生的FromForm特性实现。
假设你的应用程序正在处理一个新todo任务的表单提交。该表单包含两个字段:完成,一个复选框,和类型,一个文本字段。你可以在Rocket中轻松地处理表单请求,如下所示:
use rocket::form::Form;
#[derive(FromForm)]
struct Task<'r> {
complete: bool,
r#type: &'r str,
}
#[post("/todo", data = "<task>")]
fn new(task: Form<Task<'_>>) { /* .. */ }
只要它的通用参数实现了FromForm特质,那么Form就是数据保护。在这个例子中,我们已经为Task自动派生了FromForm特质。FromForm可以被派生到任何字段实现FromForm的结构中,或者等同于FromFormField。
如果一个POST /todo请求到达,表单数据将自动被解析为任务结构。如果到达的数据不是正确的Content-Type,该请求将被转发。如果数据没有被解析或根本无效,就会返回一个可定制的错误。和以前一样,可以通过使用选项和结果类型来捕获转发或失败:
#[post("/todo", data = "<task>")]
fn new(task: Option<Form<Task<'_>>>) { /* .. */ }
Multipart
多部分表单的处理是透明的,不需要额外的努力。大多数FromForm类型可以从传入的数据流中解析自己。例如,这是一个接受使用TempFile上传的多部分文件的表单和路由:
use rocket::form::Form;
use rocket::fs::TempFile;
#[derive(FromForm)]
struct Upload<'r> {
save: bool,
file: TempFile<'r>,
}
#[post("/upload", data = "<upload>")]
fn upload_form(upload: Form<Upload<'_>>) { /* .. */ }
Parsing Strategy
Rocket’s
FromForm
parsing is lenient by default: aForm<T>
will parse successfully from an incoming form even if it contains extra, duplicate, or missing fields. Extras or duplicates are ignored – no validation or parsing of the fields occurs – and missing fields are filled with defaults when available. To change this behavior and make form parsing strict, use theForm>
data type, which emits errors if there are any extra or missing fields, irrespective of defaults.
Rocket的FromForm解析默认是宽松的:一个Form将从一个传入的表单成功解析,即使它包含额外的、重复的或丢失的字段。额外的或重复的字段会被忽略 – 不发生验证或解析字段 – 缺少的字段在可用时用默认值填充。要改变这种行为并使表单解析变得严格,请使用**Form<Strict>**数据类型,如果有任何额外的或缺失的字段,无论默认值如何,它都会发出错误。
You can use a
Form<Strict<T>>
anywhere you’d use aForm<T>
. Its generic parameter is also required to implementFromForm
. For instance, we can simply replaceForm<T>
withForm<Strict<T>>
above to get strict parsing:
你可以在任何你使用 Form 的地方使用 Form<Strict。它的通用参数也是实现 FromForm 的必要条件。例如,我们可以简单地用上面的 Form<Strict 替换 Form 来获得严格解析:
use rocket::form::{Form, Strict};
#[post("/todo", data = "<task>")]
fn new(task: Form<Strict<Task<'_>>>) { /* .. */ }
Strict
can also be used to make individual fields strict while keeping the overall structure and remaining fields lenient:
Strict也可以用来使个别字段变得严格,同时保持整体结构和其余字段的宽松:
#[derive(FromForm)]
struct Input {
required: Strict<bool>,
uses_default: bool
}
#[post("/", data = "<input>")]
fn new(input: Form<Input>) { /* .. */ }
Lenient
is the lenient analog toStrict
, which forces parsing to be lenient.Form
is lenient by default, so aForm<Lenient<T>>
is redundant, butLenient
can be used to overwrite a strict parse as lenient:Option<Lenient<T>>
.
Lenient 是 Strict 的宽松类似物,Strict 强制解析为宽松。Form默认是宽松的,所以Form<Lenient>是多余的,但是Lenient可以用来把严格的解析覆盖为宽松的:Option<Lenient>。
Defaults
A form guard may specify a default value to use when a field is missing. The default value is used only when parsing is lenient. When strict, all errors, including missing fields, are propagated directly.
Some types with defaults include
bool
, which defaults tofalse
, useful for checkboxes,Option<T>
, which defaults toNone
, andform::Result
, which defaults toErr(Missing)
or otherwise collects errors in anErr
ofErrors<'_>
. Defaulting guards can be used just like any other form guard:
当一个字段缺失时,表单保护可以指定一个默认值来使用。默认值仅在解析宽松时使用。当严格时,所有的错误,包括缺失的字段,都会被直接传播出去。
一些有默认值的类型包括bool,默认为false,对复选框很有用;Option,默认为None;form::Result,默认为Err(Missing)或以其他方式收集Errors<'_>中的Err错误。默认守护可以像其他表单守护一样使用:
// 引入`form`模块的`Errors`类型
use rocket::form::{self, Errors};
// 定义一个结构体`MyForm`,它实现了`FromForm` trait
#[derive(FromForm)]
struct MyForm<'v> {
// 字段`maybe_string`是一个可选的字符串引用
maybe_string: Option<&'v str>,
// 字段`ok_or_error`是一个表单解析结果`form::Result`类型,它将返回一个`Vec`类型的字符串引用
ok_or_error: form::Result<'v, Vec<&'v str>>,
// 字段`here_or_false`是一个布尔值
here_or_false: bool,
}
The default can be overridden or unset using the
#[field(default = expr)]
field attribute. Ifexpr
is not literallyNone
, the parameter sets the default value of the field to beexpr.into()
. Ifexpr
isNone
, the parameter unsets the default value of the field, if any.
默认值可以使用#[field(default = expr)]字段属性来覆盖或取消设置。如果expr不是真的无,该参数将字段的默认值设置为expr.into()。如果expr是 “无”,该参数将取消字段的默认值,如果有的话。
#[derive(FromForm)]
struct MyForm {
// 将默认值设置为`"hello"`。
//
// 注意如何将`&str`自动转换为`String`。
#[field(default = "hello")]
greeting: String,
// 删除`false`的默认值,要求所有解析`MyForm`的值都包含`is_friendly`字段。
#[field(default = None)]
is_friendly: bool,
}
See the
FromForm
derive documentation for full details on thedefault
attribute parameter as well documentation on the more expressivedefault_with
parameter option.
关于默认属性参数的全部细节,请参见FromForm派生文档,以及关于更具表现力的default_with参数选项的文档。(https://api.rocket.rs/v0.5-rc/rocket/derive.FromForm.html)
Field Renaming
By default, Rocket matches the name of an incoming form field to the name of a structure field. While this behavior is typical, it may also be desired to use different names for form fields and struct fields while still parsing as expected. You can ask Rocket to look for a different form field for a given structure field by using one or more
#[field(name = "name")]
or#[field(name = uncased("name")]
field annotation. Theuncased
variant case-insensitively matches field names.
默认情况下,Rocket将传入的表单字段的名称与结构字段的名称相匹配。虽然这种行为很典型,但也可能需要为表单字段和结构字段使用不同的名称,同时仍然按照预期进行解析。你可以通过使用一个或多个#[field(name = “name”)]或#[field(name = uncased(“name”)]字段注解,要求Rocket为给定的结构字段寻找不同的表单字段。不加大小写的变体对字段名进行不敏感的匹配。
As an example, say that you’re writing an application that receives data from an external service. The external service
POST
s a form with a field namedfirst-Name
which you’d like to write asfirst_name
in Rust. Such a form structure can be written as:
举个例子,假设你正在编写一个从外部服务接收数据的应用程序。外部服务发送了一个表单,其中有一个名为first-Name的字段,你想在Rust中写成first_name。这样的表单结构可以写成:
#[derive(FromForm)]
struct External<'r> {
#[field(name = "first-Name")]
first_name: &'r str
}
If you want to accept both
firstName
case-insensitively as well asfirst_name
case-sensitively, you’ll need to use two annotations:
如果你想同时接受不区分大小写的firstName和不区分大小写的first_name,你将需要使用两个注释:
#[derive(FromForm)]
struct External<'r> {
#[field(name = uncased("firstName"))]
#[field(name = "first_name")]
first_name: &'r str
}
This will match any casing of
firstName
includingFirstName
,firstname
,FIRSTname
, and so on, but only match exactly onfirst_name
.
这将匹配firstName的任何大小写,包括FirstName、firstname、FIRSTname等,但只精确匹配first_name。
If instead you wanted to match any of
first-name
,first_name
orfirstName
, in each instance case-insensitively, you would write:
如果你想匹配first-name、first_name或firstName中的任何一个,在每个实例中都不考虑大小写,你可以写:
#[derive(FromForm)]
struct External<'r> {
#[field(name = uncased("first-name"))]
#[field(name = uncased("first_name"))]
#[field(name = uncased("firstname"))]
first_name: &'r str
}
Cased and uncased renamings can be mixed and matched, and any number of renamings is allowed. Rocket will emit an error at compile-time if field names conflict, preventing ambiguous parsing at runtime.
有名和无名的重命名可以混合匹配,并且允许任何数量的重命名。如果字段名称发生冲突,Rocket将在编译时发出错误,以防止在运行时进行模糊的解析。
Ad-Hoc Validation
Fields of forms can be easily ad-hoc validated via the
#[field(validate)]
attribute. As an example, consider a form fieldage: u16
which we’d like to ensure is greater than21
. The following structure accomplishes this:
表单字段可以通过#[field(validate)]属性轻松地进行临时验证。举个例子,考虑一个表单字段年龄:u16,我们想确保它大于21。下面的结构完成了这个任务:
#[derive(FromForm)]
struct Person {
#[field(validate = range(21..))]
age: u16
}
The expression
range(21..)
is a call toform::validate::range
. Rocket passes a borrow of the attributed field, hereself.age
, as the first parameter to the function call. The rest of the fields are pass as written in the expression.Any function in the
form::validate
module can be called, and other fields of the form can be passed in by usingself.$field
where$field
is the name of the field in the structure. You can also apply more than one validation to a field by using multiple attributes. For example, the following form validates that the value of the fieldconfirm
is equal to the value of the fieldvalue
and that it doesn’t containno
:
表达式range(21…) 是对form::validate::range的调用。Rocket 传递了一个属性字段的借款,这里是 self.age,作为函数调用的第一个参数。其余的字段按照表达式中的写法传递。
form::validate模块中的任何函数都可以被调用,表单中的其他字段可以通过使用self. f i e l d 传入,其中 field传入,其中 field传入,其中field是结构中的字段名。你也可以通过使用多个属性对一个字段应用多个验证。例如,下面这个表单验证字段confirm的值等于字段value的值,并且不包含no:
#[derive(FromForm)]
struct Password<'r> {
// 指定名称为 "password" 的表单字段解析为结构体中的 `value` 字段。
#[field(name = "password")]
value: &'r str,
// 指定对 `confirm` 字段进行验证:
// 1. 该字段的值应与 `value` 字段的值相同。
#[field(validate = eq(self.value))]
// 2. 该字段的值不能为字符串 "no"。
#[field(validate = omits("no"))]
confirm: &'r str,
}
In reality, the expression after
validate =
can be any expression as long as it evaluates to a value of typeResult<(), Errors<'_>>
(aliased byform::Result
), where anOk
value means that validation was successful while anErr
ofErrors<'_>
indicates the error(s) that occurred. For instance, if you wanted to implement an ad-hoc Luhn validator for credit-card-like numbers, you might write:
实际上,validate =后面的表达式可以是任何表达式,只要它评估为Result<(), Errors<'>>类型的值(由form::Result别名),其中Ok值表示验证成功,而Errors<'>的Err表示发生的错误。例如,如果你想为类似信用卡的数字实现一个临时的Luhn验证器,你可以写:
use rocket::time::Date;
use rocket::form::{self, Error};
// 定义了一个CreditCard结构体,表示信用卡信息
#[derive(FromForm)]
struct CreditCard {
// validate属性会使输入的值通过luhn函数进行验证
#[field(validate = luhn(self.cvv, &self.expiration))]
number: u64,
// cvv码必须是介于0和9999之间的u16类型
#[field(validate = range(..9999))]
cvv: u16,
// 信用卡过期日期使用Rocket的Date类型表示
expiration: Date,
}
// 定义了一个luhn函数,用于验证信用卡号码
fn luhn<'v>(number: &u64, cvv: u16, exp: &Date) -> form::Result<'v, ()> {
// 在这里执行验证操作,如果不合法,则使用validation函数返回错误信息
if !valid {
Err(Error::validation("invalid credit card number"))?;
}
Ok(())
}
If a field’s validation doesn’t depend on other fields (validation is local), it is validated prior to those fields that do. For
CreditCard
,cvv
andexpiration
will be validated prior tonumber
.
如果一个字段的验证不依赖于其他字段(验证是本地的),它将在那些需要验证的字段之前进行验证。对于CreditCard,cvv和expiration将在number之前被验证。
Wrapping Validators
If a particular validation is applied in more than once place, prefer creating a type that encapsulates and represents the validated value. For example, if your application often validates
age
fields, consider creating a customAge
form guard that always applies the validation:
如果一个特定的验证被应用在多个地方,最好创建一个封装和代表验证值的类型。例如,如果你的应用程序经常验证年龄字段,考虑创建一个自定义的Age表单防护,它总是应用验证:
#[derive(FromForm)]
#[field(validate = range(18..150))]
struct Age(u16);
This approach is also useful when a custom validator already exists in some other form. For instance, the following example leverages
try_with
and an existingFromStr
implementation on aToken
type to validate a string:
当一个自定义验证器已经以其他形式存在时,这种方法也很有用。例如,下面的例子利用try_with和Token类型上现有的FromStr实现来验证一个字符串:
// 通过 `FromStr` trait 尝试将字符串转换为 Token 类型,若失败则返回错误。
// 字段名为 "Token",类型为字符串引用。
#[derive(FromForm)]
#[field(validate = try_with(|s| Token::from_str(s)))]
struct Token<'r>(&'r str);
Collections
Rocket’s form support allows your application to express any structure with any level of nesting and collection, eclipsing the expressivity offered by any other web framework. To parse into these structures, Rocket separates a field’s name into “keys” by the delimiters
.
and[]
, each of which in turn is separated into “indices” by:
. In other words, a name has keys and a key has indices, each a strict subset of its parent. This is depicted in the example below with two form fields:
Rocket的表单支持允许你的应用程序以任何级别的嵌套和集合来表达任何结构,使任何其他Web框架所提供的表达能力黯然失色。为了解析这些结构,Rocket通过分隔符.和[]将一个字段的名称分离成 “键”,每个键又被:分离成 “索引”。换句话说,一个名字有键,一个键有索引,每个索引都是其父级的严格子集。这在下面的例子中描述了两个表格字段:
food.bart[bar:foo].blam[0_0][1000]=some-value&another_field=another_val
|-------------------------------| name
|--| |--| |-----| |--| |-| |--| keys
|--| |--| |-| |-| |--| |-| |--| indices
Rocket pushes form fields to
FromForm
types as they arrive. The type then operates on one key (and all of its indices) at a time and shifts to the nextkey
, from left-to-right, before invoking any otherFromForm
types with the rest of the field. A shift encodes a nested structure while indices allows for structures that need more than one value to allow indexing.
Rocket在表单字段到达时将其推送给FromForm类型。然后,该类型一次对一个键(以及它的所有索引)进行操作,并从左到右转移到下一个键,然后再调用任何其他FromForm类型与字段的其余部分。移位编码一个嵌套结构,而索引允许需要一个以上的值的结构,以允许索引。
注意:[]后面的.是可选的。
表单字段名a[b]c完全等同于a[b].c。同样,表单字段名.a也等同于a。
Nesting
Form structs can be nested:
表格结构可以被嵌套:
use rocket::form::FromForm;
#[derive(FromForm)]
struct MyForm<'r> {
owner: Person<'r>,
pet: Pet<'r>,
}
#[derive(FromForm)]
struct Person<'r> {
name: &'r str
}
#[derive(FromForm)]
struct Pet<'r> {
name: &'r str,
#[field(validate = eq(true))]
good_pet: bool,
}
To parse into a
MyForm
, a form with the following fields must be submitted:
owner.name
- stringpet.name
- stringpet.good_pet
- booleanSuch a form, URL-encoded, may look like:
要解析成MyForm,必须提交一个有以下字段的表格:
owner.name - string
Pet.name - string
pet.good_pet - boolean
这样一个表格,经过URL编码后,可能看起来像:
"owner.name=Bob&pet.name=Sally&pet.good_pet=on",
// ...which parses as this struct.
MyForm {
owner: Person {
name: "Bob".into()
},
pet: Pet {
name: "Sally".into(),
good_pet: true,
}
}
Note that
.
is used to separate each field. Identically,[]
can be used in place of or in addition to.
:
请注意,.
是用来分隔每个字段的。相同的是,[]
可以用来代替或补充.
的位置:
// All of these are identical to the previous...
"owner[name]=Bob&pet[name]=Sally&pet[good_pet]=on",
"owner[name]=Bob&pet[name]=Sally&pet.good_pet=on",
"owner.name=Bob&pet[name]=Sally&pet.good_pet=on",
"pet[name]=Sally&owner.name=Bob&pet.good_pet=on",
// ...and thus parse as this struct.
MyForm {
owner: Person {
name: "Bob".into()
},
pet: Pet {
name: "Sally".into(),
good_pet: true,
}
}
Any level of nesting is allowed.
任何级别的嵌套都是允许的。
Vectors
A form can also contain sequences:
一个表格也可以包含序列:
#[derive(FromForm)]
struct MyForm {
numbers: Vec<usize>,
}
To parse into a
MyForm
, a form with the following fields must be submitted:
numbers[$k]
- usize (or equivalently,numbers.$k
)…where
$k
is the “key” used to determine whether to push the rest of the field to the last element in the vector or create a new one. If the key is the same as the previous key seen by the vector, then the field’s value is pushed to the last element. Otherwise, a new element is created. The actual value of$k
is irrelevant: it is only used for comparison, has no semantic meaning, and is not remembered byVec
. The special blank key is never equal to any other key.
要解析成一个MyForm,必须提交一个有以下字段的表格:
numbers[
k
]
−
u
s
i
z
e
(
或等同于
n
u
m
b
e
r
s
.
k] - usize (或等同于 numbers.
k]−usize(或等同于numbers.k)
…其中
k
是
"
键
"
,用于确定是否将字段的其余部分推到向量中的最后一个元素或创建一个新的元素。如果键与矢量所见的前一个键相同,那么字段的值就被推到最后一个元素。否则,将创建一个新的元素。
k是 "键",用于确定是否将字段的其余部分推到向量中的最后一个元素或创建一个新的元素。如果键与矢量所见的前一个键相同,那么字段的值就被推到最后一个元素。否则,将创建一个新的元素。
k是"键",用于确定是否将字段的其余部分推到向量中的最后一个元素或创建一个新的元素。如果键与矢量所见的前一个键相同,那么字段的值就被推到最后一个元素。否则,将创建一个新的元素。k的实际值是不相关的:它只用于比较,没有语义,也不被Vec记住。特殊的空白键永远不等于任何其他键。
// These form strings...
"numbers[]=1&numbers[]=2&numbers[]=3",
"numbers[a]=1&numbers[b]=2&numbers[c]=3",
"numbers[a]=1&numbers[b]=2&numbers[a]=3",
"numbers[]=1&numbers[b]=2&numbers[c]=3",
"numbers.0=1&numbers.1=2&numbers[c]=3",
"numbers=1&numbers=2&numbers=3",
// ...parse as this struct:
MyForm {
numbers: vec![1 ,2, 3]
}
// These, on the other hand...
"numbers[0]=1&numbers[0]=2&numbers[]=3",
"numbers[]=1&numbers[b]=3&numbers[b]=2",
// ...parse as this struct:
MyForm {
numbers: vec![1, 3]
}
You might be surprised to see the last example,
"numbers=1&numbers=2&numbers=3"
, in the first list. This is equivalent to the previous examples as the “key” seen by theVec
(everything afternumbers
) is empty. Thus,Vec
pushes to a newusize
for every field.usize
, like all types that implementFromFormField
, discard duplicate and extra fields when parsed leniently, keeping only the first field.
你可能会惊讶地看到最后一个例子,“numbers=1&numbers=2&numbers=3”,在第一个列表中。这等同于前面的例子,因为Vec看到的 “键”(数字后面的所有内容)是空的。因此,Vec为每个字段推送到一个新的usize。usize和所有实现FromFormField的类型一样,在宽松地解析时丢弃重复和多余的字段,只保留第一个字段。
Nesting in Vectors
Any
FromForm
type can appear in a sequence:
任何FromForm类型都可以出现在一个序列中:
#[derive(FromForm)]
struct MyForm {
name: String,
pets: Vec<Pet>,
}
#[derive(FromForm)]
struct Pet {
name: String,
#[field(validate = eq(true))]
good_pet: bool,
}
To parse into a
MyForm
, a form with the following fields must be submitted:
要解析成MyForm,必须提交一个有以下字段的表格:
name
- stringpets[$k].name
- stringpets[$k].good_pet
- boolean
Examples include:
这方面的例子包括:
// These form strings...
assert_form_parses! { MyForm,
"name=Bob&pets[0].name=Sally&pets[0].good_pet=on",
"name=Bob&pets[sally].name=Sally&pets[sally].good_pet=yes",
// ...parse as this struct:
MyForm {
name: "Bob".into(),
pets: vec![Pet { name: "Sally".into(), good_pet: true }],
}
// These, on the other hand, fail to parse:
"name=Bob&pets[0].name=Sally&pets[1].good_pet=on",
"name=Bob&pets[].name=Sally&pets[].good_pet=on",
Nested Vectors
Since vectors are
FromForm
themselves, they can appear inside of vectors:
由于向量本身是FromForm,它们可以出现在向量的内部:
#[derive(FromForm)]
struct MyForm {
v: Vec<Vec<usize>>,
}
The rules are exactly the same.
规则是完全一样的。
"v=1&v=2&v=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },
"v[][]=1&v[][]=2&v[][]=3" => MyForm { v: vec![vec![1], vec![2], vec![3]] },
"v[0][]=1&v[0][]=2&v[][]=3" => MyForm { v: vec![vec![1, 2], vec![3]] },
"v[][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1], vec![2, 3]] },
"v[0][]=1&v[0][]=2&v[0][]=3" => MyForm { v: vec![vec![1, 2, 3]] },
"v[0][0]=1&v[0][0]=2&v[0][]=3" => MyForm { v: vec![vec![1, 3]] },
"v[0][0]=1&v[0][0]=2&v[0][0]=3" => MyForm { v: vec![vec![1]] },
Maps
A form can also contain maps:
一个表格也可以包含Map:
use std::collections::HashMap;
#[derive(FromForm)]
struct MyForm {
ids: HashMap<String, usize>,
}
To parse into a
MyForm
, a form with the following fields must be submitted:
要解析成MyForm,必须提交一个带有以下字段的表格:
ids[$string]
- usize (or equivalently,ids.$string
)
…where
$string
is the “key” used to determine which value in the map to push the rest of the field to. Unlike with vectors, the key does have a semantic meaning and is remembered, so ordering of fields is inconsequential: a given string$string
always maps to the same element.
…其中 s t r i n g 是 " 键 " ,用于确定在地图中把字段的其余部分推到哪个值。与向量不同的是,键确实有语义并被记住,所以字段的排序是不重要的:一个给定的字符串 string是 "键",用于确定在地图中把字段的其余部分推到哪个值。与向量不同的是,键确实有语义并被记住,所以字段的排序是不重要的:一个给定的字符串 string是"键",用于确定在地图中把字段的其余部分推到哪个值。与向量不同的是,键确实有语义并被记住,所以字段的排序是不重要的:一个给定的字符串string总是映射到同一个元素。
As an example, the following are equivalent and all parse to
{ "a" => 1, "b" => 2 }
:
作为一个例子,以下是等价的,都解析为{ “a” => 1, “b” => 2 }:
// These form strings...
"ids[a]=1&ids[b]=2",
"ids[b]=2&ids[a]=1",
"ids[a]=1&ids[a]=2&ids[b]=2",
"ids.a=1&ids.b=2",
// ...parse as this struct:
MyForm {
ids: map! {
"a" => 1usize,
"b" => 2usize,
}
}
Both the key and value of a
HashMap
can be any type that implementsFromForm
. Consider a value representing another structure:
HashMap的键和值都可以是任何实现了FromForm的类型。考虑一个代表另一个结构的值:
#[derive(FromForm)]
struct MyForm {
ids: HashMap<usize, Person>,
}
#[derive(FromForm)]
struct Person {
name: String,
age: usize
}
To parse into a
MyForm
, a form with the following fields must be submitted:
ids[$usize].name
- stringids[$usize].age
- usize
要解析成MyForm,必须提交一个有以下字段的表格:
ids[$usize].name
- stringids[$usize].age
- usize
// These form strings...
"ids[0]name=Bob&ids[0]age=3&ids[1]name=Sally&ids[1]age=10",
"ids[0]name=Bob&ids[1]age=10&ids[1]name=Sally&ids[0]age=3",
"ids[0]name=Bob&ids[1]name=Sally&ids[0]age=3&ids[1]age=10",
// ...which parse as this struct:
MyForm {
ids: map! {
0usize => Person { name: "Bob".into(), age: 3 },
1usize => Person { name: "Sally".into(), age: 10 },
}
}
Now consider the following structure where both the key and value represent structures:
现在考虑以下结构,其中键和值都代表结构:
#[derive(FromForm)]
struct MyForm {
m: HashMap<Person, Pet>,
}
#[derive(FromForm, PartialEq, Eq, Hash)]
struct Person {
name: String,
age: usize
}
#[derive(FromForm)]
struct Pet {
wags: bool
}
Warning: The
HashMap
key type, herePerson
, must implementEq + Hash
.Since the key is a collection, here
Person
, it must be built up from multiple fields. This requires being able to specify via the form field name that the field’s value corresponds to a key in the map. The is done with the syntaxk:$key
which indicates that the field corresponds to thek
ey named$key
. Thus, to parse into aMyForm
, a form with the following fields must be submitted:
m[k:$key].name
- stringm[k:$key].age
- usizem[$key].wags
orm[v:$key].wags
- boolean
警告:HashMap的键类型,这里是Person,必须实现Eq + Hash。
由于键是一个集合,这里是Person,它必须由多个字段建立起来。这就要求能够通过表单字段名指定该字段的值与地图中的一个键相对应。这是通过语法k:
k
e
y
完成的,它表示字段对应于名为
key完成的,它表示字段对应于名为
key完成的,它表示字段对应于名为key的键。因此,要解析成一个MyForm,必须提交一个有以下字段的表单:
m[k:$key].name
- stringm[k:$key].age
- usizem[$key].wags
orm[v:$key].wags
- boolean
Note: The syntax
v:$key
also exists.The shorthand
m[$key]
is equivalent tom[v:$key]
.
注意:也存在v: k e y 的语法。简称 m [ key的语法。 简称m[ key的语法。简称m[key]等同于m[v:$key]。
Note that
$key
can be anything: it is simply a symbolic identifier for a key/value pair in the map and has no bearing on the actual values that will be parsed into the map.
请注意,$key可以是任何东西:它只是地图中键/值对的一个符号标识,与将被解析到地图中的实际值没有关系。
// These form strings...
"m[k:alice]name=Alice&m[k:alice]age=30&m[v:alice].wags=no",
"m[k:alice]name=Alice&m[k:alice]age=30&m[alice].wags=no",
"m[k:123]name=Alice&m[k:123]age=30&m[123].wags=no",
// ...which parse as this struct:
MyForm {
m: map! {
Person { name: "Alice".into(), age: 30 } => Pet { wags: false }
}
}
// While this longer form string...
"m[k:a]name=Alice&m[k:a]age=40&m[a].wags=no&\
m[k:b]name=Bob&m[k:b]age=72&m[b]wags=yes&\
m[k:cat]name=Katie&m[k:cat]age=12&m[cat]wags=yes",
// ...parses as this struct:
MyForm {
m: map! {
Person { name: "Alice".into(), age: 40 } => Pet { wags: false },
Person { name: "Bob".into(), age: 72 } => Pet { wags: true },
Person { name: "Katie".into(), age: 12 } => Pet { wags: true },
}
}
Arbitrary Collections
Any collection can be expressed with any level of arbitrary nesting, maps, and sequences. Consider the extravagantly contrived type:
任何集合都可以用任何级别的任意嵌套、映射和序列来表达。考虑一下更好地构思类型:
use std::collections::{BTreeMap, HashMap};
#[derive(FromForm, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Person {
name: String,
age: usize
}
HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>
|-[k:$k1]-----------|------|------| |-[$k1]-----------------|
|---[$i]-------|------|------| |-[k:$j]*|
|-[k:$k2]|------| ~~[$j]~~|name*|
|-name*| ~~[$j]~~|age-*|
|-age*-|
|~~~~~~~~~~~~~~~|v:$k2*|
Warning: The
BTreeMap
key type, herePerson
, must implementOrd
.As illustrated above with
*
marking terminals, we need the following form fields for this structure:
[k:$k1][$i][k:$k2]name
- string[k:$k1][$i][k:$k2]age
- usize[k:$k1][$i][$k2]
- usize[$k1][k:$j]
- usize[$k1][$j]name
- string[$k1][$j]age
- stringWhere we have the following symbolic keys:
$k1
: symbolic name of the top-level key$i
: symbolic name of the vector index$k2
: symbolic name of the sub-level (BTreeMap
) key$j
: symbolic name and/or value top-level value’s key
警告:BTreeMap的键类型,这里是Person,必须实现Ord。
正如上面用*标记的终端所说明的那样,我们需要这个结构的以下表格字段:
[k:$k1][$i][k:$k2]name
- string[k:$k1][$i][k:$k2]age
- usize[k:$k1][$i][$k2]
- usize[$k1][k:$j]
- usize[$k1][$j]name
- string[$k1][$j]age
- string
其中我们有以下符号键:
$k1:顶层键的符号名称
$i: 矢量索引的符号名称
$k2:子级(BTreeMap)键的符号名称
$j: 符号名称和/或顶层值的键值
type Foo = HashMap<Vec<BTreeMap<Person, usize>>, HashMap<usize, Person>>;
// This (long, contrived) form string...
"[k:top_key][i][k:sub_key]name=Bobert&\
[k:top_key][i][k:sub_key]age=22&\
[k:top_key][i][sub_key]=1337&\
[top_key][7]name=Builder&\
[top_key][7]age=99",
// We could also set the top-level value's key explicitly:
// [top_key][k:7]=7
// ...parses as this (long, contrived) map:
map! {
vec![bmap! {
Person { name: "Bobert".into(), age: 22 } => 1337usize,
}]
=>
map! {
7usize => Person { name: "Builder".into(), age: 99 }
}
}
Context
The
Contextual
form guard acts as a proxy for any other form guard, recording all submitted form values and produced errors and associating them with their corresponding field name.Contextual
is particularly useful for rendering forms with previously submitted values and errors associated with form input.
Contextual表单防护作为任何其他表单防护的代理,记录所有提交的表单值和产生的错误,并将它们与相应的字段名相关联。Contextual对于渲染具有先前提交的值和与表单输入相关的错误的表单特别有用。
To retrieve the context for a form, use
Form<Contextual<'_, T>>
as a data guard, whereT
implementsFromForm
. Thecontext
field contains the form’sContext
:
为了检索表单的上下文,使用Form<Contextual<'_, T>>作为数据守护,其中T实现了FromForm。上下文字段包含表单的Context:
use rocket::form::{Form, Contextual};
// 定义一个 POST 路由处理函数,使用 Form 和 Contextual。
#[post("/submit", data = "<form>")]
fn submit(form: Form<Contextual<'_, T>>) {
// 判断表单是否成功解析
if let Some(ref value) = form.value {
// 表单解析成功,`value` 是 `T` 类型的值。
}
// 可以检索原始字段值和错误
let raw_id_value = form.context.field_value("id");
let id_errors = form.context.field_errors("id");
}
Context
is nesting-aware for errors. WhenContext
is queried for errors for a field namedfoo.bar
, it returns errors for fields that are a prefix offoo.bar
, namelyfoo
andfoo.bar
. Similarly, if queried for errors for a field namedfoo.bar.baz
, errors for fieldfoo
,foo.bar
, andfoo.bar.baz
will be returned.
Context
serializes as a map, so it can be rendered in templates that requireSerialize
types. SeeContext
for details about its serialization format. The forms example, too, makes use of form contexts, as well as every other forms feature.
Context对错误是有嵌套意识的。当Context被查询名为foo.bar的字段的错误时,它会返回foo.bar的前缀字段的错误,即foo和foo.bar。同样地,如果查询名为foo.bar.baz的字段的错误,将返回字段foo、foo.bar和foo.bar.baz的错误。
Context被序列化为一个map,所以它可以在需要Serialize类型的模板中被渲染。关于其序列化格式的细节,请参见Context。表单的例子也使用了表单上下文,以及其他所有的表单特性。
Query Strings
Query strings are URL-encoded forms that appear in the URL of a request. Query parameters are declared like path parameters but otherwise handled like regular URL-encoded form fields. The table below summarizes the analogy:
查询字符串是在请求的URL中出现的URL编码的形式。查询参数的声明与路径参数类似,但在其他方面的处理与普通的URL编码的表格字段类似。下表总结了这种类似的情况:
Path Syntax | Query Syntax | Path Type Bound | Query Type Bound |
---|---|---|---|
<param> | <param> | FromParam | FromForm |
<param..> | <param..> | FromSegments | FromForm |
static | static | N/A | N/A |
Because dynamic parameters are form types, they can be single values, collections, nested collections, or anything in between, just like any other form field.
因为动态参数是表单类型,它们可以是单值、集合、嵌套集合,或介于两者之间的任何东西,就像其他表单字段一样。
Static Parameters
A request matches a route iff its query string contains all of the static parameters in the route’s query string. A route with a static parameter
param
(any UTF-8 text string) in a query will only match requests with that exact path segment in its query string.
如果一个请求的查询字符串包含路由的查询字符串中的所有静态参数,则该请求与路由匹配。在查询中带有静态参数参数(任何UTF-8文本字符串)的路由将只匹配其查询字符串中带有该确切路径段的请求。
Note: This is truly an iff!
Only the static parameters in query route string affect routing. Dynamic parameters are allowed to be missing by default.
注意:这确实是一个iff!
只有查询路由字符串中的静态参数影响路由。动态参数默认是允许缺失的。
For example, the route below will match requests with path
/
and at least the query segmentshello
andcat=♥
:
例如,下面的路由将匹配具有路径/和至少是查询段hello和cat=♥的请求:
#[get("/?hello&cat=♥")]
fn cats() -> &'static str {
"Hello, kittens!"
}
// The following GET requests match `cats`. `%E2%99%A5` is encoded `♥`.
"/?cat=%E2%99%A5&hello"
"/?hello&cat=%E2%99%A5"
"/?dogs=amazing&hello&there&cat=%E2%99%A5"
Dynamic Parameters
的单个动态参数与声明为param的表单字段的作用相同。特别是,Rocket将期望查询表单包含一个键为param的字段,并将移位的字段推至param类型。与表单一样,当解析失败时,会使用默认值。下面的例子说明了这一点,有一个单一的值名称,一个集合的颜色,一个嵌套的表单人,和一个其他的值,将默认为无:A single dynamic parameter of
<param>
acts identically to a form field declared asparam
. In particular, Rocket will expect the query form to contain a field with keyparam
and push the shifted field to theparam
type. As with forms, default values are used when parsing fails. The example below illustrates this with a single valuename
, a collectioncolor
, a nested formperson
, and another
value that will default toNone
:
#[derive(Debug, PartialEq, FromFormField)]
enum Color {
Red,
Blue,
Green
}
#[derive(Debug, PartialEq, FromForm)]
struct Pet<'r> {
name: &'r str,
age: usize,
}
#[derive(Debug, PartialEq, FromForm)]
struct Person<'r> {
pet: Pet<'r>,
}
#[get("/?<name>&<color>&<person>&<other>")]
fn hello(name: &str, color: Vec<Color>, person: Person<'_>, other: Option<usize>) {
assert_eq!(name, "George");
assert_eq!(color, [Color::Red, Color::Green, Color::Green, Color::Blue]);
assert_eq!(other, None);
assert_eq!(person, Person {
pet: Pet { name: "Fi Fo Alex", age: 1 }
});
}
// A request with these query segments matches as above.
name=George&\
color=red&\
color=green&\
person.pet.name=Fi+Fo+Alex&\
color=green&\
person.pet.age=1&\
color=blue&\
extra=yes\
Note that, like forms, parsing is field-ordering insensitive and lenient by default
请注意,与表单一样,解析对字段排序不敏感,默认情况下是宽松的。
Trailing Parameter
A trailing dynamic parameter of
<param..>
collects all of the query segments that don’t otherwise match a declared static or dynamic parameter. In other words, the otherwise unmatched segments are pushed, unshifted, to the<param..>
type:
<param…>的尾部动态参数收集了所有不匹配的静态或动态参数的查询段。换句话说,原本不匹配的片段被推到<param…>类型中,没有移位:
use rocket::form::Form;
#[derive(FromForm)]
struct User<'r> {
name: &'r str,
active: bool,
}
#[get("/?hello&<id>&<user..>")]
fn user(id: usize, user: User<'_>) {
assert_eq!(id, 1337);
assert_eq!(user.name, "Bob Smith");
assert_eq!(user.active, true);
}
// A request with these query segments matches as above.
hello&\
name=Bob+Smith&\
id=1337&\
active=yes\
Error Catchers
Application processing is fallible. Errors arise from the following sources:
- A failing guard.
- A failing responder.
- A routing failure.
If any of these occur, Rocket returns an error to the client. To generate the error, Rocket invokes the catcher corresponding to the error’s status code and scope. Catchers are similar to routes except in that:
- Catchers are only invoked on error conditions.
- Catchers are declared with the
catch
attribute.- Catchers are registered with
register()
instead ofmount()
.- Any modifications to cookies are cleared before a catcher is invoked.
- Error catchers cannot invoke guards.
- Error catchers should not fail to produce a response.
- Catchers are scoped to a path prefix.
应用程序的处理是有缺陷的。错误来自于以下几个方面:
失败的守护者。
一个失败的响应者。
路由失败。
如果其中任何一个发生,Rocket会向客户端返回一个错误。为了产生错误,Rocket调用了与错误的状态代码和范围相对应的捕获器。捕集器与路由类似,除了以下几点:
1.捕集器只在错误条件下被调用。
2.捕集器是用catch属性声明的。
3.捕手用 register() 而不是 mount() 注册。
4.对cookie的任何修改都会在catcher被调用之前被清除掉。
5.错误捕捉器不能调用守卫。
6.错误捕获器不应该无法产生响应。
7.捕集器的范围是路径前缀。
To declare a catcher for a given status code, use the
catch
attribute, which takes a single integer corresponding to the HTTP status code to catch. For instance, to declare a catcher for404 Not Found
errors, you’d write:
要为一个给定的状态代码声明一个捕捉器,请使用catch属性,它需要一个对应于HTTP状态代码的整数来捕捉。例如,要为404 Not Found错误声明一个捕捉器,你可以这样写:
use rocket::Request;
#[catch(404)]
fn not_found(req: &Request) { /* .. */ }
Catchers may take zero, one, or two arguments. If the catcher takes one argument, it must be of type
&Request
. It it takes two, they must be of typeStatus
and&Request
, in that order. As with routes, the return type must implementResponder
. A concrete implementation may look like:
捕手可以接受零、一或两个参数。如果捕手需要一个参数,它必须是&Request类型。如果它需要两个参数,它们必须是Status和&Request类型,按顺序排列。与路由一样,返回类型必须实现 Responder。一个具体的实现可能看起来像:
#[catch(404)]
fn not_found(req: &Request) -> String {
format!("Sorry, '{}' is not a valid path.", req.uri())
}
Also as with routes, Rocket needs to know about a catcher before it is used to handle errors. The process, known as “registering” a catcher, is similar to mounting a route: call the
register()
method with a list of catchers via thecatchers!
macro. The invocation to add the 404 catcher declared above looks like:
和路由一样,Rocket在处理错误之前需要知道捕手的情况。这个过程被称为 "注册 "捕手,与安装路由类似:通过捕手!宏调用register()方法和捕手的列表。添加上面声明的404捕手的调用看起来像:
fn main() {
rocket::build().register("/", catchers![not_found]);
}
Scoping
The first argument to
register()
is a path to scope the catcher under called the catcher’s base. A catcher’s base determines which requests it will handle errors for. Specifically, a catcher’s base must be a prefix of the erroring request for it to be invoked. When multiple catchers can be invoked, the catcher with the longest base takes precedence.
register()的第一个参数是捕捉器的范围路径,称为捕捉器的基础。捕集器的基础决定了它将为哪些请求处理错误。具体来说,捕手的基础必须是出错请求的前缀,才能被调用。当多个捕手可以被调用时,具有最长基数的捕手优先。
#[catch(404)]
fn general_not_found() -> &'static str {
"General 404"
}
#[catch(404)]
fn foo_not_found() -> &'static str {
"Foo 404"
}
#[launch]
fn rocket() -> _ {
rocket::build()
.register("/", catchers![general_not_found])
.register("/foo", catchers![foo_not_found])
}
Since there are no mounted routes, all requests will
404
. Any request whose path begins with/foo
(i.e,GET /foo
,GET /foo/bar
, etc) will be handled by thefoo_not_found
catcher while all other requests will be handled by thegeneral_not_found
catcher.
由于没有装入的路由,所有的请求都是404。任何路径以/foo开头的请求(例如,GET /foo,GET /foo/bar,等等)将由foo_not_found捕手处理,而所有其他请求将由General_not_found捕手处理。
Default Catchers
A default catcher is a catcher that handles all status codes. They are invoked as a fallback if no status-specific catcher is registered for a given error. Declaring a default catcher is done with
#[catch(default)]
and must similarly be registered withregister()
:
默认捕获器是一个处理所有状态代码的捕获器。如果没有为某一特定错误注册特定的状态捕捉器,它们会被调用作为后备手段。用#[catch(default)]来声明默认捕手,同样也必须用register()来注册:
use rocket::Request;
use rocket::http::Status;
#[catch(default)]
fn default_catcher(status: Status, request: &Request) { /* .. */ }
#[launch]
fn rocket() -> _ {
rocket::build().register("/", catchers![default_catcher])
}
Catchers with longer bases are preferred, even when there is a status-specific catcher. In other words, a default catcher with a longer matching base than a status-specific catcher takes precedence.
即使有一个特定身份的捕手,也会优先考虑有较长基数的捕手。换句话说,与特定身份的捕手相比,默认的捕手具有更长的匹配基数,因此优先考虑。
Built-In Catcher
Rocket provides a built-in default catcher. It produces HTML or JSON, depending on the value of the
Accept
header. As such, custom catchers only need to be registered for custom error handling.
Rocket提供了一个内置的默认捕捉器。它产生HTML或JSON,取决于接受头的值。因此,自定义捕捉器只需要为自定义错误处理注册。
The error handling example illustrates catcher use in full, while the
Catcher
API documentation provides further details.
错误处理的例子充分说明了捕手的使用,而捕手API文档提供了进一步的细节。
Responses
You may have noticed that the return type of a handler appears to be arbitrary, and that’s because it is! A value of any type that implements the
Responder
trait can be returned, including your own. In this section, we describe theResponder
trait as well as several usefulResponder
s provided by Rocket. We’ll also briefly discuss how to implement your ownResponder
.
你可能已经注意到,处理程序的返回类型似乎是任意的,这是因为它是任意的!任何类型的值都可以返回,包括你自己的!任何实现了Responder trait的类型的值都可以被返回,包括你自己的。在本节中,我们将描述Responder trait以及Rocket提供的几个有用的Responder。我们还将简要地讨论如何实现你自己的Responder。
Responder
Types that implement
Responder
know how to generate aResponse
from their values. AResponse
includes an HTTP status, headers, and body. The body may either be fixed-sized or streaming. The givenResponder
implementation decides which to use. For instance,String
uses a fixed-sized body, whileFile
uses a streamed response. Responders may dynamically adjust their responses according to the incomingRequest
they are responding to.
实现Responder的类型知道如何从它们的值中生成一个Response。一个Response包括HTTP状态、头信息和正文。主体可以是固定大小的,也可以是流式的。给定的 Responder 实现决定使用哪一种。例如,String使用固定大小的主体,而File使用流式响应。响应者可以根据他们所响应的传入请求动态地调整他们的响应。
Wrapping
Before we describe a few responders, we note that it is typical for responders to wrap other responders. That is, responders can be of the following form, where
R
is some type that implementsResponder
:
在我们描述几个应答器之前,我们注意到,应答器包裹其他应答器是很典型的。也就是说,响应者可以是以下形式,其中R是一些实现了响应者的类型:
struct WrappingResponder<R>(R);
A wrapping responder modifies the response returned by
R
before responding with that same response. For instance, Rocket providesResponder
s in thestatus
module that override the status code of the wrappedResponder
. As an example, theAccepted
type sets the status to202 - Accepted
. It can be used as follows:
包装响应器在用相同的响应进行回应之前,会修改由R返回的响应。例如,Rocket在状态模块中提供了Responders,覆盖了包装Responder的状态代码。作为一个例子,Accepted类型将状态设置为202 - Accepted。它可以如下使用:
use rocket::response::status;
#[post("/<id>")]
fn new(id: usize) -> status::Accepted<String> {
status::Accepted(Some(format!("id: '{}'", id)))
}
Similarly, the types in the
content
module can be used to override the Content-Type of a response. For instance, to set the Content-Type of&'static str
to JSON, as well as setting the status code to an arbitrary one like418 I'm a teapot
, combine [content::RawJson
] withstatus::Custom
:
同样地,内容模块中的类型可以用来覆盖响应的Content-Type。例如,要将&'static str的Content-Type设置为JSON,以及将状态代码设置为任意的代码,如418 I’m a teapot,请将[content::RawJson]与status::Custom相结合:
use rocket::http::Status;
use rocket::response::{content, status};
#[get("/")]
fn json() -> status::Custom<content::RawJson<&'static str>> {
status::Custom(Status::ImATeapot, content::RawJson("{ \"hi\": \"world\" }"))
}
Warning: This is not the same as
serde::json::Json
!
警告:这与serde::json::Json不一样!
The built-in
(Status, R)
and(ContentType, R)
responders, whereR: Responder
, also override theStatus
andContent-Type
of responses, respectively:
内置的(Status, R)和(ContentType, R)响应器,其中R:响应者,也分别覆盖了响应的状态和内容类型:
use rocket::http::{Status, ContentType};
#[get("/")]
fn json() -> (Status, (ContentType, &'static str)) {
(Status::ImATeapot, (ContentType::JSON, "{ \"hi\": \"world\" }"))
}
For pithy reusability, it is advisable to derive a custom responder:
为了节省时间,最好是派生出一个自定义的响应者:
#[derive(Responder)]
#[response(status = 418, content_type = "json")]
struct RawTeapotJson(&'static str);
#[get("/")]
fn json() -> RawTeapotJson {
RawTeapotJson("{ \"hi\": \"world\" }")
}
Errors
Responders may fail instead of generating a response by returning an
Err
with a status code. When this happens, Rocket forwards the request to the error catcher for that status code.If an error catcher has been registered for the given status code, Rocket will invoke it. The catcher creates and returns a response to the client. If no error catcher has been registered and the error status code is one of the standard HTTP status code, a default error catcher will be used. Default error catchers return an HTML page with the status code and description. If there is no catcher for a custom status code, Rocket uses the 500 error catcher to return a response.
响应者可能会失败,而不是通过返回一个带有状态代码的Err来生成一个响应。当这种情况发生时,Rocket将请求转发给该状态代码的错误捕捉器。
如果一个错误捕捉器已经为给定的状态代码注册了,Rocket将调用它。捕捉器会创建并返回一个响应给客户端。如果没有注册错误捕捉器,并且错误状态代码是标准的HTTP状态代码之一,将使用一个默认的错误捕捉器。默认的错误捕捉器会返回一个带有状态代码和描述的HTML页面。如果没有自定义状态代码的捕捉器,Rocket使用500错误捕捉器来返回响应。
Status
While not encouraged, you can also forward a request to a catcher manually by returning a
Status
directly. For instance, to forward to the catcher for 406: Not Acceptable, you would write:
虽然不鼓励这样做,但你也可以通过直接返回状态来手动转发一个请求给捕获器。例如,要转发406:不可接受的请求给接收者,你可以写:
use rocket::http::Status;
#[get("/")]
fn just_fail() -> Status {
Status::NotAcceptable
}
The response generated by
Status
depends on the status code itself. As indicated above, for error status codes (in range [400, 599]),Status
forwards to the corresponding error catcher. The table below summarizes responses generated byStatus
for these and other codes:
Status产生的响应取决于状态代码本身。如上所述,对于错误状态代码(范围[400, 599]),Status转发到相应的错误捕获器。下表总结了Status对这些和其他代码产生的响应:
Status Code Range | Response |
---|---|
[400, 599] | Forwards to catcher for given status. |
100, [200, 205] | Empty with given status. |
All others. | Invalid. Errors to 500 catcher. |
Custom Responders
The
Responder
trait documentation details how to implement your own custom responders by explicitly implementing the trait. For most use cases, however, Rocket makes it possible to automatically derive an implementation ofResponder
. In particular, if your custom responder wraps an existing responder, headers, or sets a custom status or content-type,Responder
can be automatically derived:
Responder特质文档详细介绍了如何通过明确实现该特质来实现你自己的自定义响应器。然而,对于大多数用例来说,Rocket使得自动派生出一个Responder的实现成为可能。特别是,如果你的自定义应答器包装了一个现有的应答器、头文件,或者设置了一个自定义的状态或内容类型,应答器可以被自动派生:
use rocket::http::{Header, ContentType};
#[derive(Responder)]
#[response(status = 500, content_type = "json")]
struct MyResponder {
inner: OtherResponder,
header: ContentType,
more: Header<'static>,
#[response(ignore)]
unrelated: MyType,
}
For the example above, Rocket generates a
Responder
implementation that:
- Set the response’s status to
500: Internal Server Error
.- Sets the Content-Type to
application/json
.- Adds the headers
self.header
andself.more
to the response.- Completes the response using
self.inner
.Note that the first field is used as the inner responder while all remaining fields (unless ignored with
#[response(ignore)]
) are added as headers to the response. The optional#[response]
attribute can be used to customize the status and content-type of the response. BecauseContentType
andStatus
are themselves headers, you can also dynamically set the content-type and status by simply including fields of these types.For more on using the
Responder
derive, see theResponder
derive documentation.
对于上面的例子,Rocket生成了一个响应器的实现,即:
将响应的状态设置为500:内部服务器错误。
设置Content-Type为application/json。
在响应中添加头信息self.header和self.more。
使用self.inner完成响应。
注意,第一个字段被用作内部响应,而所有其余的字段(除非用#[response(ignore)]忽略)被作为头文件添加到响应中。可选的#[response]属性可以用来定制响应的状态和内容类型。因为ContentType和Status本身就是头信息,你也可以通过简单地包括这些类型的字段来动态地设置内容类型和状态。
关于使用Responder derive的更多信息,请看Responder derive文档。
Implementations
Rocket implements
Responder
for many types in Rust’s standard library includingString
,&str
,File
,Option
, andResult
. TheResponder
documentation describes these in detail, but we briefly cover a few here.
Rocket为Rust标准库中的许多类型实现了Responder,包括String, &str, File, Option, 和Result。Responder文档对这些类型进行了详细描述,但我们在这里简单介绍一下。
Strings
The
Responder
implementations for&str
andString
are straight-forward: the string is used as a sized body, and the Content-Type of the response is set totext/plain
. To get a taste for what such aResponder
implementation looks like, here’s the implementation forString
:
对&str和String的Responder实现是直截了当的:字符串被用作大小体,而响应的Content-Type被设置为text/plain。为了了解这样的Responder实现是什么样子的,这里是String的实现:
use std::io::Cursor;
use rocket::request::Request;
use rocket::response::{self, Response, Responder};
use rocket::http::ContentType;
#[rocket::async_trait]
impl<'r> Responder<'r, 'static> for String {
fn respond_to(self, _: &'r Request<'_>) -> response::Result<'static> {
Response::build()
.header(ContentType::Plain)
.sized_body(self.len(), Cursor::new(self))
.ok()
}
}
Because of these implementations, you can directly return an
&str
orString
type from a handler:
因为这些实现,你可以直接从处理程序中返回一个&str或String类型:
#[get("/string")]
fn handler() -> &'static str {
"Hello there! I'm a string!"
}
Option
Option
is a wrapping responder: anOption<T>
can only be returned whenT
implementsResponder
. If theOption
isSome
, the wrapped responder is used to respond to the client. Otherwise, a error of 404 - Not Found is returned to the client.This implementation makes
Option
a convenient type to return when it is not known until process-time whether content exists. For example, because ofOption
, we can implement a file server that returns a200
when a file is found and a404
when a file is not found in just 4, idiomatic lines:
Option是一个封装的应答器:只有当T实现了应答器时才能返回Option。如果Option是Some,包装好的应答器将被用于响应客户端。否则,就会向客户端返回404 - Not Found的错误。
这个实现使得Option成为一个方便的类型,当在处理时间之前不知道内容是否存在时,可以返回。例如,由于Option的存在,我们可以实现一个文件服务器,当找到一个文件时返回200,当没有找到一个文件时返回404,只需4行就可以实现:
use rocket::fs::NamedFile;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).await.ok()
}
Result
Result
is another wrapping responder: aResult<T, E>
can only be returned whenT
implementsResponder
andE
implementsResponder
.The wrapped
Responder
inOk
orErr
, whichever it might be, is used to respond to the client. This means that the responder can be chosen dynamically at run-time, and two different kinds of responses can be used depending on the circumstances. Revisiting our file server, for instance, we might wish to provide more feedback to the user when a file isn’t found. We might do this as follows:
Result是另一个封装的响应器:只有当T实现了响应器和E实现了响应器时,才能返回一个Result<T, E>。
Ok或Err中被包装的Responder,无论它是什么,都被用来响应客户端。这意味着可以在运行时动态地选择响应者,并且可以根据情况使用两种不同的响应。例如,重新审视我们的文件服务器,我们可能希望在没有找到文件时向用户提供更多的反馈。我们可以这样做:
use rocket::fs::NamedFile;
use rocket::response::status::NotFound;
#[get("/<file..>")]
async fn files(file: PathBuf) -> Result<NamedFile, NotFound<String>> {
let path = Path::new("static/").join(file);
NamedFile::open(&path).await.map_err(|e| NotFound(e.to_string()))
}
Rocket Responders
Some of Rocket’s best features are implemented through responders. Among these are:
NamedFile
- Streams a file to the client; automatically sets the Content-Type based on the file’s extension.Redirect
- Redirects the client to a different URI.content
- Contains types that override the Content-Type of a response.status
- Contains types that override the status code of a response.Flash
- Sets a “flash” cookie that is removed when accessed.Json
- Automatically serializes values into JSON.MsgPack
- Automatically serializes values into MessagePack.Template
- Renders a dynamic template using handlebars or Tera.
Rocket的一些最佳功能是通过响应者实现的。其中包括:
NamedFile - 将一个文件流向客户端;根据文件的扩展名自动设置内容类型。
Redirect - 将客户端重定向到一个不同的URI。
content - 包含覆盖响应的Content-Type的类型。
status - 包含覆盖响应的状态代码的类型。
Flash - 设置一个 "flash "cookie,在访问时被删除。
Json - 自动将值序列化为JSON。
MsgPack - 自动将值序列化为MessagePack。
Template - 使用handlebars或Tera渲染一个动态模板。
Async Streams
The
stream
responders allow serving potentially infinite asyncStream
s. A stream can be created from any asyncStream
orAsyncRead
type, or via generator syntax using thestream!
macro and its typed equivalents. Streams are the building blocks for unidirectional real-time communication. For instance, thechat
example uses anEventStream
to implement a real-time, multi-room chat application using Server-Sent Events (SSE).
流响应器允许服务于潜在的无限的异步流。一个流可以从任何异步流或异步读类型中创建,或者通过生成器语法使用stream!宏及其类型化的等价物。流是单向实时通信的构建块。例如,聊天示例使用EventStream来实现一个实时的、多房间的聊天应用程序,使用服务器发送的事件(SSE)。
The simplest version creates a ReaderStream
from a single AsyncRead
type. For example, to stream from a TCP connection, we might write:
最简单的版本是从一个单一的AsyncRead类型创建一个ReaderStream。例如,为了从一个TCP连接中获取数据流,我们可以这样写:
use std::io;
use std::net::SocketAddr;
use rocket::tokio::net::TcpStream;
use rocket::response::stream::ReaderStream;
#[get("/stream")]
async fn stream() -> io::Result<ReaderStream![TcpStream]> {
let addr = SocketAddr::from(([127, 0, 0, 1], 9999));
let stream = TcpStream::connect(addr).await?;
Ok(ReaderStream::one(stream))
}
Streams can also be created using generator syntax. The following example returns an infinite
TextStream
that produces one"hello"
every second:
流也可以用生成器的语法来创建。下面的例子返回一个无限的TextStream,每秒产生一个 “hello”:
use rocket::tokio::time::{Duration, interval};
use rocket::response::stream::TextStream;
/// Produce an infinite series of `"hello"`s, one per second.
#[get("/infinite-hellos")]
fn hello() -> TextStream![&'static str] {
TextStream! {
let mut interval = interval(Duration::from_secs(1));
loop {
yield "hello";
interval.tick().await;
}
}
}
See the
stream
docs for full details on creating streams including notes on how to detect and handle graceful shutdown requests.
关于创建流的全部细节,包括如何检测和处理优雅关机请求的说明,请参见流文档。
JSON
The
Json
responder in allows you to easily respond with well-formed JSON data: simply return a value of typeJson<T>
whereT
is the type of a structure to serialize into JSON. The typeT
must implement theSerialize
trait fromserde
, which can be automatically derived.
Json响应器允许你轻松地响应格式良好的JSON数据:只需返回一个Json类型的值,其中T是要序列化为JSON的结构类型。类型T必须实现Serde的Serialize特性,它可以被自动派生。
As an example, to respond with the JSON value of a
Task
structure, we might write:
作为一个例子,为了响应一个任务结构的JSON值,我们可以写:
use rocket::serde::{Serialize, json::Json};
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
struct Task { /* .. */ }
#[get("/todo")]
fn todo() -> Json<Task> {
Json(Task { /* .. */ })
}
The
Json
type serializes the structure into JSON, sets the Content-Type to JSON, and emits the serialized data in a fixed-sized body. If serialization fails, a 500 - Internal Server Error is returned.
Json类型将结构序列化为JSON,将内容类型设置为JSON,并将序列化的数据排放在一个固定大小的主体中。如果序列化失败,将返回500 - 内部服务器错误。
Templates
Rocket has first-class templating support that works largely through a
Template
responder in therocket_dyn_templates
contrib library. To render a template named “index”, for instance, you might return a value of typeTemplate
as follows:
Rocket有一流的模板支持,主要是通过rocket_dyn_templates contrib库中的Template响应器工作。例如,要渲染一个名为 "index "的模板,你可以返回一个Template类型的值,如下所示:
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
let context = /* object-like value */;
Template::render("index", &context)
}
Templates are rendered with the
render
method. The method takes in the name of a template and a context to render the template with. The context can be any type that implementsSerialize
and serializes into anObject
value, such as structs,HashMaps
, and others.
模板是用render方法渲染的。该方法接收一个模板的名字和一个用于渲染模板的上下文。上下文可以是任何实现了Serialize并能序列化为对象值的类型,例如结构体、HashMaps和其他。
You can also use [
context!
] to create ad-hoc templating contexts without defining a new type:
use rocket_dyn_templates::Template;
#[get("/")]
fn index() -> Template {
Template::render("index", context! {
foo: 123,
})
}
For a template to be renderable, it must first be registered. The
Template
fairing automatically registers all discoverable templates when attached. The Fairings sections of the guide provides more information on fairings. To attach the template fairing, simply call.attach(Template::fairing())
on an instance ofRocket
as follows:
为了使一个模板能够被渲染,它必须首先被注册。模板整流罩在连接时自动注册所有可发现的模板。本指南的整流罩部分提供了关于整流罩的更多信息。要附加模板整流罩,只需在Rocket的一个实例上调用.attach(Template::fairing()),如下所示:
use rocket_dyn_templates::Template;
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![/* .. */])
.attach(Template::fairing())
}
Rocket discovers templates in the configurable
template_dir
directory. Templating support in Rocket is engine agnostic. The engine used to render a template depends on the template file’s extension. For example, if a file ends with.hbs
, Handlebars is used, while if a file ends with.tera
, Tera is used.
Rocket在可配置的template_dir目录中发现了模板。Rocket中的模板支持是与引擎无关的。用来渲染模板的引擎取决于模板文件的扩展名。例如,如果一个文件以.hbs结尾,则使用Handlebars,而如果一个文件以.tera结尾,则使用Tera。
Note: The name of the template does not include its extension.
For a template file named
index.html.tera
, callrender("index")
and use the name"index"
in templates, i.e,{% extends "index" %}
or{% extends "base" %}
forbase.html.tera
.
注意:模板的名称不包括其扩展名。
对于一个名为index.html.tera的模板文件,调用render(“index”)并在模板中使用 "index "这个名字,即{% extends “index” %}或{% extends “base” %}用于base.html.tera。
Live Reloading
When your application is compiled in
debug
mode (without the--release
flag passed tocargo
), templates are automatically reloaded when they are modified on supported platforms. This means that you don’t need to rebuild your application to observe template changes: simply refresh! In release builds, reloading is disabled.The
Template
API documentation contains more information about templates, including how to customize a template engine to add custom helpers and filters. The templating example uses both Tera and Handlebars templating to implement the same application.
实时重新加载
当你的应用程序在调试模式下编译时(没有传递给cargo的–release标志),模板在支持的平台上被修改时,会自动重新加载。这意味着你不需要重建你的应用程序来观察模板的变化:只需刷新即可!在发布版本中,重载被禁用。
模板API文档包含了更多关于模板的信息,包括如何定制模板引擎以添加自定义的帮助器和过滤器。模板的例子同时使用Tera和Handlebars模板来实现同一个应用程序。
Typed URIs
Rocket’s
uri!
macro allows you to build URIs to routes in your application in a robust, type-safe, and URI-safe manner. Type or route parameter mismatches are caught at compile-time, and changes to route URIs are automatically reflected in the generated URIs.The
uri!
macro returns anOrigin
structure with the URI of the supplied route interpolated with the given values. Each value passed intouri!
is rendered in its appropriate place in the URI using theUriDisplay
implementation for the value’s type. TheUriDisplay
implementation ensures that the rendered value is URI-safe.Note that
Origin
implementsInto<Uri>
(and by extension,TryInto<Uri>
), so it can be converted into aUri
using.into()
as needed and passed into methods such asRedirect::to()
.
Rocket的uri!宏允许你在你的应用程序中以一种强大的、类型安全的和URI安全的方式建立URI到路由。类型或路由参数不匹配会在编译时被捕获,路由URI的变化会自动反映在生成的URI中。
uri!宏返回一个Origin结构,其中包含了用给定值插值的路由URI。每个传入 uri! 的值都会在URI中的适当位置使用UriDisplay实现来呈现该值的类型。UriDisplay 实现确保渲染的值是URI安全的。
请注意,Origin 实现了 Into (以及延伸的 TryInto),所以它可以根据需要使用 .into() 转换为 Uri,并传入 Redirect::to() 等方法。
#[get("/<id>/<name>?<age>")]
fn person(id: Option<usize>, name: &str, age: Option<u8>) { /* .. */ }
URIs to
person
can be created as follows:
对person
的URI可以按以下方式创建:
// with unnamed parameters, in route path declaration order
let mike = uri!(person(101, "Mike Smith", Some(28)));
assert_eq!(mike.to_string(), "/101/Mike%20Smith?age=28");
// with named parameters, order irrelevant
let mike = uri!(person(name = "Mike", id = 101, age = Some(28)));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
let mike = uri!(person(id = 101, age = Some(28), name = "Mike"));
assert_eq!(mike.to_string(), "/101/Mike?age=28");
// with a specific mount-point
let mike = uri!("/api", person(id = 101, name = "Mike", age = Some(28)));
assert_eq!(mike.to_string(), "/api/101/Mike?age=28");
// with optional (defaultable) query parameters ignored
let mike = uri!(person(101, "Mike", _));
assert_eq!(mike.to_string(), "/101/Mike");
let mike = uri!(person(id = 101, name = "Mike", age = _));
assert_eq!(mike.to_string(), "/101/Mike");
Rocket informs you of any mismatched parameters at compile-time:
Rocket在编译时通知你任何不匹配的参数:
error: `person` route uri expects 3 parameters but 1 was supplied
--> examples/uri/main.rs:7:26
|
7 | let x = uri!(person("Mike Smith"));
| ^^^^^^^^^^^^
|
= note: expected parameters: id: Option <usize>, name: &str, age: Option <u8>
Rocket also informs you of any type errors at compile-time:
Rocket也会在编译时通知你任何类型错误:
--> examples/uri/src/main.rs:7:31
|
7 | let x = uri!(person(id = "10", name = "Mike Smith", age = Some(10)));
| ^^^^ `FromUriParam<Path, &str>` is not implemented for `usize`
We recommend that you use
uri!
exclusively when constructing URIs to your routes.
我们建议你在构建路由的URI时只使用uri!
Ignorables
As illustrated in the previous above, query parameters can be ignored using
_
in place of an expression in auri!
invocation. The corresponding type in the route URI must implementIgnorable
. Ignored parameters are not interpolated into the resultingOrigin
. Path parameters are not ignorable.
如上文所示,查询参数可以用_代替uri!调用中的表达式而被忽略。路由URI中的相应类型必须实现Ignorable。忽略的参数不会被插进结果的Origin中。路径参数不是可忽略的。
Deriving UriDisplay
衍生出UriDisplay
The
UriDisplay
trait can be derived for custom types. For types that appear in the path part of a URI, derive usingUriDisplayPath
; for types that appear in the query part of a URI, derive usingUriDisplayQuery
.
UriDisplay 特质可以被派生为自定义类型。对于出现在URI的路径部分的类型,使用UriDisplayPath派生;对于出现在URI的查询部分的类型,使用UriDisplayQuery派生。
As an example, consider the following form structure and route:
作为一个例子,考虑下面的表格结构和路线:
use rocket::form::Form;
#[derive(FromForm, UriDisplayQuery)]
struct UserDetails<'r> {
age: Option<usize>,
nickname: &'r str,
}
#[post("/user/<id>?<details..>")]
fn add_user(id: usize, details: UserDetails) { /* .. */ }
Typed URI Parts
The
Part
trait categorizes types that mark a part of the URI as either aPath
or aQuery
. Said another way, types that implementPart
are marker types that represent a part of a URI at the type-level. Traits such asUriDisplay
andFromUriParam
bound a generic parameter byPart
:P: Part
. This creates two instances of each trait:UriDisplay<Query>
andUriDisplay<Path>
, andFromUriParam<Query>
andFromUriParam<Path>
.
Part特性对标记URI的一部分为路径或查询的类型进行分类。换句话说,实现Part的类型是在类型级别上代表URI的一部分的标记类型。诸如UriDisplay和FromUriParam这样的特质通过Part绑定一个通用参数:P: Part。这就为每个特质创建了两个实例:UriDisplay 和 UriDisplay
As the names might imply, the
Path
version of the traits is used when displaying parameters in the path part of the URI while theQuery
version is used when displaying parameters in the query part of the URI. These distinct versions of the traits exist exactly to differentiate, at the type-level, where in the URI a value is to be written to, allowing for type safety in the face of differences between the two locations. For example, while it is valid to use a value ofNone
in the query part, omitting the parameter entirely, doing so is not valid in the path part. By differentiating in the type system, both of these conditions can be enforced appropriately through distinct implementations ofFromUriParam<Path>
andFromUriParam<Query>
.
正如名字所暗示的那样,当在URI的路径部分显示参数时,使用Path版本的特征,而当在URI的查询部分显示参数时,则使用Query版本。这些不同版本的特征的存在,正是为了在类型层面上区分URI中一个值的写入位置,在面对两个位置之间的差异时允许类型安全。例如,虽然在查询部分使用None的值是有效的,但完全省略参数,这样做在路径部分是无效的。通过在类型系统中进行区分,这两个条件可以通过FromUriParam
This division has an effect on how the
uri!
macro can be invoked. In query parts, for a route type ofOption<T>
, you must supply a type ofOption
,Result
, or an ignored_
to theuri!
invocation. By contrast, you cannot supply such a type in the path part. This ensures that a valid URI is always generated.
这种划分对如何调用 uri! 宏有影响。在查询部分,对于 Option 的路由类型,你必须向 uri!的调用提供 Option、Result 或忽略的 _ 的类型。相比之下,你不能在路径部分提供这种类型。这确保了有效的URI总是被生成。
Conversions
FromUriParam
is used to perform a conversion for each value passed touri!
before it is displayed withUriDisplay
. If aT: FromUriParam<P, S>
implementation exists for a typeT
for part URI partP
, then a value of typeS
can be used inuri!
macro for a route URI parameter declared with a type ofT
in partP
. For example, the following implementation, provided by Rocket, allows an&str
to be used in auri!
invocation for route URI parameters declared asString
:
FromUriParam 用于在用 UriDisplay 显示之前对传递给 uri! 的每个值进行转换。如果有一个T:FromUriParam<P, S>的实现存在于URI部分P的T类型中,那么S类型的值可以在URI!宏中用于P部分中以T类型声明的路由URI参数。例如,以下由Rocket提供的实现允许在URI!调用中使用&str,用于声明为String的路由URI参数:
impl<'a, P: Part> FromUriParam<P, &'a str> for String {
type Target = &'a str;
}
Other conversions to be aware of are:
&T
toT
&mut T
toT
String
to&str
&str
to&Path
&str
toPathBuf
T
toForm<T>
The following conversions only apply to path parts:
T
toOption<T>
T
toResult<T, E>
The following conversions are implemented only in query parts:
Option<T>
toResult<T, E>
(for anyE
)Result<T, E>
toOption<T>
(for anyE
)
这段话讲了几种类型之间的转换关系和特殊情况:
-
&T 可以转换成 T;
-
&mut T 可以转换成 T;
-
String 可以转换成 &str;
-
&str 可以转换成 &Path;
-
&str 可以转换成 PathBuf;
-
T 可以转换成 Form;
只有 path 部分可用的转换:
T 可以转换成 Option;
T 可以转换成 Result<T, E>;
只有 query 部分可用的转换:
Option 可以转换成 Result<T, E>;
Result<T, E> 可以转换成 Option;
这些转换可以是连续的,比如 &str 可以被用在期望 Option 类型的参数中。
Conversions are transitive. That is, a conversion from
A -> B
and a conversionB -> C
implies a conversion fromA -> C
. For instance, a value of type&str
can be supplied when a value of typeOption<PathBuf>
is expected:
转换是传递性的。也就是说,从A->B的转换和从B->C的转换意味着从A->C的转换。例如,当期望一个Option类型的值时,可以提供一个&str类型的值:
use std::path::PathBuf;
#[get("/person/<id>/<details..>")]
fn person(id: usize, details: Option<PathBuf>) { /* .. */ }
uri!(person(id = 100, details = "a/b/c"));
See the FromUriParam
documentation for further details.
更多细节请参见FromUriParam文档。
State
Many web applications have a need to maintain state. This can be as simple as maintaining a counter for the number of visits or as complex as needing to access job queues and multiple databases. Rocket provides the tools to enable these kinds of interactions in a safe and simple manner.
许多网络应用程序都需要维护状态。这可以简单到维护一个访问次数的计数器,也可以复杂到需要访问作业队列和多个数据库。Rocket提供的工具能够以安全和简单的方式实现这些类型的互动。
Managed State
The enabling feature for maintaining state is managed state. Managed state, as the name implies, is state that Rocket manages for your application. The state is managed on a per-type basis: Rocket will manage at most one value of a given type.
维护状态的启用功能是管理状态。管理状态,顾名思义,是Rocket为你的应用程序管理的状态。状态的管理是以每一个类型为基础的:Rocket将最多管理一个给定类型的值。
The process for using managed state is simple:
- Call
manage
on theRocket
instance corresponding to your application with the initial value of the state.- Add a
&State<T>
type to any request handler, whereT
is the type of the value passed intomanage
.
使用托管状态的过程很简单:
用状态的初始值在与你的应用程序相对应的Rocket实例上调用manage。
在任何请求处理程序中添加一个&State类型,其中T是传入manage的值的类型。
Note: All managed state must be thread-safe.
Because Rocket automatically multithreads your application, handlers can concurrently access managed state. As a result, managed state must be thread-safe. Thanks to Rust, this condition is checked at compile-time by ensuring that the type of values you store in managed state implement
Send
+Sync
.
注意:所有的托管状态必须是线程安全的。
因为Rocket会自动对你的应用程序进行多线程处理,处理程序可以并发地访问托管状态。因此,托管状态必须是线程安全的。多亏了Rust,这个条件在编译时就被检查了,确保你存储在托管状态中的值的类型实现了Send + Sync。
Adding State
To instruct Rocket to manage state for your application, call the
manage
method on an instance ofRocket
. For example, to ask Rocket to manage aHitCount
structure with an internalAtomicUsize
with an initial value of0
, we can write the following:
要指示Rocket为你的应用程序管理状态,请在Rocket的一个实例上调用manage方法。例如,要让Rocket管理一个初始值为0的内部AtomicUsize的HitCount结构,我们可以写如下:
use std::sync::atomic::AtomicUsize;
struct HitCount {
count: AtomicUsize
}
rocket::build().manage(HitCount { count: AtomicUsize::new(0) });
The
manage
method can be called any number of times as long as each call refers to a value of a different type. For instance, to have Rocket manage both aHitCount
value and aConfig
value, we can write:
管理方法可以被调用任意次数,只要每次调用都指向不同类型的值。例如,为了让Rocket同时管理一个HitCount值和一个Config值,我们可以这样写:
rocket::build()
.manage(HitCount { count: AtomicUsize::new(0) })
.manage(Config::from(user_input));
Retrieving State
State that is being managed by Rocket can be retrieved via the
&State
type: a request guard for managed state. To use the request guard, add a&State<T>
type to any request handler, whereT
is the type of the managed state. For example, we can retrieve and respond with the currentHitCount
in acount
route as follows:
被Rocket管理的状态可以通过&State类型进行检索:一个管理状态的请求保护。要使用请求保护,在任何请求处理程序中添加一个&State类型,其中T是被管理状态的类型。例如,我们可以在一个计数路由中检索并响应当前的HitCount,如下所示:
use rocket::State;
#[get("/count")]
fn count(hit_count: &State<HitCount>) -> String {
let current_count = hit_count.count.load(Ordering::Relaxed);
format!("Number of visits: {}", current_count)
}
You can retrieve more than one
&State
type in a single route as well:
你也可以在一个途径中检索一个以上的&State类型:
#[get("/state")]
fn state(hit_count: &State<HitCount>, config: &State<Config>) { /* .. */ }
Warning
If you request a
&State<T>
for aT
that is notmanaged
, Rocket will refuse to start your application. This prevents what would have been an unmanaged state runtime error. Unmanaged state is detected at runtime through sentinels, so there are limitations. If a limitation is hit, Rocket still won’t call the offending route. Instead, Rocket will log an error message and return a 500 error to the client.
警告
如果你为一个没有被管理的T请求一个&State,Rocket将拒绝启动你的应用程序。这就避免了本来会出现的非管理状态的运行时错误。未被管理的状态是在运行时通过哨兵检测出来的,所以有一些限制。如果遇到了限制,Rocket仍然不会调用违规的路由。相反,Rocket会记录一个错误信息,并向客户端返回一个500错误。
You can find a complete example using the
HitCount
structure in the state example on GitHub and learn more about themanage
method andState
type in the API docs.
你可以在GitHub上的状态示例中找到一个使用HitCount结构的完整例子,并在API文档中了解更多关于管理方法和状态类型。
Within Guards
Because
State
is itself a request guard, managed state can be retrieved from another request guard’s implementation using eitherRequest::guard()
orRocket::state()
. In the following code example, theItem
request guard retrievesMyConfig
from managed state using both methods:
因为State本身就是一个请求保护程序,所以可以使用Request::guard()或Rocket::state()从另一个请求保护程序的实现中检索托管状态。在下面的代码示例中,Item request guard使用两种方法从托管状态中检索MyConfig:
use rocket::State;
use rocket::request::{self, Request, FromRequest};
use rocket::outcome::IntoOutcome;
struct Item<'r>(&'r str);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for Item<'r> {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, ()> {
// Using `State` as a request guard. Use `inner()` to get an `'r`.
let outcome = request.guard::<&State<MyConfig>>().await
.map(|my_config| Item(&my_config.user_val));
// Or alternatively, using `Rocket::state()`:
let outcome = request.rocket().state::<MyConfig>()
.map(|my_config| Item(&my_config.user_val))
.or_forward(());
outcome
}
}
Request-Local State
While managed state is global and available application-wide, request-local state is local to a given request, carried along with the request, and dropped once the request is completed. Request-local state can be used whenever a
Request
is available, such as in a fairing, a request guard, or a responder.Request-local state is cached: if data of a given type has already been stored, it will be reused. This is especially useful for request guards that might be invoked multiple times during routing and processing of a single request, such as those that deal with authentication.
管理状态是全局性的,在整个应用中都可以使用,而请求-本地状态是特定请求的本地状态,与请求一起携带,并在请求完成后丢弃。请求本地状态可以在请求可用的时候使用,例如在整流罩、请求保护器或响应者中。
请求-本地状态是被缓存的:如果一个给定类型的数据已经被存储,它将被重新使用。这对于在一个请求的路由和处理过程中可能被多次调用的请求保护器特别有用,比如那些处理认证的请求。
As an example, consider the following request guard implementation for
RequestId
that uses request-local state to generate and expose a unique integer ID per request:
作为一个例子,考虑以下RequestId的请求保护实现,它使用请求-本地状态来生成和公开每个请求的唯一整数ID:
use rocket::request::{self, Request, FromRequest};
/// A global atomic counter for generating IDs.
static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
/// A type that represents a request's ID.
struct RequestId(pub usize);
/// Returns the current request's ID, assigning one only as necessary.
#[rocket::async_trait]
impl<'r> FromRequest<'r> for &'r RequestId {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
// The closure passed to `local_cache` will be executed at most once per
// request: the first time the `RequestId` guard is used. If it is
// requested again, `local_cache` will return the same value.
request::Outcome::Success(request.local_cache(|| {
RequestId(ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}))
}
}
#[get("/")]
fn id(id: &RequestId) -> String {
format!("This is request #{}.", id.0)
}
Note that, without request-local state, it would not be possible to:
- Associate a piece of data, here an ID, directly with a request.
- Ensure that a value is generated at most once per request.
For more examples, see the
FromRequest
request-local state documentation, which uses request-local state to cache expensive authentication and authorization computations, and theFairing
documentation, which uses request-local state to implement request timing.
请注意,如果没有请求-本地状态,就不可能做到:
1.将一块数据,这里是一个ID,直接与一个请求相关联。
2.确保一个值在每个请求中最多产生一次。
关于更多的例子,请参阅FromRequest请求本地状态文档,它使用请求本地状态来缓存昂贵的认证和授权计算,以及Fairing文档,它使用请求本地状态来实现请求计时。
Databases
Rocket includes built-in, ORM-agnostic support for databases via
rocket_db_pools
. The library simplifies accessing one or more databases via connection pools: data structures that maintain active database connections for use in the application. Database configuration occurs via Rocket’s regular configuration mechanisms.
Rocket包括内置的,与ORM无关的,通过rocket_db_pools对数据库的支持。该库通过连接池简化了对一个或多个数据库的访问:数据结构保持活跃的数据库连接,以便在应用程序中使用。数据库配置通过Rocket的常规配置机制进行。
Connecting your Rocket application to a database using
rocket_db_pools
happens in three simple steps:
使用rocket_db_pools将你的Rocket应用程序连接到数据库,只需三个简单的步骤:
1.Choose your database(s) from the supported database driver list. Add
rocket_db_pools
as a dependency inCargo.toml
with respective database driver feature(s) enabled:
1.从支持的数据库驱动列表中选择你的数据库。在Cargo.toml中添加rocket_db_pools作为依赖项,并启用相应的数据库驱动功能:
[dependencies.rocket_db_pools]
version = "=0.1.0-rc.3"
features = ["sqlx_sqlite"]
2.Choose a name for your database, here
sqlite_logs
. Configure at least a URL for the database underdatabases.$name
(here, inRocket.toml
), where$name
is your choice of database name:
为你的数据库选择一个名字,这里是sqlite_logs。在databases. n a m e 下至少配置一个数据库的 U R L (这里,在 R o c k e t . t o m l 中),其中 name下至少配置一个数据库的URL(这里,在Rocket.toml中),其中 name下至少配置一个数据库的URL(这里,在Rocket.toml中),其中name是你选择的数据库名称:
3.Derive
Database
for a unitType
(Logs
here) which wraps the selected driver’sPool
type from the supported database driver list. Decorated the struct with#[database("$name")]
with the$name
from2.
. Attach$Type::init()
to your application’sRocket
to initialize the database pool and useConnection<$Type>
as a request guard to retrieve an active database connection:
为单元类型派生数据库(Logs here),从支持的数据库驱动列表中包装所选驱动的Pool类型。用#[database(" n a m e " ) ] 对结构进行装饰,其中的 name")]对结构进行装饰,其中的 name")]对结构进行装饰,其中的name来自2.将 T y p e : : i n i t ( ) 附加到你的应用程序的 R o c k e t 中,以初始化数据库池,并使用 C o n n e c t i o n < Type::init()附加到你的应用程序的Rocket中,以初始化数据库池,并使用Connection< Type::init()附加到你的应用程序的Rocket中,以初始化数据库池,并使用Connection<Type>作为请求保护,以检索活动数据库连接:
#[macro_use] extern crate rocket;
use rocket_db_pools::{Database, Connection};
use rocket_db_pools::sqlx::{self, Row};
#[derive(Database)]
#[database("sqlite_logs")]
struct Logs(sqlx::SqlitePool);
#[get("/<id>")]
async fn read(mut db: Connection<Logs>, id: i64) -> Option<String> {
sqlx::query("SELECT content FROM logs WHERE id = ?").bind(id)
.fetch_one(&mut *db).await
.and_then(|r| Ok(r.try_get(0)?))
.ok()
}
#[launch]
fn rocket() -> _ {
rocket::build().attach(Logs::init()).mount("/", routes![read])
}
For complete usage details, see rocket_db_pools
.
Driver Features
Only the minimal features for each driver crate are enabled by
rocket_db_pools
. To use additional driver functionality exposed via its crate’s features, you’ll need to depend on the crate directly with those features enabled inCargo.toml
:
rocket_db_pools只启用了每个驱动板块的最小功能。要使用通过其cockate的特性暴露出来的额外驱动功能,你需要在Cargo.toml中启用这些特性后直接依赖该cockate:
[dependencies.sqlx]
version = "0.6"
default-features = false
features = ["macros", "offline", "migrate"]
[dependencies.rocket_db_pools]
version = "=0.1.0-rc.3"
features = ["sqlx_sqlite"]
Synchronous ORMs
While
rocket_db_pools
provides support forasync
ORMs and should thus be the preferred solution, Rocket also provides support for synchronous, blocking ORMs like Diesel via therocket_sync_db_pools
library, which you may wish to explore. Usage is similar, but not identical, torocket_db_pools
. See the crate docs for complete usage details.
虽然rocket_db_pools提供了对异步ORM的支持,因此应该是首选的解决方案,但Rocket也通过rocket_sync_db_pools库提供了对同步、阻塞ORM的支持,比如Diesel,你可能希望探索一下。使用方法与rocket_db_pools类似,但不完全相同。有关完整的使用细节,请参见crate文档。
Examples
For examples of CRUD-like “blog” JSON APIs backed by a SQLite database driven by each of
sqlx
,diesel
, andrusqlite
, with migrations run automatically for the former two drivers, see the databases example. Thesqlx
example usesrocket_db_pools
while thediesel
andrusqlite
examples userocket_sync_db_pools
.
关于类似CRUD的 "博客 "JSON API的例子,由sqlx、diesel和rusqlite各自驱动的SQLite数据库支持,前两个驱动自动运行迁移,见数据库的例子。sqlx的例子使用了rocket_db_pools,而diesel和rusqlite的例子使用了rocket_sync_db_pools。
62 70 65 145
342
Fairings
Fairings are Rocket’s approach to structured middleware. With fairings, your application can hook into the request lifecycle to record or rewrite information about incoming requests and outgoing responses.
Fairings是Rocket的结构化中间件的方法。有了Fairings,你的应用程序就可以钩住请求生命周期,记录或重写有关传入请求和传出响应的信息。
Overview
Any type that implements the
Fairing
trait is a fairing. Fairings hook into Rocket’s request lifecycle, receiving callbacks for events such as incoming requests and outgoing responses. Rocket passes information about these events to the fairing; the fairing can do what it wants with the information. This includes rewriting requests or responses, recording information about the event, or doing nothing at all.Rocket’s fairings are a lot like middleware from other frameworks, but they bear a few key distinctions:
- Fairings cannot terminate or respond to an incoming request directly.
- Fairings cannot inject arbitrary, non-request data into a request.
- Fairings can prevent an application from launching.
- Fairings can inspect and modify the application’s configuration.
If you are familiar with middleware from other frameworks, you may find yourself reaching for fairings instinctively. Before doing so, remember that Rocket provides a rich set of mechanisms such as request guards and data guards that can be used to solve problems in a clean, composable, and robust manner.
任何实现了Fairing特性的类型都是Fairing。Fairing与Rocket的请求生命周期挂钩,接收事件的回调,如传入请求和传出响应。Rocket将这些事件的信息传递给Fairing;Fairing可以用这些信息做它想做的事。这包括重写请求或响应,记录关于事件的信息,或者什么都不做。
Rocket的Fairing很像其他框架的中间件,但它们有几个关键的区别:
Fairings不能直接终止或响应传入的请求。
Fairings不能将任意的、非请求的数据注入到请求中。
Fairings可以阻止一个应用程序的启动。
Fairings可以检查和修改应用程序的配置。
如果你熟悉其他框架的中间件,你可能会发现自己本能地伸手去拿Fairings。在这样做之前,请记住,Rocket提供了一套丰富的机制,如请求防护和数据防护,可以用来以干净、可组合和健壮的方式解决问题。
Warning
As a general rule of thumb, only globally applicable actions should be effected through fairings. You should *not* use a fairing to implement authentication or authorization (preferring to use a request guard instead) unless the authentication or authorization applies to all or the overwhelming majority of the application. On the other hand, you should use a fairing to record timing and usage statistics or to enforce global security policies.
警告
作为一般的经验法则,只有全局适用的操作才能通过Fairings 实现。你不应该使用Fairings 来实现认证或授权(更倾向于使用请求保护),除非认证或授权适用于所有或绝大部分的应用程序。另一方面,你应该使用Fairings 来记录时间和使用情况的统计数据,或者执行全局安全策略。
Attaching
Fairings are registered with Rocket via the
attach
method on aRocket
instance. Only when a fairing is attached will its callbacks fire. As an example, the following snippet attached two fairings,req_fairing
andres_fairing
, to a new Rocket instance:
Fairings 是通过火箭实例上的attach方法与rocket注册的。只有当Fairings 被附加时,其回调才会启动。作为一个例子,下面的片段将两个Fairings ,req_fairing和res_fairing,连接到一个新的rocket实例:
#[launch]
fn rocket() -> _ {
rocket::build()
.attach(req_fairing)
.attach(res_fairing)
}
Fairings are executed in the order in which they are attached: the first attached fairing has its callbacks executed before all others. A fairing can be attached any number of times. Except for singleton fairings, all attached instances are polled at runtime. Fairing callbacks may not be commutative; the order in which fairings are attached may be significant.
Fairings是按照它们被连接的顺序执行的:第一个被连接的Fairings 在所有其他Fairings之前执行其回调。一个Fairings 可以被连接任意次数。除了 singleton fairings,所有附加的实例在运行时被轮询。Fairings 的回调可能不是互换的;Fairings 的连接顺序可能是重要的。
Callbacks
There are five events for which Rocket issues fairing callbacks. Each of these events is briefly described below and in details in the
Fairing
trait docs:
有五个事件,Rocket会对其进行公平化回调。每一个事件在下面都有简单的描述,详细情况请参见整流特性文档:
Ignite (
on_ignite
)An ignite callback is called during ignition An ignite callback can arbitrarily modify the
Rocket
instance being built. They are commonly used to parse and validate configuration values, aborting on bad configurations, and inserting the parsed value into managed state for later retrieval.
Ignite(on_ignite)
在Ignite过程中会调用一个Ignite回调 一个Ignite回调可以任意修改正在构建的rocket实例。它们通常被用来解析和验证配置值,在坏的配置上中止,并将解析后的值插入到管理状态中以便以后检索。
Liftoff (
on_liftoff
)A liftoff callback is called immediately after a Rocket application has launched. A liftoff callback can inspect the
Rocket
instance being launched. A liftoff callback can be a convenient hook for launching services related to the Rocket application being launched.
Liftoff (on_liftoff)。
liftoff回调会在rocket应用启动后立即被调用。liftoff回调可以检查正在启动的Rocket实例。liftoff回调可以成为一个方便的钩子,用于启动与被启动的Rocket应用相关的服务。
Request (
on_request
)A request callback is called just after a request is received. A request callback can modify the request at will and peek into the incoming data. It may not, however, abort or respond directly to the request; these issues are better handled via request guards or via response callbacks.
Request(on_request)
请求回调就在收到请求后被调用。一个请求回调可以随意修改请求,并偷看传入的数据。然而,它不能中止或直接响应请求;这些问题最好通过请求防护或通过响应回调来处理。
Response (
on_response
)A response callback is called when a response is ready to be sent to the client. A response callback can modify part or all of the response. As such, a response fairing can be used to provide a response when the greater application fails by rewriting 404 responses as desired. As another example, response fairings can also be used to inject headers into all outgoing responses.
Response(on_response)
响应回调在响应准备好被发送到客户端时被调用。响应回调可以修改部分或全部的响应。因此,响应整流可以用来在更大的应用程序失败时,通过按需要重写404响应来提供一个响应。作为另一个例子,响应整顿也可以用来在所有发出的响应中注入头信息。
Shutdown (
on_shutdown
)A shutdown callback is called when shutdown is triggered. At this point, graceful shutdown has commenced but not completed; no new requests are accepted but the application may still be actively serving existing requests. All registered shutdown fairings are run concurrently; resolution of all fairings is awaited before resuming shutdown.
Shutdown(on_shutdown)
当Shutdown被触发时,会调用一个Shutdown回调。此时,优雅的关闭已经开始,但尚未完成;不接受新的请求,但应用程序可能仍在积极地为现有的请求服务。所有注册的Shutdown整定都是同时进行的;在恢复Shutdown之前,要等待所有整定的解决。
Implementing
Recall that a fairing is any type that implements the
Fairing
trait. AFairing
implementation has one required method:info
, which returns anInfo
structure. This structure is used by Rocket to assign a name to the fairing and determine the set of callbacks the fairing is registering for. AFairing
can implement any of the available callbacks:on_ignite
,on_liftoff
,on_request
,on_response
, andon_shutdown
. Each callback has a default implementation that does absolutely nothing.
回顾一下,Fairing是任何实现了Fairing特性的类型。一个Fairing的实现有一个必要的方法:info,它返回一个Info结构。这个结构被Rocket用来给Fairing分配一个名字,并确定Fairing正在注册的回调集合。一个Fairing可以实现任何可用的回调:on_ignite, on_liftoff, on_request, on_response, and on_shutdown
。每个回调都有一个默认实现,完全不做任何事情。
Requirements
A type implementing
Fairing
is required to beSend + Sync + 'static
. This means that the fairing must be sendable across thread boundaries (Send
), thread-safe (Sync
), and have only static references, if any ('static
). Note that these bounds do not prohibit aFairing
from holding state: the state need simply be thread-safe and statically available or heap allocated.
实现Fairing的类型被要求是Send + Sync + 'static
。这就意味着,公平必须是可跨线程发送的(Send),是线程安全的(Sync),并且只有静态引用,如果有的话('static)。请注意,这些界限并不禁止Fairing持有状态:该状态只需要是线程安全的、静态可用的或堆分配的。
Example
As an example, we want to record the number of
GET
andPOST
requests that our application has received. While we could do this with request guards and managed state, it would require us to annotate everyGET
andPOST
request with custom types, polluting handler signatures. Instead, we can create a simple fairing that acts globally.
例如,我们想记录我们的应用程序收到的GET和POST请求的数量。虽然我们可以用request guards和managed state来做这件事,但这需要我们用自定义类型来注释每个GET和POST请求,污染处理程序的签名。相反,我们可以创建一个简单的Fairing
,在全局范围内发挥作用。
The code for a
Counter
fairing below implements exactly this. The fairing receives a request callback, where it increments a counter on eachGET
andPOST
request. It also receives a response callback, where it responds to unrouted requests to the/counts
path by returning the recorded number of counts.
下面的计数器Fairing
的代码正是实现了这一点。Fairing
收到一个请求回调,它在每个GET和POST请求中增加一个计数器。它还接收一个响应回调,通过返回记录的计数数量来响应对/counts路径的非路由请求。
use std::io::Cursor;
use std::sync::atomic::{AtomicUsize, Ordering};
use rocket::{Request, Data, Response};
use rocket::fairing::{Fairing, Info, Kind};
use rocket::http::{Method, ContentType, Status};
struct Counter {
get: AtomicUsize,
post: AtomicUsize,
}
#[rocket::async_trait]
impl Fairing for Counter {
// This is a request and response fairing named "GET/POST Counter".
fn info(&self) -> Info {
Info {
name: "GET/POST Counter",
kind: Kind::Request | Kind::Response
}
}
// Increment the counter for `GET` and `POST` requests.
async fn on_request(&self, request: &mut Request<'_>, _: &mut Data<'_>) {
match request.method() {
Method::Get => self.get.fetch_add(1, Ordering::Relaxed),
Method::Post => self.post.fetch_add(1, Ordering::Relaxed),
_ => return
};
}
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
// Don't change a successful user's response, ever.
if response.status() != Status::NotFound {
return
}
// Rewrite the response to return the current counts.
if request.method() == Method::Get && request.uri().path() == "/counts" {
let get_count = self.get.load(Ordering::Relaxed);
let post_count = self.post.load(Ordering::Relaxed);
let body = format!("Get: {}\nPost: {}", get_count, post_count);
response.set_status(Status::Ok);
response.set_header(ContentType::Plain);
response.set_sized_body(body.len(), Cursor::new(body));
}
}
}
The complete example can be found in the Fairing
documentation.
Ad-Hoc Fairings
For simpler cases, implementing the
Fairing
trait can be cumbersome. This is why Rocket provides theAdHoc
type, which creates a fairing from a simple function or closure. Using theAdHoc
type is easy: simply call theon_ignite
,on_liftoff
,on_request
,on_response
, oron_shutdown
constructors onAdHoc
to create a fairing from a function or closure.As an example, the code below creates a
Rocket
instance with two attached ad-hoc fairings. The first, a liftoff fairing named “Liftoff Printer”, prints a message indicating that the application has launched. The second named “Put Rewriter”, a request fairing, rewrites the method of all requests to bePUT
.
对于更简单的情况,实现整齐划一的特性可能很麻烦。这就是为什么Rocket提供了AdHoc类型,它从一个简单的函数或闭包中创建一个整流罩。使用AdHoc类型很简单:只需调用AdHoc的on_ignite、on_liftoff、on_request、on_response或on_shutdown构造函数,就可以从一个函数或闭包中创建一个整流罩。
作为一个例子,下面的代码创建了一个带有两个附加的临时整流罩的火箭实例。第一个,名为 "Liftoff Printer "的启动整流罩,打印一条信息,表明应用程序已经启动。第二个名为 "Put Rewriter "的请求整流罩,将所有请求的方法改写为PUT。
use rocket::fairing::AdHoc;
use rocket::http::Method;
rocket::build()
.attach(AdHoc::on_liftoff("Liftoff Printer", |_| Box::pin(async move {
println!("...annnddd we have liftoff!");
})))
.attach(AdHoc::on_request("Put Rewriter", |req, _| Box::pin(async move {
req.set_method(Method::Put);
})))
.attach(AdHoc::on_shutdown("Shutdown Printer", |_| Box::pin(async move {
println!("...shutdown has commenced!");
})));
Testing
Every application should be well tested and understandable. Rocket provides the tools to perform unit and integration tests. It also provides a means to inspect code generated by Rocket.
每个应用程序都应该被很好地测试和理解。Rocket提供了执行单元和集成测试的工具。它还提供了一种检查Rocket生成的代码的方法。
Local Dispatching
Rocket applications are tested by dispatching requests to a local instance of
Rocket
. Thelocal
module contains all of the structures necessary to do so. In particular, it contains aClient
structure that is used to createLocalRequest
structures that can be dispatched against a givenRocket
instance. Usage is straightforward:
Rocket 应用的测试是通过向Rocket 的一个本地实例派发请求来进行的。本地模块包含这样做的所有必要结构。特别是,它包含了一个客户端结构,用于创建LocalRequest结构,这些结构可以针对给定的Rocket实例进行调度。使用方法很简单:
1.Construct a
Rocket
instance that represents the application.
1.构建一个代表应用程序的Rocket实例。
let rocket = rocket::build();
2.Construct a
Client
using theRocket
instance.
2.使用Rocket实例构建一个客户端。
let client = Client::tracked(rocket).unwrap();
3.Construct requests using the
Client
instance.
3.使用客户端实例构建请求。
let req = client.get("/");
4.Dispatch the request to retrieve the response.
- 派遣请求以检索响应。
let response = req.dispatch();
Validating Responses
A
dispatch
of aLocalRequest
returns aLocalResponse
which can be inspected for validity. During testing, the response is usually validated against expected properties. These includes things like the response HTTP status, the inclusion of headers, and expected body data.
LocalResponse
type provides methods to ease this sort of validation. We list a few below:
status
: returns the HTTP status in the response.content_type
: returns the Content-Type header in the response.headers
: returns a map of all of the headers in the response.into_string
: reads the body data into aString
.into_bytes
: reads the body data into aVec<u8>
.into_json
: deserializes the body data on-the-fly as JSON.into_msgpack
: deserializes the body data on-the-fly as MessagePack.
一个LocalRequest的派发会返回一个LocalResponse,它可以被检查是否有效。在测试过程中,通常会根据预期的属性对响应进行验证。这包括像响应的HTTP状态、包含的头文件和预期的正文数据。
LocalResponse类型提供了缓解这种验证的方法。我们在下面列出一些:
status:返回响应中的HTTP状态。
content_type:返回响应中的Content-Type头。
headers:返回响应中的所有头信息的映射。
into_string:将正文数据读成一个字符串。
into_bytes:将正文数据读成Vec。
into_json:将正文数据实时反序列化为JSON格式。
into_msgpack:将主体数据实时反序列化为MessagePack。
These methods are typically used in combination with the
assert_eq!
orassert!
macros as follows:
这些方法通常与assert_eq!或assert!宏结合使用,具体如下:
use rocket::http::{ContentType, Status};
let mut response = client.get(uri!(hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::Plain));
assert!(response.headers().get_one("X-Special").is_some());
assert_eq!(response.into_string().unwrap(), "Expected Body");
Testing “Hello, world!”
To solidify an intuition for how Rocket applications are tested, we walk through how to test the “Hello, world!” application below:
为了巩固对Rocket应用程序如何测试的直觉,我们在下面走过如何测试 "Hello, world!"应用程序:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![hello])
}
Notice that we’ve separated the creation of the
Rocket
instance from the launch of the instance. As you’ll soon see, this makes testing our application easier, less verbose, and less error-prone.
注意,我们已经把创建Rocket实例和启动实例分开。正如你很快就会看到的,这使得测试我们的应用程序更加容易,不那么冗长,也不那么容易出错。
Setting Up
First, we’ll create a
test
module with the proper imports:
首先,我们将创建一个具有适当导入的测试模块:
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
/* .. */
}
}
You can also move the body of the
test
module into its own file, saytests.rs
, and then import the module into the main file using:
#[cfg(test)] mod tests;
Testing
To test our “Hello, world!” application, we create a
Client
for ourRocket
instance. It’s okay to use methods likeexpect
andunwrap
during testing: we want our tests to panic when something goes wrong.
为了测试我们的 "Hello, world!"应用程序,我们为我们的Rocket实例创建一个客户端。在测试过程中,使用像expect和unwrap这样的方法是可以的:我们希望我们的测试在出错的时候会惊慌失措。
let client = Client::tracked(rocket()).expect("valid rocket instance");
Then, we create a new
GET /
request and dispatch it, getting back our application’s response:
然后,我们创建一个新的GET/请求并派发它,得到我们应用程序的响应:
let mut response = client.get(uri!(hello)).dispatch();
Finally, we ensure that the response contains the information we expect it to. Here, we want to ensure two things:
- The status is
200 OK
.- The body is the string “Hello, world!”.
最后,我们确保响应包含我们期望的信息。在这里,我们要确保两件事:
- 状态是200 OK。
- 主体是字符串 “你好,世界!”。
We do this by checking the
Response
object directly:
我们通过直接检查响应对象来做到这一点:
use rocket::http::{ContentType, Status};
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string(), Some("Hello, world!".into()));
That’s it! Altogether, this looks like:
这就是了,总的来说,这看起来像:
#[get("/")]
fn hello() -> &'static str {
"Hello, world!"
}
#[launch]
fn rocket() -> Rocket<Build> {
rocket::build().mount("/", routes![hello])
}
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::blocking::Client;
use rocket::http::Status;
#[test]
fn hello_world() {
let client = Client::tracked(rocket()).expect("valid rocket instance");
let mut response = client.get(uri!(super::hello)).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.into_string().unwrap(), "Hello, world!");
}
}
The tests can be run with
cargo test
. You can find the full source code to this example on GitHub.
这些测试可以用cargo test来运行。你可以在GitHub上找到这个例子的完整源代码。
Asynchronous Testing
You may have noticed the use of a “
blocking
” API in these examples, even thoughRocket
is anasync
web framework. In most situations, theblocking
testing API is easier to use and should be preferred. However, when concurrent execution of two or more requests is required for the server to make progress, you will need the more flexibleasynchronous
API; theblocking
API is not capable of dispatching multiple requests simultaneously. While synthetic, theasync_required
testing
example uses anasync
barrier to demonstrate such a case. For more information, see therocket::local
androcket::local::asynchronous
documentation.
你可能已经注意到在这些例子中使用了 "阻塞 "的API,尽管Rocket是一个异步的Web框架。在大多数情况下,阻塞式测试API更容易使用,应该是首选。然而,当需要同时执行两个或更多的请求以使服务器取得进展时,你将需要更灵活的异步API;阻塞式API不能够同时调度多个请求。虽然是合成的,但async_required测试例子使用了一个异步屏障来演示这样的情况。欲了解更多信息,请参见rocket::local和rocket::local::asynchronous文档。
Codegen Debug
It can be useful to inspect the code that Rocket’s code generation is emitting, especially when you get a strange type error. To have Rocket log the code that it is emitting to the console, set the
ROCKET_CODEGEN_DEBUG
environment variable when compiling:
检查Rocket的代码生成所发出的代码可能很有用,特别是当你得到一个奇怪的类型错误时。要让Rocket把它发出的代码记录到控制台,可以在编译的时候设置ROCKET_CODEGEN_DEBUG环境变量:
ROCKET_CODEGEN_DEBUG=1 cargo build
During compilation, you should see output like:
在编译过程中,你应该看到类似的输出:
note: emitting Rocket code generation debug output
--> examples/hello/src/main.rs:14:1
|
14 | #[get("/world")]
| ^^^^^^^^^^^^^^^^
|
= note:
impl world {
fn into_info(self) -> rocket::StaticRouteInfo {
fn monomorphized_function<'_b>(
__req: &'_b rocket::request::Request<'_>,
__data: rocket::data::Data,
) -> ::rocket::route::BoxFuture<'_b> {
::std::boxed::Box::pin(async move {
let ___responder = world();
::rocket::handler::Outcome::from(__req, ___responder)
})
}
::rocket::StaticRouteInfo {
name: "world",
method: ::rocket::http::Method::Get,
path: "/world",
handler: monomorphized_function,
format: ::std::option::Option::None,
rank: ::std::option::Option::None,
sentinels: sentinels![&'static str],
}
}
}
This corresponds to the facade request handler Rocket has generated for the
hello
route.
这与Rocket为hello路由生成的facade请求处理程序相对应。
Configuration
Rocket’s configuration system is flexible. Based on Figment, it allows you to configure your application the way you want while also providing with a sensible set of defaults.
Rocket的配置系统很灵活。基于Figment,它允许你以你想要的方式配置你的应用程序,同时还提供了一套合理的默认值。
Overview
Rocket’s configuration system is based on Figment’s
Provider
s, types which provide configuration data. Rocket’sConfig
andConfig::figment()
, as well as Figment’sToml
andJson
, are some examples of providers. Providers can be combined into a singleFigment
provider from which any configuration structure that implementsDeserialize
can be extracted.
Rocket 的配置系统是基于 Figment 的 Providers,即提供配置数据的类型。Rocket 的 Config 和 Config::figment(),以及 Figment 的 Toml 和 Json,都是提供者的一些例子。提供者可以组合成一个单一的 Figment 提供者,从中可以提取任何实现 Deserialize 的配置结构。
Rocket expects to be able to extract a
Config
structure from the provider it is configured with. This means that no matter which configuration provider Rocket is asked to use, it must be able to read the following configuration values:
Rocket希望能够从它所配置的提供者那里提取一个Config结构。这意味着无论Rocket被要求使用哪个配置提供者,它都必须能够读取以下配置值:
key | kind | description | debug/release default |
---|---|---|---|
address | IpAddr | IP address to serve on | 127.0.0.1 |
port | u16 | Port to serve on. | 8000 |
workers * | usize | Number of threads to use for executing futures. | cpu core count |
max_blocking * | usize | Limit on threads to start for blocking tasks. | 512 |
ident | string , false | If and how to identify via the Server header. | "Rocket" |
ip_header | string , false | IP header to inspect to get client’s real IP. | "X-Real-IP" |
keep_alive | u32 | Keep-alive timeout seconds; disabled when 0 . | 5 |
log_level | LogLevel | Max level to log. (off/normal/debug/critical) | normal /critical |
cli_colors | bool | Whether to use colors and emoji when logging. | true |
secret_key | SecretKey | Secret key for signing and encrypting values. | None |
tls | TlsConfig | TLS configuration, if any. | None |
limits | Limits | Streaming read size limits. | Limits::default() |
limits.$name | &str /uint | Read limit for $name . | form = “32KiB” |
ctrlc | bool | Whether ctrl-c initiates a server shutdown. | true |
shutdown * | Shutdown | Graceful shutdown configuration. | Shutdown::default() |
* Note: the
workers
,max_blocking
, andshutdown.force
configuration parameters are only read from the default provider.
- 注意:workers、max_blocking和shutdown.force等配置参数只从默认提供者那里读取。
Profiles
Configurations can be arbitrarily namespaced by
Profile
s. Rocket’sConfig
andConfig::figment()
providers automatically set the configuration profile to “debug” when compiled in “debug” mode and “release” when compiled in release mode, but you can arbitrarily name and set profiles to your desire. For example, with the default provider, you can set the selected profile viaROCKET_PROFILE
. This results in Rocket preferring the values in theROCKET_PROFILE
profile.
配置可以由Profiles任意命名。Rocket的Config和Config::figment()提供者在 "调试 "模式下编译时自动将配置配置文件设置为 “调试”,在发布模式下编译时设置为 “发布”,但你可以根据自己的意愿任意命名和设置配置文件。例如,对于默认的提供者,你可以通过ROCKET_PROFILE
设置所选的配置文件。这导致Rocket更倾向于ROCKET_PROFILE
配置文件中的值。
In addition to any profiles you declare, there are two meta-profiles,
default
andglobal
, which can be used to provide values that apply to all profiles. Values provided in adefault
profile are used as fall-back values when the selected profile doesn’t contain a requested value, while values in theglobal
profile supplant any values with the same name in any profile.
除了你声明的任何配置文件之外,还有两个元配置文件,default
和global
,它们可以用来提供适用于所有配置文件的值。当选定的配置文件不包含所要求的值时,default
配置文件中提供的值被用作后备值,而global
配置文件中的值取代任何配置文件中的同名值。
Default Provider
Rocket’s default configuration provider is
Config::figment()
; this is the provider that’s used when callingrocket::build()
.The default figment reads from and merges, at a per-key level, the following sources in ascending priority order:
Config::default()
, which provides default values for all parameters.Rocket.toml
or TOML file path inROCKET_CONFIG
environment variable.ROCKET_
prefixed environment variables.The selected profile is the value of the
ROCKET_PROFILE
environment variable, or if it is not set, “debug” when compiled in debug mode and “release” when compiled in release mode. With the exception oflog_level
, which changes fromnormal
in debug tocritical
in release, all of the default configuration values are the same in all profiles. What’s more, all configuration values have defaults, so no configuration is needed to get started.
Rocket的默认配置提供者是Config::figment();这是调用rocket::build()时使用的提供者。
默认的figment在每个键的层面上,按照升序的优先级,从以下来源读取和合并:
1.Config::default()
,它为所有参数提供默认值。
2.ROCKET_CONFIG
环境变量中的Rocket.toml或TOML文件路径。
3.ROCKET_
前缀的环境变量。
选择的配置文件是ROCKET_PROFILE
环境变量的值,如果没有设置,在调试模式下编译时为 “debug”,在发布模式下编译时为 “release”。除了log_level从debug模式下的正常值变为release模式下的关键值外,所有的默认配置值在所有配置文件中都是一样的。更重要的是,所有的配置值都有默认值,所以不需要配置就可以开始使用。
As a result of
Config::figment()
, without any effort, Rocket can be configured via aRocket.toml
file and/or via environment variables, the latter of which take precedence over the former.
作为Config::figment()的结果,不费吹灰之力,Rocket可以通过Rocket.toml文件和/或环境变量进行配置,后者优先于前者。
Rocket.toml
Rocket searches for
Rocket.toml
or the filename in aROCKET_CONFIG
environment variable starting at the current working directory. If it is not found, the parent directory, its parent, and so on, are searched until the file is found or the root is reached. If the path set inROCKET_CONFIG
is absolute, no such search occurs and the set path is used directly.The file is assumed to be nested, so each top-level key declares a profile and its values the value for the profile. The following is an example of what such a file might look like:
Rocket从当前工作目录开始搜索Rocket.toml
或ROCKET_CONFIG
环境变量中的文件名。如果没有找到,则搜索父目录、其父目录,以此类推,直到找到该文件或到达根目录。如果ROCKET_CONFIG
中设置的路径是绝对的,就不会发生这种搜索,而是直接使用设置的路径。
文件被假定为是嵌套的,所以每个顶层的键都声明了一个配置文件,其值则是该配置文件的值。下面是这样一个文件的例子:
// defaults for _all_ profiles
// 适用于所有profile的默认配置
[default]
address = "0.0.0.0" // 监听的IP地址
limits = { form = "64 kB", json = "1 MiB" } // 请求body的大小限制
// set only when compiled in debug mode, i.e, `cargo build`
// 只在debug模式下设置,即 `cargo build`
[debug]
port = 8000 // 监听的端口号
// 只有 `json` 选项从 `default` 中继承并覆盖;`form` 选项不变
limits = { json = "10MiB" } // 请求body的大小限制
// set only when the `nyc` profile is selected
// 只在 `nyc` profile 被选中时设置
[nyc]
port = 9001 // 监听的端口号
// set only when compiled in release mode, i.e, `cargo build --release`
// 只在release模式下设置,即 `cargo build --release`
[release]
port = 9999 // 监听的端口号
ip_header = false // 是否开启ip头部验证
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk=" // session加密密钥
The following is a
Rocket.toml
file with all configuration options set for demonstration purposes. You do not and should not set a value for configuration options needlessly, preferring to use the default value when sensible.
下面是一个Rocket.toml文件,其中设置了所有的配置选项用于演示。你不会也不应该为配置选项无谓地设置一个值,在合理的情况下,最好使用默认值。
[default] # 默认配置文件块
address = "127.0.0.1" # 监听地址
port = 8000 # 监听端口
workers = 16 # 工作线程数
max_blocking = 512 # 最大阻塞任务数
keep_alive = 5 # 连接最长保持时间,单位秒
ident = "Rocket" # 用户代理标识
ip_header = "X-Real-IP" # 指定IP头,设置为false可以禁用该功能
log_level = "normal" # 日志记录级别
temp_dir = "/tmp" # 指定临时文件夹路径
cli_colors = true # 是否使用CLI颜色
secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk=" # 用于加密会话的密钥
[default.limits] # 默认的请求限制
form = "64 kB" # 表单请求体大小限制
json = "1 MiB" # JSON请求体大小限制
msgpack = "2 MiB" # MessagePack请求体大小限制
"file/jpg" = "5 MiB" # 特定类型文件大小限制
[default.tls] # 默认的TLS设置
certs = "path/to/cert-chain.pem" # SSL证书路径
key = "path/to/key.pem" # SSL私钥路径
[default.shutdown] # 默认的服务关闭设置
ctrlc = true # 是否接受终端的Ctrl+C关闭信号
signals = ["term", "hup"] # 其他关闭信号
grace = 5 # 服务停止时的渐进关闭时间,单位秒
mercy = 5 # 等待服务退出的时间,单位秒
Environment Variables
Rocket reads all environment variable names prefixed with
ROCKET_
using the string after the_
as the name of a configuration value as the value of the parameter as the value itself. Environment variables take precedence over values inRocket.toml
. Values are parsed as loose form of TOML syntax. Consider the following examples:
Rocket读取所有以ROCKET__为前缀的环境变量名称,使用_后面的字符串作为配置值的名称,作为参数的值本身。在Rocket.toml中,环境变量优先于值。值被解析为TOML语法的松散形式。考虑一下下面的例子:
ROCKET_FLOAT=3.14
ROCKET_ARRAY=[1,"b",3.14]
ROCKET_STRING=Hello
ROCKET_STRING="Hello There"
ROCKET_KEEP_ALIVE=1
ROCKET_IDENT=Rocket
ROCKET_IDENT="Hello Rocket"
ROCKET_IDENT=false
ROCKET_TLS={certs="abc",key="foo/bar"}
ROCKET_LIMITS={form="64 KiB"}
Configuration Parameters
Secret Key
The
secret_key
parameter configures a cryptographic key to use when encrypting application values. In particular, the key is used to encrypt private cookies, which are available only when thesecrets
crate feature is enabled.Generating a string suitable for use as a
secret_key
configuration value is usually done through tools likeopenssl
. Usingopenssl
, a 256-bit base64 key can be generated with the commandopenssl rand -base64 32
.When compiled in debug mode, a fresh key is generated automatically. In release mode, Rocket requires you to set a secret key if the
secrets
feature is enabled. Failure to do so results in a hard error at launch time. The value of the parameter may either be a 256-bit base64 or hex string or a slice of 32 bytes.
secret_key参数配置了一个加密密钥,以便在加密应用程序的值时使用。特别是,该密钥用于加密私人cookies,只有在启用secrets crate功能时才能使用。
生成一个适合用作secret_key配置值的字符串通常是通过openssl等工具完成的。使用openssl,可以用命令openssl rand -base64 32生成一个256位的base64密钥。
当在调试模式下编译时,一个新的密钥会自动生成。在发布模式下,如果秘密功能被启用,Rocket要求你设置一个秘密密钥。如果不这样做,在启动时就会出现一个硬错误。参数的值可以是一个256位base64或十六进制的字符串,也可以是一个32字节的片断。
Limits
The
limits
parameter configures the maximum amount of data Rocket will accept for a given data type. The value is expected to be a dictionary table where each key corresponds to a data type and each value corresponds to the maximum size in bytes Rocket should accept for that type. Rocket can parse both integers (32768
) or SI unit based strings ("32KiB"
) as limits.By default, Rocket specifies a
32 KiB
limit for incoming forms. Since Rocket requires specifying a read limit whenever data is read, external data guards may also choose to have a configure limit via thelimits
parameter. TheJson
type, for instance, uses thelimits.json
parameter.
limits参数配置了Rocket对某一数据类型所接受的最大数据量。该值应该是一个字典表,每个键对应一个数据类型,每个值对应Rocket应该接受的该类型的最大字节数。Rocket可以解析整数(32768)或基于SI单位的字符串(“32KiB”)作为限制。
默认情况下,Rocket为传入的表单指定了32KiB的限制。由于Rocket要求在读取数据时指定一个读取限制,所以外部数据守护者也可以通过limit参数选择一个配置限制。例如,Json类型使用limits.json参数。
TLS
Rocket includes built-in, native support for TLS >= 1.2 (Transport Layer Security). To enable TLS support:
Rocket包括对TLS >= 1.2(传输层安全)的内置、本地支持。要启用TLS支持:
Enable the
tls
crate feature inCargo.toml
:
在Cargo.toml中启用tls crate功能:
[dependencies]
rocket = { version = "=0.5.0-rc.3", features = ["tls"] }
Configure a TLS certificate chain and private key via the
tls.key
andtls.certs
configuration parameters. With the default provider, this can be done viaRocket.toml
as:
通过tls.key和tls.certs配置参数配置TLS证书链和私钥。在默认的提供者中,这可以通过Rocket.toml完成,因为:
[default.tls]
key = "path/to/key.pem" # Path or bytes to DER-encoded ASN.1 PKCS#1/#8 or SEC1 key.
certs = "path/to/certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.
The
tls
parameter is expected to be a dictionary that deserializes into aTlsConfig
structure:
tls参数应该是一个字典,可以反序列化为一个TlsConfig结构:
key | required | type |
---|---|---|
key | yes | Path or bytes to DER-encoded ASN.1 PKCS#1/#8 or SEC1 key.DER编码的ASN.1 PKCS#1/#8或SEC1密钥的路径或字节。 |
certs | yes | Path or bytes to DER-encoded X.509 TLS cert chain.DER编码的X.509 TLS证书链的路径或字节。 |
ciphers | no | Array of CipherSuite s to enable.要启用的CipherSuites阵列。 |
prefer_server_cipher_order | no | Boolean for whether to prefer server cipher suites.布尔型,表示是否偏好服务器密码套件。 |
mutual | no | A map with mutual TLS configuration.一个具有相互TLS配置的地图。 |
When specified via TOML or other serialized formats, each
CipherSuite
is written as a string representation of the respective variant. For example,CipherSuite::TLS_AES_256_GCM_SHA384
is"TLS_AES_256_GCM_SHA384"
. In TOML, the defaults (with an arbitrarycerts
andkey
) are written:
当通过 TOML 或其他序列化格式指定时,每个 CipherSuite 被写成各自变体的字符串表示。例如,CipherSuite::TLS_AES_256_GCM_SHA384就是 “TLS_AES_256_GCM_SHA384”。在TOML中,默认值(带有任意的证书和密钥)被写入:
[default.tls]
certs = "/ssl/cert.pem"
key = "/ssl/key.pem"
prefer_server_cipher_order = false
ciphers = [
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
]
Mutual TLS
Rocket supports mutual TLS client authentication. Configuration works in concert with the
mtls
module, which provides a request guard to validate, verify, and retrieve client certificates in routes.
Rocket支持相互的TLS客户端认证。配置与mtls模块协同工作,mtls模块提供了一个请求保护,以验证、核实和检索路由中的客户端证书。
By default, mutual TLS is disabled and client certificates are not required, validated or verified. To enable mutual TLS, the
mtls
feature must be enabled and support configured via thetls.mutual
config parameter:
默认情况下,相互TLS是禁用的,客户证书不需要,也不需要验证或核实。要启用相互TLS,必须启用mtls功能,并通过tls.mutual配置参数配置支持:
1.Enable the
mtls
crate feature inCargo.toml
:
在Cargo.toml中启用mtls crate功能:
[dependencies]
rocket = { version = "=0.5.0-rc.3", features = ["mtls"] }
这就隐含地启用了tls功能。
Configure a CA certificate chain via the
tls.mutual.ca_certs
configuration parameter. With the default provider, this can be done viaRocket.toml
as:
通过tls.mutual.ca_certs配置参数配置一个CA证书链。在默认的提供者中,这可以通过Rocket.toml完成,因为:
[default.tls.mutual]
ca_certs = "path/to/ca_certs.pem" # Path or bytes to DER-encoded X.509 TLS cert chain.
mandatory = true # when absent, defaults to false
The
tls.mutual
parameter is expected to be a dictionary that deserializes into aMutualTls
structure:
tls.mutual参数应该是一个字典,可以反序列化为一个MutualTls结构:
key | required | type |
---|---|---|
ca_certs | yes | Path or bytes to DER-encoded X.509 TLS cert chain. |
mandatory | no | Boolean controlling whether the client must authenticate. |
Rocket reports if TLS and/or mTLS are enabled at launch time:
Rocket报告在启动时是否启用了TLS和/或mTLS:
🔧 Configured for debug.
...
>> tls: enabled w/mtls
Once mutual TLS is properly enabled, the
mtls::Certificate
request guard can be used to retrieve validated, verified client certificates:
一旦相互TLS被正确启用,mtls::Certificate request guard就可以被用来检索经过验证的,经过核实的客户证书:
use rocket::mtls::Certificate;
#[get("/auth")]
fn auth(cert: Certificate<'_>) {
// This handler only runs when a valid certificate was presented.
}
The TLS example illustrates a fully configured TLS server with mutual TLS.
Warning: Rocket’s built-in TLS supports only TLS 1.2 and 1.3. This may not be suitable for production use.
TLS的例子说明了一个完全配置的TLS服务器与相互TLS。
警告:Rocket的内置TLS只支持TLS 1.2和1.3。这可能不适合在生产中使用。
案例
use rocket::{fairing::AdHoc, http::Status, request::Request};
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::templates::Template;
use rustls::{ClientCertVerified, TLSError};
// 定义检查客户端证书回调函数
fn verify_certificate(cert: &[u8], _: &[u8], _: &[u8]) -> Result<ClientCertVerified, TLSError> {
// ... 对客户端证书进行验证 ...
// 如果客户端证书有效,返回 ClientCertVerified::assertion()
Ok(ClientCertVerified::assertion())
}
fn main() {
rocket::ignite()
.mount("/", StaticFiles::from("static/"))
.mount(
"/",
routes![
// 定义需要身份验证的路由
hello_protected
],
)
.attach(Template::fairing())
.attach(AdHoc::on_attach("TLS Config", |rocket| {
let config = rocket.config();
let mut tls_config = config
.get_str("tls")
.unwrap_or("false")
.parse::<rustls::ServerConfig>()
.unwrap_or_else(|_| {
panic!(
"Failed to parse TLS configuration at '{}'",
config.get_str("tls").unwrap_or("null")
)
});
// 启用客户端证书验证
tls_config.set_client_certificate_verifier(
rustls::AllowAnyAuthenticatedClient::new(
rustls::RootCertStore::empty(),
verify_certificate,
)
);
// 把配置设置到 Rocket 中
Ok(rocket
.manage(tls_config)
.manage(config.clone())
)
}))
.launch();
}
// 需要身份验证的路由
#[get("/hello")]
fn hello_protected() -> Status {
Status::Ok
}
Workers
The
workers
parameter sets the number of threads used for parallel task execution; there is no limit to the number of concurrent tasks. Due to a limitation in upstream async executers, unlike other values, theworkers
configuration value cannot be reconfigured or be configured from sources other than those provided byConfig::figment()
. In other words, only the values set by theROCKET_WORKERS
environment variable or in theworkers
property ofRocket.toml
will be considered - all otherworkers
values are ignored.
workers参数设置了用于并行任务执行的线程数;对并发任务的数量没有限制。由于上游异步执行器的限制,与其他数值不同,workers配置值不能被重新配置,也不能从Config::figment()提供的其他来源配置。换句话说,只有ROCKET_WORKERS环境变量或Rocket.toml的workers属性中设置的值才会被考虑,所有其他的workers值都会被忽略。
The
max_blocking
parameter sets an upper limit on the number of threads the underlyingasync
runtime will spawn to execute potentially blocking, synchronous tasks viaspawn_blocking
or equivalent. Similar to theworkers
parameter,max_blocking
cannot be reconfigured or be configured from sources other than those provided byConfig::figment()
. Unlikeworkers
, threads corresponding tomax_blocking
are not always active and will exit if idling. In general, the default value of512
should not be changed unless physical or virtual resources are scarce. Rocket only executes work on blocking threads when required such as when performing file system I/O viaTempFile
or wrapping synchronous work viarocket_sync_db_pools
.
max_blocking参数为底层异步运行时催生的线程数量设定了上限,以通过spawn_blocking或同等方式执行潜在的阻塞、同步任务。与workers参数类似,max_blocking不能被重新配置,也不能从Config::figment()提供的其他来源进行配置。与workers不同的是,max_blocking对应的线程并不总是活跃的,如果空闲,会退出。一般来说,除非物理或虚拟资源匮乏,否则不应改变默认值512。Rocket只在需要时在阻塞线程上执行工作,比如通过TempFile执行文件系统I/O或通过rocket_sync_db_pools包装同步工作。
Extracting Values
Your application can extract any configuration that implements
Deserialize
from the configured provider, which is exposed viaRocket::figment()
:
你的应用程序可以从配置的提供者中提取任何实现了反序列化的配置,该配置通过Rocket::figment()公开:
use rocket::serde::Deserialize;
#[launch]
fn rocket() -> _ {
let rocket = rocket::build();
let figment = rocket.figment();
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
struct Config {
port: u16,
custom: Vec<String>,
}
// extract the entire config any `Deserialize` value
//提取整个配置的任何`去序列化'值
let config: Config = figment.extract().expect("config");
// or a piece of it into any `Deserialize` value
//或其中的一部分进入任何 "解序列化 "值中
let custom: Vec<String> = figment.extract_inner("custom").expect("custom");
rocket
}
Both values recognized by Rocket and values not recognized by Rocket can be extracted. This means you can configure values recognized by your application in Rocket’s configuration sources directly. The next section describes how you can customize configuration sources by supplying your own
Provider
.
Rocket认可的值和Rocket不认可的值都可以被提取出来。这意味着你可以在Rocket的配置源中直接配置你的应用程序所识别的值。下一节将介绍如何通过提供你自己的提供者来定制配置源。
Because it is common to store configuration in managed state, Rocket provides an
AdHoc
fairing that 1) extracts a configuration from the configured provider, 2) pretty prints any errors, and 3) stores the value in managed state:
因为在管理状态下存储配置是很常见的,Rocket提供了一个AdHoc整流,
1)从配置的提供者中提取配置,
2)漂亮地打印出任何错误,
3)在管理状态下存储值:
use rocket::{State, fairing::AdHoc};
#[get("/custom")]
fn custom(config: &State<Config>) -> String {
config.custom.get(0).cloned().unwrap_or("default".into())
}
#[launch]
fn rocket() -> _ {
rocket::build()
.mount("/", routes![custom])
.attach(AdHoc::config::<Config>())
}
Custom Providers
A custom provider can be set via
rocket::custom()
, which replaces calls torocket::build()
. The configured provider can be built on top ofConfig::figment()
,Config::default()
, both, or neither. The Figment documentation has full details on instantiating existing providers likeToml
andJson
as well as creating custom providers for more complex cases.
自定义提供者可以通过rocket::custom()来设置,它取代了对rocket::build()的调用。配置的提供者可以建立在Config::figment()、Config::default()或两者之上,也可以不建立。Figment 文档有关于实例化现有提供者(如 Toml 和 Json)以及为更复杂的情况创建自定义提供者的全部细节。
Note: You may need to depend on
figment
andserde
directly.Rocket reexports
figment
andserde
from its crate root, so you can refer tofigment
types viarocket::figment
andserde
types viarocket::serde
. However, Rocket does not enable all features from either crate. As such, you may need to import crates directly:figment = { version = "0.10", features = ["env", "toml", "json"] }
注意:你可能需要直接依赖figment和serde。
Rocket从它的箱根中重新导出了figment和serde,所以你可以通过rocket::figment来引用figment类型,通过rocket::serde来引用serde类型。然而,Rocket并没有启用这两个板块的所有功能。因此,你可能需要直接导入crate:
As a first example, we override configuration values at runtime by merging figment’s tuple providers with Rocket’s default provider:
作为第一个例子,我们通过将figment的元组提供者与Rocket的默认提供者合并,在运行时覆盖配置值:
use rocket::data::{Limits, ToByteUnit};
#[launch]
fn rocket() -> _ {
let figment = rocket::Config::figment()
.merge(("port", 1111))
.merge(("limits", Limits::new().limit("json", 2.mebibytes())));
rocket::custom(figment).mount("/", routes![/* .. */])
}
More involved, consider an application that wants to use Rocket’s defaults for
Config
, but not its configuration sources, while allowing the application to be configured via anApp.toml
file that uses top-level keys as profiles (.nested()
),APP_
environment variables as global overrides (.global()
), andAPP_PROFILE
to configure the selected profile:
更为复杂的是,考虑一个希望使用Rocket的Config默认值,但不使用其配置源的应用程序,同时允许通过App.toml文件来配置应用程序,该文件使用顶级键作为profile(.nested()),APP_环境变量作为全局覆盖(.global()),以及APP_PROFILE来配置选定的profile:
use rocket::serde::{Serialize, Deserialize};
use rocket::fairing::AdHoc;
use figment::{Figment, Profile, providers::{Format, Toml, Serialized, Env}};
#[derive(Debug, Deserialize, Serialize)]
#[serde(crate = "rocket::serde")]
struct Config {
app_value: usize,
/* and so on.. */
}
impl Default for Config {
fn default() -> Config {
Config { app_value: 3, }
}
}
#[launch]
fn rocket() -> _ {
let figment = Figment::from(rocket::Config::default())
.merge(Serialized::defaults(Config::default()))
.merge(Toml::file("App.toml").nested())
.merge(Env::prefixed("APP_").global())
.select(Profile::from_env_or("APP_PROFILE", "default"));
rocket::custom(figment)
.mount("/", routes![/* .. */])
.attach(AdHoc::config::<Config>())
}
Rocket will extract its configuration from the configured provider. This means that if values like
port
andaddress
are configured inConfig
,App.toml
orAPP_
environment variables, Rocket will make use of them. The application can also extract its configuration, done here via theAdhoc::config()
fairing.
Rocket将从配置的提供者那里提取其配置。这意味着,如果端口和地址等值在Config、App.toml或APP_环境变量中被配置了,Rocket将利用它们。应用程序也可以提取其配置,这里通过Adhoc::config()fairing完成。
Pastebin Tutorial
This section of the guide is a tutorial intended to demonstrate how real-world Rocket applications are crafted. We’ll build a simple pastebin service that allows users to upload a file from any HTTP client, including
curl
. The service will respond back with a URL to the uploaded file.
本指南的这一部分是一个教程,旨在展示真实世界的Rocket应用程序是如何制作的。我们将建立一个简单的pastebin服务,允许用户从任何HTTP客户端上传文件,包括curl。该服务将回应一个上传文件的URL。
Note: What’s a pastebin?
A pastebin is a simple web application that allows users to upload a document and later retrieve it via a special URL. They’re often used to share code snippets, configuration files, and error logs.
注:什么是pastebin?
pastebin是一个简单的网络应用,它允许用户上传文件,然后通过一个特殊的URL检索它。它们经常被用来分享代码片段、配置文件和错误日志。
Finished Product
A souped-up, completed version of the application you’re about to build is deployed live at paste.rs. Feel free to play with the application to get a feel for how it works. For example, to upload a text document named
test.txt
, you can run:
你将要建立的应用程序的一个强化的、完整的版本已经部署在paste.rs上。你可以自由地玩玩这个应用程序,感受一下它是如何工作的。例如,要上传一个名为test.txt的文本文件,你可以运行:
curl --data-binary @test.txt https://paste.rs/
The finished product is composed of the following routes:
index
-#[get("/")]
returns a simple HTML page with instructions about how to use the service
upload
-#[post("/")]
accepts raw data in the body of the request and responds with a URL of a page containing the body’s content
retrieve
-#[get("/<id>")]
retrieves the content for the paste with id
<id>
成品由以下路线组成:
index - #[get("/")]
返回一个简单的HTML页面,包含关于如何使用该服务的说明
upload - #[post("/")]
接受请求正文中的原始数据,并以一个包含正文内容的页面的URL作为响应。
retrieve - #[get("/<id>")]
检索id为的粘贴内容。
Getting Started
Let’s get started! First, create a fresh Cargo binary project named
rocket-pastebin
:
让我们开始吧!首先,创建一个新的Cargo二进制项目,名为rocket-pastebin:
cargo new --bin rocket-pastebin
cd rocket-pastebin
Then add the usual Rocket dependencies to the
Cargo.toml
file:
然后在Cargo.toml文件中添加常规的Rocket依赖项:
[dependencies]
rocket = "=0.5.0-rc.3"
And finally, create a skeleton Rocket application to work off of in
src/main.rs
:
最后,在src/main.rs中创建一个Rocket应用程序的骨架来工作:
#[macro_use] extern crate rocket;
#[launch]
fn rocket() -> _ {
rocket::build()
}
Ensure everything works by running the application:
通过运行应用程序确保一切正常:
cargo run
At this point, we haven’t declared any routes or handlers, so visiting any page will result in Rocket returning a 404 error. Throughout the rest of the tutorial, we’ll create the three routes and accompanying handlers.
在这一点上,我们还没有声明任何路由或处理程序,所以访问任何页面都会导致Rocket返回404错误。在本教程的其余部分中,我们将创建三个路由和相应的处理程序。
Index
The first route we’ll create is
index
. This is the page users will see when they first visit the service. As such, the route should handleGET /
. We declare the route and its handler by adding theindex
function below tosrc/main.rs
:
我们要创建的第一个路由是index。这是用户第一次访问服务时将看到的页面。因此,该路由应该处理GET /。我们通过在 src/main.rs 中添加下面的 index 函数来声明这个路由和它的处理器:
#[get("/")]
fn index() -> &'static str {
"
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>`
"
}
This declares the
index
route for requests toGET /
as returning a static string with the specified contents. Rocket will take the string and return it as the body of a fully formed HTTP response withContent-Type: text/plain
. You can read more about how Rocket formulates responses in the responses section of the guide or at the API documentation for the Responder trait.
这声明了对GET/的请求的索引路由将返回一个具有指定内容的静态字符串。Rocket会接受这个字符串并将其作为一个完整的HTTP响应的主体返回,内容类型为:text/plain。你可以在本指南的响应部分或响应者特性的API文档中阅读更多关于Rocket如何形成响应的信息。
Remember that routes first need to be mounted before Rocket dispatches requests to them. To mount the
index
route, modify the main function so that it reads:
请记住,在Rocket向它们派发请求之前,路由首先需要被挂载。要挂载索引路由,请修改主函数,使其读取:
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index])
}
You should now be able to
cargo run
the application and visit the root path (/
) to see the text.
现在你应该可以货真价实地运行应用程序,并访问根路径(/)来查看文本。
Design
Before we continue, we’ll need to make a few design decisions.
在我们继续之前,我们需要做出一些设计决定。
Where should pastes be stored?
To keep things simple, we’ll store uploaded pastes on the file system inside of an
upload/
directory. Let’s create that directory next tosrc/
in our project now:
文件应该存放在哪里?
为了简单起见,我们将在文件系统中的upload/目录下存储上传的粘贴物。现在让我们在项目中的 src/ 旁边创建这个目录:
mkdir upload
Our project tree now looks like:
.
├── Cargo.toml
├── src
│ └── main.rs
└── upload
What should we name the uploaded paste files?
Similarly, we’ll keep things simple by naming paste files a string of random but readable characters. We’ll call this random string the paste’s “ID”. To represent, generate, and store the ID, we’ll create a
PasteId
structure in a new module file namedpaste_id.rs
with the following contents:
我们应该如何命名上传的粘贴文件?
同样,我们将通过给粘贴文件命名一串随机但可读的字符来保持简单。我们将这个随机字符串称为粘贴的 “ID”。为了表示、生成和存储该ID,我们将在一个名为paste_id.rs的新模块文件中创建一个PasteId结构,其内容如下:
use std::borrow::Cow;
use std::path::{Path, PathBuf};
use rand::{self, Rng};
/// A _probably_ unique paste ID.
pub struct PasteId<'a>(Cow<'a, str>);
impl PasteId<'_> {
/// Generate a _probably_ unique ID with `size` characters. For readability,
/// the characters used are from the sets [0-9], [A-Z], [a-z]. The
/// probability of a collision depends on the value of `size` and the number
/// of IDs generated thus far.
pub fn new(size: usize) -> PasteId<'static> {
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
let mut id = String::with_capacity(size);
let mut rng = rand::thread_rng();
for _ in 0..size {
id.push(BASE62[rng.gen::<usize>() % 62] as char);
}
PasteId(Cow::Owned(id))
}
/// Returns the path to the paste in `upload/` corresponding to this ID.
pub fn file_path(&self) -> PathBuf {
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
Path::new(root).join(self.0.as_ref())
}
}
在 Rust 中,有一些特殊的编译时变量可用于获取与编译环境相关的信息。以下是几个常用的特殊编译时变量:
CARGO_MANIFEST_DIR
: 表示 Cargo.toml 所在目录的路径。CARGO_PKG_VERSION
: 表示当前 crate 的版本号。CARGO_PKG_NAME
: 表示当前 crate 的名称。CARGO_PKG_DESCRIPTION
: 表示当前 crate 的描述信息。CARGO_PKG_AUTHORS
: 表示当前 crate 的作者信息。CARGO_PKG_LICENSE
: 表示当前 crate 的许可证信息。CARGO_PKG_HOMEPAGE
: 表示当前 crate 的主页 URL。
这些变量在编译时会被 Cargo 替换为相应的值,并可以在代码中使用。它们对于获取有关项目、版本和作者等信息非常有用。
We’ve given you the ID and path generation code for free. Our project tree now looks like:
我们已经免费为你提供了ID和路径生成代码。我们的项目树现在看起来像:
.
├── Cargo.toml
├── src
│ ├── main.rs
│ └── paste_id.rs # new! contains `PasteId`
└── upload
We’ll import the new module and struct in
src/main.rs
, after theextern crate rocket
:
我们将在 src/main.rs 中,在 extern crate rocket 之后导入新模块和结构:
mod paste_id;
use paste_id::PasteId;
You’ll notice that our code to generate paste IDs uses the
rand
crate, so we’ll need to add it as a dependency in ourCargo.toml
file:
你会注意到我们生成粘贴ID的代码使用了rand crate,所以我们需要在Cargo.toml文件中把它作为一个依赖项:
[dependencies]
## existing Rocket dependencies...
rand = "0.8"
Ensure that your application builds with the new code:
确保你的应用程序能用新的代码进行构建:
cargo build
You’ll likely see many “unused” warnings for the new code we’ve added: that’s okay and expected. We’ll be using the new code soon.
对于我们添加的新代码,你可能会看到许多 "未使用 "的警告:这没关系,也是意料之中的。我们很快就会使用这些新代码。
With these design decisions made, we’re ready to continue writing our application.
在做出这些设计决定后,我们准备继续编写我们的应用程序。
Retrieving Pastes
We’ll proceed with a
retrieve
route which, given an<id>
, will return the corresponding paste if it exists or otherwise 404. As we now know, that means we’ll be reading the contents of the file corresponding to<id>
in theupload/
directory and return them to the user.
我们将采用一个检索路由,给定一个,如果它存在,将返回相应的粘贴,否则就是404。正如我们现在所知,这意味着我们将读取upload/目录中对应的文件内容,并将其返回给用户。
Here’s a first take at implementing the
retrieve
route. The route below takes in an<id>
as a dynamic path element. The handler uses theid
to construct a path to the paste insideupload/
, and then attempts to open the file at that path, optionally returning theFile
if it exists. Rocket treats aNone
Responder as a 404 error, which is exactly what we want to return when the requested paste doesn’t exist.
这里是实现检索路由的第一次尝试。下面的路由接收了一个作为动态路径元素。处理程序使用id来构建一个指向upload/里面的粘贴文件的路径,然后尝试在这个路径上打开文件,如果文件存在,可以选择返回该文件。Rocket将None Responder视为404错误,这正是我们在请求的粘贴不存在时想要返回的。
use std::path::Path;
use rocket::tokio::fs::File;
#[get("/<id>")]
async fn retrieve(id: &str) -> Option<File> {
let upload_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
let filename = Path::new(upload_dir).join(id);
File::open(&filename).await.ok()
}
Make sure that the route is mounted at the root path:
确保路由被安装在根路径上:
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index, retrieve])
}
Give it a try! Create some fake pastes in the
upload/
directory, run the application, and try to retrieve them by visiting the corresponding URL.
试一试吧!在upload/目录下创建一些假的粘贴物,运行应用程序,并尝试通过访问相应的URL来检索它们。
A Problem
Unfortunately, there’s a problem with this code. Can you spot the issue? The
&str
type inretrieve
should tip you off! We’ve crafted a wonderful type to represent paste IDs but have ignored it!
不幸的是,这段代码有一个问题。你能发现这个问题吗?retrieve中的&str类型应该提示你!我们精心设计了一个美妙的类型来表示粘贴的ID,但却忽略了这一点
The issue is that the user controls the value of
id
, and as a result, can coerce the service into opening files insideupload/
that aren’t meant to be opened. For instance, imagine that you later decide that a special fileupload/_credentials.txt
will store some important, private information. If the user issues aGET
request to/_credentials.txt
, the server will read and return theupload/_credentials.txt
file, leaking the sensitive information. This is a big problem; it’s known as the full path disclosure attack, and Rocket provides the tools to prevent this and other kinds of attacks from happening.
问题是,用户控制着id的值,因此,可以胁迫服务打开upload/里面不应该被打开的文件。例如,想象一下,你后来决定一个特殊的文件upload/_credentials.txt将存储一些重要的私人信息。如果用户向/_credentials.txt发出GET请求,服务器将读取并返回upload/_credentials.txt文件,泄露敏感信息。这是一个大问题;它被称为全路径泄露攻击,Rocket提供了防止这种和其他类型攻击发生的工具。
The Solution
To prevent the attack, we need to validate
id
before we use it. We do so by using a type more specific than&str
to represent IDs and then asking Rocket to validate the untrustedid
input as that type. If validation fails, Rocket will take care to not call our routes with bad input.
为了防止这种攻击,我们需要在使用id之前对其进行验证。我们通过使用一个比&str更具体的类型来表示ID,然后要求Rocket将未受信任的id输入验证为该类型。如果验证失败,Rocket会注意不要用不良输入调用我们的路由。
Typed validation for dynamic parameters like
id
is implemented via theFromParam
trait. Rocket usesFromParam
to automatically validate and parse dynamic path parameters likeid
. We already have a type that represents valid paste IDs,PasteId
, so we’ll simply need to implementFromParam
forPasteId
.
像id这样的动态参数的类型验证是通过FromParam trait实现的。Rocket使用FromParam来自动验证和解析id等动态路径参数。我们已经有一个表示有效的粘贴ID的类型,PasteId,所以我们只需要为PasteId实现FromParam。
Here’s the
FromParam
implementation forPasteId
insrc/paste_id.rs
:
下面是 src/paste_id.rs 中 PasteId 的 FromParam 实现:
use rocket::request::FromParam;
/// Returns an instance of `PasteId` if the path segment is a valid ID.
/// Otherwise returns the invalid ID as the `Err` value.
impl<'a> FromParam<'a> for PasteId<'a> {
type Error = &'a str;
fn from_param(param: &'a str) -> Result<Self, Self::Error> {
param.chars().all(|c| c.is_ascii_alphanumeric())
.then(|| PasteId(param.into()))
.ok_or(param)
}
}
Note: This implementation, while secure, could be improved.
Our
from_param
function is simplistic and could be improved by, for example, checking that the length of theid
is within some known bound, introducing stricter character checks, checking for the existing of a paste file, and/or potentially blacklisting sensitive files as needed.
注意:这个实现虽然安全,但还可以改进。
我们的from_param函数是简单的,可以通过以下方式进行改进,例如,检查id的长度是否在某个已知范围内,引入更严格的字符检查,检查是否存在一个粘贴文件,和/或根据需要将敏感文件列入黑名单。
Given this implementation, we can change the type of
id
inretrieve
toPasteId
. Rocket will then ensure that<id>
represents a validPasteId
before calling theretrieve
route, preventing the previous attack entirely:
鉴于这种实现,我们可以将retrieve中的id类型改为PasteId。然后,Rocket将确保代表一个有效的PasteId,然后再调用retrieve路由,完全防止之前的攻击:
use rocket::tokio::fs::File;
#[get("/<id>")]
async fn retrieve(id: PasteId<'_>) -> Option<File> {
File::open(id.file_path()).await.ok()
}
Notice how much nicer this implementation is! And this time, it’s secure.
The wonderful thing about using
FromParam
and other Rocket traits is that they centralize policies. For instance, here, we’ve centralized the policy for validPasteId
s in dynamic parameters. At any point in the future, if other routes are added that require aPasteId
, no further work has to be done: simply use the type in the signature and Rocket takes care of the rest.
请注意,这个实施方案是多么的漂亮啊!而且这一次,它是安全的。
使用FromParam和其他Rocket特性的美妙之处在于,它们集中了策略。例如,在这里,我们把有效的PasteIds的策略集中在动态参数中。在未来的任何时候,如果添加了需要PasteId的其他路由,就不需要做进一步的工作:只需在签名中使用该类型,Rocket就会处理剩下的事情。
Uploading
Now that we can retrieve pastes safely, it’s time to actually store them. We’ll write an
upload
route that, according to our design, takes a paste’s contents and writes them to a file with a randomly generated ID inside of theupload/
directory. It’ll return a URL to the client for the paste corresponding to theretrieve
route we just route.
现在,我们可以安全地检索粘贴,现在是时候实际存储它们了。我们将编写一个上传路由,根据我们的设计,获取粘贴的内容并将其写入upload/目录下的一个随机生成的ID的文件。它将向客户端返回一个URL,以获取与我们刚才的retrieve路由相对应的粘贴内容。
Streaming Data
To stream the incoming paste data to a file, we’ll make use of
Data
, a data guard that represents an unopened stream to the incoming request body data. Before we show you the code, you should attempt to write the route yourself. Here’s a hint: one possible route and handler signature look like this:
为了将传入的粘贴数据流转到一个文件中,我们将利用Data,一个代表传入请求正文数据的未打开流的数据保护器。在我们向你展示代码之前,你应该尝试自己编写路由。这里有一个提示:一个可能的路由和处理程序签名看起来像这样:
use rocket::Data;
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
/* .. */
}
Your code should:
- Create a new
PasteId
of a length of your choosing.- Construct a path to the
PasteId
inside ofupload/
.- Stream the
Data
to the file at the constructed path.- Construct a URL for the
PasteId
.- Return the URL to the client.
你的代码应该:
1.创建一个新的PasteId,长度由你选择。
2.在upload/中构建一个指向PasteId的路径。
3.在构建的路径上将数据流向文件。
4.为PasteId构建一个URL。
5.将该URL返回给客户端。
Solution
Here’s our version:
// We derive `UriDisplayPath` for `PasteId` in `paste_id.rs`:
#[derive(UriDisplayPath)]
pub struct PasteId<'a>(Cow<'a, str>);
// We implement the `upload` route in `main.rs`:
use rocket::data::{Data, ToByteUnit};
use rocket::http::uri::Absolute;
// In a real application, these would be retrieved dynamically from a config.
const ID_LENGTH: usize = 3;
const HOST: Absolute<'static> = uri!("http://localhost:8000");
#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
let id = PasteId::new(ID_LENGTH);
paste.open(128.kibibytes()).into_file(id.file_path()).await?;
Ok(uri!(HOST, retrieve(id)).to_string())
}
We note the following Rocket APIs being used in our implementation:
- The
kibibytes()
method, which comes from theToByteUnit
trait.Data::open()
to openData
as aDataStream
.DataStream::into_file()
for writing the data stream into a file.- The
UriDisplayPath
derive, allowingPasteId
to be used inuri!
.- The
uri!
macro to crate type-safe, URL-safe URIs.
我们注意到在我们的实现中使用了以下Rocket APIs:
kibibytes()
方法,它来自ToByteUnit
特性。
Data::open()
将数据打开为DataStream
。
DataStream::into_file()
用于将数据流写进一个文件。
UriDisplayPath
派生,允许PasteId
被用于uri!
uri!
宏来创建类型安全的、URL安全的URI。
Ensure that the route is mounted at the root path:
确保路由被安装在根路径上:
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![index, retrieve, upload])
}
Test that your route works via
cargo run
. From a separate terminal, upload a file usingcurl
then retrieve the paste using the returned URL.
通过货物运行测试你的路线是否有效。从一个单独的终端,用curl上传一个文件,然后用返回的URL检索粘贴的内容。
## in the project root
cargo run
## in a separate terminal
echo "Hello, Rocket!" | curl --data-binary @- http://localhost:8000
## => http://localhost:8000/eGs
## confirm we can retrieve the paste (replace with URL from above)
curl http://localhost:8000/eGs
## we can check the contents of `upload/` as well
<ctrl-c> # kill running process
ls upload # ensure the upload is there
cat upload/* # ensure that contents are correct
Conclusion
That’s it! Ensure that all of your routes are mounted and test your application. You’ve now written a simple (~75 line!) pastebin in Rocket! There are many potential improvements to this small application, and we encourage you to work through some of them to get a better feel for Rocket. Here are some ideas:
- Add a web form to the
index
where users can manually input new pastes. Accept the form atPOST /
. Useformat
and/orrank
to specify which of the twoPOST /
routes should be called.- Support deletion of pastes by adding a new
DELETE /<id>
route. UsePasteId
to validate<id>
.- Indicate partial uploads with a 206 partial status code. If the user uploads a paste that meets or exceeds the allowed limit, return a 206 partial status code. Otherwise, return a 201 created status code.
- Set the
Content-Type
of the return value inupload
andretrieve
totext/plain
.- Return a unique “key” after each upload and require that the key is present and matches when doing deletion. Use one of Rocket’s core traits to do the key validation.
- Add a
PUT /<id>
route that allows a user with the key for<id>
to replace the existing paste, if any.- Add a new route,
GET /<id>/<lang>
that syntax highlights the paste with ID<id>
for language<lang>
. If<lang>
is not a known language, do no highlighting. Possibly validate<lang>
withFromParam
.- Use the
local
module to write unit tests for your pastebin.- Dispatch a thread before
launch
ing Rocket inmain
that periodically cleans up idling old pastes inupload/
.You can find the full source code for the completed pastebin tutorial on GitHub.
确保你所有的路由都已安装,并测试你的应用程序。你现在已经在Rocket中写了一个简单的(~75行!)pastebin这个小程序有很多潜在的改进,我们鼓励你通过一些改进来更好地了解Rocket。这里有一些想法:
1.在索引中添加一个网络表格,用户可以手动输入新的粘贴内容。在POST /上接受表单。使用格式和/或等级来指定应该调用两条POST/路线中的哪一条。
2通过添加一个新的DELETE /路由来支持删除粘贴。使用PasteId来验证。
3.用206部分状态代码表示部分上传。如果用户上传的粘贴符合或超过允许的限制,返回206部分状态代码。否则,返回一个201创建的状态代码。
4.在upload和retrieve中设置返回值的Content-Type为text/plain。
5.在每次上传后返回一个独特的 “键”,并要求在进行删除时键是存在的,并且是匹配的。使用Rocket的一个核心特征来做键的验证。
6.添加一个PUT /路由,允许用户用的密钥替换现有的粘贴,如果有的话。
7.添加一个新的路由,GET //,语法上突出ID为的语言的粘贴。如果不是一种已知的语言,就不做高亮处理。可能会用FromParam验证。
8.使用本地模块为你的pastebin编写单元测试。
9.在main中启动Rocket之前调度一个线程,定期清理upload/中闲置的旧粘贴。
你可以在GitHub上找到完整的pastebin教程的源代码。
https://rocket.rs/ 内容来自于rocket官网