|
@@ -0,0 +1,211 @@
|
|
|
|
|
+use std::{path::PathBuf, time::Duration};
|
|
|
|
|
+
|
|
|
|
|
+use clap::{Parser, Subcommand};
|
|
|
|
|
+use client_shared::{message::Message, utils::{UrlError, fix_url}};
|
|
|
|
|
+use utils::{binary::checksum, serialize::Serialize as _, time::time_millis};
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Parser, Debug)]
|
|
|
|
|
+#[command(version, about, long_about = None)]
|
|
|
|
|
+struct Args {
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'H', long = "host",
|
|
|
|
|
+ id = "HOST",
|
|
|
|
|
+ default_value = "localhost:13337",
|
|
|
|
|
+ )]
|
|
|
|
|
+ host: String,
|
|
|
|
|
+
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'p', long = "password",
|
|
|
|
|
+ id = "PASSWORD",
|
|
|
|
|
+ default_value = "null",
|
|
|
|
|
+ )]
|
|
|
|
|
+ password: String,
|
|
|
|
|
+
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'B',
|
|
|
|
|
+ id = "BLOCKED_WORDS_FILE",
|
|
|
|
|
+ required = false,
|
|
|
|
|
+ help = "A file containing blocked words separated by newlines.\nExample: \n\tWord1\n\tWord2\n\tWord3\n\t...",
|
|
|
|
|
+ )]
|
|
|
|
|
+ blocked_words: Option<PathBuf>,
|
|
|
|
|
+
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ long = "timeout",
|
|
|
|
|
+ id = "TIMEOUT",
|
|
|
|
|
+ default_value = "2",
|
|
|
|
|
+ help = "Time out connection to server after given value in seconds."
|
|
|
|
|
+ )]
|
|
|
|
|
+ timeout_sec: u64,
|
|
|
|
|
+
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'c', long = "channel",
|
|
|
|
|
+ id = "CHANNEL",
|
|
|
|
|
+ default_value = "root",
|
|
|
|
|
+ )]
|
|
|
|
|
+ channel: String,
|
|
|
|
|
+
|
|
|
|
|
+ #[command(subcommand)]
|
|
|
|
|
+ command: Commands,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Subcommand, Debug)]
|
|
|
|
|
+enum Commands {
|
|
|
|
|
+ Get {
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ long = "json",
|
|
|
|
|
+ help = "Return data as JSON."
|
|
|
|
|
+ )]
|
|
|
|
|
+ json: bool,
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ Send {
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'n', long = "name",
|
|
|
|
|
+ id = "NAME",
|
|
|
|
|
+ required = true,
|
|
|
|
|
+ help = "Name of the author of the message."
|
|
|
|
|
+ )]
|
|
|
|
|
+ name: String,
|
|
|
|
|
+
|
|
|
|
|
+ #[arg(
|
|
|
|
|
+ short = 'd', long = "content",
|
|
|
|
|
+ id = "CONTENT",
|
|
|
|
|
+ required = true,
|
|
|
|
|
+ help = "Message content."
|
|
|
|
|
+ )]
|
|
|
|
|
+ content: String,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+fn main() {
|
|
|
|
|
+ let args = Args::parse();
|
|
|
|
|
+
|
|
|
|
|
+ match args.command {
|
|
|
|
|
+ Commands::Get { json } => {
|
|
|
|
|
+ let mut messages = load_messages(&args)
|
|
|
|
|
+ .expect("Failed to load messages.");
|
|
|
|
|
+
|
|
|
|
|
+ messages.sort_by(|a, b| {
|
|
|
|
|
+ a.time.cmp(&b.time)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if json {
|
|
|
|
|
+ let mut messages_json = json::JsonValue::new_array();
|
|
|
|
|
+ for msg in &messages {
|
|
|
|
|
+ if msg.channel.to_lowercase() == args.channel.to_lowercase() {
|
|
|
|
|
+ messages_json.push(json::object! {
|
|
|
|
|
+ sender: *msg.sender,
|
|
|
|
|
+ content: *msg.content,
|
|
|
|
|
+ channel: *msg.channel,
|
|
|
|
|
+ time_s: (msg.time / 1000) as u64,
|
|
|
|
|
+ }).expect("Failed to create JSON object.");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ println!("{}", messages_json.pretty(2));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ todo!()
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ Commands::Send { ref name, ref content } => {
|
|
|
|
|
+ let mut message = Message {
|
|
|
|
|
+ sender: name.clone(),
|
|
|
|
|
+ content: content.clone(),
|
|
|
|
|
+ channel: args.channel.clone(),
|
|
|
|
|
+ time: time_millis(),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ message.sanitize();
|
|
|
|
|
+
|
|
|
|
|
+ send_message(&args, message)
|
|
|
|
|
+ .expect("Failed to send message.");
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+#[derive(Debug)]
|
|
|
|
|
+#[allow(unused)]
|
|
|
|
|
+enum ClientError {
|
|
|
|
|
+ UrlError(UrlError),
|
|
|
|
|
+ ReqwestError(reqwest::Error),
|
|
|
|
|
+ StatusCodeError(u16),
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn send_message(args: &Args, message: Message) -> Result<(), ClientError> {
|
|
|
|
|
+ let url = fix_url(&args.host)
|
|
|
|
|
+ .map_err(|x| ClientError::UrlError(x))?;
|
|
|
|
|
+
|
|
|
|
|
+ let mut bytes = message.serialize_checked();
|
|
|
|
|
+ utils::aes::encrypt_cbc(&mut bytes, &args.password);
|
|
|
|
|
+ bytes.push(checksum(&bytes));
|
|
|
|
|
+
|
|
|
|
|
+ let str = utils::binary::bin2hex(bytes);
|
|
|
|
|
+
|
|
|
|
|
+ let resp = reqwest::blocking::Client::new()
|
|
|
|
|
+ .post(format!("{}", url))
|
|
|
|
|
+ .body(str)
|
|
|
|
|
+ .timeout(Duration::from_secs(args.timeout_sec))
|
|
|
|
|
+ .send()
|
|
|
|
|
+ .map_err(|e| ClientError::ReqwestError(e))?;
|
|
|
|
|
+
|
|
|
|
|
+ if resp.status().is_success() {
|
|
|
|
|
+ Ok(())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Err(ClientError::StatusCodeError(resp.status().as_u16()))
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+fn load_messages(args: &Args) -> Result<Vec<Message>, ClientError> {
|
|
|
|
|
+ let url = fix_url(&args.host)
|
|
|
|
|
+ .map_err(|x| ClientError::UrlError(x))?;
|
|
|
|
|
+
|
|
|
|
|
+ let resp = reqwest::blocking::Client::new()
|
|
|
|
|
+ .get(url)
|
|
|
|
|
+ .timeout(Duration::from_secs(args.timeout_sec))
|
|
|
|
|
+ .send()
|
|
|
|
|
+ .map_err(|x| ClientError::ReqwestError(x))?;
|
|
|
|
|
+
|
|
|
|
|
+ if resp.status() != reqwest::StatusCode::OK {
|
|
|
|
|
+ return Err(ClientError::StatusCodeError(resp.status().as_u16()));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let bytes = resp.bytes()
|
|
|
|
|
+ .map_err(|e| ClientError::ReqwestError(e))?;
|
|
|
|
|
+
|
|
|
|
|
+ let contents = String::from_utf8_lossy(&bytes);
|
|
|
|
|
+ let mut messages = vec![];
|
|
|
|
|
+
|
|
|
|
|
+ for message_ser_hex in contents.split(",") {
|
|
|
|
|
+ let Some(mut message_ser_bin) = utils::binary::hex2bin(message_ser_hex) else {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if message_ser_bin.len() < 2 {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Checksum before decryption
|
|
|
|
|
+ if checksum(&message_ser_bin[..message_ser_bin.len()-1]) != message_ser_bin[message_ser_bin.len()-1] {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Remove checksum
|
|
|
|
|
+ message_ser_bin.pop();
|
|
|
|
|
+
|
|
|
|
|
+ if utils::aes::decrypt_cbc(&mut message_ser_bin, &args.password).is_err() {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let Ok(message) = Message::deserialize_checked(message_ser_bin) else {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if message.is_valid() {
|
|
|
|
|
+ messages.push(message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Ok(messages)
|
|
|
|
|
+}
|
|
|
|
|
+
|