Skip to content

Commit 35a5023

Browse files
committed
allow both individual ports and ranges of ports to be passed as opt
1 parent 042875a commit 35a5023

File tree

5 files changed

+264
-149
lines changed

5 files changed

+264
-149
lines changed

benches/benchmark_portscan.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use async_std::task::block_on;
22
use criterion::{criterion_group, criterion_main, Criterion};
3-
use rustscan::input::{Opts, PortRange, ScanOrder};
3+
use rustscan::input::{Opts, ScanOrder};
44
use rustscan::port_strategy::PortStrategy;
55
use rustscan::scanner::Scanner;
66
use std::hint::black_box;
@@ -20,11 +20,8 @@ fn bench_address() {
2020
}
2121

2222
fn bench_port_strategy() {
23-
let range = PortRange {
24-
start: 1,
25-
end: 1_000,
26-
};
27-
let _strategy = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
23+
let range = (1..=1000).collect::<Vec<u16>>();
24+
let _strategy = PortStrategy::pick(Some(range), ScanOrder::Serial);
2825
}
2926

3027
fn bench_address_parsing() {
@@ -47,12 +44,9 @@ fn bench_address_parsing() {
4744

4845
fn criterion_benchmark(c: &mut Criterion) {
4946
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
50-
let range = PortRange {
51-
start: 1,
52-
end: 1_000,
53-
};
54-
let strategy_tcp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
55-
let strategy_udp = PortStrategy::pick(&Some(range.clone()), None, ScanOrder::Serial);
47+
let range = (1..=1000).collect::<Vec<u16>>();
48+
let strategy_tcp = PortStrategy::pick(Some(range.clone()), ScanOrder::Serial);
49+
let strategy_udp = PortStrategy::pick(Some(range.clone()), ScanOrder::Serial);
5650

5751
let scanner_tcp = Scanner::new(
5852
&addrs,

src/input.rs

Lines changed: 196 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
//! Provides a means to read, parse and hold configuration options for scans.
22
use clap::{Parser, ValueEnum};
33
use serde_derive::Deserialize;
4-
use std::collections::HashMap;
54
use std::fs;
65
use std::path::PathBuf;
76

@@ -28,35 +27,83 @@ pub enum ScriptsRequired {
2827
Custom,
2928
}
3029

31-
/// Represents the range of ports to be scanned.
32-
#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
33-
pub struct PortRange {
34-
pub start: u16,
35-
pub end: u16,
30+
#[cfg(not(tarpaulin_include))]
31+
pub fn parse_ports_and_ranges(input: &str) -> Result<Vec<u16>, String> {
32+
let mut ports = Vec::new();
33+
34+
for part in input.split(',') {
35+
let part = part.trim();
36+
if part.is_empty() {
37+
continue;
38+
}
39+
40+
if part.contains('-') {
41+
let range_ports = parse_port_range(part)?;
42+
ports.extend(range_ports);
43+
} else {
44+
let port = parse_single_port(part)?;
45+
ports.push(port);
46+
}
47+
}
48+
49+
if ports.is_empty() {
50+
return Err("No valid ports or ranges provided".to_string());
51+
}
52+
53+
ports.sort_unstable();
54+
ports.dedup();
55+
56+
Ok(ports)
3657
}
3758

38-
#[cfg(not(tarpaulin_include))]
39-
fn parse_range(input: &str) -> Result<PortRange, String> {
40-
let range = input
41-
.split('-')
42-
.map(str::parse)
43-
.collect::<Result<Vec<u16>, std::num::ParseIntError>>();
44-
45-
if range.is_err() {
46-
return Err(String::from(
47-
"the range format must be 'start-end'. Example: 1-1000.",
59+
fn parse_port_range(range_str: &str) -> Result<Vec<u16>, String> {
60+
let range_parts: Vec<&str> = range_str.split('-').collect();
61+
if range_parts.len() != 2 {
62+
return Err(format!(
63+
"Invalid range format '{range_str}'. Expected 'start-end'. Example: 1-1000.",
64+
));
65+
}
66+
67+
let start: u16 = range_parts[0].parse().map_err(|_| {
68+
format!(
69+
"Invalid start port '{}' in range '{range_str}'",
70+
range_parts[0]
71+
)
72+
})?;
73+
let end: u16 = range_parts[1].parse().map_err(|_| {
74+
format!(
75+
"Invalid end port '{}' in range '{range_str}'",
76+
range_parts[1]
77+
)
78+
})?;
79+
80+
if start > end {
81+
return Err(format!(
82+
"Start port {start} is greater than end port {end} in range '{range_str}'",
4883
));
4984
}
5085

51-
match range.unwrap().as_slice() {
52-
[start, end] => Ok(PortRange {
53-
start: *start,
54-
end: *end,
55-
}),
56-
_ => Err(String::from(
57-
"the range format must be 'start-end'. Example: 1-1000.",
58-
)),
86+
if start < LOWEST_PORT_NUMBER {
87+
return Err(format!(
88+
"Ports in range '{range_str}' must be between {LOWEST_PORT_NUMBER} and {TOP_PORT_NUMBER}",
89+
));
90+
}
91+
92+
Ok((start..=end).collect())
93+
}
94+
95+
fn parse_single_port(port_str: &str) -> Result<u16, String> {
96+
let port: u16 = port_str
97+
.parse()
98+
.map_err(|_| format!("Invalid port number '{port_str}'"))?;
99+
100+
if port < LOWEST_PORT_NUMBER {
101+
return Err(format!(
102+
"Port {port} must be between {LOWEST_PORT_NUMBER} and {TOP_PORT_NUMBER}",
103+
));
59104
}
105+
106+
Ok(port)
60107
}
61108

62109
#[derive(Parser, Debug, Clone)]
@@ -77,14 +124,10 @@ pub struct Opts {
77124
#[arg(short, long, value_delimiter = ',')]
78125
pub addresses: Vec<String>,
79126

80-
/// A list of comma separated ports to be scanned. Example: 80,443,8080.
81-
#[arg(short, long, value_delimiter = ',')]
127+
/// A list of ports and/or port ranges to be scanned. Examples: 80,443,8080 or 1-1000 or 1-1000,8080
128+
#[arg(short, long, alias = "range", value_parser = parse_ports_and_ranges)]
82129
pub ports: Option<Vec<u16>>,
83130

84-
/// A range of ports with format start-end. Example: 1-1000.
85-
#[arg(short, long, conflicts_with = "ports", value_parser = parse_range)]
86-
pub range: Option<PortRange>,
87-
88131
/// Whether to ignore the configuration file or not.
89132
#[arg(short, long)]
90133
pub no_config: bool,
@@ -169,11 +212,8 @@ impl Opts {
169212
pub fn read() -> Self {
170213
let mut opts = Opts::parse();
171214

172-
if opts.ports.is_none() && opts.range.is_none() {
173-
opts.range = Some(PortRange {
174-
start: LOWEST_PORT_NUMBER,
175-
end: TOP_PORT_NUMBER,
176-
});
215+
if opts.ports.is_none() {
216+
opts.ports = Some((LOWEST_PORT_NUMBER..=TOP_PORT_NUMBER).collect());
177217
}
178218

179219
opts
@@ -218,14 +258,10 @@ impl Opts {
218258

219259
// Only use top ports when the user asks for them
220260
if self.top && config.ports.is_some() {
221-
let mut ports: Vec<u16> = Vec::with_capacity(config.ports.as_ref().unwrap().len());
222-
for entry in config.ports.as_ref().unwrap().keys() {
223-
ports.push(entry.parse().unwrap());
224-
}
225-
self.ports = Some(ports);
261+
self.ports = config.ports.clone();
226262
}
227263

228-
merge_optional!(range, resolver, ulimit, exclude_ports, exclude_addresses);
264+
merge_optional!(resolver, ulimit, exclude_ports, exclude_addresses);
229265
}
230266
}
231267

@@ -234,7 +270,6 @@ impl Default for Opts {
234270
Self {
235271
addresses: vec![],
236272
ports: None,
237-
range: None,
238273
greppable: true,
239274
batch_size: 0,
240275
timeout: 0,
@@ -263,8 +298,7 @@ impl Default for Opts {
263298
#[derive(Debug, Deserialize)]
264299
pub struct Config {
265300
addresses: Option<Vec<String>>,
266-
ports: Option<HashMap<String, u16>>,
267-
range: Option<PortRange>,
301+
ports: Option<Vec<u16>>,
268302
greppable: Option<bool>,
269303
accessible: Option<bool>,
270304
batch_size: Option<u16>,
@@ -332,14 +366,13 @@ mod tests {
332366
use clap::{CommandFactory, Parser};
333367
use parameterized::parameterized;
334368

335-
use super::{Config, Opts, PortRange, ScanOrder, ScriptsRequired};
369+
use super::{parse_ports_and_ranges, Config, Opts, ScanOrder, ScriptsRequired};
336370

337371
impl Config {
338372
fn default() -> Self {
339373
Self {
340374
addresses: Some(vec!["127.0.0.1".to_owned()]),
341375
ports: None,
342-
range: None,
343376
greppable: Some(true),
344377
batch_size: Some(25_000),
345378
timeout: Some(1_000),
@@ -417,17 +450,129 @@ mod tests {
417450
fn opts_merge_optional_arguments() {
418451
let mut opts = Opts::default();
419452
let mut config = Config::default();
420-
config.range = Some(PortRange {
421-
start: 1,
422-
end: 1_000,
423-
});
453+
config.ports = Some((1..=1000).collect::<Vec<u16>>());
424454
config.ulimit = Some(1_000);
425455
config.resolver = Some("1.1.1.1".to_owned());
426456

427457
opts.merge_optional(&config);
428458

429-
assert_eq!(opts.range, config.range);
459+
assert_eq!(opts.ports, Some((1..=1000).collect::<Vec<u16>>()));
430460
assert_eq!(opts.ulimit, config.ulimit);
431461
assert_eq!(opts.resolver, config.resolver);
432462
}
463+
464+
#[test]
465+
fn test_parse_ports_and_ranges_single_port() {
466+
let result = parse_ports_and_ranges("80");
467+
assert_eq!(result, Ok(vec![80]));
468+
}
469+
470+
#[test]
471+
fn test_parse_ports_and_ranges_multiple_ports() {
472+
let result = parse_ports_and_ranges("80,443,8080");
473+
assert_eq!(result, Ok(vec![80, 443, 8080]));
474+
}
475+
476+
#[test]
477+
fn test_parse_ports_and_ranges_single_range() {
478+
let result = parse_ports_and_ranges("1-5");
479+
assert_eq!(result, Ok(vec![1, 2, 3, 4, 5]));
480+
}
481+
482+
#[test]
483+
fn test_parse_ports_and_ranges_mixed_ports_and_ranges() {
484+
let result = parse_ports_and_ranges("80,443,1-3,8080");
485+
assert_eq!(result, Ok(vec![1, 2, 3, 80, 443, 8080]));
486+
}
487+
488+
#[test]
489+
fn test_parse_ports_and_ranges_with_spaces() {
490+
let result = parse_ports_and_ranges("80, 443, 1-3, 8080");
491+
assert_eq!(result, Ok(vec![1, 2, 3, 80, 443, 8080]));
492+
}
493+
494+
#[test]
495+
fn test_parse_ports_and_ranges_duplicates() {
496+
let result = parse_ports_and_ranges("80,443,80,443");
497+
assert_eq!(result, Ok(vec![80, 443]));
498+
}
499+
500+
#[test]
501+
fn test_parse_ports_and_ranges_empty_input() {
502+
let result = parse_ports_and_ranges("");
503+
assert!(result.is_err());
504+
assert!(result
505+
.unwrap_err()
506+
.contains("No valid ports or ranges provided"));
507+
}
508+
509+
#[test]
510+
fn test_parse_ports_and_ranges_invalid_port() {
511+
let result = parse_ports_and_ranges("80,abc,443");
512+
assert!(result.is_err());
513+
assert!(result.unwrap_err().contains("Invalid port number 'abc'"));
514+
}
515+
516+
#[test]
517+
fn test_parse_ports_and_ranges_invalid_range() {
518+
let result = parse_ports_and_ranges("80,1-abc,443");
519+
assert!(result.is_err());
520+
assert!(result
521+
.unwrap_err()
522+
.contains("Invalid end port 'abc' in range '1-abc'"));
523+
}
524+
525+
#[test]
526+
fn test_parse_ports_and_ranges_invalid_range_format() {
527+
let result = parse_ports_and_ranges("80,1-2-3,443");
528+
assert!(result.is_err());
529+
assert!(result
530+
.unwrap_err()
531+
.contains("Invalid range format '1-2-3'. Expected 'start-end'"));
532+
}
533+
534+
#[test]
535+
fn test_parse_ports_and_ranges_reverse_range() {
536+
let result = parse_ports_and_ranges("80,5-1,443");
537+
assert!(result.is_err());
538+
assert!(result
539+
.unwrap_err()
540+
.contains("Start port 5 is greater than end port 1 in range '5-1'"));
541+
}
542+
543+
#[test]
544+
fn test_parse_ports_and_ranges_out_of_bounds_port() {
545+
let result = parse_ports_and_ranges("80,70000,443");
546+
assert!(result.is_err());
547+
let error_msg = result.unwrap_err();
548+
println!("Actual error message: {}", error_msg);
549+
assert!(error_msg.contains("Invalid port number '70000'"));
550+
}
551+
552+
#[test]
553+
fn test_parse_ports_and_ranges_out_of_bounds_range() {
554+
let result = parse_ports_and_ranges("80,1-70000,443");
555+
assert!(result.is_err());
556+
let error_msg = result.unwrap_err();
557+
println!("Actual error message: {}", error_msg);
558+
assert!(error_msg.contains("Invalid end port '70000' in range '1-70000'"));
559+
}
560+
561+
#[test]
562+
fn test_parse_ports_and_ranges_zero_port() {
563+
let result = parse_ports_and_ranges("80,0,443");
564+
assert!(result.is_err());
565+
assert!(result
566+
.unwrap_err()
567+
.contains("Port 0 must be between 1 and 65535"));
568+
}
569+
570+
#[test]
571+
fn test_parse_ports_and_ranges_complex_mixed() {
572+
let result = parse_ports_and_ranges("1,80,443,1-5,8080,9090,10-12");
573+
assert_eq!(
574+
result,
575+
Ok(vec![1, 2, 3, 4, 5, 10, 11, 12, 80, 443, 8080, 9090])
576+
);
577+
}
433578
}

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ fn main() {
8989
Duration::from_millis(opts.timeout.into()),
9090
opts.tries,
9191
opts.greppable,
92-
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
92+
PortStrategy::pick(opts.ports, opts.scan_order),
9393
opts.accessible,
9494
opts.exclude_ports.unwrap_or_default(),
9595
opts.udp,

0 commit comments

Comments
 (0)