package com.limegroup.gnutella.malware; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import org.limewire.concurrent.ExecutorsHelper; import org.limewire.concurrent.ThreadPoolListeningExecutor; import org.limewire.core.api.malware.VirusEngine; import org.limewire.core.settings.DownloadSettings; import org.limewire.inject.EagerSingleton; import org.limewire.lifecycle.Service; import org.limewire.lifecycle.ServiceRegistry; import org.limewire.logging.Log; import org.limewire.logging.LogFactory; import org.limewire.setting.IntSetting; import org.limewire.setting.PropertiesSetting; import org.limewire.util.FileUtils; import org.limewire.util.StringUtils; import org.limewire.util.SystemUtils; import com.google.inject.Inject; import com.jacob.activeX.ActiveXComponent; import com.jacob.com.CLSCTX; import com.jacob.com.ComFailException; import com.jacob.com.ComThread; import com.jacob.com.Dispatch; import com.jacob.com.Variant; import com.sun.jna.Native; import com.sun.jna.ptr.IntByReference; import com.sun.jna.ptr.PointerByReference; import com.sun.jna.win32.StdCallLibrary; import com.sun.jna.win32.W32APIFunctionMapper; import com.sun.jna.win32.W32APITypeMapper; @EagerSingleton // Register eagerly to guarantee it stops, but initialize lazily class VirusScannerImpl implements VirusScanner, Service { private static final Log LOG = LogFactory.getLog(VirusScannerImpl.class); private final VirusDefinitionManager virusDefinitionManager; private final ExecutorService queue; private volatile int definitionsVersion = -1; // These must only be accessed from the queue's thread private boolean comInitialized = false; private Dispatch avg = null; private boolean vdbLoaded = false; private volatile boolean loadFailed = false; private AntivirusSupportConfiguration supportConfiguration; /** The thread that is running the timer. */ private volatile Thread comThread; private volatile Boolean canLoadAvg = null; @Inject VirusScannerImpl(VirusDefinitionManager virusDefinitionManager, AntivirusSupportConfiguration supportConfiguration) { LOG.debug("Creating VirusScannerImpl"); this.virusDefinitionManager = virusDefinitionManager; this.supportConfiguration = supportConfiguration; ThreadPoolListeningExecutor executor = ExecutorsHelper.newSingleThreadExecutor(ExecutorsHelper.daemonThreadFactory("VirusScannerThread")); executor.allowCoreThreadTimeOut(false); // All COM calls must happen on the same thread queue = ExecutorsHelper.unconfigurableExecutorService(executor); queue.execute(new Runnable() { @Override public void run() { comThread = Thread.currentThread(); } }); } @Inject public void register(ServiceRegistry serviceRegistry) { serviceRegistry.register(this); } @Override public void initialize() { // Initialize lazily } @Override public void start() { // Initialize lazily } @Override public String getServiceName() { return "AntiVirusService"; } private <T> T runAndGet(Callable<T> callable) throws InterruptedException, ExecutionException { // If the current thread is the COM thread, submitting & getting will deadlock, // so we must run it directly & pretend it was submitted (by wrapping exceptions). if(Thread.currentThread() == comThread) { try { return callable.call(); } catch(InterruptedException ie) { throw ie; } catch(Throwable e) { throw new ExecutionException(e); } } else { return queue.submit(callable).get(); } } @Override public void stop() { try { runAndGet(new StopCommand()); } catch(ExecutionException e) { LOG.debug("Failed to stop", e); } catch(InterruptedException e) { LOG.debug("Failed to stop", e); } } /** * @return true if virus scanning *should* be supported on this platform. * Use isSupported to determine if AVG actually is supported. */ private boolean shouldBeSupported() { synchronized(this) { if(!supportConfiguration.isAVGCompatibleWindows()) { LOG.debug("Not supported: incompatible OS"); return false; } else { LOG.debug("Supported: AVG Module Activated"); return true; } } } @Override public boolean isSupported() { LOG.debug("entered isSupported();"); boolean canSupport = false; boolean canLoad = false; if (shouldBeSupported()) { if (supportConfiguration.isTemporaryDirectoryInUse()) { LOG.debug("Not supported: temporary settings directory"); // Add other else ifs here... } else { canSupport = true; } if (canSupport) { canLoad = isAvgLoadable(); } } if (LOG.isDebugEnabled()) LOG.debug("shouldBeSupported(); canSupport <" + canSupport + "> canLoad <" + canLoad + ">"); return canSupport && canLoad; } private boolean isAvgLoadable(){ synchronized (this) { if (canLoadAvg == null) { //null means we haven't checked yet try { canLoadAvg = loadAvgInComThread(); } catch (VirusScanException e) { // VirusScanException means that we can't load AVG canLoadAvg = false; } } else { LOG.debug("Already checked, can't load AVG"); } } LOG.debug("Can load AVG: " + canLoadAvg); return canLoadAvg; } @Override public boolean isEnabled() { if (LOG.isDebugEnabled()) LOG.debug("entered isEnabled(); shouldBeSupported() <" + shouldBeSupported() + "> supportConfiguration.isVirusScannerEnabledInSettings() <" + supportConfiguration.isVirusScannerEnabledInSettings() + ">"); return shouldBeSupported() && supportConfiguration.isVirusScannerEnabledInSettings(); } private String readLicense() throws IOException { // reading avg license file File licenseFile = VirusUtils.getLicenseFile(); if(!licenseFile.exists()) throw new IOException("License file not found"); byte[] license = FileUtils.readFileFully(licenseFile); if(license == null || license.length == 0) throw new IOException("Error reading license"); return new String(license); } /** Loads AVG if it needs to, returning true if AVG is loaded afterwards. */ private boolean loadAvgInComThread() throws VirusScanException { if(loadFailed) { throw new VirusScanException("failed to load AVG last time we tried, aborting early."); } try { return runAndGet(new Callable<Boolean>() { @Override public Boolean call() throws Exception { if(avg == null) { loadAvgDirectly(); } return avg != null; } }); } catch (InterruptedException e) { throw new VirusScanException(e); } catch (ExecutionException e) { if(e.getCause() instanceof VirusScanException) { throw (VirusScanException)e.getCause(); } else { throw new VirusScanException(e); } } } /** Loads AVG. */ private void loadAvgDirectly() throws VirusScanException { LOG.debug("Loading AVG SDK..."); // First load the COM component try { ComThread.InitMTA(); comInitialized = true; avg = new ActiveXComponent("AvgSdkCom.AvgSdk", CLSCTX.CLSCTX_LOCAL_SERVER).getObject(); Dispatch.call(avg, "InitializeLicense", readLicense()); LOG.debug("...Loaded AVG"); } catch(ComFailException e) { uninitialize(); int hr = e.getHResult(); LOG.debugf(e, "Failed to initialize: {0}", hr); throw new VirusScanException("hr: " + hr, e); } catch(IOException e) { uninitialize(); LOG.debugf(e, "Error reading license"); throw new VirusScanException(e); } finally { loadFailed = (avg == null); } } private void initializeLazily() throws VirusScanException { // Load AVG first, if it wasn't already loaded. if(avg == null) { loadAvgDirectly(); } // Then load the VDB if we need to. if(!vdbLoaded) { LOG.debug("Loading VDBs..."); // Then try to load definitions -- // if definitions can't load, throw an exception relating // to the fact that definitions aren't loaded yet. getDefinitionsVersion(); if(definitionsVersion == 0) { throw new VirusScanException("No virus definitions", VirusEngine.HintReason.NO_DEFINITIONS); } else { File db = VirusUtils.getDatabaseDirectory(); try { Dispatch.call(avg, "LoadVdbFiles", db.getAbsolutePath()); vdbLoaded = true; LOG.debug("... Loaded VDBs"); } catch(ComFailException e) { uninitialize(); // Wipe out the definitions and get a new set FileUtils.forceDeleteRecursive(db); definitionsVersion = 0; virusDefinitionManager.checkForDefinitions(); int hr = e.getHResult(); LOG.debugf(e, "Failed to load definitions: {0}", hr); throw new VirusScanException("hr: " + hr, e); } } } } @Override public int getDefinitionsVersion() { if(definitionsVersion == -1) { File db = VirusUtils.getDatabaseDirectory(); File nfo = new File(db, "version.nfo"); try { String version = VirusUtils.getNfoValue(nfo, "VDB_RELEASE_VERSION"); definitionsVersion = Integer.parseInt(version); } catch(FileNotFoundException e) { LOG.debug("Cannot find version.nfo"); definitionsVersion = 0; } catch(IOException e) { LOG.debug("Cannot find VBD_RELEASE_VERSION"); definitionsVersion = 0; } catch(NumberFormatException e) { LOG.debug("Cannot parse VDB_RELEASE_VERSION"); definitionsVersion = 0; } } LOG.debugf("Virus definitions version {0}", definitionsVersion); return definitionsVersion; } @Override public long getLibraryBuildVersion() throws IOException { try { Long v = runAndGet(new GetLibraryVersionCommand()); if(v == null) { return 0; } else { return v; } } catch(ExecutionException e) { if(e.getCause() instanceof IOException) { throw (IOException)e.getCause(); } else { throw new IOException(e); } } catch(InterruptedException e) { throw new IOException(e); } } @Override public boolean isInfected(File file) throws VirusScanException { if(!isSupported()) { throw new VirusScanException("Not supported!", VirusEngine.HintReason.NOT_SUPPORTED); } try { boolean result = runAndGet(new ScanCommand(file)); return result; } catch(ExecutionException e) { DownloadSettings.NUM_SCANS_FAILED.set(DownloadSettings.NUM_SCANS_FAILED.get() + 1); if(e.getCause() instanceof VirusScanException) { throw (VirusScanException)e.getCause(); } else { throw new VirusScanException(e); } } catch(InterruptedException e) { DownloadSettings.NUM_SCANS_FAILED.set(DownloadSettings.NUM_SCANS_FAILED.get() + 1); throw new VirusScanException(e); } } private void updateStats(File file, boolean infected) { IntSetting numScannedInfected = DownloadSettings.NUM_SCANNED_INFECTED; IntSetting numScannedClean = DownloadSettings.NUM_SCANNED_CLEAN; PropertiesSetting infectedExtensions = DownloadSettings.INFECTED_EXTENSIONS; if(infected) { numScannedInfected.set(numScannedInfected.get() + 1); String ext = FileUtils.getFileExtension(file); Properties props = infectedExtensions.get(); String inf = props.getProperty(ext); Integer numInfectedForExtension = inf != null ? Integer.valueOf(inf) : 0; numInfectedForExtension++; props.put(ext, numInfectedForExtension.toString()); infectedExtensions.set(props); } else { numScannedClean.set(numScannedClean.get() + 1); } } @Override public void stopScanner() { stop(); } @Override public void loadFullUpdate(File updateDir) throws VirusScanException { if(!isSupported()) { throw new VirusScanException("not supported", VirusEngine.HintReason.NOT_SUPPORTED); } try { runAndGet(new LoadFullUpdateCommand(updateDir)); } catch(ExecutionException e) { DownloadSettings.NUM_AV_FULL_UPDATES_FAILED.set(DownloadSettings.NUM_AV_FULL_UPDATES_FAILED.get() + 1); if(e.getCause() instanceof VirusScanException) { throw (VirusScanException)e.getCause(); } else { throw new VirusScanException(e); } } catch(InterruptedException e) { DownloadSettings.NUM_AV_FULL_UPDATES_FAILED.set(DownloadSettings.NUM_AV_FULL_UPDATES_FAILED.get() + 1); throw new VirusScanException(e); } DownloadSettings.NUM_AV_FULL_UPDATES_SUCCEEDED.set(DownloadSettings.NUM_AV_FULL_UPDATES_SUCCEEDED.get() + 1); } @Override public void loadIncrementalUpdate(File updateDir) throws IOException, VirusScanException { if(!isSupported()) { throw new VirusScanException("not supported", VirusEngine.HintReason.NOT_SUPPORTED); } try { runAndGet(new LoadIncrementalUpdateCommand(updateDir)); } catch(ExecutionException e) { DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_FAILED.set(DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_FAILED.get() + 1); if(e.getCause() instanceof VirusScanException) { throw (VirusScanException)e.getCause(); } else if(e.getCause() instanceof IOException) { throw (IOException)e.getCause(); } else { throw new VirusScanException(e); } } catch(InterruptedException e) { DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_FAILED.set(DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_FAILED.get() + 1); throw new VirusScanException(e); } DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_SUCCEEDED.set(DownloadSettings.NUM_AV_INCREMENTAL_UPDATES_SUCCEEDED.get() + 1); } private void uninitialize() { if(avg != null) { LOG.debug("Releasing AvgSdk instance"); avg.safeRelease(); avg = null; } vdbLoaded = false; if(comInitialized) { try { LOG.debug("Uninitializing COM"); ComThread.Release(); } catch(ComFailException e) { int hr = e.getHResult(); LOG.debugf(e, "Failed to uninitialize COM: {0}", hr); } comInitialized = false; } definitionsVersion = -1; } private class ScanCommand implements Callable<Boolean> { private final File file; ScanCommand(File file) { this.file = file; } /** * Returns true if the file is infected or false if it's clean. * @throws VirusScanException if the file cannot be scanned. */ @Override public Boolean call() throws VirusScanException { if(!file.exists()) return false; initializeLazily(); try { Variant resultVar = new Variant(); Dispatch params = new ActiveXComponent("AvgSdkCom.AvgScanParameters", CLSCTX.CLSCTX_LOCAL_SERVER).getObject(); String path = file.getAbsolutePath(); Variant infected; if(file.isDirectory()) { LOG.debugf("Scanning directory: {0}", path); infected = Dispatch.call(avg, "ScanDirectory", path, resultVar, params); } else { LOG.debugf("Scanning file: {0}", path); infected = Dispatch.call(avg, "ScanFile", path, path, resultVar, params); } params.safeRelease(); boolean isInfected = infected.getBoolean(); LOG.debugf("Infected: {0}", infected); updateStats(file, isInfected); return isInfected; } catch(ComFailException e) { int hr = e.getHResult(); LOG.debugf(e, "Failed to scan {0}: {1}", file, hr); throw new VirusScanException("hr: " + hr, e); } } } private class StopCommand implements Callable<Void> { @Override public Void call() { uninitialize(); return null; } } private class LoadIncrementalUpdateCommand implements Callable<Void> { private final File updateDir; LoadIncrementalUpdateCommand(File updateDir) { this.updateDir = updateDir; } @Override public Void call() throws IOException, VirusScanException { LOG.debugf("Loading incremental update from {0}", updateDir); definitionsVersion = -1; try { initializeLazily(); } catch(VirusScanException e) { // Convert to IOException because the old definitions are intact throw new IOException(e); } // Find the patch file in the update directory File patch = null; File[] files = updateDir.listFiles(); if(files == null) throw new IOException("Update directory is empty"); for(File file : files) { if(file.getName().endsWith(".trs")) { patch = file; break; } } if(patch == null) throw new IOException("Could not find patch file"); // Find the source file for the merge in the database directory File db = VirusUtils.getDatabaseDirectory(); File source = new File(db, "incavi.avm"); if(!source.exists()) throw new IOException("Could not find source file"); // The destination file will be created in the temporary directory File temp = VirusUtils.getTemporaryDirectory(); temp.mkdirs(); File destination = new File(temp, "incavi.avm"); try { Dispatch avgVDBUpdate = new ActiveXComponent("AvgSdkCom.AvgVdbUpd", CLSCTX.CLSCTX_LOCAL_SERVER).getObject(); Dispatch.call(avgVDBUpdate, "UpdateIncrementalVdbFile", source.getAbsolutePath(), destination.getAbsolutePath(), 1, patch.getAbsolutePath(), new Variant()); avgVDBUpdate.safeRelease(); } catch(ComFailException e) { int hr = e.getHResult(); LOG.debugf(e, "Failed to merge incremental update: {0}", hr); throw new IOException("hr: " + hr, e); } // Move the new incavi.avm into the database directory definitionsVersion = 0; source.delete(); if(!destination.renameTo(source)) throw new VirusScanException("Failed to rename incavi.avm"); // Move the new version.nfo into the database directory File newVersion = new File(updateDir, "version.nfo"); File oldVersion = new File(db, "version.nfo"); oldVersion.delete(); if(!newVersion.renameTo(oldVersion)) throw new VirusScanException("Failed to rename version.nfo"); // Load the new definitions try { Dispatch.call(avg, "ReloadVdbFiles"); } catch(ComFailException e) { int hr = e.getHResult(); LOG.debugf(e, "Failed to reload definitions: {0}", hr); throw new VirusScanException("hr: " + hr, e); } // Determine the new version definitionsVersion = -1; getDefinitionsVersion(); return null; } } private class LoadFullUpdateCommand implements Callable<Void> { private final File updateDir; LoadFullUpdateCommand(File updateDir) { this.updateDir = updateDir; } @Override public Void call() throws VirusScanException { LOG.debugf("Loading full update from {0}", updateDir); // Shut down the scanner uninitialize(); // Wipe out the old definitions (if any) definitionsVersion = 0; File db = VirusUtils.getDatabaseDirectory(); FileUtils.forceDeleteRecursive(db); db.mkdirs(); db.mkdir(); // Move the update files into the database directory File[] files = updateDir.listFiles(); if(files == null) throw new VirusScanException("Update directory is empty"); for(File file : files) { if(!file.renameTo(new File(db, file.getName()))) throw new VirusScanException("Failed to rename file"); } // Determine the new version definitionsVersion = -1; getDefinitionsVersion(); return null; } } private static class GetLibraryVersionCommand implements Callable<Long> { @Override public Long call() throws IOException { LOG.debug("Getting library version."); String clsid = SystemUtils.registryReadText("HKEY_CLASSES_ROOT", "AvgSdkCom.AvgSdk\\CLSID", ""); if(StringUtils.isEmpty(clsid)) { throw new IOException("unable to get clsid (registry not setup?)"); } String sdkPath = SystemUtils.registryReadText("HKEY_CLASSES_ROOT", "CLSID\\" + clsid + "\\InprocServer32", ""); if(StringUtils.isEmpty(sdkPath)) { throw new IOException("unable to get path (registry not setup right?)"); } File avgSdkCom = new File(sdkPath).getAbsoluteFile(); if(avgSdkCom.getParentFile() == null) { throw new IOException("No parent path for avgSdkCom [" + avgSdkCom + "]"); } File avgCoreEx = new File(avgSdkCom.getParentFile(), "avgcorex.dll"); String corePath = avgCoreEx.getAbsolutePath(); VersionDll v = VersionDll.INSTANCE; int size = v.GetFileVersionInfoSize(corePath, new IntByReference()); if(size == 0) { throw new IOException("unable to get size data"); } Buffer buffer = ByteBuffer.allocateDirect(size); boolean result = v.GetFileVersionInfo(corePath, 0, size, buffer); if(!result) { throw new IOException("unable to get version info"); } PointerByReference versionData = new PointerByReference(); IntByReference vdLen = new IntByReference(); result = v.VerQueryValue(buffer, "\\", versionData, vdLen); if(!result) { throw new IOException("Unable to get value out of version info"); } VersionDll.VS_FIXEDFILEINFO info = new VersionDll.VS_FIXEDFILEINFO(versionData.getValue()); long v1 = (short)(info.dwFileVersionMS >> 16); // long v2 = (short)(info.dwFileVersionMS &0xffff); // long v3 = (short)(info.dwFileVersionLS >>16); long v4 = (short)(info.dwFileVersionLS &0xffff); return v1 * 1000 + v4; } } private static interface VersionDll extends StdCallLibrary { public class VS_FIXEDFILEINFO extends com.sun.jna.Structure { @SuppressWarnings("unused") public int dwSignature; @SuppressWarnings("unused") public int dwStrucVersion; public int dwFileVersionMS; public int dwFileVersionLS; @SuppressWarnings("unused") public int dwProductVersionMS; @SuppressWarnings("unused") public int dwProductVersionLS; @SuppressWarnings("unused") public int dwFileFlagsMask; @SuppressWarnings("unused") public int dwFileFlags; @SuppressWarnings("unused") public int dwFileOS; @SuppressWarnings("unused") public int dwFileType; @SuppressWarnings("unused") public int dwFileSubtype; @SuppressWarnings("unused") public int dwFileDateMS; @SuppressWarnings("unused") public int dwFileDateLS; public VS_FIXEDFILEINFO(com.sun.jna.Pointer p){ super(p); read(); } } /** Standard options to use the unicode version of a w32 API. */ @SuppressWarnings("unchecked") Map UNICODE_OPTIONS = new HashMap() { { put(OPTION_TYPE_MAPPER, W32APITypeMapper.UNICODE); put(OPTION_FUNCTION_MAPPER, W32APIFunctionMapper.UNICODE); } }; /** Standard options to use the ASCII/MBCS version of a w32 API. */ @SuppressWarnings("unchecked") Map ASCII_OPTIONS = new HashMap() { { put(OPTION_TYPE_MAPPER, W32APITypeMapper.ASCII); put(OPTION_FUNCTION_MAPPER, W32APIFunctionMapper.ASCII); } }; Map DEFAULT_OPTIONS = Boolean.getBoolean("w32.ascii") ? ASCII_OPTIONS : UNICODE_OPTIONS; VersionDll INSTANCE = (VersionDll) Native.loadLibrary("version", VersionDll.class, DEFAULT_OPTIONS); int GetFileVersionInfoSize(String lpstrFilename, IntByReference lpdwHandle); boolean GetFileVersionInfo(String lpstrFilename, int dwHandle, int dwLen, Buffer lpData); boolean VerQueryValue(Buffer pBlock, String lpSubBlock, PointerByReference lplpBuffer, IntByReference puLen); } }