summaryrefslogtreecommitdiff
path: root/src/main/java/com/it_jaros/jscanner
diff options
context:
space:
mode:
authorGravatar Matthias Jaros <jarlucmat@mailbox.org>2026-04-13 17:31:55 +0200
committerGravatar Matthias Jaros <jarlucmat@mailbox.org>2026-07-09 09:59:28 +0200
commit8b881560dd8884e8e54e1e64654f79749d9ff456 (patch)
treefaffd2348b9430bcda9591120a0aa392923e422c /src/main/java/com/it_jaros/jscanner
parentb1bdcf02479f67a65f6176ed8a87b47925903e81 (diff)
Renamed package from network_scanner to jscanner
Diffstat (limited to 'src/main/java/com/it_jaros/jscanner')
-rw-r--r--src/main/java/com/it_jaros/jscanner/App.java45
-rw-r--r--src/main/java/com/it_jaros/jscanner/CliDefaults.java12
-rw-r--r--src/main/java/com/it_jaros/jscanner/CliParser.java112
-rw-r--r--src/main/java/com/it_jaros/jscanner/Counter.java25
-rw-r--r--src/main/java/com/it_jaros/jscanner/PortRange.java91
-rw-r--r--src/main/java/com/it_jaros/jscanner/PortResult.java4
-rw-r--r--src/main/java/com/it_jaros/jscanner/PortState.java7
-rw-r--r--src/main/java/com/it_jaros/jscanner/Progress.java5
-rw-r--r--src/main/java/com/it_jaros/jscanner/ProgressBar.java118
-rw-r--r--src/main/java/com/it_jaros/jscanner/ScanOptions.java5
-rw-r--r--src/main/java/com/it_jaros/jscanner/ScanResult.java5
-rw-r--r--src/main/java/com/it_jaros/jscanner/Scanner.java209
12 files changed, 638 insertions, 0 deletions
diff --git a/src/main/java/com/it_jaros/jscanner/App.java b/src/main/java/com/it_jaros/jscanner/App.java
new file mode 100644
index 0000000..c94b2df
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/App.java
@@ -0,0 +1,45 @@
+package com.it_jaros.jscanner;
+
+import java.util.BitSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class App {
+ public static void main(String[] args) {
+ CliParser cliParser = new CliParser();
+ try {
+ ScanOptions options = cliParser.parseArgs(args);
+ Scanner scanner = new Scanner(options.openSocketLimit(), options.timeoutInMillis(),
+ options.delayInMillis(), options.maxWorkersPerHost(), options.maxHostsLimit(), options.disableHostCheck());
+ printResults(scanner.scan(options), options.showFilteredPorts());
+ } catch (IllegalArgumentException e) {
+ System.out.printf("Error while parsing arguments: %s", e.getMessage());
+ cliParser.usageAndExit();
+ } catch (Exception e) {
+ System.out.printf("ERROR: %s", e.getMessage());
+ }
+ }
+
+ public static void printResults(List<ScanResult> results, boolean showFilteredPorts) {
+ results.forEach((ScanResult r) -> printResult(r, showFilteredPorts));
+ }
+
+ private static void printResult(ScanResult result, boolean showFilteredPorts) {
+ if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) {
+ return;
+ }
+
+ System.out.println(result.host());
+ if (!result.openPorts().isEmpty()) {
+ System.out.printf("\t (%d) open:\t%s%n", result.openPorts().cardinality(), map(result.openPorts()));
+ }
+ if (showFilteredPorts && !result.filteredPorts().isEmpty()) {
+ System.out.printf("\t (%d) filtered:\t%s%n", result.filteredPorts().cardinality(), map(result.filteredPorts()));
+ }
+ System.out.println();
+ }
+
+ private static String map(BitSet ports) {
+ return ports.stream().mapToObj(String::valueOf).collect(Collectors.joining(","));
+ }
+}
diff --git a/src/main/java/com/it_jaros/jscanner/CliDefaults.java b/src/main/java/com/it_jaros/jscanner/CliDefaults.java
new file mode 100644
index 0000000..ee24660
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/CliDefaults.java
@@ -0,0 +1,12 @@
+package com.it_jaros.jscanner;
+
+public class CliDefaults {
+ public static final int SOCKET_LIMIT = 1000;
+ public static final int DELAY_IN_MILLIS = 0;
+ public static final int TIMEOUT_IN_MILLIS = 300;
+ public static final int MAX_WORKERS_PER_HOST = 1000;
+ public static final int MAX_HOSTS_LIMIT = 10;
+ public static final String PORTS = "1-1024";
+ public static final boolean SHOW_FILTERED_PORTS = false;
+ public static final boolean DISABLE_HOST_CHECK = false;
+}
diff --git a/src/main/java/com/it_jaros/jscanner/CliParser.java b/src/main/java/com/it_jaros/jscanner/CliParser.java
new file mode 100644
index 0000000..1ab6b22
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/CliParser.java
@@ -0,0 +1,112 @@
+package com.it_jaros.jscanner;
+
+import java.util.HashSet;
+import java.util.Set;
+
+public class CliParser {
+
+ public ScanOptions parseArgs(String[] args) {
+ if (args.length < 1 || args[0].trim().equals("")) {
+ throw new IllegalArgumentException("No argument given");
+ }
+
+ Set<String> hostsArgv = new HashSet<>();
+ int socketLimit = CliDefaults.SOCKET_LIMIT;
+ int delayInMillis = CliDefaults.DELAY_IN_MILLIS;
+ int timeoutInMillis = CliDefaults.TIMEOUT_IN_MILLIS;
+ int maxWorkersPerHost = CliDefaults.MAX_WORKERS_PER_HOST;
+ int maxHostsLimit = CliDefaults.MAX_HOSTS_LIMIT;
+ String ports = CliDefaults.PORTS;
+ boolean showFilteredPorts = CliDefaults.SHOW_FILTERED_PORTS;
+ boolean disableHostCheck = CliDefaults.DISABLE_HOST_CHECK;
+ String hostsFile = null;
+
+ for (int i = 0; i < args.length; i++) {
+ final String currentArg = args[i];
+ if (!currentArg.startsWith("-")) {
+ hostsArgv.add(currentArg);
+ continue;
+ }
+
+ switch (currentArg) {
+ case "--help", "-h":
+ usageAndExit();
+ break;
+ case "--socketLimit", "-sl":
+ socketLimit = parseIntOption(args, ++i, currentArg);
+ break;
+ case "--delay", "-d":
+ delayInMillis = parseIntOption(args, ++i, currentArg);
+ break;
+ case "--timeout", "-t":
+ timeoutInMillis = parseIntOption(args, ++i, currentArg);
+ break;
+ case "--maxWorkersPerHost", "-wh":
+ maxWorkersPerHost = parseIntOption(args, ++i, currentArg);
+ break;
+ case "--maxHostsLimit", "-tl":
+ maxHostsLimit = parseIntOption(args, ++i, currentArg);
+ break;
+ case "--ports", "-p":
+ ports = requireValue(args, ++i, currentArg);
+ break;
+ case "--showFilteredPorts", "-sf":
+ showFilteredPorts = true;
+ break;
+ case "--disableHostCheck", "-dt":
+ disableHostCheck = true;
+ break;
+ case "--file", "-f":
+ hostsFile = requireValue(args, ++i, currentArg);
+ break;
+ default:
+ throw new IllegalArgumentException("No such param " + currentArg);
+ }
+ }
+
+ System.out.printf(
+ "Parsed arguments: hostsArgv('%s'), ports(%s), socketLimit(%s), delayInMillis(%s), timeoutInMillis(%s), maxWorkersPerHost(%s), maxTargetsLimit(%s), showFilteredPorts(%s), disableTargetCheck(%s), file(%s) %n",
+ hostsArgv, ports, socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, showFilteredPorts, disableHostCheck, hostsFile);
+
+ return new ScanOptions(hostsArgv.stream().toList(), socketLimit, delayInMillis, timeoutInMillis, maxWorkersPerHost, maxHostsLimit, ports, showFilteredPorts, disableHostCheck, hostsFile);
+ }
+
+ private String requireValue(String[] args, int index, String option) {
+ if (index >= args.length) {
+ throw new IllegalArgumentException(String.format("Missing argument for option %s", option));
+ }
+ return args[index];
+ }
+
+ private int parseIntOption(String[] args, int index, String option) {
+ String value = requireValue(args, index, option);
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException(String.format("Invalid argument %s for option %s", value, option));
+ }
+ }
+
+ public void usageAndExit() {
+ System.err.println("""
+ Usage:
+ java -jar scanner.jar [OPTIONS] <host>
+
+ Options:
+ --delay, -d:\t\tWaiting period in millis for connection attempts for a host (Default: 0)
+ --socketLimit, -sl:\t\tSocket Connection limit for a host (Default: 1000)
+ --timeout, -t:\t\tTimeout per connection attempt, 0 means infinite (Default: 1000)
+ --ports, -p:\t\tPorts to be scanned
+ --maxWorkersPerHost, -wh:\tVirtual Worker Threads run per Host. Higher doesn't mean necessarily faster. But the more you use the more RAM is required (Default: 1000)
+ --maxHostsLimit, -tl:\tMax number of Hosts that are scanned at the same time (Default: 10)
+ --showFilteredPorts, -sf:\tShow also filtered ports
+ --disableHostCheck, -dt:\tDon't check if Host is reachable
+ --file, -f:\t\t\tRead hosts to scan from file instead of stdin
+
+ Examples:
+ java -jar scanner.jar localhost
+ java -jar scanner.jar fd00::1
+ """);
+ System.exit(1);
+ }
+}
diff --git a/src/main/java/com/it_jaros/jscanner/Counter.java b/src/main/java/com/it_jaros/jscanner/Counter.java
new file mode 100644
index 0000000..a62bee7
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/Counter.java
@@ -0,0 +1,25 @@
+package com.it_jaros.jscanner;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public final class Counter {
+ private final AtomicInteger current = new AtomicInteger(0);
+ private final AtomicInteger max = new AtomicInteger(0);
+
+ public void inc() {
+ int now = current.incrementAndGet();
+ max.accumulateAndGet(now, Math::max);
+ }
+
+ public void dec() {
+ current.decrementAndGet();
+ }
+
+ public int current() {
+ return current.get();
+ }
+
+ public int max() {
+ return max.get();
+ }
+}
diff --git a/src/main/java/com/it_jaros/jscanner/PortRange.java b/src/main/java/com/it_jaros/jscanner/PortRange.java
new file mode 100644
index 0000000..dd67176
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/PortRange.java
@@ -0,0 +1,91 @@
+package com.it_jaros.jscanner;
+
+import java.util.BitSet;
+import java.util.NoSuchElementException;
+
+public class PortRange {
+
+ public static final int MIN_PORT = 1;
+ public static final int MAX_PORT = 65535;
+ private final BitSet specifiedPorts = new BitSet(MAX_PORT);
+ private final BitSet availablePorts = new BitSet(MAX_PORT);
+
+ private int cursor;
+ private int done = 0;
+
+ public PortRange(String ports) {
+ if (ports == null || "".equals(ports)) {
+ // keep defaults
+ specifiedPorts.set(1, 1024 + 1);
+ return;
+ }
+
+ String[] splitComma = ports.split(",");
+ for (String commaValue : splitComma) {
+ if (commaValue.contains("-")) {
+ String[] rangeValue = commaValue.split("-");
+ if (rangeValue.length > 2) {
+ throw new IllegalArgumentException(String.format("Argument contains too many '-'' %s", commaValue));
+ }
+
+ int start = Integer.parseInt(rangeValue[0]);
+ int end = Integer.parseInt(rangeValue[1]);
+ checkValues(start, end);
+ specifiedPorts.set(start, end + 1);
+ } else {
+ int port = Integer.parseInt(commaValue);
+ checkValue(port);
+ specifiedPorts.set(Integer.parseInt(commaValue));
+ }
+ }
+
+ availablePorts.or(specifiedPorts);
+ cursor = availablePorts.nextSetBit(MIN_PORT);
+ }
+
+ private void checkValues(int start, int end) {
+ checkValue(start);
+ checkValue(end);
+
+ if (start >= end) {
+ throw new IllegalArgumentException(
+ String.format("Start value cannot be equal or bigger than end value '%s >= %s'", start, end));
+ }
+ }
+
+ private void checkValue(int port) {
+ if (port < MIN_PORT) {
+ throw new IllegalArgumentException(
+ String.format("Start value smaller than allowed range %s < %s", port, MIN_PORT));
+ }
+
+ if (port > MAX_PORT) {
+ throw new IllegalArgumentException(
+ String.format("End value bigger than allowed range %s > %s", port, MAX_PORT));
+ }
+ }
+
+ public int next() {
+ int p = availablePorts.nextSetBit(cursor);
+ if (p > 0) {
+ availablePorts.clear(p);
+ cursor = p + 1;
+ done++;
+ return p;
+ }
+
+ throw new NoSuchElementException("Reached end of port range");
+ }
+
+ public boolean hasNext() {
+ return availablePorts.nextSetBit(cursor) > 0;
+ }
+
+ public int getTotal() {
+ return specifiedPorts.cardinality();
+ }
+
+ public int getDone() {
+ return done;
+ }
+}
diff --git a/src/main/java/com/it_jaros/jscanner/PortResult.java b/src/main/java/com/it_jaros/jscanner/PortResult.java
new file mode 100644
index 0000000..53d9040
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/PortResult.java
@@ -0,0 +1,4 @@
+package com.it_jaros.jscanner;
+
+public record PortResult(int port, PortState state) {
+}
diff --git a/src/main/java/com/it_jaros/jscanner/PortState.java b/src/main/java/com/it_jaros/jscanner/PortState.java
new file mode 100644
index 0000000..d2ef9a6
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/PortState.java
@@ -0,0 +1,7 @@
+package com.it_jaros.jscanner;
+
+public enum PortState {
+ OPEN,
+ CLOSED,
+ FILTERED
+}
diff --git a/src/main/java/com/it_jaros/jscanner/Progress.java b/src/main/java/com/it_jaros/jscanner/Progress.java
new file mode 100644
index 0000000..aebbf9b
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/Progress.java
@@ -0,0 +1,5 @@
+package com.it_jaros.jscanner;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public record Progress(String host, int total, AtomicInteger done, AtomicInteger open, AtomicInteger filtered) {}
diff --git a/src/main/java/com/it_jaros/jscanner/ProgressBar.java b/src/main/java/com/it_jaros/jscanner/ProgressBar.java
new file mode 100644
index 0000000..5ea347b
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/ProgressBar.java
@@ -0,0 +1,118 @@
+package com.it_jaros.jscanner;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ProgressBar {
+
+ private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor();
+ private final ReentrantLock lock = new ReentrantLock();
+ private final int columnWidth = ProgressBar.getColumnWidth();
+
+ private List<Progress> hosts;
+
+ private static int getColumnWidth() {
+ try {
+ Process process = new ProcessBuilder("sh", "-c", "stty size < /dev/tty").start();
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream()))) {
+ String line = reader.readLine();
+ if (line != null && !line.isBlank()) {
+ Matcher matcher = Pattern.compile("^\\d+\\s+(\\d+)").matcher(line.trim());
+ if (matcher.matches()) {
+ return Integer.parseInt(matcher.group(1));
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ // does not matter why it did not work
+ }
+
+ return 120;
+ }
+
+ public void start() {
+ this.hosts = new ArrayList<>();
+ System.out.print("Scanning targets...\n\n");
+ ui.scheduleAtFixedRate(() -> {
+ lock.lock();
+ drawProgress(false);
+ lock.unlock();
+ }, 1, 200, TimeUnit.MILLISECONDS);
+ }
+
+ private void drawProgress(boolean last) {
+ // find and print finished hosts one last time
+ for (Iterator<Progress> it = hosts.iterator(); it.hasNext();) {
+ Progress p = it.next();
+ if (p.done().get() == p.total()) {
+ it.remove();
+ drawBar(p);
+ }
+ }
+
+ // the more progressed the host the more up it is
+ hosts.sort(Comparator.comparingInt((Progress p) -> p.done().get()).reversed());
+ for (Progress progress : hosts) {
+ drawBar(progress);
+ }
+
+ if (!last) {
+ // move cursor up again
+ System.out.print("\u001b[" + hosts.size() + "A");
+ }
+ }
+
+ public void submit(Progress progress) {
+ lock.lock();
+ hosts.add(progress);
+ lock.unlock();
+ }
+
+ private void drawBar(Progress progress) {
+ System.out.print("\u001b[2K\r"); // clear whole line
+ System.out.println(createBar(
+ progress.host(),
+ progress.open().get(),
+ progress.filtered().get(),
+ progress.done().get(),
+ progress.total()));
+ }
+
+ public String createBar(String label, int open, int filtered, int done, int total) {
+ int terminalWidth = columnWidth;
+ int bracketWidth = terminalWidth / 3;
+ double pct = total == 0 ? 1.0 : (done / (double) total);
+ int percent = (int) (pct * 100);
+
+ String statsLeft = String.format("open:%d filtered:%d (%d/%d)", open, filtered, done, total);
+ String statsRight = String.format("%3d%%", percent);
+ int filled = (int) (pct * bracketWidth);
+ String brackets = "[" + "#".repeat(filled) + "-".repeat(bracketWidth - filled) + "]";
+
+ int spacesBetween = terminalWidth - bracketWidth - (label + statsLeft + statsRight).length() - 5;
+ return String.format("%s%s%s %s %s", label, " ".repeat(spacesBetween),statsLeft, brackets, statsRight);
+ }
+
+ public void stop() {
+ ui.shutdown();
+ try {
+ ui.awaitTermination(10, TimeUnit.SECONDS);
+ // we need to draw one last time for the 100% to appear
+ drawProgress(true);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/src/main/java/com/it_jaros/jscanner/ScanOptions.java b/src/main/java/com/it_jaros/jscanner/ScanOptions.java
new file mode 100644
index 0000000..d23fc8d
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/ScanOptions.java
@@ -0,0 +1,5 @@
+package com.it_jaros.jscanner;
+
+import java.util.List;
+
+public record ScanOptions(List<String> hostsArgv, int openSocketLimit, int delayInMillis, int timeoutInMillis, int maxWorkersPerHost, int maxHostsLimit, String ports, boolean showFilteredPorts, boolean disableHostCheck, String hostsFile) {}
diff --git a/src/main/java/com/it_jaros/jscanner/ScanResult.java b/src/main/java/com/it_jaros/jscanner/ScanResult.java
new file mode 100644
index 0000000..95a273d
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/ScanResult.java
@@ -0,0 +1,5 @@
+package com.it_jaros.jscanner;
+
+import java.util.BitSet;
+
+public record ScanResult(String host, BitSet openPorts, BitSet filteredPorts) {}
diff --git a/src/main/java/com/it_jaros/jscanner/Scanner.java b/src/main/java/com/it_jaros/jscanner/Scanner.java
new file mode 100644
index 0000000..2fa1fd0
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/Scanner.java
@@ -0,0 +1,209 @@
+package com.it_jaros.jscanner;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.net.*;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+import java.util.stream.Stream;
+
+public class Scanner {
+
+ private final Counter counter = new Counter();
+ private final ProgressBar progressBar = new ProgressBar();
+ private final Semaphore socketLimit;
+ private final int timeoutInMillis;
+ private final long delayInNanos;
+ private final int maxWorkersPerHost;
+ private final int maxHostsLimit;
+ private final boolean disableHostCheck;
+
+ public Scanner(int socketLimit, int timeoutInMillis, int delayInMillis, int maxWorkersPerHost, int maxHostsLimit, boolean disableHostCheck) {
+ this.timeoutInMillis = timeoutInMillis;
+ this.delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, delayInMillis));
+ this.socketLimit = new Semaphore(socketLimit);
+ this.maxWorkersPerHost = maxWorkersPerHost;
+ this.maxHostsLimit = maxHostsLimit;
+ this.disableHostCheck = disableHostCheck;
+ }
+
+ public List<ScanResult> scan(ScanOptions options) {
+ if (options.hostsFile() != null) {
+ return scanHosts(options.hostsFile(), options.ports());
+ }
+ return scanHosts(options.hostsArgv(), options.ports());
+ }
+
+ public List<ScanResult> scanHosts(String sourceFile, String ports) {
+ try (BufferedReader reader = Files.newBufferedReader(Path.of(sourceFile))) {
+ return scanHosts(reader.lines(), ports);
+ } catch (FileNotFoundException | NoSuchFileException e) {
+ System.out.printf("Could not open file %s%n", sourceFile);
+ } catch (IOException e) {
+ System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage());
+ }
+
+ return List.of();
+ }
+
+ public List<ScanResult> scanHosts(List<String> hostsArgv, String ports) {
+ try (Stream<String> stream = hostsArgv.stream()){
+ return scanHosts(stream, ports);
+ } catch (IOException e) {
+ System.out.printf("Error while trying to read hosts: %s -> %s %n", e.getClass().getSimpleName(), e.getMessage());
+ }
+
+ return List.of();
+ }
+
+ public List<ScanResult> scanHosts(Stream<String> hosts, String ports) throws IOException {
+ List<ScanResult> results = new ArrayList<>();
+ progressBar.start();
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ List<Future<ScanResult>> futures = new ArrayList<>();
+ Semaphore maxHosts = new Semaphore(maxHostsLimit);
+ hosts.forEach((final String host) -> {
+ maxHosts.acquireUninterruptibly();
+ futures.add(executor.submit(() -> {
+ try {
+ return scanHost(host, ports);
+ } finally {
+ maxHosts.release();
+ }
+ }));
+ });
+
+ futures.forEach((Future<ScanResult> f) -> {
+ try {
+ results.add(f.get());
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ System.out.printf("%s -> %s%n", e.getClass().getSimpleName(), e.getMessage());
+ }
+ });
+ }
+
+ progressBar.stop();
+ System.out.println("--------------------");
+ System.out.println("Finished scan!");
+ System.out.println("Stats:");
+ System.out.println("Peak concurrent connects: " + counter.max());
+ System.out.println("--------------------");
+ return results;
+ }
+
+ private ScanResult scanHost(String host, String ports) {
+ PortRange portRange = new PortRange(ports);
+ AtomicLong portSlotFactory = new AtomicLong(System.nanoTime());
+ Semaphore maxWorkers = new Semaphore(maxWorkersPerHost);
+ BitSet openPorts = new BitSet(PortRange.MAX_PORT);
+ BitSet filteredPorts = new BitSet(PortRange.MAX_PORT);
+
+ Progress progress = new Progress(host, portRange.getTotal(), new AtomicInteger(), new AtomicInteger(), new AtomicInteger());
+ progressBar.submit(progress);
+
+ if(!disableHostCheck && !checkHostOnline(host)) {
+ // Visually show that this host is basically done
+ progress.done().set(progress.total());
+ return new ScanResult(host, openPorts, filteredPorts);
+ }
+
+ try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
+ List<Future<PortResult>> futures = new ArrayList<>();
+ while (portRange.hasNext()) {
+ maxWorkers.acquireUninterruptibly();
+ final int currentPort = portRange.next();
+ futures.add(executor.submit(() -> {
+ try {
+ waitForSlot(portSlotFactory);
+ PortState state = getPortState(host, currentPort);
+ if (state.equals(PortState.OPEN)) {
+ progress.open().incrementAndGet();
+ } else if (state.equals(PortState.FILTERED)) {
+ progress.filtered().incrementAndGet();
+ }
+ return new PortResult(currentPort, state);
+ } finally {
+ maxWorkers.release();
+ progress.done().incrementAndGet();
+ }
+ }));
+ }
+
+ List<Throwable> errors = new ArrayList<>();
+ for (Future<PortResult> f : futures) {
+ try {
+ // bitset is not thread-safe, so it is set
+ // outside the other virtual threads that update progress
+ PortResult portResult = f.get();
+ if (portResult.state().equals(PortState.OPEN)) {
+ openPorts.set(portResult.port());
+ } else if (portResult.state().equals(PortState.FILTERED)) {
+ filteredPorts.set(portResult.port());
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ errors.add(e);
+ }
+ }
+
+ if (!errors.isEmpty()) {
+ System.out.printf("Errors happened during scan of host %s%nErrors:%s -> %s", host, errors.size(), errors);
+ }
+ }
+
+ return new ScanResult(host, openPorts, filteredPorts);
+ }
+
+ private boolean checkHostOnline(String host) {
+ try {
+ return InetAddress.getByName(host).isReachable(timeoutInMillis);
+ } catch (IOException e) {
+ // something went wrong
+ }
+
+ return false;
+ }
+
+ private void waitForSlot(AtomicLong scanSlotFactory) {
+ if (delayInNanos > 0) {
+ long slot = scanSlotFactory.getAndAdd(delayInNanos);
+ long wait = slot - System.nanoTime();
+ if (wait > 0)
+ LockSupport.parkNanos(wait);
+ }
+ }
+
+ private PortState getPortState(String host, int port) {
+ socketLimit.acquireUninterruptibly();
+ counter.inc();
+ try (Socket socket = new Socket()) {
+ socket.connect(new InetSocketAddress(host, port), timeoutInMillis);
+ return PortState.OPEN;
+ } catch (NoRouteToHostException ignored) {
+ // this can be safely ignored because the port is closed if a host is unreachable
+ } catch (SocketTimeoutException ignored) {
+ return PortState.FILTERED;
+ } catch (ConnectException ignored) {
+ return PortState.CLOSED;
+ } catch (IOException ignored) {
+ // Will happen a lot when scanning for open ports, so not needed
+ } finally {
+ counter.dec();
+ socketLimit.release();
+ }
+
+ return PortState.CLOSED;
+ }
+}