package freenet.node.updater; import static java.util.concurrent.TimeUnit.SECONDS; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import freenet.client.FetchContext; import freenet.client.FetchException; import freenet.client.FetchException.FetchExceptionMode; import freenet.client.FetchResult; import freenet.client.async.BinaryBlobWriter; import freenet.client.async.ClientContext; import freenet.client.async.ClientGetCallback; import freenet.client.async.ClientGetter; import freenet.client.async.PersistenceDisabledException; import freenet.l10n.NodeL10n; import freenet.node.NodeClientCore; import freenet.node.RequestClient; import freenet.node.RequestStarter; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.MediaType; import freenet.support.api.Bucket; import freenet.support.api.RandomAccessBucket; import freenet.support.api.RandomAccessBuffer; import freenet.support.io.ArrayBucket; import freenet.support.io.BucketTools; import freenet.support.io.ByteArrayRandomAccessBuffer; import freenet.support.io.FileBucket; import freenet.support.io.FileUtil; import freenet.support.io.FileRandomAccessBuffer; /** * Fetches the revocation key. Each time it starts, it will try to fetch it until it has 3 DNFs. If it ever finds it, it will * be immediately fed to the NodeUpdateManager. */ public class RevocationChecker implements ClientGetCallback, RequestClient { public final static int REVOCATION_DNF_MIN = 3; private boolean logMINOR; private NodeUpdateManager manager; private NodeClientCore core; private int revocationDNFCounter; private FetchContext ctxRevocation; private ClientGetter revocationGetter; private boolean wasAggressive; /** Last time at which we got 3 DNFs on the revocation key */ private long lastSucceeded; // Kept separately from NodeUpdateManager.hasBeenBlown because there are local problems that can blow the key. private volatile boolean blown; private File blobFile; /** The original binary blob bucket. */ private ArrayBucket blobBucket; public RevocationChecker(NodeUpdateManager manager, File blobFile) { this.manager = manager; core = manager.node.clientCore; this.revocationDNFCounter = 0; this.blobFile = blobFile; this.logMINOR = Logger.shouldLog(LogLevel.MINOR, this); ctxRevocation = core.makeClient((short)0, true, false).getFetchContext(); // Do not allow redirects etc. // If we allow redirects then it will take too long to download the revocation. // Anyone inserting it should be aware of this fact! // You must insert with no content type, and be less than the size limit, and less than the block size after compression! // If it doesn't fit, we'll still tell the user, but the message may not be easily readable. ctxRevocation.allowSplitfiles = false; ctxRevocation.maxArchiveLevels = 0; ctxRevocation.followRedirects = false; // big enough ? ctxRevocation.maxOutputLength = NodeUpdateManager.MAX_REVOCATION_KEY_LENGTH; ctxRevocation.maxTempLength = NodeUpdateManager.MAX_REVOCATION_KEY_TEMP_LENGTH; ctxRevocation.maxSplitfileBlockRetries = -1; // if we find content, try forever to get it; not used because of the above size limits. ctxRevocation.maxNonSplitfileRetries = 0; // but return quickly normally } public int getRevocationDNFCounter() { return revocationDNFCounter; } public void start(boolean aggressive) { start(aggressive, true); if(blobFile.exists()) { ArrayBucket bucket = new ArrayBucket(); try { BucketTools.copy(new FileBucket(blobFile, true, false, false, true), bucket); // Allow to free if bogus. manager.uom.processRevocationBlob(bucket, "disk", true); } catch (IOException e) { Logger.error(this, "Failed to read old revocation blob: "+e, e); System.err.println("We may have downloaded an old revocation blob before restarting but it cannot be read: "+e); e.printStackTrace(); } } } /** Start a fetch. * @param aggressive If set to true, then we have just fetched an update, and therefore can increase the priority of the * fetch to maximum. * @return True if the checker was already running and the counter was not reset. * */ public boolean start(boolean aggressive, boolean reset) { if(manager.isBlown()) { Logger.error(this, "Not starting revocation checker: key already blown!"); return false; } boolean wasRunning = false; logMINOR = Logger.shouldLog(LogLevel.MINOR, this); ClientGetter cg = null; try { ClientGetter toCancel = null; synchronized(this) { if(aggressive && !wasAggressive) { // Ignore old one. toCancel = revocationGetter; if(logMINOR) Logger.minor(this, "Ignoring old request, because was low priority"); revocationGetter = null; if(toCancel != null) wasRunning = true; } wasAggressive = aggressive; if(revocationGetter != null && !(revocationGetter.isCancelled() || revocationGetter.isFinished())) { if(logMINOR) Logger.minor(this, "Not queueing another revocation fetcher yet, old one still running"); reset = false; wasRunning = false; } else { if(reset) { if(logMINOR) Logger.minor(this, "Resetting DNF count from "+revocationDNFCounter, new Exception("debug")); revocationDNFCounter = 0; } else { if(logMINOR) Logger.minor(this, "Revocation count "+revocationDNFCounter); } if(logMINOR) Logger.minor(this, "fetcher="+revocationGetter); if(revocationGetter != null && logMINOR) Logger.minor(this, "revocation fetcher: cancelled="+revocationGetter.isCancelled()+", finished="+revocationGetter.isFinished()); // Client startup may not have completed yet. manager.node.clientCore.getPersistentTempDir().mkdirs(); cg = revocationGetter = new ClientGetter(this, manager.getRevocationURI(), ctxRevocation, aggressive ? RequestStarter.MAXIMUM_PRIORITY_CLASS : RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS, null, new BinaryBlobWriter(new ArrayBucket()), null); if(logMINOR) Logger.minor(this, "Queued another revocation fetcher (count="+revocationDNFCounter+")"); } } if(toCancel != null) toCancel.cancel(core.clientContext); if(cg != null) { core.clientContext.start(cg); if(logMINOR) Logger.minor(this, "Started revocation fetcher"); } return wasRunning; } catch (FetchException e) { if(e.mode == FetchExceptionMode.RECENTLY_FAILED) { Logger.error(this, "Cannot start revocation fetcher because recently failed"); } else { Logger.error(this, "Cannot start fetch for the auto-update revocation key: "+e, e); manager.blow("Cannot start fetch for the auto-update revocation key: "+e, true); } synchronized(this) { if(revocationGetter == cg) { revocationGetter = null; } } return false; } catch (PersistenceDisabledException e) { // Impossible return false; } } long lastSucceeded() { return lastSucceeded; } long lastSucceededDelta() { if(lastSucceeded <= 0) return -1; return System.currentTimeMillis() - lastSucceeded; } /** Called when the revocation URI changes. */ public void onChangeRevocationURI() { kill(); start(wasAggressive); } @Override public void onSuccess(FetchResult result, ClientGetter state) { onSuccess(result, state, state.getBlobBucket()); } void onSuccess(FetchResult result, ClientGetter state, Bucket blob) { // The key has been blown ! // FIXME: maybe we need a bigger warning message. blown = true; moveBlob(blob); String msg = null; try { byte[] buf = result.asByteArray(); msg = new String(buf, MediaType.getCharsetRobustOrUTF(result.getMimeType())); } catch (Throwable t) { try { msg = "Failed to extract result when key blown: "+t; Logger.error(this, msg, t); System.err.println(msg); t.printStackTrace(); } catch (Throwable t1) { msg = "Internal error after retreiving revocation key"; } } manager.blow(msg, false); // Real one, even if we can't extract the message. } public boolean hasBlown() { return blown; } private void moveBlob(Bucket tmpBlob) { if(tmpBlob == null) { Logger.error(this, "No temporary binary blob file moving it: may not be able to propagate revocation, bug???"); return; } if(tmpBlob instanceof ArrayBucket) { synchronized(this) { if(tmpBlob == blobBucket) return; blobBucket = (ArrayBucket) tmpBlob; } } else { try { ArrayBucket buf = new ArrayBucket(BucketTools.toByteArray(tmpBlob)); synchronized(this) { blobBucket = buf; } } catch (IOException e) { System.err.println("Unable to copy data from revocation bucket!"); System.err.println("This should not happen and indicates there may be a problem with the auto-update checker."); // Don't blow(), as that's already happened. return; } if(tmpBlob instanceof FileBucket) { File f = ((FileBucket)tmpBlob).getFile(); synchronized(this) { if(f == blobFile) return; if(f.equals(blobFile)) return; if(FileUtil.getCanonicalFile(f).equals(FileUtil.getCanonicalFile(blobFile))) return; } } System.out.println("Unexpected blob file in revocation checker: "+tmpBlob); } FileBucket fb = new FileBucket(blobFile, false, false, false, false); try { BucketTools.copy(tmpBlob, fb); } catch (IOException e) { System.err.println("Got revocation but cannot write it to disk: "+e); System.err.println("This means the auto-update system is blown but we can't tell other nodes about it!"); e.printStackTrace(); } } @Override public void onFailure(FetchException e, ClientGetter state) { onFailure(e, state, state.getBlobBucket()); } void onFailure(FetchException e, ClientGetter state, Bucket blob) { logMINOR = Logger.shouldLog(LogLevel.MINOR, this); if(logMINOR) Logger.minor(this, "Revocation fetch failed: "+e); FetchExceptionMode errorCode = e.getMode(); boolean completed = false; long now = System.currentTimeMillis(); if(errorCode == FetchExceptionMode.CANCELLED) { return; // cancelled by us above, or killed; either way irrelevant and doesn't need to be restarted } if(e.isFatal()) { if(!e.isDefinitelyFatal()) { // INTERNAL_ERROR could be related to the key but isn't necessarily. // FIXME somebody should look at these two strings and de-uglify them! // They should never be seen but they should be idiot-proof if they ever are. // FIXME split into two parts? Fetch manually should be a second part? String message = l10n("revocationFetchFailedMaybeInternalError", new String[] { "detail", "key" }, new String[] { e.toUserFriendlyString(), manager.getRevocationURI().toASCIIString() }); System.err.println(message); e.printStackTrace(); manager.blow(message, true); return; } // Really fatal, i.e. something was inserted but can't be decoded. // FIXME somebody should look at these two strings and de-uglify them! // They should never be seen but they should be idiot-proof if they ever are. String message = l10n("revocationFetchFailedFatally", new String[] { "detail", "key" }, new String[] { e.toUserFriendlyString(), manager.getRevocationURI().toASCIIString() }); manager.blow(message, false); moveBlob(blob); return; } if(e.newURI != null) { manager.blow("Revocation URI redirecting to "+e.newURI+" - maybe you set the revocation URI to the update URI?", false); } synchronized(this) { if(errorCode == FetchExceptionMode.DATA_NOT_FOUND){ revocationDNFCounter++; if(logMINOR) Logger.minor(this, "Incremented DNF counter to "+revocationDNFCounter); } if(revocationDNFCounter >= 3) { lastSucceeded = now; completed = true; revocationDNFCounter = 0; } revocationGetter = null; } if(completed) manager.noRevocationFound(); else { if(errorCode == FetchExceptionMode.RECENTLY_FAILED) { // Try again in 1 second. // This ensures we don't constantly start them, fail them, and start them again. this.manager.node.ticker.queueTimedJob(new Runnable() { @Override public void run() { start(wasAggressive, false); } }, SECONDS.toMillis(1)); } else { start(wasAggressive, false); } } } private String l10n(String key, String[] pattern, String[] value) { return NodeL10n.getBase().getString("RevocationChecker." + key, pattern, value); } public void kill() { if(revocationGetter != null) revocationGetter.cancel(core.clientContext); } public long getBlobSize() { return blobFile.length(); } public RandomAccessBucket getBlobBucket() { if(!manager.isBlown()) return null; synchronized(this) { if(blobBucket != null) return blobBucket; } File f = getBlobFile(); if(f == null) return null; return new FileBucket(f, true, false, false, false); } public RandomAccessBuffer getBlobBuffer() { if(!manager.isBlown()) return null; synchronized(this) { if(blobBucket != null) { try { ByteArrayRandomAccessBuffer t = new ByteArrayRandomAccessBuffer(blobBucket.toByteArray()); t.setReadOnly(); return t; } catch (IOException e) { Logger.error(this, "Impossible: "+e, e); return null; } } } File f = getBlobFile(); if(f == null) return null; try { return new FileRandomAccessBuffer(f, true); } catch(FileNotFoundException e) { Logger.error(this, "We do not have the blob file for the revocation even though we have successfully downloaded it!", e); return null; } catch (IOException e) { Logger.error(this, "Error reading downloaded revocation blob file: "+e, e); return null; } } /** Get the binary blob, if we have fetched it. */ private File getBlobFile() { if(blobFile.exists()) return blobFile; return null; } @Override public boolean persistent() { return false; } @Override public boolean realTimeFlag() { return false; } @Override public void onResume(ClientContext context) { // Do nothing. Not persistent. } @Override public RequestClient getRequestClient() { return this; } }