/* This code is part of Freenet. It is distributed under the GNU General * Public License, version 2 (or at your option any later version). See * http://www.gnu.org/ for further details of the GPL. */ package freenet.client.async; import java.io.IOException; import freenet.client.ClientMetadata; import freenet.client.InsertBlock; import freenet.client.InsertContext; import freenet.client.InsertContext.CompatibilityMode; import freenet.client.InsertException; import freenet.client.InsertException.InsertExceptionMode; import freenet.client.Metadata; import freenet.client.events.SendingToNetworkEvent; import freenet.client.events.SplitfileProgressEvent; import freenet.crypt.ChecksumChecker; import freenet.keys.BaseClientKey; import freenet.keys.FreenetURI; import freenet.keys.Key; import freenet.support.LogThresholdCallback; import freenet.support.Logger; import freenet.support.Logger.LogLevel; import freenet.support.api.Bucket; import freenet.support.api.RandomAccessBucket; import freenet.support.io.ResumeFailedException; /** A high level insert. */ public class ClientPutter extends BaseClientPutter implements PutCompletionCallback { private static final long serialVersionUID = 1L; /** Callback for when the insert completes. */ final ClientPutCallback client; /** The data to insert. */ final RandomAccessBucket data; /** The URI to insert it to. Can be CHK@. */ final FreenetURI targetURI; /** The ClientMetadata i.e. the MIME type and any other client-visible metadata. */ final ClientMetadata cm; /** Config settings for this insert - what kind of splitfile to use if needed etc. */ final InsertContext ctx; /** Target filename. If specified, we create manifest metadata so that the file can be accessed at * [ final key ] / [ target filename ]. */ final String targetFilename; /** The current state of the insert. */ private ClientPutState currentState; /** Whether the insert has finished. */ private boolean finished; /** Are we inserting metadata? */ private final boolean isMetadata; private boolean startedStarting; /** Are we inserting a binary blob? */ private final boolean binaryBlob; /** The final URI for the data. */ private FreenetURI uri; private final byte[] overrideSplitfileCrypto; /** Random or overriden splitfile cryptokey. Valid after start(). */ private byte[] cryptoKey; /** When positive, means we will return metadata rather than a URI, once the * metadata is under this length. If it is too short it is still possible to * return a URI, but we won't return both. */ private final long metadataThreshold; private boolean gotFinalMetadata; private static volatile boolean logMINOR; static { Logger.registerLogThresholdCallback(new LogThresholdCallback(){ @Override public void shouldUpdate(){ logMINOR = Logger.shouldLog(LogLevel.MINOR, this); } }); } /** * zero arg c'tor for db4o on jamvm */ @SuppressWarnings("unused") private ClientPutter() { targetURI = null; targetFilename = null; overrideSplitfileCrypto = null; isMetadata = false; data = null; ctx = null; cm = null; client = null; binaryBlob = false; metadataThreshold = 0; } /** * @param client The object to call back when we complete, or don't. * @param data The data to insert. This will be freed when the insert has completed, whether * it succeeds or not, so wrap it in a @link freenet.support.io.NoFreeBucket if you don't want * it to be freed. * @param targetURI * @param cm * @param ctx * @param priorityClass * @param isMetadata * @param targetFilename If set, create a one-file manifest containing this filename pointing to this file. * @param binaryBlob * @param context The client object for purposs of round-robin client balancing. * @param overrideSplitfileCrypto * @param metadataThreshold */ public ClientPutter(ClientPutCallback client, RandomAccessBucket data, FreenetURI targetURI, ClientMetadata cm, InsertContext ctx, short priorityClass, boolean isMetadata, String targetFilename, boolean binaryBlob, ClientContext context, byte[] overrideSplitfileCrypto, long metadataThreshold) { super(priorityClass, client); this.cm = cm; this.isMetadata = isMetadata; this.client = client; this.data = data; this.targetURI = targetURI; this.ctx = ctx; this.finished = false; this.cancelled = false; this.targetFilename = targetFilename; this.binaryBlob = binaryBlob; this.overrideSplitfileCrypto = overrideSplitfileCrypto; this.metadataThreshold = metadataThreshold; } /** Start the insert. * @param earlyEncode If true, try to find the final URI as quickly as possible, and insert the upper * layers as soon as we can, rather than waiting for the lower layers. The default behaviour is safer, * because an attacker can usually only identify the datastream once he has the top block, or once you * have announced the key. * @param context Contains some useful transient fields such as the schedulers. * @throws InsertException If the insert cannot be started for some reason. */ public void start(ClientContext context) throws InsertException { start(false, context); } /** Start the insert. * @param restart If true, restart the insert even though it has completed before. * @param context Contains some useful transient fields such as the schedulers. * @throws InsertException If the insert cannot be started for some reason. */ public boolean start(boolean restart, ClientContext context) throws InsertException { if(logMINOR) Logger.minor(this, "Starting "+this+" for "+targetURI); byte cryptoAlgorithm; CompatibilityMode mode = ctx.getCompatibilityMode(); if(!(mode == CompatibilityMode.COMPAT_CURRENT || mode.ordinal() >= CompatibilityMode.COMPAT_1416.ordinal())) cryptoAlgorithm = Key.ALGO_AES_PCFB_256_SHA256; else cryptoAlgorithm = Key.ALGO_AES_CTR_256_SHA256; try { this.targetURI.checkInsertURI(); // If the top level key is an SSK, all CHK blocks and particularly splitfiles below it should have // randomised keys. This substantially improves security by making it impossible to identify blocks // even if you know the content. In the user interface, we will offer the option of inserting as a // random SSK to take advantage of this. boolean randomiseSplitfileKeys = randomiseSplitfileKeys(targetURI, ctx, persistent()); if(data == null) throw new InsertException(InsertExceptionMode.BUCKET_ERROR, "No data to insert", null); boolean cancel = false; synchronized(this) { if(restart) { clearCountersOnRestart(); if(currentState != null && !finished) { if(logMINOR) Logger.minor(this, "Can't restart, not finished and currentState != null : "+currentState); return false; } if(finished) startedStarting = false; finished = false; } if(startedStarting) { if(logMINOR) Logger.minor(this, "Can't "+(restart?"restart":"start")+" : startedStarting = true"); return false; } startedStarting = true; if(currentState != null) { if(logMINOR) Logger.minor(this, "Can't "+(restart?"restart":"start")+" : currentState != null : "+currentState); return false; } cancel = this.cancelled; cryptoKey = null; if(overrideSplitfileCrypto != null) { cryptoKey = overrideSplitfileCrypto; if (cryptoKey.length != 32) throw new InsertException(InsertExceptionMode.INVALID_URI, "overrideSplitfileCryptoKey must be of length 32", null); } else if(randomiseSplitfileKeys) { cryptoKey = new byte[32]; context.random.nextBytes(cryptoKey); } if(!cancel) { if(!binaryBlob) { ClientMetadata meta = cm; if(meta != null) meta = persistent() ? meta.clone() : meta; currentState = new SingleFileInserter(this, this, new InsertBlock(data, meta, targetURI), isMetadata, ctx, realTimeFlag, false, false, null, null, false, targetFilename, false, persistent(), 0, 0, null, cryptoAlgorithm, cryptoKey, metadataThreshold); } else currentState = new BinaryBlobInserter(data, this, getClient(), false, priorityClass, ctx, context); } } if(cancel) { onFailure(new InsertException(InsertExceptionMode.CANCELLED), null, context); return false; } synchronized(this) { cancel = cancelled; } if(cancel) { onFailure(new InsertException(InsertExceptionMode.CANCELLED), null, context); return false; } if(logMINOR) Logger.minor(this, "Starting insert: "+currentState); if(currentState instanceof SingleFileInserter) ((SingleFileInserter)currentState).start(context); else currentState.schedule(context); synchronized(this) { cancel = cancelled; } if(cancel) { onFailure(new InsertException(InsertExceptionMode.CANCELLED), null, context); return false; } } catch (InsertException e) { Logger.error(this, "Failed to start insert: "+e, e); synchronized(this) { finished = true; currentState = null; } // notify the client that the insert could not even be started if (this.client!=null) { this.client.onFailure(e, this); } } catch (IOException e) { Logger.error(this, "Failed to start insert: "+e, e); synchronized(this) { finished = true; currentState = null; } // notify the client that the insert could not even be started if (this.client!=null) { this.client.onFailure(new InsertException(InsertExceptionMode.BUCKET_ERROR, e, null), this); } } catch (BinaryBlobFormatException e) { Logger.error(this, "Failed to start insert: "+e, e); synchronized(this) { finished = true; currentState = null; } // notify the client that the insert could not even be started if (this.client!=null) { this.client.onFailure(new InsertException(InsertExceptionMode.BINARY_BLOB_FORMAT_ERROR, e, null), this); } } if(logMINOR) Logger.minor(this, "Started "+this); return true; } public static boolean randomiseSplitfileKeys(FreenetURI targetURI, InsertContext ctx, boolean persistent) { // If the top level key is an SSK, all CHK blocks and particularly splitfiles below it should have // randomised keys. This substantially improves security by making it impossible to identify blocks // even if you know the content. In the user interface, we will offer the option of inserting as a // random SSK to take advantage of this. boolean randomiseSplitfileKeys = targetURI.isSSK() || targetURI.isKSK() || targetURI.isUSK(); if(randomiseSplitfileKeys) { CompatibilityMode cmode = ctx.getCompatibilityMode(); if(!(cmode == CompatibilityMode.COMPAT_CURRENT || cmode.ordinal() >= CompatibilityMode.COMPAT_1255.ordinal())) randomiseSplitfileKeys = false; } return randomiseSplitfileKeys; } /** Called when the insert succeeds. */ @Override public void onSuccess(ClientPutState state, ClientContext context) { synchronized(this) { finished = true; currentState = null; } if(super.failedBlocks > 0 || super.fatallyFailedBlocks > 0 || super.successfulBlocks < super.totalBlocks) { // USK auxiliary inserts are allowed to fail. // If only generating the key, splitfile may not have reported the blocks as inserted. if (!uri.isUSK() && !ctx.getCHKOnly) Logger.error(this, "Failed blocks: "+failedBlocks+", Fatally failed blocks: "+fatallyFailedBlocks+ ", Successful blocks: "+successfulBlocks+", Total blocks: "+totalBlocks+" but success?! on "+this+" from "+state, new Exception("debug")); } client.onSuccess(this); } /** Called when the insert fails. */ @Override public void onFailure(InsertException e, ClientPutState state, ClientContext context) { if(logMINOR) Logger.minor(this, "onFailure() for "+this+" : "+state+" : "+e, e); synchronized(this) { finished = true; currentState = null; } client.onFailure(e, this); } /** Called when we know the final URI of the insert. */ @Override public void onEncode(BaseClientKey key, ClientPutState state, ClientContext context) { FreenetURI u; synchronized(this) { u = key.getURI(); if(gotFinalMetadata) { Logger.error(this, "Generated URI *and* sent final metadata??? on "+this+" from "+state); } if(targetFilename != null) u = u.pushMetaString(targetFilename); if(this.uri != null) { if(!this.uri.equals(u)) { Logger.error(this, "onEncode() called twice with different URIs: "+this.uri+" -> "+u+" for "+this, new Exception("error")); } return; } this.uri = u; } client.onGeneratedURI(u, this); } /** Called when metadataThreshold was specified and metadata is being returned * instead of a URI. */ public void onMetadata(Bucket finalMetadata, ClientPutState state, ClientContext context) { boolean freeIt = false; synchronized(this) { if(uri != null) { Logger.error(this, "Generated URI *and* sent final metadata??? on "+this+" from "+state); } if(gotFinalMetadata) { Logger.error(this, "onMetadata called twice - already sent metadata to client for "+this); freeIt = true; } else { gotFinalMetadata = true; } } if(freeIt) { finalMetadata.free(); return; } client.onGeneratedMetadata(finalMetadata, this); } /** Cancel the insert. Will call onFailure() if it is not already cancelled, so the callback will * normally be called. */ @Override public void cancel(ClientContext context) { if(logMINOR) Logger.minor(this, "Cancelling "+this, new Exception("debug")); ClientPutState oldState = null; synchronized(this) { if(cancelled) return; if(finished) return; super.cancel(); oldState = currentState; } if(oldState != null) oldState.cancel(context); onFailure(new InsertException(InsertExceptionMode.CANCELLED), null, context); } /** Has the insert completed already? */ @Override public synchronized boolean isFinished() { return finished || cancelled; } /** * @return The data {@link Bucket} which is used by this ClientPutter. */ public Bucket getData() { return data; } /** * Get the target URI with which this insert was started. */ public FreenetURI getTargetURI() { return targetURI; } /** Get the final URI to the inserted data */ @Override public FreenetURI getURI() { return uri; } /** * Get used splitfile cryptokey. Valid only after start(). */ public byte[] getSplitfileCryptoKey() { return cryptoKey; } /** Called when a ClientPutState transitions to a new state. If this is the current state, then we update * it, but it might also be a subsidiary state, in which case we ignore it. */ @Override public void onTransition(ClientPutState oldState, ClientPutState newState, ClientContext context) { if(newState == null) throw new NullPointerException(); synchronized (this) { if (currentState == oldState) { currentState = newState; return; } } if(persistent()) context.jobRunner.setCheckpointASAP(); Logger.normal(this, "onTransition: cur=" + currentState + ", old=" + oldState + ", new=" + newState); } /** Called when we have generated metadata for the insert. This should not happen, because we should * insert the metadata! */ @Override public void onMetadata(Metadata m, ClientPutState state, ClientContext context) { Logger.error(this, "Got metadata on "+this+" from "+state+" (this means the metadata won't be inserted)"); } /** The number of blocks that will be needed to fetch the data. We put this in the top block metadata. */ protected int minSuccessFetchBlocks; @Override public int getMinSuccessFetchBlocks() { return minSuccessFetchBlocks; } @Override public void addBlock() { synchronized(this) { minSuccessFetchBlocks++; } super.addBlock(); } @Override public void addBlocks(int num) { synchronized(this) { minSuccessFetchBlocks+=num; } super.addBlocks(num); } /** Add one or more blocks to the number of requires blocks, and don't notify the clients. */ @Override public void addMustSucceedBlocks(int blocks) { synchronized(this) { minSuccessFetchBlocks += blocks; } super.addMustSucceedBlocks(blocks); } /** Add one or more blocks to the number of requires blocks, and don't notify the clients. * These blocks are added to the minSuccessFetchBlocks for the insert, but not to the counter for what * the requestor must fetch. */ @Override public void addRedundantBlocksInsert(int blocks) { super.addMustSucceedBlocks(blocks); } @Override protected void clearCountersOnRestart() { minSuccessFetchBlocks = 0; super.clearCountersOnRestart(); } @Override protected void innerNotifyClients(ClientContext context) { SplitfileProgressEvent e; synchronized(this) { e = new SplitfileProgressEvent( this.totalBlocks, this.successfulBlocks, this.latestSuccess, this.failedBlocks, this.fatallyFailedBlocks, this.latestFailure, this.minSuccessBlocks, this.minSuccessFetchBlocks, this.blockSetFinalized); } ctx.eventProducer.produceEvent(e, context); } /** Notify listening clients that an insert has been sent to the network. */ @Override protected void innerToNetwork(ClientContext context) { ctx.eventProducer.produceEvent(new SendingToNetworkEvent(), context); } /** Called when we know exactly how many blocks will be needed. */ @Override public void onBlockSetFinished(ClientPutState state, ClientContext context) { if(logMINOR) Logger.minor(this, "Set finished", new Exception("debug")); blockSetFinalized(context); } /** Called (sometimes) when enough of the data has been inserted that the file can now be fetched. Not * very useful unless earlyEncode was enabled. */ @Override public void onFetchable(ClientPutState state) { client.onFetchable(this); } /** Can we restart the insert? */ public boolean canRestart() { if(currentState != null && !finished) { Logger.minor(this, "Cannot restart because not finished for "+uri); return false; } if(data == null) return false; return true; } /** Restart the insert. * @return True if the insert restarted successfully. * @throws InsertException If the insert could not be restarted for some reason. * */ public boolean restart(ClientContext context) throws InsertException { return start(true, context); } @Override public void onTransition(ClientGetState oldState, ClientGetState newState, ClientContext context) { // Ignore, at the moment // This exists here because e.g. USKInserter does requests as well as inserts. // FIXME I'm not sure that's a good enough reason though! Get rid ... } @Override public void dump() { System.out.println("URI: "+uri); System.out.println("Client: "+client); System.out.println("Finished: "+finished); System.out.println("Data: "+data); } public byte[] getClientDetail(ChecksumChecker checker) throws IOException { if(client instanceof PersistentClientCallback) { return getClientDetail((PersistentClientCallback)client, checker); } else return new byte[0]; } @Override public void innerOnResume(ClientContext context) throws ResumeFailedException { super.innerOnResume(context); if(currentState != null) { try { currentState.onResume(context); } catch (InsertException e) { this.onFailure(e, null, context); return; } } if(data != null) data.onResume(context); notifyClients(context); } @Override protected ClientBaseCallback getCallback() { return client; } @Override public void onShutdown(ClientContext context) { ClientPutState state; synchronized(this) { state = currentState; } if(state != null) state.onShutdown(context); } }