pub mod host_info;
use crate::{exit, parse_arg::CliArgs, port_descriptions::PortDescriptions};
use std::{collections::HashSet, future::Future, net::IpAddr, pin::Pin};
use tokio::{net::TcpStream, sync::mpsc::Sender};
#[derive(Debug, Clone, Hash, Copy)]
struct PortMessage {
port: u16,
open: bool,
}
impl PortMessage {
fn can_spawn(self, chunk: u16, port_max: u16) -> Option<u16> {
self.port.checked_add(chunk).filter(|&p| p <= port_max)
}
}
#[derive(Debug, Clone)]
pub struct AllPortStatus {
open: HashSet<u16>,
number_ports: u16,
port_max: u16,
pub closed: u16,
}
impl From<&Self> for AllPortStatus {
fn from(value: &Self) -> Self {
Self {
open: HashSet::with_capacity(value.open_len().into()),
closed: value.closed,
number_ports: value.number_ports,
port_max: value.port_max,
}
}
}
impl AllPortStatus {
fn new(cli_args: &CliArgs) -> Self {
Self {
open: HashSet::with_capacity(64),
closed: 0,
number_ports: cli_args.ports.range_size,
port_max: cli_args.ports.max,
}
}
pub fn get_all_open<'a>(&self) -> Option<Vec<(u16, &'a str)>> {
if !self.open.is_empty() && self.complete() {
let port_details = PortDescriptions::new();
let mut output = self
.open
.iter()
.map(|i| (*i, port_details.get(*i)))
.collect::<Vec<_>>();
output.sort_by(|a, b| a.0.cmp(&b.0));
Some(output)
} else {
None
}
}
pub fn open_len(&self) -> u16 {
u16::try_from(self.open.len()).unwrap_or_default()
}
fn complete(&self) -> bool {
self.open_len() + self.closed == self.number_ports
}
fn insert(&mut self, message: PortMessage) {
if message.open {
self.open.insert(message.port);
} else {
self.closed += 1;
}
}
fn scan_port(
port: u16,
ip: IpAddr,
timeout: u32,
sx: Sender<PortMessage>,
counter: u8,
) -> Pin<Box<dyn Future<Output = ()> + Send>> {
Box::pin(async move {
if let Ok(Ok(_)) = tokio::time::timeout(
std::time::Duration::from_millis(u64::from(timeout)),
TcpStream::connect(format!("{ip}:{port}")),
)
.await
{
if sx.send(PortMessage { port, open: true }).await.is_err() {
exit(1);
};
} else if counter > 1 {
Self::scan_port(port, ip, timeout, sx, counter - 1).await;
} else if sx.send(PortMessage { port, open: false }).await.is_err() {
exit(1);
};
})
}
fn spawn_scan_port(port: u16, ip: IpAddr, timeout: u32, sx: Sender<PortMessage>, counter: u8) {
tokio::spawn(Self::scan_port(port, ip, timeout, sx, counter));
}
async fn first_pass(cli_args: &CliArgs, ip: &IpAddr) -> Self {
let mut first_pass = Self::new(cli_args);
let retry = cli_args.retry + 1;
let (sx, mut rx) = tokio::sync::mpsc::channel(usize::from(cli_args.concurrent));
for port in cli_args.ports.get_range().take(cli_args.concurrent.into()) {
Self::spawn_scan_port(port, *ip, cli_args.timeout, sx.clone(), retry);
}
while let Some(message) = rx.recv().await {
first_pass.insert(message);
if first_pass.complete() {
rx.close();
}
if let Some(new_port) = message.can_spawn(cli_args.concurrent, first_pass.port_max) {
Self::spawn_scan_port(new_port, *ip, cli_args.timeout, sx.clone(), retry);
}
}
first_pass
}
async fn second_pass(first_pass: Self, cli_args: &CliArgs, ip: &IpAddr) -> Self {
let mut validated_result = Self::from(&first_pass);
let (sx, mut rx) = tokio::sync::mpsc::channel(first_pass.open.len());
for port in &first_pass.open {
Self::scan_port(*port, *ip, cli_args.timeout, sx.clone(), cli_args.retry + 1).await;
}
drop(sx);
while let Some(message) = rx.recv().await {
validated_result.insert(message);
}
validated_result
}
pub async fn scan_ports(cli_args: &CliArgs, ip: &IpAddr) -> Self {
let first_pass = Self::first_pass(cli_args, ip).await;
if first_pass.open_len() > 0 {
Self::second_pass(first_pass, cli_args, ip).await
} else {
first_pass
}
}
}
#[cfg(test)]
impl AllPortStatus {
pub const fn test_new(
open: HashSet<u16>,
number_ports: u16,
port_max: u16,
closed: u16,
) -> Self {
Self {
open,
number_ports,
port_max,
closed,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use std::{
collections::HashSet,
net::{IpAddr, Ipv4Addr},
};
use warp::Filter;
use crate::{
parse_arg,
scanner::{AllPortStatus, PortMessage},
};
async fn start_server(port: u16) {
let routes = warp::any().map(|| "Test Sever");
tokio::spawn(warp::serve(routes).run(([127, 0, 0, 1], port)));
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
}
#[test]
fn test_scanner_can_portmessage_spawn() {
let message = PortMessage {
port: 80,
open: true,
};
assert!(message.can_spawn(10, 81).is_none());
assert!(message.can_spawn(10, 90).is_some());
assert!(message.can_spawn(10, 91).is_some());
}
#[test]
fn test_scanner_get_insert() {
let mut result = AllPortStatus {
open: HashSet::new(),
closed: 8,
number_ports: 10,
port_max: 10,
};
result.insert(PortMessage {
port: 1,
open: false,
});
assert_eq!(result.closed, 9);
result.insert(PortMessage {
port: 80,
open: true,
});
assert_eq!(result.closed, 9);
assert_eq!(result.open_len(), 1);
assert!(result.open.get(&80).is_some());
}
#[test]
fn test_scanner_get_open_none() {
let mut result = AllPortStatus {
open: HashSet::new(),
closed: 8,
number_ports: 10,
port_max: 10,
};
assert!(!result.complete());
assert!(result.get_all_open().is_none());
result.closed = 9;
assert!(!result.complete());
assert!(result.get_all_open().is_none());
result.closed = 10;
assert!(result.complete());
assert!(result.get_all_open().is_none());
}
#[test]
fn test_scanner_get_open_some() {
let mut result = AllPortStatus {
open: HashSet::new(),
closed: 8,
number_ports: 10,
port_max: 10,
};
assert!(!result.complete());
assert!(result.get_all_open().is_none());
result.open.insert(6379);
assert!(!result.complete());
assert!(result.get_all_open().is_none());
result.open.insert(443);
assert!(result.complete());
let result = result.get_all_open();
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0], (443, "https"));
assert_eq!(result[1], (6379, "redis"));
}
#[tokio::test]
async fn test_scanner_1000_empty() {
let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
assert_eq!(result.closed, 1000);
assert_eq!(result.open.len(), 0);
}
#[tokio::test]
async fn test_scanner_all() {
let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
println!("{result:#?}");
assert_eq!(
usize::from(result.closed) + usize::from(result.open_len()),
65535
);
}
#[tokio::test]
async fn test_scanner_10_port_80_empty() {
start_server(80).await;
let cli_args = parse_arg::CliArgs::test_new("1-10".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
assert_eq!(result.closed, 10);
assert_eq!(result.open.len(), 0);
}
#[tokio::test]
async fn test_scanner_port_80() {
start_server(80).await;
let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
assert_eq!(result.closed, 999);
assert_eq!(result.open.len(), 1);
assert_eq!(
usize::from(result.closed) + usize::from(result.open_len()),
1000
);
}
#[tokio::test]
async fn test_scanner_1000_80_443() {
start_server(80).await;
start_server(443).await;
let cli_args = parse_arg::CliArgs::test_new("1-1000".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
assert_eq!(result.closed, 998);
assert_eq!(result.open.len(), 2);
assert_eq!(
usize::from(result.closed) + usize::from(result.open_len()),
1000
);
}
#[tokio::test]
async fn test_scanner_all_80() {
start_server(80).await;
let cli_args = parse_arg::CliArgs::test_new("1-65535".to_owned(), 2048, None);
let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let result = AllPortStatus::first_pass(&cli_args, &ip).await;
assert_eq!(
usize::from(result.closed) + usize::from(result.open_len()),
65535
);
assert!(result.open.len() >= 2);
let result = result.get_all_open();
assert!(result.is_some());
let result = result.unwrap();
assert!(result.contains(&(80, "http")));
}
}