/* 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.clients.fcp; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.util.HashMap; import java.util.Map; import freenet.client.HighLevelSimpleClientImpl; import freenet.client.InsertContext; import freenet.client.async.PersistenceDisabledException; import freenet.clients.fcp.ClientPutBase.UploadFrom; import freenet.clients.fcp.ClientRequest.Persistence; import freenet.keys.FreenetURI; import freenet.node.Node; import freenet.node.RequestStarter; import freenet.support.HexUtil; import freenet.support.Logger; import freenet.support.SimpleFieldSet; import freenet.support.api.BucketFactory; import freenet.support.api.RandomAccessBucket; import freenet.support.compress.Compressor.COMPRESSOR_TYPE; import freenet.support.compress.InvalidCompressionCodecException; import freenet.support.io.FileBucket; /** * * ClientPut * URI=CHK@ // could as easily be an insertable SSK URI * Metadata.ContentType=text/html * Identifier=Insert-1 // identifier, as always * Verbosity=0 // just report when complete * MaxRetries=999999 // lots of retries * PriorityClass=1 // FProxy priority level * * UploadFrom=direct // attached directly to this message * DataLength=100 // 100kB * or * UploadFrom=disk // upload a file from disk * Filename=/home/toad/something.html * FileHash=021349568329403123 * Data * * Neither IgnoreDS nor DSOnly make sense for inserts. */ public class ClientPutMessage extends DataCarryingMessage { public final static String NAME = "ClientPut"; final FreenetURI uri; final String contentType; final long dataLength; final String identifier; final int verbosity; final int maxRetries; final boolean getCHKOnly; final short priorityClass; final Persistence persistence; final UploadFrom uploadFromType; /** The hash of the file you want the node to deal with. * it is MANDATORY to do DDA operations and should be computed like that: * * Base64Encode(SHA256( Handler.connectionIdentifer + ClientPutMessage.identifier + content)) */ final String fileHash; final boolean dontCompress; final String clientToken; final File origFilename; final boolean global; final FreenetURI redirectTarget; /** Filename (hint for the final filename) */ final String targetFilename; final boolean earlyEncode; final boolean binaryBlob; final boolean canWriteClientCache; final String compressorDescriptor; final boolean forkOnCacheable; final int extraInsertsSingleBlock; final int extraInsertsSplitfileHeaderBlock; final InsertContext.CompatibilityMode compatibilityMode; final byte[] overrideSplitfileCryptoKey; final boolean localRequestOnly; final boolean realTimeFlag; final long metadataThreshold; final boolean ignoreUSKDatehints; public ClientPutMessage(SimpleFieldSet fs) throws MessageInvalidException { String fnam = null; identifier = fs.get("Identifier"); binaryBlob = fs.getBoolean("BinaryBlob", false); global = fs.getBoolean("Global", false); localRequestOnly = fs.getBoolean("LocalRequestOnly", false); String s = fs.get("CompatibilityMode"); InsertContext.CompatibilityMode cmode = null; if(s == null) cmode = InsertContext.CompatibilityMode.COMPAT_DEFAULT; else { try { cmode = InsertContext.CompatibilityMode.valueOf(s); } catch (IllegalArgumentException e) { try { cmode = InsertContext.CompatibilityMode.values()[Integer.parseInt(s)]; } catch (NumberFormatException e1) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid CompatibilityMode (not a name and not a number)", identifier, global); } catch (ArrayIndexOutOfBoundsException e1) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid CompatibilityMode (not a valid number)", identifier, global); } } } compatibilityMode = cmode.intern(); s = fs.get("OverrideSplitfileCryptoKey"); if(s == null) overrideSplitfileCryptoKey = null; else try { overrideSplitfileCryptoKey = HexUtil.hexToBytes(s); } catch (NumberFormatException e1) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid splitfile crypto key (not hex)", identifier, global); } catch (IndexOutOfBoundsException e1) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid splitfile crypto key (too short)", identifier, global); } if(identifier == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "No Identifier", null, global); try { if(binaryBlob) uri = new FreenetURI("CHK@"); else { String u = fs.get("URI"); if(u == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "No URI", identifier, global); FreenetURI uu = new FreenetURI(u); String[] metas = uu.getAllMetaStrings(); if(metas != null && metas.length == 1) { fnam = metas[0]; uu = uu.setMetaString(null); } // if >1, will fail later uri = uu; } } catch (MalformedURLException e) { throw new MessageInvalidException(ProtocolErrorMessage.FREENET_URI_PARSE_ERROR, e.getMessage(), identifier, global); } String verbosityString = fs.get("Verbosity"); if(verbosityString == null) verbosity = 0; else { try { verbosity = Integer.parseInt(verbosityString, 10); } catch (NumberFormatException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing Verbosity field: "+e.getMessage(), identifier, global); } } contentType = fs.get("Metadata.ContentType"); String maxRetriesString = fs.get("MaxRetries"); if(maxRetriesString == null) // default to 0 maxRetries = 0; else { try { maxRetries = Integer.parseInt(maxRetriesString, 10); } catch (NumberFormatException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing MaxSize field: "+e.getMessage(), identifier, global); } } getCHKOnly = fs.getBoolean("GetCHKOnly", false); String priorityString = fs.get("PriorityClass"); if(priorityString == null) { // defaults to the one just below FProxy priorityClass = RequestStarter.IMMEDIATE_SPLITFILE_PRIORITY_CLASS; } else { try { priorityClass = Short.parseShort(priorityString); if(!RequestStarter.isValidPriorityClass(priorityClass)) throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid priority class "+priorityClass+" - range is "+RequestStarter.PAUSED_PRIORITY_CLASS+" to "+RequestStarter.MAXIMUM_PRIORITY_CLASS, identifier, global); } catch (NumberFormatException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing PriorityClass field: "+e.getMessage(), identifier, global); } } // We do *NOT* check that FileHash is valid here for backward compatibility... and to make the override work this.fileHash = fs.get(ClientPutBase.FILE_HASH); String uploadFrom = fs.get("UploadFrom"); if((uploadFrom == null) || uploadFrom.equalsIgnoreCase("direct")) { uploadFromType = UploadFrom.DIRECT; String dataLengthString = fs.get("DataLength"); if(dataLengthString == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "Need DataLength on a ClientPut", identifier, global); try { dataLength = Long.parseLong(dataLengthString, 10); } catch (NumberFormatException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing DataLength field: "+e.getMessage(), identifier, global); } this.origFilename = null; redirectTarget = null; } else if(uploadFrom.equalsIgnoreCase("disk")) { uploadFromType = UploadFrom.DISK; String filename = fs.get("Filename"); if(filename == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "Missing field Filename", identifier, global); File f = new File(filename); if(!(f.exists() && f.isFile() && f.canRead())) throw new MessageInvalidException(ProtocolErrorMessage.FILE_NOT_FOUND, null, identifier, global); dataLength = f.length(); FileBucket fileBucket = new FileBucket(f, true, false, false, false); this.bucket = fileBucket; this.origFilename = f; redirectTarget = null; if(fnam == null) fnam = origFilename.getName(); } else if(uploadFrom.equalsIgnoreCase("redirect")) { uploadFromType = UploadFrom.REDIRECT; String target = fs.get("TargetURI"); if(target == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "TargetURI missing but UploadFrom=redirect", identifier, global); try { redirectTarget = new FreenetURI(target); } catch (MalformedURLException e) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Invalid TargetURI: "+e, identifier, global); } dataLength = 0; origFilename = null; bucket = null; } else throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "UploadFrom invalid or unrecognized: "+uploadFrom, identifier, global); dontCompress = fs.getBoolean("DontCompress", false); String persistenceString = fs.get("Persistence"); persistence = Persistence.parseOrThrow(persistenceString, identifier, global); canWriteClientCache = fs.getBoolean("WriteToClientCache", false); clientToken = fs.get("ClientToken"); String f = fs.get("TargetFilename"); if(f != null) fnam = f; if(fnam != null && fnam.indexOf('/') > -1) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "TargetFilename must not contain slashes", identifier, global); } if(fnam != null && fnam.length() == 0) { fnam = null; // Deliberate override to tell us not to create one. } if(uri.getRoutingKey() == null && !uri.isKSK()) targetFilename = fnam; else targetFilename = null; earlyEncode = fs.getBoolean("EarlyEncode", false); String codecs = fs.get("Codecs"); if (codecs != null) { COMPRESSOR_TYPE[] ca; try { ca = COMPRESSOR_TYPE.getCompressorsArrayNoDefault(codecs); } catch (InvalidCompressionCodecException e) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, e.getMessage(), identifier, global); } if (ca == null) codecs = null; } compressorDescriptor = codecs; if(fs.get("ForkOnCacheable") != null) forkOnCacheable = fs.getBoolean("ForkOnCacheable", false); else forkOnCacheable = Node.FORK_ON_CACHEABLE_DEFAULT; extraInsertsSingleBlock = fs.getInt("ExtraInsertsSingleBlock", HighLevelSimpleClientImpl.EXTRA_INSERTS_SINGLE_BLOCK); extraInsertsSplitfileHeaderBlock = fs.getInt("ExtraInsertsSplitfileHeaderBlock", HighLevelSimpleClientImpl.EXTRA_INSERTS_SPLITFILE_HEADER); realTimeFlag = fs.getBoolean("RealTimeFlag", false); metadataThreshold = fs.getLong("MetadataThreshold", -1); ignoreUSKDatehints = fs.getBoolean("IgnoreUSKDatehints", false); } @Override public SimpleFieldSet getFieldSet() { SimpleFieldSet sfs = new SimpleFieldSet(true); sfs.putSingle("URI", uri.toString()); sfs.putSingle("Identifier", identifier); sfs.put("Verbosity", verbosity); sfs.put("MaxRetries", maxRetries); sfs.putSingle("Metadata.ContentType", contentType); sfs.putSingle("ClientToken", clientToken); switch(uploadFromType) { case DIRECT: sfs.putSingle("UploadFrom", "direct"); sfs.put("DataLength", dataLength); break; case DISK: sfs.putSingle("UploadFrom", "disk"); sfs.putSingle("Filename", origFilename.getAbsolutePath()); sfs.put("DataLength", dataLength); break; case REDIRECT: sfs.putSingle("UploadFrom", "redirect"); sfs.putSingle("TargetURI", redirectTarget.toString()); break; } sfs.put("GetCHKOnly", getCHKOnly); sfs.put("PriorityClass", priorityClass); sfs.putSingle("Persistence", persistence.toString().toLowerCase()); sfs.put("DontCompress", dontCompress); if (compressorDescriptor != null) sfs.putSingle("Codecs", compressorDescriptor); sfs.put("Global", global); sfs.put("BinaryBlob", binaryBlob); return sfs; } @Override public String getName() { return NAME; } @Override public void run(FCPConnectionHandler handler, Node node) throws MessageInvalidException { handler.startClientPut(this); } /** * Get the length of the trailing field. */ @Override long dataLength() { if(uploadFromType == UploadFrom.DIRECT) return dataLength; else return -1; } @Override String getIdentifier() { return identifier; } @Override RandomAccessBucket createBucket(BucketFactory bf, long length, FCPServer server) throws IOException, PersistenceDisabledException { if(persistence == Persistence.FOREVER) { if(server.core.killedDatabase()) throw new PersistenceDisabledException(); return server.core.persistentTempBucketFactory.makeBucket(length); } else { return super.createBucket(bf, length, server); } } @Override boolean isGlobal() { return global; } public void freeData() { if(bucket == null) { if(dataLength() <= 0) return; // Okay. Logger.error(this, "bucket is null on "+this+" - freed twice?", new Exception("error")); return; } bucket.free(); } }