|
|
|
@ -8,12 +8,16 @@ use die::{die, Die};
@@ -8,12 +8,16 @@ use die::{die, Die};
|
|
|
|
|
use gumdrop::Options; |
|
|
|
|
use serde_derive::Deserialize; |
|
|
|
|
|
|
|
|
|
use tokio_xmpp::SimpleClient as Client; |
|
|
|
|
use std::process::{Command, Stdio}; |
|
|
|
|
use tokio_xmpp::{xmpp_stream, SimpleClient as Client}; |
|
|
|
|
use xmpp_parsers::message::{Body, Message}; |
|
|
|
|
use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType}; |
|
|
|
|
use xmpp_parsers::{Element, Jid}; |
|
|
|
|
use std::process::{Command, Stdio}; |
|
|
|
|
|
|
|
|
|
use anyhow::{Result, bail, anyhow}; |
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt}; |
|
|
|
|
use tokio_tls::TlsStream; |
|
|
|
|
|
|
|
|
|
use anyhow::{anyhow, bail, Result}; |
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)] |
|
|
|
|
struct Config { |
|
|
|
@ -36,9 +40,7 @@ struct MyOptions {
@@ -36,9 +40,7 @@ struct MyOptions {
|
|
|
|
|
#[options(help = "show this help message and exit")] |
|
|
|
|
help: bool, |
|
|
|
|
|
|
|
|
|
#[options(
|
|
|
|
|
help = "path to config file. default: ~/.config/sendxmpp.toml with fallback to /etc/sendxmpp/sendxmpp.toml" |
|
|
|
|
)] |
|
|
|
|
#[options(help = "path to config file. default: ~/.config/sendxmpp.toml with fallback to /etc/sendxmpp/sendxmpp.toml")] |
|
|
|
|
config: Option<String>, |
|
|
|
|
|
|
|
|
|
#[options(help = "Force OpenPGP encryption for all recipients", short = "e")] |
|
|
|
@ -46,6 +48,12 @@ struct MyOptions {
@@ -46,6 +48,12 @@ struct MyOptions {
|
|
|
|
|
|
|
|
|
|
#[options(help = "Attempt OpenPGP encryption for all recipients")] |
|
|
|
|
attempt_pgp: bool, |
|
|
|
|
|
|
|
|
|
#[options(help = "Send raw XML stream, cannot be used with recipients or PGP")] |
|
|
|
|
raw: bool, |
|
|
|
|
|
|
|
|
|
#[options(help = "Send a <presence/> after connecting before sending messages, required for receiving for --raw")] |
|
|
|
|
presence: bool, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[tokio::main] |
|
|
|
@ -55,30 +63,23 @@ async fn main() {
@@ -55,30 +63,23 @@ async fn main() {
|
|
|
|
|
// Remember to skip the first argument. That's the program name.
|
|
|
|
|
let opts = match MyOptions::parse_args_default(&args[1..]) { |
|
|
|
|
Ok(opts) => opts, |
|
|
|
|
Err(e) => die!( |
|
|
|
|
"{}: {}\nUsage: {} [OPTIONS] [ARGUMENTS]\n\n{}", |
|
|
|
|
args[0], |
|
|
|
|
e, |
|
|
|
|
args[0], |
|
|
|
|
MyOptions::usage() |
|
|
|
|
), |
|
|
|
|
Err(e) => die!("{}: {}\nUsage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], e, args[0], MyOptions::usage()), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if opts.help { |
|
|
|
|
die!( |
|
|
|
|
"Usage: {} [OPTIONS] [ARGUMENTS]\n\n{}", |
|
|
|
|
args[0], |
|
|
|
|
MyOptions::usage() |
|
|
|
|
); |
|
|
|
|
die!("Usage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], MyOptions::usage()); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let recipients: Vec<Jid> = opts |
|
|
|
|
.recipients |
|
|
|
|
.iter() |
|
|
|
|
.map(|s| s.parse::<Jid>().die("invalid recipient jid")) |
|
|
|
|
.collect(); |
|
|
|
|
let recipients: Vec<Jid> = opts.recipients.iter().map(|s| s.parse::<Jid>().die("invalid recipient jid")).collect(); |
|
|
|
|
|
|
|
|
|
if recipients.is_empty() { |
|
|
|
|
if opts.raw { |
|
|
|
|
if opts.force_pgp || opts.attempt_pgp { |
|
|
|
|
die!("--raw is incompatible with --force-pgp and --attempt-pgp"); |
|
|
|
|
} |
|
|
|
|
if !recipients.is_empty() { |
|
|
|
|
die!("--raw is incompatible with recipients"); |
|
|
|
|
} |
|
|
|
|
} else if recipients.is_empty() { |
|
|
|
|
die!("no recipients specified!"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -86,50 +87,103 @@ async fn main() {
@@ -86,50 +87,103 @@ async fn main() {
|
|
|
|
|
|
|
|
|
|
let cfg = match opts.config { |
|
|
|
|
Some(config) => parse_cfg(&config).die("provided config cannot be found/parsed"), |
|
|
|
|
None => parse_cfg( |
|
|
|
|
dirs::config_dir() |
|
|
|
|
.die("cannot find home directory") |
|
|
|
|
.join("sendxmpp.toml"), |
|
|
|
|
) |
|
|
|
|
.or_else(|_| parse_cfg("/etc/sendxmpp/sendxmpp.toml")) |
|
|
|
|
.die("valid config file not found"), |
|
|
|
|
None => parse_cfg(dirs::config_dir().die("cannot find home directory").join("sendxmpp.toml")) |
|
|
|
|
.or_else(|_| parse_cfg("/etc/sendxmpp/sendxmpp.toml")) |
|
|
|
|
.die("valid config file not found"), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let mut data = String::new(); |
|
|
|
|
stdin() |
|
|
|
|
.read_to_string(&mut data) |
|
|
|
|
.die("error reading from stdin"); |
|
|
|
|
let data = data.trim(); |
|
|
|
|
|
|
|
|
|
// Client instance
|
|
|
|
|
let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server"); |
|
|
|
|
|
|
|
|
|
for recipient in recipients { |
|
|
|
|
let reply = if opts.force_pgp || opts.attempt_pgp { |
|
|
|
|
let encrypted = gpg_encrypt(recipient.clone(), &data); |
|
|
|
|
if encrypted.is_err() { |
|
|
|
|
if opts.force_pgp { |
|
|
|
|
die!("pgp encryption to jid '{}' failed!", recipient); |
|
|
|
|
if opts.raw { |
|
|
|
|
let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server"); |
|
|
|
|
|
|
|
|
|
if opts.presence { |
|
|
|
|
client.send_stanza(make_presence()).await.die("could not send presence"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// can paste this to test: <message xmlns="jabber:client" to="travis@burtrum.org" type="chat"><body>woot</body></message>
|
|
|
|
|
|
|
|
|
|
pub struct OpenClient { |
|
|
|
|
pub stream: xmpp_stream::XMPPStream<TlsStream<tokio::net::TcpStream>>, |
|
|
|
|
} |
|
|
|
|
let client: OpenClient = unsafe { std::mem::transmute(client) }; |
|
|
|
|
let mut open_client = client.stream.stream.try_lock().die("could not lock client stream"); |
|
|
|
|
let open_client = open_client.get_mut(); |
|
|
|
|
|
|
|
|
|
let mut rd_buf = [0u8; 256]; // todo: proper buffer size?
|
|
|
|
|
let mut stdin_buf = rd_buf.clone(); |
|
|
|
|
|
|
|
|
|
let mut stdin = tokio::io::stdin(); |
|
|
|
|
let mut stdout = tokio::io::stdout(); |
|
|
|
|
|
|
|
|
|
loop { |
|
|
|
|
tokio::select! { |
|
|
|
|
n = open_client.read(&mut rd_buf) => { |
|
|
|
|
let n = n.unwrap_or(0); |
|
|
|
|
if n == 0 { |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
stdout.write_all(&rd_buf[0..n]).await.die("could not send bytes"); |
|
|
|
|
stdout.flush().await.die("could not flush"); |
|
|
|
|
}, |
|
|
|
|
n = stdin.read(&mut stdin_buf) => { |
|
|
|
|
let n = n.unwrap_or(0); |
|
|
|
|
if n == 0 { |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
open_client.write_all(&stdin_buf[0..n]).await.die("could not send bytes"); |
|
|
|
|
open_client.flush().await.die("could not flush"); |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Close client connection, ignoring errors
|
|
|
|
|
open_client.write_all("</stream:stream>".as_bytes()).await.ok(); |
|
|
|
|
open_client.flush().await.ok(); |
|
|
|
|
open_client.read(&mut rd_buf).await.ok(); |
|
|
|
|
} else { |
|
|
|
|
let mut data = String::new(); |
|
|
|
|
stdin().lock().read_to_string(&mut data).die("error reading from stdin"); |
|
|
|
|
let data = data.trim(); |
|
|
|
|
|
|
|
|
|
let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server"); |
|
|
|
|
|
|
|
|
|
if opts.presence { |
|
|
|
|
client.send_stanza(make_presence()).await.die("could not send presence"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for recipient in recipients { |
|
|
|
|
let reply = if opts.force_pgp || opts.attempt_pgp { |
|
|
|
|
let encrypted = gpg_encrypt(recipient.clone(), &data); |
|
|
|
|
if encrypted.is_err() { |
|
|
|
|
if opts.force_pgp { |
|
|
|
|
die!("pgp encryption to jid '{}' failed!", recipient); |
|
|
|
|
} else { |
|
|
|
|
make_reply(recipient.clone(), &data) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
make_reply(recipient.clone(), &data) |
|
|
|
|
let encrypted = encrypted.unwrap(); |
|
|
|
|
let encrypted = encrypted.trim(); |
|
|
|
|
let mut reply = make_reply(recipient.clone(), "pgp"); |
|
|
|
|
let mut x = Element::bare("x", "jabber:x:encrypted"); |
|
|
|
|
x.append_text_node(encrypted); |
|
|
|
|
reply.append_child(x); |
|
|
|
|
reply |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
let encrypted = encrypted.unwrap(); |
|
|
|
|
let encrypted = encrypted.trim(); |
|
|
|
|
let mut reply = make_reply(recipient.clone(), "pgp"); |
|
|
|
|
let mut x = Element::bare("x", "jabber:x:encrypted"); |
|
|
|
|
x.append_text_node(encrypted); |
|
|
|
|
reply.append_child(x); |
|
|
|
|
reply |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
make_reply(recipient.clone(), &data) |
|
|
|
|
}; |
|
|
|
|
client.send_stanza(reply).await.die("sending message failed"); |
|
|
|
|
make_reply(recipient.clone(), &data) |
|
|
|
|
}; |
|
|
|
|
client.send_stanza(reply).await.die("sending message failed"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Close client connection
|
|
|
|
|
client.end().await.ok(); // ignore errors here, I guess
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Close client connection
|
|
|
|
|
client.end().await.ok(); // ignore errors here, I guess
|
|
|
|
|
// Construct a <presence/>
|
|
|
|
|
fn make_presence() -> Element { |
|
|
|
|
let mut presence = Presence::new(PresenceType::None); |
|
|
|
|
presence.show = Some(PresenceShow::Chat); |
|
|
|
|
presence.into() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Construct a chat <message/>
|
|
|
|
@ -180,8 +234,8 @@ fn gpg_encrypt(to: Jid, body: &str) -> Result<String> {
@@ -180,8 +234,8 @@ fn gpg_encrypt(to: Jid, body: &str) -> Result<String> {
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn first_index_of(start_index: usize, haystack: &[u8], needle: &[u8]) -> Result<usize> { |
|
|
|
|
for i in start_index..haystack.len()-needle.len()+1 { |
|
|
|
|
if haystack[i..i+needle.len()] == needle[..] { |
|
|
|
|
for i in start_index..haystack.len() - needle.len() + 1 { |
|
|
|
|
if haystack[i..i + needle.len()] == needle[..] { |
|
|
|
|
return Ok(i); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|