pijul 0.7.2

A patch-based distributed version control system, easy to use and fast. Command-line interface.
use clap::{SubCommand, Arg, ArgMatches};
use error::{ErrorKind, Error};
use commands::{BasicOptions, StaticSubcommand, remote};
use std::io::{Write,stderr};
use std::process::exit;
use meta;
use meta::KeyType;
use thrussh;
use bincode;
use futures;
use thrussh::{client, key, ChannelId};
use futures::Future;
use std::rc::Rc;
use libpijul;
use regex::Regex;
use thrussh_keys;
use std;
use super::ask;

pub fn invocation() -> StaticSubcommand {
    return SubCommand::with_name("keys")
        .about("Manage signing and SSH keys")
        .arg(Arg::with_name("repository")
             .long("repository")
             .help("The repository where to record, defaults to the current directory.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("generate-signing-key")
             .long("generate-signing")
             .help("Generate a signing key.")
             .takes_value(false)
             .required(false))
        .arg(Arg::with_name("port")
             .long("port")
             .short("p")
             .help("Port of the SSH server.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("upload")
             .long("upload-to")
             .help("Upload your public signing key to a server.")
             .takes_value(true)
             .required(false))
        .arg(Arg::with_name("generate-SSH-key")
             .long("generate-ssh")
             .help("Generate an SSH key.")
             .takes_value(false)
             .required(false))
        .arg(Arg::with_name("local")
             .long("local")
             .help("Save keys for this repository only")
             .takes_value(false)
             .required(false));
}

pub struct Params<'a> {
    pub generate_signing_key: bool,
    pub generate_ssh_key: bool,
    pub server: Option<&'a str>,
    pub local: bool,
    pub port: u16,
}

pub fn parse_args<'a>(args: &'a ArgMatches) -> Params<'a> {
    Params {
        generate_signing_key: args.is_present("generate-signing-key"),
        generate_ssh_key: args.is_present("generate-SSH-key"),
        server: args.value_of("upload"),
        local: args.is_present("local"),
        port: args.value_of("port").and_then(|x| x.parse().ok()).unwrap_or(22),
    }
}

pub fn run(arg_matches: &ArgMatches) -> Result<(), Error> {
    let args = parse_args(arg_matches);

    let dot_pijul = if args.local {
        let opts = BasicOptions::from_args(arg_matches)?;
        Some(opts.repo_dir())
    } else {
        None
    };

    if args.generate_signing_key {
        if let Some(ref dot_pijul) = dot_pijul {
            meta::generate_key(&dot_pijul, KeyType::Signing)?
        } else {
            meta::generate_global_key(KeyType::Signing)?
        }
    }

    if let Some(ref user_host) = args.server {
        match meta::load_global_or_local_key(dot_pijul.as_ref(), KeyType::Signing) {
            Ok(key) => {
                let config = Rc::new(thrussh::client::Config::default());
                let ssh_user_host = Regex::new(r"^([^@]*)@(.*)$").unwrap();
                if !ssh_user_host.is_match(user_host) {
                    return Err(ErrorKind::CannotParseRemote.into())
                }
                let cap = ssh_user_host.captures(user_host).unwrap();
                let user = &cap[1];
                let server = &cap[2];

                let client = SshClient {
                    exit_status: None,
                    key_pair: key,
                    host: server.to_string(),
                    port: args.port
                };
                let (path_sec, path_pub) = remote::guess_secret_key_path(None);
                let key = thrussh_keys::load_secret_key(
                    path_sec.to_str().unwrap(),
                    None,
                    path_pub.as_ref().map(|x| x.as_path())
                )?;

                thrussh::client::connect(
                    (server, args.port), config, None, client,
                    |connection| {
                        connection.authenticate_key(user, key)
                            .and_then(|session| {

                                session.channel_open_session().and_then(|(mut session, channelid)| {
                                    session.exec(channelid, false, &format!("pijul challenge"));
                                    session.flush().unwrap();
                                    session.wait(move |session| {
                                        session.handler().exit_status.is_some()
                                    })
                                })
                            })
                    }
                ).unwrap();
            }
            Err(e) => {
                panic!("Could not find key: {:?}", e)
            }
        }
    }

    if args.generate_ssh_key {
        if let Some(ref dot_pijul) = dot_pijul {
            meta::generate_key(&dot_pijul, KeyType::SSH)?
        } else {
            meta::generate_global_key(KeyType::SSH)?
        }
    }

    Ok(())
}

pub fn explain(r: Result<(), Error>) {
    if let Err(Error(kind, _)) = r {
        if let ErrorKind::InARepository(p) = kind {
            writeln!(stderr(), "Repository {} already exists", p.display()).unwrap();
        } else {
            writeln!(stderr(), "error: {}", kind).unwrap();
        }
        exit(1)
    }
}


struct SshClient {
    exit_status: Option<u32>,
    key_pair: libpijul::signature::KeyPair,
    host: String,
    port: u16,
}

impl client::Handler for SshClient {
    type Error = Error;
    type FutureBool = futures::Finished<(Self, bool), Self::Error>;
    type FutureUnit = futures::Finished<Self, Self::Error>;
    type SessionUnit = futures::Finished<(Self, client::Session), Self::Error>;
    fn check_server_key(self, server_public_key: &key::PublicKey) -> Self::FutureBool {
        let path = std::env::home_dir().unwrap().join(".ssh").join("known_hosts");
        match thrussh_keys::check_known_hosts_path(&self.host, self.port, &server_public_key, &path) {
            Ok(true) => futures::done(Ok((self, true))),
            Ok(false) => {
                if let Ok(false) = ask::ask_learn_ssh(&self.host,
                                                      self.port, "") {
                    futures::done(Ok((self, false)))
                } else {
                    thrussh_keys::learn_known_hosts_path(&self.host,
                                                         self.port,
                                                         &server_public_key,
                                                         &path)
                        .unwrap();
                    futures::done(Ok((self, true)))
                }
            }
            Err(e) => if let thrussh_keys::ErrorKind::KeyChanged(line) = *e.kind() {
                println!("Host key changed! Someone might be eavesdropping this communication, \
                          refusing to continue. Previous key found line {}",
                         line);
                futures::done(Ok((self, false)))
            } else {
                futures::done(Err(From::from(e)))
            }
        }
    }
    fn data(self, channel: ChannelId, _: Option<u32>, data: &[u8], mut session: client::Session)
            -> Self::SessionUnit {
        // msg is a challenge, we need to sign it and return.
        let signature = self.key_pair.sign(data);
        session.data(channel, None, &bincode::serialize(&signature, bincode::Infinite).unwrap()).unwrap();
        futures::finished((self, session))
    }
    fn exit_status(mut self,
                   channel: thrussh::ChannelId,
                   exit_status: u32,
                   session: thrussh::client::Session)
                   -> Self::SessionUnit {
        debug!("exit_status received on channel {:?}: {:?}:", channel, exit_status);
        self.exit_status = Some(exit_status);
        debug!("self.exit_status = {:?}", self.exit_status);
        futures::finished((self, session))
    }
}