You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

243 lines
8.1 KiB

4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
  1. use std::env::args;
  2. use std::fs::File;
  3. use std::io::{stdin, Read, Write};
  4. use std::iter::Iterator;
  5. use std::path::Path;
  6. use die::{die, Die};
  7. use gumdrop::Options;
  8. use serde_derive::Deserialize;
  9. use std::process::{Command, Stdio};
  10. use tokio_xmpp::{xmpp_stream, SimpleClient as Client};
  11. use xmpp_parsers::message::{Body, Message};
  12. use xmpp_parsers::presence::{Presence, Show as PresenceShow, Type as PresenceType};
  13. use xmpp_parsers::{Element, Jid};
  14. use tokio::io::{AsyncReadExt, AsyncWriteExt};
  15. use tokio_tls::TlsStream;
  16. use anyhow::{anyhow, bail, Result};
  17. #[derive(Deserialize)]
  18. struct Config {
  19. jid: String,
  20. password: String,
  21. }
  22. fn parse_cfg<P: AsRef<Path>>(path: P) -> Result<Config> {
  23. let mut f = File::open(path)?;
  24. let mut input = String::new();
  25. f.read_to_string(&mut input)?;
  26. Ok(toml::from_str(&input)?)
  27. }
  28. #[derive(Default, Options)]
  29. struct MyOptions {
  30. #[options(free)]
  31. recipients: Vec<String>,
  32. #[options(help = "show this help message and exit")]
  33. help: bool,
  34. #[options(help = "path to config file. default: ~/.config/sendxmpp.toml with fallback to /etc/sendxmpp/sendxmpp.toml")]
  35. config: Option<String>,
  36. #[options(help = "Force OpenPGP encryption for all recipients", short = "e")]
  37. force_pgp: bool,
  38. #[options(help = "Attempt OpenPGP encryption for all recipients")]
  39. attempt_pgp: bool,
  40. #[options(help = "Send raw XML stream, cannot be used with recipients or PGP")]
  41. raw: bool,
  42. #[options(help = "Send a <presence/> after connecting before sending messages, required for receiving for --raw")]
  43. presence: bool,
  44. }
  45. #[tokio::main]
  46. async fn main() {
  47. let args: Vec<String> = args().collect();
  48. // Remember to skip the first argument. That's the program name.
  49. let opts = match MyOptions::parse_args_default(&args[1..]) {
  50. Ok(opts) => opts,
  51. Err(e) => die!("{}: {}\nUsage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], e, args[0], MyOptions::usage()),
  52. };
  53. if opts.help {
  54. die!("Usage: {} [OPTIONS] [ARGUMENTS]\n\n{}", args[0], MyOptions::usage());
  55. }
  56. let recipients: Vec<Jid> = opts.recipients.iter().map(|s| s.parse::<Jid>().die("invalid recipient jid")).collect();
  57. if opts.raw {
  58. if opts.force_pgp || opts.attempt_pgp {
  59. die!("--raw is incompatible with --force-pgp and --attempt-pgp");
  60. }
  61. if !recipients.is_empty() {
  62. die!("--raw is incompatible with recipients");
  63. }
  64. } else if recipients.is_empty() {
  65. die!("no recipients specified!");
  66. }
  67. let recipients = &recipients;
  68. let cfg = match opts.config {
  69. Some(config) => parse_cfg(&config).die("provided config cannot be found/parsed"),
  70. None => parse_cfg(dirs::config_dir().die("cannot find home directory").join("sendxmpp.toml"))
  71. .or_else(|_| parse_cfg("/etc/sendxmpp/sendxmpp.toml"))
  72. .die("valid config file not found"),
  73. };
  74. if opts.raw {
  75. let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server");
  76. if opts.presence {
  77. client.send_stanza(make_presence()).await.die("could not send presence");
  78. }
  79. // can paste this to test: <message xmlns="jabber:client" to="travis@burtrum.org" type="chat"><body>woot</body></message>
  80. pub struct OpenClient {
  81. pub stream: xmpp_stream::XMPPStream<TlsStream<tokio::net::TcpStream>>,
  82. }
  83. let client: OpenClient = unsafe { std::mem::transmute(client) };
  84. let mut open_client = client.stream.stream.try_lock().die("could not lock client stream");
  85. let open_client = open_client.get_mut();
  86. let mut rd_buf = [0u8; 256]; // todo: proper buffer size?
  87. let mut stdin_buf = rd_buf.clone();
  88. let mut stdin = tokio::io::stdin();
  89. let mut stdout = tokio::io::stdout();
  90. loop {
  91. tokio::select! {
  92. n = open_client.read(&mut rd_buf) => {
  93. let n = n.unwrap_or(0);
  94. if n == 0 {
  95. break;
  96. }
  97. stdout.write_all(&rd_buf[0..n]).await.die("could not send bytes");
  98. stdout.flush().await.die("could not flush");
  99. },
  100. n = stdin.read(&mut stdin_buf) => {
  101. let n = n.unwrap_or(0);
  102. if n == 0 {
  103. break;
  104. }
  105. open_client.write_all(&stdin_buf[0..n]).await.die("could not send bytes");
  106. open_client.flush().await.die("could not flush");
  107. },
  108. }
  109. }
  110. // Close client connection, ignoring errors
  111. open_client.write_all("</stream:stream>".as_bytes()).await.ok();
  112. open_client.flush().await.ok();
  113. open_client.read(&mut rd_buf).await.ok();
  114. } else {
  115. let mut data = String::new();
  116. stdin().lock().read_to_string(&mut data).die("error reading from stdin");
  117. let data = data.trim();
  118. let mut client = Client::new(&cfg.jid, &cfg.password).await.die("could not connect to xmpp server");
  119. if opts.presence {
  120. client.send_stanza(make_presence()).await.die("could not send presence");
  121. }
  122. for recipient in recipients {
  123. let reply = if opts.force_pgp || opts.attempt_pgp {
  124. let encrypted = gpg_encrypt(recipient.clone(), &data);
  125. if encrypted.is_err() {
  126. if opts.force_pgp {
  127. die!("pgp encryption to jid '{}' failed!", recipient);
  128. } else {
  129. make_reply(recipient.clone(), &data)
  130. }
  131. } else {
  132. let encrypted = encrypted.unwrap();
  133. let encrypted = encrypted.trim();
  134. let mut reply = make_reply(recipient.clone(), "pgp");
  135. let mut x = Element::bare("x", "jabber:x:encrypted");
  136. x.append_text_node(encrypted);
  137. reply.append_child(x);
  138. reply
  139. }
  140. } else {
  141. make_reply(recipient.clone(), &data)
  142. };
  143. client.send_stanza(reply).await.die("sending message failed");
  144. }
  145. // Close client connection
  146. client.end().await.ok(); // ignore errors here, I guess
  147. }
  148. }
  149. // Construct a <presence/>
  150. fn make_presence() -> Element {
  151. let mut presence = Presence::new(PresenceType::None);
  152. presence.show = Some(PresenceShow::Chat);
  153. presence.into()
  154. }
  155. // Construct a chat <message/>
  156. fn make_reply(to: Jid, body: &str) -> Element {
  157. let mut message = Message::new(Some(to));
  158. message.bodies.insert(String::new(), Body(body.to_owned()));
  159. message.into()
  160. }
  161. fn gpg_encrypt(to: Jid, body: &str) -> Result<String> {
  162. let to: String = std::convert::From::from(to);
  163. let mut gpg_cmd = Command::new("gpg")
  164. .arg("--encrypt")
  165. .arg("--armor")
  166. .arg("-r")
  167. .arg(to)
  168. .stdin(Stdio::piped())
  169. .stdout(Stdio::piped())
  170. .spawn()?;
  171. {
  172. let stdin = gpg_cmd.stdin.as_mut().ok_or_else(|| anyhow!("no gpg stdin"))?;
  173. stdin.write_all(body.as_bytes())?;
  174. }
  175. let output = gpg_cmd.wait_with_output()?;
  176. if !output.status.success() {
  177. bail!("gpg exited with non-zero status code");
  178. }
  179. let output = output.stdout;
  180. // strip off headers per https://xmpp.org/extensions/xep-0027.html
  181. // header spec: https://tools.ietf.org/html/rfc4880#section-6.2
  182. // find index of leading blank line (2 newlines in a row)
  183. let start = first_index_of(0, &output, &[10, 10])? + 2;
  184. if output.len() <= start {
  185. bail!("length {} returned by gpg too short to be valid", output.len());
  186. }
  187. // find first newline+dash after the start
  188. let end = first_index_of(start, &output, &[10, 45])?;
  189. Ok(String::from_utf8((&output[start..end]).to_vec())?)
  190. }
  191. fn first_index_of(start_index: usize, haystack: &[u8], needle: &[u8]) -> Result<usize> {
  192. for i in start_index..haystack.len() - needle.len() + 1 {
  193. if haystack[i..i + needle.len()] == needle[..] {
  194. return Ok(i);
  195. }
  196. }
  197. Err(anyhow!("not found"))
  198. }