/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.ctask.general;
// above package assignment temporary pending better aysnch release process
// package org.dspace.ctask.integrity;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Bitstream;
import org.dspace.content.Bundle;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.ConfigurationManager;
import org.dspace.curate.AbstractCurationTask;
import org.dspace.curate.Curator;
import org.dspace.curate.Suspendable;
/** ClamScan.java
*
* A set of methods to scan using the
* clamav daemon.
*
* TODO: add a check for the inputstream size limit
*
* @author wbossons
*/
@Suspendable(invoked= Curator.Invoked.INTERACTIVE)
public class ClamScan extends AbstractCurationTask
{
private static final int DEFAULT_CHUNK_SIZE = 4096;//2048
private static final byte[] INSTREAM = "zINSTREAM\0".getBytes();
private static final byte[] PING = "zPING\0".getBytes();
private static final byte[] STATS = "nSTATS\n".getBytes();//prefix with z
private static final byte[] IDSESSION = "zIDSESSION\0".getBytes();
private static final byte[] END = "zEND\0".getBytes();
private static final String PLUGIN_PREFIX = "clamav";
private static final String INFECTED_MESSAGE = "had virus detected.";
private static final String CLEAN_MESSAGE = "had no viruses detected.";
private static final String CONNECT_FAIL_MESSAGE = "Unable to connect to virus service - check setup";
private static final String SCAN_FAIL_MESSAGE = "Error encountered using virus service - check setup";
private static final String NEW_ITEM_HANDLE = "in workflow";
private static Logger log = Logger.getLogger(ClamScan.class);
private static String host = null;
private static int port = 0;
private static int timeout = 0;
private static boolean failfast = true;
private int status = Curator.CURATE_UNSET;
private List<String> results = null;
private Socket socket = null;
private DataOutputStream dataOutputStream = null;
@Override
public void init(Curator curator, String taskId) throws IOException
{
super.init(curator, taskId);
host = ConfigurationManager.getProperty(PLUGIN_PREFIX, "service.host");
port = ConfigurationManager.getIntProperty(PLUGIN_PREFIX, "service.port");
timeout = ConfigurationManager.getIntProperty(PLUGIN_PREFIX, "socket.timeout");
failfast = ConfigurationManager.getBooleanProperty(PLUGIN_PREFIX, "scan.failfast");
}
@Override
public int perform(DSpaceObject dso) throws IOException
{
status = Curator.CURATE_SKIP;
logDebugMessage("The target dso is " + dso.getName());
if (dso instanceof Item)
{
status = Curator.CURATE_SUCCESS;
Item item = (Item)dso;
try
{
openSession();
}
catch (IOException ioE)
{
// no point going further - set result and error out
closeSession();
setResult(CONNECT_FAIL_MESSAGE);
return Curator.CURATE_ERROR;
}
try
{
Bundle bundle = item.getBundles("ORIGINAL")[0];
results = new ArrayList<String>();
for (Bitstream bitstream : bundle.getBitstreams())
{
InputStream inputstream = bitstream.retrieve();
logDebugMessage("Scanning " + bitstream.getName() + " . . . ");
int bstatus = scan(bitstream, inputstream, getItemHandle(item));
inputstream.close();
if (bstatus == Curator.CURATE_ERROR)
{
// no point going further - set result and error out
setResult(SCAN_FAIL_MESSAGE);
status = bstatus;
break;
}
if (failfast && bstatus == Curator.CURATE_FAIL)
{
status = bstatus;
break;
}
else if (bstatus == Curator.CURATE_FAIL &&
status == Curator.CURATE_SUCCESS)
{
status = bstatus;
}
}
}
catch (AuthorizeException authE)
{
throw new IOException(authE.getMessage(), authE);
}
catch (SQLException sqlE)
{
throw new IOException(sqlE.getMessage(), sqlE);
}
finally
{
closeSession();
}
if (status != Curator.CURATE_ERROR)
{
formatResults(item);
}
}
return status;
}
/** openSession
*
* This method opens a session.
*/
private void openSession() throws IOException
{
socket = new Socket();
try
{
logDebugMessage("Connecting to " + host + ":" + port);
socket.connect(new InetSocketAddress(host, port));
}
catch (IOException e)
{
log.error("Failed to connect to clamd . . .", e);
throw (e);
}
try
{
socket.setSoTimeout(timeout);
}
catch (SocketException e)
{
log.error("Could not set socket timeout . . . " + timeout + "ms", e);
throw (new IOException(e));
}
try
{
dataOutputStream = new DataOutputStream(socket.getOutputStream());
}
catch (IOException e)
{
log.error("Failed to open OutputStream . . . ", e);
throw (e);
}
try
{
dataOutputStream.write(IDSESSION);
}
catch (IOException e)
{
log.error("Error initiating session with IDSESSION command . . . ", e);
throw (e);
}
}
/** closeSession
*
* Close the IDSESSION in CLAMD
*
*
*/
private void closeSession()
{
if (dataOutputStream != null)
{
try
{
dataOutputStream.write(END);
}
catch (IOException e)
{
log.error("Exception closing dataOutputStream", e);
}
}
try
{
logDebugMessage("Closing the socket for ClamAv daemon . . . ");
socket.close();
}
catch (IOException e)
{
log.error("Exception closing socket", e);
}
}
/** scan
*
* Issue the INSTREAM command and return the response to
* and from the clamav daemon
*
* @param the bitstream for reporting results
* @param the InputStream to read
* @param the item handle for reporting results
* @return a ScanResult representing the server response
* @throws IOException
*/
final static byte[] buffer = new byte[DEFAULT_CHUNK_SIZE];;
private int scan(Bitstream bitstream, InputStream inputstream, String itemHandle)
{
try
{
dataOutputStream.write(INSTREAM);
}
catch (IOException e)
{
log.error("Error writing INSTREAM command . . .");
return Curator.CURATE_ERROR;
}
int read = DEFAULT_CHUNK_SIZE;
while (read == DEFAULT_CHUNK_SIZE)
{
try
{
read = inputstream.read(buffer);
}
catch (IOException e)
{
log.error("Failed attempting to read the InputStream . . . ");
return Curator.CURATE_ERROR;
}
if (read == -1)
{
break;
}
try
{
dataOutputStream.writeInt(read);
dataOutputStream.write(buffer, 0, read);
}
catch (IOException e)
{
log.error("Could not write to the socket . . . ");
return Curator.CURATE_ERROR;
}
}
try
{
dataOutputStream.writeInt(0);
dataOutputStream.flush();
}
catch (IOException e)
{
log.error("Error writing zero-length chunk to socket") ;
return Curator.CURATE_ERROR;
}
try
{
read = socket.getInputStream().read(buffer);
}
catch (IOException e)
{
log.error( "Error reading result from socket");
return Curator.CURATE_ERROR;
}
if (read > 0)
{
String response = new String(buffer, 0, read);
logDebugMessage("Response: " + response);
if (response.indexOf("FOUND") != -1)
{
String itemMsg = "item - " + itemHandle + ": ";
String bsMsg = "bitstream - " + bitstream.getName() +
": SequenceId - " + bitstream.getSequenceID() + ": infected";
report(itemMsg + bsMsg);
results.add(bsMsg);
return Curator.CURATE_FAIL;
}
else
{
return Curator.CURATE_SUCCESS;
}
}
return Curator.CURATE_ERROR;
}
private void formatResults(Item item) throws IOException
{
StringBuilder sb = new StringBuilder();
sb.append("Item: ").append(getItemHandle(item)).append(" ");
if (status == Curator.CURATE_FAIL)
{
sb.append(INFECTED_MESSAGE);
int count = 0;
for (String scanresult : results)
{
sb.append("\n").append(scanresult).append("\n");
count++;
}
sb.append(count).append(" virus(es) found. ")
.append(" failfast: ").append(failfast);
}
else
{
sb.append(CLEAN_MESSAGE);
}
setResult(sb.toString());
}
private static String getItemHandle(Item item)
{
String handle = item.getHandle();
return (handle != null) ? handle: NEW_ITEM_HANDLE;
}
private void logDebugMessage(String message)
{
if (log.isDebugEnabled())
{
log.debug(message);
}
}
}