2019-01-25 21:56:50 -05:00
|
|
|
use std::env::args;
|
2017-08-11 00:37:50 -04:00
|
|
|
use std::fs::File;
|
2021-01-15 01:05:15 -05:00
|
|
|
use std::io::{stdin, Read, Write};
|
2019-01-25 21:56:50 -05:00
|
|
|
use std::iter::Iterator;
|
2017-08-12 23:59:43 -04:00
|
|
|
use std::path::Path;
|
2022-09-24 18:05:46 -04:00
|
|
|
use std::str::FromStr;
|
2017-08-11 00:37:50 -04:00
|
|
|
|
2019-03-02 13:19:48 -05:00
|
|
|
use die::{die, Die};
|
2017-08-11 00:37:50 -04:00
|
|
|
use gumdrop::Options;
|
2019-01-25 21:56:50 -05:00
|
|
|
use serde_derive::Deserialize;
|
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
use std::process::{Command, Stdio};
|
2022-11-04 22:59:46 -04:00
|
|
|
use tokio_xmpp::{SimpleClient as Client};
|
2022-09-24 18:05:46 -04:00
|
|
|
use xmpp_parsers::message::{Body, Message, MessageType};
|
|
|
|
use xmpp_parsers::muc::Muc;
|
2021-03-23 21:17:00 -04:00
|
|
|
use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType};
|
2022-09-24 18:05:46 -04:00
|
|
|
use xmpp_parsers::{BareJid, Element, FullJid, Jid};
|
2021-01-15 01:05:15 -05:00
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
|
|
|
|
|
|
use anyhow::{anyhow, bail, Result};
|
2017-08-11 00:37:50 -04:00
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Config {
|
|
|
|
jid: String,
|
|
|
|
password: String,
|
2022-09-24 18:35:25 -04:00
|
|
|
nick: Option<String>,
|
2017-08-11 00:37:50 -04:00
|
|
|
}
|
|
|
|
|
2021-01-16 02:06:05 -05:00
|
|
|
fn parse_cfg<P: AsRef<Path>>(path: P) -> Result<Config> {
|
|
|
|
let mut f = File::open(path)?;
|
|
|
|
let mut input = String::new();
|
|
|
|
f.read_to_string(&mut input)?;
|
|
|
|
Ok(toml::from_str(&input)?)
|
2017-08-11 00:37:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Default, Options)]
|
|
|
|
struct MyOptions {
|
|
|
|
#[options(free)]
|
|
|
|
recipients: Vec<String>,
|
|
|
|
|
|
|
|
#[options(help = "show this help message and exit")]
|
|
|
|
help: bool,
|
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
#[options(help = "path to config file. default: ~/.config/sendxmpp.toml with fallback to /etc/sendxmpp/sendxmpp.toml")]
|
2017-08-11 00:37:50 -04:00
|
|
|
config: Option<String>,
|
|
|
|
|
|
|
|
#[options(help = "Force OpenPGP encryption for all recipients", short = "e")]
|
|
|
|
force_pgp: bool,
|
|
|
|
|
|
|
|
#[options(help = "Attempt OpenPGP encryption for all recipients")]
|
|
|
|
attempt_pgp: bool,
|
2021-03-23 21:17:00 -04:00
|
|
|
|
|
|
|
#[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,
|
2022-09-24 18:05:46 -04:00
|
|
|
|
|
|
|
#[options(help = "Recipients are Multi-User Chats")]
|
|
|
|
muc: bool,
|
|
|
|
|
|
|
|
#[options(help = "Nickname to use in Multi-User Chats")]
|
|
|
|
nick: Option<String>,
|
2017-08-11 00:37:50 -04:00
|
|
|
}
|
|
|
|
|
2021-01-14 23:34:52 -05:00
|
|
|
#[tokio::main]
|
|
|
|
async fn main() {
|
2021-12-31 15:44:44 -05:00
|
|
|
env_logger::init();
|
|
|
|
|
2017-08-11 00:37:50 -04:00
|
|
|
let args: Vec<String> = args().collect();
|
|
|
|
|
|
|
|
// Remember to skip the first argument. That's the program name.
|
|
|
|
let opts = match MyOptions::parse_args_default(&args[1..]) {
|
|
|
|
Ok(opts) => opts,
|
2021-03-23 21:17:00 -04:00
|
|
|
Err(e) => die!("{}: {}\nUsage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], e, args[0], MyOptions::usage()),
|
2017-08-11 00:37:50 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
if opts.help {
|
2021-03-23 21:17:00 -04:00
|
|
|
die!("Usage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], MyOptions::usage());
|
2017-08-11 00:37:50 -04:00
|
|
|
}
|
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
let recipients: Vec<Jid> = opts.recipients.iter().map(|s| s.parse::<Jid>().die("invalid recipient jid")).collect();
|
2019-03-02 13:19:48 -05:00
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
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");
|
|
|
|
}
|
2022-09-24 18:05:46 -04:00
|
|
|
if opts.muc {
|
|
|
|
die!("--raw is incompatible with --muc");
|
|
|
|
}
|
2021-03-23 21:17:00 -04:00
|
|
|
} else if recipients.is_empty() {
|
2019-03-02 13:19:48 -05:00
|
|
|
die!("no recipients specified!");
|
|
|
|
}
|
|
|
|
|
2022-09-24 18:05:46 -04:00
|
|
|
if opts.muc {
|
|
|
|
if opts.force_pgp || opts.attempt_pgp {
|
|
|
|
die!("--force-pgp and --attempt-pgp isn't implemented with --muc");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-25 21:56:50 -05:00
|
|
|
let recipients = &recipients;
|
2017-08-11 00:37:50 -04:00
|
|
|
|
|
|
|
let cfg = match opts.config {
|
2019-03-02 13:19:48 -05:00
|
|
|
Some(config) => parse_cfg(&config).die("provided config cannot be found/parsed"),
|
2021-03-23 21:17:00 -04:00
|
|
|
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"),
|
2017-08-11 00:37:50 -04:00
|
|
|
};
|
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
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>
|
|
|
|
|
2022-11-04 22:59:46 -04:00
|
|
|
let mut open_client = client.into_inner().into_inner();
|
2021-03-23 21:17:00 -04:00
|
|
|
|
|
|
|
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();
|
2022-11-04 21:52:14 -04:00
|
|
|
if data.is_empty() {
|
|
|
|
// don't send empty stanzas
|
|
|
|
return;
|
|
|
|
}
|
2021-03-23 21:17:00 -04:00
|
|
|
|
|
|
|
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 {
|
2022-09-24 18:05:46 -04:00
|
|
|
if opts.muc {
|
2022-09-24 18:35:25 -04:00
|
|
|
let nick = opts
|
|
|
|
.nick
|
|
|
|
.clone()
|
|
|
|
.or(cfg.nick.clone())
|
|
|
|
.or_else(|| BareJid::from_str(cfg.jid.as_str()).unwrap().node)
|
|
|
|
.die("couldn't find a nick to use");
|
2022-09-24 18:05:46 -04:00
|
|
|
let participant = match recipient.clone() {
|
|
|
|
Jid::Full(_) => die!("Invalid room address"),
|
|
|
|
Jid::Bare(bare) => bare.with_resource(nick.clone()),
|
|
|
|
};
|
|
|
|
let join = make_join(participant.clone());
|
|
|
|
client.send_stanza(join).await.die("failed to join MUC");
|
|
|
|
|
|
|
|
let reply = make_reply(recipient.clone(), &data, opts.muc);
|
|
|
|
client.send_stanza(reply).await.die("sending message failed");
|
|
|
|
} else {
|
|
|
|
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, opts.muc)
|
|
|
|
}
|
2021-03-23 21:17:00 -04:00
|
|
|
} else {
|
2022-09-24 18:05:46 -04:00
|
|
|
let encrypted = encrypted.unwrap();
|
|
|
|
let encrypted = encrypted.trim();
|
|
|
|
let mut reply = make_reply(recipient.clone(), "pgp", opts.muc);
|
|
|
|
let mut x = Element::bare("x", "jabber:x:encrypted");
|
|
|
|
x.append_text_node(encrypted);
|
|
|
|
reply.append_child(x);
|
|
|
|
reply
|
2021-03-23 21:17:00 -04:00
|
|
|
}
|
2021-01-15 01:05:15 -05:00
|
|
|
} else {
|
2022-09-24 18:05:46 -04:00
|
|
|
make_reply(recipient.clone(), &data, opts.muc)
|
|
|
|
};
|
|
|
|
client.send_stanza(reply).await.die("sending message failed");
|
|
|
|
}
|
2021-03-23 21:17:00 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close client connection
|
|
|
|
client.end().await.ok(); // ignore errors here, I guess
|
2021-01-14 23:34:52 -05:00
|
|
|
}
|
2021-03-23 21:17:00 -04:00
|
|
|
}
|
2017-08-11 00:37:50 -04:00
|
|
|
|
2021-03-23 21:17:00 -04:00
|
|
|
// Construct a <presence/>
|
|
|
|
fn make_presence() -> Element {
|
|
|
|
let mut presence = Presence::new(PresenceType::None);
|
|
|
|
presence.show = Some(PresenceShow::Chat);
|
|
|
|
presence.into()
|
2019-01-25 21:56:50 -05:00
|
|
|
}
|
|
|
|
|
2022-09-24 18:05:46 -04:00
|
|
|
fn make_join(to: FullJid) -> Element {
|
|
|
|
Presence::new(PresenceType::None).with_to(Jid::Full(to)).with_payloads(vec![Muc::new().into()]).into()
|
|
|
|
}
|
|
|
|
|
2019-01-25 21:56:50 -05:00
|
|
|
// Construct a chat <message/>
|
2022-09-24 18:05:46 -04:00
|
|
|
fn make_reply(to: Jid, body: &str, groupchat: bool) -> Element {
|
2019-01-25 21:56:50 -05:00
|
|
|
let mut message = Message::new(Some(to));
|
2022-09-24 18:05:46 -04:00
|
|
|
if groupchat {
|
|
|
|
message.type_ = MessageType::Groupchat;
|
|
|
|
}
|
2019-01-25 21:56:50 -05:00
|
|
|
message.bodies.insert(String::new(), Body(body.to_owned()));
|
|
|
|
message.into()
|
2017-08-11 00:37:50 -04:00
|
|
|
}
|
2021-01-15 01:05:15 -05:00
|
|
|
|
|
|
|
fn gpg_encrypt(to: Jid, body: &str) -> Result<String> {
|
|
|
|
let to: String = std::convert::From::from(to);
|
|
|
|
let mut gpg_cmd = Command::new("gpg")
|
|
|
|
.arg("--encrypt")
|
|
|
|
.arg("--armor")
|
|
|
|
.arg("-r")
|
|
|
|
.arg(to)
|
|
|
|
.stdin(Stdio::piped())
|
|
|
|
.stdout(Stdio::piped())
|
|
|
|
.spawn()?;
|
|
|
|
|
|
|
|
{
|
|
|
|
let stdin = gpg_cmd.stdin.as_mut().ok_or_else(|| anyhow!("no gpg stdin"))?;
|
|
|
|
stdin.write_all(body.as_bytes())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
let output = gpg_cmd.wait_with_output()?;
|
|
|
|
|
|
|
|
if !output.status.success() {
|
|
|
|
bail!("gpg exited with non-zero status code");
|
|
|
|
}
|
|
|
|
|
|
|
|
let output = output.stdout;
|
|
|
|
|
2021-01-15 02:08:42 -05:00
|
|
|
// strip off headers per https://xmpp.org/extensions/xep-0027.html
|
|
|
|
// header spec: https://tools.ietf.org/html/rfc4880#section-6.2
|
|
|
|
|
|
|
|
// find index of leading blank line (2 newlines in a row)
|
|
|
|
let start = first_index_of(0, &output, &[10, 10])? + 2;
|
|
|
|
|
|
|
|
if output.len() <= start {
|
2021-01-15 01:05:15 -05:00
|
|
|
bail!("length {} returned by gpg too short to be valid", output.len());
|
|
|
|
}
|
|
|
|
|
2021-01-15 02:08:42 -05:00
|
|
|
// find first newline+dash after the start
|
|
|
|
let end = first_index_of(start, &output, &[10, 45])?;
|
2021-01-15 01:05:15 -05:00
|
|
|
|
|
|
|
Ok(String::from_utf8((&output[start..end]).to_vec())?)
|
|
|
|
}
|
2021-01-15 02:08:42 -05:00
|
|
|
|
|
|
|
fn first_index_of(start_index: usize, haystack: &[u8], needle: &[u8]) -> Result<usize> {
|
2021-03-23 21:17:00 -04:00
|
|
|
for i in start_index..haystack.len() - needle.len() + 1 {
|
|
|
|
if haystack[i..i + needle.len()] == needle[..] {
|
2021-01-15 02:08:42 -05:00
|
|
|
return Ok(i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err(anyhow!("not found"))
|
|
|
|
}
|