diff --git a/.github/workflows/websocket.yml b/.github/workflows/websocket.yml index 644b9f17e..d451b928e 100644 --- a/.github/workflows/websocket.yml +++ b/.github/workflows/websocket.yml @@ -2,9 +2,9 @@ name: WebSocket on: push: - branches: [main, moli] + branches: [main, moli, moli-apple-sectrust] pull_request: - branches: [main, moli] + branches: [main, moli, moli-apple-sectrust] permissions: contents: read diff --git a/curl-sys/lib.rs b/curl-sys/lib.rs index bb89e926e..a9c9bbc00 100644 --- a/curl-sys/lib.rs +++ b/curl-sys/lib.rs @@ -199,6 +199,11 @@ pub const CURLINFO_SSL_DATA_OUT: curl_infotype = 6; pub type curl_debug_callback = extern "C" fn(*mut CURL, curl_infotype, *mut c_char, size_t, *mut c_void) -> c_int; +pub const CURL_PREREQFUNC_OK: c_int = 0; +pub const CURL_PREREQFUNC_ABORT: c_int = 1; +pub type curl_prereq_callback = + extern "C" fn(*mut c_void, *mut c_char, *mut c_char, c_int, c_int) -> c_int; + pub const CURLE_OK: CURLcode = 0; pub const CURLE_UNSUPPORTED_PROTOCOL: CURLcode = 1; pub const CURLE_FAILED_INIT: CURLcode = 2; @@ -638,6 +643,9 @@ pub const CURLOPT_DOH_SSL_VERIFYSTATUS: CURLoption = CURLOPTTYPE_LONG + 308; pub const CURLOPT_CAINFO_BLOB: CURLoption = CURLOPTTYPE_BLOB + 309; pub const CURLOPT_PROXY_CAINFO_BLOB: CURLoption = CURLOPTTYPE_BLOB + 310; +pub const CURLOPT_PREREQFUNCTION: CURLoption = CURLOPTTYPE_FUNCTIONPOINT + 312; +pub const CURLOPT_PREREQDATA: CURLoption = CURLOPTTYPE_OBJECTPOINT + 313; + pub const CURLOPT_WS_OPTIONS: CURLoption = CURLOPTTYPE_LONG + 320; #[repr(C)] diff --git a/src/easy/handle.rs b/src/easy/handle.rs index e29ede888..ab9369657 100644 --- a/src/easy/handle.rs +++ b/src/easy/handle.rs @@ -110,6 +110,7 @@ struct Callbacks<'a> { read: Option Result + 'a>>, seek: Option SeekResult + 'a>>, debug: Option>, + prereq: Option bool + 'a>>, header: Option bool + 'a>>, progress: Option bool + 'a>>, ssl_ctx: Option Result<(), Error> + 'a>>, @@ -151,6 +152,11 @@ impl Easy { self.inner.progress(progress) } + /// Same as [`Easy2::prereq`](struct.Easy2.html#method.prereq). + pub fn prereq(&mut self, enable: bool) -> Result<(), Error> { + self.inner.prereq(enable) + } + /// Same as [`Easy2::signal`](struct.Easy2.html#method.signal) pub fn signal(&mut self, signal: bool) -> Result<(), Error> { self.inner.signal(signal) @@ -401,6 +407,29 @@ impl Easy { Ok(()) } + /// Called after connection setup and before sending a request. + /// + /// The arguments are the remote and local IP addresses and their + /// respective ports. Return `true` to continue or `false` to abort with + /// `is_aborted_by_callback`. This runs for new and reused connections, + /// including after TLS setup, and may run again for followed redirects. + /// + /// Setting this callback enables [`Easy::prereq`] without requiring + /// verbose output. [`Easy::reset`] disables it while retaining the closure; + /// [`Easy::prereq`] can enable it again. Use [`Transfer::prereq_function`] + /// to borrow stack-local data instead. + /// + /// This corresponds to `CURLOPT_PREREQFUNCTION` and `CURLOPT_PREREQDATA`, + /// available since libcurl 7.80.0. + pub fn prereq_function(&mut self, f: F) -> Result<(), Error> + where + F: FnMut(&str, &str, u16, u16) -> bool + Send + 'static, + { + self.inner.prereq(true)?; + self.inner.get_mut().owned.prereq = Some(Box::new(f)); + Ok(()) + } + /// Callback to SSL context /// /// This callback function gets called by libcurl just before the @@ -1623,6 +1652,21 @@ impl Handler for EasyData { } } + fn prereq( + &mut self, + primary_ip: &str, + local_ip: &str, + primary_port: u16, + local_port: u16, + ) -> bool { + unsafe { + match self.callback(|s| &mut s.prereq) { + Some(prereq) => prereq(primary_ip, local_ip, primary_port, local_port), + None => true, + } + } + } + fn ssl_ctx(&mut self, cx: *mut c_void) -> Result<(), Error> { unsafe { match self.callback(|s| &mut s.ssl_ctx) { @@ -1680,6 +1724,17 @@ impl<'easy, 'data> Transfer<'easy, 'data> { Ok(()) } + /// Same as `Easy::prereq_function`, just takes a non `'static` lifetime + /// corresponding to the lifetime of this transfer. + pub fn prereq_function(&mut self, f: F) -> Result<(), Error> + where + F: FnMut(&str, &str, u16, u16) -> bool + 'data, + { + self.easy.inner.prereq(true)?; + self.data.prereq = Some(Box::new(f)); + Ok(()) + } + /// Same as `Easy::ssl_ctx_function`, just takes a non `'static` /// lifetime corresponding to the lifetime of this transfer. pub fn ssl_ctx_function(&mut self, f: F) -> Result<(), Error> diff --git a/src/easy/handler.rs b/src/easy/handler.rs index 299b4263b..82e203e7e 100644 --- a/src/easy/handler.rs +++ b/src/easy/handler.rs @@ -153,6 +153,30 @@ pub trait Handler { debug(kind, data) } + /// Called after connection setup and before sending a request. + /// + /// This runs for both new and reused connections, after any TLS handshake. + /// Followed redirects can invoke it more than once per transfer. The + /// arguments are the remote and local IP addresses (without IPv6 brackets) + /// and their respective ports. The address strings borrow libcurl's data. + /// + /// Return `true` to continue or `false` to abort with + /// [`Error::is_aborted_by_callback`](crate::Error::is_aborted_by_callback). + /// Enable this callback with [`Easy2::prereq`]; verbose is not required. + /// + /// This corresponds to `CURLOPT_PREREQFUNCTION` and `CURLOPT_PREREQDATA`, + /// available since libcurl 7.80.0. + fn prereq( + &mut self, + primary_ip: &str, + local_ip: &str, + primary_port: u16, + local_port: u16, + ) -> bool { + let _ = (primary_ip, local_ip, primary_port, local_port); + true + } + /// Callback that receives header data /// /// This function gets called by libcurl as soon as it has received header @@ -620,6 +644,23 @@ impl Easy2 { self.default_configure(); } + /// Enables or disables the [`Handler::prereq`] callback. + /// + /// The callback is disabled by default and by [`Easy2::reset`]. Enabling + /// it does not require verbose output or allocate callback storage. + /// + /// This corresponds to `CURLOPT_PREREQFUNCTION` and `CURLOPT_PREREQDATA`, + /// available since libcurl 7.80.0. Older versions return an option error. + pub fn prereq(&mut self, enable: bool) -> Result<(), Error> { + if !enable { + return self.setopt_ptr(curl_sys::CURLOPT_PREREQFUNCTION, ptr::null()); + } + let cb: curl_sys::curl_prereq_callback = prereq_cb::; + let data = &*self.inner as *const _ as *const _; + self.setopt_ptr(curl_sys::CURLOPT_PREREQDATA, data)?; + self.setopt_ptr(curl_sys::CURLOPT_PREREQFUNCTION, cb as *const _) + } + fn default_configure(&mut self) { self.setopt_ptr( curl_sys::CURLOPT_ERRORBUFFER, @@ -3796,6 +3837,33 @@ extern "C" fn progress_cb( } } +extern "C" fn prereq_cb( + data: *mut c_void, + primary_ip: *mut c_char, + local_ip: *mut c_char, + primary_port: c_int, + local_port: c_int, +) -> c_int { + let keep_going = panic::catch(|| unsafe { + // libcurl supplies NUL-terminated numeric IP addresses and socket + // ports. As with the other callbacks, data points into Easy2's boxed + // Inner, which remains alive and stationary until curl_easy_cleanup. + let primary_ip = CStr::from_ptr(primary_ip).to_str().unwrap(); + let local_ip = CStr::from_ptr(local_ip).to_str().unwrap(); + let primary_port = u16::try_from(primary_port).unwrap(); + let local_port = u16::try_from(local_port).unwrap(); + (*(data as *mut Inner)) + .handler + .prereq(primary_ip, local_ip, primary_port, local_port) + }) + .unwrap_or(false); + if keep_going { + curl_sys::CURL_PREREQFUNC_OK + } else { + curl_sys::CURL_PREREQFUNC_ABORT + } +} + // TODO: expose `handle`? is that safe? extern "C" fn debug_cb( _handle: *mut curl_sys::CURL, diff --git a/src/multi.rs b/src/multi.rs index a362fe30f..96ddfe388 100644 --- a/src/multi.rs +++ b/src/multi.rs @@ -517,12 +517,14 @@ impl Multi { pub fn action(&self, socket: Socket, events: &Events) -> Result { let mut remaining = 0; unsafe { - cvt(curl_sys::curl_multi_socket_action( + let result = curl_sys::curl_multi_socket_action( self.raw.handle, socket, events.bits, &mut remaining, - ))?; + ); + panic::propagate(); + cvt(result)?; Ok(remaining as u32) } } @@ -545,12 +547,14 @@ impl Multi { pub fn timeout(&self) -> Result { let mut remaining = 0; unsafe { - cvt(curl_sys::curl_multi_socket_action( + let result = curl_sys::curl_multi_socket_action( self.raw.handle, curl_sys::CURL_SOCKET_BAD, 0, &mut remaining, - ))?; + ); + panic::propagate(); + cvt(result)?; Ok(remaining as u32) } } @@ -735,7 +739,12 @@ impl Multi { pub fn perform(&self) -> Result { unsafe { let mut ret = 0; - cvt(curl_sys::curl_multi_perform(self.raw.handle, &mut ret))?; + let result = curl_sys::curl_multi_perform(self.raw.handle, &mut ret); + // Re-throw only after libcurl has returned, just as Easy2::perform + // does. Leaving the stored panic behind suppresses later callbacks + // on this thread, even for otherwise unrelated transfers. + panic::propagate(); + cvt(result)?; Ok(ret as u32) } } diff --git a/systest/build.rs b/systest/build.rs index b3a3e1a00..c959d276c 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -54,7 +54,7 @@ fn main() { s => s.to_string(), }); // cfg.fn_cname(|s, l| l.unwrap_or(s).to_string()); - cfg.skip_type(|n| n == "__enum_ty"); + cfg.skip_type(move |n| n == "__enum_ty" || (version < (7, 80) && n == "curl_prereq_callback")); cfg.skip_signededness(|s| s.ends_with("callback") || s.ends_with("function")); cfg.skip_struct(move |s| { @@ -79,6 +79,17 @@ fn main() { // Version symbols are extracted from https://curl.se/libcurl/c/symbols-in-versions.html cfg.skip_const(move |s| { + if version < (7, 80) + && matches!( + s, + "CURLOPT_PREREQFUNCTION" + | "CURLOPT_PREREQDATA" + | "CURL_PREREQFUNC_OK" + | "CURL_PREREQFUNC_ABORT" + ) + { + return true; + } if version < (7, 86) && (s.starts_with("CURLWS_") || s == "CURLOPT_WS_OPTIONS") { return true; } diff --git a/tests/multi.rs b/tests/multi.rs index 62711d68a..44178e153 100644 --- a/tests/multi.rs +++ b/tests/multi.rs @@ -19,6 +19,56 @@ macro_rules! t { use crate::server::Server; mod server; +#[test] +fn callback_panic_is_rethrown_and_does_not_poison_later_transfers() { + use curl::easy::{Easy2, Handler}; + use std::panic::{catch_unwind, AssertUnwindSafe}; + + struct PanicOnce(bool); + impl Handler for PanicOnce { + fn header(&mut self, _: &[u8]) -> bool { + if std::mem::replace(&mut self.0, false) { + panic!("header callback panic"); + } + true + } + } + + let response_server = || { + let server = Server::new(); + server.receive("GET / HTTP/1.1\r\nHost: 127.0.0.1:$PORT\r\nAccept: */*\r\n\r\n"); + server.send("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"); + server + }; + let first = response_server(); + let multi = Multi::new(); + let mut easy = Easy2::new(PanicOnce(true)); + t!(easy.url(&first.url("/"))); + t!(easy.proxy("")); + t!(easy.timeout(Duration::from_secs(10))); + let handle = t!(multi.add2(easy)); + let panic = catch_unwind(AssertUnwindSafe(|| { + while t!(multi.perform()) > 0 { + t!(multi.wait(&mut [], Duration::from_secs(1))); + } + })) + .unwrap_err(); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"header callback panic")); + let mut easy = t!(multi.remove2(handle)); + let second = response_server(); + t!(easy.url(&second.url("/"))); + let handle = t!(multi.add2(easy)); + while t!(multi.perform()) > 0 { + t!(multi.wait(&mut [], Duration::from_secs(1))); + } + let mut completed = false; + multi.messages(|message| { + t!(message.result_for2(&handle).unwrap()); + completed = true; + }); + assert!(completed); +} + #[test] fn smoke() { let m = Multi::new(); diff --git a/tests/prereq.rs b/tests/prereq.rs new file mode 100644 index 000000000..f24e55ef9 --- /dev/null +++ b/tests/prereq.rs @@ -0,0 +1,297 @@ +use std::io::{Read, Write}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use curl::easy::{Easy, Easy2, Handler, InfoType}; +use curl::multi::Multi; +use curl::Version; + +const TIMEOUT: Duration = Duration::from_secs(10); +const EMPTY: &[u8] = b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"; + +fn supported() -> bool { + Version::get().version_num() >= 0x075000 +} + +fn read_head(stream: &mut TcpStream) -> Vec { + stream.set_read_timeout(Some(TIMEOUT)).unwrap(); + let mut head = Vec::new(); + let mut byte = [0]; + while !head.ends_with(b"\r\n\r\n") { + if stream.read(&mut byte).unwrap() == 0 { + break; + } + head.push(byte[0]); + assert!(head.len() < 64 * 1024); + } + head +} + +fn keepalive_server(responses: Vec<&'static [u8]>) -> (SocketAddr, JoinHandle>>) { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream.set_write_timeout(Some(TIMEOUT)).unwrap(); + responses + .into_iter() + .map(|response| { + let head = read_head(&mut stream); + assert!(head.ends_with(b"\r\n\r\n")); + stream.write_all(response).unwrap(); + head + }) + .collect() + }); + (address, server) +} + +fn configure(easy: &mut Easy2, address: SocketAddr) { + easy.url(&format!("http://{}/", address)).unwrap(); + easy.proxy("").unwrap(); + easy.timeout(TIMEOUT).unwrap(); +} + +struct Probe { + port: u16, + calls: usize, + debug_calls: usize, + allow: bool, + panic_once: bool, +} + +impl Probe { + fn new(port: u16) -> Self { + Self { + port, + calls: 0, + debug_calls: 0, + allow: true, + panic_once: false, + } + } +} + +impl Handler for Probe { + fn prereq(&mut self, remote: &str, local: &str, remote_port: u16, local_port: u16) -> bool { + assert_eq!(remote, "127.0.0.1"); + assert_eq!(local, "127.0.0.1"); + assert_eq!(remote_port, self.port); + assert_ne!(local_port, 0); + self.calls += 1; + if self.panic_once { + self.panic_once = false; + panic!("prereq panic"); + } + self.allow + } + + fn debug(&mut self, _: InfoType, _: &[u8]) { + self.debug_calls += 1; + } +} + +#[test] +fn handler_prereq_is_opt_in_across_moves_reuse_and_reset() { + if !supported() { + return; + } + let (address, server) = keepalive_server(vec![EMPTY; 6]); + let mut easy = Easy2::new(Probe::new(address.port())); + configure(&mut easy, address); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 0); + + easy.prereq(true).unwrap(); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 1); + easy = thread::spawn(move || { + easy.perform().unwrap(); + easy + }) + .join() + .unwrap(); + assert_eq!(easy.get_ref().calls, 2); + + easy.reset(); + configure(&mut easy, address); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 2, "reset disables the callback"); + easy.prereq(true).unwrap(); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 3); + easy.prereq(false).unwrap(); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 3); + assert_eq!(easy.get_ref().debug_calls, 0); + assert_eq!(easy.num_connects().unwrap(), 0, "reuse the same socket"); + assert_eq!(server.join().unwrap().len(), 6); +} + +#[test] +fn handler_prereq_runs_for_each_followed_redirect() { + if !supported() { + return; + } + let (address, server) = keepalive_server(vec![ + b"HTTP/1.1 302 Found\r\nLocation: /final\r\nContent-Length: 0\r\n\r\n", + EMPTY, + ]); + let mut easy = Easy2::new(Probe::new(address.port())); + configure(&mut easy, address); + easy.prereq(true).unwrap(); + easy.follow_location(true).unwrap(); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 2); + let requests = server.join().unwrap(); + assert!(requests[0].starts_with(b"GET / HTTP/1.1\r\n")); + assert!(requests[1].starts_with(b"GET /final HTTP/1.1\r\n")); +} + +#[test] +fn handler_prereq_can_abort_before_sending_headers() { + if !supported() { + return; + } + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || read_head(&mut listener.accept().unwrap().0)); + let mut probe = Probe::new(address.port()); + probe.allow = false; + let mut easy = Easy2::new(probe); + configure(&mut easy, address); + easy.prereq(true).unwrap(); + assert!(easy.perform().unwrap_err().is_aborted_by_callback()); + assert_eq!(easy.get_ref().calls, 1); + drop(easy); + assert!(server.join().unwrap().is_empty()); +} + +#[test] +fn handler_prereq_panic_is_rethrown_and_does_not_poison_reuse() { + if !supported() { + return; + } + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || { + assert!(read_head(&mut listener.accept().unwrap().0).is_empty()); + let (mut stream, _) = listener.accept().unwrap(); + assert!(read_head(&mut stream).starts_with(b"GET / ")); + stream.write_all(EMPTY).unwrap(); + }); + let mut probe = Probe::new(address.port()); + probe.panic_once = true; + let mut easy = Easy2::new(probe); + configure(&mut easy, address); + easy.prereq(true).unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| easy.perform())).unwrap_err(); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"prereq panic")); + easy.perform().unwrap(); + assert_eq!(easy.get_ref().calls, 2); + server.join().unwrap(); +} + +#[test] +fn handler_prereq_panic_is_rethrown_from_multi() { + if !supported() { + return; + } + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = thread::spawn(move || read_head(&mut listener.accept().unwrap().0)); + let mut probe = Probe::new(address.port()); + probe.panic_once = true; + let mut easy = Easy2::new(probe); + configure(&mut easy, address); + easy.prereq(true).unwrap(); + let multi = Multi::new(); + let handle = multi.add2(easy).unwrap(); + let panic = catch_unwind(AssertUnwindSafe(|| { + while multi.perform().unwrap() != 0 { + multi.wait(&mut [], Duration::from_millis(100)).unwrap(); + } + })) + .unwrap_err(); + assert_eq!(panic.downcast_ref::<&str>(), Some(&"prereq panic")); + drop(handle); + drop(multi); + assert!(server.join().unwrap().is_empty()); +} + +#[test] +fn easy_prereq_supports_owned_and_scoped_callbacks() { + if !supported() { + return; + } + let (address, server) = keepalive_server(vec![EMPTY; 5]); + let calls = Arc::new(AtomicUsize::new(0)); + let owned_calls = Arc::clone(&calls); + let mut easy = Easy::new(); + easy.url(&format!("http://{}/", address)).unwrap(); + easy.proxy("").unwrap(); + easy.timeout(TIMEOUT).unwrap(); + easy.prereq_function(move |remote, local, remote_port, local_port| { + assert_eq!(remote, "127.0.0.1"); + assert_eq!(local, "127.0.0.1"); + assert_eq!(remote_port, address.port()); + assert_ne!(local_port, 0); + owned_calls.fetch_add(1, Ordering::SeqCst); + true + }) + .unwrap(); + easy.perform().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 1); + let mut scoped_calls = 0; + { + let mut transfer = easy.transfer(); + transfer + .prereq_function(|_, _, port, _| { + assert_eq!(port, address.port()); + scoped_calls += 1; + true + }) + .unwrap(); + transfer.perform().unwrap(); + } + assert_eq!(scoped_calls, 1); + assert_eq!(calls.load(Ordering::SeqCst), 1); + easy.perform().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 2); + easy.reset(); + easy.url(&format!("http://{}/", address)).unwrap(); + easy.proxy("").unwrap(); + easy.timeout(TIMEOUT).unwrap(); + easy.perform().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 2); + assert_eq!( + Arc::strong_count(&calls), + 2, + "reset retains the closure while disabling its callback" + ); + easy.prereq(true).unwrap(); + easy.perform().unwrap(); + assert_eq!(calls.load(Ordering::SeqCst), 3); + drop(easy); + assert_eq!(Arc::strong_count(&calls), 1); + assert_eq!(server.join().unwrap().len(), 5); +} + +#[test] +fn prereq_reports_unsupported_libcurl_instead_of_panicking() { + struct Empty; + impl Handler for Empty {} + let mut easy = Easy2::new(Empty); + let result = easy.prereq(true); + if supported() { + result.unwrap(); + } else { + assert!(result.unwrap_err().is_unknown_option()); + } +}