use super::super::*;
impl DaemonState {
pub(crate) async fn resolve_peer_name(&self, name: &str) -> Option<EndpointId> {
let suffix = format!(".{}", crate::DNS_DOMAIN);
let qualified = if name.ends_with(&suffix) {
name.to_string()
} else {
format!("{name}{suffix}")
};
if let Some((ip, _)) = dns::resolve_name(&qualified, &suffix, &self.hostname_table).await {
if let Some(route) = self.peers.lookup_v4(&ip) {
return Some(route.endpoint_id);
}
for entry in self.networks.iter() {
let state = entry.value().state.read().unwrap();
if let Some(m) = state.members.all().iter().find(|m| m.ip == ip) {
return Some(m.identity);
}
}
}
self.resolve_short_id_any_network(name)
}
pub(crate) async fn send_file(&self, path: &str, peer: &str) -> IpcMessage {
let peer_id = match self.resolve_peer_name(peer).await {
Some(id) => id,
None => {
return IpcMessage::Error {
message: format!("unknown peer '{peer}'"),
};
}
};
let file_path = std::path::Path::new(path);
let file_bytes = match std::fs::read(file_path) {
Ok(b) => b,
Err(e) => {
return IpcMessage::Error {
message: format!("cannot read '{}': {e}", file_path.display()),
};
}
};
let filename = file_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "file".to_string());
let size = file_bytes.len() as u64;
let mime_type = guess_mime_type(&filename);
let hash = blake3::hash(&file_bytes);
if let Err(e) = self.blob_store.blobs().add_slice(&file_bytes).await {
return IpcMessage::Error {
message: format!("blob store error: {e}"),
};
}
let msg = control::ControlMsg::FileOffer {
from: self.endpoint.id(),
filename: filename.clone(),
size,
mime_type: mime_type.clone(),
blob_hash: hash,
};
match transport::connect_to_peer_with_alpn(&self.endpoint, peer_id, transport::FILES_ALPN)
.await
{
Ok(conn) => match conn.open_bi().await {
Ok((mut send, _)) => {
if let Err(e) = control::send_msg(&mut send, &msg).await {
return IpcMessage::Error {
message: format!("failed to send offer: {e}"),
};
}
let _ = tokio::time::timeout(Duration::from_secs(5), conn.closed()).await;
}
Err(e) => {
return IpcMessage::Error {
message: format!("failed to open stream: {e}"),
};
}
},
Err(e) => {
return IpcMessage::Error {
message: format!("cannot reach peer '{peer}': {e}"),
};
}
}
IpcMessage::Ok {
message: format!("offered {} ({}) to {}", filename, format_size(size), peer),
}
}
pub(crate) fn list_files(&self) -> IpcMessage {
let pending = self.protocol_router.pending_files.lock().unwrap();
let files = pending
.iter()
.map(|f| ipc::PendingFileInfo {
id: f.id,
from: f.from.fmt_short().to_string(),
filename: f.filename.clone(),
size: f.size,
mime_type: f.mime_type.clone(),
})
.collect();
IpcMessage::FileList { files }
}
pub(crate) async fn accept_file(
&self,
id: u64,
output: Option<String>,
peer_cred: Option<(u32, u32)>,
) -> IpcMessage {
let pending_file = {
let mut pending = self.protocol_router.pending_files.lock().unwrap();
let idx = pending.iter().position(|f| f.id == id);
match idx {
Some(i) => pending.remove(i),
None => {
return IpcMessage::Error {
message: format!("no pending file with id {id}"),
};
}
}
};
let blob_hash = iroh_blobs::Hash::from_bytes(*pending_file.blob_hash.as_bytes());
let conn = match transport::connect_to_peer_with_alpn(
&self.endpoint,
pending_file.from,
iroh_blobs::protocol::ALPN,
)
.await
{
Ok(c) => c,
Err(e) => {
return IpcMessage::Error {
message: format!("cannot reach sender: {e}"),
};
}
};
if let Err(e) = self
.blob_store
.remote()
.fetch(conn, iroh_blobs::HashAndFormat::raw(blob_hash))
.await
{
return IpcMessage::Error {
message: format!("blob fetch failed: {e}"),
};
}
let bytes = match self.blob_store.blobs().get_bytes(blob_hash).await {
Ok(b) => b,
Err(e) => {
return IpcMessage::Error {
message: format!("blob read failed: {e}"),
};
}
};
let dir = match output {
Some(ref p) => PathBuf::from(p),
None => dirs::download_dir().unwrap_or_else(|| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("Downloads")
}),
};
if let Err(e) = std::fs::create_dir_all(&dir) {
return IpcMessage::Error {
message: format!("cannot create directory '{}': {e}", dir.display()),
};
}
let dest = dir.join(&pending_file.filename);
if let Err(e) = std::fs::write(&dest, &bytes) {
return IpcMessage::Error {
message: format!("write failed: {e}"),
};
}
if let Some((uid, gid)) = peer_cred {
use std::os::unix::ffi::OsStrExt;
if let Ok(c) = std::ffi::CString::new(dest.as_os_str().as_bytes()) {
unsafe { libc::chown(c.as_ptr(), uid, gid) };
}
if let Ok(c) = std::ffi::CString::new(dir.as_os_str().as_bytes()) {
unsafe { libc::chown(c.as_ptr(), uid, gid) };
}
}
IpcMessage::Ok {
message: format!("saved to {}", dest.display()),
}
}
pub(crate) fn start_pairing(&self) -> IpcMessage {
let secret: [u8; 32] = rand::random();
let endpoint_id = self.endpoint.id();
let mut ticket_bytes = Vec::with_capacity(64);
ticket_bytes.extend_from_slice(endpoint_id.as_bytes());
ticket_bytes.extend_from_slice(&secret);
let ticket = bs58::encode(&ticket_bytes).into_string();
*self.pairing_secret.lock().unwrap() = Some(secret);
IpcMessage::PairingTicket { ticket }
}
pub(crate) async fn pair_with_device(&self, endpoint_id: EndpointId, secret: Vec<u8>) -> IpcMessage {
let addr: iroh::EndpointAddr = endpoint_id.into();
let conn = match self.endpoint.connect(addr, PAIR_ALPN).await {
Ok(c) => c,
Err(e) => {
return IpcMessage::Error {
message: format!("failed to connect to primary device: {e}"),
};
}
};
let (mut send, mut recv) = match conn.open_bi().await {
Ok(pair) => pair,
Err(e) => {
return IpcMessage::Error {
message: format!("failed to open stream: {e}"),
};
}
};
let secret_arr: [u8; 32] = match secret.try_into() {
Ok(a) => a,
Err(_) => {
return IpcMessage::Error {
message: "invalid secret length".to_string(),
};
}
};
let request = control::PairMsg::Request {
secret: secret_arr,
device_pubkey: self.endpoint.id(),
};
let request_bytes = match rmp_serde::to_vec_named(&request) {
Ok(b) => b,
Err(e) => {
return IpcMessage::Error {
message: format!("failed to encode pair request: {e}"),
};
}
};
let len = (request_bytes.len() as u32).to_be_bytes();
if let Err(e) = send.write_all(&len).await {
return IpcMessage::Error {
message: format!("failed to send pair request: {e}"),
};
}
if let Err(e) = send.write_all(&request_bytes).await {
return IpcMessage::Error {
message: format!("failed to send pair request: {e}"),
};
}
let mut len_buf = [0u8; 4];
if let Err(e) = recv.read_exact(&mut len_buf).await {
return IpcMessage::Error {
message: format!("failed to read pair response: {e}"),
};
}
let body_len = u32::from_be_bytes(len_buf) as usize;
let mut body = vec![0u8; body_len];
if let Err(e) = recv.read_exact(&mut body).await {
return IpcMessage::Error {
message: format!("failed to read pair response body: {e}"),
};
}
let response: control::PairMsg = match rmp_serde::from_slice(&body) {
Ok(r) => r,
Err(e) => {
return IpcMessage::Error {
message: format!("failed to decode pair response: {e}"),
};
}
};
match response {
control::PairMsg::Response { cert } => {
if !cert.verify() {
return IpcMessage::Error {
message: "received invalid device certificate".to_string(),
};
}
if let Err(e) = identity::store_device_cert(&cert) {
return IpcMessage::Error {
message: format!("failed to store device certificate: {e}"),
};
}
IpcMessage::PairingComplete {
user_identity: cert.user_identity,
}
}
_ => IpcMessage::Error {
message: "unexpected pairing response".to_string(),
},
}
}
}