/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Partial libcurl bindings with some wrappers for safe cleanup. use crate::std::path::Path; use libloading::{Library, Symbol}; use once_cell::sync::Lazy; use std::ffi::{c_char, c_long, c_uint, CStr, CString}; // Constants lifted from `curl.h` const CURLE_OK: CurlCode = 0; const CURLE_OUT_OF_MEMORY: CurlCode = 27; const CURL_ERROR_SIZE: usize = 256; const CURLOPTTYPE_LONG: CurlOption = 0; const CURLOPTTYPE_OBJECTPOINT: CurlOption = 10000; const CURLOPTTYPE_FUNCTIONPOINT: CurlOption = 20000; const CURLOPTTYPE_STRINGPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT; const CURLOPTTYPE_CBPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT; const CURLOPTTYPE_SLISTPOINT: CurlOption = CURLOPTTYPE_OBJECTPOINT; const CURLOPT_WRITEDATA: CurlOption = CURLOPTTYPE_CBPOINT + 1; const CURLOPT_URL: CurlOption = CURLOPTTYPE_STRINGPOINT + 2; const CURLOPT_ERRORBUFFER: CurlOption = CURLOPTTYPE_OBJECTPOINT + 10; const CURLOPT_WRITEFUNCTION: CurlOption = CURLOPTTYPE_FUNCTIONPOINT + 11; const CURLOPT_USERAGENT: CurlOption = CURLOPTTYPE_STRINGPOINT + 18; const CURLOPT_MIMEPOST: CurlOption = CURLOPTTYPE_OBJECTPOINT + 269; const CURLOPT_MAXREDIRS: CurlOption = CURLOPTTYPE_LONG + 68; const CURLOPT_HTTPHEADER: CurlOption = CURLOPTTYPE_SLISTPOINT + 23; const CURLOPT_POSTFIELDS: CurlOption = CURLOPTTYPE_OBJECTPOINT + 15; const CURLOPT_POSTFIELDSIZE: CurlOption = CURLOPTTYPE_LONG + 60; const CURLINFO_LONG: CurlInfo = 0x200000; const CURLINFO_RESPONSE_CODE: CurlInfo = CURLINFO_LONG + 2; const CURL_LIB_NAMES: &[&str] = if cfg!(target_os = "linux") { &[ "libcurl.so", "libcurl.so.4", // Debian gives libcurl a different name when it is built against GnuTLS "libcurl-gnutls.so", "libcurl-gnutls.so.4", // Older versions in case we find nothing better "libcurl.so.3", "libcurl-gnutls.so.3", // See above for Debian ] } else if cfg!(target_os = "macos") { &[ "/usr/lib/libcurl.dylib", "/usr/lib/libcurl.4.dylib", "/usr/lib/libcurl.3.dylib", ] } else if cfg!(target_os = "windows") { &["libcurl.dll", "curl.dll"] } else { &[] }; #[repr(transparent)] #[derive(Clone, Copy)] struct CurlHandle(*mut ()); type CurlCode = c_uint; type CurlOption = c_uint; type CurlInfo = c_uint; #[repr(transparent)] #[derive(Clone, Copy)] struct CurlMime(*mut ()); #[repr(transparent)] #[derive(Clone, Copy)] struct CurlMimePart(*mut ()); #[repr(transparent)] #[derive(Clone, Copy)] struct CurlSlist(*mut ()); // # Safety // Curl handles are safe to pass among threads: https://curl.se/libcurl/c/threadsafe.html. unsafe impl Send for CurlHandle {} unsafe impl Send for CurlMime {} unsafe impl Send for CurlMimePart {} unsafe impl Send for CurlSlist {} macro_rules! library_binding { ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] fn $name:ident $args:tt $( -> $ret:ty )? ; $($rest:tt)* ) => { library_binding! { $localname members[ $($members)* $name: Symbol<'static, unsafe extern "C" fn $args $(->$ret)?>, ] load[ $($load)* $name: unsafe { let symbol = $localname.get::$ret)?>(stringify!($name).as_bytes()) .map_err(|e| std::io::Error::new(std::io::ErrorKind::NotFound, e))?; // All symbols refer to library, so `'static` lifetimes are safe (`library` // will outlive them). std::mem::transmute(symbol) }, ] $($rest)* } }; ( $localname:ident members[$($members:tt)*] load[$($load:tt)*] ) => { pub struct Curl { $($members)* _library: Library } impl Curl { fn load() -> std::io::Result { // Try each of the libraries, debug-logging load failures. let library = CURL_LIB_NAMES.iter().find_map(|name| { log::debug!("attempting to load {name}"); match unsafe { Library::new(name) } { Ok(lib) => { log::info!("loaded {name}"); Some(lib) } Err(e) => { log::debug!("error when loading {name}: {e}"); None } } }); let $localname = library.ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::NotFound, "failed to find curl library") })?; Ok(Curl { $($load)* _library: $localname }) } } }; ( $($rest:tt)* ) => { library_binding! { library members[] load[] $($rest)* } } } library_binding! { fn curl_easy_init() -> CurlHandle; fn curl_easy_setopt(CurlHandle, CurlOption, ...) -> CurlCode; fn curl_easy_perform(CurlHandle) -> CurlCode; fn curl_easy_getinfo(CurlHandle, CurlInfo, ...) -> CurlCode; fn curl_easy_cleanup(CurlHandle); fn curl_mime_init(CurlHandle) -> CurlMime; fn curl_mime_addpart(CurlMime) -> CurlMimePart; fn curl_mime_name(CurlMimePart, *const c_char) -> CurlCode; fn curl_mime_filename(CurlMimePart, *const c_char) -> CurlCode; fn curl_mime_type(CurlMimePart, *const c_char) -> CurlCode; fn curl_mime_data(CurlMimePart, *const c_char, usize) -> CurlCode; fn curl_mime_filedata(CurlMimePart, *const c_char) -> CurlCode; fn curl_mime_free(CurlMime); fn curl_slist_append(CurlSlist, *const c_char) -> CurlSlist; fn curl_slist_free_all(CurlSlist); } /// Load libcurl if possible. pub fn load() -> std::io::Result<&'static Curl> { static CURL: Lazy> = Lazy::new(Curl::load); CURL.as_ref().map_err(std::io::Error::other) } #[derive(Debug)] pub struct Error { code: CurlCode, error: Option, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "curl error code {}", self.code)?; if let Some(e) = &self.error { write!(f, ": {e}")?; } Ok(()) } } impl std::error::Error for Error {} impl From for std::io::Error { fn from(e: Error) -> Self { std::io::Error::other(e) } } pub type Result = std::result::Result; fn to_result(code: CurlCode) -> Result<()> { if code == CURLE_OK { Ok(()) } else { Err(Error { code, error: None }) } } impl Curl { pub fn easy(&self) -> std::io::Result { let handle = unsafe { (self.curl_easy_init)() }; if handle.0.is_null() { Err(std::io::Error::other("curl_easy_init failed")) } else { Ok(Easy { lib: self, handle, mime: None, headers: None, postdata: None, }) } } } struct ErrorBuffer([u8; CURL_ERROR_SIZE]); impl Default for ErrorBuffer { fn default() -> Self { ErrorBuffer([0; CURL_ERROR_SIZE]) } } pub struct Easy<'a> { lib: &'a Curl, handle: CurlHandle, mime: Option>, headers: Option>, postdata: Option>, } impl<'a> Easy<'a> { pub fn set_url(&mut self, url: &str) -> Result<()> { let url = CString::new(url).unwrap(); to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_URL, url.as_ptr()) }) } pub fn set_user_agent(&mut self, user_agent: &str) -> Result<()> { let ua = CString::new(user_agent).unwrap(); to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_USERAGENT, ua.as_ptr()) }) } pub fn mime(&self) -> std::io::Result> { let handle = unsafe { (self.lib.curl_mime_init)(self.handle) }; if handle.0.is_null() { Err(std::io::Error::other("curl_mime_init failed")) } else { Ok(Mime { lib: self.lib, handle, }) } } pub fn set_mime_post(&mut self, mime: Mime<'a>) -> Result<()> { to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MIMEPOST, mime.handle) })?; self.mime = Some(mime); Ok(()) } pub fn set_max_redirs(&mut self, redirs: c_long) -> Result<()> { to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_MAXREDIRS, redirs) }) } /// Create a new, empty string list. pub fn slist(&self) -> Slist<'a> { Slist { lib: self.lib, handle: CurlSlist(std::ptr::null_mut()), } } pub fn set_headers(&mut self, headers: Slist<'a>) -> Result<()> { to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_HTTPHEADER, headers.handle) })?; self.headers = Some(headers); Ok(()) } pub fn set_postfields(&mut self, data: impl Into>) -> std::io::Result<()> { let data = data.into(); let size: c_long = data.len().try_into().map_err(std::io::Error::other)?; to_result(unsafe { (self.lib.curl_easy_setopt)(self.handle, CURLOPT_POSTFIELDSIZE, size) })?; to_result(unsafe { (self.lib.curl_easy_setopt)( self.handle, CURLOPT_POSTFIELDS, data.as_ptr() as *const c_char, ) })?; self.postdata = Some(data); Ok(()) } /// Returns the response data on success. pub fn perform(&self) -> Result> { // Set error buffer, but degrade service if it doesn't work. let mut error_buffer = ErrorBuffer::default(); let error_buffer_set = unsafe { (self.lib.curl_easy_setopt)( self.handle, CURLOPT_ERRORBUFFER, error_buffer.0.as_mut_ptr() as *mut c_char, ) } == CURLE_OK; // Set the write function to fill a Vec. If there is a panic, this might leave stale // pointers in the curl options, but they won't be used without another perform, at which // point they'll be overwritten. let mut data: Vec = Vec::new(); extern "C" fn write_callback( data: *const u8, size: usize, nmemb: usize, dest: &mut Vec, ) -> usize { let total = size * nmemb; dest.extend(unsafe { std::slice::from_raw_parts(data, total) }); total } unsafe { to_result((self.lib.curl_easy_setopt)( self.handle, CURLOPT_WRITEFUNCTION, write_callback as extern "C" fn(*const u8, usize, usize, &mut Vec) -> usize, ))?; to_result((self.lib.curl_easy_setopt)( self.handle, CURLOPT_WRITEDATA, &mut data as *mut _, ))?; }; let mut result = to_result(unsafe { (self.lib.curl_easy_perform)(self.handle) }); // Clean up a bit by unsetting the write function and write data, though they won't be used // anywhere else. Ignore return values. unsafe { (self.lib.curl_easy_setopt)( self.handle, CURLOPT_WRITEFUNCTION, std::ptr::null_mut::<()>(), ); (self.lib.curl_easy_setopt)(self.handle, CURLOPT_WRITEDATA, std::ptr::null_mut::<()>()); } if error_buffer_set { unsafe { (self.lib.curl_easy_setopt)( self.handle, CURLOPT_ERRORBUFFER, std::ptr::null_mut::<()>(), ) }; if let Err(e) = &mut result { if let Ok(cstr) = CStr::from_bytes_until_nul(error_buffer.0.as_slice()) { e.error = Some(cstr.to_string_lossy().into_owned()); } } } result.map(move |()| data) } pub fn get_response_code(&self) -> Result { let mut code = c_long::default(); to_result(unsafe { (self.lib.curl_easy_getinfo)( self.handle, CURLINFO_RESPONSE_CODE, &mut code as *mut c_long, ) })?; Ok(code.try_into().expect("negative http response code")) } } impl Drop for Easy<'_> { fn drop(&mut self) { self.mime.take(); unsafe { (self.lib.curl_easy_cleanup)(self.handle) }; } } pub struct Mime<'a> { lib: &'a Curl, handle: CurlMime, } impl<'a> Mime<'a> { pub fn add_part(&mut self) -> std::io::Result> { let handle = unsafe { (self.lib.curl_mime_addpart)(self.handle) }; if handle.0.is_null() { Err(std::io::Error::other("curl_mime_addpart failed")) } else { Ok(MimePart { lib: self.lib, handle, }) } } } impl Drop for Mime<'_> { fn drop(&mut self) { unsafe { (self.lib.curl_mime_free)(self.handle) }; } } pub struct MimePart<'a> { lib: &'a Curl, handle: CurlMimePart, } impl MimePart<'_> { pub fn set_name(&mut self, name: &str) -> Result<()> { let name = CString::new(name).unwrap(); to_result(unsafe { (self.lib.curl_mime_name)(self.handle, name.as_ptr()) }) } pub fn set_filename(&mut self, filename: &str) -> Result<()> { let filename = CString::new(filename).unwrap(); to_result(unsafe { (self.lib.curl_mime_filename)(self.handle, filename.as_ptr()) }) } pub fn set_type(&mut self, mime_type: &str) -> Result<()> { let mime_type = CString::new(mime_type).unwrap(); to_result(unsafe { (self.lib.curl_mime_type)(self.handle, mime_type.as_ptr()) }) } pub fn set_filedata(&mut self, file: &Path) -> Result<()> { let file = CString::new(file.display().to_string()).unwrap(); to_result(unsafe { (self.lib.curl_mime_filedata)(self.handle, file.as_ptr()) }) } pub fn set_data(&mut self, data: &[u8]) -> Result<()> { to_result(unsafe { (self.lib.curl_mime_data)(self.handle, data.as_ptr() as *const c_char, data.len()) }) } } pub struct Slist<'a> { lib: &'a Curl, handle: CurlSlist, } impl Slist<'_> { pub fn append(&mut self, s: &str) -> Result<()> { let cs = CString::new(s).unwrap(); let new_handle = unsafe { (self.lib.curl_slist_append)(self.handle, cs.as_ptr()) }; if new_handle.0.is_null() { return Err(Error { // From source inspection, failure modes are all malloc errors, // which are more than likely only from OOM. code: CURLE_OUT_OF_MEMORY, error: Some(format!("failed to append {s} to slist")), }); } self.handle = new_handle; Ok(()) } } impl Drop for Slist<'_> { fn drop(&mut self) { unsafe { (self.lib.curl_slist_free_all)(self.handle) }; } }