Skip to content

Commit

Permalink
Implemented DCC get for downloading files.
Browse files Browse the repository at this point in the history
Does not yet support sending files.
Closes #206

Added connection timeout

Fixed NOTICE format which was causing an issue. Spawn regular task instead of local for file download.

Only read number of bytes read.
  • Loading branch information
trevarj committed Aug 13, 2020
1 parent d516542 commit 0bca58e
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 4 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,11 @@ Commands start with `/` character.
Running this command in a server tab applies it to all channels of that
server. You can check your notify state in the status line.

## Server commands
- `/dcc [get] <sender> <filename>`: When you receive a DCC SEND message, you can
accept it and download the file with this command. Currently sending files
is not supported.

## Server Commands

For commands not supported by tiny as a slash command, sending the command in
the server tab will send the message directly to the server.
Expand Down
125 changes: 125 additions & 0 deletions libtiny_client/src/dcc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::str::FromStr;

#[derive(Debug)]
pub struct DCCRecord {
/// SEND or CHAT (only supporting SEND right now)
dcc_type: DCCType,
/// IP address and port
address: SocketAddr,
/// Nickname of person who wants to send
origin: String,
/// Client nickname
receiver: String,
/// Argument - filename or string "chat"
argument: String,
/// File size of file that will be sent in bytes
file_size: Option<u32>,
}

#[derive(Debug, Copy, Clone)]
pub enum DCCType {
SEND,
CHAT,
}

#[derive(Debug)]
pub struct DCCTypeParseError;

impl std::error::Error for DCCTypeParseError {}

impl fmt::Display for DCCTypeParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "DCCType could not be parsed from string.")
}
}

impl FromStr for DCCType {
type Err = DCCTypeParseError;
fn from_str(input: &str) -> Result<DCCType, DCCTypeParseError> {
match input {
"SEND" => Ok(DCCType::SEND),
"CHAT" => Ok(DCCType::CHAT),
_ => Err(DCCTypeParseError),
}
}
}

impl fmt::Display for DCCType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DCCType::CHAT => write!(f, "CHAT"),
DCCType::SEND => write!(f, "SEND"),
}
}
}

impl DCCRecord {
/// DCC type argument address port [size]
///
/// type - The connection type.
/// argument - The connectin type dependant argument.
/// address - The host address of the initiator as an integer.
/// port - The port or the socket on which the initiator expects
/// to receive the connection.
/// size - If the connection type is "SEND" (see below), then size
/// will indicate the size of the file being offered. Obsolete
/// IRCII clients do not send this, so be prepared if this is
/// not present.
/// The following DCC connection types are defined:
///
/// Type Purpose Argument
/// CHAT To carry on a semi-secure conversation the string "chat"
/// SEND To send a file to the recipient the file name
pub fn from(
origin: &str,
receiver: &str,
msg: &str,
) -> Result<DCCRecord, Box<dyn std::error::Error>> {
let mut param_iter = msg.split_whitespace();
let dcc_type: DCCType = param_iter.next().unwrap().parse()?;
let argument = param_iter.next().unwrap();
let address: u32 = param_iter.next().unwrap().parse()?;
let address_dot_decimal: Ipv4Addr = Ipv4Addr::new(
(address >> 24) as u8,
(address >> 16) as u8,
(address >> 8) as u8,
(address) as u8,
);
let port: u16 = param_iter.next().unwrap().parse()?;
let socket_address = SocketAddr::new(IpAddr::V4(address_dot_decimal), port);

let file_size = param_iter.next().and_then(|fs| fs.parse::<u32>().ok());

Ok(DCCRecord {
dcc_type,
address: socket_address,
origin: origin.to_string(),
receiver: receiver.to_string(),
argument: argument.to_string(),
file_size,
})
}

pub fn get_type(&self) -> DCCType {
self.dcc_type
}

pub fn get_addr(&self) -> &SocketAddr {
&self.address
}

pub fn get_argument(&self) -> &String {
&self.argument
}

pub fn get_file_size(&self) -> Option<u32> {
self.file_size
}

pub fn get_receiver(&self) -> &String {
&self.receiver
}
}
159 changes: 159 additions & 0 deletions libtiny_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
#![allow(clippy::unneeded_field_pattern)]
#![allow(clippy::cognitive_complexity)]

mod dcc;
mod pinger;
mod state;
mod stream;
mod utils;

pub use dcc::DCCType;
pub use libtiny_wire as wire;

use pinger::Pinger;
Expand All @@ -18,9 +20,12 @@ use futures::future::FutureExt;
use futures::stream::{Fuse, StreamExt};
use futures::{pin_mut, select};
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use std::time::Duration;
use tokio::fs::OpenOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc;
use tokio::time::timeout;

#[macro_use]
extern crate log;
Expand Down Expand Up @@ -249,6 +254,36 @@ impl Client {
pub fn get_chan_nicks(&self, chan: &str) -> Vec<String> {
self.state.get_chan_nicks(chan)
}

/// Checks if we have a stored DCCRecord for the provided parameters
/// Starts download of file if a record is found.
/// Deletes record after download attempt
/// Returns boolean if record is found
pub fn get_dcc_rec(&mut self, origin: &str, download_dir: PathBuf, file_name: &str) -> bool {
let rec = self.state.get_dcc_rec(origin, file_name);
if let Some(rec) = rec {
debug!("found rec {:?}", rec);
dcc_file_get(
&mut self.msg_chan,
*rec.get_addr(),
download_dir,
file_name.to_string(),
rec.get_receiver().to_string(),
);
true
} else {
false
}
}

/// Creates a dcc record and returns the argument (filename) and file size if provided
pub fn create_dcc_rec(
&self,
origin: &str,
msg: &str,
) -> Option<(dcc::DCCType, String, Option<u32>)> {
self.state.add_dcc_rec(origin, msg)
}
}

//
Expand Down Expand Up @@ -660,3 +695,127 @@ async fn try_connect(

None
}

/// Spawns a task to connect to given address to download
/// a file
/// https://www.irchelp.org/protocol/ctcpspec.html
fn dcc_file_get(
msg_chan: &mut mpsc::Sender<Cmd>,
addr: SocketAddr,
mut download_dir: PathBuf,
file_name: String,
target: String,
) {
let mut msg_chan = msg_chan.clone();
tokio::task::spawn(async move {
let stream = match timeout(
tokio::time::Duration::from_secs(25),
tokio::net::TcpStream::connect(addr),
)
.await
{
Err(err) => {
error!("{}", err);
let err_msg = "File transfer connection timed out.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
Ok(stream) => match stream {
Err(err) => {
error!("{}", err);
let err_msg = "File transfer connection failed.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
Ok(stream) => stream,
},
};

let (mut rx, mut tx) = tokio::io::split(stream);

// Try to create the download directory if it isn't there
if let Err(err) = tokio::fs::create_dir_all(&download_dir).await {
// Directory already exists
if err.kind() != tokio::io::ErrorKind::AlreadyExists {
error!("{}", err);
let err_msg = "Couldn't create download directory.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
}
// Add the file name to the file path
download_dir.push(&file_name);
// Create a new file for write inside the download directory
let mut file = match OpenOptions::new()
.write(true)
.create_new(true)
.open(download_dir)
.await
{
Err(err) => {
error!("{}", err);
let err_msg = format!("Couldn't create file: {}.", file_name);
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg.as_str()).await;
return;
}
Ok(file) => file,
};
// Stores total bytes received
let mut bytes_received: u32 = 0;
loop {
let mut buf: [u8; 1024] = [0; 1024];
match rx.read(&mut buf).await {
Err(err) => {
error!("{}", err);
let err_msg = "Error reading from socket for dcc.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
Ok(0) => break,
Ok(bytes) => {
bytes_received += bytes as u32;
// Write bytes to file
if let Err(err) = file.write(&buf[..bytes]).await {
error!("{}", err);
let err_msg = "Error writing to file.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
// Send back number of bytes read
let response: [u8; 4] = [
(bytes_received >> 24) as u8,
(bytes_received >> 16) as u8,
(bytes_received >> 8) as u8,
(bytes_received) as u8,
];
if let Err(err) = tx.write(&response).await {
error!("{}", err);
let err_msg = "Error responding to sender.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
}
}
}
// sync file to filesystem
if let Err(err) = file.sync_all().await {
error!("{}", err);
let err_msg = "Error syncing file to disk.";
dcc_to_msgchan(&mut msg_chan, target.as_str(), err_msg).await;
return;
}
// Write message to client that file was successfully downloaded
let success_msg = format!(
"Downloaded file: {} ({1:.2}KB)",
file_name,
(bytes_received as f32 / 1024 as f32)
);
dcc_to_msgchan(&mut msg_chan, target.as_str(), success_msg.as_str()).await;
});
}

async fn dcc_to_msgchan(msg_chan: &mut mpsc::Sender<Cmd>, target: &str, msg: &str) {
msg_chan
.try_send(Cmd::Msg(wire::notice(target, msg)))
.unwrap();
}
Loading

0 comments on commit 0bca58e

Please sign in to comment.