use std::{
io::{self, Write},
net::{IpAddr, Ipv6Addr, SocketAddr, ToSocketAddrs},
sync::LazyLock,
time::Duration,
};
use clap::{Parser, ValueEnum};
use indoc::indoc;
use regex::{self, Regex};
use rustyline_async::{Readline, ReadlineEvent};
use tokio::{net::TcpListener, signal};
use crate::{
protocol::{
self, AuthVersion, Command, Connection, ExitBreak, OutputType, SecureMsg, TerminateCmd,
XWingKeypair,
},
sigint, util,
};
#[derive(Parser, Debug)]
pub struct Cli {
#[arg(long, value_enum)]
auth: Option<AuthVersion>,
#[arg(required = true, value_name = "HOST:PORT")]
callback: String,
#[arg(long)]
local_port: Option<u16>,
}
struct Params {
pub callback: String,
pub listen: SocketAddr,
}
const UNSPECIFIED_IP: IpAddr = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0));
fn parse_args(args: &Cli) -> Result<Params, String> {
let (_, callback_port) = util::parse_address(&args.callback)
.map_err(|err| format!("Invalid callback address: {err}"))?;
match &args.callback.to_socket_addrs() {
Ok(addrs) => {
if addrs.len() == 0 {
eprintln!("[WARNING] Callback host did not resolve to any IP addresses.");
}
}
Err(err) => eprintln!("[WARNING] Could not resolve callback host to an IP address: {err}"),
}
let listen_port = args.local_port.unwrap_or(callback_port);
Ok(Params {
callback: args.callback.to_string(),
listen: SocketAddr::new(UNSPECIFIED_IP, listen_port),
})
}
fn print_with_trailing_newline(data: &[u8]) {
print!("{}", String::from_utf8_lossy(data));
if !data.ends_with("\n".as_bytes()) {
println!();
}
}
const CMD_EXIT: &str = "exit";
static CMD_EXIT_PATTERN: LazyLock<Regex> =
LazyLock::new(|| Regex::new(&format!("(?i)\\s*{CMD_EXIT}\\s*")).unwrap());
enum ReplInput {
Command(String),
Interrupted,
}
async fn ask_user_next_command(
cmd_history: &mut Vec<String>,
sigint_manager: &mut sigint::Manager,
) -> Result<ReplInput, String> {
let (mut readline, rl_writer) = Readline::new(">> ".to_string())
.map_err(|err| format!("Could not set up input prompt: {err}"))?;
for cmd in &mut *cmd_history {
readline.add_history_entry((*cmd).to_string());
}
let suppress_sigint = sigint_manager.suppress();
let readline_result = readline.readline().await;
suppress_sigint.release();
readline.flush().unwrap();
drop(rl_writer);
match readline_result {
Ok(ReadlineEvent::Line(command)) => Ok(ReplInput::Command(command)),
Ok(ReadlineEvent::Eof | ReadlineEvent::Interrupted) => Ok(ReplInput::Interrupted),
Err(err) => Err(format!("Couldn't read input: {err}")),
}
}
async fn hangup(conn: Connection) -> Result<(), String> {
conn.send(SecureMsg::ExitBreak(ExitBreak {}))
.await
.map_err(|err| format!("Could not tell breakpoint to exit: {err}"))?;
Ok(())
}
enum AfterSession {
Iterate,
Exit,
}
async fn run_session(
mut conn: Connection,
cmd_history: &mut Vec<String>,
sigint_manager: &mut sigint::Manager,
) -> Result<AfterSession, String> {
println!("Session ready. You can enter single-line commands. Use `{CMD_EXIT}` to exit.");
let mut cmd_seq = 0;
loop {
let command: Option<String> =
match ask_user_next_command(cmd_history, sigint_manager).await? {
ReplInput::Command(command) => {
if command.is_empty() {
continue; }
if CMD_EXIT_PATTERN.is_match(&command) {
None
} else {
Some(command)
}
}
ReplInput::Interrupted => None,
};
let Some(command) = command else {
match ask_user_exit_choice().await {
ExitChoice::Resume => {
continue;
}
ExitChoice::Iterate => {
hangup(conn).await?;
return Ok(AfterSession::Iterate);
}
ExitChoice::Exit => {
hangup(conn).await?;
return Ok(AfterSession::Exit);
}
}
};
if !command.starts_with(' ') && Some(&command) != cmd_history.last() {
cmd_history.push(command.clone());
}
conn.send(SecureMsg::Command(Command {
command: command.clone(),
seq: cmd_seq,
}))
.await
.map_err(|err| format!("Could not send command to breakpoint: {err}"))?;
let mut killing = false;
loop {
tokio::select! {
biased;
_ = signal::ctrl_c() => {
if !killing {
println!(); println!("Attempting to terminate command...");
conn.send(
SecureMsg::TerminateCmd(TerminateCmd { seq: cmd_seq })
).await.map_err(|err| format!("Could not send termination command to breakpoint: {err}"))?;
killing = true;
}
},
msg = conn.recv() => match msg.ok_or("Error reading from breakpoint.")? {
SecureMsg::OutputLine(output) => {
if output.seq != cmd_seq {
return Err(format!(
"Received output for command #{} instead of #{}",
output.seq, cmd_seq
));
}
let log_tag = match output.gender {
OutputType::Stdout => "out",
OutputType::Stderr => "err",
};
print!("[{log_tag}] ");
print_with_trailing_newline(&output.bytes);
}
SecureMsg::Finished(outcome) => {
if outcome.seq != cmd_seq {
return Err(format!(
"Received completion notification for command #{} instead of #{}",
outcome.seq, cmd_seq
));
}
match outcome.exit_code {
Some(status) => println!("[exit: {status}]"),
None => println!("[exit: terminated by signal]"),
}
break;
}
wrong_variant => Err(format!(
"Received unexpected message type: {}",
Into::<&str>::into(wrong_variant)
))?,
},
};
}
cmd_seq += 1;
}
}
enum ExitChoice {
Resume,
Iterate,
Exit,
}
async fn ask_user_exit_choice() -> ExitChoice {
println!(indoc! {
"You've left the command shell. Do you want to resume, or exit?
[1] Resume: Go back to issuing commands to the breakpoint.
[2] Iterate: Exit from breakpoint on remote end, running script normally, but leave controller running with same keys.
[3] Exit: Exit from breakpoint and controller. For additional debugging you'll need to provision the breakpoint with a new key and secret."
});
println!();
let (mut readline, mut rl_writer) = Readline::new("Choice [1/2/3]: ".to_string()).unwrap();
let choice = loop {
match readline.readline().await {
Ok(ReadlineEvent::Line(choice)) => match choice.as_str() {
"1" => break ExitChoice::Resume,
"2" => break ExitChoice::Iterate,
"3" => break ExitChoice::Exit,
_ => (),
},
Ok(ReadlineEvent::Eof) => (),
Ok(ReadlineEvent::Interrupted) => break ExitChoice::Exit,
Err(err) => {
rl_writer
.write_all(format!("Failed to read input, exiting: {err}\n").as_bytes())
.unwrap();
break ExitChoice::Exit;
}
}
};
readline.flush().unwrap(); choice
}
fn print_connection_instructions(controller_keypair: &XWingKeypair, params: &Params) {
let controller_digest = protocol::controller_digest(&controller_keypair.public_key);
println!(
indoc! {r#"Run the following command from the environment you want to inspect:
breakmancer break --auth dual-pub -c {} -i {}
You can add --which="some location" to distinguish between breakpoints.
(Now listening on {} for callback...)"#},
¶ms.callback, &controller_digest, ¶ms.listen,
);
}
fn protocol_wizard() -> Result<Option<AuthVersion>, String> {
println!(
"Will you be able to view stdout from your process in realtime? \
This is required for finding the verification string during \
connection setup."
);
print!("Answer [y/n]: ");
io::stdout().flush().unwrap();
let mut answer = String::new();
io::stdin().read_line(&mut answer).unwrap();
let answer = answer.trim().to_lowercase();
if answer == "y" || answer == "yes" {
Ok(Some(AuthVersion::DualPub))
} else if answer == "n" || answer == "no" {
Ok(None)
} else {
Err("Unknown answer, not yes or no".to_string())
}
}
pub async fn run(args: &Cli) -> Result<(), String> {
let params = parse_args(args)?;
if args.auth.is_none() {
let chosen = match protocol_wizard()? {
None => {
println!(
"This program currently cannot meet your needs. \
If after reviewing your answers you find they are unchanged, \
consider submitting a bug report describing your use-case \
and its constraints."
);
return Ok(());
}
Some(proto) => proto,
};
let arg_val = chosen
.to_possible_value()
.expect("None of the selectable AuthVersion variants should be skipped.");
println!(
"Tip: You can skip this question in the future by passing `--auth {}` \
(as long as your answers would be the same).",
arg_val.get_name()
);
}
let (controller_keypair, debug_breakpoint_digest) = match crate::debug_utils::get_debug_keys() {
Some(keys) => (
keys.controller_keypair,
Some(protocol::breakpoint_digest(
&keys.breakpoint_keypair.public_key,
)),
),
None => (XWingKeypair::new(), None),
};
let mut sigint_manager = sigint::Manager::setup();
let server_socket = TcpListener::bind(params.listen)
.await
.map_err(|err| format!("Could not listen on {}: {}", ¶ms.listen, err))?;
print_connection_instructions(&controller_keypair, ¶ms);
let mut command_history = Vec::<String>::new();
loop {
let backoff = Duration::from_secs(10);
let conn_partial = match protocol::controller_open_connection(
controller_keypair.clone(),
&server_socket,
)
.await
{
Ok(conn) => conn,
Err(err) => {
println!(
"Failed to start controller connection; waiting {backoff:?} and trying again: {err}"
);
tokio::time::sleep(backoff).await;
continue;
}
};
let expected_breakpoint_digest = match debug_breakpoint_digest.as_ref() {
None => {
print!("Enter the verification string the breakpoint has printed to stdout: ");
io::stdout().flush().unwrap();
let mut expected_breakpoint_digest = String::new();
io::stdin()
.read_line(&mut expected_breakpoint_digest)
.map_err(|err| format!("Error while reading input: {err}"))?;
expected_breakpoint_digest
}
Some(debug_value) => {
println!("[WARNING] Using debug breakpoint digest.");
debug_value.clone()
}
};
let (conn, breakpoint_intro) = match conn_partial
.verify_breakpoint_and_complete_setup(&expected_breakpoint_digest)
.await
{
Ok(conn_and_info) => conn_and_info,
Err(err) => {
println!("Unable to complete connection: {err}");
continue;
}
};
if let Some(which) = breakpoint_intro.which {
println!("Breakpoint: {which}");
}
match run_session(conn, &mut command_history, &mut sigint_manager).await {
Ok(AfterSession::Iterate) => {
println!("Will wait for new connection...");
}
Ok(AfterSession::Exit) => {
return Ok(());
}
Err(err) => {
println!("Encountered error, restarting connection: {err}");
}
}
}
}