use std::net::IpAddr;
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use ureq::Agent;
use ureq::http::Uri;
use crate::version::VERSION;
const LIMIT: Duration = Duration::from_millis(500);
pub type Job = Box<dyn FnOnce(Duration) + Send>;
pub fn run(jobs: Vec<Job>) {
if jobs.is_empty() {
return;
}
let deadline = Instant::now() + LIMIT;
let (done, finished) = mpsc::channel();
let count = jobs.len();
for job in jobs {
let done = done.clone();
thread::spawn(move || {
job(deadline.saturating_duration_since(Instant::now()));
let _ = done.send(());
});
}
for _ in 0..count {
let left = deadline.saturating_duration_since(Instant::now());
if finished.recv_timeout(left).is_err() {
return;
}
}
}
pub fn post_json(url: &str, body: &[u8], left: Duration) -> Option<u16> {
let response = agent(left, &format!("loft/{VERSION}"))
.post(allowed(url)?)
.header("Content-Type", "application/json")
.send(body)
.ok()?;
Some(response.status().as_u16())
}
pub fn get_json(url: &str, left: Duration) -> Option<(u16, Vec<u8>)> {
let mut response = agent(left, "loft")
.get(allowed(url)?)
.header("Accept", "application/vnd.github+json")
.call()
.ok()?;
let status = response.status().as_u16();
let body = response
.body_mut()
.with_config()
.limit(1 << 20)
.read_to_vec()
.ok()?;
Some((status, body))
}
fn agent(left: Duration, user_agent: &str) -> Agent {
Agent::config_builder()
.http_status_as_error(false)
.timeout_global(Some(left))
.max_redirects(0)
.user_agent(user_agent)
.build()
.into()
}
fn allowed(url: &str) -> Option<Uri> {
let uri: Uri = url.parse().ok()?;
let host = uri.host()?.trim_start_matches('[').trim_end_matches(']');
let loopback = host == "localhost"
|| host
.parse::<IpAddr>()
.is_ok_and(|address| address.is_loopback());
match uri.scheme_str()? {
"https" => Some(uri),
"http" if loopback => Some(uri),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::allowed;
#[test]
fn https_is_allowed_anywhere_and_http_only_on_the_loopback_interface() {
for url in [
"https://askloft.ai/api/telemetry",
"https://api.github.com",
"http://127.0.0.1:9/api/telemetry",
"http://127.0.0.2:8080",
"http://localhost:3000/x",
"http://[::1]:8080/",
] {
assert!(allowed(url).is_some(), "{url}");
}
for url in [
"http://askloft.ai/api/telemetry",
"http://10.0.0.5:8080",
"http://localhost.example.com/",
"http://[2001:db8::1]:80/",
"ftp://127.0.0.1/",
"127.0.0.1:9",
"http://127.0.0.1:9@askloft.ai/api/telemetry",
"http://localhost@10.0.0.5/",
] {
assert!(allowed(url).is_none(), "{url}");
}
}
}