All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.hayesroach.clamav.ClamAVClient Maven / Gradle / Ivy

The newest version!
package io.github.hayesroach.clamav;

import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;

import java.io.*;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
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 final int timeout;
  private File unixSocket;

  // "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;
  private static final int PONG_REPLY_LEN = 4;

  /**
   * @param hostName The hostname of the server running clamav-daemon
   * @param port The port that clamav-daemon listens to(By default it might not listen to a port. Check your clamav configuration).
   * @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);
  }

  /**
   * @param unixSocket The unix domain socket (AF_UNIX) of the server running clamav-daemon that is defined in the clamav configuration. Usually bound to the filesystem pathname: /var/run/clamav/clamd.ctl
   * @param timeout zero means infinite timeout. Not a good idea, but will be accepted.
   */
  public ClamAVClient(File unixSocket, int timeout)  {
    if (timeout < 0) {
      throw new IllegalArgumentException("Negative timeout value does not make sense.");
    }
    this.unixSocket = unixSocket;
    this.timeout = timeout;
  }

  public ClamAVClient(File unixSocket) {
    this(unixSocket, DEFAULT_TIMEOUT);
  }

  /**
   * Run PING command to clamd to test it is responding.
   * @return true if the server responded with proper ping reply.
   * @throws IOException when io error occurs
   */
  public boolean ping() throws IOException {
    if(hostName != null) {
      try (Socket s = new Socket(hostName, port); OutputStream outs = s.getOutputStream()) {
        return equalsPong(s, outs);
      }
    } else {
      try (AFUNIXSocket s = AFUNIXSocket.newInstance()) {
        s.connect(AFUNIXSocketAddress.of(unixSocket));
        try(OutputStream outs = s.getOutputStream()) {
          return equalsPong(s, outs);
        }
      }
    }
  }

  public boolean equalsPong(Socket s, OutputStream outs) throws IOException {
    s.setSoTimeout(timeout);
    outs.write(asBytes("zPING\0"));
    outs.flush();
    byte[] b = new byte[PONG_REPLY_LEN];
    try (InputStream inputStream = s.getInputStream()) {
      int copyIndex = 0;
      int readResult;
      do {
        readResult = inputStream.read(b, copyIndex, Math.max(b.length - copyIndex, 0));
        copyIndex += readResult;
      } while (readResult > 0);
      return Arrays.equals(b, asBytes("PONG"));
    }
  }

  /**
   * Streams the given data to the server in chunks. The whole data is not kept in memory.
   * This method is preferred if you don't want to keep the data in memory, for instance by scanning a file on disk.
   * Since the parameter InputStream is not reset, you can not use the stream afterwards, as it will be left in a EOF-state.
   * If your goal is to scan some data, and then pass that data further, consider using {@link #scan(byte[]) scan(byte[] in)}.
   * 

* 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 * @throws IOException when io error occurs */ public byte[] scan(InputStream is) throws IOException { if(hostName != null) { // connects using tcp socket try (Socket s = new Socket(hostName, port); OutputStream outs = new BufferedOutputStream(s.getOutputStream())) { return scanResult(is, s, outs); } } else { // connects using unix socket try (AFUNIXSocket s = AFUNIXSocket.newInstance()) { s.connect(AFUNIXSocketAddress.of(unixSocket)); try(OutputStream outs = new BufferedOutputStream(s.getOutputStream())) { return scanResult(is, s, outs); } } } } public byte[] scanResult(InputStream is, Socket s, OutputStream outs) throws IOException { s.setSoTimeout(timeout); // handshake outs.write(asBytes("zINSTREAM\0")); outs.flush(); byte[] chunk = new byte[CHUNK_SIZE]; try (InputStream clamIs = s.getInputStream()) { // send data int read = is.read(chunk); while (read >= 0) { // The format of the chunk is: '' where is the size of the following data in bytes expressed as a 4 byte unsigned // integer in network byte order and 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); if (clamIs.available() > 0) { // reply from server before scan command has been terminated. byte[] reply = assertSizeLimit(readAll(clamIs)); throw new IOException("Scan aborted. Reply from server: " + new String(reply, StandardCharsets.US_ASCII)); } read = is.read(chunk); } // terminate scan outs.write(new byte[]{0,0,0,0}); outs.flush(); // read reply return assertSizeLimit(readAll(clamIs)); } } /** * Scans bytes for virus by passing the bytes to clamav * @param in data to scan * @return server reply * @throws IOException when io error occurs **/ public byte[] scan(byte[] in) throws IOException { ByteArrayInputStream bis = new ByteArrayInputStream(in); return scan(bis); } /** * Interpret the result from a ClamAV scan, and determine if the result means the data is clean * * @param reply The reply from the server after scanning * @return true if no virus was found according to the clamd reply message */ public static boolean isCleanReply(byte[] reply) { String r = new String(reply, StandardCharsets.US_ASCII); return (r.contains("OK") && !r.contains("FOUND")); } 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); return reply; } // byte conversion based on ASCII character set regardless of the current system locale private static byte[] asBytes(String s) { return s.getBytes(StandardCharsets.US_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; do { read = is.read(buf); tmp.write(buf, 0, read); } while ((read > 0) && (is.available() > 0)); return tmp.toByteArray(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy