use crate::*;
use reqwest::{Client, Url};
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
use tempfile::NamedTempFile;
use tracing::{debug, trace, warn};
type FileKey = (ModuleKey, FileKind);
pub struct HttpSymbolSupplier {
#[allow(clippy::type_complexity)]
cached_file_paths: Mutex<HashMap<FileKey, CachedOperation<(PathBuf, Option<Url>), FileError>>>,
client: Client,
urls: Vec<Url>,
local: SimpleSymbolSupplier,
cache: PathBuf,
tmp: PathBuf,
}
impl HttpSymbolSupplier {
pub fn new(
urls: Vec<String>,
cache: PathBuf,
tmp: PathBuf,
mut local_paths: Vec<PathBuf>,
timeout: Duration,
) -> HttpSymbolSupplier {
let client = Client::builder().timeout(timeout).build().unwrap();
let urls = urls
.into_iter()
.filter_map(|mut u| {
if !u.ends_with('/') {
u.push('/');
}
Url::parse(&u).ok()
})
.collect();
local_paths.push(cache.clone());
let local = SimpleSymbolSupplier::new(local_paths);
let cached_file_paths = Mutex::default();
HttpSymbolSupplier {
client,
cached_file_paths,
urls,
local,
cache,
tmp,
}
}
#[tracing::instrument(level = "trace", skip(self, module), fields(module = crate::basename(&module.code_file())))]
pub async fn locate_file_internal(
&self,
module: &(dyn Module + Sync),
file_kind: FileKind,
) -> Result<(PathBuf, Option<Url>), FileError> {
let k = file_key(module, file_kind);
let file_once = self
.cached_file_paths
.lock()
.unwrap()
.entry(k)
.or_default()
.clone();
file_once
.get_or_init(|| async {
if let Ok(path) = self.local.locate_file(module, file_kind).await {
return Ok((path, None));
}
if let Some(lookup) = lookup(module, file_kind) {
for url in &self.urls {
let fetch =
fetch_lookup(&self.client, url, &lookup, &self.cache, &self.tmp).await;
if let Ok((path, url)) = fetch {
return Ok((path, url));
}
}
if cfg!(feature = "mozilla_cab_symbols") {
for url in &self.urls {
let fetch = fetch_cab_lookup(
&self.client,
url,
&lookup,
&self.cache,
&self.tmp,
)
.await;
if let Ok((path, url)) = fetch {
return Ok((path, url));
}
}
}
}
Err(FileError::NotFound)
})
.await
.clone()
}
}
fn file_key(module: &(dyn Module + Sync), file_kind: FileKind) -> FileKey {
(module_key(module), file_kind)
}
fn create_cache_file(tmp_path: &Path, final_path: &Path) -> io::Result<NamedTempFile> {
let base = final_path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::Other,
format!("Bad cache path: {:?}", final_path),
)
})?;
fs::create_dir_all(base)?;
NamedTempFile::new_in(tmp_path)
}
fn commit_cache_file(mut temp: NamedTempFile, final_path: &Path, url: &Url) -> io::Result<()> {
let cache_metadata = format!("INFO URL {}\n", url);
temp.write_all(cache_metadata.as_bytes())?;
if final_path.exists() {
fs::remove_file(final_path)?;
}
temp.persist_noclobber(final_path)?;
Ok(())
}
async fn fetch_symbol_file(
client: &Client,
base_url: &Url,
module: &(dyn Module + Sync),
cache: &Path,
tmp: &Path,
) -> Result<SymbolFile, SymbolError> {
trace!("HttpSymbolSupplier trying symbol server {}", base_url);
let sym_lookup = breakpad_sym_lookup(module).ok_or(SymbolError::MissingDebugFileOrId)?;
let mut url = base_url
.join(&sym_lookup.server_rel)
.map_err(|_| SymbolError::NotFound)?;
let code_id = module.code_identifier().unwrap_or_default();
url.query_pairs_mut()
.append_pair("code_file", crate::basename(&module.code_file()))
.append_pair("code_id", code_id.as_str());
debug!("Trying {}", url);
let res = client
.get(url.clone())
.send()
.await
.and_then(|res| res.error_for_status())
.map_err(|_| SymbolError::NotFound)?;
let final_cache_path = cache.join(sym_lookup.cache_rel);
let mut temp = create_cache_file(tmp, &final_cache_path)
.map_err(|e| {
warn!("Failed to save symbol file in local disk cache: {}", e);
})
.ok();
let mut symbol_file = SymbolFile::parse_async(res, |data| {
if let Some(file) = temp.as_mut() {
if let Err(e) = file.write_all(data) {
warn!("Failed to save symbol file in local disk cache: {}", e);
temp = None;
}
}
})
.await?;
symbol_file.url = Some(url.to_string());
if let Some(temp) = temp {
let _ = commit_cache_file(temp, &final_cache_path, &url).map_err(|e| {
warn!("Failed to save symbol file in local disk cache: {}", e);
});
}
Ok(symbol_file)
}
async fn fetch_lookup(
client: &Client,
base_url: &Url,
lookup: &FileLookup,
cache: &Path,
tmp: &Path,
) -> Result<(PathBuf, Option<Url>), SymbolError> {
let url = base_url
.join(&lookup.server_rel)
.map_err(|_| SymbolError::NotFound)?;
debug!("Trying {}", url);
let mut res = client
.get(url.clone())
.send()
.await
.and_then(|res| res.error_for_status())
.map_err(|_| SymbolError::NotFound)?;
let final_cache_path = cache.join(&lookup.cache_rel);
let mut temp = create_cache_file(tmp, &final_cache_path)?;
while let Some(chunk) = res
.chunk()
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?
{
temp.write_all(&chunk[..])?;
}
temp.persist_noclobber(&final_cache_path)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
trace!("symbols: fetched native binary: {}", lookup.cache_rel);
Ok((final_cache_path, Some(url)))
}
#[cfg(feature = "mozilla_cab_symbols")]
async fn fetch_cab_lookup(
client: &Client,
base_url: &Url,
lookup: &FileLookup,
cache: &Path,
tmp: &Path,
) -> Result<(PathBuf, Option<Url>), FileError> {
let cab_lookup = moz_lookup(lookup.clone());
let url = base_url
.join(&cab_lookup.server_rel)
.map_err(|_| FileError::NotFound)?;
debug!("Trying {}", url);
let res = client
.get(url.clone())
.send()
.await
.and_then(|res| res.error_for_status())
.map_err(|_| FileError::NotFound)?;
let cab_bytes = res.bytes().await.map_err(|_| FileError::NotFound)?;
let final_cache_path =
unpack_cabinet_file(&cab_bytes, lookup, cache, tmp).map_err(|_| FileError::NotFound)?;
trace!("symbols: fetched native binary: {}", lookup.cache_rel);
Ok((final_cache_path, Some(url)))
}
#[cfg(not(feature = "mozilla_cab_symbols"))]
async fn fetch_cab_lookup(
_client: &Client,
_base_url: &Url,
_lookup: &FileLookup,
_cache: &Path,
_tmp: &Path,
) -> Result<(PathBuf, Option<Url>), FileError> {
Err(FileError::NotFound)
}
#[cfg(feature = "mozilla_cab_symbols")]
pub fn unpack_cabinet_file(
buf: &[u8],
lookup: &FileLookup,
cache: &Path,
tmp: &Path,
) -> Result<PathBuf, std::io::Error> {
trace!("symbols: unpacking CAB file: {}", lookup.cache_rel);
use cab::Cabinet;
use std::io::Cursor;
fn get_cabinet_file(
cab: &Cabinet<Cursor<&[u8]>>,
file_name: &str,
) -> Result<String, std::io::Error> {
for folder in cab.folder_entries() {
for file in folder.file_entries() {
let cab_file_name = file.name();
if cab_file_name.ends_with(file_name) {
return Ok(cab_file_name.to_string());
}
}
}
Err(std::io::Error::from(std::io::ErrorKind::NotFound))
}
let final_cache_path = cache.join(&lookup.cache_rel);
let cursor = Cursor::new(buf);
let mut cab = Cabinet::new(cursor)?;
let file_name = final_cache_path.file_name().unwrap().to_string_lossy();
let cab_file = get_cabinet_file(&cab, &file_name)?;
let mut reader = cab.read_file(&cab_file)?;
let mut temp = create_cache_file(tmp, &final_cache_path)?;
std::io::copy(&mut reader, &mut temp)?;
temp.persist_noclobber(&final_cache_path)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
Ok(final_cache_path)
}
#[cfg(feature = "dump_syms")]
async fn dump_syms(
inputs: &[Result<(PathBuf, Option<Url>), FileError>],
output: &Path,
) -> Result<(), SymbolError> {
use dump_syms::dumper;
if !inputs.iter().any(|input| input.is_ok()) {
return Err(SymbolError::NotFound);
}
trace!("symbols: found native symbols! running dump_syms...");
let mut source_file = None;
let mut urls = vec![];
for (input_path, input_url) in inputs.iter().flatten() {
if let Some(url) = input_url {
urls.push(url.to_string());
}
source_file = Some(input_path);
}
if let Err(e) = dumper::single_file(
&dumper::Config {
output: dumper::Output::File(output.to_string_lossy()[..].into()),
symbol_server: None,
debug_id: None,
code_id: None,
arch: "unknown",
num_jobs: 2, check_cfi: false,
mapping_var: None,
mapping_src: None,
mapping_dest: None,
mapping_file: None,
emit_inlines: true,
},
&source_file.unwrap().to_string_lossy()[..],
) {
debug!("symbols: dump_syms failed: {}", e);
Err(std::io::Error::new(std::io::ErrorKind::Other, e))?;
}
{
let mut temp = std::fs::File::options().append(true).open(output)?;
let mut cache_metadata = String::new();
for url in urls {
cache_metadata.push_str(&format!("INFO URL {}\n", url));
}
temp.write_all(cache_metadata.as_bytes())?;
}
Ok(())
}
#[cfg(not(feature = "dump_syms"))]
async fn dump_syms(
_inputs: &[Result<(PathBuf, Option<Url>), FileError>],
_output: &Path,
) -> Result<(), SymbolError> {
Ok(())
}
#[async_trait]
impl SymbolSupplier for HttpSymbolSupplier {
#[tracing::instrument(name = "symbols", level = "trace", skip_all, fields(file = crate::basename(&module.code_file())))]
async fn locate_symbols(
&self,
module: &(dyn Module + Sync),
) -> Result<SymbolFile, SymbolError> {
let local_result = self.local.locate_symbols(module).await;
if !matches!(local_result, Err(SymbolError::NotFound)) {
return local_result;
}
trace!("HttpSymbolSupplier search (SimpleSymbolSupplier found nothing)");
for url in &self.urls {
let sym = fetch_symbol_file(&self.client, url, module, &self.cache, &self.tmp).await;
match sym {
Ok(file) => {
trace!("HttpSymbolSupplier parsed file!");
return Ok(file);
}
Err(e) => {
trace!("HttpSymbolSupplier failed: {}", e);
}
}
}
if cfg!(feature = "dump_syms") {
trace!("symbols: trying to fetch native symbols");
let mut native_artifacts = vec![];
native_artifacts.push(self.locate_file_internal(module, FileKind::Binary).await);
native_artifacts.push(
self.locate_file_internal(module, FileKind::ExtraDebugInfo)
.await,
);
let sym_lookup =
breakpad_sym_lookup(module).ok_or(SymbolError::MissingDebugFileOrId)?;
let output = self.cache.join(sym_lookup.cache_rel);
if dump_syms(&native_artifacts, &output).await.is_ok() {
trace!("symbols: dump_syms successful! using local result");
if let Ok(local_result) = self.local.locate_symbols(module).await {
return Ok(local_result);
} else {
warn!("dump_syms succeeded, but there was no symbol file in the cache?");
}
}
}
Err(SymbolError::NotFound)
}
async fn locate_file(
&self,
module: &(dyn Module + Sync),
file_kind: FileKind,
) -> Result<PathBuf, FileError> {
self.locate_file_internal(module, file_kind)
.await
.map(|(path, _url)| path)
}
}