summaryrefslogtreecommitdiff
path: root/src/main/java/com/it_jaros/jscanner/CliPrinter.java
diff options
context:
space:
mode:
authorGravatar Matthias Jaros <jarlucmat@mailbox.org>2026-08-12 13:24:27 +0200
committerGravatar Matthias Jaros <jarlucmat@mailbox.org>2026-08-12 13:50:46 +0200
commitff14e0fc4e3fe4f5b8a67640d28850064661d7ab (patch)
treef141a2d34d902d344207cfbbc6ac614ec1b3840d /src/main/java/com/it_jaros/jscanner/CliPrinter.java
parent540b09b1a019f95a9322a6d19e8928369bb20fcb (diff)
Major refactoring of package structure
Diffstat (limited to 'src/main/java/com/it_jaros/jscanner/CliPrinter.java')
-rw-r--r--src/main/java/com/it_jaros/jscanner/CliPrinter.java258
1 files changed, 258 insertions, 0 deletions
diff --git a/src/main/java/com/it_jaros/jscanner/CliPrinter.java b/src/main/java/com/it_jaros/jscanner/CliPrinter.java
new file mode 100644
index 0000000..709c3dd
--- /dev/null
+++ b/src/main/java/com/it_jaros/jscanner/CliPrinter.java
@@ -0,0 +1,258 @@
+package com.it_jaros.jscanner;
+
+import com.it_jaros.jscanner.scan.Scan;
+import com.it_jaros.jscanner.scan.domain.ScanFailure;
+import com.it_jaros.jscanner.scan.domain.ScanResult;
+import com.it_jaros.jscanner.scan.service.ServiceType;
+
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+public class CliPrinter {
+
+ private final ScheduledExecutorService ui = Executors.newSingleThreadScheduledExecutor();
+ private final Scan scan;
+ private final boolean quiet;
+ private final Object outputLock = new Object();
+
+ public CliPrinter(Scan scan, boolean quiet) {
+ this.scan = scan;
+ this.quiet = quiet;
+ }
+
+ private void clearStatusLine() {
+ System.err.print("\r\033[2K");
+ System.err.flush();
+ }
+
+ public void printResult(ScanResult result, boolean showFilteredPorts) {
+ synchronized (outputLock) {
+ if (result.openPorts().isEmpty()
+ && (!showFilteredPorts || result.filteredPorts().isEmpty())
+ && result.errors().isEmpty()) {
+ return;
+ }
+
+ clearStatusLine();
+ printErrors(result);
+ printPortsResults(result, showFilteredPorts);
+ printStatusLine();
+ }
+ }
+
+ private void printErrors(ScanResult result) {
+ if (result.errors().isEmpty()) return;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(String.format(
+ "%s | errors: %d%n",
+ result.host(),
+ result.errors().size()
+ ));
+ String errors = result.errors().stream().map((ScanFailure error) -> String.format(
+ "\t%s: %s, %s",
+ error.port(),
+ error.exception().type(),
+ error.exception().message()
+ )).collect(Collectors.joining("\n"));
+ sb.append(errors);
+ sb.append("\n");
+
+ System.err.println(sb);
+ System.err.flush();
+ }
+
+ private void printPortsResults(ScanResult result, boolean showFilteredPorts) {
+ if (result.openPorts().isEmpty() && (!showFilteredPorts || result.filteredPorts().isEmpty())) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append(result.host());
+ sb.append("\n");
+ if (!result.openPorts().isEmpty()) {
+ sb.append(
+ String.format("\t (%d) open:\t%s%n", result.openPorts().size(), map(result.openPorts().getPorts(), result.bannerRecognition()))
+ );
+ }
+ if (showFilteredPorts && !result.filteredPorts().isEmpty()) {
+ sb.append(
+ String.format("\t (%d) filtered:\t%s%n", result.filteredPorts().size(), map(result.filteredPorts().getPorts()))
+ );
+ }
+ sb.append("\n");
+ System.out.println(sb);
+ System.out.flush();
+ }
+
+ private static String map(List<Integer> ports) {
+ return ports.stream().map(String::valueOf).collect(Collectors.joining(","));
+ }
+
+ private static String map(List<Integer> ports, Map<Integer, ServiceType> bannerRecognition) {
+ return ports.stream().map(port -> {
+ ServiceType serviceType = bannerRecognition.getOrDefault(port, ServiceType.UNKNOWN);
+
+ if (serviceType == ServiceType.UNKNOWN) {
+ return String.valueOf(port);
+ }
+
+ return port + "/" + serviceType.name().toLowerCase();
+ }).collect(Collectors.joining(","));
+ }
+
+ 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() {
+ if (quiet) {
+ return;
+ }
+
+ // although we don't print anything, we still need to
+ // gc all the done hosts
+ System.err.print("Scanning targets...\n\n");
+ ui.scheduleAtFixedRate(() -> {
+ synchronized (outputLock) {
+ printStatusLine();
+ }
+ }, 1, 1000, TimeUnit.MILLISECONDS);
+ }
+
+ public void stop() {
+ ui.shutdownNow();
+ try {
+ ui.awaitTermination(1, TimeUnit.SECONDS);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ clearStatusLine();
+ printStats();
+ }
+
+ private void printStats() {
+ Scan.Statistics stats = scan.getStatistics();
+ System.err.println("\n--------------------");
+ System.err.println("Stats:");
+ System.err.println("Total hosts: " + stats.hostTotal());
+ System.err.println("Total ports: " + stats.portTotal());
+ System.err.println("Duration: " + formatDuration(stats.durationInMillis()));
+ System.err.println("Peak concurrent hosts: " + stats.hostMaxConcurrent());
+ System.err.println("Peak concurrent sockets: " + stats.socketMaxConcurrent());
+ System.err.println("Peak concurrent workers: " + stats.threadMaxConcurrent());
+ System.err.println("--------------------\n");
+ }
+
+ private static String formatDuration(long millis) {
+ if (millis <= 0) return "";
+ long seconds = millis / 1000 % 60;
+ long minutes = millis / 60000 % 60;
+ long hours = millis / 3600000 % 24;
+ long days = millis / 86400000;
+
+ if (days > 0) return String.format("%dd %dh:%dm:%ds", days, hours, minutes, seconds);
+ if (hours > 0) return String.format("%dh:%dm:%ds", hours, minutes, seconds);
+ if (minutes > 0) return String.format("%dm:%ds", minutes, seconds);
+ return String.format("%ds", seconds);
+ }
+
+
+ private void printStatusLine() {
+ Scan.Statistics stats = scan.getStatistics();
+ int total = stats.hostTotal();
+ int running = stats.hostCurrent();
+ int done = total - running;
+ long percent = total == 0 ? 100 : done * 100L / total;
+
+ String hostStat = String.format(
+ "hosts: %d running, %d done",
+ running,
+ done
+ );
+ String portStat = String.format(
+ " | ports: %d total",
+ stats.portTotal()
+ );
+ String durationStat = String.format(
+ " | %s",
+ formatDuration(stats.durationInMillis())
+ );
+ String socketStat = String.format(
+ " | sockets: %d active, %d peak",
+ stats.socketCurrent(),
+ stats.socketMaxConcurrent()
+ );
+ String workerStat = String.format(
+ " | workers: %d active, %d peak",
+ stats.threadCurrent(),
+ stats.threadMaxConcurrent()
+ );
+
+ int availableWidth = CliPrinter.getColumnWidth();
+ String statusLine = appendIfEnoughSpace("", availableWidth, hostStat, portStat, durationStat, socketStat, workerStat);
+
+ String barFormat = " [%s] %3d%%";
+ int remainingWidth = availableWidth - statusLine.length() - barFormat.length();
+ if (remainingWidth > 0) {
+ String progressBar = String.format(
+ barFormat,
+ progressBar(percent, remainingWidth),
+ percent
+ );
+ statusLine = appendIfEnoughSpace(statusLine, availableWidth, progressBar);
+ }
+
+ remainingWidth = availableWidth - statusLine.length();
+ if (remainingWidth > 0) {
+ statusLine += " ".repeat(remainingWidth);
+ }
+ System.err.print("\r" + statusLine);
+ System.err.flush();
+ }
+
+ private String appendIfEnoughSpace(String current, int availableSize, String... additional) {
+ StringBuilder builder = new StringBuilder(current);
+ for (String add : additional) {
+ if ((builder.length() + add.length()) <= availableSize) {
+ builder.append(add);
+ }
+ }
+
+ return builder.toString();
+ }
+
+ private static String progressBar(long percent, int width) {
+ if (width <= 0) {
+ return "";
+ }
+
+ int filled = (int) (percent * width / 100);
+ return "#".repeat(filled) + "-".repeat(width - filled);
+ }
+}