The ins and outs of Rocket, in detail.

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.

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.

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:

1
curl --data-binary @test.txt https://paste.rs/

The finished product is composed of the following routes:

Getting Started

Let's get started! First, create a fresh Cargo binary project named rocket-pastebin:

1
2
cargo new --bin rocket-pastebin
cd rocket-pastebin

Then add the usual Rocket dependencies to the Cargo.toml file:

1
2
[dependencies]
rocket = "0.5.1"

And finally, create a skeleton Rocket application to work off of in src/main.rs:

1
2
3
4
5
6
#[macro_use] extern crate rocket;

#[launch]
fn rocket() -> _ {
    rocket::build()
}

Ensure everything works by running the application:

1
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.

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 handle GET /. We declare the route and its handler by adding the index function below to src/main.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[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 to GET / 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 with Content-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.

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:

1
2
3
4
#[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.

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 the upload/ directory and return them to the user.

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 path to the paste inside upload/, and then attempts to open the file at that path, optionally returning the File if it exists. Rocket treats a None Responder as a 404 error, which is exactly what we want to return when the requested paste doesn't exist.

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
#[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.

A Problem

Unfortunately, there's a problem with this code. Can you spot the issue? The &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 coerce the service into opening files inside upload/ that aren't meant to be opened. For instance, imagine that you later decide that a special file upload/_credentials.txt will store some important, private information. If the user issues a GET request to /_credentials.txt, the server will read and return the upload/_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.

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 untrusted id input as that type. If validation fails, Rocket will take care to not call our routes with bad input.

Typed validation for dynamic parameters 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
    }
}

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 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:

1
2
3
4
5
6
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 valid PasteIds in dynamic parameters. At any point in the future, if other routes 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.

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 wrote.

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:

1
2
3
4
5
6
use rocket::Data;

#[post("/", data = "<paste>")]
async fn upload(paste: Data<'_>) -> std::io::Result<String> {
    /* .. */
}

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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:

Ensure that the route is mounted at the root path:

1
2
3
4
#[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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
## 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:

You can find the full source code for the completed pastebin tutorial on GitHub.