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 }
|
libloading = { version = "0.8", optional = true }
|
||||||
memmap2 = { version = "0.9", optional = true }
|
memmap2 = { version = "0.9", optional = true }
|
||||||
rkyv = { version = "0.7", features = ["validation", "std"], optional = true }
|
rkyv = { version = "0.7", features = ["validation", "std"], optional = true }
|
||||||
|
getrandom = { version = "0.2", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# Enables the runtime host surface (`HostApi` trait) and the out-of-process
|
# Enables the runtime host surface (`HostApi` trait) and the out-of-process
|
||||||
# plugin runtime. The OpenCADStudio binary turns this on; pure-data consumers
|
# plugin runtime. The OpenCADStudio binary turns this on; pure-data consumers
|
||||||
# leave it off.
|
# 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::host::{DocumentReader, HostApi, InteractiveCommand, ReaderEntity};
|
||||||
use crate::ipc::protocol::{
|
use crate::ipc::protocol::{
|
||||||
HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost,
|
HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost, RunnerHandshake,
|
||||||
};
|
};
|
||||||
use crate::ipc::transport::{recv, send};
|
use crate::ipc::transport::{recv, send};
|
||||||
use crate::shm::{DocumentViewInfo, SharedDocumentReader};
|
use crate::shm::{DocumentViewInfo, SharedDocumentReader};
|
||||||
|
|
@ -46,6 +46,14 @@ impl IpcClient {
|
||||||
self.stream.borrow_mut()
|
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
|
/// Send a plugin request and wait for the matching response. Any nested
|
||||||
/// host requests that arrive while we are waiting are treated as errors.
|
/// host requests that arrive while we are waiting are treated as errors.
|
||||||
pub fn request(
|
pub fn request(
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,20 @@ pub enum InteractiveEvent {
|
||||||
ObjectPick { handle: Handle, pt: [f64; 3] },
|
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.
|
/// Requests the host sends to the plugin runner.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum HostRequest {
|
pub enum HostRequest {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ use interprocess::local_socket::{GenericNamespaced, ListenerOptions, Stream, ToN
|
||||||
|
|
||||||
use crate::host::{CommandStep, HostApi};
|
use crate::host::{CommandStep, HostApi};
|
||||||
use crate::ipc::protocol::{
|
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::server::handle_plugin_request;
|
||||||
use crate::ipc::transport::{recv, send};
|
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.
|
/// Default maximum time to wait for a plugin call to respond.
|
||||||
const CALL_TIMEOUT_DEFAULT: Duration = Duration::from_secs(30);
|
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 {
|
fn call_timeout() -> Duration {
|
||||||
std::env::var("OCS_PLUGIN_CALL_TIMEOUT_SECS")
|
std::env::var("OCS_PLUGIN_CALL_TIMEOUT_SECS")
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -128,6 +132,8 @@ impl PluginProcess {
|
||||||
cdylib_path.display()
|
cdylib_path.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let token = generate_token()?;
|
||||||
|
|
||||||
// Create the listener before spawning so the runner can connect immediately.
|
// Create the listener before spawning so the runner can connect immediately.
|
||||||
let listener = ListenerOptions::new().name(socket_name_ref).create_sync()?;
|
let listener = ListenerOptions::new().name(socket_name_ref).create_sync()?;
|
||||||
|
|
||||||
|
|
@ -135,6 +141,7 @@ impl PluginProcess {
|
||||||
.arg("--ocs-plugin-runner")
|
.arg("--ocs-plugin-runner")
|
||||||
.arg(&socket_name)
|
.arg(&socket_name)
|
||||||
.arg(cdylib_path)
|
.arg(cdylib_path)
|
||||||
|
.env(PLUGIN_TOKEN_ENV, &token)
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(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
|
// The runner first answers GetManifest and GetRibbon so the host can
|
||||||
// build the UI without keeping the plugin object alive.
|
// build the UI without keeping the plugin object alive.
|
||||||
let no_op = &mut |_| {};
|
let no_op = &mut |_| {};
|
||||||
|
|
@ -559,6 +579,21 @@ fn distinct_runner_path(host: &Path) -> PathBuf {
|
||||||
path
|
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.
|
/// Generate a unique local socket name.
|
||||||
fn generate_socket_name() -> String {
|
fn generate_socket_name() -> String {
|
||||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
@ -566,6 +601,20 @@ fn generate_socket_name() -> String {
|
||||||
format!("ocs_plugin_{}_{}", std::process::id(), n)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
@ -594,6 +643,7 @@ mod timeout_tests {
|
||||||
use crate::host::{DocumentReader, HostApi, ReaderEntity};
|
use crate::host::{DocumentReader, HostApi, ReaderEntity};
|
||||||
use crate::ipc::protocol::{
|
use crate::ipc::protocol::{
|
||||||
HostRequest, HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost,
|
HostRequest, HostResponse, HostToPlugin, PluginRequest, PluginResponse, PluginToHost,
|
||||||
|
RunnerHandshake,
|
||||||
};
|
};
|
||||||
use crate::ipc::transport::{recv, send};
|
use crate::ipc::transport::{recv, send};
|
||||||
use crate::ribbon::owned::OwnedPluginManifest;
|
use crate::ribbon::owned::OwnedPluginManifest;
|
||||||
|
|
@ -858,4 +908,32 @@ mod timeout_tests {
|
||||||
None => std::env::remove_var("OCS_PLUGIN_CALL_TIMEOUT_SECS"),
|
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::host::{BuiltinPlugin, InteractiveCommand};
|
||||||
use crate::ipc::client::{InteractiveRegistry, IpcClient, PluginHostApi};
|
use crate::ipc::client::{InteractiveRegistry, IpcClient, PluginHostApi};
|
||||||
use crate::ipc::protocol::{
|
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::ipc::transport::{recv, send};
|
||||||
use crate::ribbon::owned::OwnedRibbonGroup;
|
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 plugin = unsafe { load_plugin(cdylib_path)? };
|
||||||
let interactive: InteractiveRegistry = Rc::new(RefCell::new(HashMap::new()));
|
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)?;
|
let client = IpcClient::connect(socket_name)?;
|
||||||
eprintln!("[runner] connected to host");
|
eprintln!("[runner] connected to host");
|
||||||
|
client.send_handshake(&token)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let msg: HostToPlugin = recv(&mut client.stream_ref())?;
|
let msg: HostToPlugin = recv(&mut client.stream_ref())?;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue