diff --git a/Cargo.toml b/Cargo.toml index ca5fd4b96..ba52d59ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ zlib-ng-compat = ["curl-sys/zlib-ng-compat", "static-curl"] upkeep_7_62_0 = ["curl-sys/upkeep_7_62_0"] poll_7_68_0 = ["curl-sys/poll_7_68_0"] ntlm = ["curl-sys/ntlm"] +mime = ["curl-sys/mime"] [[test]] name = "atexit" diff --git a/ci/run.sh b/ci/run.sh index 3d9e931cb..598b78e48 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -29,6 +29,7 @@ if [ -z "$NO_RUN" ]; then cargo test --target $TARGET $features cargo test --target $TARGET --features static-curl $features cargo test --target $TARGET --features static-curl,protocol-ftp $features + cargo test --target $TARGET --features static-curl,mime $features cargo test --target $TARGET --features static-curl,http2 $features # Note that `-Clink-dead-code` is passed here to suppress `--gc-sections` to diff --git a/curl-sys/Cargo.toml b/curl-sys/Cargo.toml index 3286b01d1..344f751b6 100644 --- a/curl-sys/Cargo.toml +++ b/curl-sys/Cargo.toml @@ -55,3 +55,4 @@ zlib-ng-compat = ["libz-sys/zlib-ng", "static-curl"] upkeep_7_62_0 = [] poll_7_68_0 = [] ntlm = [] +mime = [] diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index f71d99aa5..e7a079ac6 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -1167,6 +1167,46 @@ extern "C" { ) -> CURLMcode; } +#[cfg(feature = "mime")] +mod mime { + use super::*; + + pub const CURLOPT_MIMEPOST: CURLoption = CURLOPTTYPE_OBJECTPOINT + 269; + + pub enum curl_mime {} + pub enum curl_mimepart {} + + extern "C" { + pub fn curl_mime_init(easy_handle: *mut CURL) -> *mut curl_mime; + pub fn curl_mime_free(mime_handle: *mut curl_mime); + pub fn curl_mime_addpart(mime_handle: *mut curl_mime) -> *mut curl_mimepart; + pub fn curl_mime_data( + part: *mut curl_mimepart, + data: *const c_char, + datasize: size_t, + ) -> CURLcode; + pub fn curl_mime_name(part: *mut curl_mimepart, name: *const c_char) -> CURLcode; + pub fn curl_mime_filename(part: *mut curl_mimepart, filename: *const c_char) -> CURLcode; + pub fn curl_mime_type(part: *mut curl_mimepart, mimetype: *const c_char) -> CURLcode; + pub fn curl_mime_data_cb( + part: *mut curl_mimepart, + datasize: curl_off_t, + readfunc: Option, + seekfunc: Option, + freefunc: Option, + arg: *mut c_void, + ) -> CURLcode; + pub fn curl_mime_subparts(part: *mut curl_mimepart, subparts: *mut curl_mime) -> CURLcode; + pub fn curl_mime_headers( + part: *mut curl_mimepart, + headers: *mut curl_slist, + take_ownership: c_int, + ) -> CURLcode; + } +} +#[cfg(feature = "mime")] +pub use mime::*; + pub fn rust_crate_version() -> &'static str { env!("CARGO_PKG_VERSION") } diff --git a/src/easy/handle.rs b/src/easy/handle.rs index 6d074be1f..fc71a3cb6 100644 --- a/src/easy/handle.rs +++ b/src/easy/handle.rs @@ -13,6 +13,8 @@ use crate::easy::handler::{Auth, NetRc, PostRedirections, ProxyType, SslOpt}; use crate::easy::handler::{HttpVersion, IpResolve, SslVersion, TimeCondition}; use crate::easy::{Easy2, Handler}; use crate::easy::{Form, List}; +#[cfg(feature = "mime")] +use crate::mime::Mime; use crate::Error; /// Raw bindings to a libcurl "easy session". @@ -1460,6 +1462,12 @@ impl Easy { self.inner.send(data) } + /// Same as [`Easy2::new_mime`](struct.Easy2.html#method.mime) + #[cfg(feature = "mime")] + pub fn new_mime(&mut self) -> Mime { + self.inner.new_mime() + } + /// Same as [`Easy2::raw`](struct.Easy2.html#method.raw) pub fn raw(&self) -> *mut curl_sys::CURL { self.inner.raw() diff --git a/src/easy/handler.rs b/src/easy/handler.rs index 66185cc55..9794783d2 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -16,6 +16,8 @@ use crate::easy::form; use crate::easy::list; use crate::easy::windows; use crate::easy::{Form, List}; +#[cfg(feature = "mime")] +use crate::mime::{Mime, MimeHandle}; use crate::panic; use crate::Error; @@ -385,6 +387,8 @@ struct Inner { resolve_list: Option, connect_to_list: Option, form: Option
, + #[cfg(feature = "mime")] + mime: Option, error_buf: RefCell>, handler: H, } @@ -595,6 +599,8 @@ impl Easy2 { resolve_list: None, connect_to_list: None, form: None, + #[cfg(feature = "mime")] + mime: None, error_buf: RefCell::new(vec![0; curl_sys::CURL_ERROR_SIZE]), handler, }), @@ -3366,6 +3372,45 @@ impl Easy2 { } } + /// Create a new MIME handle from this easy handle. + /// + /// With the returned [`Mime`] handle, you can add some parts and attach it to the + /// easy handle using [`Mime::post`]. + /// + /// # Examples + /// + /// ```no_run + /// use curl::easy::{Easy, List}; + /// + /// let mut handle = Easy::new(); + /// let mime = handle.new_mime(); + /// + /// let mut part = mime.add_part(); + /// part.name("key1").unwrap(); + /// part.data(b"value1").unwrap(); + /// let mut part = mime.add_part(); + /// part.name("key2").unwrap(); + /// part.data(b"value2").unwrap(); + /// + /// mime.post().unwrap(); + /// + /// handle.url("https://httpbin.dev/post").unwrap(); + /// handle.perform().unwrap(); + /// ``` + #[cfg(feature = "mime")] + pub fn new_mime(&mut self) -> Mime { + Mime::new(self) + } + + #[cfg(feature = "mime")] + pub(crate) fn set_mime(&mut self, mime: MimeHandle) -> Result<(), Error> { + let code = + unsafe { curl_sys::curl_easy_setopt(self.raw(), curl_sys::CURLOPT_MIMEPOST, mime.0) }; + self.cvt(code)?; + self.inner.mime = Some(mime); + Ok(()) + } + /// Get a pointer to the raw underlying CURL handle. pub fn raw(&self) -> *mut curl_sys::CURL { self.inner.handle @@ -3493,7 +3538,7 @@ impl Easy2 { Some(msg) } - fn cvt(&self, rc: curl_sys::CURLcode) -> Result<(), Error> { + pub(crate) fn cvt(&self, rc: curl_sys::CURLcode) -> Result<(), Error> { if rc == curl_sys::CURLE_OK { return Ok(()); } diff --git a/src/easy/mod.rs b/src/easy/mod.rs index 0b0e23a7e..4feb4d48b 100644 --- a/src/easy/mod.rs +++ b/src/easy/mod.rs @@ -10,7 +10,7 @@ mod form; mod handle; mod handler; -mod list; +pub(crate) mod list; mod windows; pub use self::form::{Form, Part}; diff --git a/src/lib.rs b/src/lib.rs index 2965e2bed..3bb143451 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,6 +67,8 @@ pub use crate::version::{Protocols, Version}; mod version; pub mod easy; +#[cfg(feature = "mime")] +pub mod mime; pub mod multi; mod panic; diff --git a/src/mime.rs b/src/mime.rs new file mode 100644 index 000000000..f788f74e0 --- /dev/null +++ b/src/mime.rs @@ -0,0 +1,187 @@ +//! MIME handling in libcurl. + +use std::{ + ffi::{c_void, CString}, + io::SeekFrom, + slice, +}; + +use crate::{ + easy::{list::raw as list_raw, Easy2, List, ReadError}, + panic, Error, +}; + +mod handler; + +pub use handler::PartDataHandler; +use libc::{c_char, c_int, size_t}; + +#[derive(Debug)] +pub(crate) struct MimeHandle(pub(crate) *mut curl_sys::curl_mime); + +/// A MIME handle that holds MIME parts. +#[must_use = "Mime is not attached to the Easy handle until you call `post()` on it"] +#[derive(Debug)] +pub struct Mime<'h, H> { + handle: MimeHandle, + easy: &'h mut Easy2, +} + +/// A MIME part associated with a MIME handle. +#[derive(Debug)] +pub struct MimePart<'m, 'h, H> { + raw: *mut curl_sys::curl_mimepart, + mime: &'m Mime<'h, H>, +} + +unsafe impl Send for Mime<'_, H> {} + +impl<'h, H> Mime<'h, H> { + pub(crate) fn new(easy: &'h mut Easy2) -> Self { + let raw = unsafe { curl_sys::curl_mime_init(easy.raw()) }; + assert!(!raw.is_null()); + Self { + handle: MimeHandle(raw), + easy, + } + } + + /// Creates a new MIME part associated to this MIME handle. + pub fn add_part<'m>(&'m self) -> MimePart<'m, 'h, H> { + MimePart::new(self) + } + + /// Returns the raw MIME handle pointer. + pub fn raw(&self) -> *mut curl_sys::curl_mime { + self.handle.0 + } + + /// Pass the MIME handle to the originating Easy handle to post an HTTP form. + /// + /// This option corresponds to `CURLOPT_MIMEPOST`. + pub fn post(self) -> Result<(), Error> { + self.easy.set_mime(self.handle)?; + Ok(()) + } +} + +impl<'m, 'h, H> MimePart<'m, 'h, H> { + fn new(mime: &'m Mime<'h, H>) -> Self { + let raw = unsafe { curl_sys::curl_mime_addpart(mime.handle.0) }; + assert!(!raw.is_null()); + Self { raw, mime } + } + + /// Returns the raw MIME part pointer. + pub fn raw(&self) -> *mut curl_sys::curl_mimepart { + self.raw + } + + /// Sets the data of the content of this MIME part. + pub fn data(&mut self, data: &[u8]) -> Result<(), Error> { + let code = + unsafe { curl_sys::curl_mime_data(self.raw, data.as_ptr() as *const _, data.len()) }; + self.mime.easy.cvt(code)?; + Ok(()) + } + + /// Sets the name of this MIME part. + pub fn name(&mut self, name: &str) -> Result<(), Error> { + let name = CString::new(name)?; + let code = unsafe { curl_sys::curl_mime_name(self.raw, name.as_ptr()) }; + self.mime.easy.cvt(code)?; + Ok(()) + } + + /// Sets the filename of this MIME part. + pub fn filename(&mut self, filename: &str) -> Result<(), Error> { + let filename = CString::new(filename)?; + let code = unsafe { curl_sys::curl_mime_filename(self.raw, filename.as_ptr()) }; + self.mime.easy.cvt(code)?; + Ok(()) + } + + /// Sets the content type of this MIME part. + pub fn content_type(&mut self, content_type: &str) -> Result<(), Error> { + let content_type = CString::new(content_type)?; + let code = unsafe { curl_sys::curl_mime_type(self.raw, content_type.as_ptr()) }; + self.mime.easy.cvt(code)?; + Ok(()) + } + + /// Sets the list of headers of this MIME part. + pub fn headers(&mut self, header_list: List) -> Result<(), Error> { + let header_list = std::mem::ManuallyDrop::new(header_list); + let code = unsafe { curl_sys::curl_mime_headers(self.raw, list_raw(&header_list), 1) }; + self.mime.easy.cvt(code)?; + Ok(()) + } + + /// Sets the handler that provides content data for this MIME part. + pub fn data_handler( + &mut self, + size: usize, + handler: P, + ) -> Result<(), Error> { + let mut inner = Box::new(handler); + let code = unsafe { + curl_sys::curl_mime_data_cb( + self.raw, + size as curl_sys::curl_off_t, + Some(read_cb::

), + Some(seek_cb::

), + Some(free_handler::

), + &mut *inner as *mut _ as *mut c_void, + ) + }; + self.mime.easy.cvt(code)?; + Box::leak(inner); + Ok(()) + } +} + +impl Drop for MimeHandle { + fn drop(&mut self) { + unsafe { curl_sys::curl_mime_free(self.0) } + } +} + +extern "C" fn read_cb( + ptr: *mut c_char, + size: size_t, + nmemb: size_t, + data: *mut c_void, +) -> size_t { + panic::catch(|| unsafe { + let input = slice::from_raw_parts_mut(ptr as *mut u8, size * nmemb); + match (*(data as *mut P)).read(input) { + Ok(s) => s, + Err(ReadError::Pause) => curl_sys::CURL_READFUNC_PAUSE, + Err(ReadError::Abort) => curl_sys::CURL_READFUNC_ABORT, + } + }) + .unwrap_or(!0) +} + +extern "C" fn seek_cb( + data: *mut c_void, + offset: curl_sys::curl_off_t, + origin: c_int, +) -> c_int { + panic::catch(|| unsafe { + let from = if origin == libc::SEEK_SET { + SeekFrom::Start(offset as u64) + } else { + panic!("unknown origin from libcurl: {}", origin); + }; + (*(data as *mut P)).seek(from) as c_int + }) + .unwrap_or(!0) +} + +extern "C" fn free_handler(data: *mut c_void) { + panic::catch(|| unsafe { + let _ = Box::from_raw(data as *mut P); + }) + .unwrap_or(()); +} diff --git a/src/mime/handler.rs b/src/mime/handler.rs new file mode 100644 index 000000000..a1b5a3657 --- /dev/null +++ b/src/mime/handler.rs @@ -0,0 +1,49 @@ +use std::io::SeekFrom; + +use crate::easy::{ReadError, SeekResult}; + +/// A trait to provide data for a MIME part. +pub trait PartDataHandler { + /// Read callback for data uploads. + /// + /// This callback function gets called by libcurl as soon as it needs to + /// read data in order to send it to the peer. + /// + /// Your function must then return the actual number of bytes that it stored + /// in that memory area. Returning 0 will signal end-of-file to the library + /// and cause it to stop the current transfer. + /// + /// If you stop the current transfer by returning 0 "pre-maturely" (i.e + /// before the server expected it, like when you've said you will upload N + /// bytes and you upload less than N bytes), you may experience that the + /// server "hangs" waiting for the rest of the data that won't come. + /// + /// The read callback may return `Err(ReadError::Abort)` to stop the + /// current operation immediately, resulting in a `is_aborted_by_callback` + /// error code from the transfer. + /// + /// The callback can return `Err(ReadError::Pause)` to cause reading from + /// this connection to pause. See `unpause_read` for further details. + fn read(&mut self, data: &mut [u8]) -> Result; + + /// User callback for seeking in input stream. + /// + /// This function gets called by libcurl to seek to a certain position in + /// the input stream and can be used to fast forward a file in a resumed + /// upload (instead of reading all uploaded bytes with the normal read + /// function/callback). It is also called to rewind a stream when data has + /// already been sent to the server and needs to be sent again. This may + /// happen when doing a HTTP PUT or POST with a multi-pass authentication + /// method, or when an existing HTTP connection is reused too late and the + /// server closes the connection. + /// + /// The callback function must return `SeekResult::Ok` on success, + /// `SeekResult::Fail` to cause the upload operation to fail or + /// `SeekResult::CantSeek` to indicate that while the seek failed, libcurl + /// is free to work around the problem if possible. The latter can sometimes + /// be done by instead reading from the input or similar. + fn seek(&mut self, whence: SeekFrom) -> SeekResult { + let _ = whence; // ignore unused + SeekResult::CantSeek + } +} diff --git a/tests/mime.rs b/tests/mime.rs new file mode 100644 index 000000000..1f0fda221 --- /dev/null +++ b/tests/mime.rs @@ -0,0 +1,184 @@ +#![cfg(feature = "mime")] + +use curl::{ + easy::{ReadError, SeekResult}, + mime::PartDataHandler, + Version, +}; +use std::{io::SeekFrom, time::Duration}; + +macro_rules! t { + ($e:expr) => { + match $e { + Ok(e) => e, + Err(e) => panic!("{} failed with {:?}", stringify!($e), e), + } + }; +} + +use curl::easy::{Easy, List}; + +use crate::server::Server; +mod server; + +fn handle() -> Easy { + let mut e = Easy::new(); + t!(e.timeout(Duration::new(20, 0))); + let mut list = List::new(); + t!(list.append("Expect:")); + t!(e.http_headers(list)); + e +} + +fn multipart_boundary_size() -> usize { + // Versions before 8.4.0 used a smaller multipart mime boundary, so the + // exact content-length will differ between versions. + if Version::get().version_num() >= 0x80400 { + 148 + } else { + 136 + } +} + +#[test] +fn data() { + let s = Server::new(); + s.receive(&format!( + "\ + POST / HTTP/1.1\r\n\ + Host: 127.0.0.1:$PORT\r\n\ + Accept: */*\r\n\ + Content-Length: {}\r\n\ + Content-Type: multipart/form-data; boundary=--[..]\r\n\ + \r\n\ + --[..]\r\n\ + Content-Disposition: form-data; name=\"foo\"; filename=\"data.txt\"\r\n\ + Content-Type: text/plain\r\n\ + Content-Language: en-US\r\n\ + \r\n\ + 1234\r\n\ + --[..]\r\n", + multipart_boundary_size() + 78 + )); + s.send("HTTP/1.1 200 OK\r\n\r\n"); + + let mut handle = handle(); + let mime = handle.new_mime(); + let mut part = mime.add_part(); + t!(part.name("foo")); + t!(part.data(b"1234")); + t!(part.content_type("text/plain")); + let mut part_headers = List::new(); + part_headers.append("Content-Language: en-US").unwrap(); + t!(part.headers(part_headers)); + t!(part.filename("data.txt")); + t!(mime.post()); + t!(handle.url(&s.url("/"))); + t!(handle.perform()); +} + +#[test] +fn two_parts() { + let s = Server::new(); + s.receive(&format!( + "\ + POST / HTTP/1.1\r\n\ + Host: 127.0.0.1:$PORT\r\n\ + Accept: */*\r\n\ + Content-Length: {}\r\n\ + Content-Type: multipart/form-data; boundary=--[..]\r\n\ + \r\n\ + --[..]\r\n\ + Content-Disposition: form-data; name=\"foo\"\r\n\ + \r\n\ + 1234\r\n\ + --[..]\r\n\ + Content-Disposition: form-data; name=\"bar\"\r\n\ + \r\n\ + 5678\r\n\ + --[..]\r\n", + multipart_boundary_size() + 108 + )); + s.send("HTTP/1.1 200 OK\r\n\r\n"); + + let mut handle = handle(); + let mime = handle.new_mime(); + let mut part = mime.add_part(); + t!(part.name("foo")); + t!(part.data(b"1234")); + part = mime.add_part(); + t!(part.name("bar")); + t!(part.data(b"5678")); + t!(mime.post()); + t!(handle.url(&s.url("/"))); + t!(handle.perform()); +} + +#[test] +fn handler() { + let s = Server::new(); + s.receive(&format!( + "\ + POST / HTTP/1.1\r\n\ + Host: 127.0.0.1:$PORT\r\n\ + Accept: */*\r\n\ + Content-Length: {}\r\n\ + Content-Type: multipart/form-data; boundary=--[..]\r\n\ + \r\n\ + --[..]\r\n\ + Content-Disposition: form-data; name=\"foo\"\r\n\ + \r\n\ + 1234\r\n\ + --[..]\r\n", + multipart_boundary_size() + 6 + )); + s.send("HTTP/1.1 200 OK\r\n\r\n"); + + let mut handle = handle(); + let mime = handle.new_mime(); + let mut part = mime.add_part(); + t!(part.name("foo")); + let buf = b"1234".to_vec(); + t!(part.data_handler(buf.len(), ByteVecHandler::new(buf))); + t!(mime.post()); + t!(handle.url(&s.url("/"))); + t!(handle.perform()); +} + +#[derive(Debug)] +struct ByteVecHandler { + data: Vec, + pos: usize, +} + +impl ByteVecHandler { + fn new(data: Vec) -> Self { + Self { data, pos: 0 } + } +} + +impl PartDataHandler for ByteVecHandler { + fn read(&mut self, data: &mut [u8]) -> Result { + if self.pos > self.data.len() { + return Ok(0); + } + let remaining_data = &mut self.data[self.pos..]; + let len = remaining_data.len().min(data.len()); + data[..len].copy_from_slice(&remaining_data[..len]); + self.pos += len; + Ok(len) + } + + fn seek(&mut self, whence: SeekFrom) -> SeekResult { + match whence { + SeekFrom::Start(pos) => { + if pos > self.data.len() as u64 { + return SeekResult::Fail; + } + self.pos = pos as usize; + } + SeekFrom::End(_) | SeekFrom::Current(_) => return SeekResult::CantSeek, + } + SeekResult::Ok + } +}