2018-06-24 19:20:56 +00:00
|
|
|
#![deny(missing_docs)]
|
2018-06-28 10:59:18 +00:00
|
|
|
#![cfg_attr(feature = "cargo-clippy", deny(warnings))]
|
2018-06-24 19:20:56 +00:00
|
|
|
//! # Rocket Csrf
|
|
|
|
//!
|
|
|
|
//! A crate to protect you application against csrf.
|
|
|
|
//!
|
|
|
|
//! ## Feature
|
|
|
|
//!
|
|
|
|
//! - Automatically protect all POST, PUT, DELETE and PATCH endpoints
|
|
|
|
//! - Ability to define exceptions
|
|
|
|
//!
|
|
|
|
//! ## Usage
|
|
|
|
//!
|
|
|
|
//! First add it to your `Cargo.toml` (at the moment using git version, because it was made mainly
|
2018-06-25 12:01:08 +00:00
|
|
|
//! for [Plume](https://github.com/Plume-org/Plume) and I didn't have the time to backport it to
|
2018-06-24 19:20:56 +00:00
|
|
|
//! older Rocket version)
|
|
|
|
//!
|
2018-06-25 12:01:08 +00:00
|
|
|
//! ```toml
|
2018-06-24 19:20:56 +00:00
|
|
|
//! [dependencies.rocket_csrf]
|
|
|
|
//! git = "https://github.com/fdb-hiroshima/rocket_csrf"
|
|
|
|
//! rev = "50947b8715ae1fa7b73e60b65fdbd1aaf7754f10"
|
2018-06-25 12:01:08 +00:00
|
|
|
//! ```
|
2018-06-24 19:20:56 +00:00
|
|
|
//! Then, in your `main.rs`:
|
|
|
|
//!
|
|
|
|
//! ```rust
|
|
|
|
//! extern crate rocket_csrf;
|
|
|
|
//!
|
|
|
|
//! //...
|
|
|
|
//!
|
|
|
|
//! fn main() {
|
|
|
|
//! rocket::ignite()
|
|
|
|
//! .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
//! //configure it here
|
|
|
|
//! .finish().unwrap())
|
|
|
|
//! //add your routes, other fairings...
|
2018-06-25 12:01:08 +00:00
|
|
|
//! .launch();
|
2018-06-24 19:20:56 +00:00
|
|
|
//! }
|
|
|
|
//! ```
|
|
|
|
//!
|
|
|
|
//! You should define a route for csrf violation error, and registe it in the builder, otherwise
|
|
|
|
//! errors will simply be redirected to the route matching `/`
|
|
|
|
//!
|
2018-06-24 15:39:11 +00:00
|
|
|
extern crate csrf;
|
|
|
|
extern crate data_encoding;
|
|
|
|
extern crate rand;
|
|
|
|
extern crate rocket;
|
|
|
|
extern crate serde;
|
|
|
|
|
|
|
|
use csrf::{AesGcmCsrfProtection, CsrfProtection};
|
|
|
|
use data_encoding::{BASE64, BASE64URL_NOPAD};
|
|
|
|
use rand::prelude::thread_rng;
|
|
|
|
use rand::Rng;
|
2018-06-24 17:53:47 +00:00
|
|
|
use rocket::fairing::{Fairing, Info, Kind};
|
|
|
|
use rocket::http::uri::Uri;
|
|
|
|
use rocket::http::Method::{self, *};
|
2018-06-24 15:39:11 +00:00
|
|
|
use rocket::http::{Cookie, Status};
|
|
|
|
use rocket::outcome::Outcome;
|
|
|
|
use rocket::request::{self, FromRequest};
|
|
|
|
use rocket::response::Body::Sized;
|
2018-06-24 17:53:47 +00:00
|
|
|
use rocket::{Data, Request, Response, Rocket, State};
|
2018-06-24 15:39:11 +00:00
|
|
|
use serde::{Serialize, Serializer};
|
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::env;
|
|
|
|
use std::io::Read;
|
|
|
|
use std::str::from_utf8;
|
|
|
|
|
2018-06-25 12:01:08 +00:00
|
|
|
/// Builder for [CsrfFairing](struct.CsrfFairing.html)
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
2018-06-25 12:01:08 +00:00
|
|
|
/// The `CsrfFairingBuilder` type allows for creation and configuration of a [CsrfFairing](struct.CsrfFairing.html), the
|
2018-06-24 19:20:56 +00:00
|
|
|
/// main struct of this crate.
|
|
|
|
///
|
|
|
|
/// # Usage
|
|
|
|
/// A Builder is created via the [`new`] method. Then you can configure it with others provided
|
2018-06-25 12:01:08 +00:00
|
|
|
/// methods, and get a [CsrfFairing](struct.CsrfFairing.html) by a call to [`finalize`]
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
2018-06-25 12:01:08 +00:00
|
|
|
/// [`new`]: #method.new
|
|
|
|
/// [`finalize`]: #method.finalize
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
|
|
|
/// ## Examples
|
|
|
|
///
|
|
|
|
/// The following shippet show 'CsrfFairingBuilder' being used to create a fairing protecting all
|
|
|
|
/// endpoints and redirecting error to `/csrf-violation` and treat them as if they where `GET`
|
|
|
|
/// request then.
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// #extern crate rocket_csrf
|
|
|
|
///
|
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_default_target("/csrf-violation", rocket::http::Method::Get)
|
|
|
|
/// .finish().unwrap())
|
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
/// ```
|
|
|
|
|
2018-06-24 15:39:11 +00:00
|
|
|
pub struct CsrfFairingBuilder {
|
|
|
|
duration: i64,
|
|
|
|
default_target: (String, Method),
|
|
|
|
exceptions: Vec<(String, String, Method)>,
|
|
|
|
secret: Option<[u8; 32]>,
|
|
|
|
auto_insert: bool,
|
|
|
|
auto_insert_disable_prefix: Vec<String>,
|
|
|
|
auto_insert_max_size: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CsrfFairingBuilder {
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Create a new builder with default values.
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn new() -> Self {
|
|
|
|
CsrfFairingBuilder {
|
2018-06-24 17:53:47 +00:00
|
|
|
duration: 60 * 60,
|
|
|
|
default_target: (String::from("/"), Get),
|
2018-06-24 15:39:11 +00:00
|
|
|
exceptions: Vec::new(),
|
|
|
|
secret: None,
|
|
|
|
auto_insert: true,
|
|
|
|
auto_insert_disable_prefix: Vec::new(),
|
2018-06-24 17:53:47 +00:00
|
|
|
auto_insert_max_size: 16 * 1024,
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Set the timeout (in seconds) of CSRF tokens generated by the final Fairing. Default timeout
|
|
|
|
/// is one hour.
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn set_timeout(mut self, timeout: i64) -> Self {
|
|
|
|
self.duration = timeout;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Set the default route when an invalide request is catched, you may add a <uri> as a segment
|
|
|
|
/// or a param to get the percent-encoded original target. You can also set the method of the
|
|
|
|
/// route to which you choosed to redirect.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_default_target("/csrf-violation", rocket::http::Method::Get)
|
|
|
|
/// .finish().unwrap())
|
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn set_default_target(mut self, default_target: String, method: Method) -> Self {
|
|
|
|
self.default_target = (default_target, method);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Set the list of exceptions which will not be redirected to the default route, removing any
|
|
|
|
/// previously added exceptions, to juste add exceptions use [`add_exceptions`] instead. A route may
|
|
|
|
/// contain dynamic parts noted as <name>, which will be replaced in the target route.
|
|
|
|
/// Note that this is not aware of Rocket's routes, so matching `/something/<dynamic>` while
|
|
|
|
/// match against `/something/static`, even if those are different routes for Rocket. To
|
|
|
|
/// circunvence this issue, you can add a (not so) exception matching the static route before
|
|
|
|
/// the dynamic one, and redirect it to the default target manually.
|
|
|
|
///
|
2018-06-25 12:01:08 +00:00
|
|
|
/// [`add_exceptions`]: #method.add_exceptions
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_exceptions(vec![
|
|
|
|
/// ("/some/path".to_owned(), "/some/path".to_owned(), rocket::http::Method::Post))//don't verify csrf token
|
|
|
|
/// ("/some/<other>/path".to_owned(), "/csrf-error?where=<other>".to_owned(), rocket::http::Method::Get))
|
|
|
|
/// ])
|
|
|
|
/// .finish().unwrap())
|
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
/// ```
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn set_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self {
|
|
|
|
self.exceptions = exceptions;
|
|
|
|
self
|
|
|
|
}
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Add the to list of exceptions which will not be redirected to the default route. See
|
|
|
|
/// [`set_exceptions`] for more informations on how exceptions work.
|
|
|
|
///
|
2018-06-25 12:01:08 +00:00
|
|
|
/// [`set_exceptions`]: #method.set_exceptions
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn add_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self {
|
|
|
|
self.exceptions.extend(exceptions);
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Set the secret key used to generate secure cryptographic tokens. If not set, rocket_csrf
|
2018-06-25 11:11:58 +00:00
|
|
|
/// will attempt to get the secret used by Rocket for it's own private cookies via the
|
2018-06-24 19:20:56 +00:00
|
|
|
/// ROCKET_SECRET_KEY environment variable, or will generate a new one at each restart.
|
|
|
|
/// Having the secret key set (via this or Rocket environment variable) allow tokens to keep
|
2018-06-25 11:11:58 +00:00
|
|
|
/// their validity in case of an application restart.
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket_csrf::CsrfFairingBuilder;
|
|
|
|
/// fn main() {
|
|
|
|
/// rocket::ignite()
|
|
|
|
/// .attach(rocket_csrf::CsrfFairingBuilder::new()
|
|
|
|
/// .set_secret([0;32])//don't do this, use trully secret array instead
|
|
|
|
/// .finish().unwrap())
|
|
|
|
/// //add your routes, other fairings...
|
|
|
|
/// .launch();
|
|
|
|
/// }
|
|
|
|
/// ```
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn set_secret(mut self, secret: [u8; 32]) -> Self {
|
|
|
|
self.secret = Some(secret);
|
|
|
|
self
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Set if this should modify response to insert tokens automatically in all forms. If true,
|
|
|
|
/// this will insert tokens in all forms it encounter, if false, you will have to add them via
|
2018-06-25 12:01:08 +00:00
|
|
|
/// [CsrfFairing](struct.CsrfFairing.html), which you may obtain via request guards.
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
|
|
|
pub fn set_auto_insert(mut self, auto_insert: bool) -> Self {
|
|
|
|
self.auto_insert = auto_insert;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set prefixs for which this will not try to add tokens in forms. This has no effect if
|
|
|
|
/// auto_insert is set to false. Not having to parse response on paths witch don't need it may
|
|
|
|
/// improve performances, but not that only html documents are parsed, so it's not usefull to
|
|
|
|
/// use it on routes containing only images or stillsheets.
|
|
|
|
pub fn set_auto_insert_disable_prefix(mut self, auto_insert_prefix: Vec<String>) -> Self {
|
|
|
|
self.auto_insert_disable_prefix = auto_insert_prefix;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Set the maximum size of a request before it get send chunked. A request will need at most
|
|
|
|
/// this additional memory for the buffer used to parse and tokens into forms. This have no
|
|
|
|
/// effect if auto_insert is set to false. Default value is 16Kio
|
|
|
|
pub fn set_auto_insert_max_chunk_size(mut self, chunk_size: u64) -> Self {
|
|
|
|
self.auto_insert_max_size = chunk_size;
|
|
|
|
self
|
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Get the fairing from the builder.
|
2018-06-24 15:39:11 +00:00
|
|
|
pub fn finalize(self) -> Result<CsrfFairing, ()> {
|
|
|
|
let secret = self.secret.unwrap_or_else(|| {
|
2018-06-25 11:11:58 +00:00
|
|
|
//use provided secret if one is
|
2018-06-24 15:39:11 +00:00
|
|
|
env::vars()
|
2018-06-28 10:59:18 +00:00
|
|
|
.find(|(key, _)| key == "ROCKET_SECRET_KEY")
|
2018-06-24 15:39:11 +00:00
|
|
|
.and_then(|(_, value)| {
|
|
|
|
let b64 = BASE64.decode(value.as_bytes());
|
|
|
|
if let Ok(b64) = b64 {
|
|
|
|
if b64.len() == 32 {
|
|
|
|
let mut array = [0; 32];
|
|
|
|
array.copy_from_slice(&b64);
|
|
|
|
Some(array)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
2018-06-24 17:53:47 +00:00
|
|
|
None
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
})//else get secret environment variable
|
2018-06-24 15:39:11 +00:00
|
|
|
.unwrap_or_else(|| {
|
2018-06-25 11:11:58 +00:00
|
|
|
eprintln!("[rocket_csrf] No secret key was found, you should consider set one to keep token validity across application restart");
|
2018-06-24 15:39:11 +00:00
|
|
|
thread_rng().gen()
|
2018-06-25 11:11:58 +00:00
|
|
|
}) //if environment variable is not set, generate a random secret and print a warning
|
2018-06-24 15:39:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
let default_target = Path::from(&self.default_target.0);
|
|
|
|
let mut hashmap = HashMap::new();
|
|
|
|
hashmap.insert("uri", "");
|
2018-06-28 10:59:18 +00:00
|
|
|
if default_target.map(&hashmap).is_none() {
|
2018-06-25 11:11:58 +00:00
|
|
|
return Err(());
|
|
|
|
} //verify if this path is valid as default path, i.e. it have at most one dynamic part which is <uri>
|
2018-06-24 15:39:11 +00:00
|
|
|
Ok(CsrfFairing {
|
|
|
|
duration: self.duration,
|
2018-06-24 17:53:47 +00:00
|
|
|
default_target: (default_target, self.default_target.1),
|
|
|
|
exceptions: self
|
|
|
|
.exceptions
|
|
|
|
.iter()
|
2018-06-25 11:11:58 +00:00
|
|
|
.map(|(a, b, m)| (Path::from(&a), Path::from(&b), *m))//TODO verify if source and target are compatible
|
2018-06-24 17:53:47 +00:00
|
|
|
.collect(),
|
2018-06-28 10:59:18 +00:00
|
|
|
secret,
|
2018-06-24 15:39:11 +00:00
|
|
|
auto_insert: self.auto_insert,
|
|
|
|
auto_insert_disable_prefix: self.auto_insert_disable_prefix,
|
2018-06-24 17:53:47 +00:00
|
|
|
auto_insert_max_size: self.auto_insert_max_size,
|
2018-06-24 15:39:11 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-28 10:59:18 +00:00
|
|
|
impl Default for CsrfFairingBuilder {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self::new()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Fairing to protect against Csrf attacks.
|
|
|
|
///
|
|
|
|
/// The `CsrfFairing` type protect a Rocket instance against Csrf attack by requesting mendatory
|
|
|
|
/// token on any POST, PUT, DELETE or PATCH request.
|
2018-06-25 12:01:08 +00:00
|
|
|
/// This is created via a [CsrfFairingBuilder](struct.CsrfFairingBuilder.html), and implement nothing else than the `Fairing` trait.
|
2018-06-24 19:20:56 +00:00
|
|
|
///
|
|
|
|
/// [`CsrfFairingBuilder`]: /rocket_csrf/struct.CsrfFairing.html
|
2018-06-24 15:39:11 +00:00
|
|
|
pub struct CsrfFairing {
|
|
|
|
duration: i64,
|
2018-06-24 17:53:47 +00:00
|
|
|
default_target: (Path, Method),
|
2018-06-24 15:39:11 +00:00
|
|
|
exceptions: Vec<(Path, Path, Method)>,
|
2018-06-24 17:53:47 +00:00
|
|
|
secret: [u8; 32],
|
2018-06-24 15:39:11 +00:00
|
|
|
auto_insert: bool,
|
|
|
|
auto_insert_disable_prefix: Vec<String>,
|
|
|
|
auto_insert_max_size: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Fairing for CsrfFairing {
|
|
|
|
fn info(&self) -> Info {
|
|
|
|
if self.auto_insert {
|
|
|
|
Info {
|
|
|
|
name: "CSRF protection",
|
2018-06-24 17:53:47 +00:00
|
|
|
kind: Kind::Attach | Kind::Request | Kind::Response,
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Info {
|
|
|
|
name: "CSRF protection",
|
2018-06-24 17:53:47 +00:00
|
|
|
kind: Kind::Attach | Kind::Request,
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
fn on_attach(&self, rocket: Rocket) -> Result<Rocket, Rocket> {
|
2018-06-25 11:11:58 +00:00
|
|
|
Ok(rocket.manage((AesGcmCsrfProtection::from_key(self.secret), self.duration))) //add the Csrf engine to Rocket's managed state
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn on_request(&self, request: &mut Request, data: &Data) {
|
|
|
|
match request.method() {
|
2018-06-25 11:11:58 +00:00
|
|
|
Get | Head | Connect | Options => {
|
|
|
|
let _ = request.guard::<CsrfToken>(); //force regeneration of csrf cookies
|
2018-06-24 17:53:47 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
_ => {}
|
2018-06-24 15:39:11 +00:00
|
|
|
};
|
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
let (csrf_engine, _) = request
|
|
|
|
.guard::<State<(AesGcmCsrfProtection, i64)>>()
|
|
|
|
.unwrap()
|
|
|
|
.inner();
|
2018-06-24 15:39:11 +00:00
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
let cookie = request
|
|
|
|
.cookies()
|
|
|
|
.get(csrf::CSRF_COOKIE_NAME)
|
2018-06-24 15:39:11 +00:00
|
|
|
.and_then(|cookie| BASE64.decode(cookie.value().as_bytes()).ok())
|
2018-06-25 11:11:58 +00:00
|
|
|
.and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()); //get and parse Csrf cookie
|
2018-06-24 17:53:47 +00:00
|
|
|
|
|
|
|
let _ = request.guard::<CsrfToken>(); //force regeneration of csrf cookies
|
2018-06-24 15:39:11 +00:00
|
|
|
|
|
|
|
let token = parse_args(from_utf8(data.peek()).unwrap_or(""))
|
2018-06-24 17:53:47 +00:00
|
|
|
.filter(|(key, _)| key == &csrf::CSRF_FORM_FIELD)
|
2018-06-24 15:39:11 +00:00
|
|
|
.filter_map(|(_, token)| BASE64URL_NOPAD.decode(&token.as_bytes()).ok())
|
|
|
|
.filter_map(|token| csrf_engine.parse_token(&token).ok())
|
2018-06-25 11:11:58 +00:00
|
|
|
.next(); //get and parse Csrf token
|
2018-06-24 15:39:11 +00:00
|
|
|
|
|
|
|
if let Some(token) = token {
|
|
|
|
if let Some(cookie) = cookie {
|
|
|
|
if csrf_engine.verify_token_pair(&token, &cookie) {
|
2018-06-25 11:11:58 +00:00
|
|
|
return; //if we got both token and cookie, and they match each other, we do nothing
|
2018-06-24 17:53:47 +00:00
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-25 11:11:58 +00:00
|
|
|
//Request reaching here are violating Csrf protection
|
|
|
|
|
2018-06-28 10:59:18 +00:00
|
|
|
for (src, dst, method) in &self.exceptions {
|
2018-06-24 17:53:47 +00:00
|
|
|
if let Some(param) = src.extract(&request.uri().to_string()) {
|
2018-06-28 10:59:18 +00:00
|
|
|
if let Some(destination) = dst.map(¶m) {
|
2018-06-24 15:39:11 +00:00
|
|
|
request.set_uri(destination);
|
|
|
|
request.set_method(*method);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
|
|
|
|
//if request matched no exception, reroute it to default target
|
|
|
|
|
2018-06-24 15:39:11 +00:00
|
|
|
let uri = request.uri().to_string();
|
|
|
|
let uri = Uri::percent_encode(&uri);
|
2018-06-24 17:53:47 +00:00
|
|
|
let mut param: HashMap<&str, &str> = HashMap::new();
|
2018-06-24 15:39:11 +00:00
|
|
|
param.insert("uri", &uri);
|
2018-06-28 10:59:18 +00:00
|
|
|
request.set_uri(self.default_target.0.map(¶m).unwrap());
|
2018-06-24 15:39:11 +00:00
|
|
|
request.set_method(self.default_target.1)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn on_response<'a>(&self, request: &Request, response: &mut Response<'a>) {
|
|
|
|
if let Some(ct) = response.content_type() {
|
|
|
|
if !ct.is_html() {
|
|
|
|
return;
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
} //if content type is not html, we do nothing
|
|
|
|
|
2018-06-24 15:39:11 +00:00
|
|
|
let uri = request.uri().to_string();
|
2018-06-24 17:53:47 +00:00
|
|
|
if self
|
|
|
|
.auto_insert_disable_prefix
|
|
|
|
.iter()
|
2018-06-28 10:59:18 +00:00
|
|
|
.any(|prefix| uri.starts_with(prefix))
|
2018-06-24 17:53:47 +00:00
|
|
|
{
|
2018-06-24 15:39:11 +00:00
|
|
|
return;
|
2018-06-25 11:11:58 +00:00
|
|
|
} //if request is on an ignored prefix, ignore it
|
2018-06-24 15:39:11 +00:00
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
let token = match request.guard::<CsrfToken>() {
|
2018-06-24 15:39:11 +00:00
|
|
|
Outcome::Success(t) => t,
|
|
|
|
_ => return,
|
2018-06-25 11:11:58 +00:00
|
|
|
}; //if we can't get a token, leave request unchanged, we can't do anything anyway
|
2018-06-24 15:39:11 +00:00
|
|
|
|
2018-06-25 11:11:58 +00:00
|
|
|
let body = response.take_body(); //take request body from Rocket
|
2018-06-24 15:39:11 +00:00
|
|
|
if body.is_none() {
|
|
|
|
return;
|
2018-06-25 11:11:58 +00:00
|
|
|
} //if there was no body, leave it that way
|
2018-06-24 15:39:11 +00:00
|
|
|
let body = body.unwrap();
|
|
|
|
|
|
|
|
if let Sized(body_reader, len) = body {
|
|
|
|
if len <= self.auto_insert_max_size {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if this is a small enought body, process the full body
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut res = Vec::with_capacity(len as usize);
|
2018-06-28 10:59:18 +00:00
|
|
|
CsrfProxy::from(body_reader, &token)
|
2018-06-24 17:53:47 +00:00
|
|
|
.read_to_end(&mut res)
|
|
|
|
.unwrap();
|
2018-06-24 15:39:11 +00:00
|
|
|
response.set_sized_body(std::io::Cursor::new(res));
|
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if body is of known but long size, change it to a stream to preserve memory, by encapsulating it into our "proxy" struct
|
2018-06-24 15:39:11 +00:00
|
|
|
let body = body_reader;
|
2018-06-28 10:59:18 +00:00
|
|
|
response.set_streamed_body(Box::new(CsrfProxy::from(body, &token)));
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
2018-06-24 17:53:47 +00:00
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if body is of unknown size, encapsulate it into our "proxy" struct
|
2018-06-24 15:39:11 +00:00
|
|
|
let body = body.into_inner();
|
2018-06-28 10:59:18 +00:00
|
|
|
response.set_streamed_body(Box::new(CsrfProxy::from(body, &token)));
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-30 07:32:41 +00:00
|
|
|
#[derive(Debug)]
|
2018-06-24 15:39:11 +00:00
|
|
|
enum ParseState {
|
2018-06-24 17:53:47 +00:00
|
|
|
Reset, //default state
|
|
|
|
PartialFormMatch(u8), //when parsing "<form"
|
|
|
|
SearchInput, //like default state, but inside a form
|
|
|
|
PartialInputMatch(u8, usize), //when parsing "<input"
|
2018-06-25 11:11:58 +00:00
|
|
|
PartialFormEndMatch(u8, usize), //when parsing "</form" ('<' is actally done via PartialInputMarch)
|
|
|
|
SearchMethod(usize), //when inside the first <input>, search for begining of a param
|
2018-06-24 17:53:47 +00:00
|
|
|
PartialNameMatch(u8, usize), //when parsing "name="_method""
|
2018-06-25 11:11:58 +00:00
|
|
|
CloseInputTag, //only if insert after, search for '>' of a "<input name=\"_method\">"
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
struct CsrfProxy<'a> {
|
2018-06-25 11:11:58 +00:00
|
|
|
underlying: Box<Read + 'a>, //the underlying Reader from which we get data
|
|
|
|
token: Vec<u8>, //a full input tag loaded with a valid token
|
|
|
|
buf: Vec<(Vec<u8>, usize)>, //a stack of buffers, with a position in case a buffer was not fully transmited
|
|
|
|
state: ParseState, //state of the parser
|
|
|
|
insert_tag: Option<usize>, //if we have to insert tag here, and how fare are we in the tag (in case of very short read()s)
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
impl<'a> CsrfProxy<'a> {
|
2018-06-28 10:59:18 +00:00
|
|
|
fn from(underlying: Box<Read + 'a>, token: &CsrfToken) -> Self {
|
|
|
|
let tag_begin = b"<input type=\"hidden\" name=\"csrf-token\" value=\"";
|
2018-06-24 15:39:11 +00:00
|
|
|
let tag_middle = token.value.as_bytes();
|
2018-06-28 10:59:18 +00:00
|
|
|
let tag_end = b"\">";
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut token = Vec::new();
|
|
|
|
token.extend_from_slice(tag_begin);
|
|
|
|
token.extend_from_slice(tag_middle);
|
|
|
|
token.extend_from_slice(tag_end);
|
2018-06-24 17:53:47 +00:00
|
|
|
CsrfProxy {
|
2018-06-28 10:59:18 +00:00
|
|
|
underlying,
|
|
|
|
token,
|
2018-06-24 15:39:11 +00:00
|
|
|
buf: Vec::new(),
|
|
|
|
state: ParseState::Reset,
|
|
|
|
insert_tag: None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-24 17:53:47 +00:00
|
|
|
impl<'a> Read for CsrfProxy<'a> {
|
|
|
|
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
2018-06-24 15:39:11 +00:00
|
|
|
if let Some(pos) = self.insert_tag {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we should insert a tag
|
2018-06-24 17:53:47 +00:00
|
|
|
let size = buf.len();
|
2018-06-25 11:11:58 +00:00
|
|
|
let copy_size = std::cmp::min(size, self.token.len() - pos); //get max copy length
|
|
|
|
buf[0..copy_size].copy_from_slice(&self.token[pos..copy_size + pos]); //copy that mutch
|
2018-06-24 15:39:11 +00:00
|
|
|
if copy_size == self.token.len() - pos {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we copied the full tag, say we don't need to set it again
|
2018-06-24 15:39:11 +00:00
|
|
|
self.insert_tag = None;
|
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we didn't copy the full tag, save where we were
|
2018-06-24 15:39:11 +00:00
|
|
|
self.insert_tag = Some(pos + copy_size);
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
return Ok(copy_size); //return the lenght of the copied data
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
2018-06-24 17:53:47 +00:00
|
|
|
|
|
|
|
let len = if let Some((vec, pos)) = self.buf.pop() {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if there is a buffer to add here
|
2018-06-24 15:39:11 +00:00
|
|
|
let size = buf.len();
|
2018-06-24 17:53:47 +00:00
|
|
|
if vec.len() - pos <= size {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if the part left of the buffer is smaller than buf
|
2018-06-24 17:53:47 +00:00
|
|
|
buf[0..vec.len() - pos].copy_from_slice(&vec[pos..]);
|
2018-06-30 07:32:41 +00:00
|
|
|
vec.len() - pos
|
2018-06-24 15:39:11 +00:00
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//else if the part left of the buffer is bigger than buf
|
2018-06-24 17:53:47 +00:00
|
|
|
buf.copy_from_slice(&vec[pos..pos + size]);
|
|
|
|
self.buf.push((vec, pos + size));
|
2018-06-24 15:39:11 +00:00
|
|
|
size
|
2018-06-25 11:11:58 +00:00
|
|
|
} //send the size of what was read as if it was a normal read on underlying struct
|
2018-06-24 15:39:11 +00:00
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if there is no buffer to add, read from underlying struct
|
2018-06-24 15:39:11 +00:00
|
|
|
let res = self.underlying.read(buf);
|
|
|
|
if res.is_err() {
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
match res {
|
|
|
|
Ok(v) => v,
|
|
|
|
Err(_) => return res,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
for i in 0..len {
|
2018-06-25 11:11:58 +00:00
|
|
|
//for each byte
|
2018-06-24 15:39:11 +00:00
|
|
|
use ParseState::*;
|
|
|
|
self.state = match self.state {
|
|
|
|
Reset => if buf[i] as char == '<' {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we are in default state and we begin to match any tag
|
2018-06-24 17:53:47 +00:00
|
|
|
PartialFormMatch(0)
|
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we don't match a tag
|
2018-06-24 17:53:47 +00:00
|
|
|
Reset
|
|
|
|
},
|
2018-06-24 15:39:11 +00:00
|
|
|
PartialFormMatch(count) => match (buf[i] as char, count) {
|
2018-06-25 11:11:58 +00:00
|
|
|
//progressively match "form"
|
2018-06-24 17:53:47 +00:00
|
|
|
('f', 0) | ('F', 0) => PartialFormMatch(1),
|
|
|
|
('o', 1) | ('O', 1) => PartialFormMatch(2),
|
|
|
|
('r', 2) | ('R', 2) => PartialFormMatch(3),
|
2018-06-25 11:11:58 +00:00
|
|
|
('m', 3) | ('M', 3) => SearchInput, //when we success, go to next state
|
|
|
|
_ => Reset, //if this don't match, go back to defailt state
|
2018-06-24 17:53:47 +00:00
|
|
|
},
|
2018-06-24 15:39:11 +00:00
|
|
|
SearchInput => if buf[i] as char == '<' {
|
2018-06-25 11:11:58 +00:00
|
|
|
//begin to match any tag
|
2018-06-24 17:53:47 +00:00
|
|
|
PartialInputMatch(0, i)
|
|
|
|
} else {
|
|
|
|
SearchInput
|
|
|
|
},
|
2018-06-24 15:39:11 +00:00
|
|
|
PartialInputMatch(count, pos) => match (buf[i] as char, count) {
|
2018-06-25 11:11:58 +00:00
|
|
|
//progressively match "input"
|
2018-06-24 17:53:47 +00:00
|
|
|
('i', 0) | ('I', 0) => PartialInputMatch(1, pos),
|
|
|
|
('n', 1) | ('N', 1) => PartialInputMatch(2, pos),
|
|
|
|
('p', 2) | ('P', 2) => PartialInputMatch(3, pos),
|
|
|
|
('u', 3) | ('U', 3) => PartialInputMatch(4, pos),
|
2018-06-25 11:11:58 +00:00
|
|
|
('t', 4) | ('T', 4) => SearchMethod(pos), //when we success, go to next state
|
|
|
|
('/', 0) => PartialFormEndMatch(1, pos), //if first char is '/', it may mean we are matching end of form, go to that state
|
|
|
|
_ => SearchInput, //not a input tag, go back to SearchInput
|
2018-06-24 17:53:47 +00:00
|
|
|
},
|
|
|
|
PartialFormEndMatch(count, pos) => match (buf[i] as char, count) {
|
2018-06-25 11:11:58 +00:00
|
|
|
//progressively match "/form"
|
2018-06-24 17:53:47 +00:00
|
|
|
('/', 0) => PartialFormEndMatch(1, pos), //unreachable, here only for comprehension
|
|
|
|
('f', 1) | ('F', 1) => PartialFormEndMatch(2, pos),
|
|
|
|
('o', 2) | ('O', 2) => PartialFormEndMatch(3, pos),
|
|
|
|
('r', 3) | ('R', 3) => PartialFormEndMatch(4, pos),
|
|
|
|
('m', 4) | ('M', 4) => {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if we match end of form, save "</form>" and anything after to a buffer, and insert our token
|
2018-06-24 17:53:47 +00:00
|
|
|
self.insert_tag = Some(0);
|
2018-06-30 07:32:41 +00:00
|
|
|
self.buf.push((buf[pos..len].to_vec(), 0));
|
2018-06-24 17:53:47 +00:00
|
|
|
self.state = Reset;
|
|
|
|
return Ok(pos);
|
2018-06-24 19:20:56 +00:00
|
|
|
}
|
2018-06-24 17:53:47 +00:00
|
|
|
_ => SearchInput,
|
|
|
|
},
|
2018-06-24 15:39:11 +00:00
|
|
|
SearchMethod(pos) => match buf[i] as char {
|
2018-06-25 11:11:58 +00:00
|
|
|
//try to match params
|
|
|
|
' ' => PartialNameMatch(0, pos), //space, next char is a new param
|
2018-06-24 17:53:47 +00:00
|
|
|
'>' => {
|
2018-06-25 11:11:58 +00:00
|
|
|
//end of this <input> tag, it's not Rocket special one, so insert before, saving what comes next to buffer
|
2018-06-24 15:39:11 +00:00
|
|
|
self.insert_tag = Some(0);
|
2018-06-30 07:32:41 +00:00
|
|
|
self.buf.push((buf[pos..len].to_vec(), 0));
|
2018-06-24 15:39:11 +00:00
|
|
|
self.state = Reset;
|
2018-06-24 17:53:47 +00:00
|
|
|
return Ok(pos);
|
2018-06-24 19:20:56 +00:00
|
|
|
}
|
2018-06-24 17:53:47 +00:00
|
|
|
_ => SearchMethod(pos),
|
|
|
|
},
|
|
|
|
PartialNameMatch(count, pos) => match (buf[i] as char, count) {
|
2018-06-25 11:11:58 +00:00
|
|
|
//progressively match "name='_method'", which must be first to work
|
2018-06-24 17:53:47 +00:00
|
|
|
('n', 0) | ('N', 0) => PartialNameMatch(1, pos),
|
|
|
|
('a', 1) | ('A', 1) => PartialNameMatch(2, pos),
|
|
|
|
('m', 2) | ('M', 2) => PartialNameMatch(3, pos),
|
|
|
|
('e', 3) | ('E', 3) => PartialNameMatch(4, pos),
|
|
|
|
('=', 4) => PartialNameMatch(5, pos),
|
2018-06-25 11:11:58 +00:00
|
|
|
('"', 5) | ('\'', 5) => PartialNameMatch(6, pos),
|
2018-06-30 07:32:41 +00:00
|
|
|
('_', 6) | ('_', 5) => PartialNameMatch(7, pos),
|
2018-06-24 17:53:47 +00:00
|
|
|
('m', 7) | ('M', 7) => PartialNameMatch(8, pos),
|
|
|
|
('e', 8) | ('E', 8) => PartialNameMatch(9, pos),
|
|
|
|
('t', 9) | ('T', 9) => PartialNameMatch(10, pos),
|
|
|
|
('h', 10) | ('H', 10) => PartialNameMatch(11, pos),
|
|
|
|
('o', 11) | ('O', 11) => PartialNameMatch(12, pos),
|
|
|
|
('d', 12) | ('D', 12) => PartialNameMatch(13, pos),
|
2018-06-30 07:32:41 +00:00
|
|
|
('"', 13) | ('\'', 13) | (' ', 13) => CloseInputTag, //we matched, wait for end of this <input> and insert just after
|
2018-06-25 11:11:58 +00:00
|
|
|
_ => SearchMethod(pos), //we did not match, search next param
|
2018-06-24 17:53:47 +00:00
|
|
|
},
|
|
|
|
CloseInputTag => if buf[i] as char == '>' {
|
2018-06-25 11:11:58 +00:00
|
|
|
//search for '>' at the end of an "<input name='_method'>", and insert token after
|
2018-06-24 17:53:47 +00:00
|
|
|
self.insert_tag = Some(0);
|
2018-06-30 07:32:41 +00:00
|
|
|
self.buf.push((buf[i + 1..len].to_vec(), 0));
|
2018-06-24 17:53:47 +00:00
|
|
|
self.state = Reset;
|
|
|
|
return Ok(i + 1);
|
|
|
|
} else {
|
|
|
|
CloseInputTag
|
|
|
|
},
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(len)
|
2018-06-24 17:53:47 +00:00
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
2018-06-24 19:20:56 +00:00
|
|
|
/// Csrf token to insert into pages.
|
|
|
|
///
|
|
|
|
/// The `CsrfToken` type allow you to add tokens into your pages anywhere you want, and is mainly
|
|
|
|
/// usefull if you disabled auto-insert when building the fairing registered in Rocket.
|
|
|
|
/// This impltement Serde's Serialize so you may insert it directly into your templats as if it was
|
|
|
|
/// a String. It also implement FromRequest so you can get it as a request guard. This is also the
|
|
|
|
/// only way to get this struct.
|
2018-06-24 17:53:47 +00:00
|
|
|
#[derive(Debug, Clone)]
|
2018-06-24 15:39:11 +00:00
|
|
|
pub struct CsrfToken {
|
2018-06-24 17:53:47 +00:00
|
|
|
value: String,
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Serialize for CsrfToken {
|
|
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
2018-06-24 17:53:47 +00:00
|
|
|
where
|
|
|
|
S: Serializer,
|
|
|
|
{
|
2018-06-25 11:11:58 +00:00
|
|
|
serializer.serialize_str(&self.value) //simply serialise to the underlying String
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a, 'r> FromRequest<'a, 'r> for CsrfToken {
|
|
|
|
type Error = ();
|
|
|
|
|
|
|
|
fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, ()> {
|
2018-06-24 17:53:47 +00:00
|
|
|
let (csrf_engine, duration) = request
|
|
|
|
.guard::<State<(AesGcmCsrfProtection, i64)>>()
|
|
|
|
.unwrap()
|
|
|
|
.inner();
|
|
|
|
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut cookies = request.cookies();
|
2018-06-24 17:53:47 +00:00
|
|
|
let token_value = cookies
|
|
|
|
.get(csrf::CSRF_COOKIE_NAME)
|
2018-06-24 15:39:11 +00:00
|
|
|
.and_then(|cookie| BASE64.decode(cookie.value().as_bytes()).ok())
|
|
|
|
.and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok())
|
2018-06-24 17:53:47 +00:00
|
|
|
.and_then(|cookie| {
|
2018-06-24 15:39:11 +00:00
|
|
|
let value = cookie.value();
|
|
|
|
if value.len() == 64 {
|
|
|
|
let mut array = [0; 64];
|
|
|
|
array.copy_from_slice(&value);
|
|
|
|
Some(array)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
}); //when request guard is called, parse cookie to get it's encrypted secret (if there is a cookie)
|
2018-06-24 15:39:11 +00:00
|
|
|
|
|
|
|
match csrf_engine.generate_token_pair(token_value.as_ref(), *duration) {
|
|
|
|
Ok((token, cookie)) => {
|
2018-06-25 11:11:58 +00:00
|
|
|
let mut c = Cookie::new(csrf::CSRF_COOKIE_NAME, cookie.b64_string());
|
2018-06-28 10:59:18 +00:00
|
|
|
cookies.add(c); //TODO add a timeout, same_site, http_only and secure to the cookie
|
2018-06-24 15:39:11 +00:00
|
|
|
Outcome::Success(CsrfToken {
|
2018-06-24 17:53:47 +00:00
|
|
|
value: BASE64URL_NOPAD.encode(token.value()),
|
2018-06-24 15:39:11 +00:00
|
|
|
})
|
2018-06-24 17:53:47 +00:00
|
|
|
}
|
|
|
|
Err(_) => Outcome::Failure((Status::InternalServerError, ())),
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
struct Path {
|
|
|
|
path: Vec<PathPart>,
|
|
|
|
param: Option<HashMap<String, PathPart>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Path {
|
|
|
|
fn from(path: &str) -> Self {
|
|
|
|
let (path, query) = if let Some(pos) = path.find('?') {
|
2018-06-25 11:11:58 +00:00
|
|
|
//cut the path at pos begining of query parameters
|
2018-06-24 17:53:47 +00:00
|
|
|
let (path, query) = path.split_at(pos);
|
2018-06-24 15:39:11 +00:00
|
|
|
let query = &query[1..];
|
|
|
|
(path, Some(query))
|
|
|
|
} else {
|
|
|
|
(path, None)
|
|
|
|
};
|
|
|
|
Path {
|
2018-06-24 17:53:47 +00:00
|
|
|
path: path
|
2018-06-25 11:11:58 +00:00
|
|
|
.split('/')//split path at each '/'
|
|
|
|
.filter(|seg| seg != &"")//remove empty segments
|
2018-06-24 17:53:47 +00:00
|
|
|
.map(|seg| {
|
2018-06-25 11:11:58 +00:00
|
|
|
if seg.get(..1) == Some("<") && seg.get(seg.len() - 1..) == Some(">") {//if the segment start with '<' and end with '>', it is dynamic
|
2018-06-24 17:53:47 +00:00
|
|
|
PathPart::Dynamic(seg[1..seg.len() - 1].to_owned())
|
2018-06-25 11:11:58 +00:00
|
|
|
} else {//else it's static
|
2018-06-24 15:39:11 +00:00
|
|
|
PathPart::Static(seg.to_owned())
|
2018-06-25 11:11:58 +00:00
|
|
|
}//TODO add support for <..path> to match more than one segment
|
2018-06-24 17:53:47 +00:00
|
|
|
})
|
|
|
|
.collect(),
|
|
|
|
param: query.map(|query| {
|
|
|
|
parse_args(query)
|
|
|
|
.map(|(k, v)| {
|
|
|
|
(
|
|
|
|
k.to_owned(),
|
|
|
|
if v.get(..1) == Some("<") && v.get(v.len() - 1..) == Some(">") {
|
2018-06-25 11:11:58 +00:00
|
|
|
//do the same kind of parsing as above, but on query params
|
2018-06-24 17:53:47 +00:00
|
|
|
PathPart::Dynamic(v[1..v.len() - 1].to_owned())
|
|
|
|
} else {
|
|
|
|
PathPart::Static(v.to_owned())
|
|
|
|
},
|
|
|
|
)
|
|
|
|
})
|
|
|
|
.collect()
|
2018-06-24 15:39:11 +00:00
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn extract<'a>(&self, uri: &'a str) -> Option<HashMap<&str, &'a str>> {
|
2018-06-25 11:11:58 +00:00
|
|
|
//try to match a str against a path, give back a hashmap of correponding parts if it matched
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut res: HashMap<&str, &'a str> = HashMap::new();
|
|
|
|
let (path, query) = if let Some(pos) = uri.find('?') {
|
2018-06-24 17:53:47 +00:00
|
|
|
let (path, query) = uri.split_at(pos);
|
2018-06-24 15:39:11 +00:00
|
|
|
let query = &query[1..];
|
|
|
|
(path, Some(query))
|
|
|
|
} else {
|
|
|
|
(uri, None)
|
|
|
|
};
|
2018-06-24 17:53:47 +00:00
|
|
|
let mut path = path.split('/').filter(|seg| seg != &"");
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut reference = self.path.iter();
|
|
|
|
loop {
|
|
|
|
match path.next() {
|
|
|
|
Some(v) => {
|
|
|
|
if let Some(reference) = reference.next() {
|
|
|
|
match reference {
|
2018-06-28 10:59:18 +00:00
|
|
|
PathPart::Static(refe) => if refe != v {
|
2018-06-25 11:11:58 +00:00
|
|
|
//static, but not the same, fail to parse
|
2018-06-24 17:53:47 +00:00
|
|
|
return None;
|
|
|
|
},
|
|
|
|
PathPart::Dynamic(key) => {
|
2018-06-25 11:11:58 +00:00
|
|
|
//dynamic, store to hashmap
|
2018-06-24 17:53:47 +00:00
|
|
|
res.insert(key, v);
|
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
};
|
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//not the same lenght, fail to parse
|
2018-06-24 17:53:47 +00:00
|
|
|
return None;
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
2018-06-24 17:53:47 +00:00
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
None => if reference.next().is_some() {
|
2018-06-25 11:11:58 +00:00
|
|
|
//not the same lenght, fail to parse
|
2018-06-24 17:53:47 +00:00
|
|
|
return None;
|
2018-06-24 15:39:11 +00:00
|
|
|
} else {
|
2018-06-24 17:53:47 +00:00
|
|
|
break;
|
2018-06-24 15:39:11 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(query) = query {
|
|
|
|
if let Some(ref param) = self.param {
|
2018-06-24 17:53:47 +00:00
|
|
|
let hm = parse_args(query).collect::<HashMap<&str, &str>>();
|
2018-06-24 15:39:11 +00:00
|
|
|
for (k, v) in param {
|
|
|
|
match v {
|
2018-06-24 17:53:47 +00:00
|
|
|
PathPart::Static(val) => if val != hm.get::<str>(k)? {
|
2018-06-25 11:11:58 +00:00
|
|
|
//static but not the same, fail to parse
|
2018-06-24 17:53:47 +00:00
|
|
|
return None;
|
|
|
|
},
|
|
|
|
PathPart::Dynamic(key) => {
|
2018-06-25 11:11:58 +00:00
|
|
|
//dynamic, store to hashmap
|
2018-06-24 17:53:47 +00:00
|
|
|
res.insert(key, hm.get::<str>(k)?);
|
|
|
|
}
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
2018-06-25 11:11:58 +00:00
|
|
|
//param in query, but not in reference, fail to parse
|
2018-06-24 15:39:11 +00:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
} else if self.param.is_some() {
|
2018-06-25 11:11:58 +00:00
|
|
|
//param in reference, but not in query, fail to parse
|
2018-06-24 15:39:11 +00:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
|
|
|
|
Some(res)
|
|
|
|
}
|
|
|
|
|
2018-06-28 10:59:18 +00:00
|
|
|
fn map(&self, param: &HashMap<&str, &str>) -> Option<String> {
|
2018-06-25 11:11:58 +00:00
|
|
|
//Generate a path from a reference and a hashmap
|
2018-06-24 15:39:11 +00:00
|
|
|
let mut res = String::new();
|
2018-06-28 10:59:18 +00:00
|
|
|
for seg in &self.path {
|
2018-06-25 11:11:58 +00:00
|
|
|
//TODO add a / if no elements in self.path
|
2018-06-24 15:39:11 +00:00
|
|
|
res.push('/');
|
|
|
|
match seg {
|
|
|
|
PathPart::Static(val) => res.push_str(val),
|
2018-06-24 17:53:47 +00:00
|
|
|
PathPart::Dynamic(val) => res.push_str(param.get::<str>(val)?),
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if let Some(ref keymap) = self.param {
|
2018-06-25 11:11:58 +00:00
|
|
|
//if there is some query part
|
2018-06-24 15:39:11 +00:00
|
|
|
res.push('?');
|
2018-06-24 17:53:47 +00:00
|
|
|
for (k, v) in keymap {
|
2018-06-24 15:39:11 +00:00
|
|
|
res.push_str(k);
|
|
|
|
res.push('=');
|
|
|
|
match v {
|
|
|
|
PathPart::Static(val) => res.push_str(val),
|
2018-06-24 17:53:47 +00:00
|
|
|
PathPart::Dynamic(val) => res.push_str(param.get::<str>(val)?),
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
res.push('&');
|
|
|
|
}
|
|
|
|
}
|
2018-06-25 11:11:58 +00:00
|
|
|
Some(res.trim_right_matches('&').to_owned()) //trim the last '&' which was added if there is a query part
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2018-06-24 17:53:47 +00:00
|
|
|
enum PathPart {
|
2018-06-24 15:39:11 +00:00
|
|
|
Static(String),
|
|
|
|
Dynamic(String),
|
|
|
|
}
|
|
|
|
|
2018-06-28 10:59:18 +00:00
|
|
|
fn parse_args(args: &str) -> impl Iterator<Item = (&str, &str)> {
|
2018-06-25 11:11:58 +00:00
|
|
|
//transform a group of argument into an iterator of key and value
|
2018-06-24 17:53:47 +00:00
|
|
|
args.split('&').filter_map(|kv| parse_keyvalue(&kv))
|
2018-06-24 15:39:11 +00:00
|
|
|
}
|
|
|
|
|
2018-06-28 10:59:18 +00:00
|
|
|
fn parse_keyvalue(kv: &str) -> Option<(&str, &str)> {
|
2018-06-25 11:11:58 +00:00
|
|
|
//convert a single key-value pair into a key and a value
|
2018-06-24 15:39:11 +00:00
|
|
|
if let Some(pos) = kv.find('=') {
|
2018-06-24 17:53:47 +00:00
|
|
|
let (key, value) = kv.split_at(pos + 1);
|
2018-06-24 15:39:11 +00:00
|
|
|
Some((&key[0..pos], value))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|