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);
}
}