This commit is contained in:
2026-05-12 23:19:12 +09:00
parent b19d9d25f7
commit cde37d516b
21 changed files with 634 additions and 52 deletions

View File

@@ -44,9 +44,13 @@ impl AuthedClient {
}
let pub_openssh = priv_key.public_key().to_openssh().context("encode pubkey")?;
let (ws, _) = tokio_tungstenite::connect_async(&cfg.backend_url)
let ws_url = format!(
"{}/ws/op",
cfg.backend_url.trim_end_matches('/').trim_end_matches("/ws/op")
);
let (ws, _) = tokio_tungstenite::connect_async(&ws_url)
.await
.with_context(|| format!("ws connect {}", cfg.backend_url))?;
.with_context(|| format!("ws connect {ws_url}"))?;
let (mut sink, mut stream) = ws.split();
send_msg(&mut sink, &OpMsg::AuthInit { pubkey_openssh: pub_openssh }).await?;

View File

@@ -48,6 +48,7 @@ pub async fn run(
.req_stream(OpReq::Attach {
session: session.clone(),
connection_id: Some(target.connection_id),
shell_id: None,
pty,
cols,
rows,
@@ -68,7 +69,7 @@ pub async fn run(
}
ui::print_info(&format!(
"attached to #{} ({}@{}) — Ctrl-] to detach",
target.connection_id, target.info.user, target.info.hostname
target.connection_id, target.info.user, target.info.hostname,
));
if pty {
@@ -83,7 +84,7 @@ pub async fn run(
result
}
async fn pump(
pub async fn pump(
client: &AuthedClient,
resps: &mut tokio::sync::mpsc::Receiver<OpResp>,
pty: bool,
@@ -145,7 +146,7 @@ async fn pump(
}
}
fn spawn_resize_watch(tx: tokio::sync::mpsc::Sender<(u16, u16)>) {
pub fn spawn_resize_watch(tx: tokio::sync::mpsc::Sender<(u16, u16)>) {
tokio::spawn(async move {
let mut last = terminal::size().unwrap_or((80, 24));
loop {

View File

@@ -3,3 +3,4 @@ pub mod connection;
pub mod connect;
pub mod keys;
pub mod watch;
pub mod shell;

View File

@@ -0,0 +1,99 @@
use crate::auth::AuthedClient;
use crate::cmd::connect;
use crate::cmd::connection;
use crate::ui;
use anyhow::{anyhow, Result};
use crossterm::terminal;
use rsh_types::{OpReq, OpResp};
pub async fn run(
client: &AuthedClient,
session: String,
connection_id: Option<u64>,
shell: Option<String>,
no_pty: bool,
) -> Result<()> {
let conns = connection::fetch(client, Some(session.clone())).await?;
if conns.is_empty() {
return Err(anyhow!("no connections for session '{session}'"));
}
let conn_id = match connection_id {
Some(id) => {
conns
.iter()
.find(|c| c.connection_id == id)
.ok_or_else(|| anyhow!("no connection {id} in session '{session}'"))?
.connection_id
}
None => {
if conns.len() == 1 {
conns[0].connection_id
} else {
let labels: Vec<String> = conns
.iter()
.map(|c| format!("#{} {}@{} ({})", c.connection_id, c.info.user, c.info.hostname, ui::fmt_time(c.connected_at)))
.collect();
let pick = inquire::Select::new("select connection:", labels.clone())
.prompt()
.map_err(|e| anyhow!("prompt: {e}"))?;
let idx = labels.iter().position(|l| l == &pick).unwrap();
conns[idx].connection_id
}
}
};
let (cols, rows) = terminal::size().unwrap_or((80, 24));
let pty = !no_pty;
let spawned = client
.req(OpReq::SpawnShell {
session: session.clone(),
connection_id: Some(conn_id),
shell,
pty,
cols,
rows,
})
.await?;
let shell_id = match spawned {
OpResp::ShellSpawned { shell_id, .. } => shell_id,
OpResp::Err(e) => return Err(anyhow!(e)),
other => return Err(anyhow!("unexpected: {other:?}")),
};
let (attach_id, mut resps) = client
.req_stream(OpReq::Attach {
session: session.clone(),
connection_id: Some(conn_id),
shell_id: Some(shell_id),
pty,
cols,
rows,
})
.await?;
let ready = resps.recv().await.ok_or_else(|| anyhow!("attach: no response"))?;
match ready {
OpResp::AttachReady { .. } => {}
OpResp::Err(e) => {
client.drop_stream(attach_id).await;
return Err(anyhow!(e));
}
other => {
client.drop_stream(attach_id).await;
return Err(anyhow!("unexpected: {other:?}"));
}
}
ui::print_info(&format!("spawned shell in #{conn_id} — Ctrl-] to detach"));
if pty {
terminal::enable_raw_mode().ok();
}
let result = connect::pump(client, &mut resps, pty).await;
if pty {
terminal::disable_raw_mode().ok();
}
client.drop_stream(attach_id).await;
println!();
result
}

View File

@@ -24,7 +24,7 @@ impl Config {
std::fs::create_dir_all(parent).ok();
}
let stub = Config {
backend_url: "wss://example.invalid/ws/op".into(),
backend_url: "wss://example.invalid".into(),
ssh_key_file: "~/.ssh/id_ed25519".into(),
};
let txt = serde_yaml::to_string(&stub)?;

View File

@@ -24,6 +24,8 @@ enum Cmd {
Connection(ConnectionCmd),
#[command(alias = "c")]
Connect(ConnectArgs),
#[command(alias = "sh")]
Shell(ShellArgs),
Keys(KeysCmd),
}
@@ -84,6 +86,17 @@ struct ConnectArgs {
no_pty: bool,
}
#[derive(Args, Debug)]
struct ShellArgs {
session: String,
#[arg(long)]
connection: Option<u64>,
#[arg(long)]
shell: Option<String>,
#[arg(long)]
no_pty: bool,
}
#[derive(Args, Debug)]
struct KeysCmd {
#[command(subcommand)]
@@ -146,6 +159,7 @@ async fn run() -> Result<()> {
ConnectionSub::List { session } => cmd::connection::list(&client, session).await,
},
Cmd::Connect(a) => cmd::connect::run(&client, a.session, a.connection_id, a.no_pty).await,
Cmd::Shell(a) => cmd::shell::run(&client, a.session, a.connection, a.shell, a.no_pty).await,
Cmd::Keys(k) => match k.sub {
KeysSub::Append { key, file, url } => cmd::keys::append(&client, key, file, url).await,
KeysSub::Rm { key, file, url } => cmd::keys::remove(&client, key, file, url).await,