#![allow(non_camel_case_types)]
use std::ffi::{c_char, c_int, c_long, CStr};
use std::panic::{self, AssertUnwindSafe};
use std::ptr;
use crate::http::{Request, Response};
fn ffi_guard<T>(default: T, f: impl FnOnce() -> T) -> T {
panic::catch_unwind(AssertUnwindSafe(f)).unwrap_or(default)
}
pub enum RSURL {}
#[repr(C)]
pub enum RsurlOpt {
Url = 1,
CustomRequest = 2,
Header = 3,
PostFieldsString = 4,
ConnectTimeout = 5,
Timeout = 6,
UserAgent = 7,
Idn = 8,
FollowLocation = 9,
MaxRedirs = 10,
UserPwd = 11,
SslVerifyPeer = 12,
Proxy = 13,
Referer = 14,
Range = 15,
Cookie = 16,
Bearer = 17,
AcceptEncoding = 18,
}
#[repr(C)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RsurlCode {
Ok = 0,
InvalidHandle = 1,
UnknownOption = 2,
InvalidArg = 3,
NoResponse = 4,
Network = 5,
BadResponse = 6,
Unsupported = 7,
}
struct Handle {
url: Option<String>,
method: Option<String>,
user_agent: Option<String>,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
connect_timeout_secs: Option<u64>,
timeout_secs: Option<u64>,
idn: bool,
follow_location: bool,
max_redirs: Option<u32>,
basic_auth: Option<(String, String)>,
verify_peer: bool,
proxy: Option<String>,
referer: Option<String>,
range: Option<String>,
cookie: Option<String>,
bearer: Option<String>,
accept_encoding: Option<String>,
last_response: Option<Response>,
header_buf: Vec<Vec<u8>>,
}
impl Handle {
fn new() -> Self {
Handle {
url: None,
method: None,
user_agent: None,
headers: Vec::new(),
body: None,
connect_timeout_secs: None,
timeout_secs: None,
idn: true,
follow_location: false,
max_redirs: None,
basic_auth: None,
verify_peer: true,
proxy: None,
referer: None,
range: None,
cookie: None,
bearer: None,
accept_encoding: None,
last_response: None,
header_buf: Vec::new(),
}
}
}
fn handle_mut<'a>(h: *mut RSURL) -> Option<&'a mut Handle> {
if h.is_null() {
return None;
}
Some(unsafe { &mut *(h as *mut Handle) })
}
fn handle_ref<'a>(h: *const RSURL) -> Option<&'a Handle> {
if h.is_null() {
return None;
}
Some(unsafe { &*(h as *const Handle) })
}
#[no_mangle]
pub extern "C" fn rsurl_easy_init() -> *mut RSURL {
ffi_guard(ptr::null_mut(), || {
let boxed = Box::new(Handle::new());
Box::into_raw(boxed) as *mut RSURL
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_cleanup(handle: *mut RSURL) {
ffi_guard((), || {
if handle.is_null() {
return;
}
unsafe {
drop(Box::from_raw(handle as *mut Handle));
}
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_reset(handle: *mut RSURL) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
*h = Handle::new();
RsurlCode::Ok
})
}
#[no_mangle]
pub unsafe extern "C" fn rsurl_easy_setopt_str(
handle: *mut RSURL,
option: c_int,
value: *const c_char,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let s = if value.is_null() {
None
} else {
match unsafe { CStr::from_ptr(value) }.to_str() {
Ok(s) => Some(s.to_string()),
Err(_) => return RsurlCode::InvalidArg,
}
};
let Some(opt) = opt_from_int(option) else {
return RsurlCode::UnknownOption;
};
match opt {
RsurlOpt::Url => h.url = s,
RsurlOpt::CustomRequest => h.method = s,
RsurlOpt::UserAgent => h.user_agent = s,
RsurlOpt::Header => match s {
Some(line) => {
let Some((k, v)) = line.split_once(':') else {
return RsurlCode::InvalidArg;
};
h.headers.push((k.trim().to_string(), v.trim().to_string()));
}
None => h.headers.clear(),
},
RsurlOpt::PostFieldsString => h.body = s.map(|s| s.into_bytes()),
RsurlOpt::UserPwd => {
h.basic_auth = s.map(|v| match v.split_once(':') {
Some((u, p)) => (u.to_string(), p.to_string()),
None => (v, String::new()),
});
}
RsurlOpt::Proxy => h.proxy = s,
RsurlOpt::Referer => h.referer = s,
RsurlOpt::Range => h.range = s,
RsurlOpt::Cookie => h.cookie = s,
RsurlOpt::Bearer => h.bearer = s,
RsurlOpt::AcceptEncoding => h.accept_encoding = s,
RsurlOpt::ConnectTimeout
| RsurlOpt::Timeout
| RsurlOpt::Idn
| RsurlOpt::FollowLocation
| RsurlOpt::MaxRedirs
| RsurlOpt::SslVerifyPeer => return RsurlCode::InvalidArg,
}
RsurlCode::Ok
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_setopt_long(
handle: *mut RSURL,
option: c_int,
value: c_long,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let Some(opt) = opt_from_int(option) else {
return RsurlCode::UnknownOption;
};
let secs = if value <= 0 { None } else { Some(value as u64) };
match opt {
RsurlOpt::ConnectTimeout => h.connect_timeout_secs = secs,
RsurlOpt::Timeout => h.timeout_secs = secs,
RsurlOpt::Idn => h.idn = value != 0,
RsurlOpt::FollowLocation => h.follow_location = value != 0,
RsurlOpt::MaxRedirs => {
h.max_redirs = if value < 0 {
Some(0)
} else {
Some(value as u32)
}
}
RsurlOpt::SslVerifyPeer => h.verify_peer = value != 0,
_ => return RsurlCode::InvalidArg,
}
RsurlCode::Ok
})
}
fn opt_from_int(v: c_int) -> Option<RsurlOpt> {
Some(match v {
1 => RsurlOpt::Url,
2 => RsurlOpt::CustomRequest,
3 => RsurlOpt::Header,
4 => RsurlOpt::PostFieldsString,
5 => RsurlOpt::ConnectTimeout,
6 => RsurlOpt::Timeout,
7 => RsurlOpt::UserAgent,
8 => RsurlOpt::Idn,
9 => RsurlOpt::FollowLocation,
10 => RsurlOpt::MaxRedirs,
11 => RsurlOpt::UserPwd,
12 => RsurlOpt::SslVerifyPeer,
13 => RsurlOpt::Proxy,
14 => RsurlOpt::Referer,
15 => RsurlOpt::Range,
16 => RsurlOpt::Cookie,
17 => RsurlOpt::Bearer,
18 => RsurlOpt::AcceptEncoding,
_ => return None,
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_perform(handle: *mut RSURL) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_mut(handle) else {
return RsurlCode::InvalidHandle;
};
let Some(url) = h.url.as_deref() else {
return RsurlCode::InvalidArg;
};
let method = h.method.clone().unwrap_or_else(|| {
if h.body.is_some() {
"POST".to_string()
} else {
"GET".to_string()
}
});
let mut req = match Request::new(&method, url) {
Ok(r) => r,
Err(crate::Error::UnsupportedScheme(_)) => return RsurlCode::Unsupported,
Err(_) => return RsurlCode::InvalidArg,
};
req = req.idn(h.idn).verify_tls(h.verify_peer);
if h.follow_location {
req = req.follow_redirects(true);
if let Some(n) = h.max_redirs {
req = req.max_redirs(n);
}
}
if let Some(spec) = &h.proxy {
req = match req.proxy(spec) {
Ok(r) => r,
Err(_) => return RsurlCode::InvalidArg,
};
}
if let Some((u, p)) = &h.basic_auth {
req = req.basic_auth(u, p);
}
for (k, v) in &h.headers {
req = req.header(k, v);
}
if let Some(ua) = &h.user_agent {
req = req.header("User-Agent", ua);
}
if let Some(referer) = &h.referer {
req = req.header("Referer", referer);
}
if let Some(cookie) = &h.cookie {
req = req.header("Cookie", cookie);
}
if let Some(token) = &h.bearer {
req = req.header("Authorization", &format!("Bearer {token}"));
}
if let Some(range) = &h.range {
let v = if range.contains('=') {
range.clone()
} else {
format!("bytes={range}")
};
req = req.header("Range", &v);
}
if let Some(enc) = &h.accept_encoding {
let v = if enc.is_empty() {
"gzip, deflate, br, zstd"
} else {
enc.as_str()
};
req = req.header("Accept-Encoding", v);
}
if let Some(body) = h.body.clone() {
req = req.body(body);
}
match req.send() {
Ok(resp) => {
h.header_buf = resp
.headers
.iter()
.map(|(k, v)| {
let mut s = format!("{k}: {v}").into_bytes();
s.push(0);
s
})
.collect();
h.last_response = Some(resp);
RsurlCode::Ok
}
Err(crate::Error::UnsupportedScheme(_)) => RsurlCode::Unsupported,
Err(crate::Error::Io(_)) | Err(crate::Error::UnexpectedEof) => RsurlCode::Network,
Err(crate::Error::BadResponse(_))
| Err(crate::Error::H2NotNegotiated)
| Err(crate::Error::Ssh(_))
| Err(crate::Error::Decode(_))
| Err(crate::Error::Status { .. }) => RsurlCode::BadResponse,
Err(crate::Error::InvalidUrl(_)) => RsurlCode::InvalidArg,
}
})
}
#[no_mangle]
pub unsafe extern "C" fn rsurl_easy_response_body(
handle: *const RSURL,
out_ptr: *mut *const u8,
out_len: *mut usize,
) -> RsurlCode {
ffi_guard(RsurlCode::Network, || {
let Some(h) = handle_ref(handle) else {
return RsurlCode::InvalidHandle;
};
if out_ptr.is_null() || out_len.is_null() {
return RsurlCode::InvalidArg;
}
match &h.last_response {
Some(resp) => unsafe {
*out_ptr = resp.body.as_ptr();
*out_len = resp.body.len();
},
None => unsafe {
*out_ptr = ptr::null();
*out_len = 0;
},
}
RsurlCode::Ok
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_status(handle: *const RSURL) -> c_long {
ffi_guard(0, || {
handle_ref(handle)
.and_then(|h| h.last_response.as_ref())
.map(|r| r.status as c_long)
.unwrap_or(0)
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_header(handle: *const RSURL, index: usize) -> *const c_char {
ffi_guard(ptr::null(), || {
let Some(h) = handle_ref(handle) else {
return ptr::null();
};
h.header_buf
.get(index)
.map(|b| b.as_ptr() as *const c_char)
.unwrap_or(ptr::null())
})
}
#[no_mangle]
pub extern "C" fn rsurl_easy_response_header_count(handle: *const RSURL) -> usize {
ffi_guard(0, || {
handle_ref(handle).map(|h| h.header_buf.len()).unwrap_or(0)
})
}
#[no_mangle]
pub extern "C" fn rsurl_strerror(code: c_int) -> *const c_char {
ffi_guard(ptr::null(), || {
let s: &'static [u8] = match code {
0 => b"ok\0",
1 => b"invalid handle\0",
2 => b"unknown option\0",
3 => b"invalid argument\0",
4 => b"no response available\0",
5 => b"network error\0",
6 => b"bad response\0",
7 => b"unsupported scheme or feature\0",
_ => b"unknown error\0",
};
s.as_ptr() as *const c_char
})
}
#[no_mangle]
pub extern "C" fn rsurl_version() -> *const c_char {
ffi_guard(ptr::null(), || {
concat!("rsurl/", env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
})
}