#![deny(missing_docs)] #![cfg_attr(feature = "cargo-clippy", deny(warnings))] //! # 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 //! for [Plume](https://github.com/Plume-org/Plume) and I didn't have the time to backport it to //! older Rocket version) //! //! ```toml //! [dependencies.rocket_csrf] //! git = "https://github.com/fdb-hiroshima/rocket_csrf" //! rev = "50947b8715ae1fa7b73e60b65fdbd1aaf7754f10" //! ``` //! 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... //! .launch(); //! } //! ``` //! //! 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 `/` //! 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; use rocket::fairing::{Fairing, Info, Kind}; use rocket::http::uri::Uri; use rocket::http::Method::{self, *}; use rocket::http::{Cookie, Status}; use rocket::outcome::Outcome; use rocket::request::{self, FromRequest}; use rocket::response::Body::Sized; use rocket::{Data, Request, Response, Rocket, State}; use serde::{Serialize, Serializer}; use std::collections::HashMap; use std::env; use std::io::Read; use std::str::from_utf8; /// Builder for [CsrfFairing](struct.CsrfFairing.html) /// /// The `CsrfFairingBuilder` type allows for creation and configuration of a [CsrfFairing](struct.CsrfFairing.html), the /// main struct of this crate. /// /// # Usage /// A Builder is created via the [`new`] method. Then you can configure it with others provided /// methods, and get a [CsrfFairing](struct.CsrfFairing.html) by a call to [`finalize`] /// /// [`new`]: #method.new /// [`finalize`]: #method.finalize /// /// ## 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(); /// } /// ``` 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, auto_insert_max_size: u64, } impl CsrfFairingBuilder { /// Create a new builder with default values. pub fn new() -> Self { CsrfFairingBuilder { duration: 60 * 60, default_target: (String::from("/"), Get), exceptions: Vec::new(), secret: None, auto_insert: true, auto_insert_disable_prefix: Vec::new(), auto_insert_max_size: 16 * 1024, } } /// Set the timeout (in seconds) of CSRF tokens generated by the final Fairing. Default timeout /// is one hour. pub fn set_timeout(mut self, timeout: i64) -> Self { self.duration = timeout; self } /// Set the default route when an invalide request is catched, you may add a 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(); /// } pub fn set_default_target(mut self, default_target: String, method: Method) -> Self { self.default_target = (default_target, method); self } /// 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 , which will be replaced in the target route. /// Note that this is not aware of Rocket's routes, so matching `/something/` 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. /// /// [`add_exceptions`]: #method.add_exceptions /// /// # 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//path".to_owned(), "/csrf-error?where=".to_owned(), rocket::http::Method::Get)) /// ]) /// .finish().unwrap()) /// //add your routes, other fairings... /// .launch(); /// } /// ``` pub fn set_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self { self.exceptions = exceptions; self } /// 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. /// /// [`set_exceptions`]: #method.set_exceptions pub fn add_exceptions(mut self, exceptions: Vec<(String, String, Method)>) -> Self { self.exceptions.extend(exceptions); self } /// Set the secret key used to generate secure cryptographic tokens. If not set, rocket_csrf /// will attempt to get the secret used by Rocket for it's own private cookies via the /// 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 /// their validity in case of an application restart. /// /// # 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(); /// } /// ``` pub fn set_secret(mut self, secret: [u8; 32]) -> Self { self.secret = Some(secret); self } /// 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 /// [CsrfFairing](struct.CsrfFairing.html), which you may obtain via request guards. /// 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) -> 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 } /// Get the fairing from the builder. pub fn finalize(self) -> Result { let secret = self.secret.unwrap_or_else(|| { //use provided secret if one is env::vars() .find(|(key, _)| key == "ROCKET_SECRET_KEY") .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 { None } })//else get secret environment variable .unwrap_or_else(|| { eprintln!("[rocket_csrf] No secret key was found, you should consider set one to keep token validity across application restart"); thread_rng().gen() }) //if environment variable is not set, generate a random secret and print a warning }); let default_target = Path::from(&self.default_target.0); let mut hashmap = HashMap::new(); hashmap.insert("uri", ""); if default_target.map(&hashmap).is_none() { return Err(()); } //verify if this path is valid as default path, i.e. it have at most one dynamic part which is Ok(CsrfFairing { duration: self.duration, default_target: (default_target, self.default_target.1), exceptions: self .exceptions .iter() .map(|(a, b, m)| (Path::from(&a), Path::from(&b), *m))//TODO verify if source and target are compatible .collect(), secret, auto_insert: self.auto_insert, auto_insert_disable_prefix: self.auto_insert_disable_prefix, auto_insert_max_size: self.auto_insert_max_size, }) } } impl Default for CsrfFairingBuilder { fn default() -> Self { Self::new() } } /// 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. /// This is created via a [CsrfFairingBuilder](struct.CsrfFairingBuilder.html), and implement nothing else than the `Fairing` trait. /// /// [`CsrfFairingBuilder`]: /rocket_csrf/struct.CsrfFairing.html pub struct CsrfFairing { duration: i64, default_target: (Path, Method), exceptions: Vec<(Path, Path, Method)>, secret: [u8; 32], auto_insert: bool, auto_insert_disable_prefix: Vec, auto_insert_max_size: u64, } impl Fairing for CsrfFairing { fn info(&self) -> Info { if self.auto_insert { Info { name: "CSRF protection", kind: Kind::Attach | Kind::Request | Kind::Response, } } else { Info { name: "CSRF protection", kind: Kind::Attach | Kind::Request, } } } fn on_attach(&self, rocket: Rocket) -> Result { Ok(rocket.manage((AesGcmCsrfProtection::from_key(self.secret), self.duration))) //add the Csrf engine to Rocket's managed state } fn on_request(&self, request: &mut Request, data: &Data) { match request.method() { Get | Head | Connect | Options => { let _ = request.guard::(); //force regeneration of csrf cookies return; } _ => {} }; let (csrf_engine, _) = request .guard::>() .unwrap() .inner(); let cookie = request .cookies() .get(csrf::CSRF_COOKIE_NAME) .and_then(|cookie| BASE64.decode(cookie.value().as_bytes()).ok()) .and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()); //get and parse Csrf cookie let _ = request.guard::(); //force regeneration of csrf cookies let token = parse_args(from_utf8(data.peek()).unwrap_or("")) .filter(|(key, _)| key == &csrf::CSRF_FORM_FIELD) .filter_map(|(_, token)| BASE64URL_NOPAD.decode(&token.as_bytes()).ok()) .filter_map(|token| csrf_engine.parse_token(&token).ok()) .next(); //get and parse Csrf token if let Some(token) = token { if let Some(cookie) = cookie { if csrf_engine.verify_token_pair(&token, &cookie) { return; //if we got both token and cookie, and they match each other, we do nothing } } } //Request reaching here are violating Csrf protection for (src, dst, method) in &self.exceptions { if let Some(param) = src.extract(&request.uri().to_string()) { if let Some(destination) = dst.map(¶m) { request.set_uri(destination); request.set_method(*method); return; } } } //if request matched no exception, reroute it to default target let uri = request.uri().to_string(); let uri = Uri::percent_encode(&uri); let mut param: HashMap<&str, &str> = HashMap::new(); param.insert("uri", &uri); request.set_uri(self.default_target.0.map(¶m).unwrap()); 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; } } //if content type is not html, we do nothing let uri = request.uri().to_string(); if self .auto_insert_disable_prefix .iter() .any(|prefix| uri.starts_with(prefix)) { return; } //if request is on an ignored prefix, ignore it let token = match request.guard::() { Outcome::Success(t) => t, _ => return, }; //if we can't get a token, leave request unchanged, we can't do anything anyway let body = response.take_body(); //take request body from Rocket if body.is_none() { return; } //if there was no body, leave it that way let body = body.unwrap(); if let Sized(body_reader, len) = body { if len <= self.auto_insert_max_size { //if this is a small enought body, process the full body let mut res = Vec::with_capacity(len as usize); CsrfProxy::from(body_reader, &token) .read_to_end(&mut res) .unwrap(); response.set_sized_body(std::io::Cursor::new(res)); } else { //if body is of known but long size, change it to a stream to preserve memory, by encapsulating it into our "proxy" struct let body = body_reader; response.set_streamed_body(Box::new(CsrfProxy::from(body, &token))); } } else { //if body is of unknown size, encapsulate it into our "proxy" struct let body = body.into_inner(); response.set_streamed_body(Box::new(CsrfProxy::from(body, &token))); } } } #[derive(Debug)] enum ParseState { Reset, //default state PartialFormMatch(u8), //when parsing ", search for begining of a param PartialNameMatch(u8, usize), //when parsing "name="_method"" CloseInputTag, //only if insert after, search for '>' of a "" } struct CsrfProxy<'a> { underlying: Box, //the underlying Reader from which we get data token: Vec, //a full input tag loaded with a valid token buf: Vec<(Vec, 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, //if we have to insert tag here, and how fare are we in the tag (in case of very short read()s) } impl<'a> CsrfProxy<'a> { fn from(underlying: Box, token: &CsrfToken) -> Self { let tag_begin = b""; let mut token = Vec::new(); token.extend_from_slice(tag_begin); token.extend_from_slice(tag_middle); token.extend_from_slice(tag_end); CsrfProxy { underlying, token, buf: Vec::new(), state: ParseState::Reset, insert_tag: None, } } } impl<'a> Read for CsrfProxy<'a> { fn read(&mut self, buf: &mut [u8]) -> Result { if let Some(pos) = self.insert_tag { //if we should insert a tag let size = buf.len(); 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 if copy_size == self.token.len() - pos { //if we copied the full tag, say we don't need to set it again self.insert_tag = None; } else { //if we didn't copy the full tag, save where we were self.insert_tag = Some(pos + copy_size); } return Ok(copy_size); //return the lenght of the copied data } let len = if let Some((vec, pos)) = self.buf.pop() { //if there is a buffer to add here let size = buf.len(); if vec.len() - pos <= size { //if the part left of the buffer is smaller than buf buf[0..vec.len() - pos].copy_from_slice(&vec[pos..]); vec.len() - pos } else { //else if the part left of the buffer is bigger than buf buf.copy_from_slice(&vec[pos..pos + size]); self.buf.push((vec, pos + size)); size } //send the size of what was read as if it was a normal read on underlying struct } else { //if there is no buffer to add, read from underlying struct 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 { //for each byte use ParseState::*; self.state = match self.state { Reset => if buf[i] as char == '<' { //if we are in default state and we begin to match any tag PartialFormMatch(0) } else { //if we don't match a tag Reset }, PartialFormMatch(count) => match (buf[i] as char, count) { //progressively match "form" ('f', 0) | ('F', 0) => PartialFormMatch(1), ('o', 1) | ('O', 1) => PartialFormMatch(2), ('r', 2) | ('R', 2) => PartialFormMatch(3), ('m', 3) | ('M', 3) => SearchInput, //when we success, go to next state _ => Reset, //if this don't match, go back to defailt state }, SearchInput => if buf[i] as char == '<' { //begin to match any tag PartialInputMatch(0, i) } else { SearchInput }, PartialInputMatch(count, pos) => match (buf[i] as char, count) { //progressively match "input" ('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), ('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 }, PartialFormEndMatch(count, pos) => match (buf[i] as char, count) { //progressively match "/form" ('/', 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) => { //if we match end of form, save "" and anything after to a buffer, and insert our token self.insert_tag = Some(0); self.buf.push((buf[pos..len].to_vec(), 0)); self.state = Reset; return Ok(pos); } _ => SearchInput, }, SearchMethod(pos) => match buf[i] as char { //try to match params ' ' => PartialNameMatch(0, pos), //space, next char is a new param '>' => { //end of this tag, it's not Rocket special one, so insert before, saving what comes next to buffer self.insert_tag = Some(0); self.buf.push((buf[pos..len].to_vec(), 0)); self.state = Reset; return Ok(pos); } _ => SearchMethod(pos), }, PartialNameMatch(count, pos) => match (buf[i] as char, count) { //progressively match "name='_method'", which must be first to work ('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), ('"', 5) | ('\'', 5) => PartialNameMatch(6, pos), ('_', 6) | ('_', 5) => PartialNameMatch(7, pos), ('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), ('"', 13) | ('\'', 13) | (' ', 13) => CloseInputTag, //we matched, wait for end of this and insert just after _ => SearchMethod(pos), //we did not match, search next param }, CloseInputTag => if buf[i] as char == '>' { //search for '>' at the end of an "", and insert token after self.insert_tag = Some(0); self.buf.push((buf[i + 1..len].to_vec(), 0)); self.state = Reset; return Ok(i + 1); } else { CloseInputTag }, } } Ok(len) } } /// 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. #[derive(Debug, Clone)] pub struct CsrfToken { value: String, } impl Serialize for CsrfToken { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.value) //simply serialise to the underlying String } } impl<'a, 'r> FromRequest<'a, 'r> for CsrfToken { type Error = (); fn from_request(request: &'a Request<'r>) -> request::Outcome { let (csrf_engine, duration) = request .guard::>() .unwrap() .inner(); let mut cookies = request.cookies(); let token_value = cookies .get(csrf::CSRF_COOKIE_NAME) .and_then(|cookie| BASE64.decode(cookie.value().as_bytes()).ok()) .and_then(|cookie| csrf_engine.parse_cookie(&cookie).ok()) .and_then(|cookie| { let value = cookie.value(); if value.len() == 64 { let mut array = [0; 64]; array.copy_from_slice(&value); Some(array) } else { None } }); //when request guard is called, parse cookie to get it's encrypted secret (if there is a cookie) match csrf_engine.generate_token_pair(token_value.as_ref(), *duration) { Ok((token, cookie)) => { let mut c = Cookie::new(csrf::CSRF_COOKIE_NAME, cookie.b64_string()); cookies.add(c); //TODO add a timeout, same_site, http_only and secure to the cookie Outcome::Success(CsrfToken { value: BASE64URL_NOPAD.encode(token.value()), }) } Err(_) => Outcome::Failure((Status::InternalServerError, ())), } } } #[derive(Debug)] struct Path { path: Vec, param: Option>, } impl Path { fn from(path: &str) -> Self { let (path, query) = if let Some(pos) = path.find('?') { //cut the path at pos begining of query parameters let (path, query) = path.split_at(pos); let query = &query[1..]; (path, Some(query)) } else { (path, None) }; Path { path: path .split('/')//split path at each '/' .filter(|seg| seg != &"")//remove empty segments .map(|seg| { if seg.get(..1) == Some("<") && seg.get(seg.len() - 1..) == Some(">") {//if the segment start with '<' and end with '>', it is dynamic PathPart::Dynamic(seg[1..seg.len() - 1].to_owned()) } else {//else it's static PathPart::Static(seg.to_owned()) }//TODO add support for <..path> to match more than one segment }) .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(">") { //do the same kind of parsing as above, but on query params PathPart::Dynamic(v[1..v.len() - 1].to_owned()) } else { PathPart::Static(v.to_owned()) }, ) }) .collect() }), } } fn extract<'a>(&self, uri: &'a str) -> Option> { //try to match a str against a path, give back a hashmap of correponding parts if it matched let mut res: HashMap<&str, &'a str> = HashMap::new(); let (path, query) = if let Some(pos) = uri.find('?') { let (path, query) = uri.split_at(pos); let query = &query[1..]; (path, Some(query)) } else { (uri, None) }; let mut path = path.split('/').filter(|seg| seg != &""); let mut reference = self.path.iter(); loop { match path.next() { Some(v) => { if let Some(reference) = reference.next() { match reference { PathPart::Static(refe) => if refe != v { //static, but not the same, fail to parse return None; }, PathPart::Dynamic(key) => { //dynamic, store to hashmap res.insert(key, v); } }; } else { //not the same lenght, fail to parse return None; } } None => if reference.next().is_some() { //not the same lenght, fail to parse return None; } else { break; }, } } if let Some(query) = query { if let Some(ref param) = self.param { let hm = parse_args(query).collect::>(); for (k, v) in param { match v { PathPart::Static(val) => if val != hm.get::(k)? { //static but not the same, fail to parse return None; }, PathPart::Dynamic(key) => { //dynamic, store to hashmap res.insert(key, hm.get::(k)?); } } } } else { //param in query, but not in reference, fail to parse return None; } } else if self.param.is_some() { //param in reference, but not in query, fail to parse return None; } Some(res) } fn map(&self, param: &HashMap<&str, &str>) -> Option { //Generate a path from a reference and a hashmap let mut res = String::new(); for seg in &self.path { //TODO add a / if no elements in self.path res.push('/'); match seg { PathPart::Static(val) => res.push_str(val), PathPart::Dynamic(val) => res.push_str(param.get::(val)?), } } if let Some(ref keymap) = self.param { //if there is some query part res.push('?'); for (k, v) in keymap { res.push_str(k); res.push('='); match v { PathPart::Static(val) => res.push_str(val), PathPart::Dynamic(val) => res.push_str(param.get::(val)?), } res.push('&'); } } Some(res.trim_right_matches('&').to_owned()) //trim the last '&' which was added if there is a query part } } #[derive(Debug)] enum PathPart { Static(String), Dynamic(String), } fn parse_args(args: &str) -> impl Iterator { //transform a group of argument into an iterator of key and value args.split('&').filter_map(|kv| parse_keyvalue(&kv)) } fn parse_keyvalue(kv: &str) -> Option<(&str, &str)> { //convert a single key-value pair into a key and a value if let Some(pos) = kv.find('=') { let (key, value) = kv.split_at(pos + 1); Some((&key[0..pos], value)) } else { None } }