diff options
Diffstat (limited to 'src/main/java/com/it_jaros/jscanner/scan')
24 files changed, 0 insertions, 1126 deletions
diff --git a/src/main/java/com/it_jaros/jscanner/scan/Counter.java b/src/main/java/com/it_jaros/jscanner/scan/Counter.java deleted file mode 100644 index 0e1d5c9..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/Counter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.it_jaros.jscanner.scan; - -import java.util.concurrent.atomic.AtomicInteger; - -public final class Counter { - private final AtomicInteger current = new AtomicInteger(0); - private final AtomicInteger max = new AtomicInteger(0); - private final AtomicInteger total = new AtomicInteger(0); - - public void inc() { - total.incrementAndGet(); - 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(); - } - - public int total() { - return total.get(); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/ExceptionInfo.java b/src/main/java/com/it_jaros/jscanner/scan/ExceptionInfo.java deleted file mode 100644 index f62caf1..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/ExceptionInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.it_jaros.jscanner.scan; - -public record ExceptionInfo( - String type, - String message -) { - public static ExceptionInfo from(Throwable t) { - return new ExceptionInfo( - t.getClass().getName(), - t.getMessage() - ); - } -}
\ No newline at end of file diff --git a/src/main/java/com/it_jaros/jscanner/scan/Scan.java b/src/main/java/com/it_jaros/jscanner/scan/Scan.java deleted file mode 100644 index 0820abc..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/Scan.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.it_jaros.jscanner.scan; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.stream.Stream; - -/** - * Object holder for stateful volatile data during scan - */ -public class Scan { - private final Stream<String> hosts; - private final String ports; - private final Runnable onDone; - private final Counter hostCounter; - private final Counter portCounter; - private final Counter socketCounter; - private final Counter threadCounter; - private long start = 0; - private long stop = 0; - - private Scan(Stream<String> hosts, String ports) { - this(hosts, ports, () -> {}); - } - - private Scan(Stream<String> hosts, String ports, Runnable onDone) { - this.hosts = hosts; - this.ports = ports; - this.onDone = onDone; - this.hostCounter = new Counter(); - this.portCounter = new Counter(); - this.socketCounter = new Counter(); - this.threadCounter = new Counter(); - } - - public static Scan create(ScanOptions options) throws IOException { - if (options.hostsFile() != null) { - return create(options.hostsFile(), options.ports()); - } - return create(options.hostsArgv(), options.ports()); - } - - /** - * Create Scan based on either a source file or stdin - * - * @param sourceFile - * @param ports - * @return - * @throws IOException - */ - public static Scan create(String sourceFile, String ports) { - Stream<String> hosts; - if (sourceFile == null || sourceFile.isEmpty()) { - throw new IllegalArgumentException("Source file is null or empty"); - } - - if ("-".equals(sourceFile)) { - BufferedReader stdinReader = new BufferedReader(new InputStreamReader(System.in)); - hosts = stdinReader.lines(); - return new Scan(hosts, ports); - } - - try { - BufferedReader reader = Files.newBufferedReader(Path.of(sourceFile)); - return new Scan(reader.lines(), ports, () -> { - try { - reader.close(); - } catch (IOException ignore) { - // we ignore it because at this moment the program is shutting down anyway - } - }); - } catch (IOException e) { - throw new ScanException("Error while trying to open source File", e); - } - } - - public static Scan create(List<String> hostsArgv, String ports) { - return new Scan(hostsArgv.stream(), ports); - } - - public Stream<String> getHosts() { - return hosts; - } - - public String getPorts() { - return ports; - } - - public void stop() { - stop = System.currentTimeMillis(); - onDone.run(); - } - - public void start() { - start = System.currentTimeMillis(); - } - - private long getDurationMillis() { - if (stop == 0) { - if (start == 0) { - return 0; - } - return System.currentTimeMillis() - start; - } - return stop - start; - } - - public void portStart() { - this.portCounter.inc(); - this.threadCounter.inc(); - this.socketCounter.inc(); - } - - public void portFinish() { - this.portCounter.dec(); - this.threadCounter.dec(); - this.socketCounter.dec(); - } - - public void hostStart() { - this.hostCounter.inc(); - this.threadCounter.inc(); - } - - public void hostFinish() { - this.threadCounter.dec(); - this.hostCounter.dec(); - } - - public void producerStart() { - this.threadCounter.inc(); - } - - public void producerStop() { - this.threadCounter.dec(); - } - - /** - * This is not an atomic 100% correct snapshot but - * more a relative snapshot optimized more for performance - * than exactness - */ - public Statistics getStatistics() { - return new Statistics( - this.getDurationMillis(), - this.hostCounter.total(), - this.hostCounter.current(), - this.hostCounter.max(), - this.portCounter.total(), - this.portCounter.current(), - this.threadCounter.current(), - this.threadCounter.max(), - this.socketCounter.current(), - this.socketCounter.max() - ); - } - - public record Statistics( - long durationInMillis, - int hostTotal, - int hostCurrent, - int hostMaxConcurrent, - int portTotal, - int portCurrent, - int threadCurrent, - int threadMaxConcurrent, - int socketCurrent, - int socketMaxConcurrent - ) {} -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/ScanException.java b/src/main/java/com/it_jaros/jscanner/scan/ScanException.java deleted file mode 100644 index d667346..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/ScanException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.it_jaros.jscanner.scan; - -public class ScanException extends java.lang.RuntimeException { - public ScanException(String message) { - super(message); - } - - public ScanException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/ScanOptions.java b/src/main/java/com/it_jaros/jscanner/scan/ScanOptions.java deleted file mode 100644 index ce0d562..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/ScanOptions.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.it_jaros.jscanner.scan; - -import java.util.List; - -public record ScanOptions( - List<String> hostsArgv, - String hostsFile, - String ports, - boolean bannerRecognition, - boolean disableOnlineCheck, - boolean quiet, - boolean showFilteredPorts, - int delayInMillis, - int maxHostsLimit, - int maxWorkersPerHost, - int socketLimit, - int timeoutInMillis -) {} diff --git a/src/main/java/com/it_jaros/jscanner/scan/Scanner.java b/src/main/java/com/it_jaros/jscanner/scan/Scanner.java deleted file mode 100644 index 4c47aef..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/Scanner.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.it_jaros.jscanner.scan; - -import com.it_jaros.jscanner.scan.domain.ScanResult; -import com.it_jaros.jscanner.scan.engine.*; - -import java.time.Duration; -import java.util.concurrent.*; -import java.util.function.Consumer; - -public class Scanner implements AutoCloseable { - - private final CancelledToken cancelledToken = new CancelledToken(); - private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); - private final ScanOptions scanOptions; - private final Semaphore socketLimit; - - public Scanner(ScanOptions options) { - this.scanOptions = options; - this.socketLimit = new Semaphore(scanOptions.socketLimit()); - } - - /** - * Starts a given scan. - * - * @param scan - */ - public void runScan(final Scan scan, final Consumer<ScanResult> consumer) { - if (scan == null) { - throw new IllegalArgumentException("Scan argument cannot be null"); - } - - long delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, scanOptions.delayInMillis())); - ScanExecutionContext context = new ScanExecutionContext( - scanOptions, - executor, - socketLimit, - cancelledToken, - scan, - new PortScanRateLimiter(cancelledToken, delayInNanos) - ); - - scan.start(); - scan.producerStart(); - final ProducerState<ScanResult> state = new ProducerThread(context).startProducer( - scan.getHosts().iterator(), - scanOptions.maxHostsLimit(), - host -> new ScanHostTask(scan, host, context) - ); - - // the main thread is the consumer - // Let the consumer run as long as the producer runs - // or if still tasks are pending in pipeline - // we do not listen to canceled here because we want - // all results (also partial) collected for the consumer - // with whatever is there already - while (state.running().get() || state.inPipeline().get() > 0) { - try { - PollState<ScanResult> poll = getHostResult(state); - if (poll instanceof PollState.Success<ScanResult>(ScanResult value)) { - consumer.accept(value); - } else if (poll instanceof PollState.Failure<ScanResult>(Throwable error)) { - System.err.printf("runScan(): ScanHostTask() failed with error %s -> %s%n", error.getClass().getSimpleName(), error.getMessage()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - } - scan.producerStop(); - scan.stop(); - } - - private PollState<ScanResult> getHostResult(ProducerState<ScanResult> state) throws InterruptedException { - Future<ScanResult> finishedHost = state.completionService().poll(ProducerThread.pollInterval.toMillis(), TimeUnit.MILLISECONDS); - if (finishedHost == null) { - return new PollState.Unavailable<>(); - } - - try { - ScanResult result = finishedHost.get(); - return new PollState.Success<>(result); - } catch (ExecutionException e) { - return new PollState.Failure<>(e.getCause()); - } finally { - state.activeWorkers().release(); - state.inPipeline().decrementAndGet(); - } - } - - public boolean awaitTermination(Duration duration) throws InterruptedException { - return executor.awaitTermination(duration.toMillis(), TimeUnit.MILLISECONDS); - } - - public void cancel() { - this.cancelledToken.signal(); - executor.shutdown(); - } - - public void cancelNow() { - this.cancelledToken.signal(); - executor.shutdownNow(); - } - - @Override - public void close() throws Exception { - cancel(); - } - -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/PortList.java b/src/main/java/com/it_jaros/jscanner/scan/domain/PortList.java deleted file mode 100644 index f145700..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/PortList.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -import java.util.BitSet; -import java.util.List; - -/** - * Encapsulates a port scan result and is immutable - */ -public class PortList { - private final BitSet ports; - - public PortList(BitSet ports) { - this.ports = ports; - } - - public List<Integer> getPorts() { - return ports.stream().boxed().toList(); - } - - public boolean isEmpty() { - return ports.isEmpty(); - } - - public int size() { - return ports.cardinality(); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/PortRange.java b/src/main/java/com/it_jaros/jscanner/scan/domain/PortRange.java deleted file mode 100644 index 4434763..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/PortRange.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -import com.it_jaros.jscanner.scan.engine.PortRangeIterator; - -import java.util.BitSet; -import java.util.Iterator; - -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); - - public PortRange(String ports) { - if (ports == null || "".equals(ports)) { - specifiedPorts.set(MIN_PORT, 1024); - } else if ("all".equalsIgnoreCase(ports)) { - specifiedPorts.set(MIN_PORT, MAX_PORT + 1); - } else { - parsePortRange(ports); - } - } - - /** - * Possible values are 1,10 or 1-10 or a mix of 1,5-10 - * So we treat , stronger and handle them first and then check for - - * - * @param ports - */ - private void parsePortRange(String ports) { - String[] splitComma = ports.split(","); - for (String commaValue : splitComma) { - String[] rangeValue = commaValue.split("-"); - - // more then two values are not possible - if (rangeValue.length > 2) { - throw new IllegalArgumentException(String.format("Argument contains too many '-'' %s", commaValue)); - } - - // no range given, only single port - int port = Integer.parseInt(rangeValue[0]); - if (rangeValue.length == 1) { - checkValue(port); - specifiedPorts.set(port); - continue; - } - - // range given - int end = Integer.parseInt(rangeValue[1]); - checkValues(port, end); - specifiedPorts.set(port, end + 1); - } - } - - 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("Port value is too low: %s < %s", port, MIN_PORT)); - } - - if (port > MAX_PORT) { - throw new IllegalArgumentException( - String.format("Port value is too high: %s > %s", port, MAX_PORT)); - } - } - - public Iterator<Integer> iterator() { - return new PortRangeIterator(specifiedPorts); - } -}
\ No newline at end of file diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/PortResult.java b/src/main/java/com/it_jaros/jscanner/scan/domain/PortResult.java deleted file mode 100644 index 9e70cc5..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/PortResult.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -import java.io.IOException; - -public class PortResult { - private int port; - private PortState state; - private byte[] banner; - private Exception exception; - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public PortState getState() { - return state; - } - - public void setState(PortState state) { - this.state = state; - } - - public byte[] getBanner() { - return banner; - } - - public void setBanner(byte[] banner) { - this.banner = banner; - } - - public void setException(IOException e) { - exception = e; - } - - public Exception getException() { - return exception; - } - - public static PortResult empty() { - PortResult portResult = new PortResult(); - portResult.setState(PortState.UNKNOWN); - return portResult; - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/PortState.java b/src/main/java/com/it_jaros/jscanner/scan/domain/PortState.java deleted file mode 100644 index 6db4b2e..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/PortState.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -public enum PortState { - CLOSED, - FILTERED, - OPEN, - UNKNOWN -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/ScanFailure.java b/src/main/java/com/it_jaros/jscanner/scan/domain/ScanFailure.java deleted file mode 100644 index db33465..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/ScanFailure.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -import com.it_jaros.jscanner.scan.ExceptionInfo; - -public record ScanFailure( - Integer port, - ExceptionInfo exception -) { -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/domain/ScanResult.java b/src/main/java/com/it_jaros/jscanner/scan/domain/ScanResult.java deleted file mode 100644 index e18e75d..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/domain/ScanResult.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.it_jaros.jscanner.scan.domain; - -import com.it_jaros.jscanner.scan.service.ServiceType; - -import java.util.BitSet; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public record ScanResult( - String host, - PortList openPorts, - PortList filteredPorts, - Map<Integer, ServiceType> bannerRecognition, - List<ScanFailure> errors -) { - public static ScanResult empty(String host) { - return new ScanResult( - host, - new PortList(new BitSet(PortRange.MAX_PORT)), - new PortList(new BitSet(PortRange.MAX_PORT)), - Collections.emptyMap(), - Collections.emptyList() - ); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/CancelledToken.java b/src/main/java/com/it_jaros/jscanner/scan/engine/CancelledToken.java deleted file mode 100644 index 57993e5..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/CancelledToken.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -public class CancelledToken { - private volatile boolean cancelled = false; - - public void signal() { - if (!cancelled) { - this.cancelled = true; - } - } - - public boolean isCancelled() { - return cancelled; - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/PollState.java b/src/main/java/com/it_jaros/jscanner/scan/engine/PollState.java deleted file mode 100644 index 54ec767..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/PollState.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -/** - * A sealed interface representing the three possible outcomes of a CompletionService - * poll operation: successful result, task failure, or not ready yet. - * Makes error handling explicit — Failure and Unavailable are distinct and compile-time - * required to handle via exhaustiveness checking in switch statements. - */ -public sealed interface PollState<T> { - record Success<T>(T value) implements PollState<T> {} - record Failure<T>(Throwable error) implements PollState<T> {} - record Unavailable<T>() implements PollState<T> {} -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/PortRangeIterator.java b/src/main/java/com/it_jaros/jscanner/scan/engine/PortRangeIterator.java deleted file mode 100644 index 0673f0f..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/PortRangeIterator.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import java.util.BitSet; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import static com.it_jaros.jscanner.scan.domain.PortRange.MAX_PORT; -import static com.it_jaros.jscanner.scan.domain.PortRange.MIN_PORT; - -public class PortRangeIterator implements Iterator<Integer> { - - private int currentPortCursor; - private int done = 0; - private final BitSet availablePorts = new BitSet(MAX_PORT); - - public PortRangeIterator(BitSet specifiedPorts) { - availablePorts.or(specifiedPorts); - currentPortCursor = availablePorts.nextSetBit(MIN_PORT); - } - - @Override - public Integer next() { - int p = availablePorts.nextSetBit(currentPortCursor); - if (p > 0) { - availablePorts.clear(p); - currentPortCursor = p + 1; - done++; - return p; - } - - throw new NoSuchElementException("Reached end of port range"); - } - - @Override - public boolean hasNext() { - return availablePorts.nextSetBit(currentPortCursor) > 0; - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/PortResultAccumulator.java b/src/main/java/com/it_jaros/jscanner/scan/engine/PortResultAccumulator.java deleted file mode 100644 index 0cc26b0..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/PortResultAccumulator.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import com.it_jaros.jscanner.scan.ExceptionInfo; -import com.it_jaros.jscanner.scan.domain.*; -import com.it_jaros.jscanner.scan.service.ServiceDetector; -import com.it_jaros.jscanner.scan.service.ServiceType; - -import java.util.*; - -final class PortResultAccumulator { - private final String host; - private final BitSet openPorts = new BitSet(PortRange.MAX_PORT); - private final BitSet filteredPorts = new BitSet(PortRange.MAX_PORT); - private final Map<Integer, ServiceType> serviceTypes = new HashMap<>(); - private final List<ScanFailure> scanFailures = new ArrayList<>(); - - PortResultAccumulator(String host) { - this.host = host; - } - - void add(PortResult portResult) { - switch (portResult.getState()) { - case OPEN -> { - openPorts.set(portResult.getPort()); - serviceTypes.put(portResult.getPort(), ServiceDetector.detect(portResult.getBanner())); - } - case FILTERED -> { - filteredPorts.set(portResult.getPort()); - } - default -> { - // intentional no-op for uncovered port states - } - } - Exception e = portResult.getException(); - if (e != null) { - scanFailures.add(new ScanFailure(portResult.getPort(), ExceptionInfo.from(e))); - } - } - - ScanResult build() { - return new ScanResult( - host, - new PortList(openPorts), - new PortList(filteredPorts), - Collections.unmodifiableMap(serviceTypes), - Collections.unmodifiableList(scanFailures) - ); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/PortScanRateLimiter.java b/src/main/java/com/it_jaros/jscanner/scan/engine/PortScanRateLimiter.java deleted file mode 100644 index 92b3bcb..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/PortScanRateLimiter.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import java.util.concurrent.locks.LockSupport; - -/** - * Per-host rate limiter. Each ScanHostTask creates its own instance and - * shares it with all its ScanPortTasks via constructor. - * - * Java allows one inner class to access another's private members, so this works. - */ -public class PortScanRateLimiter { - private final Object lock = new Object(); - private volatile long nextAllowedTime; - private final long delayInNanos; - private final CancelledToken cancelledToken; - - public PortScanRateLimiter(CancelledToken cancelledToken, long delayInNanos) { - this.cancelledToken = cancelledToken; - this.delayInNanos = delayInNanos; - } - - void apply() { if (delayInNanos <= 0) { - return; - } - synchronized (lock) { - if (cancelledToken.isCancelled()) { - return; - } - long now = System.nanoTime(); - if (nextAllowedTime > now) { - LockSupport.parkNanos(nextAllowedTime - now); - now = System.nanoTime(); // re-read after waking - } - nextAllowedTime = now + delayInNanos; - } - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerState.java b/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerState.java deleted file mode 100644 index c49ea3e..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerState.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import java.util.concurrent.CompletionService; -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -public record ProducerState<OUTPUT>( - AtomicBoolean running, - AtomicInteger inPipeline, - Semaphore activeWorkers, - CompletionService<OUTPUT> completionService -) {} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerThread.java b/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerThread.java deleted file mode 100644 index e7bcbcc..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/ProducerThread.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import java.time.Duration; -import java.util.Iterator; -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; - -public class ProducerThread { - - public static final Duration pollInterval = Duration.ofSeconds(1); - - private final ExecutorService executor; - private final CancelledToken cancelledToken; - - public ProducerThread(ScanExecutionContext context) { - this.executor = context.executorService(); - this.cancelledToken = context.cancelledToken(); - } - - /** - * This method helps to cleanup the code a bit and remove redundancy - * The producer for providing hosts and the one for providing ports - * are similar and the small differences can be handled using a function - * - * @param queue - * @param maxWorkers - * @param taskFactory - * @param <INPUT> - * @param <OUTPUT> - * @return - */ - public <INPUT, OUTPUT> ProducerState<OUTPUT> startProducer( - Iterator<INPUT> queue, - int maxWorkers, - Function<INPUT, Callable<OUTPUT>> taskFactory - ) { - final AtomicInteger inPipeline = new AtomicInteger(0); - final AtomicBoolean running = new AtomicBoolean(true); - final Semaphore activeWorkers = new Semaphore(maxWorkers); - CompletionService<OUTPUT> completionService = new ExecutorCompletionService<>(executor); - executor.submit(() -> { - try { - while (!cancelledToken.isCancelled() && queue.hasNext()) { - // get semaphore and remember if task got submitted - // so in case we fail to submit we release the semaphore - activeWorkers.acquire(); - boolean isTaskSubmitted = false; - try { - // just in case something - // changed while waiting - if (cancelledToken.isCancelled()) { - break; - } - - // get next item and create callable - // using lambda expression - final INPUT item = queue.next(); - Callable<OUTPUT> task = taskFactory.apply(item); - inPipeline.incrementAndGet(); - try { - completionService.submit(task); - isTaskSubmitted = true; - } catch (Throwable e) { - inPipeline.decrementAndGet(); - throw e; - } - } finally { - if (!isTaskSubmitted) { - activeWorkers.release(); - } - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } finally { - running.set(false); - } - }); - return new ProducerState<>(running, inPipeline, activeWorkers, completionService); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanExecutionContext.java b/src/main/java/com/it_jaros/jscanner/scan/engine/ScanExecutionContext.java deleted file mode 100644 index 38e9cdb..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanExecutionContext.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import com.it_jaros.jscanner.scan.Scan; -import com.it_jaros.jscanner.scan.ScanOptions; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Semaphore; - -public record ScanExecutionContext( - ScanOptions scanOptions, - ExecutorService executorService, - Semaphore socketLimit, - CancelledToken cancelledToken, - Scan scan, - PortScanRateLimiter portScanRateLimiter -) {} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanHostTask.java b/src/main/java/com/it_jaros/jscanner/scan/engine/ScanHostTask.java deleted file mode 100644 index 7a01d54..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanHostTask.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import com.it_jaros.jscanner.scan.Scan; -import com.it_jaros.jscanner.scan.domain.PortRange; -import com.it_jaros.jscanner.scan.domain.PortResult; -import com.it_jaros.jscanner.scan.domain.ScanResult; - -import java.io.IOException; -import java.net.InetAddress; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; - -public class ScanHostTask implements Callable<ScanResult> { - - private final Scan scan; - private final String host; // input parameter - private final ScanExecutionContext context; - private final CancelledToken cancelledToken; - private final boolean disableOnlineCheck; - private final int maxWorkersPerHost; - private final int timeoutInMillis; - private final long delayInNanos; - - public ScanHostTask(Scan scan, String host, ScanExecutionContext context) { - this.scan = scan; - this.host = host; - this.context = context; - this.cancelledToken = context.cancelledToken(); - this.delayInNanos = TimeUnit.MILLISECONDS.toNanos(Math.max(0, context.scanOptions().delayInMillis())); - this.disableOnlineCheck = context.scanOptions().disableOnlineCheck(); - this.maxWorkersPerHost = context.scanOptions().maxWorkersPerHost(); - this.timeoutInMillis = context.scanOptions().timeoutInMillis(); - } - - @Override - public ScanResult call() { - try { - scan.hostStart(); - if (context.cancelledToken().isCancelled()) { - return ScanResult.empty(host); - } - return scanHostPorts(); - } finally { - scan.hostFinish(); - } - } - - private ScanResult scanHostPorts() { - if (!disableOnlineCheck) { - boolean isHostOnline = checkHostOnline(); - if (!isHostOnline) { - // Unreachable host - return ScanResult.empty(host); - } - // online check also sends packets to the target system. - // in order not to violate set delay time - // we wait here too - LockSupport.parkNanos(delayInNanos); - } - - final PortRange portRange = new PortRange(scan.getPorts()); - // producer thread - scan.producerStart(); - ProducerState<PortResult> state = new ProducerThread(context).startProducer( - portRange.iterator(), - maxWorkersPerHost, - port -> new ScanPortTask(host, port, context) - ); - - // consumer is the main thread - // we run as long as the producer is running OR - // as long as things are in pipeline waiting to be processed - // ONLY exception is when cancelled is set - final PortResultAccumulator accumulator = new PortResultAccumulator(host); - while (!cancelledToken.isCancelled() && (state.running().get() || state.inPipeline().get() > 0)) { - try { - PollState<PortResult> poll = getPortResult(state); - if (poll instanceof PollState.Success<PortResult>(PortResult value)) { - accumulator.add(value); - } else if (poll instanceof PollState.Failure(Throwable error)) { - System.err.printf("scanHostPorts(%s): ScanPortTask() failed for with error %s -> %s%n", host, error.getClass().getSimpleName(), error.getMessage()); - } - } catch (InterruptedException ignored) { - Thread.currentThread().interrupt(); - } - } - scan.producerStop(); - - return accumulator.build(); - } - - private boolean checkHostOnline() { - try { - return InetAddress.getByName(host).isReachable(timeoutInMillis); - } catch (IOException e) { - // we ignore this error because it means that the host is probably not online - } - - return false; - } - - private PollState<PortResult> getPortResult(ProducerState<PortResult> state) throws InterruptedException { - Future<PortResult> portResultFuture = state.completionService().poll(ProducerThread.pollInterval.toMillis(), TimeUnit.MILLISECONDS); - if (portResultFuture == null) { - return new PollState.Unavailable<>(); - } - - PortResult portResult; - try { - portResult = portResultFuture.get(); - return new PollState.Success<>(portResult); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - return new PollState.Failure<>(cause); - } finally { - state.activeWorkers().release(); - state.inPipeline().decrementAndGet(); - } - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanPortTask.java b/src/main/java/com/it_jaros/jscanner/scan/engine/ScanPortTask.java deleted file mode 100644 index 873ec95..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/engine/ScanPortTask.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.it_jaros.jscanner.scan.engine; - -import com.it_jaros.jscanner.scan.Scan; -import com.it_jaros.jscanner.scan.domain.PortResult; -import com.it_jaros.jscanner.scan.domain.PortState; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.net.ConnectException; -import java.net.InetSocketAddress; -import java.net.NoRouteToHostException; -import java.nio.ByteBuffer; -import java.nio.channels.SocketChannel; -import java.util.concurrent.Callable; -import java.util.concurrent.Semaphore; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.LockSupport; - -public class ScanPortTask implements Callable<PortResult> { - - private static final int READ_BUFFER_SIZE = 1024; - - private final CancelledToken cancelledToken; - private final PortScanRateLimiter portScanRateLimiter; // per-host shared limiter - private final Scan scan; - private final Semaphore socketLimit; - private final String host; - private final boolean bannerRecognition; - private final int port; - private final long timeoutInNanos; - - ScanPortTask(String host, int port, ScanExecutionContext context) { - this.host = host; - this.port = port; - this.bannerRecognition = context.scanOptions().bannerRecognition(); - this.cancelledToken = context.cancelledToken(); - this.portScanRateLimiter = context.portScanRateLimiter(); - this.scan = context.scan(); - this.socketLimit = context.socketLimit(); - this.timeoutInNanos = TimeUnit.MILLISECONDS.toNanos(context.scanOptions().timeoutInMillis()); - } - - @Override - public PortResult call() throws Exception { - try { - socketLimit.acquire(); - scan.portStart(); - portScanRateLimiter.apply(); - if (cancelledToken.isCancelled()) { - return PortResult.empty(); - } - return checkPort(); - } finally { - scan.portFinish(); - socketLimit.release(); - } - } - - private PortResult checkPort() { - PortResult result = new PortResult(); - result.setPort(port); - result.setState(PortState.UNKNOWN); - try(SocketChannel socketChannel = SocketChannel.open()) { - socketChannel.configureBlocking(false); - socketChannel.connect(new InetSocketAddress(host, port)); - final long deadlineNanos = System.nanoTime() + timeoutInNanos; - final long waitInNanos = TimeUnit.MILLISECONDS.toNanos(1000); - boolean isConnected = socketChannel.finishConnect(); - while (!cancelledToken.isCancelled() && !isConnected) { - long remainingNanos = deadlineNanos - System.nanoTime(); - if (remainingNanos <= 0) { - break; - } - LockSupport.parkNanos(Math.min(waitInNanos, remainingNanos)); - isConnected = socketChannel.finishConnect(); - } - - if (cancelledToken.isCancelled()) { - return result; - } - - if (isConnected) { - result.setState(PortState.OPEN); - if (bannerRecognition) { - result.setBanner(getBanner(socketChannel)); - } - } else { - result.setState(PortState.FILTERED); - } - } catch (ConnectException ignored) { - result.setState(PortState.CLOSED); - } catch (NoRouteToHostException ignored) { - // NoRouteToHostException: this can be safely ignored because the port is closed if a host is unreachable - // Will happen a lot when scanning for open ports, so not needed - } catch (IOException e) { - result.setException(e); - } - - return result; - } - - private byte[] getBanner(SocketChannel socketChannel) { - try { - return tryReadFrom(socketChannel); - } catch (IOException ignore) { - // ignore - } - - return null; - } - - private byte[] tryReadFrom(SocketChannel socketChannel) throws IOException { - byte[] buffer = new byte[READ_BUFFER_SIZE]; - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - - boolean timedout = false; - long deadlineNanos = System.nanoTime() + timeoutInNanos; - while (!timedout) { - int bytesRead = socketChannel.read(ByteBuffer.wrap(buffer)); - if (bytesRead == -1) { - break; - } - if (bytesRead > 0) { - byteArrayOutputStream.write(buffer, 0, bytesRead); - } - long remaining = deadlineNanos - System.nanoTime(); - if (remaining <= 0) { - timedout = true; - } else { - LockSupport.parkNanos(Math.min(remaining, TimeUnit.MILLISECONDS.toNanos(1000))); - } - } - return byteArrayOutputStream.toByteArray(); - } -} diff --git a/src/main/java/com/it_jaros/jscanner/scan/service/ServiceDetector.java b/src/main/java/com/it_jaros/jscanner/scan/service/ServiceDetector.java deleted file mode 100644 index 93e05d4..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/service/ServiceDetector.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.it_jaros.jscanner.scan.service; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.regex.Pattern; - -public class ServiceDetector { - private static final List<ServicePattern> patterns = List.of( - new ServicePattern(ServiceType.DNS, Pattern.compile("(?i)\\bDNS\\b")), - new ServicePattern(ServiceType.DNS, Pattern.compile("(?i)\\bBIND\\b")), - new ServicePattern(ServiceType.DNS, Pattern.compile("(?i)\\bNSD\\b")), - new ServicePattern(ServiceType.DNS, Pattern.compile("(?i)\\bPowerDNS\\b")), - new ServicePattern(ServiceType.DNS, Pattern.compile("(?i)\\bUnbound\\b")), - new ServicePattern(ServiceType.FTP, Pattern.compile("(?i)^220.*\\bFTP\\b")), - new ServicePattern(ServiceType.HTTP, Pattern.compile("(?i)\\bServer:\\s*")), - new ServicePattern(ServiceType.HTTP, Pattern.compile("(?i)^HTTP/")), - new ServicePattern(ServiceType.IMAP, Pattern.compile("(?i)^\\* OK.*\\bIMAP\\b")), - new ServicePattern(ServiceType.POP3, Pattern.compile("(?i)^\\+OK.*\\bPOP3\\b")), - new ServicePattern(ServiceType.SMTP, Pattern.compile("(?i)^220.*\\bESMTP\\b")), - new ServicePattern(ServiceType.SMTP, Pattern.compile("(?i)^220.*\\bSMTP\\b")), - new ServicePattern(ServiceType.SSH, Pattern.compile("(?i)^SSH-")) - ); - - public static ServiceType detect(byte[] banner) { - if (banner == null || banner.length == 0) { - return ServiceType.UNKNOWN; - } - - String bannerString = new String(banner, StandardCharsets.UTF_8); - for (ServicePattern servicePattern : patterns) { - if (servicePattern.matches(bannerString)) { - return servicePattern.serviceType(); - } - } - - return ServiceType.UNKNOWN; - } - - private record ServicePattern( - ServiceType serviceType, - Pattern pattern - ) { - boolean matches(String banner) { - return pattern.matcher(banner).find(); - } - } -}
\ No newline at end of file diff --git a/src/main/java/com/it_jaros/jscanner/scan/service/ServiceType.java b/src/main/java/com/it_jaros/jscanner/scan/service/ServiceType.java deleted file mode 100644 index 86b282a..0000000 --- a/src/main/java/com/it_jaros/jscanner/scan/service/ServiceType.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.it_jaros.jscanner.scan.service; - -public enum ServiceType { - HTTP, FTP, SMTP, IMAP, POP3, UNKNOWN, DNS, SSH -} |
