Davide Del Papa
Posted on November 1, 2020
Photo by Jean-Philippe Delberghe on Unsplash, modified(cropped)
In this series we are going to explore how to make a Rust server using Rocket
The code for this tutorial can be found in this repository: github.com/davidedelpapa/rocket-tut
git clone https://github.com/davidedelpapa/rocket-tut.git
cd rocket-tut
git checkout tags/tut1
Setup
As first thing we create a new project
cargo new rocket-tut
cd rocket-tut
We need to override the default of rustup
for this project and use Rust Nightly.
rustup override set nightly
we will use cargo add
from the cargo edit crate (if you do not have it, just run cargo install cargo-edit
):
cargo add rocket
And in fact the [dependencies]
section of our Cargo.toml should look like the following:
[dependencies]
rocket = "0.4.5"
Fantastic!
Let's substitute the default src/main.rs with the following:
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] use rocket::*;
#[get("/echo/<echo>")]
fn echo_fn(echo: String) -> String {
format!("{}", echo)
}
fn main() {
rocket::ignite().mount("/", routes![echo_fn]).launch();
}
Let's quickly analyze the code:
- we will use the rocket crate with the
#[macro_use]
directive. - we will define a function for each route we need, in this case
echo_fn()
. This function takes as a parameter aString
. - we decorate each route with a directive that explains the HTTP method for the route (in this case
GET
), with the address of the route itself, and any parameter. Notice that the parameters are specified in<>
and they must match the parameter names of the function. - the function returns a
String
with the body of the page. - lastly, in
main()
, we "ignite" the rocket engine (that is we start it), and we mount any route to it. After this, we "launch" the rocket (i.e., we start the server).
Now let's point the browser to http://localhost:8000. We will be greeted with a 404 page by Rocket, since we will not set up the root (/) route.
Let's get to the route we set up, /echo/:
http://localhost:8000/echo/test
Our page greets us back with the same text we put after the echo/
The echo route is working!
Great!
Serving static files
Let's see what else can we do now. We could make a static file server, for example.
Let's start it now with Cargo Watch (if it's not installed: cargo install cargo-watch
)
cargo watch -x run
Let's create a folder static/ and add an image file to it.
Now let's add the following to the src/main.rs:
use std::path::{Path, PathBuf};
#[macro_use] use rocket::*;
use rocket::response::NamedFile;
We will use the standard library's functions to deal with the filesystem.
At the same time we use rocket's NamedFile
response to serve a file
Next we add a new route for file serving:
#[get("/file/<file..>")]
fn fileserver(file: PathBuf) -> Option<NamedFile> {
NamedFile::open(Path::new("static/").join(file)).ok()
}
The route converts the parameter to a PathBuf
: this ensures there's not a bug that allows to excalate the filesystem and go around inside the server fetching files.
We join
the path to the Path
of the static/ folder we just created.
Finally we need to update our list of routes, adding the fileserver
function:
fn main() {
rocket::ignite().mount("/", routes![echo_fn, fileserver]).launch();
}
All easy:
There's even a easier way. On the command line, let's add the crate rocket-contrib
:
cargo add rocket_contrib
Now let's update the src/main.rs to the following:
#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] use rocket::*;
use rocket_contrib::serve::StaticFiles;
#[get("/echo/<echo>")]
fn echo_fn(echo: String) -> String {
format!("{}", echo)
}
fn main() {
rocket::ignite()
.mount("/", routes![echo_fn])
.mount("/files", StaticFiles::from("static/"))
.launch();
}
As you can see we are using now a module from the rocket_contrib
, serve::StaticFiles. This does exactly the "dirty work" of file serving.
We can see that we used as mount-point "/files" (while before it was /
+ file
-> /file
). We still fetch the files from the static/ folder (with StaticFiles::from()
)
Running it we can see its effectiveness.
Secure the server
If we get all de defaults in rocket_contrib
we will have the fileserver, as well as support for JSON (some of it later).
However, rocket_contrib
has got more interesting features, such as a security handler (a helmet
similar to helmetjs
), connections and pooling of databases, and two different templating engines, tera, and handlebars, together with a uuid
function.
Let's protect our code with helmet
! Let's update the rocket_contrib
features:
cargo add rocket_contrib --features helmet
Do not worry about the fileserver: as long as the defaults are not excluded, it will be present.
Now let's secure our code.
We import the SpaceHelmet
after rocket
:
use rocket_contrib::helmet::SpaceHelmet;
The we will use it after we ignite the rocket:
fn main() {
rocket::ignite()
.attach(SpaceHelmet::default())
.mount("/", routes![echo_fn])
.mount("/files", StaticFiles::from("static/"))
.launch();
}
Here you can consult a list of the default headers set by helmet.
This way it's super easy to secure our headers.
Testing our server
Now before wrapping out this first tutorial, let's see how to test our server. Beware, it's Rocket easy, not rocket science!
As first thing, we factor out the creation of the Rocket instance:
We create a function rocket()
which ingnites, and prepares out Rocket instance, without launching it. The function will return this Rocket instance:
fn rocket() -> rocket::Rocket {
rocket::ignite().attach(SpaceHelmet::default())
.mount("/", routes![echo_fn])
.mount("/files", StaticFiles::from("static/"))
}
Now we need to call this instance from our main()
:
fn main() {
rocket().launch();
}
This is totally equivalent from our last configuration, except that in this way we can create our Rocket instance for testing purposes, outside of main()
.
Now we need to add a test at the end of the src/main.rs:
#[cfg(test)]
mod test {
use super::rocket;
use rocket::local::Client;
use rocket::http::Status;
#[test]
fn echo_test() {
let client = Client::new(rocket()).expect("Valid Rocket instance");
let mut response = client.get("/echo/test_echo").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.body_string(), Some("test_me".into()));
}
}
With super::rocket
we get the rocket()
function in order to init the instance
We define the test function echo_test()
which as first thing sets up a client to poll the server. The first thing we test is actually that we excpect()
a valid instance of Rocket!
Then we dispatch a GET
request to the server, at the route echo/
, sending it a "test_echo" parameter. We store the response
.
We test for two things: the first is that the response status is Ok
(that is 400
).
The second thing is that the body of the response must match our echo parameter. In this case the code is set up to fail, since we sent "test_echo" as parameter, and we are testing against "test_me".
A quick run with cargo test
should in fact return a test failure:
cargo test
....
thread 'test::echo_test' panicked at 'assertion failed: `(left == right)`
left: `Some("test_echo")`,
right: `Some("test_me")`', src/main.rs:33:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
test::echo_test
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--bin rocket-tut'
One little adjustment to the code, assert_eq!(response.body_string(), Some("test_echo".into()));
and the test runs smooth:
running 1 test
test test::echo_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Conclusion
Ok, that is all for today.
We have introduced Rocket, its basic usage, the securing that can be done using rocket_contrib
, and how to run tests.
In our next installment we will see how to create a CRUD server with it. Stay tuned!
Posted on November 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.