From 20d97cd9d567e511f5447377f0b389e9f9f98189 Mon Sep 17 00:00:00 2001 From: Skyler Lipthay Date: Sun, 5 Jul 2015 22:49:11 -0700 Subject: [PATCH] (init) --- .gitignore | 2 + CONTRIBUTING.md | 26 +++ Cargo.toml | 17 ++ LICENSE | 21 ++ README.md | 10 + examples/params.rs | 36 ++++ src/lib.rs | 474 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 586 insertions(+) create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 examples/params.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1ce697c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# Contributing + +**params** uses the same conventions as **[Iron](https://github.com/iron/iron)**. + +### Overview + +* Fork body-parser to your own account +* Create a feature branch, namespaced by. + * bug/... + * feat/... + * test/... + * doc/... + * refactor/... +* Make commits to your feature branch. Prefix each commit like so: + * (feat) Added a new feature + * (fix) Fixed inconsistent tests [Fixes #0] + * (refactor) ... + * (cleanup) ... + * (test) ... + * (doc) ... +* Make a pull request with your changes directly to master. Include a + description of your changes. +* Wait for one of the reviewers to look at your code and either merge it or + give feedback which you should adapt to. + +#### Thank you for contributing! diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9176001 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "params" +version = "0.0.1" +authors = ["Skyler Lipthay "] +description = "A multi-source request parameters parser for Iron." +readme = "README.md" +license = "MIT" +repository = "https://github.com/iron/params" +documentation = "http://ironframework.io/doc/params/index.html" + +[dependencies] +bodyparser = "*" +formdata = { git = "https://github.com/SkylerLipthay/formdata" } +iron = "*" +plugin = "*" +rustc-serialize = "*" +urlencoded = "*" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27e00f8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 iron + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9df1e6b --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# params + +A plugin for the [Iron](https://github.com/iron/iron) web framework that parses parameters from incoming requests from the following sources: + +* JSON data (`Content-Type: application/json`) +* URL-encoded GET parameters +* URL-encoded `Content-Type: application/x-www-form-urlencoded` parameters +* Multipart form data (`Content-Type: multipart/form-data`) + +See `examples/params.rs` for details. diff --git a/examples/params.rs b/examples/params.rs new file mode 100644 index 0000000..62384ed --- /dev/null +++ b/examples/params.rs @@ -0,0 +1,36 @@ +extern crate iron; +extern crate params; + +use iron::prelude::*; +use iron::status; +use params::Params; + +fn handle(req: &mut Request) -> IronResult { + println!("{:?}", req.get_ref::()); + Ok(Response::with(status::Ok)) +} + +// Execute the following cURL requests and watch your terminal for the parsed parameters. +// +// `curl -i "localhost:3000" -H "Content-Type: application/json" -d '{"name":"jason","age":2}'` +// => Ok({"age": U64(2), "name": String("jason")}) +// +// `curl -i -X POST "http://localhost:3000/" --data "fruit=apple&name=iron&fruit=pear"` +// => Ok({"fruit": String("pear"), "name": String("iron")}) +// +// `curl -i "http://localhost:3000/?x\[\]=1&x\[\]=2" -F "images[]=@/path/to/file.jpg"` +// => Ok({ +// "images": Array([File(UploadedFile { +// path: "/tmp/path/to/file.jpg", +// filename: Some("file.jpg"), +// content_type: Mime(Image, Jpeg, []), +// size: 1234 +// })]), +// "x": Array([String("1"), String("2")]) +// }) +// +// `curl -i -X POST "http://localhost:3000/" --data "x[][]=2"` +// => Err(InvalidPath) +fn main() { + Iron::new(Chain::new(handle)).http("localhost:3000").unwrap(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..541ae88 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,474 @@ +//! Request parameters parser plugin for Iron +//! +//! This plugin is a multi-source request parameters parser. + +extern crate bodyparser; +extern crate formdata; +extern crate iron; +extern crate plugin; +extern crate rustc_serialize; +extern crate urlencoded; + +use std::collections::BTreeMap; +use std::error::Error as StdError; +use std::fmt; +use std::io::Read; +use std::ops::{Deref, DerefMut}; + +pub use formdata::UploadedFile; +use iron::{headers, Headers, mime, Request}; +use iron::typemap::Key; +use plugin::{Pluggable, Plugin}; +use rustc_serialize::json::Json; + +/// A representation of all possible types of request parameters. +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + /// A `null` from the request's JSON body. + Null, + /// A `true` or `false` from the request's JSON body. + Boolean(bool), + /// A signed integer from the request's JSON body. + I64(i64), + /// An unsigned integer from the request's JSON body. + U64(u64), + /// A floating point number from the request's JSON body. + F64(f64), + /// Either a string from the request's JSON body or a plain text value from the request's URL + /// (GET) parameters, `application/x-www-form-urlencoded` parameters, or `multipart/form-data` + /// parameters. + String(String), + /// A temporary file processed from a `multipart/form-data` request. + File(UploadedFile), + /// Either an array of JSON values or an amalgamation of URL (GET) parameters, + /// `application/x-www-form-urlencoded` parameters, or `multipart/form-data` parameters. Such + /// parameters should be in the form `my_list[]=1&my_list[]=2` to be processed into this + /// variant. + Array(Vec), + /// Either an object of JSON keys and values or an amalgamation of URL (GET) parameters, + /// `application/x-www-form-urlencoded` parameters, or `multipart/form-data` parameters. Such + /// parameters should be in the form `my_map[x]=1&my_map[y]=2` to be processed into this + /// variant. + Map(Map), +} + +impl Value { + fn assign(&mut self, path: &str, value: Value) -> Result<(), ParamsError> { + assert!(!path.is_empty()); + + let (key, remainder) = try!(eat_index(path)); + + if key.is_empty() { + match *self { + Value::Array(ref mut array) => { + if remainder.is_empty() { + array.push(value); + return Ok(()); + } + + let (next_key, _) = try!(eat_index(remainder)); + if next_key.is_empty() { + // Two array indices in a row ("[][]") is illegal. + return Err(InvalidPath); + } + + if let Some(map) = array.last_mut() { + if !try!(map.contains_key(next_key)) { + return map.assign(remainder, value); + } + } + + let mut map = Value::Map(Map::new()); + try!(map.assign(remainder, value)); + array.push(map); + Ok(()) + }, + _ => Err(CannotAppend), + } + } else { + match *self { + Value::Map(ref mut map) => { + if remainder.is_empty() { + map.0.insert(String::from(key), value); + return Ok(()); + } + + let (next_key, _) = try!(eat_index(remainder)); + let collection = map.0.entry(String::from(key)).or_insert_with(|| { + if next_key.is_empty() { + Value::Array(vec![]) + } else { + Value::Map(Map::new()) + } + }); + + collection.assign(remainder, value) + }, + _ => Err(CannotInsert), + } + } + } + + fn contains_key(&mut self, key: &str) -> Result { + match *self { + Value::Map(ref map) => Ok(map.contains_key(key)), + _ => Err(CannotInsert), + } + } +} + +/// A type that maps keys to request parameter values. +#[derive(Clone, PartialEq)] +pub struct Map(pub BTreeMap); + +impl Map { + /// Creates an empty map. + pub fn new() -> Map { + Map(BTreeMap::new()) + } + + /// Inserts a parameter value to the specified key path. + /// + /// Key paths are a series of values starting with a plain name and followed by zero or more + /// array-like indices. `name` is "the value called `name`", `names[]` is "the array called + /// `names`", `pts[][x]` and `pts[][y]` are the `x` and `y` values of a map at the end of + /// an array. + /// + /// This method is used during the internal parsing processes and is only made public in the + /// name of hypothetical extensibility. + /// + /// # Examples + /// + /// ``` + /// # use params::{Map, Value}; + /// let mut map = Map::new(); + /// map.assign("name", Value::String("Callie".into())).unwrap(); + /// assert_eq!(format!("{:?}", map), r#"{"name": String("Callie")}"#); + /// ``` + /// + /// ``` + /// # use params::{Map, Value}; + /// let mut map = Map::new(); + /// map.assign("names[]", Value::String("Anne".into())).unwrap(); + /// map.assign("names[]", Value::String("Bob".into())).unwrap(); + /// assert_eq!(format!("{:?}", map), r#"{"names": Array([String("Anne"), String("Bob")])}"#); + /// ``` + /// + /// ``` + /// # use params::{Map, Value}; + /// let mut map = Map::new(); + /// map.assign("pts[][x]", Value::I64(3)).unwrap(); + /// map.assign("pts[][y]", Value::I64(9)).unwrap(); + /// assert_eq!(format!("{:?}", map), r#"{"pts": Array([Map({"x": I64(3), "y": I64(9)})])}"#); + /// ``` + pub fn assign(&mut self, path: &str, value: Value) -> Result<(), ParamsError> { + let (base, remainder) = try!(eat_base(path)); + if remainder.is_empty() { + self.0.insert(String::from(base), value); + return Ok(()); + } + + let (key, _) = try!(eat_index(remainder)); + let collection = self.0.entry(String::from(base)).or_insert_with(|| { + if key.is_empty() { + Value::Array(vec![]) + } else { + Value::Map(Map::new()) + } + }); + + try!(collection.assign(remainder, value)); + + Ok(()) + } + + /// Traverses nested `Map`s to find the specified value by key. + /// + /// # Examples + /// + /// ``` + /// # use params::{Map, Value}; + /// let mut map = Map::new(); + /// map.assign("user[name]", Value::String("Marie".into())).unwrap(); + /// + /// match map.find(&["user", "name"]) { + /// Some(&Value::String(ref name)) => assert_eq!(name, "Marie"), + /// _ => panic!("Unexpected parameter type!"), + /// } + /// + /// assert!(map.find(&["user", "age"]).is_none()); + /// ``` + pub fn find(&self, keys: &[&str]) -> Option<&Value> { + if keys.is_empty() { + return None; + } + + let mut value = self.0.get(keys[0]); + + for key in &keys[1..] { + value = match value { + Some(&Value::Map(ref map)) => map.0.get(*key), + _ => return None, + } + } + + value + } +} + +impl Deref for Map { + type Target = BTreeMap; + + fn deref(&self) -> &BTreeMap { + &self.0 + } +} + +impl DerefMut for Map { + fn deref_mut(&mut self) -> &mut BTreeMap { + &mut self.0 + } +} + +impl fmt::Debug for Map { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f) + } +} + +fn eat_base<'a>(path: &'a str) -> Result<(&'a str, &'a str), ParamsError> { + let open = path.char_indices().find(|&(_, c)| c == '[').map(|(i, _)| i).unwrap_or(path.len()); + let base = &path[..open]; + let remainder = &path[open..]; + match base.is_empty() { + false => Ok((base, remainder)), + true => Err(InvalidPath), + } +} + +#[test] +fn test_eat_base() { + assert_eq!(eat_base("before[after]").unwrap(), ("before", "[after]")); + assert!(eat_base("[][something]").is_err()); +} + +fn eat_index<'a>(path: &'a str) -> Result<(&'a str, &'a str), ParamsError> { + if path.chars().next() != Some('[') { + return Err(InvalidPath); + } + + let index = path.char_indices().skip(1).find(|&(_, c)| c == ']'); + let close = try!(index.ok_or(InvalidPath)).0; + let key = &path[1..close]; + let remainder = &path[1 + close..]; + + Ok((key, remainder)) +} + +#[test] +fn test_eat_index() { + assert_eq!(eat_index("[something][fishy]").unwrap(), ("something", "[fishy]")); + assert_eq!(eat_index("[][something]").unwrap(), ("", "[something]")); + assert!(eat_index("invalid[]").is_err()); +} + +/// An error representing any of the possible errors that can occur during parameter processing. +#[derive(Debug)] +pub enum ParamsError { + /// An error from parsing the request body. + BodyError(bodyparser::BodyError), + /// An error from parsing URL encoded data. + UrlDecodingError(urlencoded::UrlDecodingError), + /// An error from parsing a `multipart/form-data` request body. + FormDataError(formdata::Error), + /// Invalid parameter path. + InvalidPath, + /// Tried to append to a non-array value. + CannotAppend, + /// Tried to insert into a non-map value. + CannotInsert, + /// Tried to make a `Map` from a non-object root JSON value. + NotJsonObject, +} + +use ParamsError::*; + +impl fmt::Display for ParamsError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + self.description().fmt(f) + } +} + +impl StdError for ParamsError { + fn description(&self) -> &str { + match *self { + BodyError(ref err) => err.description(), + UrlDecodingError(ref err) => err.description(), + FormDataError(ref err) => err.description(), + InvalidPath => "Invalid parameter path.", + CannotAppend => "Cannot append to a non-array value.", + CannotInsert => "Cannot insert into a non-map value.", + NotJsonObject => "Tried to make a `Map` from a non-object root JSON value.", + } + } + + fn cause(&self) -> Option<&StdError> { + match *self { + BodyError(ref err) => Some(err), + UrlDecodingError(ref err) => Some(err), + FormDataError(ref err) => Some(err), + _ => None, + } + } +} + +impl From for ParamsError { + fn from(err: bodyparser::BodyError) -> ParamsError { + BodyError(err) + } +} + +/// Plugin for `iron::Request` that processes and combines request parameters from the various +/// request sources. +/// +/// The following sources are merged into a `Map`: +/// +/// * JSON data (`Content-Type: application/json`) +/// * URL-encoded GET parameters +/// * URL-encoded `Content-Type: application/x-www-form-urlencoded` parameters +/// * Multipart form data (`Content-Type: multipart/form-data`) +/// +/// Use `req.get_ref::()` where `req` is an `iron::Request` to get a `Map` of the request's +/// parameters. +pub struct Params; + +impl Key for Params { + type Value = Map; +} + +impl<'a, 'b> Plugin> for Params { + type Error = ParamsError; + + fn eval(req: &mut Request) -> Result { + let mut map = try!(try_parse_json_into_map(req)); + let has_json_body = !map.is_empty(); + try!(try_parse_multipart(req, &mut map)); + try!(try_parse_url_encoded::(req, &mut map)); + if !has_json_body { + try!(try_parse_url_encoded::(req, &mut map)); + } + + Ok(map) + } +} + +fn try_parse_json_into_map(req: &mut Request) -> Result { + let need_parse = req.headers.get::().map(|header| { + match **header { + mime::Mime(mime::TopLevel::Application, mime::SubLevel::Json, _) => true, + _ => false + } + }).unwrap_or(false); + + if !need_parse { + return Ok(Map::new()); + } + + match *try!(req.get_ref::()) { + Some(ref json) => json.to_map(), + None => Ok(Map::new()), + } +} + +trait ToParams { + fn to_map(&self) -> Result; + fn to_value(&self) -> Result; +} + +impl ToParams for Json { + fn to_map(&self) -> Result { + match try!(self.to_value()) { + Value::Map(map) => Ok(map), + _ => Err(NotJsonObject), + } + } + + fn to_value(&self) -> Result { + match *self { + Json::I64(value) => Ok(Value::I64(value)), + Json::U64(value) => Ok(Value::U64(value)), + Json::F64(value) => Ok(Value::F64(value)), + Json::String(ref value) => Ok(Value::String(value.clone())), + Json::Boolean(value) => Ok(Value::Boolean(value)), + Json::Null => Ok(Value::Null), + Json::Array(ref value) => { + let mut result = Vec::with_capacity(value.len()); + for json in value { + result.push(try!(json.to_value())); + } + + Ok(Value::Array(result)) + }, + Json::Object(ref value) => { + let mut result = Map::new(); + for (key, json) in value { + result.insert(key.clone(), try!(json.to_value())); + } + + Ok(Value::Map(result)) + }, + } + } +} + +fn try_parse_multipart(req: &mut Request, map: &mut Map) -> Result<(), ParamsError> { + use formdata::Error::*; + + let form_data = match formdata::parse_multipart(&mut IronRequest(req)) { + Ok(form_data) => form_data, + Err(NoRequestContentType) | Err(NotMultipart) | Err(NotFormData) => return Ok(()), + Err(err) => return Err(FormDataError(err)), + }; + + for (path, value) in form_data.fields { + try!(map.assign(&path, Value::String(value))); + } + + for (path, value) in form_data.files { + try!(map.assign(&path, Value::File(value))); + } + + Ok(()) +} + +fn try_parse_url_encoded<'a, 'b, P>(req: &mut Request<'a, 'b>, map: &mut Map) + -> Result<(), ParamsError> + where P: Plugin, Error=urlencoded::UrlDecodingError>, + P: Key +{ + let hash_map = match req.get::

() { + Ok(hash_map) => hash_map, + Err(urlencoded::UrlDecodingError::EmptyQuery) => return Ok(()), + Err(err) => return Err(UrlDecodingError(err)), + }; + + for (path, vec) in hash_map { + for value in vec { + try!(map.assign(&path, Value::String(value))); + } + } + + Ok(()) +} + +struct IronRequest<'c, 'a: 'c, 'b: 'a>(&'c mut Request<'a, 'b>); + +impl<'c, 'a: 'c, 'b: 'a> formdata::Request for IronRequest<'c, 'a, 'b> { + fn headers(&self) -> &Headers { + &self.0.headers + } + + fn read_mut(&mut self) -> &mut Read { + &mut self.0.body + } +}