use std::fmt::Display;
use crate::apperror::AppError;
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct PortMapping {
pub in_host: Option<String>,
pub in_port: Option<u16>,
pub out_host: Option<String>,
pub out_port: Option<u16>,
}
#[derive(Debug, PartialEq)]
pub(crate) struct Address {
pub port: u16,
pub address: Option<String>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct Rule {
mappings: Vec<PortMapping>,
}
impl PortMapping {
fn parse_host_port(host_port: &str) -> Option<(Option<String>, Option<u16>)> {
let (host, port) = host_port.split_once(':')?;
let port: Option<u16> = if port == "*" {
Option::None
} else {
let Ok(port) = port.parse() else {
return Option::None;
};
Some(port)
};
let host = if host == "*" {
Option::None
} else {
Option::Some(host.to_string())
};
Some((host, port))
}
pub fn parse(port_mapping: &str) -> Result<PortMapping, AppError> {
let message =
"rule parsing error: a rule must be of the form $IN_HOST:$IN_PORT-$OUT_HOST:$OUT_PORT";
let Some((in_part, out_part)) = port_mapping.split_once('-') else {
return Err(AppError::new(message));
};
let Some((in_host, in_port)) = PortMapping::parse_host_port(in_part) else {
return Err(AppError::new(message));
};
let Some((out_host, out_port)) = PortMapping::parse_host_port(out_part) else {
return Err(AppError::new(message));
};
Ok(PortMapping {
in_host,
in_port,
out_host,
out_port,
})
}
}
impl TryFrom<&str> for Rule {
type Error = AppError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Rule::parse(value)
}
}
impl Rule {
pub fn unrestricted() -> Rule {
let mappings: Vec<PortMapping> = vec![PortMapping {
in_host: None,
in_port: None,
out_host: None,
out_port: None,
}];
Rule { mappings }
}
pub fn parse(rule: &str) -> Result<Rule, AppError> {
let mut mappings: Vec<PortMapping> = Vec::new();
for port_mapping in rule.split(',') {
let port_mapping = PortMapping::parse(port_mapping)?;
mappings.push(port_mapping)
}
Ok(Rule { mappings })
}
pub(crate) fn evaluate(&self, port: u16, host: Option<String>) -> Option<Address> {
for mapping in &self.mappings {
if mapping.in_port.is_some() && mapping.in_port != Some(port) {
continue;
}
if mapping.in_host.is_some()
&& mapping.in_host != host
&& !(mapping.in_host == Some("".to_string()) && host.is_none())
{
continue;
}
return Some(Address {
port: mapping.out_port.unwrap_or(port),
address: mapping.out_host.clone().or(host),
});
}
None
}
}
impl Display for Rule {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for mapping in &self.mappings {
if let Some(in_host) = &mapping.in_host
&& mapping.in_port.is_none()
{
if in_host.is_empty() {
f.write_str("if (typeof host === 'undefined') // check for default host\n\t")?;
} else {
f.write_fmt(format_args!("if (host === '{in_host}')\n\t"))?;
}
}
if mapping.in_host.is_none()
&& let Some(in_port) = mapping.in_port
{
f.write_fmt(format_args!("if (port === {in_port})\n\t"))?;
}
if let Some(in_host) = &mapping.in_host
&& let Some(in_port) = mapping.in_port
{
if in_host.is_empty() {
f.write_fmt(format_args!("if (typeof host === 'undefined' && port === {in_port})) // check for default host\n\t"))?;
} else {
f.write_fmt(format_args!(
"if (host === '{in_host}' && port === {in_port})\n\t"
))?;
}
}
if mapping.out_host.is_none() && mapping.out_port.is_none() {
f.write_str("return { host: host, port: port };\n")?;
}
if let Some(out_host) = &mapping.out_host
&& mapping.out_port.is_none()
{
f.write_fmt(format_args!(
"return {{ host: '{out_host}', port: port }};\n"
))?;
}
if let Some(out_port) = &mapping.out_port
&& mapping.out_host.is_none()
{
f.write_fmt(format_args!("return {{ host: host, port: {out_port} }};\n"))?;
}
if let Some(out_port) = &mapping.out_port
&& let Some(out_host) = &mapping.out_host
{
f.write_fmt(format_args!(
"return {{ host: '{out_host}', port: {out_port} }};\n"
))?;
}
}
Ok(())
}
}
#[cfg(test)]
mod print_tests {
use super::*;
#[test]
fn describe_rule() {
let expected = "if (host === '127.0.0.1' && port === 1)\n\treturn { host: 'localhost', port: 2 };\nreturn { host: host, port: port };\n";
let rule = Rule::parse("127.0.0.1:1-localhost:2,*:*-*:*").unwrap();
assert_eq!(rule.to_string(), expected);
}
}
#[cfg(test)]
mod parse_tests {
use super::*;
#[test]
fn empty() {
assert_eq!(
Rule::parse("").unwrap_err().to_string(),
"AppError: rule parsing error: a rule must be of the form $IN_HOST:$IN_PORT-$OUT_HOST:$OUT_PORT"
)
}
#[test]
fn wildcard() {
assert_eq!(
Rule::parse("*:*-*:*").unwrap().mappings,
vec![PortMapping {
in_host: None,
out_host: None,
in_port: None,
out_port: None,
}]
)
}
#[test]
fn mapped() {
assert_eq!(
Rule::parse("127.0.0.1:1-localhost:2").unwrap().mappings,
vec![PortMapping {
in_host: Some("127.0.0.1".to_string()),
out_host: Some("localhost".to_string()),
in_port: Some(1),
out_port: Some(2),
}]
)
}
#[test]
fn multiple() {
assert_eq!(
Rule::parse("127.0.0.1:1-localhost:2,*:*-*:*")
.unwrap()
.mappings,
vec![
PortMapping {
in_host: Some("127.0.0.1".to_string()),
out_host: Some("localhost".to_string()),
in_port: Some(1),
out_port: Some(2),
},
PortMapping {
in_host: None,
out_host: None,
in_port: None,
out_port: None,
}
]
)
}
#[test]
fn with_empty_in_host() {
assert_eq!(
Rule::parse(":*-*:*").unwrap().mappings,
vec![PortMapping {
in_host: Some("".to_string()),
out_host: None,
in_port: None,
out_port: None,
}]
)
}
#[test]
fn with_minus_in_dest_host() {
assert_eq!(
Rule::parse(":*-target-server-name:*").unwrap().mappings,
vec![PortMapping {
in_host: Some("".to_string()),
out_host: Some("target-server-name".to_string()),
in_port: None,
out_port: None,
}]
)
}
}
#[cfg(test)]
mod evaluate_tests {
use super::*;
fn get_rule() -> Rule {
Rule {
mappings: vec![
PortMapping {
in_host: None,
out_host: None,
in_port: Some(1),
out_port: None,
},
PortMapping {
in_host: None,
out_host: None,
in_port: Some(2),
out_port: Some(102),
},
PortMapping {
in_host: Some("".to_string()),
out_host: None,
in_port: Some(3),
out_port: Some(105),
},
PortMapping {
in_host: Some("192.168.1.1".to_string()),
out_host: None,
in_port: Some(3),
out_port: Some(103),
},
PortMapping {
in_host: Some("127.0.0.1".to_string()),
out_host: None,
in_port: Some(3),
out_port: Some(104),
},
PortMapping {
in_host: Some("localhost".to_string()),
out_host: Some("127.0.0.1".to_string()),
in_port: Some(3),
out_port: None,
},
PortMapping {
in_host: Some("192.168.1.2".to_string()),
out_host: Some("192.168.1.3".to_string()),
in_port: Some(3),
out_port: Some(103),
},
PortMapping {
in_host: None,
out_host: None,
in_port: Some(1),
out_port: Some(0),
},
],
}
}
#[test]
fn t1() {
assert_eq!(
get_rule().evaluate(3, None),
Some(Address {
address: None,
port: 105
})
)
}
#[test]
fn t2() {
assert_eq!(
get_rule().evaluate(1, Some("localhost".to_string())),
Some(Address {
address: Some("localhost".to_string()),
port: 1
})
)
}
#[test]
fn t3() {
assert_eq!(
get_rule().evaluate(1, Some("127.0.0.1".to_string())),
Some(Address {
address: Some("127.0.0.1".to_string()),
port: 1
})
)
}
#[test]
fn t4() {
assert_eq!(
get_rule().evaluate(2, Some("test".to_string())),
Some(Address {
address: Some("test".to_string()),
port: 102
})
)
}
#[test]
fn t5() {
assert_eq!(
get_rule().evaluate(3, Some("192.168.1.1".to_string())),
Some(Address {
address: Some("192.168.1.1".to_string()),
port: 103
})
)
}
#[test]
fn t6() {
assert_eq!(
get_rule().evaluate(3, Some("127.0.0.1".to_string())),
Some(Address {
address: Some("127.0.0.1".to_string()),
port: 104
})
)
}
#[test]
fn t7() {
assert_eq!(
get_rule().evaluate(3, Some("localhost".to_string())),
Some(Address {
address: Some("127.0.0.1".to_string()),
port: 3
})
)
}
#[test]
fn t8() {
assert_eq!(
get_rule().evaluate(3, Some("192.168.1.2".to_string())),
Some(Address {
address: Some("192.168.1.3".to_string()),
port: 103
})
)
}
#[test]
fn t9() {
assert_eq!(get_rule().evaluate(3, Some("test".to_string())), None)
}
#[test]
fn t10() {
assert_eq!(get_rule().evaluate(4, Some("localhost".to_string())), None)
}
}