package com.limegroup.gnutella.filters; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.RandomAccessFile; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.PublicKey; import java.security.Signature; import java.security.spec.EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpHead; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.impl.cookie.DateParseException; import org.apache.http.impl.cookie.DateUtils; 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.FilterSettings; import org.limewire.inject.EagerSingleton; import org.limewire.io.IOUtils; import org.limewire.lifecycle.Service; import org.limewire.lifecycle.ServiceRegistry; import org.limewire.util.Base32; import org.limewire.util.CommonUtils; import org.limewire.util.Visitor; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.name.Named; import com.limegroup.gnutella.SpamServices; import com.limegroup.gnutella.http.HTTPHeaderName; import com.limegroup.gnutella.http.HttpClientListener; import com.limegroup.gnutella.http.HttpExecutor; /** * Manages a file containing blacklisted URNs, which is updated periodically * via HTTP. The manager's <code>iterator()</code> method can be used to read * the URNs from disk as base32-encoded strings. */ @EagerSingleton class URNBlacklistManagerImpl implements URNBlacklistManager, Service { private static final Log LOG = LogFactory.getLog(URNBlacklistManagerImpl.class); private final Provider<HttpExecutor> httpExecutor; private final Provider<HttpParams> defaultParams; private final Provider<SpamServices> spamServices; private final AtomicBoolean updatedThisSession = new AtomicBoolean(false); @Inject URNBlacklistManagerImpl(Provider<HttpExecutor> httpExecutor, @Named("defaults") Provider<HttpParams> defaultParams, Provider<SpamServices> spamServices) { this.httpExecutor = httpExecutor; this.defaultParams = defaultParams; this.spamServices = spamServices; } @Inject void register(ServiceRegistry registry) { registry.register(this); } @Override public void start() { LOG.debug("Starting"); long now = System.currentTimeMillis(); if(now > FilterSettings.NEXT_URN_BLACKLIST_UPDATE.getValue()) checkForUpdate(); else LOG.debug("Too soon to check for an update"); } @Override public void initialize() { } @Override public void stop() { } @Override public String getServiceName() { return "URNBlacklistManager"; } /** * Loads and verifies the URN blacklist, then passes each successfully * loaded URN to the given visitor as a base32-encoded string. This method * blocks. */ @Override public void loadURNs(Visitor<String> visitor) { byte[] buf = new byte[20]; byte[] sig = new byte[SIG_LENGTH]; RandomAccessFile in = null; // Fail fast if the file is fundamentally fubar File file = getFile(); long length = file.length() - SIG_LENGTH; // The data excluding the signature should be a multiple of 20 bytes if(length <= 0 || length > MAX_LENGTH || length % 20 != 0) { LOG.debug("File is missing, empty, or an invalid size"); invalidFile(); return; } try { LOG.debug("Opening file"); in = new RandomAccessFile(file, "r"); // Initialise the signature verifier Signature signature = Signature.getInstance(SIG_ALGORITHM); byte[] keyBytes = Base32.decode(PUBLIC_KEY); KeyFactory factory = KeyFactory.getInstance(KEY_ALGORITHM); EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); PublicKey key = factory.generatePublic(keySpec); signature.initVerify(key); // Feed the data to the signature verifier for(long read = 0; read < length; read += buf.length) { // Unexpected EOF will throw an exception in.readFully(buf); signature.update(buf); } // Read the signature in.readFully(sig); // Verify the signature if(signature.verify(sig)) { LOG.debug("Valid signature"); } else { LOG.debug("Invalid signature"); invalidFile(); return; } // Rewind and read the file again, passing URNs to the visitor in.seek(0); for(long read = 0; read < length; read += buf.length) { // Unexpected EOF will throw an exception in.readFully(buf); if(!visitor.visit(Base32.encode(buf))) break; // The visitor's had enough } } catch(IOException e) { LOG.debug("Error loading URNs", e); invalidFile(); } catch(GeneralSecurityException e) { LOG.debug("Error verifying URNs", e); invalidFile(); } finally { IOUtils.close(in); } } private void invalidFile() { // The file is invalid - replace it with any available version FilterSettings.LAST_URN_BLACKLIST_UPDATE.setValue(0); checkForUpdate(); } /** * Returns the file where the URN blacklist should be stored. * Package access for testing. */ File getFile() { return new File(CommonUtils.getUserSettingsDir(), "urns.dat"); } /** * Selects one of the update URLs at random, checks for an updated version * of the blacklist, and downloads it if available. No more than one check * will be performed per session. */ private void checkForUpdate() { final String[] urls = FilterSettings.URN_BLACKLIST_UPDATE_URLS.get(); if(urls.length == 0) { LOG.debug("No request URLs"); // Pick a new update time, otherwise when the list of URLs is // updated everyone will hit the servers at once setNextUpdateTime(); return; } if(updatedThisSession.getAndSet(true)) { LOG.debug("Already updated this session"); return; } int random = (int)(Math.random() * urls.length); String url = urls[random]; if(LOG.isDebugEnabled()) LOG.debug("Sending request to " + url); sendRequest(new HttpHead(url)); } /** * Sends an HTTP request. */ private void sendRequest(HttpRequestBase request) { request.addHeader(HTTPHeaderName.CONNECTION.httpStringValue(), "close"); HttpParams params = new BasicHttpParams(); HttpConnectionParams.setConnectionTimeout(params, 10000); HttpConnectionParams.setSoTimeout(params, 10000); params = new DefaultedHttpParams(params, defaultParams.get()); httpExecutor.get().execute(request, params, new RequestHandler()); } /** * Updates the settings recording the last time an update check was * performed and the time of the next check. */ private void setNextUpdateTime() { long now = System.currentTimeMillis(); FilterSettings.LAST_URN_BLACKLIST_UPDATE.setValue(now); // Choose a random interval between zero and the maximum long max = FilterSettings.MAX_URN_BLACKLIST_UPDATE_INTERVAL.getValue(); long min = FilterSettings.MIN_URN_BLACKLIST_UPDATE_INTERVAL.getValue(); long next = now + Math.max(min, (long)(Math.random() * max)); if(LOG.isDebugEnabled()) LOG.debug("Setting next update time to " + next); FilterSettings.NEXT_URN_BLACKLIST_UPDATE.setValue(next); } private class RequestHandler implements HttpClientListener { @Override public boolean allowRequest(HttpUriRequest request) { return true; } @Override public boolean requestComplete(HttpUriRequest request, HttpResponse response) { String method = request.getMethod(); if("HEAD".equals(method)) { LOG.debug("HEAD request completed"); long modified = 0; Header header = response.getFirstHeader("Last-Modified"); if(header == null || header.getValue() == null) { LOG.debug("Response has no Last-Modified header"); } else { try { String date = header.getValue(); modified = DateUtils.parseDate(date).getTime(); } catch(DateParseException e) { LOG.debug("Error parsing date", e); } } long last = FilterSettings.LAST_URN_BLACKLIST_UPDATE.getValue(); // If the blacklist has been modified since the last check, // send a GET request to download the new blacklist if(modified > last) { String url = request.getURI().toString(); sendRequest(new HttpGet(url)); } else { setNextUpdateTime(); } } else if("GET".equals(method)) { LOG.debug("GET request completed"); HttpEntity body = response.getEntity(); if(body == null) { LOG.debug("Response has no body"); } else { BufferedOutputStream out = null; try { out = new BufferedOutputStream( new FileOutputStream(getFile())); body.writeTo(out); out.flush(); out.close(); spamServices.get().reloadSpamFilters(); } catch(IOException e) { LOG.debug("Error saving URNs", e); } finally { IOUtils.close(out); } } setNextUpdateTime(); } return false; // Do not attempt any further requests. } @Override public boolean requestFailed(HttpUriRequest request, HttpResponse response, IOException e) { if(LOG.isDebugEnabled()) { String method = request.getMethod(); String status = null; if(response != null) status = response.getStatusLine().toString(); LOG.debug(method + " request failed with status " + status, e); } setNextUpdateTime(); return false; // Do not attempt any further requests. } } }