use crate::config::RoutingMode;
use crate::protocol::{RequestContext, RequestKind, RequestRouteClass};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandRoutingDecision {
InterceptAuth,
InterceptCapabilities,
Forward,
RequireAuth,
SwitchToStateful,
Reject,
}
#[must_use]
pub fn decide_request_routing(
request: &RequestContext,
is_authenticated: bool,
auth_enabled: bool,
routing_mode: RoutingMode,
) -> CommandRoutingDecision {
match request.kind() {
RequestKind::AuthInfo => CommandRoutingDecision::InterceptAuth,
RequestKind::Capabilities => CommandRoutingDecision::InterceptCapabilities,
RequestKind::Quit => CommandRoutingDecision::Forward,
_ => match request.route_class() {
RequestRouteClass::ArticleByMessageId | RequestRouteClass::Stateless => {
if is_authenticated || !auth_enabled {
CommandRoutingDecision::Forward
} else {
CommandRoutingDecision::RequireAuth
}
}
RequestRouteClass::Stateful if routing_mode == RoutingMode::Hybrid => {
if is_authenticated || !auth_enabled {
CommandRoutingDecision::SwitchToStateful
} else {
CommandRoutingDecision::RequireAuth
}
}
RequestRouteClass::Stateful | RequestRouteClass::Reject => {
if auth_enabled && !is_authenticated {
CommandRoutingDecision::RequireAuth
} else {
CommandRoutingDecision::Reject
}
}
RequestRouteClass::Local => CommandRoutingDecision::Reject,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decide_command_routing(
command: &str,
is_authenticated: bool,
auth_enabled: bool,
routing_mode: RoutingMode,
) -> CommandRoutingDecision {
let request = RequestContext::parse(command.as_bytes()).expect("valid request line");
decide_request_routing(&request, is_authenticated, auth_enabled, routing_mode)
}
#[test]
fn test_decide_routing_auth_commands_always_intercepted() {
assert_eq!(
decide_command_routing("AUTHINFO USER test", true, true, RoutingMode::PerCommand),
CommandRoutingDecision::InterceptAuth
);
assert_eq!(
decide_command_routing("AUTHINFO USER test", false, true, RoutingMode::PerCommand),
CommandRoutingDecision::InterceptAuth
);
assert_eq!(
decide_command_routing("AUTHINFO USER test", false, false, RoutingMode::Stateful),
CommandRoutingDecision::InterceptAuth
);
}
#[test]
fn test_decide_routing_forward_when_authenticated() {
assert_eq!(
decide_command_routing("LIST", true, true, RoutingMode::PerCommand),
CommandRoutingDecision::Forward
);
assert_eq!(
decide_command_routing("LIST", true, false, RoutingMode::PerCommand),
CommandRoutingDecision::Forward
);
}
#[test]
fn test_decide_routing_forward_when_auth_disabled() {
assert_eq!(
decide_command_routing("LIST", false, false, RoutingMode::PerCommand),
CommandRoutingDecision::Forward
);
}
#[test]
fn test_decide_routing_require_auth_when_needed() {
assert_eq!(
decide_command_routing("LIST", false, true, RoutingMode::PerCommand),
CommandRoutingDecision::RequireAuth
);
}
#[test]
fn test_decide_routing_switch_to_stateful_in_hybrid_mode() {
assert_eq!(
decide_command_routing("GROUP alt.test", true, false, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful
);
assert_eq!(
decide_command_routing("XOVER 1-100", false, false, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful
);
}
#[test]
fn test_decide_routing_hybrid_stateful_requires_auth_when_enabled() {
assert_eq!(
decide_command_routing("GROUP alt.test", false, true, RoutingMode::Hybrid),
CommandRoutingDecision::RequireAuth
);
assert_eq!(
decide_command_routing("XOVER 1-100", false, true, RoutingMode::Hybrid),
CommandRoutingDecision::RequireAuth
);
assert_eq!(
decide_command_routing("GROUP alt.test", true, true, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful
);
}
#[test]
fn test_decide_routing_hybrid_unknown_extensions_require_auth_when_enabled() {
let request = RequestContext::parse(b"XFOO arg\r\n").expect("valid request line");
assert_eq!(
decide_request_routing(&request, false, true, RoutingMode::Hybrid),
CommandRoutingDecision::RequireAuth
);
assert_eq!(
decide_request_routing(&request, true, true, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful
);
}
#[test]
fn test_decide_routing_reject_in_per_command_mode() {
assert_eq!(
decide_command_routing("GROUP alt.test", true, false, RoutingMode::PerCommand),
CommandRoutingDecision::Reject
);
}
#[test]
fn test_decide_routing_requires_auth_before_rejecting_unsupported_commands() {
for mode in [
RoutingMode::PerCommand,
RoutingMode::Hybrid,
RoutingMode::Stateful,
] {
assert_eq!(
decide_command_routing("POST", false, true, mode),
CommandRoutingDecision::RequireAuth,
"POST should be auth-gated before command policy is revealed in {mode:?}"
);
assert_eq!(
decide_command_routing("STARTTLS", false, true, mode),
CommandRoutingDecision::RequireAuth,
"STARTTLS should be auth-gated before command policy is revealed in {mode:?}"
);
}
assert_eq!(
decide_command_routing("POST", true, true, RoutingMode::PerCommand),
CommandRoutingDecision::Reject
);
assert_eq!(
decide_command_routing("STARTTLS", true, true, RoutingMode::Stateful),
CommandRoutingDecision::Reject
);
}
#[test]
fn test_decide_routing_reject_in_stateful_mode() {
assert_eq!(
decide_command_routing("POST", true, false, RoutingMode::Stateful),
CommandRoutingDecision::Reject
);
}
#[test]
fn test_decide_routing_capabilities_always_intercepted() {
assert_eq!(
decide_command_routing("CAPABILITIES", false, true, RoutingMode::PerCommand),
CommandRoutingDecision::InterceptCapabilities,
"CAPABILITIES should be intercepted even when auth required"
);
assert_eq!(
decide_command_routing("CAPABILITIES", true, true, RoutingMode::PerCommand),
CommandRoutingDecision::InterceptCapabilities,
"CAPABILITIES should be intercepted when authenticated"
);
assert_eq!(
decide_command_routing("CAPABILITIES", false, false, RoutingMode::Hybrid),
CommandRoutingDecision::InterceptCapabilities,
"CAPABILITIES should be intercepted in hybrid mode"
);
assert_eq!(
decide_command_routing("CAPABILITIES", true, false, RoutingMode::Stateful),
CommandRoutingDecision::InterceptCapabilities,
"CAPABILITIES should be intercepted in stateful mode"
);
}
#[test]
fn test_decide_routing_hybrid_mode_stateless_forwarded() {
assert_eq!(
decide_command_routing("LIST", true, false, RoutingMode::Hybrid),
CommandRoutingDecision::Forward
);
}
#[test]
fn test_decide_routing_hybrid_mode_reject_non_stateful() {
assert_eq!(
decide_command_routing("POST", true, false, RoutingMode::Hybrid),
CommandRoutingDecision::Reject
);
}
#[test]
fn test_decide_routing_all_modes_with_stateful_commands() {
let stateful_commands = vec!["GROUP alt.test", "NEXT", "LAST", "XOVER 1-100"];
for cmd in stateful_commands {
assert_eq!(
decide_command_routing(cmd, true, false, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful,
"Failed for command: {cmd}"
);
assert_eq!(
decide_command_routing(cmd, true, false, RoutingMode::PerCommand),
CommandRoutingDecision::Reject,
"Failed for command: {cmd}"
);
assert_eq!(
decide_command_routing(cmd, true, false, RoutingMode::Stateful),
CommandRoutingDecision::Reject,
"Failed for command: {cmd}"
);
}
}
#[test]
fn test_decide_routing_auth_flow_progression() {
assert_eq!(
decide_command_routing("LIST", false, true, RoutingMode::PerCommand),
CommandRoutingDecision::RequireAuth
);
assert_eq!(
decide_command_routing("LIST", true, true, RoutingMode::PerCommand),
CommandRoutingDecision::Forward
);
}
#[test]
fn test_decide_request_routing_unknown_extensions_are_stateful() {
let request = RequestContext::parse(b"XFOO arg\r\n").expect("valid request line");
assert_eq!(
decide_request_routing(&request, true, false, RoutingMode::Hybrid),
CommandRoutingDecision::SwitchToStateful
);
assert_eq!(
decide_request_routing(&request, false, true, RoutingMode::Hybrid),
CommandRoutingDecision::RequireAuth
);
assert_eq!(
decide_request_routing(&request, true, false, RoutingMode::PerCommand),
CommandRoutingDecision::Reject
);
}
}