From 1cbb5b6ffd5afdf55ea6727ee23550796a57809b Mon Sep 17 00:00:00 2001 From: Matthias Jaros Date: Wed, 12 Aug 2026 16:36:28 +0200 Subject: Renamed jscanner package to jns --- .../jscanner/scan/engine/CancelledToken.java | 15 --- .../it_jaros/jscanner/scan/engine/PollState.java | 13 -- .../jscanner/scan/engine/PortRangeIterator.java | 38 ------ .../scan/engine/PortResultAccumulator.java | 49 -------- .../jscanner/scan/engine/PortScanRateLimiter.java | 37 ------ .../jscanner/scan/engine/ProducerState.java | 13 -- .../jscanner/scan/engine/ProducerThread.java | 83 ------------- .../jscanner/scan/engine/ScanExecutionContext.java | 16 --- .../jscanner/scan/engine/ScanHostTask.java | 123 ------------------- .../jscanner/scan/engine/ScanPortTask.java | 135 --------------------- 10 files changed, 522 deletions(-) delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/CancelledToken.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/PollState.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/PortRangeIterator.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/PortResultAccumulator.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/PortScanRateLimiter.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/ProducerState.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/ProducerThread.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/ScanExecutionContext.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/ScanHostTask.java delete mode 100644 src/main/java/com/it_jaros/jscanner/scan/engine/ScanPortTask.java (limited to 'src/main/java/com/it_jaros/jscanner/scan/engine') 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 { - record Success(T value) implements PollState {} - record Failure(Throwable error) implements PollState {} - record Unavailable() implements PollState {} -} 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 { - - 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 serviceTypes = new HashMap<>(); - private final List 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( - AtomicBoolean running, - AtomicInteger inPipeline, - Semaphore activeWorkers, - CompletionService 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 - * @param - * @return - */ - public ProducerState startProducer( - Iterator queue, - int maxWorkers, - Function> taskFactory - ) { - final AtomicInteger inPipeline = new AtomicInteger(0); - final AtomicBoolean running = new AtomicBoolean(true); - final Semaphore activeWorkers = new Semaphore(maxWorkers); - CompletionService 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 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 { - - 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 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 poll = getPortResult(state); - if (poll instanceof PollState.Success(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 getPortResult(ProducerState state) throws InterruptedException { - Future 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 { - - 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(); - } -} -- cgit v1.3.1