diff options
| author | 2026-08-12 13:24:27 +0200 | |
|---|---|---|
| committer | 2026-08-12 13:50:46 +0200 | |
| commit | ff14e0fc4e3fe4f5b8a67640d28850064661d7ab (patch) | |
| tree | f141a2d34d902d344207cfbbc6ac614ec1b3840d /src/main/java/com/it_jaros/jscanner/CliPrinter.java | |
| parent | 540b09b1a019f95a9322a6d19e8928369bb20fcb (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.java | 258 |
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); + } +} |
