Adding runner handshake
This commit is contained in:
parent
d209e7bbb5
commit
04240862e1
5 changed files with 114 additions and 4 deletions
|
|
@ -19,9 +19,10 @@ thiserror = { version = "1", optional = true }
|
|||
libloading = { version = "0.8", optional = true }
|
||||
memmap2 = { version = "0.9", optional = true }
|
||||
rkyv = { version = "0.7", features = ["validation", "std"], optional = true }
|
||||
getrandom = { version = "0.2", optional = true }
|
||||
|
||||
[features]
|
||||
# Enables the runtime host surface (`HostApi` trait) and the out-of-process
|
||||
# plugin runtime. The OpenCADStudio binary turns this on; pure-data consumers
|
||||
# leave it off.
|
||||
host = ["dep:acadrust", "dep:interprocess", "dep:serde", "dep:bincode", "dep:thiserror", "dep:libloading", "dep:memmap2", "dep:rkyv"]
|
||||
host = ["dep:acadrust", "dep:interprocess", "dep:serde", "dep:bincode", "dep:thiserror", "dep:libloading", "dep:memmap2", "dep:rkyv", "dep:getrandom"]
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use interprocess::local_socket::{GenericNamespaced, Stream, ToNsName};
|
|||
|
||||
use crate::host::{DocumentReader, HostApi, InteractiveCommand, ReaderEntity};
|
||||
use crate::ipc::protocol::{
|
||||
HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost,
|
||||
HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost, RunnerHandshake,
|
||||
};
|
||||
use crate::ipc::transport::{recv, send};
|
||||
use crate::shm::{DocumentViewInfo, SharedDocumentReader};
|
||||
|
|
@ -46,6 +46,14 @@ impl IpcClient {
|
|||
self.stream.borrow_mut()
|
||||
}
|
||||
|
||||
/// Send the initial runner handshake presenting the pre-shared token.
|
||||
pub fn send_handshake(&self, token: &str) -> Result<(), crate::ipc::transport::TransportError> {
|
||||
send(
|
||||
&mut self.stream.borrow_mut(),
|
||||
&RunnerHandshake::Token(token.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a plugin request and wait for the matching response. Any nested
|
||||
/// host requests that arrive while we are waiting are treated as errors.
|
||||
pub fn request(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,20 @@ pub enum InteractiveEvent {
|
|||
ObjectPick { handle: Handle, pt: [f64; 3] },
|
||||
}
|
||||
|
||||
/// Initial handshake sent by the plugin runner immediately after connecting.
|
||||
///
|
||||
/// The runner proves it was spawned by this host by presenting a pre-shared
|
||||
/// token delivered through the `OCS_PLUGIN_TOKEN` environment variable. A
|
||||
/// mismatch causes the host to close the connection.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum RunnerHandshake {
|
||||
Token(String),
|
||||
}
|
||||
|
||||
/// Environment variable through which the host passes the pre-shared
|
||||
/// authentication token to the plugin runner child process.
|
||||
pub const PLUGIN_TOKEN_ENV: &str = "OCS_PLUGIN_TOKEN";
|
||||
|
||||
/// Requests the host sends to the plugin runner.
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum HostRequest {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ use interprocess::local_socket::{GenericNamespaced, ListenerOptions, Stream, ToN
|
|||
|
||||
use crate::host::{CommandStep, HostApi};
|
||||
use crate::ipc::protocol::{
|
||||
HostRequest, HostResponse, HostToPlugin, InteractiveEvent, PluginToHost,
|
||||
HostRequest, HostResponse, HostToPlugin, InteractiveEvent, PluginToHost, RunnerHandshake,
|
||||
PLUGIN_TOKEN_ENV,
|
||||
};
|
||||
use crate::ipc::server::handle_plugin_request;
|
||||
use crate::ipc::transport::{recv, send};
|
||||
|
|
@ -37,6 +38,9 @@ fn spawn_timeout() -> Duration {
|
|||
/// Default maximum time to wait for a plugin call to respond.
|
||||
const CALL_TIMEOUT_DEFAULT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Length of the random pre-shared token used to authenticate the runner.
|
||||
const PLUGIN_TOKEN_LEN: usize = 32;
|
||||
|
||||
fn call_timeout() -> Duration {
|
||||
std::env::var("OCS_PLUGIN_CALL_TIMEOUT_SECS")
|
||||
.ok()
|
||||
|
|
@ -128,6 +132,8 @@ impl PluginProcess {
|
|||
cdylib_path.display()
|
||||
);
|
||||
|
||||
let token = generate_token()?;
|
||||
|
||||
// Create the listener before spawning so the runner can connect immediately.
|
||||
let listener = ListenerOptions::new().name(socket_name_ref).create_sync()?;
|
||||
|
||||
|
|
@ -135,6 +141,7 @@ impl PluginProcess {
|
|||
.arg("--ocs-plugin-runner")
|
||||
.arg(&socket_name)
|
||||
.arg(cdylib_path)
|
||||
.env(PLUGIN_TOKEN_ENV, &token)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
|
|
@ -167,6 +174,19 @@ impl PluginProcess {
|
|||
}
|
||||
};
|
||||
|
||||
// Verify the runner presented the token it received through the
|
||||
// environment before allowing any host→runner requests.
|
||||
let mut guard = stream.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let handshake_stream = guard.as_mut().ok_or_else(shutdown_error)?;
|
||||
if let Err(e) = verify_runner_handshake(handshake_stream, &token) {
|
||||
drop(guard);
|
||||
if let Some(child) = child.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
reap(child);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
drop(guard);
|
||||
|
||||
// The runner first answers GetManifest and GetRibbon so the host can
|
||||
// build the UI without keeping the plugin object alive.
|
||||
let no_op = &mut |_| {};
|
||||
|
|
@ -559,6 +579,21 @@ fn distinct_runner_path(host: &Path) -> PathBuf {
|
|||
path
|
||||
}
|
||||
|
||||
/// Verify that the runner on the other end of `stream` presents `expected_token`.
|
||||
fn verify_runner_handshake(
|
||||
stream: &mut Stream,
|
||||
expected_token: &str,
|
||||
) -> Result<(), PluginError> {
|
||||
match recv::<RunnerHandshake>(stream) {
|
||||
Ok(RunnerHandshake::Token(ref presented)) if presented == expected_token => {
|
||||
eprintln!("[plugin] runner authenticated");
|
||||
Ok(())
|
||||
}
|
||||
Ok(RunnerHandshake::Token(_)) => Err(PluginError::Runner("authentication failed".into())),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique local socket name.
|
||||
fn generate_socket_name() -> String {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
|
@ -566,6 +601,20 @@ fn generate_socket_name() -> String {
|
|||
format!("ocs_plugin_{}_{}", std::process::id(), n)
|
||||
}
|
||||
|
||||
/// Generate a 32-byte random token for runner authentication.
|
||||
fn generate_token() -> Result<String, PluginError> {
|
||||
let mut bytes = [0u8; PLUGIN_TOKEN_LEN];
|
||||
getrandom::getrandom(&mut bytes)
|
||||
.map_err(|e| PluginError::Runner(format!("token generation failed: {e}")))?;
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut out = String::with_capacity(bytes.len() * 2);
|
||||
for b in bytes {
|
||||
out.push(HEX[(b >> 4) as usize] as char);
|
||||
out.push(HEX[(b & 0x0f) as usize] as char);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -594,6 +643,7 @@ mod timeout_tests {
|
|||
use crate::host::{DocumentReader, HostApi, ReaderEntity};
|
||||
use crate::ipc::protocol::{
|
||||
HostRequest, HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost,
|
||||
RunnerHandshake,
|
||||
};
|
||||
use crate::ipc::transport::{recv, send};
|
||||
use crate::ribbon::owned::OwnedPluginManifest;
|
||||
|
|
@ -858,4 +908,32 @@ mod timeout_tests {
|
|||
None => std::env::remove_var("OCS_PLUGIN_CALL_TIMEOUT_SECS"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_handshake_wrong_token_is_rejected() {
|
||||
let (mut host_stream, runner_stream) = connected_pair();
|
||||
let _runner = thread::spawn(move || {
|
||||
let mut peer = runner_stream;
|
||||
send(&mut peer, &RunnerHandshake::Token("wrong-token".to_string()))
|
||||
.expect("send handshake");
|
||||
});
|
||||
let result = verify_runner_handshake(&mut host_stream, "expected-token");
|
||||
assert!(
|
||||
matches!(result, Err(PluginError::Runner(ref s)) if s == "authentication failed"),
|
||||
"expected authentication failure, got {result:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runner_handshake_correct_token_is_accepted() {
|
||||
let (mut host_stream, runner_stream) = connected_pair();
|
||||
let token = "correct-token".to_string();
|
||||
let expected = token.clone();
|
||||
let _runner = thread::spawn(move || {
|
||||
let mut peer = runner_stream;
|
||||
send(&mut peer, &RunnerHandshake::Token(token)).expect("send handshake");
|
||||
});
|
||||
let result = verify_runner_handshake(&mut host_stream, &expected);
|
||||
assert!(result.is_ok(), "expected authentication success, got {result:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use std::rc::Rc;
|
|||
use crate::host::{BuiltinPlugin, InteractiveCommand};
|
||||
use crate::ipc::client::{InteractiveRegistry, IpcClient, PluginHostApi};
|
||||
use crate::ipc::protocol::{
|
||||
HostRequest, HostResponse, HostToPlugin, InteractiveEvent, PluginToHost,
|
||||
HostRequest, HostResponse, HostToPlugin, InteractiveEvent, PluginToHost, PLUGIN_TOKEN_ENV,
|
||||
};
|
||||
use crate::ipc::transport::{recv, send};
|
||||
use crate::ribbon::owned::OwnedRibbonGroup;
|
||||
|
|
@ -29,8 +29,17 @@ pub fn run(socket_name: &str, cdylib_path: &Path) -> Result<(), Box<dyn std::err
|
|||
let plugin = unsafe { load_plugin(cdylib_path)? };
|
||||
let interactive: InteractiveRegistry = Rc::new(RefCell::new(HashMap::new()));
|
||||
|
||||
let token = match std::env::var(PLUGIN_TOKEN_ENV) {
|
||||
Ok(t) => t,
|
||||
Err(_) => {
|
||||
eprintln!("[runner] missing {PLUGIN_TOKEN_ENV}; exiting");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let client = IpcClient::connect(socket_name)?;
|
||||
eprintln!("[runner] connected to host");
|
||||
client.send_handshake(&token)?;
|
||||
|
||||
loop {
|
||||
let msg: HostToPlugin = recv(&mut client.stream_ref())?;
|
||||
|
|
|
|||
Loading…
Reference in a new issue