use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use tokio::net::UnixStream;
use super::protocol::{self, ClientMessage, Direction, LogStream, PROTOCOL_VERSION, ServerMessage};
use crate::env::broker::WorkspaceSnapshot;
use crate::home;
const BROKER_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
struct SessionEnv {
session_id: String,
token: String,
remote_sock: String,
}
fn session_from_env() -> Result<SessionEnv> {
let session_id = std::env::var("SHINE_SSH_SESSION").ok();
let token = std::env::var("SHINE_SSH_TOKEN").ok();
let remote_sock = std::env::var("SHINE_SSH_REMOTE_SOCK").ok();
let (Some(session_id), Some(token), Some(remote_sock)) = (session_id, token, remote_sock)
else {
bail!(
"this shell is not inside a `shine ssh` session (SHINE_SSH_SESSION/SHINE_SSH_TOKEN/SHINE_SSH_REMOTE_SOCK are not set); run `shine ssh <host>` first"
);
};
Ok(SessionEnv {
session_id,
token,
remote_sock,
})
}
pub fn session_available() -> bool {
std::env::var_os("SHINE_SSH_SESSION").is_some()
&& std::env::var_os("SHINE_SSH_TOKEN").is_some()
&& std::env::var_os("SHINE_SSH_REMOTE_SOCK").is_some()
}
async fn connect_and_handshake(remote_sock: &str) -> Result<UnixStream> {
let mut stream = UnixStream::connect(remote_sock).await.with_context(|| {
format!(
"could not reach the local shine transfer agent through the forwarded SSH connection at {remote_sock}; is the `shine ssh` session this shell was started under still alive?"
)
})?;
protocol::write_message(
&mut stream,
&ClientMessage::Hello {
protocol_version: PROTOCOL_VERSION,
},
)
.await?;
let ack: ServerMessage = protocol::read_message(&mut stream).await?;
match ack {
ServerMessage::HelloAck { protocol_version } if protocol_version == PROTOCOL_VERSION => {}
ServerMessage::HelloAck { protocol_version } => {
bail!(
"protocol version mismatch: this shine speaks v{PROTOCOL_VERSION}, the local agent speaks v{protocol_version}; upgrade whichever side is older"
);
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected handshake response: {other:?}"),
}
Ok(stream)
}
pub async fn request_direct_secrets(
specs: &[String],
argv: &[String],
) -> Result<BTreeMap<String, String>> {
let session = session_from_env()?;
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::DirectSecret {
token: session.token,
specs: specs.to_vec(),
argv: argv.to_vec(),
nonce: uuid::Uuid::new_v4().to_string(),
},
)
.await?;
read_secret_response(&mut stream).await
}
pub async fn request_workspace_secrets(
snapshot: WorkspaceSnapshot,
argv: &[String],
) -> Result<BTreeMap<String, String>> {
let session = session_from_env()?;
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::WorkspaceSecret {
token: session.token,
snapshot,
argv: argv.to_vec(),
nonce: uuid::Uuid::new_v4().to_string(),
},
)
.await?;
read_secret_response(&mut stream).await
}
async fn read_secret_response(stream: &mut UnixStream) -> Result<BTreeMap<String, String>> {
match read_broker_message(stream).await? {
ServerMessage::SecretResponse { values } => Ok(values),
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected secret broker response: {other:?}"),
}
}
async fn read_broker_message(stream: &mut UnixStream) -> Result<ServerMessage> {
tokio::time::timeout(BROKER_RESPONSE_TIMEOUT, protocol::read_message(stream))
.await
.context("timed out waiting for the local SSH secret broker")?
}
pub async fn describe_workspace(
snapshot: WorkspaceSnapshot,
release: &[String],
argv: &[String],
) -> Result<String> {
let session = session_from_env()?;
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::DescribeWorkspace {
token: session.token,
snapshot,
release: release.to_vec(),
argv: argv.to_vec(),
nonce: uuid::Uuid::new_v4().to_string(),
},
)
.await?;
match read_broker_message(&mut stream).await? {
ServerMessage::DescriptionResponse { summary } => Ok(summary),
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected broker description response: {other:?}"),
}
}
fn absolutize_remote_spec(raw: &str) -> Result<String> {
let expanded = home::full_expand(raw).with_context(|| format!("expanding path {raw:?}"))?;
if Path::new(&expanded).is_absolute() {
return Ok(expanded);
}
let cwd = std::env::current_dir().context("reading current directory")?;
Ok(format!("{}/{}", cwd.display(), expanded))
}
pub async fn handle_download(
remote_source: &str,
local_destination: Option<&str>,
force: bool,
dry_run: bool,
use_scp: bool,
) -> Result<()> {
let session = session_from_env()?;
let remote_spec = absolutize_remote_spec(remote_source)?;
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::Transfer {
token: session.token,
direction: Direction::Download,
remote_spec,
local_spec: local_destination.map(str::to_string),
force,
dry_run,
use_scp,
},
)
.await?;
relay_until_done(&mut stream).await
}
pub async fn handle_upload(
local_source: &str,
remote_destination: Option<&str>,
force: bool,
dry_run: bool,
use_scp: bool,
) -> Result<()> {
let session = session_from_env()?;
let remote_spec = match remote_destination {
Some(dest) => absolutize_remote_spec(dest)?,
None => std::env::current_dir()
.context("reading current directory")?
.display()
.to_string(),
};
let mut stream = connect_and_handshake(&session.remote_sock).await?;
protocol::write_message(
&mut stream,
&ClientMessage::Transfer {
token: session.token,
direction: Direction::Upload,
remote_spec,
local_spec: Some(local_source.to_string()),
force,
dry_run,
use_scp,
},
)
.await?;
relay_until_done(&mut stream).await
}
async fn relay_until_done(stream: &mut UnixStream) -> Result<()> {
loop {
match protocol::read_message(stream).await? {
ServerMessage::Starting {
fell_back, note, ..
} => {
if fell_back && let Some(note) = note {
eprintln!("shine: {note}");
}
}
ServerMessage::Log {
stream: which,
chunk,
} => match which {
LogStream::Stdout => {
print!("{chunk}");
let _ = std::io::stdout().flush();
}
LogStream::Stderr => {
eprint!("{chunk}");
let _ = std::io::stderr().flush();
}
},
ServerMessage::Done { code } => {
if code != 0 {
std::process::exit(code);
}
return Ok(());
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
}
}
}
pub async fn handle_status() -> Result<()> {
let session = session_from_env()?;
match connect_and_handshake(&session.remote_sock).await {
Ok(mut stream) => {
protocol::write_message(
&mut stream,
&ClientMessage::Status {
token: session.token,
},
)
.await?;
match protocol::read_message(&mut stream).await? {
ServerMessage::StatusResponse {
session_local_dir,
host,
} => {
println!("session: {}", session.session_id);
println!("connection: connected");
println!("protocol: v{PROTOCOL_VERSION}");
println!("host: {host}");
println!("local dir: {session_local_dir}");
Ok(())
}
ServerMessage::Error { message } => bail!("{message}"),
other => bail!("unexpected response: {other:?}"),
}
}
Err(error) => {
println!("session: {}", session.session_id);
println!("connection: unreachable ({error:#})");
Ok(())
}
}
}