use std::time::Duration;
use liminal::protocol::{
CONVERSATION_REPLY_REQUESTED_FLAG, Frame, MessageEnvelope, PUBLISH_DELIVERED_FLAG,
ProtocolError, ProtocolVersion, SchemaId as ProtocolSchemaId, WorkerRegisterOutcome,
WorkerRegistration, negotiate_version,
};
use super::conversation::ConnectionConversation;
use super::services::ConnectionServices;
use super::state::{ConnectionProcessState, FrameAction};
use super::supervisor::ConnectionRuntime;
const SERVER_ERROR_CODE: u16 = 0xFFFF;
const SUPPORTED_PROTOCOL: ProtocolVersion = ProtocolVersion::new(1, 0);
pub(super) fn apply_frame(
pid: u64,
runtime: &ConnectionRuntime,
state: &mut ConnectionProcessState,
frame: Frame,
) -> FrameAction {
if runtime.auth_token().is_some() && !state.authenticated && !permitted_before_auth(&frame) {
return FrameAction::Close;
}
let services = runtime.services();
match frame {
Frame::Connect {
min_version,
max_version,
auth_token,
..
} => connect_response(runtime, state, min_version, max_version, &auth_token),
Frame::Disconnect { .. } => FrameAction::Close,
Frame::Ping { .. } => FrameAction::Respond(Frame::Pong { flags: 0 }),
Frame::Publish {
stream_id,
channel,
envelope,
idempotency_key,
..
} => {
if runtime.notifier_channel_publish(pid, &channel, &envelope.payload) {
FrameAction::NoResponse
} else {
publish_response(
services,
stream_id,
&channel,
&envelope,
idempotency_key.as_deref(),
)
}
}
Frame::Subscribe {
stream_id,
channel,
accepted_schemas,
..
} => subscribe_response(services, state, stream_id, &channel, &accepted_schemas),
Frame::Unsubscribe {
subscription_id, ..
} => unsubscribe_response(services, state, subscription_id),
Frame::ConversationOpen {
conversation_id,
subject,
..
} => conversation_open(services, state, conversation_id, &subject),
Frame::ConversationMessage {
flags,
stream_id,
conversation_id,
envelope,
} => conversation_message(
services,
state,
flags,
stream_id,
conversation_id,
&envelope,
),
Frame::ConversationClose {
conversation_id, ..
} => conversation_close(services, state, conversation_id),
Frame::PushReply {
correlation_id,
payload,
..
} => {
runtime.resolve_push(correlation_id, payload);
FrameAction::NoResponse
}
Frame::WorkerRegister { registration, .. } => {
worker_register_response(pid, runtime, registration)
}
Frame::Push { .. }
| Frame::Deliver { .. }
| Frame::WorkerRegisterAck { .. }
| Frame::Unknown { .. }
| Frame::ConnectAck { .. }
| Frame::ConnectError { .. }
| Frame::SubscribeAck { .. }
| Frame::SubscribeError { .. }
| Frame::PublishAck { .. }
| Frame::PublishError { .. }
| Frame::ConversationError { .. }
| Frame::Accept { .. }
| Frame::Defer { .. }
| Frame::Reject { .. }
| Frame::Pong { .. } => FrameAction::NoResponse,
}
}
fn worker_register_response(
pid: u64,
runtime: &ConnectionRuntime,
registration: WorkerRegistration,
) -> FrameAction {
if let Some(notifier) = runtime.notifier() {
if let Err(error) = notifier.on_worker_registered(pid, ®istration) {
return worker_register_rejected(error.to_string());
}
}
if let Err(error) = runtime.set_registration(pid, registration) {
if let Some(notifier) = runtime.notifier() {
notifier.on_worker_unregistered(pid);
}
return worker_register_rejected(error.to_string());
}
FrameAction::Respond(Frame::WorkerRegisterAck {
flags: 0,
outcome: WorkerRegisterOutcome::Accepted,
})
}
const fn worker_register_rejected(reason: String) -> FrameAction {
FrameAction::Respond(Frame::WorkerRegisterAck {
flags: 0,
outcome: WorkerRegisterOutcome::Rejected { reason },
})
}
const fn permitted_before_auth(frame: &Frame) -> bool {
matches!(
frame,
Frame::Connect { .. }
| Frame::Disconnect { .. }
| Frame::Ping { .. }
| Frame::Push { .. }
| Frame::Deliver { .. }
| Frame::WorkerRegisterAck { .. }
| Frame::Unknown { .. }
| Frame::ConnectAck { .. }
| Frame::ConnectError { .. }
| Frame::SubscribeAck { .. }
| Frame::SubscribeError { .. }
| Frame::PublishAck { .. }
| Frame::PublishError { .. }
| Frame::ConversationError { .. }
| Frame::Accept { .. }
| Frame::Defer { .. }
| Frame::Reject { .. }
| Frame::Pong { .. }
)
}
fn connect_response(
runtime: &ConnectionRuntime,
state: &mut ConnectionProcessState,
min_version: ProtocolVersion,
max_version: ProtocolVersion,
auth_token: &[u8],
) -> FrameAction {
if let Some(expected) = runtime.auth_token() {
if !constant_time_eq(expected, auth_token) {
let error = ProtocolError::AuthenticationFailure {
message: Some("connection authentication token rejected".to_owned()),
};
return FrameAction::RespondThenClose(Frame::ConnectError {
flags: 0,
reason_code: error.reason_code(),
message: error.message().map(str::to_owned),
});
}
}
state.authenticated = true;
match negotiate_version(min_version, max_version, &[SUPPORTED_PROTOCOL]) {
Ok(selected_version) => FrameAction::Respond(Frame::ConnectAck {
flags: 0,
selected_version,
capabilities: 0,
}),
Err(error) => FrameAction::Respond(Frame::ConnectError {
flags: 0,
reason_code: error.reason_code(),
message: error.message().map(str::to_owned),
}),
}
}
fn constant_time_eq(expected: &[u8], candidate: &[u8]) -> bool {
let mut difference = u8::from(expected.len() != candidate.len());
for (left, right) in expected.iter().zip(candidate.iter()) {
difference |= left ^ right;
}
difference == 0
}
fn publish_response(
services: &dyn ConnectionServices,
stream_id: u32,
channel: &str,
envelope: &MessageEnvelope,
idempotency_key: Option<&str>,
) -> FrameAction {
match services.publish(channel, envelope, idempotency_key) {
Ok(outcome) => FrameAction::Respond(Frame::PublishAck {
flags: if outcome.delivered {
PUBLISH_DELIVERED_FLAG
} else {
0
},
stream_id,
message_id: outcome.message_id,
}),
Err(error) => FrameAction::Respond(Frame::PublishError {
flags: 0,
stream_id,
reason_code: SERVER_ERROR_CODE,
message: Some(error.to_string()),
}),
}
}
fn subscribe_response(
services: &dyn ConnectionServices,
state: &mut ConnectionProcessState,
stream_id: u32,
channel: &str,
accepted_schemas: &[ProtocolSchemaId],
) -> FrameAction {
match services.subscribe(channel, accepted_schemas) {
Ok(mut subscription) => {
subscription.set_stream_id(stream_id);
let subscription_id = subscription.id();
let selected_schema = subscription.selected_schema();
state.subscriptions.insert(subscription_id, subscription);
FrameAction::Respond(Frame::SubscribeAck {
flags: 0,
stream_id,
subscription_id,
selected_schema,
})
}
Err(error) => FrameAction::Respond(Frame::SubscribeError {
flags: 0,
stream_id,
reason_code: SERVER_ERROR_CODE,
message: Some(error.to_string()),
}),
}
}
fn unsubscribe_response(
services: &dyn ConnectionServices,
state: &mut ConnectionProcessState,
subscription_id: u64,
) -> FrameAction {
if let Some(subscription) = state.subscriptions.remove(&subscription_id) {
state.delivery_seqs.remove(&subscription_id);
state.held_deliveries.remove(&subscription_id);
if let Err(error) = services.unsubscribe(subscription) {
tracing::warn!(subscription_id, %error, "liminal unsubscribe failed");
}
}
FrameAction::NoResponse
}
fn conversation_open(
services: &dyn ConnectionServices,
state: &mut ConnectionProcessState,
conversation_id: u64,
subject: &str,
) -> FrameAction {
match services.open_conversation(conversation_id, subject) {
Ok(conversation) => {
state.conversations.insert(conversation_id, conversation);
FrameAction::NoResponse
}
Err(error) => FrameAction::Respond(Frame::ConversationError {
flags: 0,
stream_id: 1,
conversation_id,
reason_code: SERVER_ERROR_CODE,
message: Some(error.to_string()),
}),
}
}
const CONVERSATION_REPLY_TIMEOUT: Duration = Duration::from_secs(5);
fn conversation_message(
services: &dyn ConnectionServices,
state: &ConnectionProcessState,
flags: u8,
stream_id: u32,
conversation_id: u64,
envelope: &MessageEnvelope,
) -> FrameAction {
let Some(conversation) = state.conversations.get(&conversation_id) else {
return conversation_error(
stream_id,
conversation_id,
"conversation is not open on this connection",
);
};
if let Err(error) = services.conversation_message(conversation, envelope) {
return conversation_error(stream_id, conversation_id, &error.to_string());
}
if flags & CONVERSATION_REPLY_REQUESTED_FLAG == 0 {
return FrameAction::NoResponse;
}
conversation_reply(conversation, stream_id, conversation_id)
}
fn conversation_reply(
conversation: &ConnectionConversation,
stream_id: u32,
conversation_id: u64,
) -> FrameAction {
match conversation.receive_reply(CONVERSATION_REPLY_TIMEOUT) {
Ok(reply) => FrameAction::Respond(Frame::ConversationMessage {
flags: CONVERSATION_REPLY_REQUESTED_FLAG,
stream_id,
conversation_id,
envelope: reply,
}),
Err(error) => conversation_error(stream_id, conversation_id, &error.to_string()),
}
}
fn conversation_error(stream_id: u32, conversation_id: u64, message: &str) -> FrameAction {
FrameAction::Respond(Frame::ConversationError {
flags: 0,
stream_id,
conversation_id,
reason_code: SERVER_ERROR_CODE,
message: Some(message.to_owned()),
})
}
fn conversation_close(
services: &dyn ConnectionServices,
state: &mut ConnectionProcessState,
conversation_id: u64,
) -> FrameAction {
if let Some(conversation) = state.conversations.remove(&conversation_id) {
if let Err(error) = services.close_conversation(conversation) {
tracing::warn!(conversation_id, %error, "liminal conversation close failed");
}
}
FrameAction::NoResponse
}
#[cfg(test)]
#[path = "process_tests.rs"]
mod tests;