1use libloading::Library;
2use std::ffi::CStr;
3use std::fs::{self};
4use std::io;
5use std::mem::ManuallyDrop;
6use std::os::raw::{c_char, c_int, c_long, c_short, c_uint, c_ulong, c_void};
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::sync::{Arc, OnceLock};
10use tracing::{debug, info};
11
12pub type CurlCode = c_int;
13pub type CurlOption = c_uint;
14pub type CurlMCode = c_int;
15pub type CurlMOption = c_int;
16
17#[cfg(windows)]
18pub type CurlSocket = usize;
19#[cfg(not(windows))]
20pub type CurlSocket = c_int;
21
22pub type CurlMultiSocketCallback =
23 unsafe extern "C" fn(*mut Curl, CurlSocket, c_int, *mut c_void, *mut c_void) -> c_int;
24pub type CurlMultiTimerCallback =
25 unsafe extern "C" fn(*mut CurlMulti, c_long, *mut c_void) -> c_int;
26
27#[repr(C)]
28pub struct Curl {
29 _private: [u8; 0],
30}
31
32#[repr(C)]
33pub struct CurlMulti {
34 _private: [u8; 0],
35}
36
37#[repr(C)]
38pub struct CurlSlist {
39 pub data: *mut c_char,
40 pub next: *mut CurlSlist,
41}
42
43#[repr(C)]
44pub struct CurlWsFrame {
45 pub age: c_int,
46 pub flags: c_int,
47 pub offset: i64,
48 pub bytesleft: i64,
49 pub len: usize,
50}
51
52#[repr(C)]
53pub union CurlMessageData {
54 pub whatever: *mut c_void,
55 pub result: CurlCode,
56}
57
58#[repr(C)]
59pub struct CurlMessage {
60 pub msg: c_int,
61 pub easy_handle: *mut Curl,
62 pub data: CurlMessageData,
63}
64
65#[repr(C)]
66pub struct CurlWaitFd {
67 pub fd: CurlSocket,
68 pub events: c_short,
69 pub revents: c_short,
70}
71
72pub const CURLE_OK: CurlCode = 0;
73pub const CURLE_AGAIN: CurlCode = 81;
74pub const CURL_GLOBAL_DEFAULT: c_ulong = 3;
75pub const CURLM_OK: CurlMCode = 0;
76pub const CURLMSG_DONE: c_int = 1;
77
78#[cfg(windows)]
79pub const CURL_SOCKET_TIMEOUT: CurlSocket = usize::MAX;
80#[cfg(not(windows))]
81pub const CURL_SOCKET_TIMEOUT: CurlSocket = -1;
82
83pub const CURL_CSELECT_IN: c_int = 0x01;
84pub const CURL_CSELECT_OUT: c_int = 0x02;
85pub const CURL_CSELECT_ERR: c_int = 0x04;
86
87pub const CURL_POLL_NONE: c_int = 0;
88pub const CURL_POLL_IN: c_int = 1;
89pub const CURL_POLL_OUT: c_int = 2;
90pub const CURL_POLL_INOUT: c_int = 3;
91pub const CURL_POLL_REMOVE: c_int = 4;
92
93pub const CURLMOPT_SOCKETFUNCTION: CurlMOption = 20001;
94pub const CURLMOPT_SOCKETDATA: CurlMOption = 10002;
95pub const CURLMOPT_TIMERFUNCTION: CurlMOption = 20004;
96pub const CURLMOPT_TIMERDATA: CurlMOption = 10005;
97
98pub const CURLOPT_URL: CurlOption = 10002;
99pub const CURLOPT_HTTPHEADER: CurlOption = 10023;
100pub const CURLOPT_HTTP_VERSION: CurlOption = 84;
101pub const CURLOPT_CONNECT_ONLY: CurlOption = 141;
102pub const CURLOPT_VERBOSE: CurlOption = 41;
103pub const CURLOPT_PROXY: CurlOption = 10004;
104pub const CURLOPT_CAINFO: CurlOption = 10065;
105pub const CURLINFO_RESPONSE_CODE: c_uint = 0x200002;
106
107pub const CURL_HTTP_VERSION_1_1: c_long = 2;
108pub const CURLWS_TEXT: c_uint = 1;
109
110#[derive(Debug, thiserror::Error)]
111pub enum SysError {
112 #[error("failed to load dynamic library {path}: {source}")]
113 LoadLibrary {
114 path: PathBuf,
115 #[source]
116 source: libloading::Error,
117 },
118 #[error("missing symbol {name}: {source}")]
119 MissingSymbol {
120 name: String,
121 #[source]
122 source: libloading::Error,
123 },
124 #[error("CURL_IMPERSONATE_LIB points to a missing file: {0}")]
125 MissingEnvPath(PathBuf),
126 #[error("failed to locate libcurl-impersonate. searched: {0:?}")]
127 LibraryNotFound(Vec<PathBuf>),
128 #[error(
129 "failed to locate libcurl-impersonate after auto-fetch attempt. searched: {searched:?}; auto-fetch error: {auto_fetch_error}"
130 )]
131 LibraryNotFoundAfterAutoFetch {
132 searched: Vec<PathBuf>,
133 auto_fetch_error: String,
134 },
135 #[error("auto-fetch is not supported on target: {0}")]
136 AutoFetchUnsupportedTarget(String),
137 #[error("auto-fetch needs cache directory but HOME and IMPCURL_LIB_DIR are not set")]
138 AutoFetchCacheDirUnavailable,
139 #[error("failed to run downloader command {command}: {source}")]
140 AutoFetchCommandSpawn { command: String, source: io::Error },
141 #[error("downloader command {command} failed with status {status:?}: {stderr}")]
142 AutoFetchCommandFailed {
143 command: String,
144 status: Option<i32>,
145 stderr: String,
146 },
147 #[error("I/O error during auto-fetch: {0}")]
148 AutoFetchIo(#[from] io::Error),
149 #[error("no standalone libcurl-impersonate shared library was found in {cache_dir}")]
150 AutoFetchNoStandaloneRuntime { cache_dir: PathBuf },
151 #[error("no libcurl-impersonate asset naming rule for target: {0}")]
152 AutoFetchRuntimeUnsupportedTarget(String),
153}
154
155pub struct CurlApi {
156 _lib: ManuallyDrop<Library>,
159 pub global_init: unsafe extern "C" fn(c_ulong) -> CurlCode,
160 pub global_cleanup: unsafe extern "C" fn(),
161 pub easy_init: unsafe extern "C" fn() -> *mut Curl,
162 pub easy_cleanup: unsafe extern "C" fn(*mut Curl),
163 pub easy_perform: unsafe extern "C" fn(*mut Curl) -> CurlCode,
164 pub easy_setopt: unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode,
165 pub easy_getinfo: unsafe extern "C" fn(*mut Curl, c_uint, ...) -> CurlCode,
166 pub easy_strerror: unsafe extern "C" fn(CurlCode) -> *const c_char,
167 pub easy_impersonate: unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode,
168 pub slist_append: unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist,
169 pub slist_free_all: unsafe extern "C" fn(*mut CurlSlist),
170 pub ws_send:
171 unsafe extern "C" fn(*mut Curl, *const c_void, usize, *mut usize, i64, c_uint) -> CurlCode,
172 pub ws_recv: unsafe extern "C" fn(
173 *mut Curl,
174 *mut c_void,
175 usize,
176 *mut usize,
177 *mut *const CurlWsFrame,
178 ) -> CurlCode,
179 pub multi_init: unsafe extern "C" fn() -> *mut CurlMulti,
180 pub multi_cleanup: unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode,
181 pub multi_setopt: unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode,
182 pub multi_add_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
183 pub multi_remove_handle: unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode,
184 pub multi_fdset: unsafe extern "C" fn(
185 *mut CurlMulti,
186 *mut c_void,
187 *mut c_void,
188 *mut c_void,
189 *mut c_int,
190 ) -> CurlMCode,
191 pub multi_timeout: unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode,
192 pub multi_perform: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode,
193 pub multi_poll: unsafe extern "C" fn(
194 *mut CurlMulti,
195 *mut CurlWaitFd,
196 c_uint,
197 c_int,
198 *mut c_int,
199 ) -> CurlMCode,
200 pub multi_socket_action:
201 unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
202 pub multi_info_read: unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage,
203 pub multi_strerror: unsafe extern "C" fn(CurlMCode) -> *const c_char,
204}
205
206impl CurlApi {
207 pub unsafe fn load(path: &Path) -> Result<Self, SysError> {
210 debug!(path = %path.display(), "loading curl-impersonate library");
211 let lib = unsafe { Library::new(path) }.map_err(|source| SysError::LoadLibrary {
212 path: path.to_path_buf(),
213 source,
214 })?;
215
216 let global_init = unsafe {
217 load_symbol::<unsafe extern "C" fn(c_ulong) -> CurlCode>(&lib, b"curl_global_init\0")?
218 };
219 let global_cleanup =
220 unsafe { load_symbol::<unsafe extern "C" fn()>(&lib, b"curl_global_cleanup\0")? };
221 let easy_init = unsafe {
222 load_symbol::<unsafe extern "C" fn() -> *mut Curl>(&lib, b"curl_easy_init\0")?
223 };
224 let easy_cleanup = unsafe {
225 load_symbol::<unsafe extern "C" fn(*mut Curl)>(&lib, b"curl_easy_cleanup\0")?
226 };
227 let easy_perform = unsafe {
228 load_symbol::<unsafe extern "C" fn(*mut Curl) -> CurlCode>(
229 &lib,
230 b"curl_easy_perform\0",
231 )?
232 };
233 let easy_setopt = unsafe {
234 load_symbol::<unsafe extern "C" fn(*mut Curl, CurlOption, ...) -> CurlCode>(
235 &lib,
236 b"curl_easy_setopt\0",
237 )?
238 };
239 let easy_getinfo = unsafe {
240 load_symbol::<unsafe extern "C" fn(*mut Curl, c_uint, ...) -> CurlCode>(
241 &lib,
242 b"curl_easy_getinfo\0",
243 )?
244 };
245 let easy_strerror = unsafe {
246 load_symbol::<unsafe extern "C" fn(CurlCode) -> *const c_char>(
247 &lib,
248 b"curl_easy_strerror\0",
249 )?
250 };
251 let easy_impersonate = unsafe {
252 load_symbol::<unsafe extern "C" fn(*mut Curl, *const c_char, c_int) -> CurlCode>(
253 &lib,
254 b"curl_easy_impersonate\0",
255 )?
256 };
257 let slist_append = unsafe {
258 load_symbol::<unsafe extern "C" fn(*mut CurlSlist, *const c_char) -> *mut CurlSlist>(
259 &lib,
260 b"curl_slist_append\0",
261 )?
262 };
263 let slist_free_all = unsafe {
264 load_symbol::<unsafe extern "C" fn(*mut CurlSlist)>(&lib, b"curl_slist_free_all\0")?
265 };
266 let ws_send = unsafe {
267 load_symbol::<
268 unsafe extern "C" fn(
269 *mut Curl,
270 *const c_void,
271 usize,
272 *mut usize,
273 i64,
274 c_uint,
275 ) -> CurlCode,
276 >(&lib, b"curl_ws_send\0")?
277 };
278 let ws_recv = unsafe {
279 load_symbol::<
280 unsafe extern "C" fn(
281 *mut Curl,
282 *mut c_void,
283 usize,
284 *mut usize,
285 *mut *const CurlWsFrame,
286 ) -> CurlCode,
287 >(&lib, b"curl_ws_recv\0")?
288 };
289 let multi_init = unsafe {
290 load_symbol::<unsafe extern "C" fn() -> *mut CurlMulti>(&lib, b"curl_multi_init\0")?
291 };
292 let multi_cleanup = unsafe {
293 load_symbol::<unsafe extern "C" fn(*mut CurlMulti) -> CurlMCode>(
294 &lib,
295 b"curl_multi_cleanup\0",
296 )?
297 };
298 let multi_setopt = unsafe {
299 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, CurlMOption, ...) -> CurlMCode>(
300 &lib,
301 b"curl_multi_setopt\0",
302 )?
303 };
304 let multi_add_handle = unsafe {
305 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
306 &lib,
307 b"curl_multi_add_handle\0",
308 )?
309 };
310 let multi_remove_handle = unsafe {
311 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut Curl) -> CurlMCode>(
312 &lib,
313 b"curl_multi_remove_handle\0",
314 )?
315 };
316 let multi_fdset = unsafe {
317 load_symbol::<
318 unsafe extern "C" fn(
319 *mut CurlMulti,
320 *mut c_void,
321 *mut c_void,
322 *mut c_void,
323 *mut c_int,
324 ) -> CurlMCode,
325 >(&lib, b"curl_multi_fdset\0")?
326 };
327 let multi_timeout = unsafe {
328 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_long) -> CurlMCode>(
329 &lib,
330 b"curl_multi_timeout\0",
331 )?
332 };
333 let multi_perform = unsafe {
334 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> CurlMCode>(
335 &lib,
336 b"curl_multi_perform\0",
337 )?
338 };
339 let multi_poll = unsafe {
340 load_symbol::<
341 unsafe extern "C" fn(
342 *mut CurlMulti,
343 *mut CurlWaitFd,
344 c_uint,
345 c_int,
346 *mut c_int,
347 ) -> CurlMCode,
348 >(&lib, b"curl_multi_poll\0")?
349 };
350 let multi_socket_action = unsafe {
351 load_symbol::<
352 unsafe extern "C" fn(*mut CurlMulti, CurlSocket, c_int, *mut c_int) -> CurlMCode,
353 >(&lib, b"curl_multi_socket_action\0")?
354 };
355 let multi_info_read = unsafe {
356 load_symbol::<unsafe extern "C" fn(*mut CurlMulti, *mut c_int) -> *mut CurlMessage>(
357 &lib,
358 b"curl_multi_info_read\0",
359 )?
360 };
361 let multi_strerror = unsafe {
362 load_symbol::<unsafe extern "C" fn(CurlMCode) -> *const c_char>(
363 &lib,
364 b"curl_multi_strerror\0",
365 )?
366 };
367
368 info!(path = %path.display(), "curl-impersonate library loaded");
369 Ok(Self {
370 _lib: ManuallyDrop::new(lib),
371 global_init,
372 global_cleanup,
373 easy_init,
374 easy_cleanup,
375 easy_perform,
376 easy_setopt,
377 easy_getinfo,
378 easy_strerror,
379 easy_impersonate,
380 slist_append,
381 slist_free_all,
382 ws_send,
383 ws_recv,
384 multi_init,
385 multi_cleanup,
386 multi_setopt,
387 multi_add_handle,
388 multi_remove_handle,
389 multi_fdset,
390 multi_timeout,
391 multi_perform,
392 multi_poll,
393 multi_socket_action,
394 multi_info_read,
395 multi_strerror,
396 })
397 }
398
399 pub fn error_text(&self, code: CurlCode) -> String {
400 unsafe {
401 let ptr = (self.easy_strerror)(code);
402 if ptr.is_null() {
403 return format!("CURLcode {}", code);
404 }
405 CStr::from_ptr(ptr).to_string_lossy().into_owned()
406 }
407 }
408
409 pub fn multi_error_text(&self, code: CurlMCode) -> String {
410 unsafe {
411 let ptr = (self.multi_strerror)(code);
412 if ptr.is_null() {
413 return format!("CURLMcode {}", code);
414 }
415 CStr::from_ptr(ptr).to_string_lossy().into_owned()
416 }
417 }
418}
419
420unsafe impl Send for CurlApi {}
423unsafe impl Sync for CurlApi {}
424
425static SHARED_API: OnceLock<Arc<CurlApi>> = OnceLock::new();
426
427pub fn shared_curl_api(lib_path: &Path) -> Result<Arc<CurlApi>, SysError> {
430 if let Some(api) = SHARED_API.get() {
431 return Ok(Arc::clone(api));
432 }
433 let api = unsafe { CurlApi::load(lib_path) }?;
434 let arc = Arc::new(api);
435 let _ = SHARED_API.set(Arc::clone(&arc));
437 Ok(SHARED_API.get().map(Arc::clone).unwrap_or(arc))
438}
439
440impl CurlMessage {
441 pub unsafe fn done_result(&self) -> CurlCode {
444 unsafe { self.data.result }
445 }
446}
447
448unsafe fn load_symbol<T: Copy>(lib: &Library, name: &[u8]) -> Result<T, SysError> {
449 let symbol = unsafe { lib.get::<T>(name) }.map_err(|source| SysError::MissingSymbol {
450 name: String::from_utf8_lossy(name)
451 .trim_end_matches('\0')
452 .to_owned(),
453 source,
454 })?;
455 Ok(*symbol)
456}
457
458pub fn platform_library_names() -> &'static [&'static str] {
459 if cfg!(target_os = "macos") {
460 &["libcurl-impersonate.4.dylib", "libcurl-impersonate.dylib"]
461 } else if cfg!(target_os = "linux") {
462 &["libcurl-impersonate.so.4", "libcurl-impersonate.so"]
463 } else if cfg!(target_os = "windows") {
464 &[
465 "curl-impersonate.dll",
466 "libcurl-impersonate.dll",
467 "libcurl.dll",
468 ]
469 } else {
470 &[
471 "libcurl-impersonate.4.dylib",
472 "libcurl-impersonate.so.4",
473 "curl-impersonate.dll",
474 ]
475 }
476}
477
478pub fn find_near_executable() -> Option<PathBuf> {
479 let exe = std::env::current_exe().ok()?;
480 let exe_dir = exe.parent()?;
481 for name in platform_library_names() {
482 let in_lib = exe_dir.join("..").join("lib").join(name);
483 if in_lib.exists() {
484 return Some(in_lib);
485 }
486 let side_by_side = exe_dir.join(name);
487 if side_by_side.exists() {
488 return Some(side_by_side);
489 }
490 }
491 None
492}
493
494fn probe_library_dir(dir: &Path, searched: &mut Vec<PathBuf>) -> Option<PathBuf> {
495 for name in platform_library_names() {
496 let candidate = dir.join(name);
497 searched.push(candidate.clone());
498 if candidate.exists() {
499 return Some(candidate);
500 }
501 }
502 None
503}
504
505fn default_library_search_roots() -> Vec<PathBuf> {
506 let mut roots = Vec::new();
507
508 if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
509 roots.push(PathBuf::from(dir));
510 }
511
512 if let Ok(home) = std::env::var("HOME") {
513 roots.push(PathBuf::from(&home).join(".impcurl/lib"));
514 roots.push(PathBuf::from(home).join(".cuimp/binaries"));
515 }
516
517 roots
518}
519
520pub fn resolve_ca_bundle_path() -> Option<PathBuf> {
527 for key in ["CURL_CA_BUNDLE", "SSL_CERT_FILE"] {
528 if let Ok(value) = std::env::var(key) {
529 let candidate = PathBuf::from(value);
530 if candidate.is_file() {
531 debug!(path = %candidate.display(), env = key, "resolved CA bundle from env");
532 return Some(candidate);
533 }
534 }
535 }
536
537 for candidate in default_ca_bundle_candidates() {
538 if candidate.is_file() {
539 debug!(path = %candidate.display(), "resolved CA bundle from platform defaults");
540 return Some(candidate);
541 }
542 }
543
544 None
545}
546
547fn default_ca_bundle_candidates() -> Vec<PathBuf> {
548 if cfg!(target_os = "linux") {
549 vec![
550 PathBuf::from("/etc/ssl/certs/ca-certificates.crt"),
551 PathBuf::from("/etc/pki/tls/certs/ca-bundle.crt"),
552 PathBuf::from("/etc/ssl/cert.pem"),
553 PathBuf::from("/etc/pki/tls/cert.pem"),
554 PathBuf::from("/etc/ssl/ca-bundle.pem"),
555 ]
556 } else if cfg!(target_os = "macos") {
557 vec![PathBuf::from("/etc/ssl/cert.pem")]
558 } else {
559 Vec::new()
560 }
561}
562
563fn auto_fetch_enabled() -> bool {
564 match std::env::var("IMPCURL_AUTO_FETCH") {
565 Ok(value) => {
566 let normalized = value.trim().to_ascii_lowercase();
567 !matches!(normalized.as_str(), "0" | "false" | "no" | "off")
568 }
569 Err(_) => true,
570 }
571}
572
573fn auto_fetch_cache_dir() -> Result<PathBuf, SysError> {
574 if let Ok(dir) = std::env::var("IMPCURL_AUTO_FETCH_CACHE_DIR") {
575 return Ok(PathBuf::from(dir));
576 }
577 if let Ok(dir) = std::env::var("IMPCURL_LIB_DIR") {
578 return Ok(PathBuf::from(dir));
579 }
580 if let Ok(home) = std::env::var("HOME") {
581 return Ok(PathBuf::from(home).join(".impcurl/lib"));
582 }
583 Err(SysError::AutoFetchCacheDirUnavailable)
584}
585
586fn current_target_triple() -> &'static str {
587 if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
588 "aarch64-apple-darwin"
589 } else if cfg!(all(target_os = "macos", target_arch = "x86_64")) {
590 "x86_64-apple-darwin"
591 } else if cfg!(all(
592 target_os = "linux",
593 target_arch = "x86_64",
594 target_env = "gnu"
595 )) {
596 "x86_64-unknown-linux-gnu"
597 } else if cfg!(all(
598 target_os = "linux",
599 target_arch = "x86",
600 target_env = "gnu"
601 )) {
602 "i686-unknown-linux-gnu"
603 } else if cfg!(all(
604 target_os = "linux",
605 target_arch = "aarch64",
606 target_env = "gnu"
607 )) {
608 "aarch64-unknown-linux-gnu"
609 } else if cfg!(all(
610 target_os = "linux",
611 target_arch = "x86_64",
612 target_env = "musl"
613 )) {
614 "x86_64-unknown-linux-musl"
615 } else if cfg!(all(
616 target_os = "linux",
617 target_arch = "aarch64",
618 target_env = "musl"
619 )) {
620 "aarch64-unknown-linux-musl"
621 } else if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
622 "x86_64-pc-windows-msvc"
623 } else if cfg!(all(target_os = "windows", target_arch = "x86")) {
624 "i686-pc-windows-msvc"
625 } else if cfg!(all(target_os = "windows", target_arch = "aarch64")) {
626 "aarch64-pc-windows-msvc"
627 } else {
628 "unknown"
629 }
630}
631
632fn asset_target_for_triple(target: &str) -> Option<&'static str> {
633 match target {
634 "x86_64-apple-darwin" => Some("x86_64-apple-darwin"),
635 "aarch64-apple-darwin" => Some("aarch64-apple-darwin"),
636 "x86_64-unknown-linux-gnu" => Some("x86_64-unknown-linux-gnu"),
637 "aarch64-unknown-linux-gnu" => Some("aarch64-unknown-linux-gnu"),
638 "x86_64-unknown-linux-musl" => Some("x86_64-unknown-linux-musl"),
639 "aarch64-unknown-linux-musl" => Some("aarch64-unknown-linux-musl"),
640 _ => None,
641 }
642}
643
644fn asset_version() -> String {
645 std::env::var("IMPCURL_LIBCURL_VERSION")
646 .unwrap_or_else(|_| env!("CARGO_PKG_VERSION").to_owned())
647}
648
649fn asset_repo() -> String {
650 std::env::var("IMPCURL_LIBCURL_REPO").unwrap_or_else(|_| "tuchg/impcurl".to_owned())
651}
652
653fn asset_release_tag(version: &str) -> String {
654 format!("impcurl-libcurl-impersonate-v{version}")
655}
656
657fn asset_name(version: &str, target: &str) -> String {
658 format!("impcurl-libcurl-impersonate-v{version}-{target}.tar.gz")
659}
660
661fn asset_cache_dir(base: &Path, version: &str, target: &str) -> PathBuf {
662 base.join("libcurl-impersonate-assets")
663 .join(version)
664 .join(target)
665}
666
667fn asset_url(repo: &str, tag: &str, asset_name: &str) -> String {
668 format!("https://github.com/{repo}/releases/download/{tag}/{asset_name}")
669}
670
671fn run_download_command(command: &mut Command, command_label: &str) -> Result<Vec<u8>, SysError> {
672 let output = command
673 .output()
674 .map_err(|source| SysError::AutoFetchCommandSpawn {
675 command: command_label.to_owned(),
676 source,
677 })?;
678
679 if output.status.success() {
680 return Ok(output.stdout);
681 }
682
683 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
684 Err(SysError::AutoFetchCommandFailed {
685 command: command_label.to_owned(),
686 status: output.status.code(),
687 stderr,
688 })
689}
690
691fn fetch_url_to_file(url: &str, output_path: &Path) -> Result<(), SysError> {
692 if let Some(parent) = output_path.parent() {
693 fs::create_dir_all(parent)?;
694 }
695
696 let output_str = output_path.to_string_lossy().to_string();
697
698 let mut curl_cmd = Command::new("curl");
699 curl_cmd
700 .arg("-fL")
701 .arg("-o")
702 .arg(&output_str)
703 .arg("-H")
704 .arg("User-Agent: impcurl-sys")
705 .arg(url);
706 match run_download_command(&mut curl_cmd, "curl") {
707 Ok(_) => return Ok(()),
708 Err(SysError::AutoFetchCommandSpawn { .. }) => {}
709 Err(err) => return Err(err),
710 }
711
712 let mut wget_cmd = Command::new("wget");
713 wget_cmd.arg("-O").arg(&output_str).arg(url);
714 run_download_command(&mut wget_cmd, "wget")?;
715 Ok(())
716}
717
718fn extract_tar_gz_archive(archive_path: &Path, output_dir: &Path) -> Result<(), SysError> {
719 fs::create_dir_all(output_dir)?;
720 let mut tar_cmd = Command::new("tar");
721 tar_cmd
722 .arg("-xzf")
723 .arg(archive_path)
724 .arg("-C")
725 .arg(output_dir);
726 let _ = run_download_command(&mut tar_cmd, "tar")?;
727 Ok(())
728}
729
730fn auto_fetch_from_assets(cache_dir: &Path) -> Result<PathBuf, SysError> {
731 let version = asset_version();
732 let target_triple = current_target_triple().to_owned();
733 let target = asset_target_for_triple(&target_triple)
734 .ok_or_else(|| SysError::AutoFetchRuntimeUnsupportedTarget(target_triple.clone()))?;
735 let repo = asset_repo();
736 let output_dir = asset_cache_dir(cache_dir, &version, target);
737 let tag = asset_release_tag(&version);
738 let asset = asset_name(&version, target);
739 let url = asset_url(&repo, &tag, &asset);
740 let archive_path = cache_dir.join(format!(
741 ".libcurl-impersonate-asset-{version}-{target}-{}.tar.gz",
742 std::process::id()
743 ));
744
745 info!(
746 cache_dir = %cache_dir.display(),
747 output_dir = %output_dir.display(),
748 url = %url,
749 "auto-fetching libcurl-impersonate from asset release"
750 );
751
752 fetch_url_to_file(&url, &archive_path)?;
753 extract_tar_gz_archive(&archive_path, &output_dir)?;
754 let _ = fs::remove_file(&archive_path);
755
756 let mut searched = Vec::new();
757 probe_library_dir(&output_dir, &mut searched).ok_or_else(|| {
758 SysError::AutoFetchNoStandaloneRuntime {
759 cache_dir: output_dir,
760 }
761 })
762}
763
764pub fn resolve_impersonate_lib_path(extra_search_roots: &[PathBuf]) -> Result<PathBuf, SysError> {
765 if let Ok(path) = std::env::var("CURL_IMPERSONATE_LIB") {
766 let resolved = PathBuf::from(path);
767 if resolved.exists() {
768 debug!(path = %resolved.display(), "found via CURL_IMPERSONATE_LIB");
769 return Ok(resolved);
770 }
771 return Err(SysError::MissingEnvPath(resolved));
772 }
773
774 if let Some(packaged) = find_near_executable() {
775 debug!(path = %packaged.display(), "found near executable");
776 return Ok(packaged);
777 }
778
779 let mut searched = Vec::new();
780 for root in extra_search_roots {
781 if let Some(found) = probe_library_dir(root, &mut searched) {
782 return Ok(found);
783 }
784 }
785
786 for root in default_library_search_roots() {
787 if let Some(found) = probe_library_dir(&root, &mut searched) {
788 return Ok(found);
789 }
790 }
791
792 if auto_fetch_enabled() {
793 let auto_fetch_result = (|| -> Result<PathBuf, SysError> {
794 let cache_dir = auto_fetch_cache_dir()?;
795 let target_triple = current_target_triple().to_owned();
796 let target = asset_target_for_triple(&target_triple).ok_or_else(|| {
797 SysError::AutoFetchRuntimeUnsupportedTarget(target_triple.clone())
798 })?;
799 let version = asset_version();
800 let asset_dir = asset_cache_dir(&cache_dir, &version, target);
801 if let Some(found) = probe_library_dir(&asset_dir, &mut searched) {
802 return Ok(found);
803 }
804 if let Some(found) = probe_library_dir(&cache_dir, &mut searched) {
805 return Ok(found);
806 }
807
808 auto_fetch_from_assets(&cache_dir)?;
809 if let Some(found) = probe_library_dir(&asset_dir, &mut searched) {
810 return Ok(found);
811 }
812 probe_library_dir(&cache_dir, &mut searched).ok_or_else(|| {
813 SysError::AutoFetchNoStandaloneRuntime {
814 cache_dir: cache_dir.to_path_buf(),
815 }
816 })
817 })();
818
819 return match auto_fetch_result {
820 Ok(found) => Ok(found),
821 Err(err) => Err(SysError::LibraryNotFoundAfterAutoFetch {
822 searched,
823 auto_fetch_error: err.to_string(),
824 }),
825 };
826 }
827
828 Err(SysError::LibraryNotFound(searched))
829}
830
831#[cfg(test)]
832mod tests {
833 use super::{resolve_ca_bundle_path, asset_name, asset_release_tag};
834 use std::{env, ffi::OsString};
835
836 #[test]
837 fn prefers_curl_ca_bundle_env_when_file_exists() {
838 let fixture = env::current_exe().expect("current executable path should exist");
839 let _guard_ssl = EnvGuard::set("SSL_CERT_FILE", None);
840 let _guard_curl = EnvGuard::set("CURL_CA_BUNDLE", Some(fixture.as_os_str()));
841
842 let resolved = resolve_ca_bundle_path().expect("expected env CA bundle to resolve");
843 assert_eq!(resolved, fixture);
844 }
845
846 #[test]
847 fn asset_naming_is_versioned() {
848 assert_eq!(
849 asset_release_tag("1.2.3"),
850 "impcurl-libcurl-impersonate-v1.2.3"
851 );
852 assert_eq!(
853 asset_name("1.2.3", "x86_64-unknown-linux-gnu"),
854 "impcurl-libcurl-impersonate-v1.2.3-x86_64-unknown-linux-gnu.tar.gz"
855 );
856 }
857
858 struct EnvGuard {
859 key: &'static str,
860 old: Option<OsString>,
861 }
862
863 impl EnvGuard {
864 fn set(key: &'static str, new: Option<&std::ffi::OsStr>) -> Self {
865 let old = env::var_os(key);
866 unsafe {
867 match new {
868 Some(value) => env::set_var(key, value),
869 None => env::remove_var(key),
870 }
871 }
872 Self { key, old }
873 }
874 }
875
876 impl Drop for EnvGuard {
877 fn drop(&mut self) {
878 unsafe {
879 match self.old.as_ref() {
880 Some(value) => env::set_var(self.key, value),
881 None => env::remove_var(self.key),
882 }
883 }
884 }
885 }
886}