/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/antivirus/impl/ClamAVScanner.java $ * $Id: ClamAVScanner.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.antivirus.impl; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.ConnectException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.UnknownHostException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.antivirus.api.VirusFoundException; import org.sakaiproject.antivirus.api.VirusScanIncompleteException; import org.sakaiproject.antivirus.api.VirusScanner; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.content.api.ContentHostingService; import org.sakaiproject.content.api.ContentResource; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.exception.IdUnusedException; import org.sakaiproject.exception.PermissionException; import org.sakaiproject.exception.ServerOverloadException; import org.sakaiproject.exception.TypeException; /** * Provide virus scanning to email msgs & byte arrays using ClamAV software * <br>Creation Date: Mar 22, 2005 * * <p>Provide the following properties in your sakai.properties to config and enable the scanner:</p> * * <ul> * <li>virusScan.host=localhost * <li>virusScan.port=3310 * <li>virusScan.enabled=true * </ul> * * @author Mike DeSimone, mike.[at].rsmart.com * @author John Bush * @version $Revision: 105077 $ */ public class ClamAVScanner implements VirusScanner { private static final Log logger = LogFactory.getLog(ClamAVScanner.class); private final String STREAM_PORT_STRING = "PORT "; private final String FOUND_STRING = "FOUND"; private final String SCAN_INCOMPLETE_MSG = "Virus scan could not finish due to an internal error"; private final int DEFAULT_STREAM_BUFFER_SIZE = 8192; private int port = 3310; private String host = "localhost"; /** * true if virus scanning is enabled (i.e., we should scan the data) */ private boolean enabled = false; private ServerConfigurationService serverConfigurationService; public void setServerConfigurationService( ServerConfigurationService serverConfigurationService) { this.serverConfigurationService = serverConfigurationService; } public void init(){ logger.info("init()"); port = serverConfigurationService.getInt("virusScan.port", 3310); host = serverConfigurationService.getString("virusScan.host", "localhost"); enabled = serverConfigurationService.getBoolean("virusScan.enabled", false); } public void scan(byte[] bytes) throws VirusScanIncompleteException, VirusFoundException { if(!enabled) { logger.debug("Virus scanning not enabled. Skipping scan"); return; } if(bytes != null) { doScan(bytes); } } public void scan(InputStream inputStream) throws VirusFoundException, VirusScanIncompleteException { if(!enabled) { logger.debug("Virus scanning not enabled. Skipping scan"); return; } doScan(inputStream); } protected void doScan(InputStream in) throws VirusScanIncompleteException, VirusFoundException { logger.debug("doingScan!"); Socket socket = null; String virus = null; long start = System.currentTimeMillis(); //this could be a null or zero lenght stream if (in == null) { return; } try { socket = getClamdSocket(); } catch (UnknownHostException e) { logger.error("could not connect to host for virus check: " + e); throw new VirusScanIncompleteException(SCAN_INCOMPLETE_MSG); } if(socket == null || !socket.isConnected()) { logger.warn("scan is inclomplete!"); throw new VirusScanIncompleteException(SCAN_INCOMPLETE_MSG); } BufferedReader reader = null; PrintWriter writer = null; Socket streamSocket = null; boolean virusFound = false; try { // prepare the reader and writer for the commands boolean autoFlush = true; reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), "ASCII")); writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), autoFlush); // write a request for a port to use for streaming out the data to scan writer.println("STREAM"); // parse and get the "stream" port# int streamPort = getStreamPortFromAnswer(reader.readLine()); // get the "stream" socket and the related (buffered) output stream streamSocket = new Socket(socket.getInetAddress(), streamPort); OutputStream os = streamSocket.getOutputStream(); // stream out the message to the scanner int data; // -1 signals the end of the data stream while((data = in.read()) > -1) { os.write(data); } os.flush(); os.close(); streamSocket.close(); String logMessage = ""; String answer = null; for(int i=0; i < 100; ++i) { answer = reader.readLine(); if(answer != null) { answer = answer.trim(); // if a virus is found the answer will be '... FOUND' if(answer.substring(answer.length() - FOUND_STRING.length()).equals(FOUND_STRING)) { virusFound = true; logMessage = answer + " (by virus scanner)"; //virus = answer.substring(answer.indexOf(":" + 1)); virus = answer.substring(0, answer.indexOf(FOUND_STRING)).trim(); logger.debug(logMessage); } else { logger.debug("no virus found: " + answer); } } else { break; } } long finish = System.currentTimeMillis(); logger.debug("Content scanned in " + (finish - start)); } catch (UnsupportedEncodingException e) { logger.error("Exception caught calling CLAMD on " + socket.getInetAddress() + ": " + e.getMessage()); throw new VirusScanIncompleteException(SCAN_INCOMPLETE_MSG, e); } catch (IOException e) { //we expect a connection reset if we tried to send too much data to clamd if ("Connection reset".equals(e.getMessage())) { logger.warn("Clamd reset the connection maybe due to the file being too large"); return; } logger.error("Exception caught calling CLAMD on " + socket.getInetAddress() + ": " + e.getMessage()); throw new VirusScanIncompleteException(SCAN_INCOMPLETE_MSG, e); } finally { if(reader != null) { try { reader.close(); } catch (IOException e) { } } if(writer != null) { writer.close(); } if(streamSocket != null) { try { streamSocket.close(); } catch (IOException e) { } } if(socket != null) { try { socket.close(); } catch (IOException e) { } } } if(virusFound) { logger.info("Virus detected!: " + virus); throw new VirusFoundException(virus); } } protected void doScan(byte[] bytesIn) throws VirusScanIncompleteException, VirusFoundException { if (bytesIn == null) { return; } doScan(new ByteArrayInputStream(bytesIn)); } protected Socket getClamdSocket() throws UnknownHostException { InetAddress address = InetAddress.getByName(getHost()); Socket socket = null; try { socket = new Socket(); SocketAddress socketAddress = new InetSocketAddress(address, getPort()); socket.connect(socketAddress, 5000); if(!socket.isConnected()) { return null; } return socket; } catch (IOException ioe) { logger.error("Exception caught acquiring main socket to CLAMD on " + address + " on port " + getPort() + ": " + ioe.getMessage()); } return socket; } public void scanContent(String resourceReference) throws VirusFoundException, VirusScanIncompleteException { logger.debug("scanContent(" + resourceReference + ")"); if (contentHostingService.isCollection(resourceReference)) { logger.debug("this is a folder no need to scan"); return; } try { ContentResource resource = contentHostingService.getResource(resourceReference); if (resource.getContentLength() > 0) { scan(resource.streamContent()); } } catch (PermissionException e) { logger.warn("no permission to read: " + resourceReference); if (logger.isDebugEnabled()) { logger.warn("PermissionException", e); } } catch (IdUnusedException e) { logger.warn("no such resource: " + resourceReference); } catch (TypeException e) { logger.warn("TypeException: " + resourceReference); if (logger.isDebugEnabled()) { logger.warn("TypeException", e); } } catch (ServerOverloadException e) { logger.warn("ServerOverloadException: " + resourceReference); if (logger.isDebugEnabled()) { logger.warn("ServerOverloadException", e); } } catch (VirusFoundException e) { //we should log an event for this is we likely have CHS events before and after this eventTrackingService.post(eventTrackingService.newEvent("antivirus.virusfound", contentHostingService.getReference(resourceReference), false)); throw e; } } protected int getStreamPortFromAnswer(String answer) throws ConnectException { int port = -1; if(answer != null && answer.startsWith(STREAM_PORT_STRING)) { try { port = Integer.parseInt(answer.substring(STREAM_PORT_STRING.length())); } catch (NumberFormatException nfe) { } } if(port <= 0) { throw new ConnectException("\"PORT nn\" expected - unable to parse: " + "\"" + answer + "\""); } return port; } public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public boolean getEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } private ContentHostingService contentHostingService; public void setContentHostingService(ContentHostingService contentHostingService) { this.contentHostingService = contentHostingService; } private EventTrackingService eventTrackingService; public void setEventTrackingService(EventTrackingService eventTrackingService) { this.eventTrackingService = eventTrackingService; } }