Add interactive client terminal transport
This commit is contained in:
parent
d597b05484
commit
6816f0632e
1 changed files with 1454 additions and 59 deletions
|
|
@ -22,7 +22,8 @@ use lazaret_core::protocol::{
|
|||
};
|
||||
use lazaret_core::records::{
|
||||
ClientEnrollmentRequest, ClientEnrollmentResponse, ControlRequest, ControlResponse,
|
||||
DurableDecision, GenerationIdentity, NonInteractiveSessionMessage,
|
||||
DurableDecision, GenerationIdentity, InteractiveSessionBatch, InteractiveSessionEvent,
|
||||
InteractiveSessionMessage, InteractiveSessionRequest, NonInteractiveSessionMessage,
|
||||
NonInteractiveSessionRequest, ProtocolCredential, ProtocolCredentialRole,
|
||||
ProtocolPublicationIdentity, ProtocolSubjectId, ProtocolVersion, PublicationOwnership,
|
||||
SandboxCredentialBundle, Session, SessionBinding, SessionExit, SessionPhase,
|
||||
|
|
@ -834,6 +835,7 @@ pub struct PublishedControlFace {
|
|||
server_config: Arc<ServerConfig>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
|
||||
}
|
||||
|
||||
pub trait NonInteractiveSessionExecutor: Send + Sync {
|
||||
|
|
@ -858,6 +860,35 @@ impl NonInteractiveSessionExecutor for ExecutionUnavailable {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait InteractiveControlSession: Send {
|
||||
/// # Errors
|
||||
/// Returns an exact-session terminal, prompt-routing, or transport failure.
|
||||
fn exchange(
|
||||
&mut self,
|
||||
event: InteractiveSessionEvent,
|
||||
) -> Result<InteractiveSessionBatch, ControlError>;
|
||||
}
|
||||
|
||||
pub trait InteractiveSessionExecutor: Send + Sync {
|
||||
/// # Errors
|
||||
/// Returns an acquisition or exact-session identity failure.
|
||||
fn start(
|
||||
&self,
|
||||
request: &InteractiveSessionRequest,
|
||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError>;
|
||||
}
|
||||
|
||||
struct InteractiveExecutionUnavailable;
|
||||
|
||||
impl InteractiveSessionExecutor for InteractiveExecutionUnavailable {
|
||||
fn start(
|
||||
&self,
|
||||
_request: &InteractiveSessionRequest,
|
||||
) -> Result<Box<dyn InteractiveControlSession>, ControlError> {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RunscNonInteractiveExecutor {
|
||||
program: OsString,
|
||||
global_arguments: Vec<OsString>,
|
||||
|
|
@ -1009,6 +1040,7 @@ pub struct StagedControlFace {
|
|||
server_config: Arc<ServerConfig>,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
|
||||
}
|
||||
|
||||
impl StagedControlFace {
|
||||
|
|
@ -1045,6 +1077,28 @@ impl StagedControlFace {
|
|||
anchor: TrustAnchor,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
) -> Result<Self, ControlError> {
|
||||
Self::stage_with_executors(
|
||||
runtime_domain_root,
|
||||
uid,
|
||||
anchor,
|
||||
registry,
|
||||
executor,
|
||||
Arc::new(InteractiveExecutionUnavailable),
|
||||
)
|
||||
}
|
||||
|
||||
/// Stages a control face with generation-bound command and terminal execution.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses the same invalid staging conditions as [`Self::stage`].
|
||||
pub fn stage_with_executors(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
registry: Arc<SessionRegistry>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
|
||||
) -> Result<Self, ControlError> {
|
||||
if registry.generation_identity != anchor.generation_identity {
|
||||
return Err(ControlError::StaleGeneration);
|
||||
|
|
@ -1098,6 +1152,7 @@ impl StagedControlFace {
|
|||
server_config,
|
||||
registry,
|
||||
executor,
|
||||
interactive_executor,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1156,6 +1211,7 @@ impl StagedControlFace {
|
|||
server_config: Arc::clone(&self.server_config),
|
||||
registry: Arc::clone(&self.registry),
|
||||
executor: Arc::clone(&self.executor),
|
||||
interactive_executor: Arc::clone(&self.interactive_executor),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1379,6 +1435,33 @@ impl PublishedControlFace {
|
|||
.expose()
|
||||
}
|
||||
|
||||
/// Publishes one generation control face with command and terminal execution.
|
||||
///
|
||||
/// # Errors
|
||||
/// Refuses the same invalid publication conditions as [`Self::publish`].
|
||||
pub fn publish_with_executors(
|
||||
runtime_domain_root: &Path,
|
||||
uid: u32,
|
||||
anchor: TrustAnchor,
|
||||
store: Arc<HostCoordinatorDomainStore>,
|
||||
executor: Arc<dyn NonInteractiveSessionExecutor>,
|
||||
interactive_executor: Arc<dyn InteractiveSessionExecutor>,
|
||||
) -> Result<Self, ControlError> {
|
||||
let registry = Arc::new(SessionRegistry::new(
|
||||
store,
|
||||
anchor.generation_identity.clone(),
|
||||
));
|
||||
StagedControlFace::stage_with_executors(
|
||||
runtime_domain_root,
|
||||
uid,
|
||||
anchor,
|
||||
registry,
|
||||
executor,
|
||||
interactive_executor,
|
||||
)?
|
||||
.expose()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn publication_identity(&self) -> &ProtocolPublicationIdentity {
|
||||
&self.publication_identity
|
||||
|
|
@ -1534,32 +1617,50 @@ impl PublishedControlFace {
|
|||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let ControlRequest::Execute(request) = request else {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
};
|
||||
if request.session_id != session_id
|
||||
|| request.argv.is_empty()
|
||||
|| request.argv[0].is_empty()
|
||||
|| request.argv.iter().any(|argument| argument.contains(&0))
|
||||
{
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let mut send_message = |message: NonInteractiveSessionMessage| {
|
||||
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
match request {
|
||||
ControlRequest::Execute(request) => {
|
||||
validate_command(request.session_id, session_id, &request.argv)?;
|
||||
let mut send_message = |message: NonInteractiveSessionMessage| {
|
||||
if matches!(message, NonInteractiveSessionMessage::Exit(_)) {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
require_non_interactive_session(&message, request.session_id)?;
|
||||
write_frame(stream, &message)
|
||||
};
|
||||
let outcome = self.executor.execute(&request, &mut send_message)?;
|
||||
require_session_id(outcome.session_id(), request.session_id)?;
|
||||
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
||||
}
|
||||
ControlRequest::ExecuteInteractive(request) => {
|
||||
validate_command(request.session_id, session_id, &request.argv)?;
|
||||
if request.initial_size.session_id != session_id {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
let mut terminal = self.interactive_executor.start(&request)?;
|
||||
let mut event = InteractiveSessionEvent::Poll { session_id };
|
||||
loop {
|
||||
let batch = terminal.exchange(event)?;
|
||||
require_interactive_batch(&batch, session_id)?;
|
||||
let complete = batch.messages.iter().any(|message| {
|
||||
matches!(
|
||||
message,
|
||||
InteractiveSessionMessage::Terminal(
|
||||
lazaret_core::records::SessionTerminalMessage::Exit(_)
|
||||
)
|
||||
)
|
||||
});
|
||||
write_frame(stream, &batch)?;
|
||||
if complete {
|
||||
break Ok(());
|
||||
}
|
||||
event = read_frame(stream, MAX_CONTROL_BYTES)?;
|
||||
require_interactive_event(&event, session_id)?;
|
||||
}
|
||||
}
|
||||
ControlRequest::Enroll(_) | ControlRequest::Register(_) => {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
require_non_interactive_session(&message, request.session_id)?;
|
||||
write_frame(stream, &message)
|
||||
};
|
||||
let outcome = self.executor.execute(&request, &mut send_message)?;
|
||||
if outcome.session_id() != request.session_id {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
require_non_interactive_session(
|
||||
&NonInteractiveSessionMessage::Exit(outcome),
|
||||
request.session_id,
|
||||
)?;
|
||||
write_frame(stream, &NonInteractiveSessionMessage::Exit(outcome))
|
||||
})();
|
||||
let disconnected = disconnect(session_id);
|
||||
result.and(disconnected)
|
||||
|
|
@ -1738,6 +1839,61 @@ fn require_non_interactive_session(
|
|||
}
|
||||
}
|
||||
|
||||
fn validate_command(
|
||||
actual: SessionId,
|
||||
expected: SessionId,
|
||||
argv: &[Vec<u8>],
|
||||
) -> Result<(), ControlError> {
|
||||
require_session_id(actual, expected)?;
|
||||
if argv.is_empty() || argv[0].is_empty() || argv.iter().any(|argument| argument.contains(&0)) {
|
||||
return Err(ControlError::InvalidRequest);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn require_session_id(actual: SessionId, expected: SessionId) -> Result<(), ControlError> {
|
||||
if actual == expected {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ControlError::InvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
fn require_interactive_event(
|
||||
event: &InteractiveSessionEvent,
|
||||
expected: SessionId,
|
||||
) -> Result<(), ControlError> {
|
||||
let actual = match event {
|
||||
InteractiveSessionEvent::Terminal(event) => match event {
|
||||
lazaret_core::records::SessionTerminalEvent::Input(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Resize(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Signal(event) => event.session_id,
|
||||
lazaret_core::records::SessionTerminalEvent::Close(event) => event.session_id,
|
||||
},
|
||||
InteractiveSessionEvent::PromptDecision { session_id, .. }
|
||||
| InteractiveSessionEvent::Poll { session_id } => *session_id,
|
||||
};
|
||||
require_session_id(actual, expected)
|
||||
}
|
||||
|
||||
fn require_interactive_batch(
|
||||
batch: &InteractiveSessionBatch,
|
||||
expected: SessionId,
|
||||
) -> Result<(), ControlError> {
|
||||
require_session_id(batch.session_id, expected)?;
|
||||
for message in &batch.messages {
|
||||
let actual = match message {
|
||||
InteractiveSessionMessage::Terminal(message) => match message {
|
||||
lazaret_core::records::SessionTerminalMessage::Output(output) => output.session_id,
|
||||
lazaret_core::records::SessionTerminalMessage::Exit(exit) => exit.session_id(),
|
||||
},
|
||||
InteractiveSessionMessage::Prompt(prompt) => prompt.session_id,
|
||||
};
|
||||
require_session_id(actual, expected)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn leaf_parameters(
|
||||
common_name: &str,
|
||||
usage: ExtendedKeyUsagePurpose,
|
||||
|
|
|
|||
Loading…
Reference in a new issue