/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Interface to HTTP requests/responses which will transparently use various backends to send the //! data. //! //! This is far from a general implementation, instead implementing the sort of requests that we //! need.
usecrate::config::installation_program_path; usecrate::std::{ self, env,
fs::{File, OpenOptions},
io::{Read, Seek},
mem::ManuallyDrop,
path::{Path, PathBuf},
process::Child,
sync::atomic::{AtomicUsize, Ordering::Relaxed},
}; use anyhow::Context; use once_cell::sync::Lazy; use serde::Serialize;
#[cfg(mock)]
mock_key! { /// The outer Result is for RequestBuilder::build(), the inner is for Request::send(). pubstruct MockHttp => Box<dynFn(&RequestBuilder, &str) -> std::io::Result<std::io::Result<Vec<u8>>> + Send + Sync>
}
#[cfg(mock)] impl MockHttp { /// If returned from a MockHttp callback, other transports will be attempted. #[allow(unused)] pubfn try_others() -> std::io::Result<std::io::Result<Vec<u8>>> {
Err(std::io::ErrorKind::Interrupted.into())
}
}
/// The user agent used by this application. pubfn user_agent() -> &'static str { static USER_AGENT: Lazy<String> = Lazy::new(|| {
format!( "{}/{} ({} {})",
env!("CARGO_PKG_NAME"),
mozbuild::config::MOZ_APP_VERSION,
std::env::consts::OS,
std::env::consts::ARCH,
)
});
&*USER_AGENT
}
std::mock::mocked_static! { /// How many times the background task may fail before discontinuing use. /// /// The background task may repeatedly fail for many reasons, for example due to a crash or /// misconfigured network settings. static BACKGROUND_TASK_ATTEMPTS: BackgroundTaskAttempts = BackgroundTaskAttempts::new(2);
/// Types of requests that can be created. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(tag = "type")] pubenum RequestBuilder<'a> { /// Send a POST with multiple mime parts.
MimePost { parts: Vec<MimePart<'a>> }, /// Send a POST.
Post {
body: &'a [u8],
headers: &'a [(String, String)],
},
}
/// A single mime part to send. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] pubstruct MimePart<'a> { pub name: &'a str, pub content: MimePartContent<'a>, #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] pub mime_type: Option<&'a str>,
}
/// The content of a mime part. #[derive(Debug, Clone, PartialEq, Eq, Serialize)] #[serde(tag = "type", content = "value")] pubenum MimePartContent<'a> { /// Send a file's contents.
File(&'a Path), /// Send a specific string as the contents.
String(&'a str),
}
/// A request that is ready to be sent. pubenum Request<'a> {
BackgroundTaskChild {
child: Child,
file: TempRequestFile,
builder: RequestBuilder<'a>,
url: &'a str,
},
CurlChild {
child: Child,
stdin: Option<Box<dyn Read + Send + 'static>>,
},
LibCurl {
easy: super::libcurl::Easy<'static>,
}, #[cfg(mock)]
Mock {
response: std::io::Result<Vec<u8>>,
},
}
impl<'a> RequestBuilder<'a> { /// Build the request with the given url. pubfn build(&self, url: &'a str) -> std::io::Result<Request<'a>> {
log::debug!("starting request to {url}: {self:?}");
// When mocking is enabled, check for that first. #[cfg(mock)] iflet Some(r) = self.try_send_with_mock(url) { return r;
}
// First we try to invoke a firefox background task to send the request. This is // preferrable because it will respect the user's network settings, however it is less // reliable if the cause of our crash is a bug in early firefox startup or network code. if BACKGROUND_TASK_ATTEMPTS.should_attempt() { matchself.send_with_background_task(url) {
Ok(r) => return Ok(r),
Err(e) => {
log::info!( "failed to invoke background task ({e}), falling back to curl backend"
); // A failure to spawn the background task more than likely indicates it will // never work in the future.
BACKGROUND_TASK_ATTEMPTS.drain();
}
};
}
self.send_with_curl(url)
}
/// Send the request with the firefox `crashreporterNetworkBackend` background task. fn send_with_background_task(&self, url: &'a str) -> std::io::Result<Request<'a>> { let path = installation_program_path(mozbuild::config::MOZ_APP_NAME); letmut cmd = crate::process::background_command(path);
cmd.args(["--backgroundtask", "crashreporterNetworkBackend"]);
cmd.arg(url);
cmd.arg(user_agent()); // Disable crash reporting in the background task. We don't want a crash in the background // task to launch another crash reporter flow. See bugs 1991491/1987145.
cmd.env("MOZ_CRASHREPORTER_DISABLE", "1")
.env_remove("MOZ_CRASHREPORTER");
/// Send the request with the curl backend. pubfn send_with_curl(&self, url: &str) -> std::io::Result<Request<'static>> { // Windows 10+ and macOS 10.15+ contain `curl` 7.64.1+ as a system-provided executable, so // `send_with_curl_executable` should not fail. // // Linux distros generally do not contain `curl`, but `libcurl` is very likely to be // incidentally installed (if not outright part of the distro base packages). Based on a // cursory look at the debian repositories as an exemplar, the curl executable (rather than // library) is much less likely to be incidentally installed. // // For uniformity, we always will try the curl executable first, then try libcurl if that // fails.
let curl_err = matchself.send_with_curl_executable(url) {
Ok(r) => return Ok(r),
Err(e) => e,
};
// When mocking is enabled, default to _not_ using libcurl (because there is no mock // interface; it will really use libcurl). However we add a hook to use libcurl for tests // which want to send real data to some server (which may be a local test server). #[cfg(mock)] if !crate::std::mock::try_hook(false, "use_system_libcurl") {
log::error!("use_system_libcurl not enabled and curl failed: {curl_err}");
panic!("no mock handler available to build http request");
}
log::info!("failed to invoke curl ({curl_err}), trying libcurl");
self.send_with_libcurl(url)
}
/// Send the request with the `curl` executable. fn send_with_curl_executable(&self, url: &str) -> std::io::Result<Request<'static>> { letmut cmd = crate::process::background_command("curl"); letmut stdin: Option<Box<dyn Read + Send + 'static>> = None;
impl Drop for TempRequestFile { fn drop(&mutself) { // # Safety // We do not use self.file after this unsafe { ManuallyDrop::drop(&mutself.file) }; let _ = std::fs::remove_file(&self.path);
}
}
impl MimePart<'_> { fn curl_command_args(
&self,
cmd: &mutcrate::std::process::Command,
stdin: &mut Option<Box<dyn Read + Send + 'static>>,
) -> std::io::Result<()> { use std::fmt::Write; letmut formarg = format!("{}=", self.name); matchself.content {
MimePartContent::File(f) => {
write!(formarg, "@{}", CurlQuote(&f.display().to_string())).unwrap()
}
MimePartContent::String(s) => { // `@-` causes the data to be read from stdin, which is desirable to // not have to worry about process argument string length limitations // (though they are generally pretty high limits).
write!(formarg, "@-").unwrap(); if stdin
.replace(Box::new(std::io::Cursor::new(s.to_owned())))
.is_some()
{ return Err(std::io::Error::other( "only one MimePartContent::String supported",
));
}
}
} iflet Some(filename) = self.filename {
write!(formarg, ";filename={}", filename).unwrap();
} iflet Some(mime_type) = self.mime_type {
write!(formarg, ";type={}", mime_type).unwrap();
}
impl Request<'_> { /// Send the request, returning the response body (if any). pubfn send(self) -> anyhow::Result<Vec<u8>> {
Ok(matchself { Self::BackgroundTaskChild {
child, mut file,
builder,
url,
} => {
(move || { let output = child
.wait_with_output()
.context("failed to wait on background task process")?;
if !output.status.success() {
BACKGROUND_TASK_ATTEMPTS.failed(); if !BACKGROUND_TASK_ATTEMPTS.should_attempt() {
log::info!("the background task process has exceeded the acceptable number of failures and will no longer be used");
}
anyhow::bail!( "process failed (exit status {}) with stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
}
file.rewind().context("failed to rewind response file")?; letmut ret = Vec::new();
file.read_to_end(&mut ret)
.context("failed to read response file")?;
Ok(ret)
})()
.or_else(|e| { // If any error occurs, we try again with the curl backend. In theory we // could do this specifically if the process returns a failure exit code, // however since we want to give the best effort in sending the request, we // do it for any failure.
log::error!("background task error: {e:#}");
log::info!("falling back to curl backend");
builder.send_with_curl(url).context("curl error")?.send()
})?
} Self::CurlChild { mut child, stdin } => { iflet Some(mut stdin) = stdin { letmut child_stdin = child
.stdin
.take()
.context("failed to get curl process stdin")?;
std::io::copy(&mut stdin, &mut child_stdin)
.context("failed to write to stdin of curl process")?; // stdin is dropped at the end of this scope so that the stream gets an EOF, // otherwise curl will wait for more input.
} let output = child
.wait_with_output()
.context("failed to wait on curl process")?;
anyhow::ensure!(
output.status.success(), "process failed (exit status {}) with stderr: {}",
output.status,
String::from_utf8_lossy(&output.stderr)
);
output.stdout
} Self::LibCurl { easy } => { let response = easy.perform()?; let response_code = easy.get_response_code()?;
/// Quote a string per https://curl.se/docs/manpage.html#-F. /// That is, add quote characters and escape " and \ with backslashes. struct CurlQuote<'a>(&'a str);
impl CurlQuote<'_> { fn quoted(&self) -> String { let quote = std::iter::once(&b'"'); let escaped = self.0.as_bytes().iter().flat_map(|b| match b {
b'"' => br#"\""#.as_slice(),
b'\\' => br#"\\"#.as_slice(),
other => std::slice::from_ref(other),
}); let bytes = quote.clone().chain(escaped).chain(quote).copied().collect(); // # Safety // The source bytes came from a `str`, so must have been valid utf8. We are inserting valid // utf8 characters (quotes and backslashes, which are single bytes in utf8) at character // boundaries (at the beginning/end of the string and before other backslash/quote // characters). unsafe { String::from_utf8_unchecked(bytes) }
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.