Pastebin
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.
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 do:
1
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>
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 3
[]
= "0.3.17"
= "0.3.17"
And finally, create a skeleton Rocket application to work off of in src/main.rs
:
1 2 3 4 5 6 7 8
extern crate rocket;
Ensure everything works by running the application:
1
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 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
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
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.
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
You should now be able to cargo run
the application and visit the root path (/
) to see the text being displayed.
Uploading
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.
Unique IDs
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
use fmt;
use Cow;
use ;
/// Table to retrieve base62 values from.
const BASE62: &'static = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/// A _probably_ unique paste ID.
;
Then, in src/main.rs
, add the following after extern crate rocket
:
1 2 3 4 5
extern crate rand;
use PasteID;
Finally, add a dependency for the rand
crate to the Cargo.toml
file:
1 2
[]
= "0.3"
Then, ensure that your application builds with the new code:
1
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.
Processing
Believe it or not, the hard part is done! (whew!).
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:
1
For the upload
route, we'll need to use
a few items:
1 2 3 4 5
use io;
use Path;
use Data;
use RawStr;
The Data 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:
1 2
Your code should:
- Create a new
PasteID
of a length of your choosing. - Construct a filename inside
upload/
given thePasteID
. - Stream the
Data
to the file with the constructed filename. - Construct a URL given the
PasteID
. - Return the URL to the client.
Here's our version (in src/main.rs
):
1 2 3 4 5 6 7 8 9 10
Make sure that the route is mounted at the root path:
1 2 3
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:
1 2 3 4 5 6 7
|
<ctrl-c> # kill running process
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.
Retrieving Pastes
The final step is to create the retrieve
route which, given an <id>
, will return the corresponding paste if it exists.
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
use File;
use RawStr;
Unfortunately, there's a problem with this code. Can you spot the issue? The RawStr
type should tip you off!
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.
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 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
use FromParam;
/// Returns `true` if `id` is a valid paste ID and `false` otherwise.
/// Returns an instance of `PasteID` if the path segment is a valid ID.
/// Otherwise returns the invalid ID as the `Err` value.
Then, we simply need to change the type of id
in the handler to PasteID
. Rocket will then ensure that <id>
represents a valid PasteID
before calling the retrieve
route, preventing attacks on the retrieve
route:
1 2 3 4 5
Note that our valid_id
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.
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 PasteID
s 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.
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>
. - Limit the upload to a maximum size. If the upload exceeds that size, 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.