129 lines
4.7 KiB
Rust
129 lines
4.7 KiB
Rust
//! 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<String>,
|
|
}
|
|
fn post(route: &str, payload: Value) -> io::Result<Value> {
|
|
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<Self> {
|
|
serde_json::from_value(post("/ssh/identity", json!({"host": host}))?)
|
|
.map_err(|_| fail("invalid SSH identity"))
|
|
}
|
|
pub fn sign(&self, host: &str, payload: Vec<u8>) -> io::Result<Vec<u8>> {
|
|
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<Session<Box<dyn Stream>>> {
|
|
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)
|
|
}
|