blob: 9e320a20f129bab853741685d20ff5008e2fd4d0 [file] [log] [blame]
//! Integration with the [Iron](https://ironframework.io) framework, enabled with the `iron` feature (optional). Includes a `BeforeMiddleware` implementation.
//!
//! Not shown here: `impl `[`HttpRequest`](../trait.HttpRequest.html#implementors)` for
//! iron::Request`.
use iron::headers::ContentType;
use iron::mime::{Mime, TopLevel, SubLevel};
use iron::request::{Body as IronBody, Request as IronRequest};
use iron::typemap::Key;
use iron::{BeforeMiddleware, IronError, IronResult};
use std::path::PathBuf;
use std::{error, fmt, io};
use super::{FieldHeaders, HttpRequest, Multipart};
use super::save::{Entries, PartialReason, TempDir};
use super::save::SaveResult::*;
impl<'r, 'a, 'b> HttpRequest for &'r mut IronRequest<'a, 'b> {
type Body = &'r mut IronBody<'a, 'b>;
fn multipart_boundary(&self) -> Option<&str> {
let content_type = try_opt!(self.headers.get::<ContentType>());
if let Mime(TopLevel::Multipart, SubLevel::FormData, _) = **content_type {
content_type.get_param("boundary").map(|b| b.as_str())
} else {
None
}
}
fn body(self) -> &'r mut IronBody<'a, 'b> {
&mut self.body
}
}
/// The default file size limit for [`Intercept`](struct.Intercept.html), in bytes.
pub const DEFAULT_FILE_SIZE_LIMIT: u64 = 2 * 1024 * 1024;
/// The default file count limit for [`Intercept`](struct.Intercept.html).
pub const DEFAULT_FILE_COUNT_LIMIT: u32 = 16;
/// A `BeforeMiddleware` for Iron which will intercept and read-out multipart requests and store
/// the result in the request.
///
/// Successful reads will be placed in the `extensions: TypeMap` field of `iron::Request` as an
/// [`Entries`](../struct.Entries.html) instance (as both key-type and value):
///
/// ```no_run
/// extern crate iron;
/// extern crate multipart;
///
/// use iron::prelude::*;
///
/// use multipart::server::Entries;
/// use multipart::server::iron::Intercept;
///
/// fn main() {
/// let mut chain = Chain::new(|req: &mut Request| if let Some(entries) =
/// req.extensions.get::<Entries>() {
///
/// Ok(Response::with(format!("{:?}", entries)))
/// } else {
/// Ok(Response::with("Not a multipart request"))
/// });
///
/// chain.link_before(Intercept::default());
///
/// Iron::new(chain).http("localhost:80").unwrap();
/// }
/// ```
///
/// Any errors during which occur during reading will be passed on as `IronError`.
#[derive(Debug)]
pub struct Intercept {
/// The parent directory for all temporary directories created by this middleware.
/// Will be created if it doesn't exist (lazy).
///
/// If omitted, uses the OS temporary directory.
///
/// Default value: `None`.
pub temp_dir_path: Option<PathBuf>,
/// The size limit of uploaded files, in bytes.
///
/// Files which exceed this size will be rejected.
/// See the `limit_behavior` field for more info.
///
/// Default value: [`DEFAULT_FILE_SIZE_LIMIT`](constant.default_file_size_limit.html)
pub file_size_limit: u64,
/// The limit on the number of files which will be saved from
/// the request. Requests which exceed this count will be rejected.
///
/// Default value: [`DEFAULT_FILE_COUNT_LIMT`](constant.default_file_count_limit.html)
pub file_count_limit: u32,
/// What to do when a file count or size limit has been exceeded.
///
/// See [`LimitBehavior`](enum.limitbehavior.html) for more info.
pub limit_behavior: LimitBehavior,
}
impl Intercept {
/// Set the `temp_dir_path` for this middleware.
pub fn temp_dir_path<P: Into<PathBuf>>(self, path: P) -> Self {
Intercept { temp_dir_path: Some(path.into()), .. self }
}
/// Set the `file_size_limit` for this middleware.
pub fn file_size_limit(self, limit: u64) -> Self {
Intercept { file_size_limit: limit, .. self }
}
/// Set the `file_count_limit` for this middleware.
pub fn file_count_limit(self, limit: u32) -> Self {
Intercept { file_count_limit: limit, .. self }
}
/// Set the `limit_behavior` for this middleware.
pub fn limit_behavior(self, behavior: LimitBehavior) -> Self {
Intercept { limit_behavior: behavior, .. self }
}
fn read_request(&self, req: &mut IronRequest) -> IronResult<Option<Entries>> {
let multipart = match Multipart::from_request(req) {
Ok(multipart) => multipart,
Err(_) => return Ok(None),
};
let tempdir = self.temp_dir_path.as_ref()
.map_or_else(
|| TempDir::new("multipart-iron"),
|path| TempDir::new_in(path, "multipart-iron")
)
.map_err(|e| io_to_iron(e, "Error opening temporary directory for request."))?;
match self.limit_behavior {
LimitBehavior::ThrowError => self.read_request_strict(multipart, tempdir),
LimitBehavior::Continue => self.read_request_lenient(multipart, tempdir),
}
}
fn read_request_strict(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult<Option<Entries>> {
match multipart.save().size_limit(self.file_size_limit)
.count_limit(self.file_count_limit)
.with_temp_dir(tempdir) {
Full(entries) => Ok(Some(entries)),
Partial(_, PartialReason::Utf8Error(_)) => unreachable!(),
Partial(_, PartialReason::IoError(err)) => Err(io_to_iron(err, "Error midway through request")),
Partial(_, PartialReason::CountLimit) => Err(FileCountLimitError(self.file_count_limit).into()),
Partial(partial, PartialReason::SizeLimit) => {
let partial = partial.partial.expect(EXPECT_PARTIAL_FILE);
Err(
FileSizeLimitError {
field: partial.source.headers,
}.into()
)
},
Error(err) => Err(io_to_iron(err, "Error at start of request")),
}
}
fn read_request_lenient(&self, mut multipart: IronMultipart, tempdir: TempDir) -> IronResult<Option<Entries>> {
let mut entries = match multipart.save().size_limit(self.file_size_limit)
.count_limit(self.file_count_limit)
.with_temp_dir(tempdir) {
Full(entries) => return Ok(Some(entries)),
Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")),
Partial(partial, _) => partial.keep_partial(),
Error(err) => return Err(io_to_iron(err, "Error at start of request")),
};
loop {
entries = match multipart.save().size_limit(self.file_size_limit)
.count_limit(self.file_count_limit)
.with_entries(entries) {
Full(entries) => return Ok(Some(entries)),
Partial(_, PartialReason::IoError(err)) => return Err(io_to_iron(err, "Error midway through request")),
Partial(partial, _) => partial.keep_partial(),
Error(err) => return Err(io_to_iron(err, "Error at start of request")),
};
}
}
}
type IronMultipart<'r, 'a, 'b> = Multipart<&'r mut IronBody<'a, 'b>>;
const EXPECT_PARTIAL_FILE: &str = "File size limit hit but the offending \
file was not available; this is a bug.";
impl Default for Intercept {
fn default() -> Self {
Intercept {
temp_dir_path: None,
file_size_limit: DEFAULT_FILE_SIZE_LIMIT,
file_count_limit: DEFAULT_FILE_COUNT_LIMIT,
limit_behavior: LimitBehavior::ThrowError,
}
}
}
impl BeforeMiddleware for Intercept {
fn before(&self, req: &mut IronRequest) -> IronResult<()> {
self.read_request(req)?
.map(|entries| req.extensions.insert::<Entries>(entries));
Ok(())
}
}
impl Key for Entries {
type Value = Self;
}
/// The behavior of `Intercept` when a file size or count limit is exceeded.
#[derive(Clone, Copy, Debug)]
#[repr(u32)]
pub enum LimitBehavior {
/// Return an error from the middleware describing the issue.
ThrowError,
/// Ignore the limit.
///
/// In the case of file size limits, the offending file will be truncated
/// in the result.
///
/// In the case of file count limits, the request will be completed.
Continue,
}
/// An error returned from `Intercept` when the size limit
/// for an individual file is exceeded.
#[derive(Debug)]
pub struct FileSizeLimitError {
/// The field where the error occurred.
pub field: FieldHeaders,
}
impl error::Error for FileSizeLimitError {
fn description(&self) -> &str {
"file size limit reached"
}
}
impl fmt::Display for FileSizeLimitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.field.filename {
Some(ref filename) => write!(f, "File size limit reached for field \"{}\" (filename: \"{}\")", self.field.name, filename),
None => write!(f, "File size limit reached for field \"{}\" (no filename)", self.field.name),
}
}
}
impl Into<IronError> for FileSizeLimitError {
fn into(self) -> IronError {
let desc_str = self.to_string();
IronError::new(self, desc_str)
}
}
/// An error returned from `Intercept` when the file count limit
/// for a single request was exceeded.
#[derive(Debug)]
pub struct FileCountLimitError(u32);
impl error::Error for FileCountLimitError {
fn description(&self) -> &str {
"file count limit reached"
}
}
impl fmt::Display for FileCountLimitError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "File count limit reached for request. Limit: {}", self.0)
}
}
impl Into<IronError> for FileCountLimitError {
fn into(self) -> IronError {
let desc_string = self.to_string();
IronError::new(self, desc_string)
}
}
fn io_to_iron<M: Into<String>>(err: io::Error, msg: M) -> IronError {
IronError::new(err, msg.into())
}