From fccb5759db6cd0e038a5db4e9d6dcb1c2208fc43 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Sun, 8 May 2022 01:57:49 -0500 Subject: [PATCH] Revamp pastebin tutorial for Rocket v0.5. Closes #1756. --- examples/pastebin/src/main.rs | 8 +- examples/pastebin/src/paste_id.rs | 17 +- examples/pastebin/src/tests.rs | 3 + site/guide/10-pastebin.md | 533 ++++++++++++++++++------------ site/guide/index.md | 4 +- 5 files changed, 327 insertions(+), 238 deletions(-) diff --git a/examples/pastebin/src/main.rs b/examples/pastebin/src/main.rs index e88e0c07..8dd29c72 100644 --- a/examples/pastebin/src/main.rs +++ b/examples/pastebin/src/main.rs @@ -1,7 +1,8 @@ #[macro_use] extern crate rocket; +#[cfg(test)] +mod tests; mod paste_id; -#[cfg(test)] mod tests; use std::io; @@ -12,8 +13,8 @@ use rocket::tokio::fs::{self, File}; 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 ID_LENGTH: usize = 3; #[post("/", data = "")] @@ -53,6 +54,5 @@ fn index() -> &'static str { #[launch] fn rocket() -> _ { - rocket::build() - .mount("/", routes![index, upload, delete, retrieve]) + rocket::build().mount("/", routes![index, upload, delete, retrieve]) } diff --git a/examples/pastebin/src/paste_id.rs b/examples/pastebin/src/paste_id.rs index 13442756..2213a019 100644 --- a/examples/pastebin/src/paste_id.rs +++ b/examples/pastebin/src/paste_id.rs @@ -1,12 +1,8 @@ use std::borrow::Cow; use std::path::{Path, PathBuf}; -use rocket::http::uri::fmt; -use rocket::request::FromParam; use rand::{self, Rng}; - -/// Table to retrieve base62 values from. -const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; +use rocket::request::FromParam; /// A _probably_ unique paste ID. #[derive(UriDisplayPath)] @@ -18,6 +14,8 @@ impl PasteId<'_> { /// 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 { @@ -27,6 +25,7 @@ impl PasteId<'_> { 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()) @@ -44,11 +43,3 @@ impl<'a> FromParam<'a> for PasteId<'a> { .ok_or(param) } } - -impl<'a> fmt::FromUriParam for PasteId<'_> { - type Target = PasteId<'a>; - - fn from_uri_param(param: &'a str) -> Self::Target { - PasteId(param.into()) - } -} diff --git a/examples/pastebin/src/tests.rs b/examples/pastebin/src/tests.rs index ae9b6bc2..802fba28 100644 --- a/examples/pastebin/src/tests.rs +++ b/examples/pastebin/src/tests.rs @@ -1,6 +1,7 @@ use super::{rocket, index, PasteId}; use rocket::local::blocking::Client; use rocket::http::{Status, ContentType}; +use rocket::request::FromParam; fn extract_id(from: &str) -> Option { 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 { + let id = PasteId::from_param(id).expect("valid ID"); let response = client.get(uri!(super::retrieve(id))).dispatch(); if response.status().class().is_success() { Some(response.into_string().unwrap()) @@ -34,6 +36,7 @@ fn download_paste(client: &Client, id: &str) -> Option { } 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(); assert_eq!(response.status(), Status::Ok); } diff --git a/site/guide/10-pastebin.md b/site/guide/10-pastebin.md index 2bbcc7b9..99914e2d 100644 --- a/site/guide/10-pastebin.md +++ b/site/guide/10-pastebin.md @@ -1,19 +1,22 @@ -# Pastebin +# Pastebin Tutorial -To give you a taste of what a real Rocket application looks like, this section -of the guide is a tutorial on how to create a Pastebin application in Rocket. A -pastebin is a simple web application that allows users to upload a text document -and later retrieve it via a special URL. They're often used to share code -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. -The service will respond back with a URL to the uploaded file. +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. + +! 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. ## Finished Product 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 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 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: - * 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 /`** - retrieves the content for the paste with id - `` + * `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("/")]` + + retrieves the content for the paste with id `` ## Getting Started @@ -70,10 +79,10 @@ tutorial, we'll create the three routes and accompanying handlers. ## Index -The first route we'll create is the `index` route. This is the page users will -see when they first visit the service. As such, the route should field requests -of the form `GET /`. We declare the route and its handler by adding the `index` -function below to `src/main.rs`: +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`: ```rust # #[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 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 at the [API documentation -for the Responder - trait](@api/rocket/response/trait.Responder.html). +can read more about how Rocket formulates responses in the [responses section] +of the guide or at the [API documentation for the Responder +trait](@api/rocket/response/trait.Responder.html). + +[responses section]: ../responses 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: @@ -116,199 +127,123 @@ fn rocket() -> _ { ``` 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 -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. +Before we continue, we'll need to make a few design decisions. -### Unique IDs + * **Where should pastes be stored?** -Generating a unique and useful ID is an interesting topic, but it is outside the -scope of this tutorial. Instead, we simply provide the code for a `PasteId` -structure that represents a _probably_ unique ID. Read through the code, then -copy/paste it into a new file named `paste_id.rs` in the `src/` directory: + To keep things simple, we'll store uploaded pastes on the file system inside + of an `upload/` directory. Let's create that directory next to `src/` in our + project now: -```rust -use std::fmt; -use std::borrow::Cow; + ```sh + mkdir upload + ``` -use rand::{self, Rng}; + Our project tree now looks like: -/// Table to retrieve base62 values from. -const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + ```sh + . + ├── Cargo.toml + ├── src + │   └── main.rs + └── upload + ``` -/// A _probably_ unique paste ID. -pub struct PasteId<'a>(Cow<'a, str>); + * **What should we name the uploaded paste files?** -impl<'a> PasteId<'a> { - /// 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> { - let mut id = String::with_capacity(size); - let mut rng = rand::thread_rng(); - for _ in 0..size { - id.push(BASE62[rng.gen::() % 62] as char); + 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 named `paste_id.rs` with the following contents: + + ```rust + 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::() % 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 -# /* -mod paste_id; -# */ mod paste_id { pub struct PasteId; } + ```sh + . + ├── Cargo.toml + ├── 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 -[dependencies] -# existing Rocket dependencies... -rand = "0.8" -``` + use crate::paste_id::PasteId; + ``` -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 -cargo build -``` + ```toml + [dependencies] + ## existing Rocket dependencies... + rand = "0.8" + ``` -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. + Ensure that your application builds with the new code: -### 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 -simplify things, we'll store the uploads in a directory named `upload/`. Create -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 = "")] -fn upload(paste: Data<'_>) -> std::io::Result { - # 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 = "")] -async fn upload(paste: Data<'_>) -> Result> { - 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 - # 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 /` 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 +With these design decisions made, we're ready to continue writing our +application. ## Retrieving Pastes -The final step is to create the `retrieve` route which, given an ``, will -return the corresponding paste if it exists. +We'll proceed with a `retrieve` route which, given an ``, 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 `` 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 `` as a dynamic path element. The handler uses the `id` to construct a @@ -321,11 +256,13 @@ paste doesn't exist. ```rust # #[macro_use] extern crate rocket; +use std::path::Path; use rocket::tokio::fs::File; #[get("/")] async fn retrieve(id: &str) -> Option { - 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() } ``` @@ -336,17 +273,22 @@ Make sure that the route is mounted at the root path: # #[macro_use] extern crate rocket; # #[get("/")] fn index() {} -# #[post("/")] fn upload() {} # #[get("/")] fn retrieve(id: String) {} #[launch] 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 -`&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 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 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 -`id` is a dynamic parameter, we can use Rocket's -[FromParam](@api/rocket/request/trait.FromParam.html) trait to -implement the validation and ensure that the `id` is a valid `PasteId` before -using it. We do this by implementing `FromParam` for `PasteId` in -`src/paste_id.rs`, as below: +### 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 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 -use std::borrow::Cow; - use rocket::request::FromParam; - -/// A _probably_ unique paste ID. -pub struct PasteId<'a>(Cow<'a, str>); +# use std::borrow::Cow; +# pub struct PasteId<'a>(Cow<'a, str>); /// Returns an instance of `PasteId` if the path segment is a valid ID. /// Otherwise returns the invalid ID as the `Err` value. @@ -379,36 +328,48 @@ impl<'a> FromParam<'a> for PasteId<'a> { type Error = &'a str; fn from_param(param: &'a str) -> Result { - match param.chars().all(|c| c.is_ascii_alphanumeric()) { - true => Ok(PasteId(param.into())), - false => Err(param) - } + param.chars().all(|c| c.is_ascii_alphanumeric()) + .then(|| PasteId(param.into())) + .ok_or(param) } } ``` -Then, we simply need to change the type of `id` in the handler to `PasteId`. -Rocket will then ensure that `` represents a valid `PasteId` before calling -the `retrieve` route, preventing attacks on the `retrieve` route: +! 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 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 `` represents a valid `PasteId` +before calling the `retrieve` route, preventing the previous attack entirely: ```rust # #[macro_use] extern crate rocket; +use rocket::tokio::fs::File; # use std::borrow::Cow; -# use rocket::tokio::fs::File; - -# type PasteId<'a> = &'a str; +# use std::path::PathBuf; +# use rocket::request::FromParam; +# 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 { todo!() } +# } #[get("/")] async fn retrieve(id: PasteId<'_>) -> Option { - let filename = format!("upload/{id}", id = id); - File::open(&filename).await.ok() + File::open(id.file_path()).await.ok() } ``` -Note that 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 or -potentially blacklisting sensitive files as needed. +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 @@ -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 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 = "")] +async fn upload(paste: Data<'_>) -> std::io::Result { + /* .. */ + # 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 { 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("/")] fn retrieve(id: PasteId<'_>) -> Option { todo!() } + +#[post("/", data = "")] +async fn upload(paste: Data<'_>) -> std::io::Result { + 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("/")] 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 + # 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. diff --git a/site/guide/index.md b/site/guide/index.md index baf55910..d8fc9aaf 100644 --- a/site/guide/index.md +++ b/site/guide/index.md @@ -28,8 +28,8 @@ aspect of Rocket. The sections are: - **[Testing](testing/):** how to unit and integration test a Rocket application. - **[Configuration](configuration/):** how to configure a Rocket application. - - **[Pastebin](pastebin/):** a tutorial on how to create a pastebin with - Rocket. + - **[Pastebin Tutorial](pastebin/):** a tutorial on how to create a pastebin + with Rocket. - **[Conclusion](conclusion/):** concludes the guide and discusses next steps for learning. - **[FAQ](faq/):** answers to frequently asked questions about Rocket and