use alkcall::channels::client::ChannelOpenError;
pub use crate::rfc1928::server::SocksServerError;
pub use crate::rfc1928::{ReplyError, SocksError};
#[derive(Debug, thiserror::Error)]
pub enum SocksOpenError {
#[error("socks5 open failed: {0}")]
Open(#[from] ChannelOpenError),
}
impl SocksOpenError {
pub fn open_ref(&self) -> &ChannelOpenError {
match self {
SocksOpenError::Open(e) => e,
}
}
pub fn reason(&self) -> Option<SocksEstablishmentReason> {
SocksEstablishmentReason::parse(self.open_ref().establishment_reason()?)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocksEstablishmentReason {
DialFailed,
UnknownResource,
ResourceShortage,
HandlerError,
Timeout,
}
impl SocksEstablishmentReason {
pub fn parse(reason: &str) -> Option<Self> {
match reason {
"dial_failed" => Some(Self::DialFailed),
"unknown_resource" => Some(Self::UnknownResource),
"resource_shortage" => Some(Self::ResourceShortage),
"handler_error" => Some(Self::HandlerError),
"timeout" => Some(Self::Timeout),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::DialFailed => "dial_failed",
Self::UnknownResource => "unknown_resource",
Self::ResourceShortage => "resource_shortage",
Self::HandlerError => "handler_error",
Self::Timeout => "timeout",
}
}
}
impl std::fmt::Display for SocksEstablishmentReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, thiserror::Error)]
pub enum SocksSessionError {
#[error("socks5 open failed: {0}")]
Open(#[from] SocksOpenError),
#[error("socks5: {0}")]
Rfc(#[from] SocksError),
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
#[cfg(test)]
mod tests {
use super::*;
use alkcall::protocol::wire::CallError;
fn open_failed_with(reason: &str) -> SocksOpenError {
SocksOpenError::Open(ChannelOpenError::CallFailed {
error: CallError::new("channel:open_failed", "establishment failed", false)
.with_details(serde_json::json!({ "reason": reason })),
})
}
#[test]
fn open_error_maps_the_adr_049_reason_vocabulary() {
for (wire, typed) in [
("dial_failed", SocksEstablishmentReason::DialFailed),
(
"unknown_resource",
SocksEstablishmentReason::UnknownResource,
),
(
"resource_shortage",
SocksEstablishmentReason::ResourceShortage,
),
("handler_error", SocksEstablishmentReason::HandlerError),
("timeout", SocksEstablishmentReason::Timeout),
] {
let err = open_failed_with(wire);
assert_eq!(err.reason(), Some(typed));
assert_eq!(typed.as_str(), wire);
}
}
#[test]
fn open_error_non_establishment_shapes_are_none() {
for code in ["FORBIDDEN", "INVALID_INPUT", "channel:too_many_channels"] {
let err = SocksOpenError::Open(ChannelOpenError::CallFailed {
error: CallError::new(code, "gate", false),
});
assert!(
err.reason().is_none(),
"{code} is not an establishment reason"
);
}
assert!(SocksOpenError::Open(ChannelOpenError::MissingChannelId)
.reason()
.is_none());
}
#[test]
fn unknown_reason_code_does_not_alias_a_variant() {
let err = open_failed_with("some_future_reason");
assert!(err.reason().is_none());
assert_eq!(
crate::params::establishment_reason(err.open_ref()),
Some("some_future_reason")
);
}
#[test]
fn session_error_surfaces_the_three_planes() {
let open = SocksSessionError::from(open_failed_with("dial_failed"));
assert!(matches!(open, SocksSessionError::Open(_)));
let refused =
SocksSessionError::from(SocksError::ReplyError(ReplyError::ConnectionNotAllowed));
assert!(matches!(refused, SocksSessionError::Rfc(_)));
let io = SocksSessionError::from(std::io::Error::new(
std::io::ErrorKind::UnexpectedEof,
"channel cut",
));
assert!(matches!(io, SocksSessionError::Io(_)));
}
#[test]
fn establishment_reason_display_renders_the_wire_code() {
for (typed, wire) in [
(SocksEstablishmentReason::DialFailed, "dial_failed"),
(
SocksEstablishmentReason::UnknownResource,
"unknown_resource",
),
(
SocksEstablishmentReason::ResourceShortage,
"resource_shortage",
),
(SocksEstablishmentReason::HandlerError, "handler_error"),
(SocksEstablishmentReason::Timeout, "timeout"),
] {
assert_eq!(typed.to_string(), wire);
}
}
}