Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/websocket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions curl-sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down
55 changes: 55 additions & 0 deletions src/easy/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ struct Callbacks<'a> {
read: Option<Box<dyn FnMut(&mut [u8]) -> Result<usize, ReadError> + 'a>>,
seek: Option<Box<dyn FnMut(SeekFrom) -> SeekResult + 'a>>,
debug: Option<Box<dyn FnMut(InfoType, &[u8]) + 'a>>,
prereq: Option<Box<dyn FnMut(&str, &str, u16, u16) -> bool + 'a>>,
header: Option<Box<dyn FnMut(&[u8]) -> bool + 'a>>,
progress: Option<Box<dyn FnMut(f64, f64, f64, f64) -> bool + 'a>>,
ssl_ctx: Option<Box<dyn FnMut(*mut c_void) -> Result<(), Error> + 'a>>,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<F>(&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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<F>(&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<F>(&mut self, f: F) -> Result<(), Error>
Expand Down
68 changes: 68 additions & 0 deletions src/easy/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -620,6 +644,23 @@ impl<H: Handler> Easy2<H> {
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::<H>;
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,
Expand Down Expand Up @@ -3796,6 +3837,33 @@ extern "C" fn progress_cb<H: Handler>(
}
}

extern "C" fn prereq_cb<H: Handler>(
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<H>))
.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<H: Handler>(
_handle: *mut curl_sys::CURL,
Expand Down
19 changes: 14 additions & 5 deletions src/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -517,12 +517,14 @@ impl Multi {
pub fn action(&self, socket: Socket, events: &Events) -> Result<u32, MultiError> {
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)
}
}
Expand All @@ -545,12 +547,14 @@ impl Multi {
pub fn timeout(&self) -> Result<u32, MultiError> {
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)
}
}
Expand Down Expand Up @@ -735,7 +739,12 @@ impl Multi {
pub fn perform(&self) -> Result<u32, MultiError> {
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)
}
}
Expand Down
13 changes: 12 additions & 1 deletion systest/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -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;
}
Expand Down
50 changes: 50 additions & 0 deletions tests/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading
Loading