use cef::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crate::host::BUFFR_SRC_PREFIX;
pub fn register_buffr_src_scheme(registrar: &mut cef::SchemeRegistrar) {
let scheme = CefString::from("buffr-src");
let opts = (SchemeOptions::STANDARD.get_raw()
| SchemeOptions::SECURE.get_raw()
| SchemeOptions::CORS_ENABLED.get_raw()
| SchemeOptions::FETCH_ENABLED.get_raw()) as i32;
registrar.add_custom_scheme(Some(&scheme), opts);
}
pub fn register_buffr_src_handler_factory() {
let scheme = CefString::from("buffr-src");
let mut factory = BuffrSrcSchemeHandlerFactory::new();
cef::register_scheme_handler_factory(Some(&scheme), None, Some(&mut factory));
}
pub(crate) fn underlying_url(buffr_src_url: &str) -> &str {
buffr_src_url
.strip_prefix(BUFFR_SRC_PREFIX)
.unwrap_or(buffr_src_url)
}
wrap_scheme_handler_factory! {
pub struct BuffrSrcSchemeHandlerFactory {}
impl SchemeHandlerFactory {
fn create(
&self,
_browser: Option<&mut cef::Browser>,
_frame: Option<&mut cef::Frame>,
_scheme_name: Option<&CefString>,
request: Option<&mut cef::Request>,
) -> Option<cef::ResourceHandler> {
let buffr_src_url = request
.map(|r| CefStringUtf16::from(&r.url()).to_string())
.unwrap_or_default();
let underlying = underlying_url(&buffr_src_url).to_owned();
Some(BuffrSrcResourceHandler::new(
Arc::new(Mutex::new(None)),
underlying,
Arc::new(AtomicUsize::new(0)),
))
}
}
}
type BodySlot = Arc<Mutex<Option<Vec<u8>>>>;
wrap_resource_handler! {
pub struct BuffrSrcResourceHandler {
body: BodySlot,
underlying_url: String,
cursor: Arc<AtomicUsize>,
}
impl ResourceHandler {
fn open(
&self,
_request: Option<&mut cef::Request>,
handle_request: Option<&mut ::std::os::raw::c_int>,
callback: Option<&mut cef::Callback>,
) -> ::std::os::raw::c_int {
if let Some(hr) = handle_request {
*hr = 0;
}
let body_slot = Arc::clone(&self.body);
let url = self.underlying_url.clone();
let callback_arc: Option<cef::Callback> = callback.map(|c| {
c.clone()
});
std::thread::spawn(move || {
let html = fetch_and_render(&url);
let bytes = html.into_bytes();
if let Ok(mut slot) = body_slot.lock() {
*slot = Some(bytes);
}
if let Some(cb) = callback_arc {
cb.cont();
}
});
0
}
fn response_headers(
&self,
response: Option<&mut Response>,
response_length: Option<&mut i64>,
_redirect_url: Option<&mut CefString>,
) {
let body_len = self
.body
.lock()
.ok()
.and_then(|g| g.as_ref().map(|b| b.len()))
.unwrap_or(0);
if let Some(r) = response {
r.set_status(200);
let mime = CefString::from("text/html; charset=utf-8");
r.set_mime_type(Some(&mime));
}
if let Some(len) = response_length {
*len = body_len as i64;
}
}
#[allow(clippy::not_unsafe_ptr_arg_deref)]
fn read(
&self,
data_out: *mut u8,
bytes_to_read: ::std::os::raw::c_int,
bytes_read: Option<&mut ::std::os::raw::c_int>,
_callback: Option<&mut cef::ResourceReadCallback>,
) -> ::std::os::raw::c_int {
let guard = match self.body.lock() {
Ok(g) => g,
Err(_) => {
if let Some(br) = bytes_read {
*br = 0;
}
return 0;
}
};
let bytes = match guard.as_ref() {
Some(b) => b,
None => {
if let Some(br) = bytes_read {
*br = 0;
}
return 0;
}
};
let len = bytes.len();
let pos = self.cursor.load(Ordering::SeqCst);
if pos >= len || bytes_to_read <= 0 {
if let Some(br) = bytes_read {
*br = 0;
}
return 0;
}
let remaining = len - pos;
let to_copy = remaining.min(bytes_to_read as usize);
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr().add(pos), data_out, to_copy);
}
self.cursor.store(pos + to_copy, Ordering::SeqCst);
if let Some(br) = bytes_read {
*br = to_copy as i32;
}
1
}
}
}
const MAX_BODY_BYTES: usize = 10 * 1024 * 1024;
fn fetch_and_render(url: &str) -> String {
if url.is_empty() {
return error_page(url, "no URL to fetch (buffr-src: prefix with empty suffix)");
}
let result = (|| -> Result<Vec<u8>, String> {
let config = ureq::Agent::config_builder()
.timeout_connect(Some(std::time::Duration::from_secs(10)))
.timeout_recv_response(Some(std::time::Duration::from_secs(10)))
.build();
let agent = ureq::Agent::new_with_config(config);
let mut resp = agent
.get(url)
.call()
.map_err(|e| format!("network error: {e}"))?;
let status = resp.status().as_u16();
if !(200..300).contains(&status) {
return Err(format!("HTTP {status}"));
}
let body = resp
.body_mut()
.with_config()
.limit(MAX_BODY_BYTES as u64)
.read_to_vec()
.map_err(|e| format!("read error: {e}"))?;
Ok(body)
})();
match result {
Ok(body) => buffr_view_source::render(url, &body),
Err(err) => error_page(url, &err),
}
}
fn error_page(url: &str, reason: &str) -> String {
let escaped_url = html_escape(url);
let escaped_reason = html_escape(reason);
format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>buffr-src: error</title>
<style>
html,body{{margin:0;padding:1em 1.5em;background:#1a1b26;color:#c0caf5;
font-family:"SF Mono",Menlo,Consolas,monospace;font-size:13px;line-height:1.5}}
.err{{color:#f7768e;}}
</style>
</head>
<body>
<p class="err"><strong>Failed to fetch source for <code>{escaped_url}</code>:</strong></p>
<pre>{escaped_reason}</pre>
</body>
</html>"#
)
}
fn html_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn underlying_url_strips_prefix() {
assert_eq!(
underlying_url("buffr-src:https://example.com"),
"https://example.com"
);
}
#[test]
fn underlying_url_empty_suffix() {
assert_eq!(underlying_url("buffr-src:"), "");
}
#[test]
fn underlying_url_no_prefix_passthrough() {
assert_eq!(underlying_url("https://example.com"), "https://example.com");
}
#[test]
fn error_page_contains_url() {
let page = error_page("https://example.com", "timeout");
assert!(page.contains("https://example.com"));
assert!(page.contains("timeout"));
}
#[test]
fn html_escape_special_chars() {
assert_eq!(html_escape("<>&\"'"), "<>&"'");
}
}