use std::{
fmt::Display,
net::{IpAddr, SocketAddr},
path::PathBuf,
str::FromStr,
sync::OnceLock,
time::Duration,
};
use super::{super::PackageSource, PkgCapabilityCache};
use anyhow::Context;
use colored::Colorize;
use dialoguer::theme::ColorfulTheme;
use virtual_net::{
DynVirtualNetworking, IpCidr, IpRoute, NetworkError, Result, StreamSecurity,
UnsupportedVirtualNetworking, VirtualIcmpSocket, VirtualNetworking, VirtualRawSocket,
VirtualTcpListener, VirtualTcpSocket, VirtualUdpSocket,
};
#[derive(Debug, Clone)]
pub(crate) struct AskingNetworking {
pkg_cache_path: PathBuf,
enable: OnceLock<Result<bool>>,
capable: DynVirtualNetworking,
unsupported: DynVirtualNetworking,
}
macro_rules! call {
($self: expr, $fn_name: ident, $( $arg: expr ),* ) => {
let enable_networking = $self.enable.get_or_init(|| $self.ask_user(stringify!($fn_name)))
.map_err(|e| {
tracing::error!("{e}");
NetworkError::UnknownError
})?;
if enable_networking {
return $self.capable.$fn_name( $( $arg ),* ).await;
} else {
return $self.unsupported.$fn_name( $( $arg ),* ).await;
}
};
($self: expr, $fn_name: ident) => {
let enable_networking = $self.enable.get_or_init(|| $self.ask_user(stringify!($fn_name))).map_err(|e| {
tracing::error!("{e}");
NetworkError::UnknownError
})?;
if enable_networking {
return $self.capable.$fn_name().await;
} else {
return $self.unsupported.$fn_name().await;
}
};
}
#[derive(Debug, Clone)]
enum UserSelection {
Yes,
No,
Always,
}
impl Display for UserSelection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UserSelection::Yes => write!(f, "yes"),
UserSelection::No => write!(f, "no"),
UserSelection::Always => write!(f, "always"),
}
}
}
impl FromStr for UserSelection {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
anyhow::bail!("No input!")
}
if let Some(c) = s.trim().chars().next().map(|c| c.to_ascii_lowercase()) {
Ok(match c {
'n' => UserSelection::No,
'y' => UserSelection::Yes,
'a' => UserSelection::Always,
_ => anyhow::bail!("{s} could not be resolved as a selection"),
})
} else {
anyhow::bail!("{s} could not be resolved as a selection")
}
}
}
impl AskingNetworking {
pub(crate) fn new(pkg_cache_path: PathBuf, capable_networking: DynVirtualNetworking) -> Self {
let enable_networking = OnceLock::new();
Self {
enable: enable_networking,
capable: capable_networking,
unsupported: std::sync::Arc::new(UnsupportedVirtualNetworking::default()),
pkg_cache_path,
}
}
fn ask_user(&self, fn_name: &str) -> Result<bool> {
let theme = ColorfulTheme::default();
println!("The current package is requesting networking access.");
println!("Run the package with `--net` flag to bypass the prompt.");
match dialoguer::Input::with_theme(&theme)
.with_prompt(format!(
"Would you like to allow networking for this package? {}{}",
"".bold(),
"[options: yes/no/always]".dimmed()
))
.default(UserSelection::Always)
.interact()
.map_err(|_| NetworkError::UnknownError)?
{
UserSelection::No => Ok(false),
UserSelection::Yes => Ok(true),
UserSelection::Always => {
self.save_in_cache();
Ok(true)
}
}
}
fn save_in_cache(&self) -> Result<()> {
let capability = PkgCapabilityCache {
enable_networking: true,
};
if let Some(parent) = self.pkg_cache_path.parent() {
std::fs::create_dir_all(parent)
.context("could not create cache dir")
.map_err(|e| {
tracing::error!("e");
NetworkError::UnknownError
})?;
}
let data = serde_json::to_string_pretty(&capability).map_err(|e| {
tracing::error!("e");
NetworkError::UnknownError
})?;
std::fs::write(&self.pkg_cache_path, data).map_err(|e| {
tracing::error!("e");
NetworkError::UnknownError
})?;
tracing::trace!(path=%self.pkg_cache_path.display(), "persisted app template cache");
Ok(())
}
}
#[async_trait::async_trait]
#[allow(unused_variables)]
impl VirtualNetworking for AskingNetworking {
async fn bridge(
&self,
network: &str,
access_token: &str,
security: StreamSecurity,
) -> Result<()> {
call!(self, bridge, network, access_token, security);
}
async fn unbridge(&self) -> Result<()> {
call!(self, unbridge);
}
async fn dhcp_acquire(&self) -> Result<Vec<IpAddr>> {
call!(self, dhcp_acquire);
}
async fn ip_add(&self, ip: IpAddr, prefix: u8) -> Result<()> {
call!(self, ip_add, ip, prefix);
}
async fn ip_remove(&self, ip: IpAddr) -> Result<()> {
call!(self, ip_remove, ip);
}
async fn ip_clear(&self) -> Result<()> {
call!(self, ip_clear);
}
async fn ip_list(&self) -> Result<Vec<IpCidr>> {
call!(self, ip_list);
}
async fn mac(&self) -> Result<[u8; 6]> {
call!(self, mac);
}
async fn gateway_set(&self, ip: IpAddr) -> Result<()> {
call!(self, gateway_set, ip);
}
async fn route_add(
&self,
cidr: IpCidr,
via_router: IpAddr,
preferred_until: Option<Duration>,
expires_at: Option<Duration>,
) -> Result<()> {
call!(
self,
route_add,
cidr,
via_router,
preferred_until,
expires_at
);
}
async fn route_remove(&self, cidr: IpAddr) -> Result<()> {
call!(self, route_remove, cidr);
}
async fn route_clear(&self) -> Result<()> {
call!(self, route_clear);
}
async fn route_list(&self) -> Result<Vec<IpRoute>> {
call!(self, route_list);
}
async fn bind_raw(&self) -> Result<Box<dyn VirtualRawSocket + Sync>> {
call!(self, bind_raw);
}
async fn listen_tcp(
&self,
addr: SocketAddr,
only_v6: bool,
reuse_port: bool,
reuse_addr: bool,
) -> Result<Box<dyn VirtualTcpListener + Sync>> {
call!(self, listen_tcp, addr, only_v6, reuse_port, reuse_addr);
}
async fn bind_udp(
&self,
addr: SocketAddr,
reuse_port: bool,
reuse_addr: bool,
) -> Result<Box<dyn VirtualUdpSocket + Sync>> {
call!(self, bind_udp, addr, reuse_port, reuse_addr);
}
async fn bind_icmp(&self, addr: IpAddr) -> Result<Box<dyn VirtualIcmpSocket + Sync>> {
call!(self, bind_icmp, addr);
}
async fn connect_tcp(
&self,
addr: SocketAddr,
peer: SocketAddr,
) -> Result<Box<dyn VirtualTcpSocket + Sync>> {
call!(self, connect_tcp, addr, peer);
}
async fn resolve(
&self,
host: &str,
port: Option<u16>,
dns_server: Option<IpAddr>,
) -> Result<Vec<IpAddr>> {
call!(self, resolve, host, port, dns_server);
}
}