//! Public SSH identities and signing through the host's standard HTTP API. use crate::{Session, Stream, connect_tcp, ssh_wire::fail}; use base64::{Engine, engine::general_purpose::STANDARD}; use serde::Deserialize; use serde_json::{Value, json}; use std::io; use wasi::http::{outgoing_handler, types::*}; #[derive(Deserialize)] pub struct Identity { pub item: String, pub public_key: String, pub user: Option, } fn post(route: &str, payload: Value) -> io::Result { let base = std::env::var("SAUSAGE_HOST_API").unwrap_or_else(|_| "http://host.sausage".into()); let url = url::Url::parse(&format!("{}{route}", base.trim_end_matches('/'))) .map_err(|_| fail("invalid host API URL"))?; let bytes = serde_json::to_vec(&payload).map_err(|e| fail(&e.to_string()))?; let headers = Fields::new(); headers .set("content-type", &[b"application/json".to_vec()]) .map_err(|_| fail("HTTP headers"))?; headers .set("content-length", &[bytes.len().to_string().into_bytes()]) .map_err(|_| fail("HTTP headers"))?; let request = OutgoingRequest::new(headers); request .set_method(&Method::Post) .map_err(|_| fail("HTTP method"))?; request .set_scheme(Some(&Scheme::Http)) .map_err(|_| fail("HTTP scheme"))?; request .set_authority(Some( &url[url::Position::BeforeHost..url::Position::AfterPort], )) .map_err(|_| fail("HTTP authority"))?; request .set_path_with_query(Some(url.path())) .map_err(|_| fail("HTTP path"))?; let body = request.body().map_err(|_| fail("HTTP body"))?; let stream = body.write().map_err(|_| fail("HTTP stream"))?; let response = outgoing_handler::handle(request, None).map_err(|e| fail(&format!("host API: {e:?}")))?; stream .blocking_write_and_flush(&bytes) .map_err(|_| fail("host API upload"))?; drop(stream); OutgoingBody::finish(body, None).map_err(|_| fail("host API upload finish"))?; response.subscribe().block(); let response = response .get() .ok_or_else(|| fail("host API response missing"))? .map_err(|_| fail("host API response consumed"))? .map_err(|e| fail(&format!("host API: {e:?}")))?; let status = response.status(); let body = response .consume() .map_err(|_| fail("host API response body"))?; let stream = body .stream() .map_err(|_| fail("host API response stream"))?; let mut bytes = Vec::new(); loop { match stream.blocking_read(65536) { Ok(data) if data.is_empty() => stream.subscribe().block(), Ok(data) => bytes.extend(data), Err(wasi::io::streams::StreamError::Closed) => break, Err(_) => return Err(fail("host API response read")), } } drop(stream); let trailers = IncomingBody::finish(body); trailers.subscribe().block(); if !matches!(trailers.get(), Some(Ok(Ok(_)))) { return Err(fail("host API response finish")); } if status != 200 { return Err(fail(&format!( "host API HTTP {status}: {}", String::from_utf8_lossy(&bytes) ))); } serde_json::from_slice(&bytes).map_err(|_| fail("invalid host API JSON")) } impl Identity { pub fn load(host: &str) -> io::Result { serde_json::from_value(post("/ssh/identity", json!({"host": host}))?) .map_err(|_| fail("invalid SSH identity")) } pub fn sign(&self, host: &str, payload: Vec) -> io::Result> { let result = post( "/ssh/sign", json!({"host":host,"item":self.item,"payload":STANDARD.encode(payload)}), )?; STANDARD .decode( result["signature"] .as_str() .ok_or_else(|| fail("missing signature"))?, ) .map_err(|_| fail("invalid signature")) } } pub fn connect( host: &str, port: u16, user: Option<&str>, accept_new: bool, ) -> io::Result>> { let identity = Identity::load(host)?; let user = user .or(identity.user.as_deref()) .filter(|u| !u.is_empty()) .ok_or_else(|| fail("specify an SSH user"))?; let public = ssh_key::PublicKey::from_bytes( &STANDARD .decode(&identity.public_key) .map_err(|_| fail("invalid public key"))?, ) .map_err(|_| fail("invalid public key"))?; let mut session = Session::start(connect_tcp(host, port)?, host.into(), port, accept_new)?; futures_lite::future::block_on(session.authenticate_with(user, &public, |payload| { std::future::ready(identity.sign(host, payload)) }))?; Ok(session) }