package com.limegroup.gnutella.simpp; import java.io.File; import java.io.IOException; import java.net.URISyntaxException; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.DefaultedHttpParams; import org.apache.http.params.HttpConnectionParams; import org.apache.http.params.HttpParams; import org.limewire.core.settings.UpdateSettings; import org.limewire.io.IOUtils; import org.limewire.util.Clock; import org.limewire.util.CommonUtils; import org.limewire.util.FileUtils; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import com.google.inject.name.Named; import com.limegroup.gnutella.ApplicationServices; import com.limegroup.gnutella.NetworkUpdateSanityChecker; import com.limegroup.gnutella.ReplyHandler; import com.limegroup.gnutella.NetworkUpdateSanityChecker.RequestType; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HttpClientListener; import com.limegroup.gnutella.http.HttpExecutor; import com.limegroup.gnutella.settings.SimppSettingsManager; import com.limegroup.gnutella.util.LimeWireUtils; /** * Used for managing signed messages published by LimeWire, and changing settings * as necessary. */ @Singleton public class SimppManagerImpl implements SimppManager { private static final Log LOG = LogFactory.getLog(SimppManagerImpl.class); private static int MIN_VERSION = 3; private static final String FILENAME = "simpp.xml"; private static final Random RANDOM = new Random(); private static final int IGNORE_ID = Integer.MAX_VALUE; /** Cached Simpp bytes in case we need to sent it out on the wire */ private volatile byte[] _lastBytes = new byte[0]; private volatile int _lastId = MIN_VERSION; /** If an HTTP failover update is in progress */ private final HttpRequestControl httpRequestControl = new HttpRequestControl(); private final List<SimppListener> listeners = new CopyOnWriteArrayList<SimppListener>(); private final CopyOnWriteArrayList<SimppSettingsManager> simppSettingsManagers; private final Provider<NetworkUpdateSanityChecker> networkUpdateSanityChecker; private final ApplicationServices applicationServices; private final Clock clock; private final Provider<HttpExecutor> httpExecutor; private final ScheduledExecutorService backgroundExecutor; private final Provider<HttpParams> defaultParams; private final SimppDataProvider simppDataProvider; private volatile List<String> maxedUpdateList = Arrays.asList("http://simpp1.limewire.com/v2/simpp.def", "http://simpp2.limewire.com/v2/simpp.def", "http://simpp3.limewire.com/v2/simpp.def", "http://simpp4.limewire.com/v2/simpp.def", "http://simpp5.limewire.com/v2/simpp.def", "http://simpp6.limewire.com/v2/simpp.def", "http://simpp7.limewire.com/v2/simpp.def", "http://simpp8.limewire.com/v2/simpp.def", "http://simpp9.limewire.com/v2/simpp.def", "http://simpp10.limewire.com/v2/simpp.def"); private volatile int minMaxHttpRequestDelay = 1000 * 60; private volatile int maxMaxHttpRequestDelay = 1000 * 60 * 30; private volatile int silentPeriodForMaxHttpRequest = 1000 * 60 * 5; private static enum UpdateType { FROM_NETWORK, FROM_DISK, FROM_HTTP; } @Inject public SimppManagerImpl(Provider<NetworkUpdateSanityChecker> networkUpdateSanityChecker, Clock clock, ApplicationServices applicationServices, Provider<HttpExecutor> httpExecutor, @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor, @Named("defaults") Provider<HttpParams> defaultParams, SimppDataProvider simppDataProvider) { this.networkUpdateSanityChecker = networkUpdateSanityChecker; this.clock = clock; this.applicationServices = applicationServices; this.simppSettingsManagers = new CopyOnWriteArrayList<SimppSettingsManager>(); this.httpExecutor = httpExecutor; this.backgroundExecutor = backgroundExecutor; this.defaultParams = defaultParams; this.simppDataProvider = simppDataProvider; } List<String> getMaxUrls() { return maxedUpdateList; } void setMaxUrls(List<String> urls) { this.maxedUpdateList = urls; } int getMinHttpRequestUpdateDelayForMaxFailover() { return minMaxHttpRequestDelay; } int getMaxHttpRequestUpdateDelayForMaxFailover() { return maxMaxHttpRequestDelay; } void setMinHttpRequestUpdateDelayForMaxFailover(int min) { minMaxHttpRequestDelay = min; } void setMaxHttpRequestUpdateDelayForMaxFailover(int max) { maxMaxHttpRequestDelay = max; } int getSilentPeriodForMaxHttpRequest() { return silentPeriodForMaxHttpRequest; } void setSilentPeriodForMaxHttpRequest(int silentPeriodForMaxHttpRequest) { this.silentPeriodForMaxHttpRequest = silentPeriodForMaxHttpRequest; } public void initialize() { LOG.trace("Initializing SimppManager"); backgroundExecutor.execute(new Runnable() { public void run() { handleDataInternal(FileUtils.readFileFully(new File(CommonUtils .getUserSettingsDir(), FILENAME)), UpdateType.FROM_DISK, null); handleDataInternal(simppDataProvider.getDefaultData(), UpdateType.FROM_DISK, null); } }); } public int getVersion() { return _lastId; } /** * @return the cached value of the simpp bytes. */ public byte[] getSimppBytes() { return _lastBytes; } public void addSimppSettingsManager(SimppSettingsManager simppSettingsManager) { simppSettingsManagers.add(simppSettingsManager); } public List<SimppSettingsManager> getSimppSettingsManagers() { return simppSettingsManagers; } public void addListener(SimppListener listener) { listeners.add(listener); } public void removeListener(SimppListener listener) { listeners.remove(listener); } public void checkAndUpdate(final ReplyHandler handler, final byte[] data) { if(data != null) { backgroundExecutor.execute(new Runnable() { public void run() { LOG.trace("Parsing new data..."); handleDataInternal(data, UpdateType.FROM_NETWORK, handler); } }); } } private void handleDataInternal(byte[] data, UpdateType updateType, ReplyHandler handler) { if (data == null) { if (updateType == UpdateType.FROM_NETWORK && handler != null) networkUpdateSanityChecker.get().handleInvalidResponse(handler, RequestType.SIMPP); LOG.warn("No data to handle."); return; } SimppDataVerifier verifier=new SimppDataVerifier(data); if(!verifier.verifySource()) { if(updateType == UpdateType.FROM_NETWORK && handler != null) networkUpdateSanityChecker.get().handleInvalidResponse(handler, RequestType.SIMPP); LOG.warn("Couldn't verify signature on data."); return; } if(updateType == UpdateType.FROM_NETWORK && handler != null) networkUpdateSanityChecker.get().handleValidResponse(handler, RequestType.SIMPP); SimppParser parser = null; try { parser = new SimppParser(verifier.getVerifiedData()); } catch(IOException iox) { LOG.error("IOX parsing simpp data", iox); return; } if(LOG.isDebugEnabled()) { LOG.debug("Got data with version: " + parser.getVersion() + " from: " + updateType + ", current version is: " + _lastId); } switch(updateType) { case FROM_NETWORK: if(parser.getVersion() == IGNORE_ID) { if(_lastId != IGNORE_ID) doHttpMaxFailover(); } else if(parser.getVersion() > _lastId) { storeAndUpdate(data, parser, updateType); } break; case FROM_DISK: if(parser.getVersion() > _lastId) { storeAndUpdate(data, parser, updateType); } break; case FROM_HTTP: if(parser.getVersion() >= _lastId) { storeAndUpdate(data, parser, updateType); } break; } } private void storeAndUpdate(byte[] data, SimppParser parser, UpdateType updateType) { if(LOG.isTraceEnabled()) LOG.trace("Retrieved new data from: " + updateType + ", storing & updating"); if(parser.getVersion() == IGNORE_ID && updateType == UpdateType.FROM_NETWORK) throw new IllegalStateException("shouldn't be here!"); if(updateType == UpdateType.FROM_NETWORK && httpRequestControl.isRequestPending()) return; _lastId = parser.getVersion(); _lastBytes = data; if(updateType != UpdateType.FROM_DISK) { FileUtils.verySafeSave(CommonUtils.getUserSettingsDir(), FILENAME, data); } for(SimppSettingsManager ssm : simppSettingsManagers) ssm.updateSimppSettings(parser.getPropsData()); for (SimppListener listener : listeners) listener.simppUpdated(_lastId); } private void doHttpMaxFailover() { long maxTimeAgo = clock.now() - silentPeriodForMaxHttpRequest; if(!httpRequestControl.requestQueued(HttpRequestControl.RequestReason.MAX) && UpdateSettings.LAST_SIMPP_FAILOVER.getValue() < maxTimeAgo) { int rndDelay = RANDOM.nextInt(maxMaxHttpRequestDelay) + minMaxHttpRequestDelay; final String rndUri = maxedUpdateList.get(RANDOM.nextInt(maxedUpdateList.size())); LOG.debug("Scheduling http max failover in: " + rndDelay + ", to: " + rndUri); backgroundExecutor.schedule(new Runnable() { public void run() { String url = rndUri; try { launchHTTPUpdate(url); } catch (URISyntaxException e) { httpRequestControl.requestFinished(); httpRequestControl.cancelRequest(); LOG.warn("uri failure", e); } } }, rndDelay, TimeUnit.MILLISECONDS); } else { LOG.debug("Ignoring http max failover."); } } /** * Launches an http update to the failover url. */ private void launchHTTPUpdate(String url) throws URISyntaxException { if (!httpRequestControl.isRequestPending()) return; LOG.debug("about to issue http request method"); HttpGet get = new HttpGet(LimeWireUtils.addLWInfoToUrl(url, applicationServices.getMyGUID())); get.addHeader("User-Agent", LimeWireUtils.getHttpServer()); get.addHeader(HTTPHeaderName.CONNECTION.httpStringValue(),"close"); httpRequestControl.requestActive(); HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, 10000); HttpConnectionParams.setSoTimeout(params, 10000); params = new DefaultedHttpParams(params, defaultParams.get()); httpExecutor.get().execute(get, params, new RequestHandler()); } public byte[] getOldUpdateResponse() { return simppDataProvider.getOldUpdateResponse(); } private class RequestHandler implements HttpClientListener { public boolean requestComplete(HttpUriRequest request, HttpResponse response) { LOG.debug("http request method succeeded"); // remember we made an attempt even if it didn't succeed UpdateSettings.LAST_SIMPP_FAILOVER.setValue(clock.now()); final byte[] inflated; try { if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) throw new IOException("bad code " + response.getStatusLine().getStatusCode()); byte [] resp = null; if(response.getEntity() != null) { resp = IOUtils.readFully(response.getEntity().getContent()); } if (resp == null || resp.length == 0) throw new IOException("bad body"); // inflate the response and process. inflated = IOUtils.inflate(resp); } catch (IOException failed) { httpRequestControl.requestFinished(); LOG.warn("couldn't fetch data ",failed); return false; } finally { httpExecutor.get().releaseResources(response); } // Handle the data in the background thread. backgroundExecutor.execute(new Runnable() { public void run() { httpRequestControl.requestFinished(); LOG.trace("Parsing new data..."); handleDataInternal(inflated, UpdateType.FROM_HTTP, null); } }); return false; // no more requests } public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException exc) { LOG.warn("http failover failed",exc); httpRequestControl.requestFinished(); UpdateSettings.LAST_SIMPP_FAILOVER.setValue(clock.now()); httpExecutor.get().releaseResources(response); // nothing we can do. return false; } @Override public boolean allowRequest(HttpUriRequest request) { return true; } } /** * A simple control to let the flow of HTTP requests happen differently * depending on why it was requested. */ private static class HttpRequestControl { private static enum RequestReason { MAX }; private final AtomicBoolean requestQueued = new AtomicBoolean(false); private final AtomicBoolean requestActive = new AtomicBoolean(false); private volatile RequestReason requestReason; /** Returns true if a request is queued or active. */ boolean isRequestPending() { return requestActive.get() || requestQueued.get(); } /** Sets a queued request and returns true if a request is pending or active. */ boolean requestQueued(RequestReason reason) { boolean prior = requestQueued.getAndSet(true); if(!prior || reason == RequestReason.MAX) // upgrade reason requestReason = reason; return prior || requestActive.get(); } /** Sets a request to be active. */ void requestActive() { requestActive.set(true); requestQueued.set(false); } /** Returns the reason the last request was queueud. */ RequestReason getRequestReason() { return requestReason; } void cancelRequest() { requestQueued.set(false); } void requestFinished() { requestActive.set(false); } } }