use std::io::Read;
use std::net::SocketAddr;
use std::path::PathBuf;
use crate::predict::DisplayPreference;
use crate::transport_iroh::{
bind_endpoint, bind_endpoint_local, bind_endpoint_with_relay, direct_addr, format_endpoint_id,
load_or_create_secret_key, parse_endpoint_id, parse_relay_url, relay_addr,
};
use anyhow::Context;
#[cfg(feature = "cli")]
use clap::Args;
use tokio::signal::unix::{signal, SignalKind};
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::client::{run_client, BackendTerminal, ClientTerminal, DefaultBackend, IrohConnector};
#[derive(Debug, Clone)]
pub struct ConnectConfig {
pub server: String,
pub key_file: Option<PathBuf>,
pub direct: Option<SocketAddr>,
pub relay_url: Option<String>,
pub clipboard: bool,
}
impl ConnectConfig {
pub fn new(server: impl Into<String>) -> Self {
Self {
server: server.into(),
key_file: None,
direct: None,
relay_url: None,
clipboard: false,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct IdConfig {
pub key_file: Option<PathBuf>,
}
#[cfg(feature = "cli")]
#[derive(Args, Debug)]
pub struct ConnectArgs {
server: String,
#[arg(long)]
key_file: Option<PathBuf>,
#[arg(long, value_name = "IP:PORT", conflicts_with = "relay_url")]
direct: Option<SocketAddr>,
#[arg(long, value_name = "URL")]
relay_url: Option<String>,
#[arg(long)]
clipboard: bool,
}
#[cfg(feature = "cli")]
impl From<ConnectArgs> for ConnectConfig {
fn from(a: ConnectArgs) -> Self {
Self {
server: a.server,
key_file: a.key_file,
direct: a.direct,
relay_url: a.relay_url,
clipboard: a.clipboard,
}
}
}
#[cfg(feature = "cli")]
#[derive(Args, Debug)]
pub struct IdArgs {
#[arg(long)]
key_file: Option<PathBuf>,
}
#[cfg(feature = "cli")]
impl From<IdArgs> for IdConfig {
fn from(a: IdArgs) -> Self {
Self {
key_file: a.key_file,
}
}
}
fn spawn_signal_shutdown(shutdown: CancellationToken) -> anyhow::Result<()> {
let mut term = signal(SignalKind::terminate()).context("installing SIGTERM handler")?;
let mut intr = signal(SignalKind::interrupt()).context("installing SIGINT handler")?;
let mut hup = signal(SignalKind::hangup()).context("installing SIGHUP handler")?;
tokio::spawn(async move {
tokio::select! {
_ = term.recv() => {}
_ = intr.recv() => {}
_ = hup.recv() => {}
}
shutdown.cancel();
});
Ok(())
}
fn warn_if_locale_not_utf8() {
let locale = ["LC_ALL", "LC_CTYPE", "LANG"]
.iter()
.find_map(|k| std::env::var(k).ok().filter(|v| !v.is_empty()));
let looks_utf8 = locale.as_deref().is_some_and(|l| {
let l = l.to_ascii_lowercase();
l.contains("utf-8") || l.contains("utf8")
});
if !looks_utf8 {
let shown = locale.as_deref().unwrap_or("(unset)");
eprintln!(
"koh: warning: locale {shown} does not look UTF-8; non-ASCII output may be garbled. \
Set e.g. LANG=en_US.UTF-8."
);
}
}
pub fn run_id(config: impl Into<IdConfig>) -> anyhow::Result<()> {
let args: IdConfig = config.into();
let key_file = match args.key_file {
Some(p) => p,
None => crate::transport_iroh::default_key_path("client")?,
};
let secret = load_or_create_secret_key(&key_file).with_context(|| {
format!(
"loading client key from {} (pass --key-file to use a writable path)",
key_file.display()
)
})?;
println!("{}", format_endpoint_id(&secret.public()));
Ok(())
}
pub async fn connect(config: impl Into<ConnectConfig>) -> anyhow::Result<Option<u32>> {
let args: ConnectConfig = config.into();
if let Ok(path) = std::env::var("KOH_LOG") {
let created = {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&path)
}
#[cfg(not(unix))]
{
std::fs::File::create(&path)
}
};
if let Ok(file) = created {
let secured = {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let ok = file
.set_permissions(std::fs::Permissions::from_mode(0o600))
.is_ok();
if !ok {
eprintln!(
"koh: warning: could not set $KOH_LOG to 0600; file logging disabled"
);
}
ok
}
#[cfg(not(unix))]
{
true
}
};
if secured {
tracing_subscriber::fmt()
.with_writer(std::sync::Mutex::new(file))
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "koh=debug".into()),
)
.init();
}
}
}
warn_if_locale_not_utf8();
let key_file = match args.key_file {
Some(p) => p,
None => crate::transport_iroh::default_key_path("client")?,
};
let secret = load_or_create_secret_key(&key_file).with_context(|| {
format!(
"loading client key from {} (pass --key-file to use a writable path)",
key_file.display()
)
})?;
let my_id = secret.public();
let server_id = parse_endpoint_id(&args.server).context("parsing server endpoint id")?;
eprintln!("koh id: {}", format_endpoint_id(&my_id));
eprintln!(" (add this to the server with --allow if it isn't already)");
eprintln!("connecting to {} …", format_endpoint_id(&server_id));
let (endpoint, target) = if let Some(addr) = args.direct {
let ep = bind_endpoint_local(secret, false)
.await
.context("binding endpoint")?;
(ep, direct_addr(server_id, addr))
} else if let Some(url) = &args.relay_url {
let relay = parse_relay_url(url)?;
let ep = bind_endpoint_with_relay(secret, false, relay.clone())
.await
.context("binding endpoint")?;
(ep, relay_addr(server_id, relay))
} else {
let ep = bind_endpoint(secret, false)
.await
.context("binding endpoint")?;
(ep, server_id.into())
};
let connector = IrohConnector::new(endpoint.clone(), target);
let channel =
match tokio::time::timeout(super::RECONNECT_CONNECT_TIMEOUT, connector.connect()).await {
Ok(r) => r?,
Err(_) => anyhow::bail!(
"timed out connecting to {} (the server may be unreachable or not responding)",
format_endpoint_id(&server_id)
),
};
eprintln!("connected. (Ctrl-^ then . to disconnect)");
let shutdown = CancellationToken::new();
spawn_signal_shutdown(shutdown.clone())?;
let clipboard_enabled = args.clipboard;
let backend = DefaultBackend::new().context("acquiring the terminal")?;
let term = BackendTerminal::enter(backend, clipboard_enabled)
.context("entering raw mode / alt screen")?;
let (rows, cols) = term.size().unwrap_or((24, 80));
let (input_tx, input_rx) = mpsc::channel::<Vec<u8>>(64);
std::thread::Builder::new()
.name("koh-stdin".into())
.spawn(move || {
let mut stdin = std::io::stdin();
let mut buf = [0u8; 1024];
loop {
match stdin.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => {
let chunk = buf.get(..n).unwrap_or(&buf).to_vec();
if input_tx.blocking_send(chunk).is_err() {
break;
}
}
}
}
})
.context("spawning stdin reader")?;
let (resize_tx, resize_rx) = mpsc::channel::<()>(8);
let mut sigwinch =
signal(SignalKind::window_change()).context("installing SIGWINCH handler")?;
tokio::spawn(async move {
while sigwinch.recv().await.is_some() {
if resize_tx.send(()).await.is_err() {
break;
}
}
});
let result = run_client(
channel,
connector,
DisplayPreference::Always,
(rows, cols),
input_rx,
resize_rx,
term,
shutdown,
)
.await;
let _ = tokio::time::timeout(std::time::Duration::from_secs(2), endpoint.close()).await;
result
}