mirror of https://github.com/rwf2/Rocket.git
parent
4c8bd61c4f
commit
fccb5759db
|
@ -1,7 +1,8 @@
|
||||||
#[macro_use] extern crate rocket;
|
#[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
mod paste_id;
|
mod paste_id;
|
||||||
#[cfg(test)] mod tests;
|
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
|
@ -12,8 +13,8 @@ use rocket::tokio::fs::{self, File};
|
||||||
|
|
||||||
use crate::paste_id::PasteId;
|
use crate::paste_id::PasteId;
|
||||||
|
|
||||||
|
// In a real application, these would be retrieved dynamically from a config.
|
||||||
const HOST: Absolute<'static> = uri!("http://localhost:8000");
|
const HOST: Absolute<'static> = uri!("http://localhost:8000");
|
||||||
|
|
||||||
const ID_LENGTH: usize = 3;
|
const ID_LENGTH: usize = 3;
|
||||||
|
|
||||||
#[post("/", data = "<paste>")]
|
#[post("/", data = "<paste>")]
|
||||||
|
@ -53,6 +54,5 @@ fn index() -> &'static str {
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build()
|
rocket::build().mount("/", routes![index, upload, delete, retrieve])
|
||||||
.mount("/", routes![index, upload, delete, retrieve])
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use rocket::http::uri::fmt;
|
|
||||||
use rocket::request::FromParam;
|
|
||||||
use rand::{self, Rng};
|
use rand::{self, Rng};
|
||||||
|
use rocket::request::FromParam;
|
||||||
/// Table to retrieve base62 values from.
|
|
||||||
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
||||||
|
|
||||||
/// A _probably_ unique paste ID.
|
/// A _probably_ unique paste ID.
|
||||||
#[derive(UriDisplayPath)]
|
#[derive(UriDisplayPath)]
|
||||||
|
@ -18,6 +14,8 @@ impl PasteId<'_> {
|
||||||
/// probability of a collision depends on the value of `size` and the number
|
/// probability of a collision depends on the value of `size` and the number
|
||||||
/// of IDs generated thus far.
|
/// of IDs generated thus far.
|
||||||
pub fn new(size: usize) -> PasteId<'static> {
|
pub fn new(size: usize) -> PasteId<'static> {
|
||||||
|
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||||
|
|
||||||
let mut id = String::with_capacity(size);
|
let mut id = String::with_capacity(size);
|
||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
for _ in 0..size {
|
for _ in 0..size {
|
||||||
|
@ -27,6 +25,7 @@ impl PasteId<'_> {
|
||||||
PasteId(Cow::Owned(id))
|
PasteId(Cow::Owned(id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path to the paste in `upload/` corresponding to this ID.
|
||||||
pub fn file_path(&self) -> PathBuf {
|
pub fn file_path(&self) -> PathBuf {
|
||||||
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
|
let root = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
|
||||||
Path::new(root).join(self.0.as_ref())
|
Path::new(root).join(self.0.as_ref())
|
||||||
|
@ -44,11 +43,3 @@ impl<'a> FromParam<'a> for PasteId<'a> {
|
||||||
.ok_or(param)
|
.ok_or(param)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> fmt::FromUriParam<fmt::Path, &'a str> for PasteId<'_> {
|
|
||||||
type Target = PasteId<'a>;
|
|
||||||
|
|
||||||
fn from_uri_param(param: &'a str) -> Self::Target {
|
|
||||||
PasteId(param.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use super::{rocket, index, PasteId};
|
use super::{rocket, index, PasteId};
|
||||||
use rocket::local::blocking::Client;
|
use rocket::local::blocking::Client;
|
||||||
use rocket::http::{Status, ContentType};
|
use rocket::http::{Status, ContentType};
|
||||||
|
use rocket::request::FromParam;
|
||||||
|
|
||||||
fn extract_id(from: &str) -> Option<String> {
|
fn extract_id(from: &str) -> Option<String> {
|
||||||
from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_end().to_string())
|
from.rfind('/').map(|i| &from[(i + 1)..]).map(|s| s.trim_end().to_string())
|
||||||
|
@ -25,6 +26,7 @@ fn upload_paste(client: &Client, body: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn download_paste(client: &Client, id: &str) -> Option<String> {
|
fn download_paste(client: &Client, id: &str) -> Option<String> {
|
||||||
|
let id = PasteId::from_param(id).expect("valid ID");
|
||||||
let response = client.get(uri!(super::retrieve(id))).dispatch();
|
let response = client.get(uri!(super::retrieve(id))).dispatch();
|
||||||
if response.status().class().is_success() {
|
if response.status().class().is_success() {
|
||||||
Some(response.into_string().unwrap())
|
Some(response.into_string().unwrap())
|
||||||
|
@ -34,6 +36,7 @@ fn download_paste(client: &Client, id: &str) -> Option<String> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_paste(client: &Client, id: &str) {
|
fn delete_paste(client: &Client, id: &str) {
|
||||||
|
let id = PasteId::from_param(id).expect("valid ID");
|
||||||
let response = client.delete(uri!(super::delete(id))).dispatch();
|
let response = client.delete(uri!(super::delete(id))).dispatch();
|
||||||
assert_eq!(response.status(), Status::Ok);
|
assert_eq!(response.status(), Status::Ok);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
# Pastebin
|
# Pastebin Tutorial
|
||||||
|
|
||||||
To give you a taste of what a real Rocket application looks like, this section
|
This section of the guide is a tutorial intended to demonstrate how real-world
|
||||||
of the guide is a tutorial on how to create a Pastebin application in Rocket. A
|
Rocket applications are crafted. We'll build a simple pastebin service that
|
||||||
pastebin is a simple web application that allows users to upload a text document
|
allows users to upload a file from any HTTP client, including `curl`. The
|
||||||
and later retrieve it via a special URL. They're often used to share code
|
service will respond back with a URL to the uploaded file.
|
||||||
snippets, configuration files, and error logs. In this tutorial, we'll build a
|
|
||||||
simple pastebin service that allows users to upload a file from their terminal.
|
! note: What's a pastebin?
|
||||||
The service will respond back with a URL to the uploaded file.
|
|
||||||
|
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.
|
||||||
|
|
||||||
## Finished Product
|
## Finished Product
|
||||||
|
|
||||||
A souped-up, completed version of the application you're about to build is
|
A souped-up, completed version of the application you're about to build is
|
||||||
deployed live at [paste.rs](https://paste.rs). Feel free to play with the
|
deployed live at [paste.rs](https://paste.rs). Feel free to play with the
|
||||||
application to get a feel for how it works. For example, to upload a text
|
application to get a feel for how it works. For example, to upload a text
|
||||||
document named `test.txt`, you can do:
|
document named `test.txt`, you can run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl --data-binary @test.txt https://paste.rs/
|
curl --data-binary @test.txt https://paste.rs/
|
||||||
|
@ -22,12 +25,18 @@ curl --data-binary @test.txt https://paste.rs/
|
||||||
|
|
||||||
The finished product is composed of the following routes:
|
The finished product is composed of the following routes:
|
||||||
|
|
||||||
* index: **`GET /`** - returns a simple HTML page with instructions about how
|
* `index` - `#[get("/")]`
|
||||||
to use the service
|
|
||||||
* upload: **`POST /`** - accepts raw data in the body of the request and
|
returns a simple HTML page with instructions about how to use the service
|
||||||
responds with a URL of a page containing the body's content
|
|
||||||
* retrieve: **`GET /<id>`** - retrieves the content for the paste with id
|
* `upload` - `#[post("/")]`
|
||||||
`<id>`
|
|
||||||
|
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>`
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
@ -70,10 +79,10 @@ tutorial, we'll create the three routes and accompanying handlers.
|
||||||
|
|
||||||
## Index
|
## Index
|
||||||
|
|
||||||
The first route we'll create is the `index` route. This is the page users will
|
The first route we'll create is `index`. This is the page users will see when
|
||||||
see when they first visit the service. As such, the route should field requests
|
they first visit the service. As such, the route should handle `GET /`. We
|
||||||
of the form `GET /`. We declare the route and its handler by adding the `index`
|
declare the route and its handler by adding the `index` function below to
|
||||||
function below to `src/main.rs`:
|
`src/main.rs`:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate rocket;
|
# #[macro_use] extern crate rocket;
|
||||||
|
@ -98,9 +107,11 @@ fn index() -> &'static str {
|
||||||
This declares the `index` route for requests to `GET /` as returning a static
|
This declares the `index` route for requests to `GET /` as returning a static
|
||||||
string with the specified contents. Rocket will take the string and return it as
|
string with the specified contents. Rocket will take the string and return it as
|
||||||
the body of a fully formed HTTP response with `Content-Type: text/plain`. You
|
the body of a fully formed HTTP response with `Content-Type: text/plain`. You
|
||||||
can read more about how Rocket formulates responses at the [API documentation
|
can read more about how Rocket formulates responses in the [responses section]
|
||||||
for the Responder
|
of the guide or at the [API documentation for the Responder
|
||||||
trait](@api/rocket/response/trait.Responder.html).
|
trait](@api/rocket/response/trait.Responder.html).
|
||||||
|
|
||||||
|
[responses section]: ../responses
|
||||||
|
|
||||||
Remember that routes first need to be mounted before Rocket dispatches requests
|
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:
|
to them. To mount the `index` route, modify the main function so that it reads:
|
||||||
|
@ -116,199 +127,123 @@ fn rocket() -> _ {
|
||||||
```
|
```
|
||||||
|
|
||||||
You should now be able to `cargo run` the application and visit the root path
|
You should now be able to `cargo run` the application and visit the root path
|
||||||
(`/`) to see the text being displayed.
|
(`/`) to see the text.
|
||||||
|
|
||||||
## Uploading
|
## Design
|
||||||
|
|
||||||
The most complicated aspect of the pastebin, as you might imagine, is handling
|
Before we continue, we'll need to make a few design decisions.
|
||||||
upload requests. When a user attempts to upload a pastebin, our service needs to
|
|
||||||
generate a unique ID for the upload, read the data, write it out to a file or
|
|
||||||
database, and then return a URL with the ID. We'll take each of these one step
|
|
||||||
at a time, beginning with generating IDs.
|
|
||||||
|
|
||||||
### Unique IDs
|
* **Where should pastes be stored?**
|
||||||
|
|
||||||
Generating a unique and useful ID is an interesting topic, but it is outside the
|
To keep things simple, we'll store uploaded pastes on the file system inside
|
||||||
scope of this tutorial. Instead, we simply provide the code for a `PasteId`
|
of an `upload/` directory. Let's create that directory next to `src/` in our
|
||||||
structure that represents a _probably_ unique ID. Read through the code, then
|
project now:
|
||||||
copy/paste it into a new file named `paste_id.rs` in the `src/` directory:
|
|
||||||
|
|
||||||
```rust
|
```sh
|
||||||
use std::fmt;
|
mkdir upload
|
||||||
use std::borrow::Cow;
|
```
|
||||||
|
|
||||||
use rand::{self, Rng};
|
Our project tree now looks like:
|
||||||
|
|
||||||
/// Table to retrieve base62 values from.
|
```sh
|
||||||
const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
.
|
||||||
|
├── Cargo.toml
|
||||||
|
├── src
|
||||||
|
│ └── main.rs
|
||||||
|
└── upload
|
||||||
|
```
|
||||||
|
|
||||||
/// A _probably_ unique paste ID.
|
* **What should we name the uploaded paste files?**
|
||||||
pub struct PasteId<'a>(Cow<'a, str>);
|
|
||||||
|
|
||||||
impl<'a> PasteId<'a> {
|
Similarly, we'll keep things simple by naming paste files a string of random
|
||||||
/// Generate a _probably_ unique ID with `size` characters. For readability,
|
but readable characters. We'll call this random string the paste's "ID". To
|
||||||
/// the characters used are from the sets [0-9], [A-Z], [a-z]. The
|
represent, generate, and store the ID, we'll create a `PasteId` structure in
|
||||||
/// probability of a collision depends on the value of `size` and the number
|
a new module file named `paste_id.rs` with the following contents:
|
||||||
/// of IDs generated thus far.
|
|
||||||
pub fn new(size: usize) -> PasteId<'static> {
|
```rust
|
||||||
let mut id = String::with_capacity(size);
|
use std::borrow::Cow;
|
||||||
let mut rng = rand::thread_rng();
|
use std::path::{Path, PathBuf};
|
||||||
for _ in 0..size {
|
|
||||||
id.push(BASE62[rng.gen::<usize>() % 62] as char);
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
```
|
||||||
```
|
|
||||||
|
|
||||||
Then, in `src/main.rs`, add the following after `extern crate rocket`:
|
We've given you the ID and path generation code for free. Our project tree
|
||||||
|
now looks like:
|
||||||
|
|
||||||
```rust
|
```sh
|
||||||
# /*
|
.
|
||||||
mod paste_id;
|
├── Cargo.toml
|
||||||
# */ mod paste_id { pub struct PasteId; }
|
├── src
|
||||||
|
│ ├── main.rs
|
||||||
|
│ └── paste_id.rs # new! contains `PasteId`
|
||||||
|
└── upload
|
||||||
|
```
|
||||||
|
|
||||||
use paste_id::PasteId;
|
We'll import the new module and struct in `src/main.rs`, after the `extern
|
||||||
```
|
crate rocket`:
|
||||||
|
|
||||||
Finally, add a dependency for the `rand` crate to the `Cargo.toml` file:
|
```rust
|
||||||
|
# /*
|
||||||
|
mod paste_id;
|
||||||
|
# */ mod paste_id { pub struct PasteId; }
|
||||||
|
|
||||||
```toml
|
use crate::paste_id::PasteId;
|
||||||
[dependencies]
|
```
|
||||||
# existing Rocket dependencies...
|
|
||||||
rand = "0.8"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, ensure that your application builds with the new code:
|
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 our `Cargo.toml` file:
|
||||||
|
|
||||||
```sh
|
```toml
|
||||||
cargo build
|
[dependencies]
|
||||||
```
|
## existing Rocket dependencies...
|
||||||
|
rand = "0.8"
|
||||||
|
```
|
||||||
|
|
||||||
You'll likely see many "unused" warnings for the new code we've added: that's
|
Ensure that your application builds with the new code:
|
||||||
okay and expected. We'll be using the new code soon.
|
|
||||||
|
|
||||||
### Processing
|
```sh
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
Believe it or not, the hard part is done! (_whew!_).
|
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.
|
||||||
|
|
||||||
To process the upload, we'll need a place to store the uploaded files. To
|
With these design decisions made, we're ready to continue writing our
|
||||||
simplify things, we'll store the uploads in a directory named `upload/`. Create
|
application.
|
||||||
an `upload` directory next to the `src` directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
mkdir upload
|
|
||||||
```
|
|
||||||
|
|
||||||
For the `upload` route, we'll need to import `Data`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use rocket::Data;
|
|
||||||
```
|
|
||||||
|
|
||||||
The [Data](@api/rocket/data/struct.Data.html) structure is key
|
|
||||||
here: it represents an unopened stream to the incoming request body data. We'll
|
|
||||||
use it to efficiently stream the incoming request to a file.
|
|
||||||
|
|
||||||
### Upload Route
|
|
||||||
|
|
||||||
We're finally ready to write the `upload` route. Before we show you the code,
|
|
||||||
you should attempt to write the route yourself. Here's a hint: a possible route
|
|
||||||
and handler signature look like this:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #[macro_use] extern crate rocket;
|
|
||||||
|
|
||||||
use rocket::Data;
|
|
||||||
use rocket::response::Debug;
|
|
||||||
|
|
||||||
#[post("/", data = "<paste>")]
|
|
||||||
fn upload(paste: Data<'_>) -> std::io::Result<String> {
|
|
||||||
# unimplemented!()
|
|
||||||
/* .. */
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Your code should:
|
|
||||||
|
|
||||||
1. Create a new `PasteId` of a length of your choosing.
|
|
||||||
2. Construct a filename inside `upload/` given the `PasteId`.
|
|
||||||
3. Stream the `Data` to the file with the constructed filename.
|
|
||||||
4. Construct a URL given the `PasteId`.
|
|
||||||
5. Return the URL to the client.
|
|
||||||
|
|
||||||
Here's our version (in `src/main.rs`):
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #[macro_use] extern crate rocket;
|
|
||||||
# fn main() {}
|
|
||||||
|
|
||||||
# use std::fmt;
|
|
||||||
# struct PasteId;
|
|
||||||
# impl PasteId { fn new(n: usize) -> Self { PasteId } }
|
|
||||||
# impl fmt::Display for PasteId {
|
|
||||||
# fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { Ok(()) }
|
|
||||||
# }
|
|
||||||
|
|
||||||
use rocket::response::Debug;
|
|
||||||
use rocket::data::{Data, ToByteUnit};
|
|
||||||
|
|
||||||
#[post("/", data = "<paste>")]
|
|
||||||
async fn upload(paste: Data<'_>) -> Result<String, Debug<std::io::Error>> {
|
|
||||||
let id = PasteId::new(3);
|
|
||||||
let filename = format!("upload/{id}", id = id);
|
|
||||||
let url = format!("{host}/{id}\n", host = "http://localhost:8000", id = id);
|
|
||||||
|
|
||||||
// Write the paste out, limited to 128KiB, and return the URL.
|
|
||||||
paste.open(128.kibibytes()).into_file(filename).await?;
|
|
||||||
Ok(url)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note the [`kibibytes()`] method call: this method comes from the [`ToByteUnit`]
|
|
||||||
extension trait. Ensure that the route is mounted at the root path:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
# #[macro_use] extern crate rocket;
|
|
||||||
|
|
||||||
# #[get("/")] fn index() {}
|
|
||||||
# #[post("/")] fn upload() {}
|
|
||||||
|
|
||||||
#[launch]
|
|
||||||
fn rocket() -> _ {
|
|
||||||
rocket::build().mount("/", routes![index, upload])
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Test that your route works via `cargo run`. From a separate terminal, upload a
|
|
||||||
file using `curl`. Then verify that the file was saved to the `upload` directory
|
|
||||||
with the correct ID:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# in the project root
|
|
||||||
cargo run
|
|
||||||
|
|
||||||
# in a separate terminal
|
|
||||||
echo "Hello, world." | curl --data-binary @- http://localhost:8000
|
|
||||||
# => http://localhost:8000/eGs
|
|
||||||
|
|
||||||
# back to the terminal running the pastebin
|
|
||||||
<ctrl-c> # kill running process
|
|
||||||
ls upload # ensure the upload is there
|
|
||||||
cat upload/* # ensure that contents are correct
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that since we haven't created a `GET /<id>` route, visiting the returned URL
|
|
||||||
will result in a **404**. We'll fix that now.
|
|
||||||
|
|
||||||
[`kibibytes()`]: @api/rocket/data/trait.ToByteUnit.html#tymethod.kibibytes
|
|
||||||
[`ToByteUnit`]: @api/rocket/data/trait.ToByteUnit.html
|
|
||||||
|
|
||||||
## Retrieving Pastes
|
## Retrieving Pastes
|
||||||
|
|
||||||
The final step is to create the `retrieve` route which, given an `<id>`, will
|
We'll proceed with a `retrieve` route which, given an `<id>`, will return the
|
||||||
return the corresponding paste if it exists.
|
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 the
|
||||||
|
`upload/` directory and return them to the user.
|
||||||
|
|
||||||
Here's a first take at implementing the `retrieve` route. The route below takes
|
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 the `id` to construct a
|
in an `<id>` as a dynamic path element. The handler uses the `id` to construct a
|
||||||
|
@ -321,11 +256,13 @@ paste doesn't exist.
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate rocket;
|
# #[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
use rocket::tokio::fs::File;
|
use rocket::tokio::fs::File;
|
||||||
|
|
||||||
#[get("/<id>")]
|
#[get("/<id>")]
|
||||||
async fn retrieve(id: &str) -> Option<File> {
|
async fn retrieve(id: &str) -> Option<File> {
|
||||||
let filename = format!("upload/{id}", id = id);
|
let upload_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/", "upload");
|
||||||
|
let filename = Path::new(upload_dir).join(id);
|
||||||
File::open(&filename).await.ok()
|
File::open(&filename).await.ok()
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -336,17 +273,22 @@ Make sure that the route is mounted at the root path:
|
||||||
# #[macro_use] extern crate rocket;
|
# #[macro_use] extern crate rocket;
|
||||||
|
|
||||||
# #[get("/")] fn index() {}
|
# #[get("/")] fn index() {}
|
||||||
# #[post("/")] fn upload() {}
|
|
||||||
# #[get("/<id>")] fn retrieve(id: String) {}
|
# #[get("/<id>")] fn retrieve(id: String) {}
|
||||||
|
|
||||||
#[launch]
|
#[launch]
|
||||||
fn rocket() -> _ {
|
fn rocket() -> _ {
|
||||||
rocket::build().mount("/", routes![index, upload, retrieve])
|
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.
|
||||||
|
|
||||||
|
### A Problem
|
||||||
|
|
||||||
Unfortunately, there's a problem with this code. Can you spot the issue? The
|
Unfortunately, there's a problem with this code. Can you spot the issue? The
|
||||||
`&str` type should tip you off!
|
`&str` type in `retrieve` should tip you off! We've crafted a wonderful type to
|
||||||
|
represent paste IDs but have ignored it!
|
||||||
|
|
||||||
The issue is that the _user_ controls the value of `id`, and as a result, can
|
The issue is that the _user_ controls the value of `id`, and as a result, can
|
||||||
coerce the service into opening files inside `upload/` that aren't meant to be
|
coerce the service into opening files inside `upload/` that aren't meant to be
|
||||||
|
@ -358,20 +300,27 @@ This is a big problem; it's known as the [full path disclosure
|
||||||
attack](https://www.owasp.org/index.php/Full_Path_Disclosure), and Rocket
|
attack](https://www.owasp.org/index.php/Full_Path_Disclosure), and Rocket
|
||||||
provides the tools to prevent this and other kinds of attacks from happening.
|
provides the tools to prevent this and other kinds of attacks from happening.
|
||||||
|
|
||||||
To prevent the attack, we need to _validate_ `id` before we use it. Since the
|
### The Solution
|
||||||
`id` is a dynamic parameter, we can use Rocket's
|
|
||||||
[FromParam](@api/rocket/request/trait.FromParam.html) trait to
|
To prevent the attack, we need to _validate_ `id` before we use it. We do so by
|
||||||
implement the validation and ensure that the `id` is a valid `PasteId` before
|
using a type more specific than `&str` to represent IDs and then asking Rocket
|
||||||
using it. We do this by implementing `FromParam` for `PasteId` in
|
to validate the untrusted `id` input as that type. If validation fails, Rocket
|
||||||
`src/paste_id.rs`, as below:
|
will take care to not call our routes with bad input.
|
||||||
|
|
||||||
|
Typed validation for dynamic paramters like `id` is implemented via the
|
||||||
|
[`FromParam`] trait. Rocket uses `FromParam` to automatically validate and parse
|
||||||
|
dynamic path parameters like `id`. We already have a type that represents valid
|
||||||
|
paste IDs, `PasteId`, so we'll simply need to implement `FromParam` for
|
||||||
|
`PasteId`.
|
||||||
|
|
||||||
|
Here's the `FromParam` implementation for `PasteId` in `src/paste_id.rs`:
|
||||||
|
|
||||||
|
[`FromParam`]: @api/rocket/request/trait.FromParam.html
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use rocket::request::FromParam;
|
use rocket::request::FromParam;
|
||||||
|
# use std::borrow::Cow;
|
||||||
/// A _probably_ unique paste ID.
|
# pub struct PasteId<'a>(Cow<'a, str>);
|
||||||
pub struct PasteId<'a>(Cow<'a, str>);
|
|
||||||
|
|
||||||
/// Returns an instance of `PasteId` if the path segment is a valid ID.
|
/// Returns an instance of `PasteId` if the path segment is a valid ID.
|
||||||
/// Otherwise returns the invalid ID as the `Err` value.
|
/// Otherwise returns the invalid ID as the `Err` value.
|
||||||
|
@ -379,36 +328,48 @@ impl<'a> FromParam<'a> for PasteId<'a> {
|
||||||
type Error = &'a str;
|
type Error = &'a str;
|
||||||
|
|
||||||
fn from_param(param: &'a str) -> Result<Self, Self::Error> {
|
fn from_param(param: &'a str) -> Result<Self, Self::Error> {
|
||||||
match param.chars().all(|c| c.is_ascii_alphanumeric()) {
|
param.chars().all(|c| c.is_ascii_alphanumeric())
|
||||||
true => Ok(PasteId(param.into())),
|
.then(|| PasteId(param.into()))
|
||||||
false => Err(param)
|
.ok_or(param)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, we simply need to change the type of `id` in the handler to `PasteId`.
|
! note: This implementation, while secure, could be improved.
|
||||||
Rocket will then ensure that `<id>` represents a valid `PasteId` before calling
|
|
||||||
the `retrieve` route, preventing attacks on the `retrieve` route:
|
Our `from_param` function is simplistic and could be improved by, for example,
|
||||||
|
checking that the length of the `id` 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.
|
||||||
|
|
||||||
|
Given this implementation, we can change the type of `id` in `retrieve` to
|
||||||
|
`PasteId`. Rocket will then ensure that `<id>` represents a valid `PasteId`
|
||||||
|
before calling the `retrieve` route, preventing the previous attack entirely:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
# #[macro_use] extern crate rocket;
|
# #[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
use rocket::tokio::fs::File;
|
||||||
# use std::borrow::Cow;
|
# use std::borrow::Cow;
|
||||||
# use rocket::tokio::fs::File;
|
# use std::path::PathBuf;
|
||||||
|
# use rocket::request::FromParam;
|
||||||
# type PasteId<'a> = &'a str;
|
# pub struct PasteId<'a>(Cow<'a, str>);
|
||||||
|
# impl PasteId<'_> {
|
||||||
|
# pub fn new(size: usize) -> PasteId<'static> { todo!() }
|
||||||
|
# pub fn file_path(&self) -> PathBuf { todo!() }
|
||||||
|
# }
|
||||||
|
# impl<'a> FromParam<'a> for PasteId<'a> {
|
||||||
|
# type Error = &'a str;
|
||||||
|
# fn from_param(param: &'a str) -> Result<Self, Self::Error> { todo!() }
|
||||||
|
# }
|
||||||
|
|
||||||
#[get("/<id>")]
|
#[get("/<id>")]
|
||||||
async fn retrieve(id: PasteId<'_>) -> Option<File> {
|
async fn retrieve(id: PasteId<'_>) -> Option<File> {
|
||||||
let filename = format!("upload/{id}", id = id);
|
File::open(id.file_path()).await.ok()
|
||||||
File::open(&filename).await.ok()
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that our `from_param` function is simplistic and could be improved by, for
|
Notice how much nicer this implementation is! And this time, it's secure.
|
||||||
example, checking that the length of the `id` is within some known bound or
|
|
||||||
potentially blacklisting sensitive files as needed.
|
|
||||||
|
|
||||||
The wonderful thing about using `FromParam` and other Rocket traits is that they
|
The wonderful thing about using `FromParam` and other Rocket traits is that they
|
||||||
centralize policies. For instance, here, we've centralized the policy for valid
|
centralize policies. For instance, here, we've centralized the policy for valid
|
||||||
|
@ -416,6 +377,140 @@ centralize policies. For instance, here, we've centralized the policy for valid
|
||||||
are added that require a `PasteId`, no further work has to be done: simply use
|
are added that require a `PasteId`, no further work has to be done: simply use
|
||||||
the type in the signature and Rocket takes care of the rest.
|
the type in the signature and Rocket takes care of the rest.
|
||||||
|
|
||||||
|
|
||||||
|
## 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 the `upload/`
|
||||||
|
directory. It'll return a URL to the client for the paste corresponding to the
|
||||||
|
`retrieve` route we just route.
|
||||||
|
|
||||||
|
### 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:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate rocket;
|
||||||
|
use rocket::Data;
|
||||||
|
|
||||||
|
#[post("/", data = "<paste>")]
|
||||||
|
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
|
||||||
|
/* .. */
|
||||||
|
# Ok("".into())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[`Data`]: @api/rocket/data/struct.Data.html
|
||||||
|
[data guard]: ../requests/#body-data
|
||||||
|
|
||||||
|
Your code should:
|
||||||
|
|
||||||
|
1. Create a new `PasteId` of a length of your choosing.
|
||||||
|
2. Construct a path to the `PasteId` inside of `upload/`.
|
||||||
|
3. Stream the `Data` to the file at the constructed path.
|
||||||
|
4. Construct a URL for the `PasteId`.
|
||||||
|
5. Return the URL to the client.
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
|
||||||
|
Here's our version:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
// We derive `UriDisplayPath` for `PasteId` in `paste_id.rs`:
|
||||||
|
# use std::borrow::Cow;
|
||||||
|
# use std::path::{Path, PathBuf};
|
||||||
|
# use rocket::request::FromParam;
|
||||||
|
|
||||||
|
#[derive(UriDisplayPath)]
|
||||||
|
pub struct PasteId<'a>(Cow<'a, str>);
|
||||||
|
|
||||||
|
# impl PasteId<'_> {
|
||||||
|
# pub fn new(size: usize) -> PasteId<'static> { todo!() }
|
||||||
|
# pub fn file_path(&self) -> PathBuf { todo!() }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
# impl<'a> FromParam<'a> for PasteId<'a> {
|
||||||
|
# type Error = &'a str;
|
||||||
|
# fn from_param(param: &'a str) -> Result<Self, Self::Error> { todo!() }
|
||||||
|
# }
|
||||||
|
// We implement the `upload` route in `main.rs`:
|
||||||
|
|
||||||
|
use rocket::data::{Data, ToByteUnit};
|
||||||
|
use rocket::http::uri::Absolute;
|
||||||
|
# use rocket::tokio::fs::File;
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
# #[get("/")] fn index() -> &'static str { "" }
|
||||||
|
# #[get("/<id>")] fn retrieve(id: PasteId<'_>) -> Option<File> { todo!() }
|
||||||
|
|
||||||
|
#[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 the [`ToByteUnit`] trait.
|
||||||
|
* [`Data::open()`] to open [`Data`] as a [`DataStream`].
|
||||||
|
* [`DataStream::into_file()`] for writing the data stream into a file.
|
||||||
|
* The [`UriDisplayPath`] derive, allowing `PasteId` to be used in [`uri!`].
|
||||||
|
* The [`uri!`] macro to crate type-safe, URL-safe URIs.
|
||||||
|
|
||||||
|
[`Data::open()`]: @api/rocket/data/struct.Data.html#method.open
|
||||||
|
[`Data`]: @api/rocket/data/struct.Data.html
|
||||||
|
[`DataStream`]: @api/rocket/data/struct.DataStream.html
|
||||||
|
[`DataStream::into_file()`]: @api/rocket/data/struct.DataStream.html#method.into_file
|
||||||
|
[`uri!`]: @api/rocket/macro.uri.html
|
||||||
|
[`kibibytes()`]: @api/rocket/data/trait.ToByteUnit.html#tymethod.kibibytes
|
||||||
|
[`ToByteUnit`]: @api/rocket/data/trait.ToByteUnit.html
|
||||||
|
[`UriDisplayPath`]: @api/rocket/derive.UriDisplayPath.html
|
||||||
|
|
||||||
|
Ensure that the route is mounted at the root path:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# #[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
# #[get("/")] fn index() {}
|
||||||
|
# #[get("/<id>")] fn retrieve(id: &str) {}
|
||||||
|
# #[post("/")] fn upload() {}
|
||||||
|
|
||||||
|
#[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 using `curl` then retrieve the paste using the returned URL.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
## 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
|
## Conclusion
|
||||||
|
|
||||||
That's it! Ensure that all of your routes are mounted and test your application.
|
That's it! Ensure that all of your routes are mounted and test your application.
|
||||||
|
|
|
@ -28,8 +28,8 @@ aspect of Rocket. The sections are:
|
||||||
- **[Testing](testing/):** how to unit and integration test a Rocket
|
- **[Testing](testing/):** how to unit and integration test a Rocket
|
||||||
application.
|
application.
|
||||||
- **[Configuration](configuration/):** how to configure a Rocket application.
|
- **[Configuration](configuration/):** how to configure a Rocket application.
|
||||||
- **[Pastebin](pastebin/):** a tutorial on how to create a pastebin with
|
- **[Pastebin Tutorial](pastebin/):** a tutorial on how to create a pastebin
|
||||||
Rocket.
|
with Rocket.
|
||||||
- **[Conclusion](conclusion/):** concludes the guide and discusses next steps
|
- **[Conclusion](conclusion/):** concludes the guide and discusses next steps
|
||||||
for learning.
|
for learning.
|
||||||
- **[FAQ](faq/):** answers to frequently asked questions about Rocket and
|
- **[FAQ](faq/):** answers to frequently asked questions about Rocket and
|
||||||
|
|
Loading…
Reference in New Issue