Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
lokori committed Sep 25, 2014
0 parents commit e2f96a1
Show file tree
Hide file tree
Showing 8 changed files with 868 additions and 0 deletions.
502 changes: 502 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
Simple [ClamAV](http://www.clamav.net/) Java client

# What is provided

Support for basic INSTREAM scanning and PING command.

Clamd protocol is explained here:
http://linux.die.net/man/8/clamd

# Using the client

Code is self explanatory. Something like this is the idea:

```
ClamAVClient cl = new ClamAVClient("192.168.50.72", 3310);
byte[] reply = cl.scan(input);
if (!ClamAVClient.isCleanReply(reply)) throw new Exception("aaargh");
```

# Creating the jar

```
mvn install
```

# Testing the client

To run the automated tests you are assumed to run the clamd in a local virtual machine.
Configuration for [Vagrant](http://www.vagrantup.com/) and [https://www.virtualbox.org/](Oracle Virtualbox) is provided.

To start test server simply

```
cd vagrant
vagrant up clamav
```

This will kick up a CentOS virtual machine and install [ClamAV](http://www.clamav.net/) in it.

# License

Copyright © 2014 [Solita](http://www.solita.fi)

Distributed under the GNU Lesser General Public License, either version 2.1 of the License, or
(at your option) any later version.

17 changes: 17 additions & 0 deletions env/clamd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/bin/bash
set -eu

# EPEL
rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm

# ClamAV
yum install -y clamav clamd

# take off firewall (local virtual machine - ok)
iptables -F

# listen to our local IP, not only on localhost
echo 'TCPAddr 192.168.50.72' >> /etc/clamd.conf

service clamd restart

60 changes: 60 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>fi.solita.clamav</groupId>
<artifactId>clamav-client</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>Simple ClamAV client</name>
<description>Simple Java client for using clamd INSTREAM scanning in your application.</description>
<url>https://github.com/solita/clamav-java</url>
<licenses>
<license>
<name>GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1</name>
<url>http://www.gnu.org/licenses/lgpl.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>Antti Virtanen</name>
<email>[email protected]</email>
<organization>Solita</organization>
<organizationUrl>http://www.solita.fi</organizationUrl>
</developer>
</developers>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<scm>
<connection>scm:git:git://github.com/solita/clamav-java.git</connection>
<developerConnection>scm:git:[email protected]:solita/clamav-java.git</developerConnection>
<url>https://github.com/solita/clamav-java</url>
</scm>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
134 changes: 134 additions & 0 deletions src/main/java/fi/solita/clamav/ClamAVClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package fi.solita.clamav;

import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.Arrays;

/**
* Simple client for ClamAV's clamd scanner. Provides straightforward instream scanning.
*/
public class ClamAVClient {

private String hostName;
private int port;
private int timeout;

// "do not exceed StreamMaxLength as defined in clamd.conf, otherwise clamd will reply with INSTREAM size limit exceeded and close the connection."
private static final int CHUNK_SIZE = 2048;
private static final int DEFAULT_TIMEOUT = 500;

/**
* @param timeout zero means infinite timeout. Not a good idea, but will be accepted.
*/
public ClamAVClient(String hostName, int port, int timeout) {
if (timeout < 0) throw new IllegalArgumentException("Negative timeout value does not make sense.");
this.hostName = hostName;
this.port = port;
this.timeout = timeout;
}

public ClamAVClient(String hostName, int port) {
this(hostName, port, DEFAULT_TIMEOUT);
}

/**
* Run PING command to clamd to test it is responding.
*
* @return true if the server responded with proper ping reply.
*/
public boolean ping() throws IOException {
try (Socket s = new Socket(hostName,port);
OutputStream outs = s.getOutputStream(); )
{
s.setSoTimeout(timeout);
outs.write(asBytes("zPING\0"));
outs.flush();
byte[] b = new byte[4];
s.getInputStream().read(b);
return Arrays.equals(b, asBytes("PONG"));
}
}

/**
* Streams the given data to the server in chunks. The whole data is not kept in memory.
* <p>
* Opens a socket and reads the reply. Parameter input stream is NOT closed.
*
* @param is data to scan. Not closed by this method!
* @return server reply
*/
public byte[] scan(InputStream is) throws IOException {
try (Socket s = new Socket(hostName,port);
OutputStream outs = new BufferedOutputStream(s.getOutputStream()); )
{
s.setSoTimeout(timeout);

// handshake
outs.write(asBytes("zINSTREAM\0"));
outs.flush();
byte[] chunk = new byte[CHUNK_SIZE];

// send data
int read = is.read(chunk);
while (read >= 0) {
// The format of the chunk is: '<length><data>' where <length> is the size of the following data in bytes expressed as a 4 byte unsigned
// integer in network byte order and <data> is the actual chunk. Streaming is terminated by sending a zero-length chunk.
byte[] chunkSize = ByteBuffer.allocate(4).putInt(read).array();
outs.write(chunkSize);
outs.write(chunk, 0, read);
read = is.read(chunk);
}

// terminate scan
outs.write(new byte[]{0,0,0,0});
outs.flush();

// read reply
try (InputStream clamIs = s.getInputStream();) {
return readAll(clamIs);
}
}
}

/**
* @param in data to scan
* @return server reply
**/
public byte[] scan(byte[] in) throws IOException {
ByteArrayInputStream bis = new ByteArrayInputStream(in);
return scan(bis);
}

/**
* @return true if no virus was found according to the clamd reply message
*/
public static boolean isCleanReply(byte[] reply) throws UnsupportedEncodingException {
String r = new String(reply, "ASCII");
return (r.contains("OK") && !r.contains("FOUND"));
}

// byte conversion based on ASCII character set regardless of the current system locale
private static byte[] asBytes(String s) throws UnsupportedEncodingException {
return s.getBytes("ASCII");
}

// reads all available bytes from the stream
private static byte[] readAll(InputStream is) throws IOException {
ByteArrayOutputStream tmp = new ByteArrayOutputStream();

byte[] buf = new byte[2000];
int read = is.read(buf);
while (read > 0) {
tmp.write(buf, 0, read);
read = is.read(buf);
}
return tmp.toByteArray();
}
}
60 changes: 60 additions & 0 deletions src/test/java/fi/solita/clamav/InstreamTests.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package fi.solita.clamav;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.UnknownHostException;

import org.junit.Test;

/**
* These tests assume clamd is running and responding in the virtual machine.
*/
public class InstreamTests {

private byte[] scan(byte[] input) throws UnknownHostException, IOException {
ClamAVClient cl = new ClamAVClient("192.168.50.72", 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));
}

@Test
public void testEmptyBytes() throws UnknownHostException, IOException {
byte[] r = scan(new byte[]{});
assertTrue(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));
}
}
20 changes: 20 additions & 0 deletions src/test/java/fi/solita/clamav/PingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package fi.solita.clamav;

import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.UnknownHostException;

import org.junit.Test;

/**
* 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("192.168.50.72", 3310);
assertTrue(cl.ping());
}
}
29 changes: 29 additions & 0 deletions vagrant/Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|

# https://github.com/fgrehm/vagrant-cachier#quick-start
if Vagrant.has_plugin?("vagrant-cachier")
config.cache.scope = :box
config.cache.synced_folder_opts = {
type: :nfs,
mount_options: ['rw', 'vers=3', 'tcp', 'nolock']
}
end

vmbox = "CentOS-6.5-x86_64-v20140311.box"
vmbox_url = "http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.5-x86_64-v20140311.box"


# Test server for ClamAV virus scanner
config.vm.define "clamav" do |clamav|
clamav.vm.box = vmbox
clamav.vm.box_url = vmbox_url

clamav.vm.synced_folder "../env", "/env"
clamav.vm.provision "shell", inline: "cd /env && ./clamd.sh"

clamav.vm.network "private_network", ip: "192.168.50.72"
end
end

0 comments on commit e2f96a1

Please sign in to comment.