I needed to make a small web service to update the config file of a load balancer, reverse proxy (Traefik).
I thought about using PHP, but the complexity of deployment and the desire to learn more Rust (especially web/network Rust) pushed to to start with Rust for this small utility.
I pulled down Rocket framework. I looked at Rocket, Actix, and Axum about a year ago and thought positively about how Rocket was organized.
Wow, really lacking in all of the frameworks from a PHP perspective. Most of the docs in Rust are focused on the mechanics of how the framework works instead of how you can use it to get stuff done.
Firstly, I'm probably going to deploy this in a docker container, so why not supply a Config that listens on 0.0.0.0 instead of 127.0.0.1? Seems like a small win. Ok, maybe that's so minor I shouldn't include it.
I wanted to secure the calls with a pre-shared key. If the caller sends some random string that is built into the source or read by configuration at runtime, then the call is authorized. This doesn't really seem to be handled by Rocket.
In order to get function parameters from the request, you need to create what are called "Request Guards". Now, coming from
20 years of Web, I would assume that has to with authorization. Sadly, no. Then it has to do with validation? Also, no.
It has to do with protecting your own function from being called ... from Rust itself? I'm not sure. "Request Guards" are
a solution to not having DI or runtime introspection in Rust. In Laravel they would be automatically handled or you would use
"Route Model Binding" to automatically load a model.
If you're looking for how to get information from the request, you probably would have skipped right over the "Request Guards" section.
There's even a truncated example of getting an ApiKey
#[get("/sensitive")]
fn sensitive(key: ApiKey) { /* .. */ }
That's it, easy right? Now all you have to do is implement FromRequest
pub trait FromRequest<'r>: Sized {
type Error: Debug;
// Required method
fn from_request<'life0, 'async_trait>(
request: &'r Request<'life0>,
) -> Pin<Box<dyn Future<Output = Outcome<Self, Self::Error>> + Send + 'async_trait>>
where Self: 'async_trait,
'r: 'async_trait,
'life0: 'async_trait;
}
There are 0 examples of implementing this anywhere int he documentation. I kind of peiced it together from issues and
LLM tab completion. But, if you don't declare your ApiKey
struct with the 'r
lifetime you will get some insidious
error right at the end that makes no sense.
(update: found a full example: [https://api.rocket.rs/v0.5/rocket/request/trait.FromRequest#example-1])
Once you get all that sorted, you can easily add ApiKey: &'r str
to any method and it will automatically return
403 when the correct key is not supplied.
But, you have to add this parameter to every, single function call that needs protecting. There doesn't seem to be a way to authorize groups of routes. Like ... imagine trying to create an admin panel in Rust/Rocket? Also, you need to add
#[allow(unused)]
on every route because you don't actually use the key in your method. You just need to define it to get the protection.
If that isn't the way to do it, then a proper example would be welcome, also maybe the Request Guards shouldn't be called Guards if this is not the way to do authz/authn ? Request Bindings would make more sense to me coming from a PHP background.
OK, this is turning into whining territory. I can't recall each specific problem since this all happened late at night.
The take away is:
- There's no Base64 encoding in Rust
- There's no random in Rust
- My 100 line, single-file web project takes 2GB of storage for dependencies
- 2 GB Specifically for Rocket:
- There's no input validation (unless you are submitting a web-form)
- There are no examples of how to throw an error. (Or I couldn't find them)
- There are no examples of custom Request Guard impl, but promissing psuedo-code that matched my use case.
Every time I try to sanitize input I end up with long-winded complaints from Rocket on the console
Handler transfer panicked. This is an application bug. A panic in Rust must be treated as an exceptional event. Panicking is not a suitable error handling mechanism. Unwinding, the result of a panic, is an expensive operation. Panics will degrade application performance. Instead of panicking, return
Option
and/orResult
. Values of either type can be returned directly from handlers.
It's fun to have a small binary that "just runs" (but not on alpine) when you use Rust instead of PHP. But that novelty quickly wears off when you have to hunt for crates from random internet strangers just to get Base64 encoding and decoding...
So, things on my TODO list:
- figure out how to return errors
- figure out how to validate data
- get a bigger drive
I don't try to convince any non-Rustaceans to give Web centric Rust programming a try. I tell them, "Nah, it's fine, you're not missing out on anything"
wow, The base64 crate is terrible. I actually downgraded to 0.13 from 0.20. Why? Because when I copied functions from their README into my app, I got a deprecation warning:
base64::encode(&buf)
warning: use of deprecated function `base64::encode`: Use Engine::encode
It was like, warning: this is deprecated. you have to select a base64 engine first.
I was like, nope. downgrade.
Not to mention that it's stuck at 0 major version and probably will be for the entire lifetime of Rust. That really doesn't matter - i guess - because cargo doesn't resolve to the same set of dependencies on different days. But that's another rant.