Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented DCC get for downloading files. #209

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,13 @@ 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.

- `/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.

- `/quit`: Quit

## Server commands
## 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
8 changes: 7 additions & 1 deletion crates/libtiny_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ libtiny_wire = { path = "../libtiny_wire" }
log = "0.4"
native-tls = { version = "0.2", optional = true }
rustls-native-certs = { version = "0.5", optional = true }
tokio = { version = "1.6.1", default-features = false, features = ["net", "rt", "io-util", "macros"] }
tokio = { version = "1.6.1", default-features = false, features = [
"net",
"rt",
"io-util",
"macros",
"fs",
] }
tokio-native-tls = { version = "0.3", optional = true }
tokio-rustls = { version = "0.22", optional = true }
tokio-stream = { version = "0.1.6" }
172 changes: 172 additions & 0 deletions crates/libtiny_client/src/dcc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
use std::fmt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::num::ParseIntError;
use std::str::FromStr;

#[derive(Debug, PartialEq)]
pub(crate) 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, PartialEq)]
pub enum DccType {
SEND,
CHAT,
}

#[derive(Debug)]
pub struct DccRecordInfo {
/// SEND or CHAT (only supporting SEND right now)
pub dcc_type: DccType,
/// Argument - filename or string "chat"
pub argument: String,
/// File size of file that will be sent in bytes
pub file_size: Option<u32>,
}

#[derive(Debug)]
pub enum DccParseError {
DccType,
DccRecord,
}

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

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

impl From<ParseIntError> for DccParseError {
fn from(_: ParseIntError) -> Self {
DccParseError::DccRecord
}
}

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

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 new(origin: &str, receiver: &str, msg: &str) -> Result<DccRecord, DccParseError> {
// Example msg: SEND "SearchBot_results_for_ python.txt.zip" 2907707975 3078 24999
let mut param_iter: Vec<&str> = msg.split_whitespace().collect();
let dcc_type: DccType = param_iter.remove(0).parse()?;
let file_size = param_iter.pop().and_then(|fs| fs.parse::<u32>().ok());
let port: u16 = param_iter.pop().ok_or(DccParseError::DccRecord)?.parse()?;

let address: u32 = param_iter.pop().ok_or(DccParseError::DccRecord)?.parse()?;
let address_dot_decimal: Ipv4Addr = Ipv4Addr::new(
(address >> 24) as u8,
(address >> 16) as u8,
(address >> 8) as u8,
(address) as u8,
);

let socket_address = SocketAddr::new(IpAddr::V4(address_dot_decimal), port);

let argument = param_iter.join("");
let argument = argument.trim_start_matches('"').trim_end_matches('"');

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

pub(crate) fn info(&self) -> DccRecordInfo {
DccRecordInfo {
dcc_type: self.dcc_type,
argument: self.argument.clone(),
file_size: self.file_size,
}
}

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

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

pub(crate) fn receiver(&self) -> &String {
&self.receiver
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn can_parse() {
let expected = DccRecord {
dcc_type: DccType::SEND,
address: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(173, 80, 26, 71)), 3078),
origin: "Origin".to_string(),
receiver: "me".to_string(),
argument: "SearchBot_results_for_python.txt.zip".to_string(),
file_size: Some(24999),
};
let s = r#"SEND "SearchBot_results_for_ python.txt.zip" 2907707975 3078 24999"#;
let r = DccRecord::new("Origin", "me", s);
assert_eq!(expected, r.unwrap());

let s = r#"SEND SearchBot_results_for_python.txt.zip 2907707975 3078 24999"#;
let r = DccRecord::new("Origin", "me", s);
assert_eq!(expected, r.unwrap());
}
}
156 changes: 156 additions & 0 deletions crates/libtiny_client/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(clippy::unneeded_field_pattern)]
#![allow(clippy::cognitive_complexity)]

mod dcc;
mod pinger;
mod state;
mod stream;
Expand All @@ -14,15 +15,20 @@ use state::State;
use stream::{Stream, StreamError};

use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use std::time::Duration;

use futures_util::future::FutureExt;
use tokio::fs::OpenOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::mpsc;
use tokio::time::timeout;
use tokio::{pin, select};
use tokio_stream::wrappers::ReceiverStream;
use tokio_stream::StreamExt;

pub use crate::dcc::{DccRecordInfo, DccType};

#[macro_use]
extern crate log;

Expand Down Expand Up @@ -255,6 +261,32 @@ impl Client {
pub fn get_chan_nicks(&self, chan: &ChanNameRef) -> 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.address(),
download_dir,
file_name.to_string(),
rec.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<DccRecordInfo> {
self.state.add_dcc_rec(origin, msg)
}
}

//
Expand Down Expand Up @@ -676,3 +708,127 @@ async fn try_connect<S: StreamExt<Item = Cmd> + Unpin>(
}
}
}

/// 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(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_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