diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7466b12 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +*.iml +target/ diff --git a/pom.xml b/pom.xml index 7a01d24..ad7dbd3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,80 +1,104 @@ - + - 4.0.0 + 4.0.0 - - org.sonatype.oss - oss-parent - 7 - + + org.sonatype.oss + oss-parent + 9 + - fi.solita.clamav - clamav-client - 1.0.1 - jar - Simple ClamAV client - Simple Java client for using clamd INSTREAM scanning in your application. - https://github.com/solita/clamav-java - - - GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1 - http://www.gnu.org/licenses/lgpl.txt - - - - - Antti Virtanen - antti.virtanen@solita.fi - Solita - http://www.solita.fi - - + fi.solita.clamav + clamav-client + 1.0.1 + jar + Simple ClamAV client + Simple Java client for using clamd INSTREAM scanning in your application. + https://github.com/solita/clamav-java + + + GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1 + http://www.gnu.org/licenses/lgpl.txt + + + + + Antti Virtanen + antti.virtanen@solita.fi + Solita + http://www.solita.fi + + - - UTF-8 - + + UTF-8 + - - scm:git:git://github.com/solita/clamav-java.git - scm:git:git@github.com:solita/clamav-java.git - https://github.com/solita/clamav-java - + + scm:git:git://github.com/solita/clamav-java.git + scm:git:git@github.com:solita/clamav-java.git + https://github.com/solita/clamav-java + - - - junit - junit - 4.11 - test - - + + + org.junit.jupiter + junit-jupiter + 5.14.0 + test + + + org.testcontainers + testcontainers + 1.21.3 + test + + + org.testcontainers + junit-jupiter + 1.21.3 + test + + + org.slf4j + slf4j-simple + 2.0.17 + test + + + org.slf4j + slf4j-api + 2.0.17 + test + + - - - - maven-compiler-plugin - 2.3.2 - - 1.7 - 1.7 - - + + + + maven-compiler-plugin + 3.14.1 + + 11 + 11 + + - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 - true - - https://oss.sonatype.org/ - ossrh-releases-fi.solita - ff5044adfb72 - - + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.3 + true + + https://oss.sonatype.org/ + ossrh-releases-fi.solita + ff5044adfb72 + + - - + + diff --git a/src/main/java/fi/solita/clamav/ClamAVClient.java b/src/main/java/fi/solita/clamav/ClamAVClient.java index 7f14e11..ce3b1db 100644 --- a/src/main/java/fi/solita/clamav/ClamAVClient.java +++ b/src/main/java/fi/solita/clamav/ClamAVClient.java @@ -139,7 +139,7 @@ public static boolean isCleanReply(byte[] reply) { private byte[] assertSizeLimit(byte[] reply) { String r = new String(reply, StandardCharsets.US_ASCII); if (r.startsWith("INSTREAM size limit exceeded.")) - throw new ClamAVSizeLimitException("Clamd size limit exceeded. Full reply from server: " + r); + throw new ClamAVSizeLimitException("Clamd size limit exceeded. Full reply from server: " + r.trim()); return reply; } diff --git a/src/test/java/fi/solita/clamav/BaseTestcontainers.java b/src/test/java/fi/solita/clamav/BaseTestcontainers.java new file mode 100644 index 0000000..2a3499b --- /dev/null +++ b/src/test/java/fi/solita/clamav/BaseTestcontainers.java @@ -0,0 +1,49 @@ +package fi.solita.clamav; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.provider.Arguments; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy; +import org.testcontainers.utility.DockerImageName; + +import java.util.stream.Stream; + +public abstract class BaseTestcontainers { + public static final ClamavContainer clamav14 = new ClamavContainer("clamav/clamav:1.4"); + public static final ClamavContainer clamav15 = new ClamavContainer("clamav/clamav:1.5"); + + @BeforeAll + static void beforeAll() { + clamav14.start(); + clamav15.start(); + } + + @AfterAll + static void afterAll() { + clamav15.stop(); + clamav14.stop(); + } + + private static Stream provideClamdContainers() { + // INFO: We have to wrap GenericContainer info Supplier wrapper to force JUnit not to close AutoClosable Test method parameter + return Stream.of( + Arguments.of("1.4", clamav14), + Arguments.of("1.5", clamav15) + ); + } + + protected static ClamAVClient client(GenericContainer container) { + return new ClamAVClient("localhost", container.getFirstMappedPort()); + } +} + +class ClamavContainer extends GenericContainer { + public static final int DEFAULT_PORT = 3310; + + ClamavContainer(String image) { + super(DockerImageName.parse(image)); + this.waitingFor(new HostPortWaitStrategy().forPorts(3310)); + this.withExposedPorts(3310); + } +} \ No newline at end of file diff --git a/src/test/java/fi/solita/clamav/InstreamTest.java b/src/test/java/fi/solita/clamav/InstreamTest.java index 6177df6..e8c3b33 100644 --- a/src/test/java/fi/solita/clamav/InstreamTest.java +++ b/src/test/java/fi/solita/clamav/InstreamTest.java @@ -1,66 +1,72 @@ package fi.solita.clamav; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.GenericContainer; import java.io.IOException; -import java.io.InputStream; -import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * These tests assume clamd is running and responding in the virtual machine. - */ -public class InstreamTest { +public class InstreamTest extends BaseTestcontainers { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testRandomBytes(String clamdVersion, GenericContainer container) throws IOException { + byte[] r = client(container).scan("alsdklaksdla".getBytes(StandardCharsets.US_ASCII)); + assertTrue(ClamAVClient.isCleanReply(r)); + } - private static String CLAMAV_HOST = "localhost"; - - private byte[] scan(byte[] input) throws UnknownHostException, IOException { - ClamAVClient cl = new ClamAVClient(CLAMAV_HOST, 3310); - return cl.scan(input); - } - - private byte[] scan(InputStream input) throws UnknownHostException, IOException { - ClamAVClient cl = new ClamAVClient(CLAMAV_HOST, 3310); - return cl.scan(input); - } - @Test - public void testRandomBytes() throws UnknownHostException, IOException { - byte[] r = scan("alsdklaksdla".getBytes("ASCII")); - assertTrue(ClamAVClient.isCleanReply(r)); - } - - @Test - public void testPositive() throws UnknownHostException, IOException { - // http://www.eicar.org/86-0-Intended-use.html - byte[] EICAR = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*".getBytes("ASCII"); - byte[] r = scan(EICAR); - assertFalse(ClamAVClient.isCleanReply(r)); - } + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testPositive(String clamdVersion, GenericContainer container) throws IOException { + // http://www.eicar.org/86-0-Intended-use.html + byte[] EICAR = "X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*".getBytes(StandardCharsets.US_ASCII); + byte[] r = client(container).scan(EICAR); + assertFalse(ClamAVClient.isCleanReply(r)); + } - @Test - public void testStreamChunkingWorks() throws UnknownHostException, IOException { - byte[] multipleChunks = new byte[50000]; - byte[] r = scan(multipleChunks); - assertTrue(ClamAVClient.isCleanReply(r)); - } - - @Test - public void testChunkLimit() throws UnknownHostException, IOException { - byte[] maximumChunk = new byte[2048]; - byte[] r = scan(maximumChunk); - assertTrue(ClamAVClient.isCleanReply(r)); - } - - @Test - public void testZeroBytes() throws UnknownHostException, IOException { - byte[] r = scan(new byte[]{}); - assertTrue(ClamAVClient.isCleanReply(r)); - } + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testStreamChunkingWorks(String clamdVersion, GenericContainer container) throws IOException { + byte[] multipleChunks = new byte[50000]; + byte[] r = client(container).scan(multipleChunks); + assertTrue(ClamAVClient.isCleanReply(r)); + } - @Test(expected = ClamAVSizeLimitException.class) - public void testSizeLimit() throws UnknownHostException, IOException { - scan(new SlowInputStream()); - } + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testChunkLimit(String clamdVersion, GenericContainer container) throws IOException { + byte[] maximumChunk = new byte[2048]; + byte[] r = client(container).scan(maximumChunk); + assertTrue(ClamAVClient.isCleanReply(r)); + } + + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testZeroBytes(String clamdVersion, GenericContainer container) throws IOException { + byte[] r = client(container).scan(new byte[]{}); + assertTrue(ClamAVClient.isCleanReply(r)); + } + + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testSizeLimit(String clamdVersion, GenericContainer container) { + ClamAVSizeLimitException exception = Assertions.assertThrows(ClamAVSizeLimitException.class, + () -> client(container).scan(new SlowInputStream(false))); + assertEquals("Clamd size limit exceeded. Full reply from server: INSTREAM size limit exceeded. ERROR", exception.getMessage()); + } + + @Disabled + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testSizeLimitSuperSlow(String clamdVersion, GenericContainer container) { + ClamAVSizeLimitException exception = Assertions.assertThrows(ClamAVSizeLimitException.class, + () -> client(container).scan(new SlowInputStream(true))); + assertEquals("Clamd size limit exceeded. Full reply from server: INSTREAM size limit exceeded. ERROR", exception.getMessage()); + } } diff --git a/src/test/java/fi/solita/clamav/PingTest.java b/src/test/java/fi/solita/clamav/PingTest.java index 29b9f72..1ba9395 100644 --- a/src/test/java/fi/solita/clamav/PingTest.java +++ b/src/test/java/fi/solita/clamav/PingTest.java @@ -1,20 +1,18 @@ package fi.solita.clamav; -import static org.junit.Assert.assertTrue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.testcontainers.containers.GenericContainer; import java.io.IOException; -import java.net.UnknownHostException; -import org.junit.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; -/** - * These tests assume clamd is running and responding in the virtual machine. - */ -public class PingTest { - - @Test - public void testPingPong() throws UnknownHostException, IOException { - ClamAVClient cl = new ClamAVClient("localhost", 3310); - assertTrue(cl.ping()); - } +public class PingTest extends BaseTestcontainers { + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("provideClamdContainers") + public void testPingPong(String clamdVersion, GenericContainer container) throws IOException { + ClamAVClient client = client(container); + assertTrue(client.ping()); + } } diff --git a/src/test/java/fi/solita/clamav/SlowInputStream.java b/src/test/java/fi/solita/clamav/SlowInputStream.java index 46e6899..236b796 100644 --- a/src/test/java/fi/solita/clamav/SlowInputStream.java +++ b/src/test/java/fi/solita/clamav/SlowInputStream.java @@ -3,27 +3,34 @@ import java.io.InputStream; import java.util.Random; -/** - * Random bytes generated, until in the end it slows down.. this enables testing the clamd +/** + * Random bytes generated, until in the end it slows down.. this enables testing the clamd * stream length limit as clamd has enough time to reply, close the socket or whatever. */ public class SlowInputStream extends InputStream { - private static long SLOW_BYTE = 1000; - private long bytesAvail = 60000; - private Random r = new Random(); + private static final long SLOW_BYTE = 1000; + private final Random r = new Random(); + private final boolean sleep; + private long bytesAvail = 60000; + + public SlowInputStream(boolean sleep) { + this.sleep = sleep; + } @Override public int read() { - int nextByte = r.nextInt(256); - if ((bytesAvail < 12000) && ((bytesAvail % SLOW_BYTE) == 0)) { - try { - Thread.sleep(80); // 80 ms sleep - } catch (Exception e) { - throw new RuntimeException(e); + int nextByte = r.nextInt(256); + if ((bytesAvail < 12000) && ((bytesAvail % SLOW_BYTE) == 0)) { + if (sleep) { + try { + Thread.sleep(80); // 80 ms sleep + } catch (Exception e) { + throw new RuntimeException(e); + } + } } - } - bytesAvail--; - return (bytesAvail == 0) ? -1 : nextByte; + bytesAvail--; + return (bytesAvail == 0) ? -1 : nextByte; } }